diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bbc9ddb10..f131747fa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -364,6 +364,20 @@ jobs: target: wasm32-wasi helper: "false" - run: pnpm build + # The sanitized seed compiles the compiler's 700 MB module with one clang + # process under AddressSanitizer. It peaks near 14 GB while the Node + # process that emitted the module still holds about 6 GB. Together they + # exceed the runner's 16 GB of memory, so back them with swap. + - name: Enlarge swap for the sanitized seed + if: matrix.flavor == 'san' + run: | + sudo swapoff -a + sudo rm -f /mnt/swapfile + sudo fallocate -l 24G /mnt/swapfile + sudo chmod 600 /mnt/swapfile + sudo mkswap /mnt/swapfile + sudo swapon /mnt/swapfile + free -h - name: Prepare bootstrap seed run: node scripts/bootstrap-seed.mjs env: @@ -759,17 +773,22 @@ jobs: run: >- pnpm exec vitest run tests/harness/worktree-files.test.ts --testNamePattern "workspace reset keeps Linux Sandbox paths POSIX" + # The Node host checks with tsc-ts, the default, over the named pipe it + # serves on Windows, and with tsgo. - name: Windows scriptc run smoke shell: pwsh run: | zig version - $output = & node packages/cli/dist/main.js run tests/corpus/001-hello.ts --backend llvm - if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE - } - $actual = ($output -join "`n").Trim() - if ($actual -ne "hello world") { - throw "unexpected scriptc output: $actual" + foreach ($checker in @("tsc-ts", "tsgo")) { + $env:SCRIPTC_CHECKER = $checker + $output = & node packages/cli/dist/main.js run tests/corpus/001-hello.ts --backend llvm + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } + $actual = ($output -join "`n").Trim() + if ($actual -ne "hello world") { + throw "unexpected scriptc output with ${checker}: $actual" + } } - name: Windows PE subsystem selection env: diff --git a/CHANGELOG.md b/CHANGELOG.md index 31d34e8db..ec0b27dbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,161 @@ All notable changes to scriptc will be documented in this file. +## 0.4.2 + +### Highlights + +- **Faster builds of large programs:** + - scriptc checks with tsc-ts 0.0.7, which answers contextual types and resolved signatures in batches, so lowering asks for them in one request per chunk instead of one per node. A native build of tsc-ts is 7% faster (110.5 → 103.5 s on 8 vCPUs, lowering 12% faster), with an identical executable. + - The native compiler is built with profile-guided optimization, so tsc-ts builds 6.9% faster cold and 10.7% faster after an edit. + - The native compiler's optimization profile is refreshed, so a native build of tsc-ts is a further 10% faster (124.7 → 111.7 s on 16 vCPUs), with lowering 17% faster. + - The class dispatch passes index their receivers by name instead of testing each one, and they reuse their per-function scans while a function's body is unchanged. + - Checker replies resolve objects the compiler already holds from their ids without decoding them. +- **Leaner builds:** + - The frontend of an optimized executable's first build runs in a child process, so the native code generator starts after its memory is released. The cold build of tsc-ts peaks at 2.21 GB instead of 3.30 GB. + - The compiler collects its heap as lowering and LLVM emission grow it. +- **WebCrypto:** + - The `crypto` global, `node:crypto`'s `webcrypto` and `crypto.subtle` are one object. + - `generateKey`, `importKey`, `exportKey`, `encrypt`, `decrypt`, `deriveBits`, `deriveKey`, `wrapKey` and `unwrapKey` cover HMAC, AES-GCM, AES-CBC, AES-CTR, AES-KW, PBKDF2, HKDF and JSON Web Keys. + - `crypto.subtle` signs and verifies with ECDSA, RSASSA-PKCS1-v1_5, RSA-PSS and Ed25519. + - All of it follows Node 24's argument conversion, algorithm normalization and `DOMException`s. jose's symmetric JWE paths run as in Node. +- **JSX:** `.tsx`, `.jsx` and JavaScript files with JSX compile under the project's `jsx` options. + - `react`, `react-jsx` and `react-jsxdev` lower to the factory calls TypeScript emits, with the same keys, fallbacks and source positions. + - `preserve` and `react-native` are refused with a named diagnostic. +- **Faster worker messages:** + - A message is encoded into one block sized to its contents. + - A typed record is posted from its fields. + - A `'message'` listener with a typed parameter builds it from the message, without a checked-dynamic copy. + - Messages between `MessageChannel` ports take 90 ms where they took 250 ms in 0.4.1 (Node: 337 ms), with a peak RSS of 45 MB instead of 115 MB. +- **Loops over typed arrays vectorize:** a loop to a typed array's length keeps one bounds test for every typed array it indexes. + - Index temporaries such as `const j = i * 4` are computed as integers. + - Uint8ClampedArray stores convert inline. + - Row-major index terms such as `r * cols + j` are tested once before the loop. + - Typed-array callback methods call a literal callback directly on an integer counter. + - An 896 × 896 matrix-vector product with four accumulators takes 55 ms instead of 396 ms (Node: 275 ms). `Uint8Array.map` is 4.5 times as fast and `Int32Array.some` 7.8 times as fast as in 0.4.1. + +### Features + +- **Asymmetric keys and signatures in `node:crypto`:** + - `generateKeyPair`, `generateKeyPairSync`, `createPrivateKey` and `createPublicKey` handle RSA, EC and Ed25519 keys in PEM, DER and JWK. + - `sign`, `verify`, `createSign` and `createVerify` sign and verify with those keys, with Node's output and errors. + - Ed25519 verification matches Node on 2,083 test vectors, including small-order and mixed-order keys. +- **`crypto.createDiffieHellman`, `crypto.getDiffieHellman` and `crypto.createDiffieHellmanGroup`:** + - The modp1, modp2 and modp5 to modp18 groups, explicit primes and generators, and generated safe primes are supported, with Node 24's key widths, `verifyError` bits, key range checks and errors. + - Secrets of 0, 1 and p − 1 are rejected, and so are peer keys outside the RFC 5114 subgroups. + - Primes longer than 8192 bits are refused. +- **TLS:** + - `tls.connect` and `tls.createServer` negotiate `ALPNProtocols`, and both sockets report `alpnProtocol`. `minVersion` and `maxVersion` bound the handshake, and `getProtocol()` reports the version it picked. + - `tls.createServer` takes an `SNICallback` that answers a `SecureContext` synchronously or later. + - `new tls.TLSSocket(socket, { isServer: true, ... })` and `tls.connect({ socket })` run the handshake on an open socket, such as an HTTP `CONNECT` tunnel. + - A client whose peer closes before the handshake completes fails with Node's `ECONNRESET`. +- **Streams:** + - Readable, Writable, Duplex, Transform and PassThrough streams take `objectMode`, `readableObjectMode` and `writableObjectMode`, and an object-mode side carries any value through `push`, `write`, `pipe`, `pipeline`, `for await` and `'data'` listeners. + - `_write` and `_transform` receive the encoding argument as Node passes it. + - `Cipheriv` and `Decipheriv` are Transforms: they can be piped, used as `pipeline()` stages, read and iterated. + - `pipeline()` accepts function stages anywhere among its streams, and an array, a string or a synchronous generator as its source. +- **Packages compiled from their JavaScript:** + - js-yaml and smol-toml compile statically and match Node: ES5 error constructors (`Object.create(Error.prototype)`, `Error.call(this)`, `Error.captureStackTrace`) and classes extending `Date` behave as in Node. + - TypeScript's UMD module wrapper compiles as the module body, and tsc's `__importDefault` and `__importStar` helpers around builtin modules compile as plain `require()` calls. + - Interfaces, type aliases and enums that only a package's `.d.ts` declares keep their types. +- **JavaScript sources:** + - Error constructors can be read as values, so `err.constructor === TypeError` works. + - `workerData` can be destructured. + - Unary `+` converts unions of strings, numbers, booleans, `null` and `undefined` as `Number()` does, so `+process.argv[2]` compiles. + - These compile and behave as in Node: + - chained readline listener registration; + - `Number.parseInt` without a radix; + - `match` and `search` with a possibly undefined regular expression; + - `Buffer.from` over an untyped `ArrayBuffer`, with its byte offset; + - Atomics on untyped arrays. + - Buffer methods run on Buffers held as untyped values. +- **`fs`:** + - The synchronous functions (`readFileSync`, `writeFileSync`, `statSync`, `readdirSync`, `mkdirSync`, `rmSync`, `openSync` and the others) accept `file:` URLs wherever they accept a path, with Node's conversion errors. + - `fs.promises.rm` takes `recursive`, `force`, `maxRetries` and `retryDelay`, and removing a directory without `recursive` throws Node's `ERR_FS_EISDIR`. +- **`TypedArray.of`** creates a typed array of its arguments, and a dynamic iterable spread into `Array.of`, `push` or `TypedArray.of` is drained through the iterator protocol. +- **Global `isFinite` converts its argument with ToNumber,** as `isNaN` does. The Math functions and `String.fromCharCode` evaluate every argument before converting any. +- **`Object.freeze`, `Object.seal` and `Object.preventExtensions` of a function** act on its own properties, and `Object.isFrozen`, `Object.isSealed` and `Object.isExtensible` answer for functions. @noble/hashes' frozen hashers work. +- **EventEmitters used through both typed and untyped references** share one listener list. An untyped `emit` reaches typed listeners, an untyped `emit('error')` follows Node's contract, and `listenerCount`, `eventNames`, `listeners`, `rawListeners`, `removeAllListeners`, `setMaxListeners` and `getMaxListeners` work on untyped values. +- **Template literals, `join`, `Array.prototype.toString` and the default `sort()`** call an object's own or inherited `toString`, as `String()` does, instead of printing `[object Object]`. +- **Classes and generics:** + - A generic factory declared to return a constructor type can return one of its local classes, including an anonymous class expression, and `class X extends factory(...)` extends it. + - Static methods of a function-local class compile. + - Constructor types declared in the program (`new () => unknown`, types that also name static members, construct overloads) hold class values. + - Members typed by a conditional type over a literal class type argument (`Field`) resolve. + - Object literals under an erased generic member may hold callbacks with optional or defaulted parameters. +- **Assignment and update targets wrapped in parentheses or non-null assertions** (`(x) = 1`, `h.sums[k]! += 10`) compile. +- **Untyped values keep what JavaScript code adds:** + - Properties that untyped code adds to a typed object literal stay on it. + - Named properties written to an array through an `unknown` view stay on the array, and `for...in` visits them after the indices. + - An `unknown` value asserted to a structural type with methods keeps its class's methods. + - The type inferred from an empty object literal holds any non-nullish value, as TypeScript's `{}` does. +- **A WebAssembly module the build finds but does not translate** rejects with a `CompileError` that names the reason, for example a proposal outside the translated feature set. +- **SC6101 names the functions that likely run often and use untyped values:** + - It shows up to five, most likely hot first, with why their values are untyped: an `any`, an untyped `JSON.parse` result or an untyped JavaScript file. + - `scriptc coverage --fail-on=dynamic` fails on it, and `--no-dynamic-tier-warning` leaves it out. + +### Performance + +- **Regular expressions skip start positions that their leading character sets rule out,** testing 16 positions at once. Searching 50 MB of text for nine case-insensitive alternations takes 0.83 s instead of 3.88 s (Node: 4.38 s). +- **Loop counters stay integers in more loops:** + - Counters that step beside a counted loop's counter (`for (let i = 0, p = 0; i < n; i++, p += 4)`) stay integers: a pixel-stride loop takes 114 ms instead of 340 ms (Node: 190 ms). + - So do two counters that close in on each other (`lo++, hi--`), 17% faster on a permutation-reversal loop. + - So do counters of loops bounded by a string's length or by an invariant limit other than an array's length, and while-loop counters that nested scans advance. A non-ASCII lexer is 9.5% faster. +- **Loops that call a known callback** test its closure once before the loop and call the function directly, so captured `let` bindings stay in registers. `forEach` sums with a captured accumulator are 25 to 55% faster. +- **A speculative integer loop whose check fails** finishes the iteration outside the ordinary loop, which stays optimizable, and a loop site that keeps failing stops trying. With the hole-free-prefix test that element reads now take first, a pairwise loop over an array of class instances with number fields is about 1.25 times as fast as in 0.4.1. +- **Typed arrays over a SharedArrayBuffer** read and write elements inline as relaxed atomic accesses. A numeric kernel over shared `Float64Array`s takes 0.78 s instead of 1.56 s (Node: 0.84 s). +- **`Math.pow(2, n)` for integer exponents** builds the result from its bits. A counter that masks with `Math.pow(2, length)` takes 11.0 s instead of 20.4 s. +- **Async calls that never suspend run in place,** and so does `await Promise.all` of such calls, with Node's microtask turns and rejection handling. An async pipeline takes 5.5 ms instead of 9.7 ms. +- **Strings:** + - `Number()` of a short integer string returns the integer directly. + - `split` builds its result in one inline block, and `indexOf` and `slice` on ASCII strings skip the UTF-16 index map. A log summary is 20% faster. + - `charAt` and `s[i]` on ASCII strings read the character from a table. + - Chains of one-character `replace` calls (the HTML-escape idiom) replace a block of 16 bytes at a time, 12.8% faster on a template renderer. +- **`JSON.parse` and `JSON.stringify`** scan whole words and append parsed typed arrays in bulk, 22% faster to parse and 10% faster to stringify records. +- **Smaller arrays, maps and program-lifetime instances:** + - The array header is 32 bytes instead of 48. + - Arrays of up to 16 known elements keep their slots in their own block. + - Arrays of a known final length get exactly that capacity. + - Small map entry arrays grow in finer steps. + - On Linux, `ProgramLifetime` instances take a 32-bit header and are referenced in 4 bytes. + - They lower the peak memory of a large TypeScript type checker: the 4-byte references alone take a large project's check from 4.13 GB to 3.75 GB, and the 32-bit header saves a further 5%. +- **Worker messages queued together are delivered in one batch** before immediates run. 20,000 messages interleaved with a worker's output drain in 0.5 s instead of 5.6 s (Node: 0.57 s). +- **The cycle collector** never buffers instances of classes whose fields hold nothing cycle-capable, and its per-object paths are shorter. A workload of cyclic graphs is about 5% faster. +- **Array helpers specialized for a callback that cannot throw** skip the pending-exception test, 4.5% faster on a functional pipeline. +- **Compile time:** + - A file's function declarations are walked once for their references, so lowering a 10 MB single-module bundle takes 69 s instead of 219 s. + - LLVM emission computes its module facts once, 6.9% faster on tsc-ts. + - AST walks visit subtrees by their node ranges and read node words through a `Uint32Array`. + - Program builds no longer import runtime bitcode, so builds through Node and through the native compiler produce identical executables. + +### Fixes + +- **Member reads and method calls on an array hole** held in a binding narrowed to one arm of a wide union throw Node's `Cannot read properties of undefined` TypeError. +- **`AsyncLocalStorage`:** + - A store entered before an async function's first await, or in a body that never suspends, is the caller's store after the call. + - Each synchronous generator step runs in its resumer's context, as in Node. +- **Generic arrow and function-expression bindings used before their declarations** behave as in Node, including the `ReferenceError` of a `let` or `const`. +- **Assignment expressions into a module `let` declared without an initializer** (`new Promise((r) => (resolve = r))`) store the value instead of throwing a `ReferenceError`. +- **The `extends` expression of a class defined inside a function** is evaluated each time the definition runs. +- **A `super()` call that leads an `if` condition's comma chain** compiles. +- **A catch binding tests `instanceof`** correctly for values thrown as `unknown`, rejected promises and runtime errors. +- **zlib callbacks receive `undefined` as their result when the operation fails,** as in Node, and a callback may assign a fallback to it. +- **`Readable.push` and `Writable.write` with `string | null` or `Buffer | null` chunks** no longer crash. +- **Object literals keep their key order** through compiler rewrites and on shapes a typed `JSON.parse` also builds, so `JSON.stringify`, `Object.keys` and `console.log` match Node. +- **`console.log` prints class instances with their class name** in programs that test `instanceof` on unknown values. +- **`String`, `Number` and `Boolean` passed to array methods** convert as their direct calls do, and nested array literals in a recursive JSON-style union build. +- **A dynamic method call that the program also reads through a prototype** finds the method when the instance's property bag does not hold it. +- **Storing an element on `Array.prototype`** fails with SC2020, because holes in native arrays do not read inherited elements. +- **A recursive type alias in a generic signature** no longer crashes the compiler. +- **A `Buffer` view of an untyped typed array** compiles. +- **JSX programs checked with tsgo** compile as they do with tsc-ts. +- **Two programs whose entries share a name and build into one directory** no longer share a cached translation unit, so a later cache hit always relinks the right program. +- **A warm build whose cached LLVM exceeds the engine's string limit** reads it in parts instead of failing with `Invalid string length`. +- **The same program compiles to the same executable wherever it sits on disk** and whatever order its functions lower in, and edits outside a declaration do not rename the helpers and closures inside it. +- **Libraries that use untyped byte arrays** link. + + + ## 0.4.1 ### Highlights @@ -77,8 +232,6 @@ All notable changes to scriptc will be documented in this file. - **TypeScript enum exports in compiled packages stay `undefined` until they are assigned,** as in Node. - **Element writes accept a possibly undefined index** (typed-array writes, compound and update writes, element assignments used as values and destructuring targets) and convert it with ToNumber, as element reads already did. - - ## 0.4.0 ### Breaking changes diff --git a/benchmarks/runtime/README.md b/benchmarks/runtime/README.md index 2653ec51a..4f152e4bb 100644 --- a/benchmarks/runtime/README.md +++ b/benchmarks/runtime/README.md @@ -11,6 +11,8 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea | `channel-messages` | cpu | 200,000 MessageChannel messages in one thread, half numbers and half small objects, all posted before the port delivers any | | `csv-numbers` | cpu | `split`, `Number`, `parseInt`, `toFixed` | | `cyclic-graphs` | cpu | object graphs with back references built and dropped: trees with parent and sibling links, rings of records sharing a long-lived registry, closures capturing their owner | +| `dictionary-walks` | cpu | `Record` dictionaries walked with for-in and `Object.keys`/`values`/`entries` while entries are added, updated and deleted | +| `dynamic-methods` | cpu | untyped JavaScript calling builtin methods on values it does not type: string `slice`/`indexOf`/`toUpperCase`/`padStart`, array `push`/`pop`/`includes`, `Map` and `Set`, and an object's own methods | | `functional-pipeline` | cpu | `filter`/`map`/`reduce`/`flatMap` chains, closures, small records | | `intern-table`, `intern-table-map`, `intern-table-1`, `intern-table-map-1` | cpu | worker threads interning the same type-like objects under string keys and records under number keys, then hits and misses: one shared `InternTable` per kind of key, or a module-level `Map` per thread (the `-map` entries), with 8 threads or 1 (the `-1` entries); under Node.js every thread has its own tables | | `json-records` | cpu | `JSON.stringify`, typed `JSON.parse`, Map aggregation | diff --git a/benchmarks/runtime/workloads/dictionary-walks.json b/benchmarks/runtime/workloads/dictionary-walks.json new file mode 100644 index 000000000..ebfb6ea3f --- /dev/null +++ b/benchmarks/runtime/workloads/dictionary-walks.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/dictionary-walks.ts", + "args": [ + "1" + ] +} diff --git a/benchmarks/runtime/workloads/dictionary-walks.ts b/benchmarks/runtime/workloads/dictionary-walks.ts new file mode 100644 index 000000000..30b18be63 --- /dev/null +++ b/benchmarks/runtime/workloads/dictionary-walks.ts @@ -0,0 +1,53 @@ +// String-keyed dictionaries (`Record`) walked with for-in, +// Object.keys, Object.values and Object.entries while entries are added, +// updated and deleted: per-category totals, a pruning pass and a merge. +let seed = 777; +function random(): number { + seed = (Math.imul(seed, 1103515245) + 12345) >>> 0; + return seed / 4294967296; +} + +const scale = Number(process.argv[2] ?? "1"); +const categories = 4000; +const counts: Record = {}; +for (let i = 0; i < Math.floor(200000 * scale); i++) { + const key = "c" + Math.floor(random() * categories); + counts[key] = (counts[key] ?? 0) + 1; +} + +let walked = 0; +let sum = 0; +for (let pass = 0; pass < 40; pass++) { + for (const key in counts) { + walked++; + sum += counts[key]!; + } +} + +// Prune: delete the small categories during the walk, including keys the +// walk has not reached yet. +let pruned = 0; +for (const key in counts) { + const n = counts[key]!; + if (n < 45) { + delete counts[key]; + pruned++; + } + const ahead = "c" + ((Number(key.slice(1)) + 1) % categories); + if (n > 60 && ahead in counts && counts[ahead]! < 45) { + delete counts[ahead]; + pruned++; + } +} + +// Merge into a second dictionary and read it back every way. +const merged: Record = {}; +for (const key of Object.keys(counts)) merged[key.toUpperCase()] = counts[key]! * 2; +let values = 0; +for (const v of Object.values(merged)) values += v; +let entries = 0; +for (const [key, v] of Object.entries(merged)) entries += key.length + v; + +console.log( + `walked ${walked}, sum ${sum}, pruned ${pruned}, kept ${Object.keys(counts).length}, values ${values}, entries ${entries}`, +); diff --git a/benchmarks/runtime/workloads/dynamic-methods.json b/benchmarks/runtime/workloads/dynamic-methods.json new file mode 100644 index 000000000..fa27f4ea8 --- /dev/null +++ b/benchmarks/runtime/workloads/dynamic-methods.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/dynamic-methods.mjs", + "args": [ + "300000" + ] +} diff --git a/benchmarks/runtime/workloads/dynamic-methods.mjs b/benchmarks/runtime/workloads/dynamic-methods.mjs new file mode 100644 index 000000000..bbf217701 --- /dev/null +++ b/benchmarks/runtime/workloads/dynamic-methods.mjs @@ -0,0 +1,36 @@ +// Untyped JavaScript calling builtin methods on values it never declares +// types for: string slice/indexOf/toUpperCase/startsWith/padStart, array +// push/pop/at/join/includes/indexOf, Map and Set get/set/has/add, number +// toFixed, and calls of an object's own methods. Argument: the number of +// iterations. +const rounds = Number(process.argv[2] ?? "300000"); +const words = ["alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta"]; +const counts = new Map(); +const seen = new Set(); +const stack = []; +const recent = []; +const tally = { + total: 0, + add(value) { + this.total += value; + return this.total; + }, +}; +let checksum = 0; +for (let i = 0; i < rounds; i++) { + const word = words[i % words.length]; + const head = word.slice(0, 3).toUpperCase(); + const key = head.padStart(4, "-"); + if (word.indexOf("e") >= 0 && !word.startsWith("z")) checksum += 1; + counts.set(key, (counts.get(key) ?? 0) + 1); + seen.add(i % 97); + if (seen.has(i % 89)) checksum += 2; + stack.push(i); + if (stack.length > 32) checksum += stack.pop() % 3; + recent.push(word); + if (recent.length > 4) recent.shift(); + if (recent.includes("gamma")) checksum += recent.indexOf("gamma"); + checksum += tally.add(i % 5) % 11; + if (i % 1000 === 0) checksum += Number((i / 7).toFixed(2)) | 0; +} +console.log(checksum, counts.size, seen.size, stack.at(-1), recent.join(","), tally.total); diff --git a/docs/content/docs/cli.mdx b/docs/content/docs/cli.mdx index 8c57d2e2a..8b0353620 100644 --- a/docs/content/docs/cli.mdx +++ b/docs/content/docs/cli.mdx @@ -206,7 +206,7 @@ With `--sparse`, the profile leaves out the functions the runs never reached. Th
A runtime pack directory to use instead of an installed @scriptc/runtime-<target> package.
SCRIPTC_CHECKER
-
The TypeScript 7 API server that checks the program: tsc-ts (the default), which scriptc includes, or tsgo, from the typescript package scriptc also includes. Both give the same diagnostics and generated code. On Windows the default is tsgo, because tsc-ts's API server does not yet accept the named pipe scriptc connects over there.
+
The TypeScript 7 API server that checks the program: tsc-ts (the default), which scriptc includes, or tsgo, from the typescript package scriptc also includes. Both give the same diagnostics and generated code.
```console diff --git a/docs/content/docs/contracts.mdx b/docs/content/docs/contracts.mdx index 5af1cad2f..948fe7960 100644 --- a/docs/content/docs/contracts.mdx +++ b/docs/content/docs/contracts.mdx @@ -349,7 +349,7 @@ class Identifier extends SyntaxNode { } ``` -On Linux, except in builds with sanitizers, the instances also take less memory. An executable keeps them in one address range below 32 GiB that it reserves at startup (only the pages in use take memory), and a field typed `C`, `C | undefined` or `C | null`, where `C` is a program-lifetime class, stores the instance's address in 4 bytes instead of 8. In the example, `parent` takes 4 bytes. +On Linux, except in builds with sanitizers, the instances also take less memory. An executable keeps them in one address range below 32 GiB that it reserves at startup (only the pages in use take memory), and a field typed `C`, `C | undefined` or `C | null`, where `C` is a program-lifetime class, stores the instance's address in 4 bytes instead of 8, as does each element of an array of such a type and each value of a `Map` with `string` keys and such a value type. In the example, `parent` takes 4 bytes. scriptc checks that the promise cannot change what the program computes (SC7008): @@ -360,7 +360,7 @@ A class can be both `ProgramLifetime` and `Acyclic`. `Acyclic` is still checked Instances can be published to other threads with `@scriptc/threads` like other class instances: `publish()` makes them immutable and shares them by reference, writes to a published instance throw the same `TypeError` as under Node.js, and instances that were not published stay writable and are cloned by `postMessage` and `workerData`. -Static code has no `WeakRef` or `FinalizationRegistry`, and a `WeakMap` or `WeakSet` entry whose key is never freed is never observable as removed, so nothing else can tell the difference. +Static code has no `WeakRef`, a `FinalizationRegistry` never runs its cleanup callbacks, and a `WeakMap` or `WeakSet` entry whose key is never freed is never observable as removed, so nothing else can tell the difference. ### Lifetime regions diff --git a/docs/content/docs/coverage.mdx b/docs/content/docs/coverage.mdx index ae36f736d..db1193b98 100644 --- a/docs/content/docs/coverage.mdx +++ b/docs/content/docs/coverage.mdx @@ -38,6 +38,8 @@ When a program has unsupported operations, the report adds a group for each root compile staticallyStatements that compile to native instructions. in dynamic tierOf those, the statements in functions that operate on values typed only at runtime, such as any values and untyped JavaScript. Shown when there are any. See Dynamic tier. blockersUnsupported operations with counts, diagnostic codes, reasons, locations, and hints. + runs at startupDeferred operations in a JavaScript module's top-level code, which runs when the module loads: at startup for a module the program imports. + deferred to runtimeOther deferred operations in JavaScript code. Each throws only if it runs. in unreached codeBlockers in code the entry never reaches. A build never compiles that code, so these cannot fail it. differs from NodeSites that compile but can behave differently from Node.js. See Node divergences. npm package compilationNative package compilation outcomes and refusal reasons. @@ -46,7 +48,7 @@ When a program has unsupported operations, the report adds a group for each root Blockers in unreached code and deferred operations in JavaScript packages are listed separately because they don't fail a build. A deferred operation throws a runtime error if it runs, so a successful build doesn't mean every API of a package works; see [unsupported code inside a package](/docs/dependencies#unsupported-code-inside-a-package). -If a function's signature can't compile, the statements in its body count as not compiling, so the percentage reflects how much of the program compiles. +If a function's signature can't compile, the statements in its body count as not compiling, so the percentage reflects how much of the program compiles. The report still lists the diagnostics of those statements; a use of a parameter whose type can't compile reports `SC2004` back to the signature. @@ -95,7 +97,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`: 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. +- `SC6002`: values of one record type are built in key orders scriptc cannot track per value, 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. @@ -211,6 +213,10 @@ For editors, CI annotations, and other tools, `scriptc coverage --print=diagnost group The id of the root-cause group the diagnostic belongs to. A group's primary site is its first in source order. + + startup + true on a deferred diagnostic in a module's top-level code, which runs when the module loads. Absent otherwise. + diff --git a/docs/content/docs/dependencies.mdx b/docs/content/docs/dependencies.mdx index 85a232bb2..409b9cb30 100644 --- a/docs/content/docs/dependencies.mdx +++ b/docs/content/docs/dependencies.mdx @@ -132,7 +132,7 @@ CommonJS packages compile with Node.js's module semantics. Each `require()` eval ## Unsupported code inside a package -A JavaScript package can contain operations scriptc can't compile, often in functions your program never calls. These don't fail the build: each one compiles to code that throws a runtime error if it ever runs. Coverage reports list them as deferred sites, so review the list and test the parts of the package your program uses. +A JavaScript package can contain operations scriptc can't compile, often in functions your program never calls. These don't fail the build: each one compiles to code that throws a runtime error if it ever runs. Coverage reports list them as deferred sites, so review the list and test the parts of the package your program uses. Sites in a module's top-level code are listed under `runs at startup`: that code runs when the module loads, at startup for a module the program imports. ## Provenance sources (experimental) diff --git a/docs/content/docs/limitations.mdx b/docs/content/docs/limitations.mdx index e132f27da..a86ed217e 100644 --- a/docs/content/docs/limitations.mdx +++ b/docs/content/docs/limitations.mdx @@ -24,6 +24,8 @@ title: "Limitations" ../limitations/language-features/async-iteration.mdx +../limitations/language-features/iterable-values.mdx + ../limitations/language-features/generics.mdx ../limitations/language-features/function-values.mdx diff --git a/docs/content/limitations/builtin-apis/crypto.mdx b/docs/content/limitations/builtin-apis/crypto.mdx index c84ce3e52..5d7ff0e56 100644 --- a/docs/content/limitations/builtin-apis/crypto.mdx +++ b/docs/content/limitations/builtin-apis/crypto.mdx @@ -8,7 +8,7 @@ `randomFill`, `randomFillSync` (every view kind and ArrayBuffer), `randomInt`, `randomUUID`, and `randomUUIDv7` follow Node 24.22, including callback timing; a `randomInt` callback whose error parameter is typed `Error | null` receives `null` where Node passes `undefined`. Scrypt supports its default parameters, numeric options, aliases, and memory limit. Both KDFs accept strings, Buffers, numeric typed arrays, DataViews, and fixed-length ArrayBuffers. - `createCipheriv` and `createDecipheriv` support aes-128, aes-192, and aes-256 in CBC, CTR, and GCM modes and chacha20-poly1305, on native targets, through `update()`, `final()`, `setAAD()`, `setAutoPadding()`, `getAuthTag()`, and `setAuthTag()`; other ciphers and wasm32-wasi builds are refused. + `createCipheriv` and `createDecipheriv` support aes-128, aes-192, and aes-256 in CBC, CTR, GCM, ECB, CFB, CFB8, CFB1, and OFB modes, des-ede3 in CBC, ECB, CFB, CFB8, CFB1, and OFB modes, des-ede in CBC, ECB, CFB, and OFB modes, chacha20, and chacha20-poly1305, on native targets, through `update()`, `final()`, `setAAD()`, `setAutoPadding()`, `getAuthTag()`, and `setAuthTag()`; other ciphers and wasm32-wasi builds are refused. Cipheriv and Decipheriv values are also Transforms: they can be written, piped into, used as `pipeline()` stages, read, iterated with `for await`, and listened to, and their stream properties read as in Node; written chunks go through `update()`, and the end pushes `final()` or reports its error through `'error'`. Through `unknown` and `any` slots and untyped JavaScript, in a program that uses streams, their stream properties and their `on`, `once`, `off`, `pipe`, `unpipe`, `read`, `pause`, `resume`, `setEncoding`, `write`, `end`, `destroy`, `cork`, and `uncork` members work; `write` and `end` take no encoding or callback there, and the other stream members throw a coded SC2020 error. Hash, Hmac, Cipheriv, and Decipheriv streams count a written string's bytes in `writableLength`, where Node counts the string's length. @@ -25,7 +25,12 @@ `Object.keys()` and `in` do not see a DiffieHellman's own `verifyError` property, and `JSON.stringify()` and `instanceof` are refused. After `createDiffieHellman(primeLength)` throws `Invalid DH parameters`, the next OpenSSL error Node throws can list the failed generation's errors in `opensslErrorStack`; errors in a compiled binary list only their own. `crypto.diffieHellman`, which takes DH, EC, or X25519 KeyObjects, and the `DiffieHellman` and `DiffieHellmanGroup` constructors are not available. + `checkPrime`, `checkPrimeSync`, `generatePrime`, and `generatePrimeSync` support bigint, ArrayBuffer, typed-array, Buffer, and DataView values and the `checks`, `safe`, `add`, `rem`, and `bigint` options on native targets, with Node's errors; the callback forms compute before they return and call back in a later macrotask. + Candidates and sizes longer than 262144 bits throw a coded SC2020 error, and wasm32-wasi builds are refused. + Node calls their callbacks with `undefined` as the error, which a callback whose error parameter is typed `Error | null`, as @types/node declares it, receives as `null`. RSA, EC, and Ed25519 private and public KeyObjects come from `generateKeyPairSync`, `generateKeyPair`, `createPrivateKey`, and `createPublicKey`, on native targets. + `generateKeyPairSync` and `generateKeyPair` check their arguments in Node's order, with Node's errors, including the OpenSSL errors for RSA modulus lengths below 512 or above 2147483647 bits and for public exponents of 0, 1, or an even number, which `generateKeyPair` passes to its callback. + RSA keys generate with even modulus lengths from 1024 to 8192 bits and public exponents up to 2147483647; other lengths and exponents that Node accepts, the `raw-public`, `raw-private`, and `raw-seed` encodings, and `paramEncoding: 'explicit'` throw a coded SC2020 error. The EC curves are prime256v1 (P-256), secp384r1 (P-384), secp521r1 (P-521), and secp256k1. Keys read and write PEM and DER as PKCS#1, SEC1, PKCS#8, and SubjectPublicKeyInfo, and JWK (Ed25519 keys use PKCS#8, SubjectPublicKeyInfo, and JWK); `createPublicKey` also takes a private key or an X.509 certificate. They support `type`, `asymmetricKeyType`, `asymmetricKeyDetails`, `export()`, `equals()`, and `instanceof KeyObject`. diff --git a/docs/content/limitations/builtin-apis/map-and-set.mdx b/docs/content/limitations/builtin-apis/map-and-set.mdx index bf77001ee..ee7435b2d 100644 --- a/docs/content/limitations/builtin-apis/map-and-set.mdx +++ b/docs/content/limitations/builtin-apis/map-and-set.mdx @@ -7,4 +7,6 @@ They support `.next()`, `for...of`, array spreads, and mapper-less `Array.from`. They also support the iterator helper methods, such as `.map()` and `.take()`. Detached collection methods are unsupported. + Constructing a Map, Set, WeakMap or WeakSet, or calling `Object.fromEntries`, with a checked-dynamic iterable (a native iterator, a generator, or an object with a `Symbol.iterator` method) takes all of its values before adding the first one. + When an entry of a Map seed is not an object, the TypeError comes after the iterable is exhausted; Node closes the iterator at that entry. The Set composition methods (`union`, `intersection`, `difference`, `symmetricDifference`, `isSubsetOf`, `isSupersetOf`, `isDisjointFrom`) accept a Set of the same element type or a Map keyed by it; other set-like arguments are unsupported. diff --git a/docs/content/limitations/builtin-apis/prototypes.mdx b/docs/content/limitations/builtin-apis/prototypes.mdx index 3e0b5789d..170b11793 100644 --- a/docs/content/limitations/builtin-apis/prototypes.mdx +++ b/docs/content/limitations/builtin-apis/prototypes.mdx @@ -3,4 +3,6 @@ Plain objects, arrays, functions, class instances, and Date, RegExp, Map, Set, WeakMap, WeakSet, and Promise values inherit `constructor`, `valueOf`, `toString`, `hasOwnProperty`, and `propertyIsEnumerable` from `Object.prototype` as in Node, so `value.constructor === Object` and `value.valueOf === Object.prototype.valueOf` compare identities exactly. Named reads such as `value.valueOf` answer these inherited members; computed-key reads such as `value[key]` do not. `Object.prototype`'s own members read as values; its mutation and other reflection, such as `isPrototypeOf` and `__proto__`, are unsupported. - The constructors `Object`, `Function`, `Date`, `RegExp`, `Map`, `Set`, `WeakMap`, `WeakSet`, and `Promise` read as function values; through a stored value only `Object` can be called, and `instanceof` and `new` against them remain unsupported. + The constructors `Object`, `Function`, `Date`, `RegExp`, `Map`, `Set`, `WeakMap`, `WeakSet`, and `Promise` read as function values; through a stored value only `Object` can be called. + `new` through a stored `Object`, `Date`, `Map`, `Set`, `WeakMap`, or `WeakSet` builds the same value as the `new` expression. + `new` through a stored `RegExp` or `Promise`, `new` of a stored `Object` with a primitive argument, and `instanceof` against these constructors are unsupported. diff --git a/docs/content/limitations/builtin-apis/streams-and-readline.mdx b/docs/content/limitations/builtin-apis/streams-and-readline.mdx index 8461cc71c..fe6bb2dfb 100644 --- a/docs/content/limitations/builtin-apis/streams-and-readline.mdx +++ b/docs/content/limitations/builtin-apis/streams-and-readline.mdx @@ -10,19 +10,30 @@ The `objectMode` option, and the `readableObjectMode` and `writableObjectMode` options of a `Duplex`, `Transform`, or `PassThrough`, take `true` or `false` literals in an options object literal; options passed as a value are read when the stream is built. In typed code, a `write` or `transform` callback, or a `_write` or `_transform` method, may declare its chunk with any type that converts to a checked-dynamic value: the chunk converts when the callback runs, and a value of another type throws a TypeError there. An object-mode write passes its encoding argument to `write` and `transform` as Node does (`undefined` when absent, `null` when a callback took its place); a parameter declared `string` receives `""` for `undefined` and `null`, and one declared `string | null | undefined` receives Node's value. - `readable.read()` on an object-mode stream answers its Buffers; another value throws a not-supported error (read such values with `'data'` listeners, `for await`, or `pipe`). + `readable.read()` on an object-mode stream answers any value it holds. `node:stream/consumers` and readline interfaces read object-mode streams of strings and Buffers; other values throw a not-supported error. - `pipeline` accepts function stages anywhere among its streams: async generator functions (`async function* (source)`, iterating `source` with `for await`), an async generator source function (`async function* ({ signal })`), and an async function destination whose value resolves the promise form and reaches the callback's second argument. + `pipeline` accepts function stages anywhere among its streams: async generator functions (`async function* (source)`, iterating `source` with `for await`), an async generator source function (`async function* ({ signal })`), an async generator function as the destination, and an async function destination whose value resolves the promise form and reaches the callback's second argument. A stage is written inline, or named by a function declaration or a `const` bound to a function expression that nothing reassigns. The source may also be an array, a string, a synchronous generator object, or a synchronous generator function without parameters; their elements reach the next stage on the same ticks as in Node. Stages may take the `{ signal }` argument. - An async generator function as the destination, using the PassThrough that `pipeline()` returns for a function destination, and function stages or iterable sources together with pipeline options are unsupported. + With a function destination, `pipeline()` returns an object-mode PassThrough that receives an async generator destination's values, or an async function destination's value unless it is `null` or `undefined`. + `@types/node` types that result as `NodeJS.WritableStream`, so `for await` over it needs the `PassThrough` type (`pipeline(...) as PassThrough`, or a JSDoc `@type`). + In typed code, the callback of a pipeline with an async function destination receives `null` instead of `undefined` when there is no error. `Readable.from` accepts `objectMode` and `highWaterMark` options. - The `Readable` operators (`map`, `filter`, `forEach`, `toArray`, `some`, `find`, `every`, `flatMap`, `drop`, `take`, and `reduce`) accept `concurrency`, `highWaterMark`, and `signal` options; their callbacks take only the chunk, `map` returns strings or Buffers, and `flatMap` returns strings or arrays of them. - `readable.compose` is unsupported. - `stream.compose` accepts two or more streams, or one `Duplex`; functions, iterables, and web streams as stages are unsupported, and the `ERR_INVALID_ARG_VALUE` message for a stream that is no longer readable or writable names the stream's class instead of printing the stream. - `pipeline` from `node:stream/promises` accepts a trailing `{ signal, end }` object, and `finished` accepts `readable`, `writable`, `error`, `cleanup`, and `signal` options, with boolean literals for the flags. + The `Readable` operators (`map`, `filter`, `forEach`, `toArray`, `some`, `find`, `every`, `flatMap`, `drop`, `take`, and `reduce`) accept `concurrency`, `highWaterMark`, and `signal` options. + Their callbacks may take the `{ signal }` argument, `map` may return any value, and `flatMap` may return any iterable, async iterable, or stream. + `Duplex.from` accepts an async generator function, which reads what is written to the Duplex, a `Duplex`, `Transform` or `PassThrough`, which it returns, and the values `Readable.from` accepts, which give a Duplex that is not writable. + It also accepts a lone `Readable` or `Writable` and a `{ readable, writable }` pair, which the Duplex reads and writes through, a promise, whose value it reads, and an async function, which reads what is written and must resolve to `null` or `undefined`. + A `Duplex.from` body function may take the `{ signal }` argument, which is aborted with the error the Duplex is destroyed with. + `stream.compose` accepts streams and async generator functions in any position, an async function as the last stage, and an array, a string, or a generator as the first stage; one stage is `Duplex.from` of it. + A web `ReadableStream` is read as an async iterable by `Readable.from` and as the first stage of `compose` or `pipeline`, and through its reader by `Readable.fromWeb` (with `objectMode`, `highWaterMark`, `encoding`, and `signal` in an options object literal) and `Duplex.from`. + A web stream in a later compose stage, `Readable.toWeb`, and web `WritableStream` and `TransformStream` interop are unsupported. + The `ERR_INVALID_ARG_VALUE` message for a stream that is no longer readable or writable names the stream's class instead of printing the stream. + `readable.compose(stage, { signal })` is `stream.compose(readable, stage)` with the signal added; the options are an object literal whose `signal` is a variable or a property read. + `pipeline` from `node:stream/promises` accepts a trailing `{ signal, end }` object, with or without function stages, and `finished` accepts `readable`, `writable`, `error`, `cleanup`, and `signal` options, with boolean literals for the flags. Stream constructors accept a `signal` option in an options object literal, and `stream.addAbortSignal` accepts any stream. + The `construct` option and `_construct` method, `decodeStrings`, and `defaultEncoding` work on every stream class, and the `writev` option and `_writev` method on Writable and Duplex; `writev` on a Transform or PassThrough, assigning `_construct` after construction, and a non-literal `decodeStrings` or `defaultEncoding` in an options object literal are unsupported. + `setDefaultEncoding()` takes any encoding Node knows. A `transform` or `flush` option callback whose completion callback types `data` as `any`, as @types/node's `TransformCallback` does, may pass a Buffer, a string, or nothing; any other value throws a TypeError where the callback is called, instead of Node's `ERR_INVALID_ARG_TYPE` stream error. `node:readline` and `node:readline/promises` (also reached as `readline.promises`) interfaces, from `createInterface(options)` or `createInterface(input, output, completer, terminal)`, read `process.stdin` or a readable stream: `question`, `prompt`, `write` (with a key object literal of `name`, `ctrl`, `meta`, and `shift`), `pause`, `resume`, `'line'`/`'close'`/`'pause'`/`'resume'`/`'error'`/`'history'`/`'SIGINT'`/`'SIGTSTP'`/`'SIGCONT'` listeners, and `for await` compile. `on`, `once`, and `addListener` return the interface, so registrations chain. diff --git a/docs/content/limitations/builtin-function-values.mdx b/docs/content/limitations/builtin-function-values.mdx index c1bab6680..0fa62e558 100644 --- a/docs/content/limitations/builtin-function-values.mdx +++ b/docs/content/limitations/builtin-function-values.mdx @@ -28,7 +28,8 @@ The `captureRejections` option and `events.errorMonitor` listeners are not suppo The stream calls the object's `_read`, `_write`, `_final`, `_transform`, `_flush`, and `_destroy` with the object as `this`, and the stream prototypes' methods and accessors work on it. Its listeners live in the native stream rather than in `_events`, and `_readableState` and `_writableState` are read-only views that answer the commonly read fields. Typed code that receives such an object as a stream class uses its native stream, and piping a native stream into it works. -The options the native streams refuse (`decodeStrings: false`, `signal`, and others) are refused here too, as are `_writev`, `_construct`, symbol event names, `listeners()`, `rawListeners()`, `wrap()`, strings written to a byte-mode stream in encodings other than UTF-8, and pipes into a destination that overrides `write()` or `end()`. +The options the native streams refuse (`signal` and others) are refused here too, as are `_writev` on a Transform, `_final` on a Transform, symbol event names, `listeners()`, `rawListeners()`, `wrap()`, and pipes into a destination that overrides `write()` or `end()`. +`_construct` and `_writev` are read when `Readable.call(this, options)` or `Writable.call(this, options)` runs, as Node's constructors read them. Other class constructors passed to `util.inherits` or `util.deprecate` are refused. Deprecation warnings are emitted on the next tick and follow assignments to `process.noDeprecation` and `process.throwDeprecation`; compiled binaries take no Node command-line flags such as `--no-deprecation` or `--trace-deprecation`, and `process.traceDeprecation` is unsupported. A stored `util.deprecate` reports a `length` of 4, where Node reports 3. diff --git a/docs/content/limitations/language-features/iterable-values.mdx b/docs/content/limitations/language-features/iterable-values.mdx new file mode 100644 index 000000000..2feedc02f --- /dev/null +++ b/docs/content/limitations/language-features/iterable-values.mdx @@ -0,0 +1,2 @@ +- **Iterable values:** A value typed `Iterable` can hold an array, a Set, a Map (for `Iterable<[K, V]>`), a string (for `Iterable`), a generator, or a Map, Set or array iterator. + Class instances with their own `[Symbol.iterator]` method and typed arrays can't be stored in an `Iterable` and are reported at the conversion. diff --git a/docs/content/limitations/record-identity-and-property-order.mdx b/docs/content/limitations/record-identity-and-property-order.mdx index 2299122a4..dd180cb7b 100644 --- a/docs/content/limitations/record-identity-and-property-order.mdx +++ b/docs/content/limitations/record-identity-and-property-order.mdx @@ -12,9 +12,10 @@ 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`, `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. +`Object.keys`, `Object.values`, `Object.entries`, `for...in`, `JSON.stringify`, and `util.inspect` enumerate a record's keys in the order the object created them, as in Node.js. +scriptc chooses one order per record type 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. +When the object literals of one record type create its keys in different orders, including literals of two types with the same properties and property types, each value records the order its literal used: copies made by object spread, rest destructuring, and record conversions keep their source's order, and assigning an optional key the object lacks, including after `delete`, moves the key last. +A type keeps the declaration order of its first type, and `SC6002` reports object literals whose order differs where the program observes it, when one of its values gets its keys from a spread whose source order is not known, from a spread that also sets an optional key the source lacks, or from a copy of a value scriptc cannot trace, or when the type's values can take more than 16 orders. 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/types-and-class-definitions/local-class-expressions.mdx b/docs/content/limitations/types-and-class-definitions/local-class-expressions.mdx index 173bdc0c6..de24038cc 100644 --- a/docs/content/limitations/types-and-class-definitions/local-class-expressions.mdx +++ b/docs/content/limitations/types-and-class-definitions/local-class-expressions.mdx @@ -1,5 +1,6 @@ -- **Local class expressions:** Static fields and static blocks in TypeScript files, decorators, private brands, generic members, and references through the class's internal name are unsupported. +- **Local class expressions:** Static fields and static blocks in TypeScript files, decorators, private brands, and generic members are unsupported. Static methods read the enclosing function's bindings and are inherited by subclasses. + A named class expression's methods and static members can refer to the class by its name. Supported expressions preserve fresh constructor identity and shared captures across constructors, methods, accessors, and arrow fields. Local inheritance requires a compiled base class. Constructors retain identity, `typeof`, `name`, and `length` through `unknown`; cast back to their exact type before construction. diff --git a/docs/content/limitations/weak-collections-and-memory.mdx b/docs/content/limitations/weak-collections-and-memory.mdx index 97ee20d7b..21c6128d9 100644 --- a/docs/content/limitations/weak-collections-and-memory.mdx +++ b/docs/content/limitations/weak-collections-and-memory.mdx @@ -3,6 +3,15 @@ Copied values, other native handles, and symbol keys are unsupported. Constructor seeds support arrays; custom iterators and detached methods are unsupported. Weak collections do not retain their keys, but values that refer back to a key can keep it alive until deletion or collection release. +`FinalizationRegistry` keeps its registrations: `register` and `unregister` check their arguments and answer as in Node.js. +Its cleanup callbacks never run, which the ECMAScript specification allows. +Program output matches Node.js, which runs cleanup callbacks only when its garbage collector decides to. +Memory that a package frees only from a cleanup callback is not reclaimed until the program exits. +For example, a wasm-bindgen package frees the WebAssembly memory behind a wrapper object from its cleanup callback, so a long-running server that drops such wrappers without calling their `free()` method keeps that memory. +Call `free()` on wrappers the program no longer needs. +Symbol targets and unregister tokens are unsupported. +`WeakRef` is unsupported. + Memory uses reference counting. Acyclic values are freed deterministically, and cycles are collected at defined collection points. Native checked objects, arrays, Sets, and captured closures participate in cycle collection. diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index 574c49ff4..c341da852 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -11085,9 +11085,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" } }, { @@ -11108,9 +11108,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" } }, { @@ -11614,9 +11614,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" } }, { @@ -11637,9 +11637,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" } }, { diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 31ce5da02..9393fcaa8 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -19328,6 +19328,10 @@ "tests/corpus/7084-crypto-key-pairs.cjs", "tests/corpus/7122-webcrypto-node-crypto.ts", "tests/corpus/7123-webcrypto-node-crypto.cjs", + "tests/corpus/7150-cipher-legacy-modes.ts", + "tests/corpus/7151-cipher-legacy-errors.cjs", + "tests/corpus/7152-crypto-prime.ts", + "tests/corpus/7153-crypto-prime-errors.cjs", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/node24-crypto-kdf.ts", @@ -19658,8 +19662,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "surface-manifest:node-builtin.crypto.checkPrime", + "tests": [ + "tests/corpus/7152-crypto-prime.ts", + "tests/corpus/7153-crypto-prime-errors.cjs" + ] }, "anchorSource": "exact" }, @@ -19681,8 +19689,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "surface-manifest:node-builtin.crypto.checkPrimeSync", + "tests": [ + "tests/corpus/7152-crypto-prime.ts", + "tests/corpus/7153-crypto-prime-errors.cjs" + ] }, "anchorSource": "exact" }, @@ -19707,6 +19719,8 @@ "status": "partial", "evidence": "surface-manifest:node-builtin.crypto.createCipheriv", "tests": [ + "tests/corpus/7150-cipher-legacy-modes.ts", + "tests/corpus/7151-cipher-legacy-errors.cjs", "tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", @@ -19746,6 +19760,8 @@ "status": "partial", "evidence": "surface-manifest:node-builtin.crypto.createDecipheriv", "tests": [ + "tests/corpus/7150-cipher-legacy-modes.ts", + "tests/corpus/7151-cipher-legacy-errors.cjs", "tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", @@ -20364,8 +20380,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "surface-manifest:node-builtin.crypto.generatePrime", + "tests": [ + "tests/corpus/7152-crypto-prime.ts", + "tests/corpus/7153-crypto-prime-errors.cjs" + ] }, "anchorSource": "exact" }, @@ -20387,8 +20407,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "surface-manifest:node-builtin.crypto.generatePrimeSync", + "tests": [ + "tests/corpus/7152-crypto-prime.ts", + "tests/corpus/7153-crypto-prime-errors.cjs" + ] }, "anchorSource": "exact" }, diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index a59d6365a..06aad26cf 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -42,8 +42,10 @@ "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"], "node-builtin.console": ["tests/corpus/console-imports.cjs", "tests/corpus/2440-console-inspect-args.ts", "tests/corpus/console-native-value.js", "tests/harness/npm-static.test.ts"], - "node-builtin.crypto.createCipheriv": ["tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/corpus/7055-cipher-streams.ts", "tests/corpus/7056-cipher-stream-errors.ts", "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts"], - "node-builtin.crypto.createDecipheriv": ["tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/corpus/7055-cipher-streams.ts", "tests/corpus/7056-cipher-stream-errors.ts", "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts"], + "node-builtin.crypto.checkPrime": ["tests/corpus/7152-crypto-prime.ts", "tests/corpus/7153-crypto-prime-errors.cjs"], + "node-builtin.crypto.checkPrimeSync": ["tests/corpus/7152-crypto-prime.ts", "tests/corpus/7153-crypto-prime-errors.cjs"], + "node-builtin.crypto.createCipheriv": ["tests/corpus/7150-cipher-legacy-modes.ts", "tests/corpus/7151-cipher-legacy-errors.cjs", "tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/corpus/7055-cipher-streams.ts", "tests/corpus/7056-cipher-stream-errors.ts", "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts"], + "node-builtin.crypto.createDecipheriv": ["tests/corpus/7150-cipher-legacy-modes.ts", "tests/corpus/7151-cipher-legacy-errors.cjs", "tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/corpus/7055-cipher-streams.ts", "tests/corpus/7056-cipher-stream-errors.ts", "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts"], "node-builtin.crypto.createDiffieHellman": ["tests/corpus/6706-dh-groups.ts", "tests/corpus/6707-dh-explicit-params.ts", "tests/corpus/6708-dh-errors.js", "tests/corpus/6709-dh-generate.ts", "tests/corpus/6710-dh-encodings.cjs", "tests/corpus/6711-dh-handles.ts", "tests/fixtures/node-types/dh.ts"], "node-builtin.crypto.createDiffieHellmanGroup": ["tests/corpus/6706-dh-groups.ts", "tests/corpus/6707-dh-explicit-params.ts", "tests/corpus/6708-dh-errors.js", "tests/corpus/6709-dh-generate.ts", "tests/corpus/6710-dh-encodings.cjs", "tests/corpus/6711-dh-handles.ts", "tests/fixtures/node-types/dh.ts"], "node-builtin.crypto.createECDH": ["tests/corpus/6700-ecdh-shared-secret.ts", "tests/corpus/6701-ecdh-formats.ts", "tests/corpus/6702-ecdh-errors.js", "tests/corpus/6703-ecdh-private-keys.ts", "tests/corpus/6704-ecdh-set-public-key.cjs", "tests/corpus/6705-ecdh-handles.ts", "tests/fixtures/node-types/ecdh.ts"], @@ -120,6 +122,8 @@ "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.generatePrime": ["tests/corpus/7152-crypto-prime.ts", "tests/corpus/7153-crypto-prime-errors.cjs"], + "node-builtin.crypto.generatePrimeSync": ["tests/corpus/7152-crypto-prime.ts", "tests/corpus/7153-crypto-prime-errors.cjs"], "node-builtin.crypto.getDiffieHellman": ["tests/corpus/6706-dh-groups.ts", "tests/corpus/6707-dh-explicit-params.ts", "tests/corpus/6708-dh-errors.js", "tests/corpus/6709-dh-generate.ts", "tests/corpus/6710-dh-encodings.cjs", "tests/corpus/6711-dh-handles.ts", "tests/fixtures/node-types/dh.ts"], "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"], diff --git a/packages/cli-darwin-arm64/package.json b/packages/cli-darwin-arm64/package.json index 482e5f1e5..23584b60a 100644 --- a/packages/cli-darwin-arm64/package.json +++ b/packages/cli-darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/cli-darwin-arm64", - "version": "0.4.1", + "version": "0.4.2", "description": "Native scriptc compiler for darwin-arm64", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/cli-darwin-x64/package.json b/packages/cli-darwin-x64/package.json index b1ced2c03..d9e6dafc6 100644 --- a/packages/cli-darwin-x64/package.json +++ b/packages/cli-darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/cli-darwin-x64", - "version": "0.4.1", + "version": "0.4.2", "description": "Native scriptc compiler for darwin-x64", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/cli-linux-arm64-gnu/package.json b/packages/cli-linux-arm64-gnu/package.json index 0aa213279..5b8cb4db2 100644 --- a/packages/cli-linux-arm64-gnu/package.json +++ b/packages/cli-linux-arm64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/cli-linux-arm64-gnu", - "version": "0.4.1", + "version": "0.4.2", "description": "Native scriptc compiler for linux-arm64-gnu", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/cli-linux-arm64-musl/package.json b/packages/cli-linux-arm64-musl/package.json index 4ac5434ae..0901602ec 100644 --- a/packages/cli-linux-arm64-musl/package.json +++ b/packages/cli-linux-arm64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/cli-linux-arm64-musl", - "version": "0.4.1", + "version": "0.4.2", "description": "Native scriptc compiler for linux-arm64-musl", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/cli-linux-x64-gnu/package.json b/packages/cli-linux-x64-gnu/package.json index 7473c36bc..f01478aad 100644 --- a/packages/cli-linux-x64-gnu/package.json +++ b/packages/cli-linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/cli-linux-x64-gnu", - "version": "0.4.1", + "version": "0.4.2", "description": "Native scriptc compiler for linux-x64-gnu", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/cli-linux-x64-musl/package.json b/packages/cli-linux-x64-musl/package.json index 08e048cd1..a6986a66a 100644 --- a/packages/cli-linux-x64-musl/package.json +++ b/packages/cli-linux-x64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/cli-linux-x64-musl", - "version": "0.4.1", + "version": "0.4.2", "description": "Native scriptc compiler for linux-x64-musl", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/cli-win32-x64-msvc/package.json b/packages/cli-win32-x64-msvc/package.json index 54e323839..6b03811b8 100644 --- a/packages/cli-win32-x64-msvc/package.json +++ b/packages/cli-win32-x64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/cli-win32-x64-msvc", - "version": "0.4.1", + "version": "0.4.2", "description": "Native scriptc compiler for win32-x64-msvc", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/cli/package.json b/packages/cli/package.json index d66ba3247..034747c29 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "scriptc", - "version": "0.4.1", + "version": "0.4.2", "description": "Compile ordinary TypeScript and JavaScript to small, fast native executables and WebAssembly modules that run without Node.js", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/cli/test/native-link-info.test.ts b/packages/cli/test/native-link-info.test.ts index a7a5e718a..a5edb11bb 100644 --- a/packages/cli/test/native-link-info.test.ts +++ b/packages/cli/test/native-link-info.test.ts @@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => { minimum_os: "14.0", }, program: { object, entry_symbol: "main" }, - runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" }, + runtime_abi: { version: 36, marker: "scr_runtime_abi_v36" }, runtime_pack: { kind: "precompiled", package: "@scriptc/runtime-darwin-arm64", @@ -165,6 +165,6 @@ describe.runIf(supported)("macOS arm64 native link info", () => { () => null, (failure: { stderr?: string }) => failure, ); - expect(error?.stderr).toContain("scr_runtime_abi_v27"); + expect(error?.stderr).toContain("scr_runtime_abi_v36"); }); }); diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 2a3f11be9..b195c3fbb 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -3171,7 +3171,8 @@ declare module "crypto" { callback: (error: Error | null, buffer: Buffer) => void, ): void; export type BinaryLike = string | ArrayBuffer | ArrayBufferView; - /* Cipheriv/Decipheriv: aes-128/192/256 in cbc, ctr, and gcm modes and + /* Cipheriv/Decipheriv: aes-128/192/256 in cbc, ctr, gcm, ecb, cfb, cfb8, + * cfb1, and ofb modes, des-ede3 and des-ede, chacha20, and * chacha20-poly1305. Both are Transforms: written chunks go through * update(), and the end pushes final(). */ export type CipherGCMTypes = "aes-128-gcm" | "aes-192-gcm" | "aes-256-gcm"; @@ -3351,6 +3352,61 @@ declare module "crypto" { ): DiffieHellman; export function getDiffieHellman(groupName: string): DiffieHellmanGroup; export function createDiffieHellmanGroup(name: string): DiffieHellmanGroup; + export type LargeNumberLike = ArrayBufferView | SharedArrayBuffer | ArrayBuffer | bigint; + export interface GeneratePrimeOptions { + add?: LargeNumberLike | undefined; + rem?: LargeNumberLike | undefined; + safe?: boolean | undefined; + bigint?: boolean | undefined; + } + export interface GeneratePrimeOptionsBigInt extends GeneratePrimeOptions { + bigint: true; + } + export interface GeneratePrimeOptionsArrayBuffer extends GeneratePrimeOptions { + bigint?: false | undefined; + } + export function generatePrime( + size: number, + callback: (err: Error | null, prime: ArrayBuffer) => void, + ): void; + export function generatePrime( + size: number, + options: GeneratePrimeOptionsBigInt, + callback: (err: Error | null, prime: bigint) => void, + ): void; + export function generatePrime( + size: number, + options: GeneratePrimeOptionsArrayBuffer, + callback: (err: Error | null, prime: ArrayBuffer) => void, + ): void; + export function generatePrime( + size: number, + options: GeneratePrimeOptions, + callback: (err: Error | null, prime: ArrayBuffer | bigint) => void, + ): void; + export function generatePrimeSync(size: number): ArrayBuffer; + export function generatePrimeSync(size: number, options: GeneratePrimeOptionsBigInt): bigint; + export function generatePrimeSync( + size: number, + options: GeneratePrimeOptionsArrayBuffer, + ): ArrayBuffer; + export function generatePrimeSync( + size: number, + options: GeneratePrimeOptions, + ): ArrayBuffer | bigint; + export interface CheckPrimeOptions { + checks?: number | undefined; + } + export function checkPrime( + value: LargeNumberLike, + callback: (err: Error | null, result: boolean) => void, + ): void; + export function checkPrime( + value: LargeNumberLike, + options: CheckPrimeOptions, + callback: (err: Error | null, result: boolean) => void, + ): void; + export function checkPrimeSync(candidate: LargeNumberLike, options?: CheckPrimeOptions): boolean; export interface ScryptOptions { N?: number; r?: number; @@ -3512,7 +3568,8 @@ declare module "child_process" { * killSignal at the deadline and the result carries error: ETIMEDOUT + * the signal, never a throw), killSignal (a signal NAME literal), * stdio ("ignore"/"inherit"/"pipe" or a 3-tuple of those — captures - * read "" for non-piped streams), and windowsHide (a POSIX no-op). */ + * read "" for non-piped streams), env (replaces the child's + * environment), and windowsHide (a POSIX no-op). */ export function spawnSync( command: string, args?: string[], @@ -3521,6 +3578,7 @@ declare module "child_process" { timeout?: number; killSignal?: string; stdio?: "pipe" | "ignore" | "inherit" | ("pipe" | "ignore" | "inherit")[]; + env?: { [k: string]: string | undefined }; windowsHide?: boolean; }, ): SpawnSyncReturns; @@ -3554,20 +3612,20 @@ declare module "child_process" { export interface ChildProcess extends Disposable { ref(): void; - on(event: "spawn", listener: () => void): void; - once(event: "spawn", listener: () => void): void; + on(event: "spawn", listener: () => void): this; + once(event: "spawn", listener: () => void): this; /* The exit listener may also take Node's second parameter — the * terminating signal's name, null for a normal exit. */ - on(event: "exit", listener: (code: number | null, signal: string | null) => void): void; - on(event: "close", listener: (code: number | null, signal: string | null) => void): void; - on(event: "error", listener: (err: Error) => void): void; - once(event: "exit", listener: (code: number | null, signal: string | null) => void): void; - once(event: "close", listener: (code: number | null, signal: string | null) => void): void; - once(event: "error", listener: (err: Error) => void): void; - on(event: "message", listener: (message: any) => void): void; - on(event: "disconnect", listener: () => void): void; - once(event: "message", listener: (message: any) => void): void; - once(event: "disconnect", listener: () => void): void; + on(event: "exit", listener: (code: number | null, signal: string | null) => void): this; + on(event: "close", listener: (code: number | null, signal: string | null) => void): this; + on(event: "error", listener: (err: Error) => void): this; + once(event: "exit", listener: (code: number | null, signal: string | null) => void): this; + once(event: "close", listener: (code: number | null, signal: string | null) => void): this; + once(event: "error", listener: (err: Error) => void): this; + on(event: "message", listener: (message: any) => void): this; + on(event: "disconnect", listener: () => void): this; + once(event: "message", listener: (message: any) => void): this; + once(event: "disconnect", listener: () => void): this; /* The lifecycle members, Node's exact shapes: pid is undefined exactly * when the spawn failed; exitCode is null while running, the code * after a normal exit, null for a signal death, and -errno once a @@ -3780,9 +3838,8 @@ declare module "util" { export type TextDecoder = globalThis.TextDecoder; export var TextEncoder: typeof globalThis.TextEncoder; export var TextDecoder: typeof globalThis.TextDecoder; - /* stream.pipeline's (...streams: unknown[]) => unknown is assignable to - * every other target's parameter type, so its overload comes first; no - * other target is assignable to it. */ + /* stream.pipeline's overload comes first: its __promisify__ member keeps + * every other target out of it. */ export function promisify( fn: typeof import("node:stream").pipeline, ): typeof import("node:stream/promises").pipeline; @@ -7090,7 +7147,11 @@ declare module "stream" { encoding: string, callback: (error?: Error | null) => void, ): void; - writev?(this: Writable, chunks: unknown[], callback: (error?: Error | null) => void): void; + writev?( + this: Writable, + chunks: Array<{ chunk: any; encoding: string }>, + callback: (error?: Error | null) => void, + ): void; final?(this: Writable, callback: (error?: Error | null) => void): void; destroy?(this: Writable, error: Error | null, callback: (error?: Error | null) => void): void; construct?(this: Writable, callback: (error?: Error | null) => void): void; @@ -7125,7 +7186,11 @@ declare module "stream" { readable?: boolean; writable?: boolean; defaultEncoding?: string; - writev?(this: Duplex, chunks: unknown[], callback: (error?: Error | null) => void): void; + writev?( + this: Duplex, + chunks: Array<{ chunk: any; encoding: string }>, + callback: (error?: Error | null) => void, + ): void; signal?: AbortSignal; } interface TransformOptions { @@ -7210,6 +7275,36 @@ declare module "stream" { ): void; } & ObjectModeFlag; + /* decodeStrings: false — a string write reaches write/transform as the + * string with its encoding (Buffers keep 'buffer'). */ + interface StringWritableOptions extends Omit { + decodeStrings: false; + write?( + this: Writable, + chunk: string | Buffer, + encoding: string, + callback: (error?: Error | null) => void, + ): void; + } + type StringDuplexOptions = Omit & { + decodeStrings: false; + write?( + this: Duplex, + chunk: string | Buffer, + encoding: string, + callback: (error?: Error | null) => void, + ): void; + }; + type StringTransformOptions = Omit & { + decodeStrings: false; + transform?( + this: Transform, + chunk: string | Buffer, + encoding: string, + callback: (error?: Error | null, data?: Buffer | string) => void, + ): void; + }; + /* The operators' options: concurrency and highWaterMark bound map's * in-flight callbacks and queued results. */ interface ArrayOptions { @@ -7228,12 +7323,16 @@ declare module "stream" { _destroy(error: Error | null, callback: (error?: Error | null) => void): void; [Symbol.asyncIterator](): AsyncIterableIterator; iterator(options?: { destroyOnReturn?: boolean }): AsyncIterableIterator; + /* compose(this, stage), destroyed when options.signal aborts. */ + compose(stage: ComposeStage, options?: { signal: AbortSignal }): Duplex; push(chunk: Buffer | string | null, encoding?: string): boolean; /* An object-mode stream's values. */ push(chunk: any, encoding?: string): boolean; unshift(chunk: Buffer | string, encoding?: string): void; unshift(chunk: any, encoding?: string): void; - read(size?: number): Buffer | null; + /* A Buffer, a string once encoded, an object-mode value, or null (as + * @types/node types it). */ + read(size?: number): any; pause(): this; resume(): this; isPaused(): boolean; @@ -7251,6 +7350,16 @@ declare module "stream" { readonly destroyed: boolean; readonly errored: Error | null; static from(iterable: unknown, options?: ReadableOptions): Readable; + /* A Readable over a web ReadableStream's reader. */ + static fromWeb( + readableStream: globalThis.ReadableStream, + options?: { + encoding?: string; + highWaterMark?: number; + objectMode?: boolean; + signal?: AbortSignal; + }, + ): Readable; /* The Readable operators: chunks are strings or Buffers, map and * flatMap callbacks return strings or Buffers (flatMap: arrays of * them, or strings), and callbacks that declare the options argument @@ -7340,6 +7449,7 @@ declare module "stream" { class Writable extends EventEmitter { constructor(opts: ObjectWritableOptions); + constructor(opts: StringWritableOptions); constructor(opts?: WritableOptions); /* The underscore-method surface (see Readable). */ _write(chunk: any, encoding: string, callback: (error?: Error | null) => void): void; @@ -7417,7 +7527,11 @@ declare module "stream" { * mirrors @types/node: extends Readable, the writable half declared * again. The lowering treats both halves as first-class. */ class Duplex extends Readable { + /* Duplex.from: an async generator function, an iterable, a string or + * a stream (duplexify.js). */ + static from(body: unknown): Duplex; constructor(opts: ObjectDuplexOptions); + constructor(opts: StringDuplexOptions); constructor(opts?: DuplexOptions); /* The underscore-method surface (see Readable). */ _write(chunk: any, encoding: string, callback: (error?: Error | null) => void): void; @@ -7517,6 +7631,7 @@ declare module "stream" { class Transform extends Duplex { constructor(opts: ObjectTransformOptions); + constructor(opts: StringTransformOptions); constructor(opts?: TransformOptions); /* The underscore-method surface (see Readable). */ _transform( @@ -7528,17 +7643,43 @@ declare module "stream" { } class PassThrough extends Transform { constructor(opts: ObjectTransformOptions); + constructor(opts: StringTransformOptions); constructor(opts?: TransformOptions); } /* addAbortSignal(signal, stream): the stream, destroyed with an * AbortError (cause: signal.reason) when the signal aborts. */ function addAbortSignal(signal: AbortSignal, stream: T): T; - /* Declared so requires typecheck; each fences at its use site. */ - function pipeline(...streams: unknown[]): unknown; - /* compose(...streams): a Duplex that writes to the first stream and - * reads from the last, with the streams running as a pipeline. */ - function compose(...streams: Array): Duplex; + /* pipeline(...stages, callback): the last stream, or the object-mode + * PassThrough that a function destination feeds. */ + type PipelineResult = T extends [ + ...unknown[], + infer D, + (...args: any[]) => unknown, + ] + ? D extends (...args: any[]) => unknown + ? PassThrough + : D + : unknown; + function pipeline(...streams: T): PipelineResult; + /* util.promisify's pipeline overload matches only pipeline itself: a + * generic signature alone admits every function. */ + namespace pipeline { + const __promisify__: typeof promises.pipeline; + } + /* A compose stage: a stream, an async generator function over the + * previous stage, or (first) an iterable, async iterable or web + * ReadableStream. */ + type ComposeStage = + | Readable + | Writable + | Iterable + | AsyncIterable + | globalThis.ReadableStream + | ((source: any, options?: { signal: AbortSignal }) => unknown); + /* compose(...streams): a Duplex that writes to the first stage and + * reads from the last, with the stages running as a pipeline. */ + function compose(...streams: ComposeStage[]): Duplex; /* finished's options: readable/writable choose which ends count, * error: false ignores 'error' events, signal aborts the wait. */ interface FinishedOptions { @@ -7557,13 +7698,20 @@ declare module "stream" { * else 65536 — Node's own state.js split); literal argument only. */ function getDefaultHighWaterMark(objectMode: boolean): number; /* The promise pipeline's result: a last stage that is an async - * function resolves the promise with its value. */ + * function resolves the promise with its value (before the options + * argument, when one follows). */ type PipelinePromise = T extends [ ...unknown[], (...args: any[]) => Promise, ] ? Promise

- : Promise; + : T extends [ + ...unknown[], + (...args: any[]) => Promise, + { signal?: AbortSignal; end?: boolean }, + ] + ? Promise

+ : Promise; namespace promises { function pipeline(...streams: T): PipelinePromise; function finished( @@ -7593,6 +7741,7 @@ declare module "stream" { DuplexOptions, TransformOptions, FinishedOptions, + PipelinePromise, }; } declare module "node:stream" { @@ -7607,9 +7756,7 @@ declare module "stream/promises" { * pipeline's trailing { signal, end } object and finished's options * (readable, writable, error, cleanup, signal) are supported. A last * stage that is an async function resolves the promise with its value. */ - function pipeline( - ...streams: T - ): T extends [...unknown[], (...args: any[]) => Promise] ? Promise

: Promise; + function pipeline(...streams: T): import("stream").PipelinePromise; function finished( stream: unknown, options?: { diff --git a/packages/compiler/native/cli.profdata.gz b/packages/compiler/native/cli.profdata.gz index 55523e765..aa0e9c36b 100644 Binary files a/packages/compiler/native/cli.profdata.gz and b/packages/compiler/native/cli.profdata.gz differ diff --git a/packages/compiler/native/host.c b/packages/compiler/native/host.c index 6aa1b2a28..f54bdaa3f 100644 --- a/packages/compiler/native/host.c +++ b/packages/compiler/native/host.c @@ -1,6 +1,10 @@ #ifndef _WIN32 #define _POSIX_C_SOURCE 200809L #endif +/* flock and LOCK_EX/LOCK_UN, which Darwin hides under _POSIX_C_SOURCE. */ +#if defined(__APPLE__) && !defined(_DARWIN_C_SOURCE) +#define _DARWIN_C_SOURCE 1 +#endif #include #include #include @@ -56,3 +60,78 @@ void scr_collect_cycles_phase(void); void scriptc_native_collect_cycles(void) { scr_collect_cycles_phase(); } + +/* Forked lowering workers (frontend/process-fork.ts). A worker shares the + * parent's heap copy-on-write, writes its results to a file and leaves with + * _exit, so no buffered output or exit handler of the parent runs twice. + * Checker requests from workers hold an exclusive flock on a file each + * worker opens after the fork, so every worker has its own open file + * description and the lock excludes the other workers. */ +#ifdef _WIN32 +int32_t scriptc_native_fork(void) { return -1; } +void scriptc_native_exit(int32_t code) { ExitProcess((UINT)code); } +int32_t scriptc_native_wait(int32_t pid) { (void)pid; return -1; } +uint8_t scriptc_native_lock_open(const uint8_t *bytes, size_t size) { (void)bytes; (void)size; return 0; } +void scriptc_native_lock(void) {} +void scriptc_native_unlock(void) {} +#else +#include +#include +#include +#include + +int32_t scriptc_native_fork(void) { return (int32_t)fork(); } + +void scriptc_native_exit(int32_t code) { _exit(code); } + +/* The worker's exit status, 128 plus the signal for a worker a signal + * ended, or -1 when waitpid fails. */ +int32_t scriptc_native_wait(int32_t pid) { + int status = 0; + while (waitpid((pid_t)pid, &status, 0) < 0) { + if (errno != EINTR) return -1; + } + if (WIFEXITED(status)) return WEXITSTATUS(status); + if (WIFSIGNALED(status)) return 128 + WTERMSIG(status); + return -1; +} + +static int scriptc_native_lock_fd = -1; + +uint8_t scriptc_native_lock_open(const uint8_t *bytes, size_t size) { + if (size == 0 || memchr(bytes, 0, size) != NULL) return 0; + char *path = malloc(size + 1); + if (!path) return 0; + memcpy(path, bytes, size); + path[size] = 0; + if (scriptc_native_lock_fd >= 0) close(scriptc_native_lock_fd); + scriptc_native_lock_fd = open(path, O_CREAT | O_RDWR | O_CLOEXEC, 0600); + free(path); + return scriptc_native_lock_fd >= 0; +} + +void scriptc_native_lock(void) { + if (scriptc_native_lock_fd < 0) return; + while (flock(scriptc_native_lock_fd, LOCK_EX) < 0 && errno == EINTR) { + } +} + +void scriptc_native_unlock(void) { + if (scriptc_native_lock_fd >= 0) flock(scriptc_native_lock_fd, LOCK_UN); +} +#endif + +/* The runtime's growth cap for full cycle passes (scr_rc.h). */ +void scr_cyc_set_growth_cap(long cap); + +/* Bounds the cyclic garbage a build can hold to twice its live heap. + * Lowering a large program leaves hundreds of megabytes of cyclic garbage, + * and whether a scheduled full pass reclaims it before the build's peak + * depends on where the growth rule fires: with the default cap of 16, small + * changes to the compiler moved the peak of one build by 390 MB. With a cap + * of 2 a full pass runs once the heap has grown to three times its size + * after the last one; a cap of 1 bounds the garbage further but runs more + * passes over the live heap. SCR_CYCLE_GROWTH_CAP still overrides it. */ +void scriptc_native_bound_cycle_growth(void) { + scr_cyc_set_growth_cap(2); +} diff --git a/packages/compiler/native/host.ffi.json b/packages/compiler/native/host.ffi.json index d7e0e92aa..e7ecbca60 100644 --- a/packages/compiler/native/host.ffi.json +++ b/packages/compiler/native/host.ffi.json @@ -2,6 +2,13 @@ "ffi_format": 6, "functions": [ { "name": "compilerNativePrivateDirectory", "symbol": "scriptc_native_private_directory", "params": ["string", "bool"], "returns": "bool" }, - { "name": "compilerNativeCollectCycles", "symbol": "scriptc_native_collect_cycles", "params": [], "returns": "void" } + { "name": "compilerNativeCollectCycles", "symbol": "scriptc_native_collect_cycles", "params": [], "returns": "void" }, + { "name": "compilerNativeFork", "symbol": "scriptc_native_fork", "params": [], "returns": "i32" }, + { "name": "compilerNativeExit", "symbol": "scriptc_native_exit", "params": ["i32"], "returns": "void" }, + { "name": "compilerNativeWait", "symbol": "scriptc_native_wait", "params": ["i32"], "returns": "i32" }, + { "name": "compilerNativeLockOpen", "symbol": "scriptc_native_lock_open", "params": ["string"], "returns": "bool" }, + { "name": "compilerNativeLock", "symbol": "scriptc_native_lock", "params": [], "returns": "void" }, + { "name": "compilerNativeUnlock", "symbol": "scriptc_native_unlock", "params": [], "returns": "void" }, + { "name": "compilerNativeBoundCycleGrowth", "symbol": "scriptc_native_bound_cycle_growth", "params": [], "returns": "void" } ] } diff --git a/packages/compiler/package.json b/packages/compiler/package.json index a283f821a..a358ae545 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/compiler", - "version": "0.4.1", + "version": "0.4.2", "description": "The scriptc compiler — TypeScript/JavaScript frontend, typed IR, LLVM backend", "license": "Apache-2.0", "homepage": "https://scriptc.dev", @@ -40,7 +40,7 @@ }, "dependencies": { "@scriptc/runtime": "workspace:*", - "tsc-ts": "0.0.7", + "tsc-ts": "0.0.8", "typescript": "7.0.2", "typescript5": "npm:typescript@5.9.3" }, diff --git a/packages/compiler/shims/chalk/v5.ts b/packages/compiler/shims/chalk/v5.ts new file mode 100644 index 000000000..2d34c5c53 --- /dev/null +++ b/packages/compiler/shims/chalk/v5.ts @@ -0,0 +1,688 @@ +// scriptc's built-in implementation of the `chalk` package, 5.x API. +// +// Programs whose installed chalk is 5.x compile this module in its place +// (packages/compiler/src/frontend/package-shims.ts). chalk builds its +// styles as functions whose prototypes it swaps at run time; here a style +// chain is an instance of one class whose style members are cached +// getters, and the instance is callable through its `%call` field (the +// merged interface below gives it chalk's call signature; the lowerer calls +// the field). The output, color-level detection (supports-color 10: +// FORCE_COLOR, the --color/--no-color flags, TTY, TERM, CI variables) and +// the shared, settable `level` follow chalk 5.6 byte for byte. +import * as os from "node:os"; +import * as tty from "node:tty"; + +export type ColorSupportLevel = 0 | 1 | 2 | 3; + +export interface ColorSupport { + level: ColorSupportLevel; + hasBasic: boolean; + has256: boolean; + has16m: boolean; +} + +export type ColorInfo = ColorSupport | false; + +export interface Options { + readonly level?: ColorSupportLevel; +} + +export type ModifierName = + | "reset" + | "bold" + | "dim" + | "italic" + | "underline" + | "overline" + | "inverse" + | "hidden" + | "strikethrough"; + +export type ForegroundColorName = + | "black" + | "red" + | "green" + | "yellow" + | "blue" + | "cyan" + | "magenta" + | "white" + | "gray" + | "grey" + | "blackBright" + | "redBright" + | "greenBright" + | "yellowBright" + | "blueBright" + | "cyanBright" + | "magentaBright" + | "whiteBright"; + +export type BackgroundColorName = + | "bgBlack" + | "bgRed" + | "bgGreen" + | "bgYellow" + | "bgBlue" + | "bgCyan" + | "bgMagenta" + | "bgWhite" + | "bgGray" + | "bgGrey" + | "bgBlackBright" + | "bgRedBright" + | "bgGreenBright" + | "bgYellowBright" + | "bgBlueBright" + | "bgCyanBright" + | "bgMagentaBright" + | "bgWhiteBright"; + +export type ColorName = ForegroundColorName | BackgroundColorName; + +export type Modifiers = ModifierName; +export type ForegroundColor = ForegroundColorName; +export type BackgroundColor = BackgroundColorName; +export type Color = ColorName; + +// ── ansi-styles ────────────────────────────────────────────────────────── + +export const modifierNames: ModifierName[] = [ + "reset", + "bold", + "dim", + "italic", + "underline", + "overline", + "inverse", + "hidden", + "strikethrough", +]; +export const foregroundColorNames: ForegroundColorName[] = [ + "black", + "red", + "green", + "yellow", + "blue", + "magenta", + "cyan", + "white", + "blackBright", + "gray", + "grey", + "redBright", + "greenBright", + "yellowBright", + "blueBright", + "magentaBright", + "cyanBright", + "whiteBright", +]; +export const backgroundColorNames: BackgroundColorName[] = [ + "bgBlack", + "bgRed", + "bgGreen", + "bgYellow", + "bgBlue", + "bgMagenta", + "bgCyan", + "bgWhite", + "bgBlackBright", + "bgGray", + "bgGrey", + "bgRedBright", + "bgGreenBright", + "bgYellowBright", + "bgBlueBright", + "bgMagentaBright", + "bgCyanBright", + "bgWhiteBright", +]; +export const colorNames: ColorName[] = [...foregroundColorNames, ...backgroundColorNames]; +export const modifiers: Modifiers[] = modifierNames; +export const foregroundColors: ForegroundColor[] = foregroundColorNames; +export const backgroundColors: BackgroundColor[] = backgroundColorNames; +export const colors: Color[] = colorNames; + +const ESC = "\u001B["; + +/** Open and close codes, indexed by style id (the order of the getters). */ +const OPEN_CODES: number[] = [ + 0, 1, 2, 3, 4, 53, 7, 8, 9, 30, 31, 32, 33, 34, 35, 36, 37, 90, 90, 90, 91, 92, 93, 94, 95, 96, + 97, 40, 41, 42, 43, 44, 45, 46, 47, 100, 100, 100, 101, 102, 103, 104, 105, 106, 107, +]; +const CLOSE_CODES: number[] = [ + 0, 22, 22, 23, 24, 55, 27, 28, 29, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, + 39, 39, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, +]; +const VISIBLE = 45; + +function rgbToAnsi256(red: number, green: number, blue: number): number { + if (red === green && green === blue) { + if (red < 8) return 16; + if (red > 248) return 231; + return Math.round(((red - 8) / 247) * 24) + 232; + } + return ( + 16 + + 36 * Math.round((red / 255) * 5) + + 6 * Math.round((green / 255) * 5) + + Math.round((blue / 255) * 5) + ); +} + +function hexToRgb(hex: string): number[] { + const matches = /[a-f\d]{6}|[a-f\d]{3}/i.exec(hex); + if (matches === null) return [0, 0, 0]; + let colorString = matches[0]; + if (colorString.length === 3) { + let doubled = ""; + for (const character of colorString) doubled += character + character; + colorString = doubled; + } + const integer = Number.parseInt(colorString, 16); + return [(integer >> 16) & 0xff, (integer >> 8) & 0xff, integer & 0xff]; +} + +function ansi256ToAnsi(code: number): number { + if (code < 8) return 30 + code; + if (code < 16) return 90 + (code - 8); + let red: number; + let green: number; + let blue: number; + if (code >= 232) { + red = ((code - 232) * 10 + 8) / 255; + green = red; + blue = red; + } else { + code -= 16; + const remainder = code % 36; + red = Math.floor(code / 36) / 5; + green = Math.floor(remainder / 6) / 5; + blue = (remainder % 6) / 5; + } + const value = Math.max(red, green, blue) * 2; + if (value === 0) return 30; + let result = 30 + ((Math.round(blue) << 2) | (Math.round(green) << 1) | Math.round(red)); + if (value === 2) result += 60; + return result; +} + +/** The open code of a color model at a level: offset 0 is the foreground, + * 10 the background (ansi-styles' wrapAnsi16/256/16m). */ +function rgbOpen(level: number, offset: number, red: number, green: number, blue: number): string { + if (level === 3) return `${ESC}${38 + offset};2;${red};${green};${blue}m`; + if (level === 2) return `${ESC}${38 + offset};5;${rgbToAnsi256(red, green, blue)}m`; + return `${ESC}${ansi256ToAnsi(rgbToAnsi256(red, green, blue)) + offset}m`; +} + +/** chalk's ansi256 model emits the 256-color code at every level (only + * the rgb and hex models downsample). */ +function ansi256Open(offset: number, code: number): string { + return `${ESC}${38 + offset};5;${code}m`; +} + +// ── supports-color ─────────────────────────────────────────────────────── + +function hasFlag(flag: string): boolean { + const argv = process.argv; + const prefix = flag.startsWith("-") ? "" : flag.length === 1 ? "-" : "--"; + const position = argv.indexOf(prefix + flag); + const terminatorPosition = argv.indexOf("--"); + return position !== -1 && (terminatorPosition === -1 || position < terminatorPosition); +} + +let flagForceColor: number | undefined = undefined; +if ( + hasFlag("no-color") || + hasFlag("no-colors") || + hasFlag("color=false") || + hasFlag("color=never") +) { + flagForceColor = 0; +} else if ( + hasFlag("color") || + hasFlag("colors") || + hasFlag("color=true") || + hasFlag("color=always") +) { + flagForceColor = 1; +} + +function envForceColor(): number | undefined { + const force = process.env["FORCE_COLOR"]; + if (force !== undefined) { + if (force === "true") return 1; + if (force === "false") return 0; + return force.length === 0 ? 1 : Math.min(Number.parseInt(force, 10), 3); + } + return undefined; +} + +function translateLevel(level: number): ColorInfo { + if (level === 0) return false; + return { + level: level as ColorSupportLevel, + hasBasic: true, + has256: level >= 2, + has16m: level >= 3, + }; +} + +function inEnv(name: string): boolean { + return process.env[name] !== undefined; +} + +function detectLevel(streamIsTTY: boolean): number { + const noFlagForceColor = envForceColor(); + if (noFlagForceColor !== undefined) flagForceColor = noFlagForceColor; + const forceColor = flagForceColor; + if (forceColor === 0) return 0; + if (hasFlag("color=16m") || hasFlag("color=full") || hasFlag("color=truecolor")) return 3; + if (hasFlag("color=256")) return 2; + if (inEnv("TF_BUILD") && inEnv("AGENT_NAME")) return 1; + if (!streamIsTTY && forceColor === undefined) return 0; + const min = + forceColor === undefined || forceColor === 0 || Number.isNaN(forceColor) ? 0 : forceColor; + const env = process.env; + const term = env["TERM"]; + if (term === "dumb") return min; + if (process.platform === "win32") { + const osRelease = os.release().split("."); + if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10_586) { + return Number(osRelease[2]) >= 14_931 ? 3 : 2; + } + return 1; + } + if (inEnv("CI")) { + if (inEnv("GITHUB_ACTIONS") || inEnv("GITEA_ACTIONS") || inEnv("CIRCLECI")) return 3; + if ( + inEnv("TRAVIS") || + inEnv("APPVEYOR") || + inEnv("GITLAB_CI") || + inEnv("BUILDKITE") || + inEnv("DRONE") || + env["CI_NAME"] === "codeship" + ) { + return 1; + } + return min; + } + const teamcity = env["TEAMCITY_VERSION"]; + if (teamcity !== undefined) { + return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(teamcity) ? 1 : 0; + } + if (env["COLORTERM"] === "truecolor") return 3; + if (term === "xterm-kitty") return 3; + if (term === "xterm-ghostty") return 3; + if (term === "wezterm") return 3; + const termProgram = env["TERM_PROGRAM"]; + if (termProgram !== undefined) { + const version = Number.parseInt((env["TERM_PROGRAM_VERSION"] || "").split(".")[0]!, 10); + if (termProgram === "iTerm.app") return version >= 3 ? 3 : 2; + if (termProgram === "Apple_Terminal") return 2; + } + // supports-color tests these against `env.TERM`, which is the string + // "undefined" when TERM is unset. + const termText = term === undefined ? "undefined" : term; + if (/-256(color)?$/i.test(termText)) return 2; + if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(termText)) return 1; + if (inEnv("COLORTERM")) return 1; + return min; +} + +const stdoutColor: ColorInfo = translateLevel(detectLevel(tty.isatty(1))); +const stderrColor: ColorInfo = translateLevel(detectLevel(tty.isatty(2))); + +export const supportsColor: ColorInfo = stdoutColor; +export const supportsColorStderr: ColorInfo = stderrColor; + +// ── the builder ────────────────────────────────────────────────────────── + +/** One link of a style chain (chalk's styler). */ +class Styler { + open: string; + close: string; + openAll: string; + closeAll: string; + parent: Styler | undefined; + constructor(open: string, close: string, parent: Styler | undefined) { + this.open = open; + this.close = close; + this.parent = parent; + if (parent === undefined) { + this.openAll = open; + this.closeAll = close; + } else { + this.openAll = parent.openAll + open; + this.closeAll = close + parent.closeAll; + } + } +} + +/** The level every builder of one chalk instance shares. */ +class LevelBox { + level: number; + constructor(level: number) { + this.level = level; + } +} + +function stringReplaceAll(string: string, substring: string, replacer: string): string { + let index = string.indexOf(substring); + if (index === -1) return string; + const substringLength = substring.length; + let endIndex = 0; + let returnValue = ""; + do { + returnValue += string.slice(endIndex, index) + substring + replacer; + endIndex = index + substringLength; + index = string.indexOf(substring, endIndex); + } while (index !== -1); + returnValue += string.slice(endIndex); + return returnValue; +} + +function stringEncaseCRLFWithFirstIndex( + string: string, + prefix: string, + postfix: string, + index: number, +): string { + let endIndex = 0; + let returnValue = ""; + do { + const gotCR = string[index - 1] === "\r"; + returnValue += + string.slice(endIndex, gotCR ? index - 1 : index) + + prefix + + (gotCR ? "\r\n" : "\n") + + postfix; + endIndex = index + 1; + index = string.indexOf("\n", endIndex); + } while (index !== -1); + returnValue += string.slice(endIndex); + return returnValue; +} + +function applyStyle(self: ChalkInstance, string: string): string { + if (self.box.level <= 0 || !string) return self.isEmpty ? "" : string; + let styler = self.styler; + if (styler === undefined) return string; + const openAll = styler.openAll; + const closeAll = styler.closeAll; + if (string.includes("\u001B")) { + while (styler !== undefined) { + string = stringReplaceAll(string, styler.close, styler.open); + styler = styler.parent; + } + } + const lfIndex = string.indexOf("\n"); + if (lfIndex !== -1) string = stringEncaseCRLFWithFirstIndex(string, closeAll, openAll, lfIndex); + return openAll + string + closeAll; +} + +function joinArguments(args: unknown[]): string { + let out = ""; + for (let i = 0; i < args.length; i++) { + if (i > 0) out += " "; + const value = args[i]; + if (value !== undefined && value !== null) out += String(value); + } + return out; +} + +/** A chalk instance or one of its style chains: callable (chalk's call + * signature merges in from the interface), with every style a cached + * getter. */ +// The merge is the point: the interface gives instances chalk's call +// signature, and the class's `%call` field implements it. +// oxlint-disable-next-line typescript/no-unsafe-declaration-merging +export interface ChalkInstance { + (...text: unknown[]): string; +} + +// oxlint-disable-next-line typescript/no-unsafe-declaration-merging +export class ChalkInstance { + "%call": (...text: unknown[]) => string = (...text: unknown[]): string => + this.root + ? joinArguments(text) + : applyStyle(this, text.length === 1 ? "" + String(text[0]) : joinArguments(text)); + box: LevelBox; + styler: Styler | undefined; + isEmpty: boolean; + private readonly root: boolean; + private readonly cache: (ChalkInstance | undefined)[] = []; + + constructor(box: LevelBox, styler: Styler | undefined, isEmpty: boolean, root: boolean) { + this.box = box; + this.styler = styler; + this.isEmpty = isEmpty; + this.root = root; + } + + /** One string argument: the compiler calls this instead of `%call` + * (no variadic arguments to box). */ + "%call1"(text: string): string { + return this.root ? text : applyStyle(this, text); + } + + get level(): ColorSupportLevel { + return this.box.level as ColorSupportLevel; + } + + set level(level: ColorSupportLevel) { + this.box.level = level; + } + + private style(id: number): ChalkInstance { + const cached = this.cache[id]; + if (cached !== undefined) return cached; + let builder: ChalkInstance; + if (id === VISIBLE) { + builder = new ChalkInstance(this.box, this.styler, true, false); + } else { + const styler = new Styler( + `${ESC}${OPEN_CODES[id]!}m`, + `${ESC}${CLOSE_CODES[id]!}m`, + this.styler, + ); + builder = new ChalkInstance(this.box, styler, this.isEmpty, false); + } + while (this.cache.length <= id) this.cache.push(undefined); + this.cache[id] = builder; + return builder; + } + + private model(open: string, background: boolean): ChalkInstance { + const styler = new Styler(open, background ? `${ESC}49m` : `${ESC}39m`, this.styler); + return new ChalkInstance(this.box, styler, this.isEmpty, false); + } + + rgb(red: number, green: number, blue: number): ChalkInstance { + return this.model(rgbOpen(this.box.level, 0, red, green, blue), false); + } + hex(color: string): ChalkInstance { + const [red, green, blue] = hexToRgb(color); + return this.model(rgbOpen(this.box.level, 0, red!, green!, blue!), false); + } + ansi256(index: number): ChalkInstance { + return this.model(ansi256Open(0, index), false); + } + bgRgb(red: number, green: number, blue: number): ChalkInstance { + return this.model(rgbOpen(this.box.level, 10, red, green, blue), true); + } + bgHex(color: string): ChalkInstance { + const [red, green, blue] = hexToRgb(color); + return this.model(rgbOpen(this.box.level, 10, red!, green!, blue!), true); + } + bgAnsi256(index: number): ChalkInstance { + return this.model(ansi256Open(10, index), true); + } + + get reset(): ChalkInstance { + return this.style(0); + } + get bold(): ChalkInstance { + return this.style(1); + } + get dim(): ChalkInstance { + return this.style(2); + } + get italic(): ChalkInstance { + return this.style(3); + } + get underline(): ChalkInstance { + return this.style(4); + } + get overline(): ChalkInstance { + return this.style(5); + } + get inverse(): ChalkInstance { + return this.style(6); + } + get hidden(): ChalkInstance { + return this.style(7); + } + get strikethrough(): ChalkInstance { + return this.style(8); + } + get visible(): ChalkInstance { + return this.style(VISIBLE); + } + get black(): ChalkInstance { + return this.style(9); + } + get red(): ChalkInstance { + return this.style(10); + } + get green(): ChalkInstance { + return this.style(11); + } + get yellow(): ChalkInstance { + return this.style(12); + } + get blue(): ChalkInstance { + return this.style(13); + } + get magenta(): ChalkInstance { + return this.style(14); + } + get cyan(): ChalkInstance { + return this.style(15); + } + get white(): ChalkInstance { + return this.style(16); + } + get blackBright(): ChalkInstance { + return this.style(17); + } + get gray(): ChalkInstance { + return this.style(18); + } + get grey(): ChalkInstance { + return this.style(19); + } + get redBright(): ChalkInstance { + return this.style(20); + } + get greenBright(): ChalkInstance { + return this.style(21); + } + get yellowBright(): ChalkInstance { + return this.style(22); + } + get blueBright(): ChalkInstance { + return this.style(23); + } + get magentaBright(): ChalkInstance { + return this.style(24); + } + get cyanBright(): ChalkInstance { + return this.style(25); + } + get whiteBright(): ChalkInstance { + return this.style(26); + } + get bgBlack(): ChalkInstance { + return this.style(27); + } + get bgRed(): ChalkInstance { + return this.style(28); + } + get bgGreen(): ChalkInstance { + return this.style(29); + } + get bgYellow(): ChalkInstance { + return this.style(30); + } + get bgBlue(): ChalkInstance { + return this.style(31); + } + get bgMagenta(): ChalkInstance { + return this.style(32); + } + get bgCyan(): ChalkInstance { + return this.style(33); + } + get bgWhite(): ChalkInstance { + return this.style(34); + } + get bgBlackBright(): ChalkInstance { + return this.style(35); + } + get bgGray(): ChalkInstance { + return this.style(36); + } + get bgGrey(): ChalkInstance { + return this.style(37); + } + get bgRedBright(): ChalkInstance { + return this.style(38); + } + get bgGreenBright(): ChalkInstance { + return this.style(39); + } + get bgYellowBright(): ChalkInstance { + return this.style(40); + } + get bgBlueBright(): ChalkInstance { + return this.style(41); + } + get bgMagentaBright(): ChalkInstance { + return this.style(42); + } + get bgCyanBright(): ChalkInstance { + return this.style(43); + } + get bgWhiteBright(): ChalkInstance { + return this.style(44); + } +} + +function optionsLevel(options: Options | undefined): number { + const level = options === undefined ? undefined : options.level; + if ( + level !== undefined && + level !== 0 && + !(Number.isInteger(level) && level >= 0 && level <= 3) + ) { + throw new Error("The `level` option should be an integer from 0 to 3"); + } + if (level === undefined) return stdoutColor === false ? 0 : stdoutColor.level; + return level; +} + +/** `new Chalk(options)`: a fresh chalk instance with its own level. */ +export class Chalk extends ChalkInstance { + constructor(options?: Options) { + super(new LevelBox(optionsLevel(options)), undefined, false, true); + } +} + +const chalk: ChalkInstance = new Chalk(); +export const chalkStderr: ChalkInstance = new Chalk({ + level: stderrColor === false ? 0 : stderrColor.level, +}); + +export default chalk; diff --git a/packages/compiler/shims/hot-shots/client.ts b/packages/compiler/shims/hot-shots/client.ts new file mode 100644 index 000000000..354744e7b --- /dev/null +++ b/packages/compiler/shims/hot-shots/client.ts @@ -0,0 +1,1126 @@ +// scriptc's built-in implementation of the `hot-shots` package, 8.x API: +// a typed port of hot-shots 8.5's StatsD client with the same datagrams, +// callbacks, error messages and child-client sharing, over node:dgram +// (udp), node:net (tcp) and a native Unix datagram socket (uds). The +// entry modules (v8.ts, v8-unix-dgram.ts) say whether the optional +// unix-dgram addon that hot-shots' uds protocol loads is installed. +import * as dgram from "node:dgram"; +import * as dns from "node:dns"; +import * as net from "node:net"; +import * as os from "node:os"; + +export type Tags = { [key: string]: string } | string[]; + +export interface ClientOptions { + bufferFlushInterval?: number; + bufferHolder?: { buffer: string }; + cacheDns?: boolean; + cacheDnsTtl?: number; + errorHandler?: (err: Error) => void; + globalTags?: Tags; + globalize?: boolean; + host?: string; + isChild?: boolean; + maxBufferSize?: number; + mock?: boolean; + path?: string; + port?: number; + prefix?: string; + protocol?: "tcp" | "udp" | "uds"; + sampleRate?: number; + suffix?: string; + telegraf?: boolean; + useDefaultRoute?: boolean; + tagPrefix?: string; + tagSeparator?: string; + udsGracefulErrorHandling?: boolean; + udsGracefulRestartRateLimit?: number; + closingFlushInterval?: number; +} + +export interface ChildClientOptions { + globalTags?: Tags; + prefix?: string; + suffix?: string; + errorHandler?: (err: Error) => void; +} + +export interface CheckOptions { + /** A Unix timestamp; hot-shots also takes a Date, which this shim's + * option records cannot hold yet. */ + date_happened?: number; + hostname?: string; + message?: string; +} + +export interface DatadogChecks { + OK: 0; + WARNING: 1; + CRITICAL: 2; + UNKNOWN: 3; +} + +export type DatadogChecksValues = 0 | 1 | 2 | 3; + +export interface EventOptions { + aggregation_key?: string; + alert_type?: "info" | "warning" | "success" | "error"; + /** A Unix timestamp; hot-shots also takes a Date, which this shim's + * option records cannot hold yet. */ + date_happened?: number; + hostname?: string; + priority?: "low" | "normal"; + source_type_name?: string; +} + +/** A send's completion: hot-shots passes `null` on success. */ +export type StatsCb = (error: Error | null | undefined, bytes?: any) => void; + +type SampleArg = number | Tags | StatsCb | undefined; +type TagsArg = Tags | StatsCb | undefined; + +const PROTOCOL_TCP = "tcp"; +const PROTOCOL_UDS = "uds"; +const PROTOCOL_UDP = "udp"; +const UDS_DEFAULT_GRACEFUL_RESTART_LIMIT = 1000; +const CACHE_DNS_TTL_DEFAULT = 60000; + +function udsErrorCodes(): number[] { + if (process.platform === "linux") { + return [os.constants.errno.ENOTCONN, os.constants.errno.ECONNREFUSED]; + } + if (process.platform === "darwin") { + return [os.constants.errno.EDESTADDRREQ, os.constants.errno.ECONNRESET]; + } + return []; +} +const UDS_ERROR_CODES = udsErrorCodes(); + +// ── helpers ────────────────────────────────────────────────────────────── + +function sanitizeTags(value: string, telegraf: boolean): string { + const blacklist = telegraf ? /:|\||,/g : /:|\||@|,/g; + return String(value).replace(blacklist, "_"); +} + +function formatTags(tags: Tags, telegraf: boolean): string[] { + if (Array.isArray(tags)) return tags; + const out: string[] = []; + for (const key of Object.keys(tags)) { + out.push(`${sanitizeTags(key, telegraf)}:${sanitizeTags(tags[key]!, telegraf)}`); + } + return out; +} + +function isArrayIndexKey(key: string): boolean { + return /^(0|[1-9]\d*)$/.test(key) && Number(key) < 4294967295; +} + +function overrideTags(parent: string[], child: Tags, telegraf: boolean): string[] { + const childKeys: string[] = []; + const childValues = new Map(); + const toAppend: string[] = []; + for (const tag of formatTags(child, telegraf)) { + const idx = tag.indexOf(":"); + if (idx < 1) { + toAppend.push(tag); + } else { + const key = tag.substring(0, idx); + const value = tag.substring(idx + 1); + let values = childValues.get(key); + if (values === undefined) { + values = []; + childValues.set(key, values); + childKeys.push(key); + } + values.push(value); + } + } + const result = parent.filter((tag) => { + const idx = tag.indexOf(":"); + if (idx < 1) return true; + return !childValues.has(tag.substring(0, idx)); + }); + // hot-shots walks an object's keys: array-index keys ascending first, + // then the rest in insertion order. + const indexKeys = childKeys.filter(isArrayIndexKey).sort((a, b) => Number(a) - Number(b)); + const ordered = indexKeys.concat(childKeys.filter((key) => !isArrayIndexKey(key))); + for (const key of ordered) { + for (const value of childValues.get(key)!) result.push(`${key}:${value}`); + } + return result.concat(toAppend); +} + +function formatDate(date: number): number { + return Math.round(date); +} + +/** console.error of an arbitrary value, as hot-shots logs errors. The + * value crosses as unknown: the typed Error path has no inspect lowering + * in programs whose Error subclasses define accessors. */ +function logError(value: unknown): void { + console.error(value); +} + +function errorWithCode(message: string, code: string | undefined): Error { + const error: NodeJS.ErrnoException = new Error(message); + error.code = code; + return error; +} + +// ── transports ─────────────────────────────────────────────────────────── + +type SendCallback = (err: Error | null, bytes?: number) => void; + +/** The socket a client sends through (hot-shots' transport interface). */ +interface Transport { + createdAt: number; + send(buf: Buffer, callback: SendCallback): void; + close(): void; + onError(listener: (err: Error) => void): void; + offError(listener: (err: Error) => void): void; + /** Emits 'error' on the socket, as hot-shots' `socket.emit('error')`. */ + emitError(err: Error): void; + onClose(listener: () => void): void; +} + +/** A socket's 'error' listeners, kept by the transport (the typed + * sockets have no removeListener): one socket listener dispatches to a + * snapshot, and an error nobody listens for throws, as an unhandled + * 'error' event does. */ +class ErrorListeners { + private listeners: ((err: Error) => void)[] = []; + add(listener: (err: Error) => void): void { + this.listeners.push(listener); + } + remove(listener: (err: Error) => void): void { + const index = this.listeners.indexOf(listener); + if (index !== -1) this.listeners.splice(index, 1); + } + emit(err: Error): void { + const snapshot = this.listeners.slice(); + if (snapshot.length === 0) throw err; + for (const listener of snapshot) listener(err); + } +} + +function addEol(buf: Buffer): string { + let msg = buf.toString(); + if (msg.length > 0 && msg[msg.length - 1] !== "\n") msg += "\n"; + return msg; +} + +class UdpTransport implements Transport { + createdAt = 0; + private readonly socket: dgram.Socket; + private readonly errors = new ErrorListeners(); + private resolvedAddress: string | undefined = undefined; + private resolvedAt = 0; + private readonly host: string | undefined; + private readonly port: number; + private readonly cacheDns: boolean; + private readonly cacheDnsTtl: number; + constructor(host: string | undefined, port: number, cacheDns: boolean, cacheDnsTtl: number) { + this.host = host; + this.port = port; + this.cacheDns = cacheDns; + this.cacheDnsTtl = cacheDnsTtl; + this.socket = dgram.createSocket("udp4"); + this.socket.unref(); + // The listener captures the list, not the transport: a socket that is + // never closed keeps its listener, and a capture of `this` would make + // a cycle the reference counts cannot free at exit. + const errors = this.errors; + this.socket.on("error", (err) => errors.emit(err)); + } + send(buf: Buffer, callback: SendCallback): void { + if (!this.cacheDns) { + this.socket.send(buf, this.port, this.host || "127.0.0.1", callback); + return; + } + const now = Date.now(); + const cached = this.resolvedAddress; + if (cached === undefined || now - this.resolvedAt > this.cacheDnsTtl) { + dns.lookup(this.host ?? "localhost", (error, address) => { + if (error) { + callback(error); + return; + } + this.resolvedAddress = address; + this.resolvedAt = now; + this.socket.send(buf, this.port, address, callback); + }); + } else { + this.socket.send(buf, this.port, cached, callback); + } + } + close(): void { + this.socket.close(); + } + onError(listener: (err: Error) => void): void { + this.errors.add(listener); + } + offError(listener: (err: Error) => void): void { + this.errors.remove(listener); + } + emitError(err: Error): void { + this.errors.emit(err); + } + onClose(listener: () => void): void { + this.socket.on("close", listener); + } +} + +class TcpTransport implements Transport { + createdAt = 0; + private readonly socket: net.Socket; + private readonly errors = new ErrorListeners(); + constructor(host: string | undefined, port: number) { + this.socket = net.connect(port, host ?? "localhost"); + this.socket.setKeepAlive(true); + this.socket.unref(); + // The listener captures the list, not the transport: a socket that is + // never closed keeps its listener, and a capture of `this` would make + // a cycle the reference counts cannot free at exit. + const errors = this.errors; + this.socket.on("error", (err) => errors.emit(err)); + } + send(buf: Buffer, callback: SendCallback): void { + // hot-shots writes the line with the 'ascii' encoding, which encodes + // strings as 'latin1' does. The callback runs once the data is written, + // so a message held back by a full socket buffer stays in flight. + this.socket.write(Buffer.from(addEol(buf), "latin1"), (err) => callback(err ?? null)); + } + close(): void { + this.socket.destroy(); + } + onError(listener: (err: Error) => void): void { + this.errors.add(listener); + } + offError(listener: (err: Error) => void): void { + this.errors.remove(listener); + } + emitError(err: Error): void { + this.errors.emit(err); + } + onClose(listener: () => void): void { + this.socket.on("close", () => listener()); + } +} + +// The runtime's AF_UNIX datagram calls (the unix-dgram addon's binding): +// results >= 0, or a negative errno; send answers 1 when it would block. +declare function scriptcUnixDgramSocket(): number; +declare function scriptcUnixDgramConnect(fd: number, path: string): number; +declare function scriptcUnixDgramSend(fd: number, data: Buffer): number; +declare function scriptcUnixDgramClose(fd: number): number; + +let unixDgramInstalled = false; + +/** Whether the unix-dgram addon is installed beside hot-shots (set by the + * entry module). */ +export function configureUnixDgram(installed: boolean): void { + unixDgramInstalled = installed; +} + +const UDS_PATH_DEFAULT = "/var/run/datadog/dsd.socket"; + +/** unix-dgram's errnoException: " " (the code is the + * negative errno; this Error's code field holds its decimal string). */ +function errnoException(errorno: number, syscall: string): Error { + const error: NodeJS.ErrnoException = new Error(`${syscall} ${errorno}`); + error.errno = errorno; + error.code = String(errorno); + error.syscall = syscall; + return error; +} + +/** hot-shots' uds transport over unix-dgram: a connected, non-blocking + * AF_UNIX datagram socket. Sends complete synchronously, as the addon's + * do. The addon polls the socket for reads, which holds the event loop + * until close; a ref'd timer stands in for that poll. */ +class UdsTransport implements Transport { + createdAt = 0; + private fd: number; + private readonly errors = new ErrorListeners(); + private readonly closeListeners: (() => void)[] = []; + private readonly stopHold: () => void; + constructor(path: string) { + const fd = scriptcUnixDgramSocket(); + if (fd < 0) throw errnoException(fd, "socket"); + this.fd = fd; + const hold = setInterval(() => {}, 2147483647); + this.stopHold = () => clearInterval(hold); + const err = scriptcUnixDgramConnect(fd, path); + if (err < 0) { + // The addon emits the connect failure as 'error' before anyone + // listens, which throws; hot-shots closes the socket and rethrows. + this.close(); + throw errnoException(err, "connect"); + } + } + send(buf: Buffer, callback: SendCallback): void { + const result = scriptcUnixDgramSend(this.fd, buf); + if (result < 0) { + callback(errnoException(result, "send")); + } else if (result === 1) { + const congestion: NodeJS.ErrnoException = new Error("congestion"); + congestion.code = "1"; + callback(congestion); + } else { + callback(null); + } + } + close(): void { + const result = scriptcUnixDgramClose(this.fd); + if (result < 0) throw errnoException(result, "close"); + this.fd = -1; + this.stopHold(); + for (const listener of this.closeListeners.slice()) listener(); + } + onError(listener: (err: Error) => void): void { + this.errors.add(listener); + } + offError(listener: (err: Error) => void): void { + this.errors.remove(listener); + } + emitError(err: Error): void { + this.errors.emit(err); + } + onClose(listener: () => void): void { + this.closeListeners.push(listener); + } +} + +/** hot-shots' createUdsTransport: without the unix-dgram addon the + * protocol reports it missing. */ +function createUdsTransport(path: string | undefined): Transport { + if (!unixDgramInstalled) { + throw new Error( + "The library `unix_dgram`, needed for the uds protocol to work, is not installed. " + + "You need to pick another protocol to use hot-shots. " + + "See the hot-shots README for additional details.", + ); + } + return new UdsTransport(path ? path : UDS_PATH_DEFAULT); +} + +function createTransport(client: StatsD): Transport | null { + let transport: Transport | null = null; + const protocol = client.protocol; + try { + if (protocol === PROTOCOL_TCP) { + transport = new TcpTransport(client.host, client.port); + } else if (protocol === PROTOCOL_UDS) { + transport = createUdsTransport(client.path); + } else if (protocol === PROTOCOL_UDP) { + transport = new UdpTransport(client.host, client.port, client.cacheDns, client.cacheDnsTtl); + } else { + throw new Error(`Unsupported protocol '${protocol}'`); + } + transport.createdAt = Date.now(); + } catch (e) { + const handler = client.errorHandler; + if (handler) { + handler(e as Error); + } else { + console.error(e); + } + } + return transport; +} + +// ── the client ─────────────────────────────────────────────────────────── + +class BufferHolder { + buffer = ""; +} + +/** What a child client inherits from its parent. */ +class ChildInit { + socket: Transport | null; + bufferHolder: BufferHolder; + dnsError: Error | undefined; + constructor(socket: Transport | null, bufferHolder: BufferHolder, dnsError: Error | undefined) { + this.socket = socket; + this.bufferHolder = bufferHolder; + this.dnsError = dnsError; + } +} + +export class StatsD { + protocol: string; + cacheDns: boolean; + cacheDnsTtl: number; + host: string | undefined; + port: number; + path: string | undefined; + prefix: string; + suffix: string; + tagPrefix: string; + tagSeparator: string; + mock: boolean; + globalTags: string[]; + telegraf: boolean; + maxBufferSize: number; + sampleRate: number; + bufferFlushInterval: number; + errorHandler: ((err: Error) => void) | undefined; + udsGracefulErrorHandling: boolean; + udsGracefulRestartRateLimit: number; + isChild: boolean; + closingFlushInterval: number; + mockBuffer: string[] | undefined = undefined; + messagesInFlight = 0; + CHECKS: DatadogChecks = { OK: 0, WARNING: 1, CRITICAL: 2, UNKNOWN: 3 }; + private bufferHolder: BufferHolder; + private transport: Transport | null = null; + private dnsError: Error | undefined = undefined; + private stopFlushInterval: (() => void) | undefined = undefined; + + constructor(options?: ClientOptions, child?: ChildInit) { + const o: ClientOptions = options ?? {}; + const protocol = o.protocol ? o.protocol.toLowerCase() : ""; + this.protocol = protocol ? protocol : PROTOCOL_UDP; + this.cacheDns = o.cacheDns === true; + this.cacheDnsTtl = o.cacheDnsTtl || CACHE_DNS_TTL_DEFAULT; + this.host = o.host || process.env["DD_AGENT_HOST"]; + this.port = o.port || parseInt(process.env["DD_DOGSTATSD_PORT"] ?? "", 10) || 8125; + this.path = o.path; + this.prefix = o.prefix || ""; + this.suffix = o.suffix || ""; + this.tagPrefix = o.tagPrefix || "#"; + this.tagSeparator = o.tagSeparator || ","; + this.mock = o.mock === true; + this.telegraf = o.telegraf || false; + const globalTags = o.globalTags; + this.globalTags = globalTags !== undefined ? formatTags(globalTags, this.telegraf) : []; + const entityId = process.env["DD_ENTITY_ID"]; + if (entityId) { + this.globalTags = this.globalTags.filter( + (item) => item.indexOf("dd.internal.entity_id:") !== 0, + ); + this.globalTags.push("dd.internal.entity_id:".concat(sanitizeTags(entityId, false))); + } + this.maxBufferSize = o.maxBufferSize || 0; + this.sampleRate = o.sampleRate || 1; + this.bufferFlushInterval = o.bufferFlushInterval || 1000; + this.bufferHolder = child !== undefined ? child.bufferHolder : new BufferHolder(); + this.errorHandler = o.errorHandler; + const graceful = o.udsGracefulErrorHandling; + this.udsGracefulErrorHandling = graceful === undefined ? true : graceful; + this.udsGracefulRestartRateLimit = + o.udsGracefulRestartRateLimit || UDS_DEFAULT_GRACEFUL_RESTART_LIMIT; + this.isChild = child !== undefined; + this.closingFlushInterval = o.closingFlushInterval || 50; + if (this.mock) this.mockBuffer = []; + if (child === undefined && this.maxBufferSize > 0) { + const handle = setInterval(() => this.onBufferFlushInterval(), this.bufferFlushInterval); + handle.unref(); + this.stopFlushInterval = () => clearInterval(handle); + } + if (child !== undefined) { + if (child.dnsError) this.dnsError = child.dnsError; + this.transport = child.socket; + } + if (!this.transport) this.transport = createTransport(this); + const handler = o.errorHandler; + const transport = this.transport; + if (transport !== null && child === undefined && handler) transport.onError(handler); + if (transport !== null && o.protocol === PROTOCOL_UDS) this.maybeAddUDSErrorHandler(); + } + + private maybeAddUDSErrorHandler(): void { + if (this.isChild || !this.udsGracefulErrorHandling) return; + const transport = this.transport; + if (transport !== null) transport.onError((err) => this.udsErrorHandler(err)); + } + + private udsErrorHandler(err: Error): void { + const code = Number((err as NodeJS.ErrnoException).code); + if (!UDS_ERROR_CODES.includes(-code)) return; + const socket = this.transport; + if (socket === null) return; + if (!socket.createdAt || Date.now() - socket.createdAt < this.udsGracefulRestartRateLimit) { + return; + } + const handler = this.errorHandler; + if (handler) socket.offError(handler); + const newSocket = createTransport(this); + if (newSocket === null) { + const errorMessage = "Could not replace UDS connection with new socket"; + if (handler) { + handler(new Error(errorMessage)); + } else { + console.error(errorMessage); + } + return; + } + socket.close(); + this.transport = newSocket; + this.maybeAddUDSErrorHandler(); + if (handler) { + newSocket.onError(handler); + } else { + newSocket.onError((error) => console.error(`hot-shots UDS error: ${String(error)}`)); + } + } + + // ── stats functions ── + + timing( + stat: string | string[], + time: number, + sampleRate?: number, + tags?: Tags, + callback?: StatsCb, + ): void; + timing(stat: string | string[], time: number, tags?: Tags, callback?: StatsCb): void; + timing(stat: string | string[], time: number, callback?: StatsCb): void; + timing(stat: string | string[], time: number, sampleRate?: number, callback?: StatsCb): void; + timing( + stat: string | string[], + time: number, + sampleRate?: SampleArg, + tags?: TagsArg, + callback?: StatsCb, + ): void { + this.sendAll(stat, time, "ms", sampleRate, tags, callback); + } + + increment(stat: string, tags?: Tags): void; + increment( + stat: string | string[], + value: number, + sampleRate?: number, + tags?: Tags, + callback?: StatsCb, + ): void; + increment(stat: string | string[], value: number, tags?: Tags, callback?: StatsCb): void; + increment(stat: string | string[], value: number, callback?: StatsCb): void; + increment(stat: string | string[], value: number, sampleRate?: number, callback?: StatsCb): void; + increment( + stat: string | string[], + value?: number | Tags, + sampleRate?: SampleArg, + tags?: TagsArg, + callback?: StatsCb, + ): void { + // A non-number second argument of a two-argument call is the tags. + let amount = 1; + let tagArg: TagsArg = tags; + if (typeof value === "number") { + amount = value; + } else if (sampleRate === undefined && tags === undefined && callback === undefined) { + tagArg = value; + } + this.sendAll(stat, amount, "c", sampleRate, tagArg, callback); + } + + decrement(stat: string, tags?: Tags): void; + decrement( + stat: string | string[], + value: number, + sampleRate?: number, + tags?: Tags, + callback?: StatsCb, + ): void; + decrement(stat: string | string[], value: number, tags?: Tags, callback?: StatsCb): void; + decrement(stat: string | string[], value: number, callback?: StatsCb): void; + decrement(stat: string | string[], value: number, sampleRate?: number, callback?: StatsCb): void; + decrement( + stat: string | string[], + value?: number | Tags, + sampleRate?: SampleArg, + tags?: TagsArg, + callback?: StatsCb, + ): void { + let amount = 1; + let tagArg: TagsArg = tags; + if (typeof value === "number") { + amount = value; + } else if (sampleRate === undefined && tags === undefined && callback === undefined) { + tagArg = value; + } + this.sendAll(stat, -amount, "c", sampleRate, tagArg, callback); + } + + histogram( + stat: string | string[], + value: number, + sampleRate?: number, + tags?: Tags, + callback?: StatsCb, + ): void; + histogram(stat: string | string[], value: number, tags?: Tags, callback?: StatsCb): void; + histogram(stat: string | string[], value: number, callback?: StatsCb): void; + histogram(stat: string | string[], value: number, sampleRate?: number, callback?: StatsCb): void; + histogram( + stat: string | string[], + value: number, + sampleRate?: SampleArg, + tags?: TagsArg, + callback?: StatsCb, + ): void { + this.sendAll(stat, value, "h", sampleRate, tags, callback); + } + + distribution( + stat: string | string[], + value: number, + sampleRate?: number, + tags?: Tags, + callback?: StatsCb, + ): void; + distribution(stat: string | string[], value: number, tags?: Tags, callback?: StatsCb): void; + distribution(stat: string | string[], value: number, callback?: StatsCb): void; + distribution( + stat: string | string[], + value: number, + sampleRate?: number, + callback?: StatsCb, + ): void; + distribution( + stat: string | string[], + value: number, + sampleRate?: SampleArg, + tags?: TagsArg, + callback?: StatsCb, + ): void { + this.sendAll(stat, value, "d", sampleRate, tags, callback); + } + + gauge( + stat: string | string[], + value: number, + sampleRate?: number, + tags?: Tags, + callback?: StatsCb, + ): void; + gauge(stat: string | string[], value: number, tags?: Tags, callback?: StatsCb): void; + gauge(stat: string | string[], value: number, callback?: StatsCb): void; + gauge(stat: string | string[], value: number, sampleRate?: number, callback?: StatsCb): void; + gauge( + stat: string | string[], + value: number, + sampleRate?: SampleArg, + tags?: TagsArg, + callback?: StatsCb, + ): void { + this.sendAll(stat, value, "g", sampleRate, tags, callback); + } + + set( + stat: string | string[], + value: number | string, + sampleRate?: number, + tags?: Tags, + callback?: StatsCb, + ): void; + set(stat: string | string[], value: number | string, tags?: Tags, callback?: StatsCb): void; + set(stat: string | string[], value: number | string, callback?: StatsCb): void; + set( + stat: string | string[], + value: number | string, + sampleRate?: number, + callback?: StatsCb, + ): void; + set( + stat: string | string[], + value: number | string, + sampleRate?: SampleArg, + tags?: TagsArg, + callback?: StatsCb, + ): void { + this.sendAll(stat, value, "s", sampleRate, tags, callback); + } + + unique( + stat: string | string[], + value: number | string, + sampleRate?: number, + tags?: Tags, + callback?: StatsCb, + ): void; + unique(stat: string | string[], value: number | string, tags?: Tags, callback?: StatsCb): void; + unique(stat: string | string[], value: number | string, callback?: StatsCb): void; + unique( + stat: string | string[], + value: number | string, + sampleRate?: number, + callback?: StatsCb, + ): void; + unique( + stat: string | string[], + value: number | string, + sampleRate?: SampleArg, + tags?: TagsArg, + callback?: StatsCb, + ): void { + this.sendAll(stat, value, "s", sampleRate, tags, callback); + } + + check( + name: string, + status: DatadogChecksValues, + options?: CheckOptions, + tags?: Tags | StatsCb, + callback?: StatsCb, + ): void { + if (this.telegraf) { + const err = new Error("Not supported by Telegraf / InfluxDB"); + if (callback) return callback(err); + const handler = this.errorHandler; + if (handler) return handler(err); + throw err; + } + const check: string[] = ["_sc", this.prefix + name + this.suffix, String(status)]; + const metadata: CheckOptions = options ?? {}; + const happened = metadata.date_happened; + if (happened) { + const timestamp = formatDate(happened); + if (timestamp) check.push(`d:${timestamp}`); + } + if (metadata.hostname) check.push(`h:${metadata.hostname}`); + let mergedTags = this.globalTags; + if (tags !== undefined && typeof tags !== "function") { + mergedTags = overrideTags(mergedTags, tags, this.telegraf); + } + if (mergedTags.length > 0) check.push(`#${mergedTags.join(",")}`); + if (metadata.message) check.push(`m:${metadata.message}`); + let cb = callback; + if (typeof tags === "function" && callback === undefined) cb = tags; + this.sendRaw(check.join("|"), cb); + } + + event( + title: string, + text?: string, + options?: EventOptions, + tags?: Tags | StatsCb, + callback?: StatsCb, + ): void { + if (this.telegraf) { + const err = new Error("Not supported by Telegraf / InfluxDB"); + if (callback) return callback(err); + const handler = this.errorHandler; + if (handler) return handler(err); + throw err; + } + const msgTitle = String(title ? title : ""); + let msgText = String(text ? text : msgTitle); + msgText = msgText.replace(/\n/g, "\\n"); + let message = `_e{${msgTitle.length},${msgText.length}}:${msgTitle}|${msgText}`; + if (options) { + const happened = options.date_happened; + if (happened) { + const timestamp = formatDate(happened); + if (timestamp) message += `|d:${timestamp}`; + } + if (options.hostname) message += `|h:${options.hostname}`; + if (options.aggregation_key) message += `|k:${options.aggregation_key}`; + if (options.priority) message += `|p:${options.priority}`; + if (options.source_type_name) message += `|s:${options.source_type_name}`; + if (options.alert_type) message += `|t:${options.alert_type}`; + } + let cb = callback; + let tagList: Tags | undefined = undefined; + if (typeof tags === "function") { + if (callback === undefined) cb = tags; + } else { + tagList = tags; + } + this.send(message, tagList, cb); + } + + // ── sending ── + + private sendAll( + stat: string | string[], + value: number | string, + type: string, + sampleRateArg: SampleArg, + tagsArg: TagsArg, + callbackArg: StatsCb | undefined, + ): void { + let sampleRate: number | undefined = undefined; + let tags: Tags | undefined = undefined; + let callback: StatsCb | undefined = callbackArg; + if (typeof sampleRateArg === "number") { + sampleRate = sampleRateArg; + if (typeof tagsArg === "function") { + callback = tagsArg; + } else { + tags = tagsArg; + } + } else if (sampleRateArg !== undefined) { + // A non-number sample rate shifts into the tags, and the tags + // argument into the callback. + if (typeof sampleRateArg === "function") { + callback = sampleRateArg; + } else { + tags = sampleRateArg; + callback = typeof tagsArg === "function" ? tagsArg : undefined; + } + } else if (typeof tagsArg === "function") { + callback = tagsArg; + } else { + tags = tagsArg; + } + + if (!Array.isArray(stat)) { + this.sendStat(stat, value, type, sampleRate, tags, callback); + return; + } + let completed = 0; + let calledback = false; + let sentBytes = 0; + const count = stat.length; + const onSend: StatsCb = (error, bytes) => { + completed += 1; + if (calledback) return; + if (error) { + if (callback !== undefined) { + calledback = true; + callback(error); + } else { + const handler = this.errorHandler; + if (handler) { + calledback = true; + handler(error); + } + } + return; + } + if (bytes) sentBytes += bytes as number; + if (completed === count && callback !== undefined) callback(null, sentBytes); + }; + for (const item of stat) this.sendStat(item, value, type, sampleRate, tags, onSend); + } + + private sendStat( + stat: string, + value: number | string, + type: string, + sampleRateArg: number | undefined, + tags: Tags | undefined, + callback: StatsCb | undefined, + ): void { + let message = `${this.prefix + stat + this.suffix}:${value}|${type}`; + const sampleRate = sampleRateArg || this.sampleRate; + if (sampleRate && sampleRate < 1) { + if (Math.random() < sampleRate) { + message += `|@${sampleRate}`; + } else { + if (callback) callback(undefined); + return; + } + } + this.send(message, tags, callback); + } + + private send(message: string, tags: Tags | undefined, callback: StatsCb | undefined): void { + let mergedTags = this.globalTags; + if (tags !== undefined) mergedTags = overrideTags(mergedTags, tags, this.telegraf); + if (mergedTags.length > 0) { + if (this.telegraf) { + const parts = message.split(":"); + message = `${parts[0]!},${mergedTags.join(",").replace(/:/g, "=")}:${parts.slice(1).join(":")}`; + } else { + message += `|${this.tagPrefix}${mergedTags.join(this.tagSeparator)}`; + } + } + this.sendRaw(message, callback); + } + + /** hot-shots' `_send`: the message with its tags. */ + private sendRaw(message: string, callback: StatsCb | undefined): void { + const dnsError = this.dnsError; + if (dnsError) { + if (callback) return callback(dnsError); + const handler = this.errorHandler; + if (handler) return handler(dnsError); + throw dnsError; + } + if (!this.mock) { + if (this.maxBufferSize === 0) { + this.sendMessage(message, callback); + } else { + this.enqueue(message, callback); + } + } else { + const buffer = this.mockBuffer; + if (buffer !== undefined) buffer.push(message); + if (callback) callback(null, 0); + } + } + + private enqueue(message: string, callback: StatsCb | undefined): void { + message += "\n"; + if (this.bufferHolder.buffer.length + message.length > this.maxBufferSize) { + this.flushQueue(callback); + this.bufferHolder.buffer += message; + } else { + this.bufferHolder.buffer += message; + if (callback) callback(null); + } + } + + private flushQueue(callback?: StatsCb): void { + this.sendMessage(this.bufferHolder.buffer, callback); + this.bufferHolder.buffer = ""; + } + + private sendMessage(message: string, callback: StatsCb | undefined): void { + if (message === "") { + if (callback) callback(null); + return; + } + const socketWasMissing = this.transport === null; + if (socketWasMissing && this.protocol === PROTOCOL_UDS) { + this.transport = createTransport(this); + if (this.transport !== null) this.maybeAddUDSErrorHandler(); + } + const socket = this.transport; + if (socketWasMissing || socket === null) { + const error = new Error("Socket not created properly. Check previous errors for details."); + if (callback) return callback(error); + const handler = this.errorHandler; + if (handler) return handler(error); + console.error(String(error)); + return; + } + const handleCallback = (err: Error | null, bytes?: number): void => { + this.messagesInFlight--; + let errFormatted: Error | null = null; + if (err) { + errFormatted = errorWithCode( + `Error sending hot-shots message: ${String(err)}`, + (err as NodeJS.ErrnoException).code, + ); + if (this.protocol === PROTOCOL_UDS && (callback || this.errorHandler)) { + this.udsErrorHandler(err); + } + } + if (callback) { + callback(errFormatted, bytes); + } else if (errFormatted) { + const handler = this.errorHandler; + if (handler) { + handler(errFormatted); + } else { + console.error(String(errFormatted)); + // hot-shots also emits the error on the current socket, where the + // uds reconnection handler listens; with no listener it throws. + const current = this.transport; + if (current !== null) current.emitError(errFormatted); + } + } + }; + try { + this.messagesInFlight++; + socket.send(Buffer.from(message), handleCallback); + } catch (err) { + handleCallback(err as Error); + } + } + + private onBufferFlushInterval(): void { + this.flushQueue(); + } + + close(callback?: (error?: Error) => void): void { + const stop = this.stopFlushInterval; + if (stop !== undefined) stop(); + this.flushQueue((err) => { + if (err) { + if (callback) return callback(err); + logError(err); + return; + } + let intervalAttempts = 0; + const waitForMessages = setInterval(() => { + intervalAttempts++; + if (intervalAttempts > 10) { + console.log("hot-shots could not clear out messages in flight but closing anyways"); + this.messagesInFlight = 0; + } + if (this.messagesInFlight <= 0) { + clearInterval(waitForMessages); + this.closeSocket(callback); + } + }, this.closingFlushInterval); + }); + } + + private closeSocket(callback: ((error?: Error) => void) | undefined): void { + let handledError = false; + const handleErr = (err: unknown): void => { + const errMessage = `Error closing hot-shots socket: ${String(err)}`; + if (handledError) { + console.error(errMessage); + } else { + handledError = true; + if (callback) { + callback(new Error(errMessage)); + } else { + const handler = this.errorHandler; + if (handler) { + handler(new Error(errMessage)); + } else { + console.error(errMessage); + } + } + } + }; + const socket = this.transport; + if (socket === null) { + handleErr(new TypeError("Cannot read properties of null (reading 'removeListener')")); + return; + } + const handler = this.errorHandler; + if (handler) socket.offError(handler); + socket.onError(handleErr); + if (callback) { + socket.onClose(() => { + if (!handledError) callback(undefined); + }); + } + try { + socket.close(); + } catch (err) { + handleErr(err); + } + } + + childClient(options?: ChildClientOptions): StatsD { + const o: ChildClientOptions = options ?? {}; + const childTags = o.globalTags; + return new StatsD( + { + isChild: true, + errorHandler: o.errorHandler || this.errorHandler, + host: this.host, + port: this.port, + tagPrefix: this.tagPrefix, + tagSeparator: this.tagSeparator, + prefix: (o.prefix || "") + this.prefix, + suffix: this.suffix + (o.suffix || ""), + globalize: false, + mock: this.mock, + globalTags: + childTags !== undefined + ? overrideTags(this.globalTags, childTags, this.telegraf) + : this.globalTags, + maxBufferSize: this.maxBufferSize, + bufferFlushInterval: this.bufferFlushInterval, + telegraf: this.telegraf, + protocol: this.protocol === "tcp" ? "tcp" : this.protocol === "uds" ? "uds" : "udp", + closingFlushInterval: this.closingFlushInterval, + }, + new ChildInit(this.transport, this.bufferHolder, this.dnsError), + ); + } +} + +export default StatsD; diff --git a/packages/compiler/shims/hot-shots/v8-unix-dgram.ts b/packages/compiler/shims/hot-shots/v8-unix-dgram.ts new file mode 100644 index 000000000..f4bfb180e --- /dev/null +++ b/packages/compiler/shims/hot-shots/v8-unix-dgram.ts @@ -0,0 +1,8 @@ +// The hot-shots 8.x shim for installations with the optional unix-dgram +// addon: the uds protocol sends through a native Unix datagram socket. +import { configureUnixDgram } from "./client.ts"; + +configureUnixDgram(true); + +export * from "./client.ts"; +export { default } from "./client.ts"; diff --git a/packages/compiler/shims/hot-shots/v8.ts b/packages/compiler/shims/hot-shots/v8.ts new file mode 100644 index 000000000..0d1871170 --- /dev/null +++ b/packages/compiler/shims/hot-shots/v8.ts @@ -0,0 +1,8 @@ +// The hot-shots 8.x shim for installations without the optional unix-dgram +// addon: the uds protocol reports it missing, as hot-shots does under Node. +import { configureUnixDgram } from "./client.ts"; + +configureUnixDgram(false); + +export * from "./client.ts"; +export { default } from "./client.ts"; diff --git a/packages/compiler/src/backend/cycle-analysis.test.ts b/packages/compiler/src/backend/cycle-analysis.test.ts index c78420f73..340ffa8f9 100644 --- a/packages/compiler/src/backend/cycle-analysis.test.ts +++ b/packages/compiler/src/backend/cycle-analysis.test.ts @@ -17,6 +17,7 @@ import { } from "../ir/ir.js"; import { computeTraced, explainCycle } from "./cycle-analysis.js"; import { computeTraced as llvmTraced } from "./llvm/shapes.js"; +import { addRecordResultEntries } from "./llvm/record-results.js"; const loc = { file: "cycles.ts", start: 0, end: 1 }; function module(): IrModule { @@ -404,6 +405,61 @@ test("a later write through any alias makes the edge mutable", () => { checkExact(record, ["record:list"]); }); +test("a result-slot entry's stores into its destination are not writes", () => { + const next: IrType = { kind: "union", unionId: "next" }; + const listModule = (): IrModule => { + const mod = module(); + mod.records = [shape("list", [next, F64])]; + mod.unions = [{ id: "next", arms: [ref("list"), { kind: "nullT" }] }]; + mod.functions.push({ + name: "cell", + params: [{ localId: "rest", name: "rest", type: next }], + locals: [], + returnType: ref("list"), + body: [ + { + kind: "return", + value: { + kind: "recordLit", + fields: [ + { name: "field0", value: { kind: "varRef", localId: "rest", type: next, loc } }, + { name: "field1", value: { kind: "numLit", value: 1, type: F64, loc } }, + ], + type: ref("list"), + loc, + }, + loc, + }, + ], + loc, + }); + return addRecordResultEntries(mod); + }; + // The entry stores the literal's fields into caller frame storage that + // nothing can point to: the list stays acyclic. + const entries = listModule(); + expect(entries.functions.map((fn) => fn.name)).toContain("%into.cell"); + checkExact(entries, []); + // The same store into the destination's local id outside an entry, and a + // store into any other binding of the shape inside one, are writes. + const store = (localId: string): IrStmt => ({ + kind: "recordSet", + obj: { kind: "varRef", localId, type: ref("list"), loc }, + shapeId: "list", + field: "field0", + value: value(next), + loc, + }); + for (const [fn, localId] of [ + ["main", "%into.dst"], + ["%into.cell", "v"], + ] as const) { + const written = listModule(); + written.functions.find((f) => f.name === fn)!.body.unshift(store(localId)); + checkExact(written, ["record:list"], ["next"]); + } +}); + test("field increments count as writes", () => { const mod = tree(); mod.functions[0]!.body.push({ diff --git a/packages/compiler/src/backend/cycle-analysis.ts b/packages/compiler/src/backend/cycle-analysis.ts index aa624579f..fbd96e854 100644 --- a/packages/compiler/src/backend/cycle-analysis.ts +++ b/packages/compiler/src/backend/cycle-analysis.ts @@ -11,7 +11,7 @@ import { STRING, VOID, } from "../ir/ir.js"; -import { computeCycleMutability, fieldKey } from "./cycle-mutability.js"; +import { type CycleMutability, computeCycleMutability, fieldKey } from "./cycle-mutability.js"; /** Cycle capability for native code generation. * Nodes are shape units and unions; edges are the references their fields @@ -38,8 +38,10 @@ import { computeCycleMutability, fieldKey } from "./cycle-mutability.js"; export function computeTraced( mod: IrModule, stores?: DynamicFieldStores, + /** computeCycleMutability's answer for this module, when the caller has it. */ + precomputed?: CycleMutability, ): { shapes: Set; unions: Set } { - const { shapes, unions } = analyzeCycles(mod, false, stores); + const { shapes, unions } = analyzeCycles(mod, false, stores, precomputed); return { shapes, unions }; } @@ -93,6 +95,7 @@ function analyzeCycles( mod: IrModule, labels: boolean, stores?: DynamicFieldStores, + precomputed?: CycleMutability, ): { shapes: Set; unions: Set; @@ -125,7 +128,7 @@ function analyzeCycles( const lifetimeRoots = new Set(mod.contracts?.programLifetime ?? []); const programLifetime = (className: string): boolean => lifetimeRoots.size > 0 && lifetimeRoots.has(rootOf(className)); - const mutability = computeCycleMutability(mod, unitOfClass, stores); + const mutability = precomputed ?? computeCycleMutability(mod, unitOfClass, stores); const shapeDefs = [ ...classes.map((c) => ({ key: `object:${c.name}`, diff --git a/packages/compiler/src/backend/cycle-mutability.ts b/packages/compiler/src/backend/cycle-mutability.ts index 78daa6204..541a1312f 100644 --- a/packages/compiler/src/backend/cycle-mutability.ts +++ b/packages/compiler/src/backend/cycle-mutability.ts @@ -2,6 +2,7 @@ import type { IrExpr, IrFunction, IrModule, IrStmt, IrType } from "../ir/ir.js"; import { RUNTIME_EMITTER_CLASS, RUNTIME_STREAM_CLASSES, typeKey } from "../ir/ir.js"; import { dynamicFieldStores, type DynamicFieldStores } from "../ir/int32-slots.js"; import { everyExprChild, everyStmtChild, everyTypeChild } from "../ir/traverse.js"; +import { resultSlotStore } from "./result-slots.js"; /** Which shape fields can be written after their object escapes. * @@ -17,6 +18,10 @@ import { everyExprChild, everyStmtChild, everyTypeChild } from "../ir/traverse.j * - every `fieldSet` outside a constructor's own `this`, `fieldIncDec`, and * `recordSet` makes its (unit, field) mutable; `recordKeySet` on declared * fields and `recordKeyDelete` make the whole record mutable; + * - a result-slot entry's stores into its destination (result-slots.ts) are + * not writes: the destination is frame storage of a caller that only + * projects the value, so no object can point to it and its fields cannot + * lie on a cycle; * - a constructor's stores into its own `this` are construction-only up to * the first top-level statement in which `this` escapes (any use other * than a field store target, a field read, or the receiver of the direct @@ -69,6 +74,22 @@ export function fieldKey(unit: string, field: string): string { return `${unit}\0${field}`; } +/** The cycle unit of a class: `object:`, the root of its hierarchy, + * as computeImmutableFields and computeTraced group classes. */ +export function hierarchyUnitOfClass(mod: IrModule): (className: string) => string { + const baseOf = new Map((mod.classes ?? []).map((c) => [c.name, c.base ?? null] as const)); + const roots = new Map(); + return (name) => { + const known = roots.get(name); + if (known !== undefined) return `object:${known}`; + let cur = name; + for (let base = baseOf.get(cur); base !== null && base !== undefined; base = baseOf.get(cur)) + cur = base; + roots.set(name, cur); + return `object:${cur}`; + }; +} + export function computeCycleMutability( mod: IrModule, unitOfClass: (className: string) => string, @@ -296,7 +317,8 @@ export function computeCycleMutability( result.fields.add(fieldKey(unitOfClass(s.className), s.field)); break; case "recordSet": - result.fields.add(fieldKey(`record:${s.shapeId}`, s.field)); + if (!resultSlotStore(fn.name, s)) + result.fields.add(fieldKey(`record:${s.shapeId}`, s.field)); break; case "recordKeySet": if (s.overflowOnly !== true) result.units.add(`record:${s.shapeId}`); diff --git a/packages/compiler/src/backend/executable-features.ts b/packages/compiler/src/backend/executable-features.ts index 672fddd96..32318d53e 100644 --- a/packages/compiler/src/backend/executable-features.ts +++ b/packages/compiler/src/backend/executable-features.ts @@ -28,6 +28,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures { ...(features.dynTier ? { dynTier: true } : {}), ...(features.streamDyn ? { streamDyn: true } : {}), ...(features.handleProps ? { handleProps: true } : {}), + ...(features.declAdopt ? { declAdopt: true } : {}), ...(features.dynForAwait ? { dynForAwait: true } : {}), ...(features.eventsOnceSignal ? { eventsOnceSignal: true } : {}), ...(features.dynBuffer ? { dynBuffer: true } : {}), diff --git a/packages/compiler/src/backend/immutability.ts b/packages/compiler/src/backend/immutability.ts index 6c37731e3..21380de8f 100644 --- a/packages/compiler/src/backend/immutability.ts +++ b/packages/compiler/src/backend/immutability.ts @@ -48,6 +48,8 @@ export class ImmutableFields { export function computeImmutableFields( mod: IrModule, stores?: DynamicFieldStores, + /** computeCycleMutability's answer for this module, when the caller has it. */ + precomputed?: CycleMutability, ): ImmutableFields | null { if (mod.lib !== undefined) return null; const baseOf = new Map((mod.classes ?? []).map((c) => [c.name, c.base ?? null] as const)); @@ -62,7 +64,7 @@ export function computeImmutableFields( return cur; }; const unitOfClass = (name: string): string => `object:${rootOf(name)}`; - const mutability = computeCycleMutability(mod, unitOfClass, stores); + const mutability = precomputed ?? computeCycleMutability(mod, unitOfClass, stores); return mutability.all ? null : new ImmutableFields(mutability, unitOfClass); } @@ -213,6 +215,9 @@ export function stableGlobals(mod: IrModule): Set { } return everyStmtChild(s, expr, stmt); }; - for (const fn of mod.functions) fn.body.every(stmt); + for (const fn of mod.functions) { + if (candidates.size === 0) break; + fn.body.every(stmt); + } return candidates; } diff --git a/packages/compiler/src/backend/llvm/appended-array-literals.test.ts b/packages/compiler/src/backend/llvm/appended-array-literals.test.ts index c483b7848..11a9704f6 100644 --- a/packages/compiler/src/backend/llvm/appended-array-literals.test.ts +++ b/packages/compiler/src/backend/llvm/appended-array-literals.test.ts @@ -57,10 +57,11 @@ test("only empty literals stored where the module appends construct small", () = expect(found.has(idle)).toBe(false); expect(found.has(returned)).toBe(false); - // Only `fill` builds arrays. + // Only `fill` builds arrays. They may hold int32 slots (element kind 7), + // which does not change the choice of constructor. const llvm = emitLlvmModule(module(body, ["xs", "ys"])); - expect(llvm.match(/call ptr @scr_arr_new_small\(i32 0\)/g)).toHaveLength(2); - expect(llvm.match(/call ptr @scr_arr_new\(i32 0, i64 0\)/g)).toHaveLength(2); + expect(llvm.match(/call ptr @scr_arr_new_small\(i32 [07]\)/g)).toHaveLength(2); + expect(llvm.match(/call ptr @scr_arr_new\(i32 [07], i64 0\)/g)).toHaveLength(2); }); test("a binding with the same id in another function does not count", () => { diff --git a/packages/compiler/src/backend/llvm/appended-array-literals.ts b/packages/compiler/src/backend/llvm/appended-array-literals.ts index 1d4577ae6..09a2976de 100644 --- a/packages/compiler/src/backend/llvm/appended-array-literals.ts +++ b/packages/compiler/src/backend/llvm/appended-array-literals.ts @@ -85,6 +85,7 @@ export function findAppendedArrayLiterals(module: IrModule): ReadonlySet for (const field of fieldAliases.get(binding) ?? []) appendedFields.add(field); const result = new Set(); + if (appendedBindings.size === 0 && appendedFields.size === 0) return result; const consider = (value: IrExpr | null, appended: boolean): void => { if (value && appended && isEmptyLiteral(value)) result.add(value); }; diff --git a/packages/compiler/src/backend/llvm/array-search.ts b/packages/compiler/src/backend/llvm/array-search.ts index d61d91e5c..41b64d418 100644 --- a/packages/compiler/src/backend/llvm/array-search.ts +++ b/packages/compiler/src/backend/llvm/array-search.ts @@ -10,6 +10,8 @@ * SameValueZero are both pointer identity (class instances, records, * arrays, maps, sets, typed arrays): 64-bit compares, four slots per * branch. + * - sc_arr_index_of_cref, the same over the 4-byte slots of compact class + * references (SCR_ELEM_CREF), against the needle's 32-bit reference. * - sc_arr_index_of_str, for strings: pointer identity, then the length, * then the bytes. Strings of up to 16 bytes compare as two words; a short * string's word is the eight bytes ending at its last byte, which start @@ -26,10 +28,12 @@ import type { IrType } from "../../ir/ir.js"; import { SCR_ARR_FIELD } from "./common.js"; import { FN_ATTRS } from "./shapes.js"; -export type ArraySearchKind = "ptr" | "str"; +export type ArraySearchKind = "ptr" | "cref" | "str"; -/** The inline search for arrays of `elem`, or null for the runtime search. */ -export function arraySearchKind(elem: IrType): ArraySearchKind | null { +/** The inline search for arrays of `elem`, or null for the runtime search. + * `compact`: the arrays hold compact class references (SCR_ELEM_CREF). */ +export function arraySearchKind(elem: IrType, compact: boolean): ArraySearchKind | null { + if (compact) return elem.kind === "object" ? "cref" : null; switch (elem.kind) { case "object": case "record": @@ -46,7 +50,7 @@ export function arraySearchKind(elem: IrType): ArraySearchKind | null { } export function arraySearchHelperName(kind: ArraySearchKind): string { - return kind === "ptr" ? "sc_arr_index_of_ptr" : "sc_arr_index_of_str"; + return `sc_arr_index_of_${kind}`; } /** The definitions of the helpers in `kinds`; they declare their slow paths @@ -76,9 +80,18 @@ export function emitArraySearchHelpers( ` %dp = getelementptr inbounds %ScrArr, ptr %a, i32 0, i32 ${SCR_ARR_FIELD.data}`, ` %data = load ptr, ptr %dp`, ); - if (kind === "ptr") { + if (kind === "ptr" || kind === "cref") { + // A compact reference is the address over 8 (classes.ts + // compactRefLoadLines). + const slot = kind === "ptr" ? "i64" : "i32"; defs.push( - ` %needle = ptrtoint ptr %v to i64`, + ...(kind === "ptr" + ? [` %needle = ptrtoint ptr %v to i64`] + : [ + ` %vaddr = ptrtoint ptr %v to i64`, + ` %vref = lshr i64 %vaddr, 3`, + ` %needle = trunc i64 %vref to i32`, + ]), // Four slots per step with one branch, then one at a time. ` br label %quad`, `quad:`, @@ -89,9 +102,9 @@ export function emitArraySearchHelpers( `quadbody:`, ...[0, 1, 2, 3].flatMap((k) => [ ` %qi${k} = add ${sz} %q, ${k}`, - ` %qp${k} = getelementptr inbounds i64, ptr %data, ${sz} %qi${k}`, - ` %qs${k} = load i64, ptr %qp${k}`, - ` %qh${k} = icmp eq i64 %qs${k}, %needle`, + ` %qp${k} = getelementptr inbounds ${slot}, ptr %data, ${sz} %qi${k}`, + ` %qs${k} = load ${slot}, ptr %qp${k}`, + ` %qh${k} = icmp eq ${slot} %qs${k}, %needle`, ]), ` %qh01 = or i1 %qh0, %qh1`, ` %qh23 = or i1 %qh2, %qh3`, @@ -107,9 +120,9 @@ export function emitArraySearchHelpers( ` %done = icmp eq ${sz} %i, %len`, ` br i1 %done, label %miss, label %body`, `body:`, - ` %sp = getelementptr inbounds i64, ptr %data, ${sz} %i`, - ` %s = load i64, ptr %sp`, - ` %hit = icmp eq i64 %s, %needle`, + ` %sp = getelementptr inbounds ${slot}, ptr %data, ${sz} %i`, + ` %s = load ${slot}, ptr %sp`, + ` %hit = icmp eq ${slot} %s, %needle`, ` br i1 %hit, label %found, label %cont`, ); } else { diff --git a/packages/compiler/src/backend/llvm/blocks.ts b/packages/compiler/src/backend/llvm/blocks.ts index 6d22bf3bf..354e631c6 100644 --- a/packages/compiler/src/backend/llvm/blocks.ts +++ b/packages/compiler/src/backend/llvm/blocks.ts @@ -95,8 +95,8 @@ export class BlockBuilder { this.terminate(`br label %${label}`); } - condBr(v: string, t: string, f: string): void { - this.terminate(`br i1 ${v}, label %${t}, label %${f}`); + condBr(v: string, t: string, f: string, attachment = ""): void { + this.terminate(`br i1 ${v}, label %${t}, label %${f}${attachment}`); } /** Emit a double-indexed `for (i = 0; i < len; i++)` loop. `next` is diff --git a/packages/compiler/src/backend/llvm/byte-windows.ts b/packages/compiler/src/backend/llvm/byte-windows.ts index 5fffa1428..9741cc547 100644 --- a/packages/compiler/src/backend/llvm/byte-windows.ts +++ b/packages/compiler/src/backend/llvm/byte-windows.ts @@ -43,6 +43,7 @@ export function findInitializedByteLoopBindings( if (stmt.init) list([stmt.init], inner); list(stmt.body, inner); } + if (stmt.kind === "while") entries.set(stmt, new Set(known)); if (stmt.kind === "while" || stmt.kind === "doWhile") list(stmt.body, new Set(known)); } } diff --git a/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts b/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts index ace8f294c..cee91c66d 100644 --- a/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts @@ -99,6 +99,7 @@ function match( f: IrFunction, loop: IrStmt & { kind: "for" }, captured: string[] = [], + scalarStrings: string[] = [], ): BytesLoopVersion | null { const ranges = analyzeIntegerRanges(f); const locals = new Map(f.locals.map((l) => [l.id, l])); @@ -111,6 +112,7 @@ function match( proven: new Set(), workers: false, owned: new Set(), + scalarStrings: new Set(scalarStrings), }); } @@ -177,6 +179,11 @@ test("statically ranged indices compare one constant with the length", () => { ]); expect(version.affine).toEqual([]); expect(version.counted).toBeNull(); + // A uint32 index (an array counter's range) gets no guard: no typed array + // in practice has 2^31 elements. + const wide = bin(">>>", bin("^", ref("i"), ref("k")), num(0)); + const unbounded = counted([set(ref("out", f64s), num(0), get(ref("table", i32s), wide))]); + expect(match(fn([unbounded]), unbounded)!.fixed).toEqual([{ receiver: 0, end: 0 }]); }); test("scaled counters and body temps resolve to their receiver and index", () => { @@ -358,6 +365,64 @@ test("index temps of a version's counter are integers in the fast copy", () => { expect(fast).not.toContain("bytes.index"); }); +test("while loop counters below a bound version their accesses", () => { + // while (i < n) { out[i] = src[i + 1]; i = i + 1 } + const step: IrStmt = { kind: "assign", localId: "i", value: bin("+", ref("i"), num(1)), loc }; + const whileLoop = (body: IrStmt[]): IrStmt & { kind: "while" } => ({ + kind: "while", + cond: { kind: "bin", op: "<", left: ref("i"), right: ref("n"), type: BOOL, loc }, + body, + loc, + }); + const copy = whileLoop([ + set(ref("out", f64s), ref("i"), get(ref("src", f64s), bin("+", ref("i"), num(1)))), + step, + ]); + const declared = (loop: IrStmt): IrFunction => + fn([{ kind: "varDecl", localId: "i", init: ref("k"), loc }, loop]); + const versionOf = (loop: IrStmt & { kind: "while" }): BytesLoopVersion | null => { + const f = declared(loop); + const ranges = analyzeIntegerRanges(f); + return matchBytesLoopVersion(loop, null, { + locals: new Map(f.locals.map((l) => [l.id, l])), + invariantGlobals: new Set(), + captured: new Set(), + ranges, + initialized: findInitializedByteLoopBindings(f).get(loop) ?? new Set(), + proven: new Set(), + scalarStrings: new Set(), + workers: false, + owned: new Set(), + }); + }; + const version = versionOf(copy)!; + expect(version.bounds.size).toBe(2); + expect(version.bounded.map((c) => [version.receivers[c.receiver]!.ref, c.slack])).toEqual([ + [ref("out", f64s), 0], + [ref("src", f64s), 1], + ]); + // `out[i]` needs `i` to start at 0 or above (`src[i + 1]` only at -1). + expect(version.counters.map((c) => [c.ref.kind === "varRef" && c.ref.localId, c.min])).toEqual([ + ["i", 0], + ]); + // An access after the counter advances in the same pass stays checked. + const advanced = whileLoop([step, set(ref("out", f64s), ref("i"), num(0))]); + expect(versionOf(advanced)).toBeNull(); + const mod: IrModule = { + irVersion: 17, + sourceFile: loc.file, + entry: "__main", + functions: [ + { name: "__main", params: [], returnType: VOID, locals: [], body: [], loc }, + declared(copy), + ], + }; + const body = /^define internal [^\n]*@sc_bf_kernel[^]*?^}/m.exec(emitLlvmModule(mod))![0]; + const fast = /^bytes\.version\.fast\d+:$[^]*?^bytes\.version\.done\d+:$/m.exec(body)![0]; + expect(fast).not.toContain("bytes.index"); + expect(body.slice(0, body.indexOf(fast))).toContain("bytes.index.invalid"); +}); + test("Float16Array accesses version in elements and share the element tag", () => { const f16s: IrType = { kind: "bytes", elem: "f16" }; const withHalves = (body: IrStmt[]): IrFunction => { @@ -425,3 +490,131 @@ test("bodies compiled without optimization keep one checked loop", () => { expect(body).not.toContain("bytes.version"); expect(body).toContain("bytes.index.invalid"); }); + +const STR: IrType = { kind: "string" }; +const charCode = (receiver: IrExpr, index: IrExpr): IrExpr => ({ + kind: "strIntrinsic", + method: "charCodeAt", + receiver, + args: [index], + type: F64, + loc, +}); + +/** The kernel with a string parameter `text`. */ +function textKernel(body: IrStmt[], mutable = false): IrFunction { + const kernel = fn(body, [local("text", STR, mutable)]); + kernel.params.push({ localId: "text", name: "text", type: STR }); + return kernel; +} + +test("charCodeAt reads of a string binding version with the typed-array reads", () => { + const read = charCode(ref("text", STR), bin("+", ref("i"), num(1))); + const loop = counted([set(ref("out", f64s), ref("i"), read)]); + const version = match(textKernel([loop]), loop)!; + expect(version.bounds.has(read)).toBe(true); + expect( + version.receivers.map((r) => [r.ref.kind === "varRef" && r.ref.localId, r.string]), + ).toEqual([ + ["out", false], + ["text", true], + ]); + expect(version.affine.map((c) => [c.receiver, c.minOffset, c.maxEnd])).toEqual([ + [0, 0, 0], + [1, 1, 1], + ]); + // A string the loop rebinds keeps its checked reads. + const rebinding = counted([ + set(ref("out", f64s), ref("i"), charCode(ref("text", STR), ref("i"))), + { kind: "assign", localId: "text", value: ref("text", STR), loc }, + ]); + const rebound = match(textKernel([rebinding], true), rebinding)!; + expect(rebound.receivers.map((r) => r.string)).toEqual([false]); + // A string held as a slice of another string has no header to test. + const sliced = match(textKernel([loop]), loop, [], ["text"])!; + expect(sliced.receivers.map((r) => r.string)).toEqual([false]); + expect(sliced.bounds.has(read)).toBe(false); +}); + +test("charAt reads of a string binding version as one-unit reads", () => { + const read: IrExpr = { + kind: "strIntrinsic", + method: "charAt", + receiver: ref("text", STR), + args: [bin("-", ref("i"), num(1))], + type: STR, + loc, + }; + const loop = counted([{ kind: "exprStmt", expr: read, loc }]); + const version = match(textKernel([loop]), loop)!; + expect(version.bounds.has(read)).toBe(true); + expect(version.receivers.map((r) => r.string)).toEqual([true]); + expect(version.affine.map((c) => [c.receiver, c.minOffset, c.maxEnd])).toEqual([[0, -1, -1]]); +}); + +test("the guard tests a string's ASCII bit and the fast copy reads bytes", () => { + const loop = counted([set(ref("out", f64s), ref("i"), charCode(ref("text", STR), ref("i")))]); + const kernel = textKernel([loop]); + const mod: IrModule = { + irVersion: 17, + sourceFile: loc.file, + entry: "__main", + functions: [ + { name: "__main", params: [], returnType: VOID, locals: [], body: [], loc }, + kernel, + ], + }; + const body = /^define internal [^\n]*@sc_bf_kernel[^]*?^}/m.exec(emitLlvmModule(mod))![0]; + const guard = body.slice(0, body.search(/^bytes\.version\.slow\d*:/m)); + expect(guard).toMatch(/load i32, ptr %\S+\n\s+%\S+ = icmp slt i32 %\S+, 0/); + const fast = body.slice(body.lastIndexOf("bytes.version.fast")); + expect(fast).toMatch(/load i8, ptr %\S+\n\s+%\S+ = uitofp i8/); + expect(fast).not.toContain("@sc_str_char_code_at_int"); + // The checked copy keeps the inline helper and its fallback. + expect(body).toContain("@sc_str_char_code_at_int"); +}); + +test("while loop counters below a string's length version its reads", () => { + // while (i < text.length && text.charCodeAt(i) === 32) i = i + 1 + const text = ref("text", STR); + const read = charCode(text, ref("i")); + const length: IrExpr = { + kind: "strIntrinsic", + method: "length", + receiver: text, + args: [], + type: F64, + loc, + }; + const loop: IrStmt & { kind: "while" } = { + kind: "while", + cond: { + kind: "logical", + op: "&&", + left: { kind: "bin", op: "<", left: ref("i"), right: length, type: BOOL, loc }, + right: { kind: "bin", op: "===", left: read, right: num(32), type: BOOL, loc }, + type: BOOL, + loc, + }, + body: [{ kind: "assign", localId: "i", value: bin("+", ref("i"), num(1)), loc }], + loc, + }; + const f = textKernel([{ kind: "varDecl", localId: "i", init: ref("k"), loc }, loop]); + const version = matchBytesLoopVersion(loop, null, { + locals: new Map(f.locals.map((l) => [l.id, l])), + invariantGlobals: new Set(), + captured: new Set(), + ranges: analyzeIntegerRanges(f), + initialized: findInitializedByteLoopBindings(f).get(loop) ?? new Set(), + proven: new Set(), + scalarStrings: new Set(), + workers: false, + owned: new Set(), + })!; + // The read is bounded by the counter's test against the length, not by + // a static range the guard could not meet. + expect(version.bounds.has(read)).toBe(true); + expect(version.receivers.map((r) => r.string)).toEqual([true]); + expect(version.bounded.map((c) => c.slack)).toEqual([0]); + expect(version.fixed).toEqual([]); +}); diff --git a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts index c3cee3feb..704b6723a 100644 --- a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts @@ -1,4 +1,4 @@ -import type { IrExpr, IrLocal, IrStmt } from "../../ir/ir.js"; +import { F64, type IrExpr, type IrLocal, type IrStmt } from "../../ir/ir.js"; import type { IntegerRange, IntegerRanges } from "../../ir/integer-ranges.js"; import type { IntegerCountedForLoop } from "../../ir/integer-loops.js"; import type { LlvmEmitterContext } from "./expr-context.js"; @@ -8,6 +8,7 @@ import { exactInteger, widenInteger } from "./integer-values.js"; import { emitCountedLoopLimit, emitCountedLoopSafety } from "./counted-loops.js"; import { emitBytesHeaderLength, emitBytesSharedStorage } from "./expr-bytes.js"; import { f64Lit } from "./common.js"; +import { emitStringAsciiFlag, emitStringByteLength } from "./string-reads.js"; type Node = IrExpr | IrStmt; type VarRef = IrExpr & { kind: "varRef" }; @@ -23,6 +24,24 @@ interface KeyedTerm extends Term { key: string; } +/** Every access of `receiver` at a while loop counter `c + d` happens while + * `c < bound` (`c <= bound` when `inclusive`) holds and before the counter + * advances, so it is in bounds when `bound + slack <= length`. */ +interface BoundedCheck { + receiver: number; + bound: IrExpr; + slack: number; +} + +/** A while loop counter whose entry value the guard tests: an exact + * integer of at least `min`. A counter that `| 0` wraps also needs every + * bound of its loops at most `wrapLimit`, so that it never reaches 2^31. */ +interface CounterCheck { + ref: VarRef; + min: number; + wrapBounds: { bound: IrExpr; limit: number }[]; +} + /** Every access of `receiver` at a static index range ends below `end + 1`. */ interface FixedCheck { receiver: number; @@ -45,12 +64,16 @@ export interface BytesLoopVersion { /** Worker programs: typed-array accesses whose views the guard proves * thread-owned (no shared storage). */ owned: ReadonlySet; - /** Root receiver bindings, read once before the loop. */ - receivers: { ref: VarRef; global: boolean }[]; + /** Root receiver bindings, read once before the loop; `string` marks a + * string whose `charCodeAt` and `charAt` reads the version covers. */ + receivers: { ref: VarRef; global: boolean; string: boolean }[]; /** Receivers the guard requires to have no shared storage. */ ownedReceivers: number[]; fixed: FixedCheck[]; affine: AffineCheck[]; + /** While loops: accesses at a counter below a loop-invariant bound. */ + bounded: BoundedCheck[]; + counters: CounterCheck[]; /** Keys of affine terms the guard tests at run time: exact integers * within GUARDED_TERM. */ guarded: ReadonlySet; @@ -80,10 +103,13 @@ export interface BytesLoopFacts { workers: boolean; /** Accesses already proved thread-owned by an enclosing version. */ owned: ReadonlySet; + /** String bindings held as a slice of another string (string-slices.ts): + * they have no string header for the guard to read. */ + scalarStrings: ReadonlySet; } const MAX_COST = 400; -const MAX_FIXED_END = 2 ** 32; +const MAX_FIXED_END = 2 ** 31; const MAX_CHECKS = 8; const MAX_OFFSET = 2 ** 31; const SAFE = Number.MAX_SAFE_INTEGER; @@ -106,6 +132,14 @@ const INVARIANT_OPS = new Set(["+", "-", "*", "&", "|", "^", "<<", ">>", ">>>"]) * such as `base` for `const base = r * cols`, must also be an exact * integer between -2^32 and 2^32, which the guard tests. * + * `s.charCodeAt(index)` and `s.charAt(index)` (so `s[index]`) of a string + * binding qualify the same way. A string is immutable: its bytes, its + * length and the capacity-word bit that proves it ASCII (string-reads.ts) + * are fixed when it is built. The guard also requires that bit, so each + * UTF-16 unit is one byte and the length is the byte length. The fast copy + * reads each unit with one byte load, and each character as the immortal + * one-character string for its byte. + * * In worker programs, every access of a view that may be shared tests its * shared storage. A view's storage is also fixed at construction, so the * same guard tests each invariant receiver once, and the fast copy reads @@ -116,7 +150,7 @@ const INVARIANT_OPS = new Set(["+", "-", "*", "&", "|", "^", "<<", ">>", ">>>"]) * whenever the guard fails, and keeps every out-of-bounds read, ignored * write and NaN-index behavior. */ export function matchBytesLoopVersion( - loop: IrStmt & { kind: "for" }, + loop: IrStmt & { kind: "for" | "while" }, counted: IntegerCountedForLoop | null, facts: BytesLoopFacts, ): BytesLoopVersion | null { @@ -155,10 +189,10 @@ export function matchBytesLoopVersion( // writes is not invariant either: `for (let i = (a = b, 0); ...)` must // not keep the bounds the guard proved for the old `a`. if ( - (loop.init !== null && !scanStmt(loop.init)) || + (loop.kind === "for" && loop.init !== null && !scanStmt(loop.init)) || !loop.body.every(scanStmt) || (loop.cond !== null && !scanExpr(loop.cond)) || - (loop.update !== null && !scanStmt(loop.update)) + (loop.kind === "for" && loop.update !== null && !scanStmt(loop.update)) ) return null; // A body temp stands for its initializer only when it is a constant @@ -196,7 +230,8 @@ export function matchBytesLoopVersion( }; const resolving = new Set(); const root = (e: IrExpr): VarRef | null => { - if (e.kind !== "varRef" || e.type.kind !== "bytes") return null; + if (e.kind !== "varRef" || (e.type.kind !== "bytes" && e.type.kind !== "string")) return null; + if (facts.scalarStrings.has(e.localId)) return null; const init = temp(e.localId); if (init) { if (resolving.has(e.localId)) return null; @@ -337,7 +372,45 @@ export function matchBytesLoopVersion( return sum + Math.max(Math.abs(r.min), Math.abs(r.max)); }, 0); - const receivers: { ref: VarRef; global: boolean }[] = []; + const whileAccesses = loop.kind === "while" ? whileCounterAccesses(loop) : null; + function whileCounterAccesses(loop: IrStmt & { kind: "while" }) { + return findWhileCounterAccesses(loop, { + counterLocal: (id) => { + const local = facts.locals.get(id); + return ( + local?.type.kind === "f64" && + !local.boxed && + !local.tdz && + !facts.captured.has(id) && + facts.initialized.has(id) + ); + }, + boundKey: (e) => { + if (e.type.kind !== "f64") return null; + // The guard evaluates the bound before the loop: a typed array's + // length needs the binding itself, not a temp the body declares. + if ( + e.kind === "bytesIntrinsic" && + e.method === "length" && + e.receiver.kind === "varRef" && + e.receiver.type.kind === "bytes" && + invariant(e.receiver.localId) + ) + return `bytes-length:${e.receiver.localId}`; + if ( + e.kind === "strIntrinsic" && + e.method === "length" && + e.args.length === 0 && + e.receiver.kind === "varRef" && + e.receiver.type.kind === "string" && + invariant(e.receiver.localId) + ) + return `string-length:${e.receiver.localId}`; + return pureKey(e); + }, + }); + } + const receivers: { ref: VarRef; global: boolean; string: boolean }[] = []; const receiverIndex = new Map(); const fixed = new Map(); const affineChecks = new Map< @@ -349,17 +422,26 @@ export function matchBytesLoopVersion( if (index === undefined) { index = receivers.length; receiverIndex.set(ref.localId, index); - receivers.push({ ref, global: !facts.locals.has(ref.localId) }); + receivers.push({ + ref, + global: !facts.locals.has(ref.localId), + string: ref.type.kind === "string", + }); } return index; }; // Worker programs: accesses through invariant receivers, proven or not, // become thread-owned when the guard finds no shared storage. const owned = new Map(); + // While loops: accesses at a counter below a bound, by receiver and bound. + const boundedChecks = new Map< + string, + { check: BoundedCheck; nodes: { node: Node; access: WhileAccess }[] } + >(); const access = (node: Node, array: IrExpr, index: IrExpr, width: number): void => { const ref = root(array); if (!ref) return; - if (facts.workers && !facts.owned.has(node)) { + if (facts.workers && ref.type.kind === "bytes" && !facts.owned.has(node)) { const receiver = receiverOf(ref); const nodes = owned.get(receiver); if (nodes) nodes.push(node); @@ -390,11 +472,30 @@ export function matchBytesLoopVersion( }); return; } + const bounded = whileAccesses?.accesses.get(node); + const loopCounter = bounded && whileAccesses!.counters.get(bounded.counter); + if (bounded && loopCounter && (!loopCounter.wraps || loopCounter.bounds !== null)) { + const receiver = receiverOf(ref); + const key = `${receiver}|${bounded.key}|${bounded.adj}`; + const slack = bounded.offset + width - 1 + bounded.adj; + const group = boundedChecks.get(key); + if (group) { + group.check.slack = Math.max(group.check.slack, slack); + group.nodes.push({ node, access: bounded }); + } else + boundedChecks.set(key, { + check: { receiver, bound: bounded.bound, slack }, + nodes: [{ node, access: bounded }], + }); + return; + } const range = facts.ranges.get(index); if (!range) return; const end = range.max + width - 1; - // Only a length a buffer can have: an outer loop must not pay for a - // guard that bounds an inner loop's unbounded counter. + // Only an end below 2^31: an outer loop must not pay for a guard that + // bounds an inner loop's unbounded counter, and the counter of a loop to + // an array's length reaches 2^32 - 2, a length no typed array in + // practice has. if (range.min < 0 || end >= MAX_FIXED_END) return; const receiver = receiverOf(ref); const group = fixed.get(receiver); @@ -408,7 +509,12 @@ export function matchBytesLoopVersion( const numeric = byteNumberAccess(e); if (numeric) access(e, e.receiver, e.args[numeric.offsetArg]!, numeric.width); else if (e.method === "get" && e.args[0]) access(e, e.receiver, e.args[0], 1); - } + } else if ( + e.kind === "strIntrinsic" && + (e.method === "charCodeAt" || e.method === "charAt") && + e.args[0] + ) + access(e, e.receiver, e.args[0], 1); return everyExprChild(e, visitExpr, visitStmt); }; const visitStmt = (s: IrStmt): boolean => { @@ -416,6 +522,7 @@ export function matchBytesLoopVersion( return everyStmtChild(s, visitExpr, visitStmt); }; loop.body.every(visitStmt); + if (loop.kind === "while") visitExpr(loop.cond); // Bound the counter so every counter operand stays a safe integer. let bound = SAFE; const affineGroups = [...affineChecks.values()].filter((group) => { @@ -431,8 +538,18 @@ export function matchBytesLoopVersion( }); const fixedKept = [...fixed.values()].slice(0, MAX_CHECKS); const affineKept = affineGroups.slice(0, MAX_CHECKS - fixedKept.length); + const boundedKept = [...boundedChecks.values()].slice( + 0, + Math.max(0, MAX_CHECKS - fixedKept.length - affineKept.length), + ); const ownedKept = [...owned.entries()].slice(0, MAX_CHECKS); - if (fixedKept.length === 0 && affineKept.length === 0 && ownedKept.length === 0) return null; + if ( + fixedKept.length === 0 && + affineKept.length === 0 && + boundedKept.length === 0 && + ownedKept.length === 0 + ) + return null; const bounds = new Set(); const ownedNodes = new Set(); const used = new Set(); @@ -477,6 +594,39 @@ export function matchBytesLoopVersion( for (const group of affineKept) for (const term of group.check.terms) if (guardedRanges.has(term.expr)) guarded.add(term.key); for (const [node, key] of guardedNodes) if (guarded.has(key)) ranges.set(node, GUARDED_TERM); + // While loop counters of the kept checks: each entry value is an exact + // integer that no access offset takes below 0. In the fast copy each + // access's index is below its length, and its counter read at least the + // most negative offset. + const narrow = (e: IrExpr, range: IntegerRange): void => { + const known = ranges.get(e) ?? facts.ranges.get(e); + ranges.set( + e, + known ? { min: Math.max(known.min, range.min), max: Math.min(known.max, range.max) } : range, + ); + }; + const counterMins = new Map(); + for (const group of boundedKept) { + used.add(group.check.receiver); + for (const { node, access } of group.nodes) { + bounds.add(node); + const min = 0 - access.offset; + counterMins.set(access.counter, Math.max(counterMins.get(access.counter) ?? -SAFE, min)); + narrow(access.ref, { min: -MAX_OFFSET, max: SAFE }); + if (access.index !== access.ref) narrow(access.index, { min: 0, max: SAFE }); + } + } + const counterChecks: CounterCheck[] = []; + for (const [id, min] of counterMins) { + const info = whileAccesses!.counters.get(id)!; + counterChecks.push({ + ref: { kind: "varRef", localId: id, type: F64, loc: loop.loc }, + min, + wrapBounds: info.wraps + ? info.bounds!.map((b) => ({ bound: b.bound, limit: 2 ** 31 - info.sum - b.adj })) + : [], + }); + } // Renumber the receivers the kept checks use. const order = [...used].sort((a, b) => a - b); const renumber = new Map(order.map((old, index) => [old, index])); @@ -500,6 +650,12 @@ export function matchBytesLoopVersion( minOffset: g.check.minOffset, maxEnd: g.check.maxEnd, })), + bounded: boundedKept.map((g) => ({ + receiver: renumber.get(g.check.receiver)!, + bound: g.check.bound, + slack: g.check.slack, + })), + counters: counterChecks, guarded, counted: affineKept.length > 0 ? counted : null, counterBound: needsBound ? bound : null, @@ -507,6 +663,314 @@ export function matchBytesLoopVersion( }; } +/** A typed-array access at a while loop counter: `counter + offset`, read + * or written while `counter < bound` (`<=` when `adj` is 1) holds. */ +interface WhileAccess { + counter: string; + /** The index, and the counter's read in it. */ + index: IrExpr; + ref: IrExpr; + bound: IrExpr; + key: string; + adj: number; + offset: number; +} + +interface WhileCounter { + /** Some increment is `c = (c + k) | 0`. */ + wraps: boolean; + /** The sum of every increment literal in the loop. */ + sum: number; + /** One bound from the condition of the loop and of every nested while + * loop that advances the counter; null when one of them has none. */ + bounds: { bound: IrExpr; adj: number }[] | null; +} + +/** `counter = counter + k`, `counter = (counter + k) | 0` or `counter++`. */ +function counterStep(s: IrStmt): { id: string; k: number; wraps: boolean } | null { + if (s.kind === "exprStmt" && s.expr.kind === "incDec" && s.expr.op === "+") + return { id: s.expr.localId, k: 1, wraps: false }; + if (s.kind !== "assign") return null; + let value = s.value, + wraps = false; + if ( + value.kind === "bin" && + value.op === "|" && + value.right.kind === "numLit" && + value.right.value === 0 && + value.left.kind === "bin" + ) { + value = value.left; + wraps = true; + } + if ( + value.kind !== "bin" || + value.op !== "+" || + value.left.kind !== "varRef" || + value.left.localId !== s.localId || + value.right.kind !== "numLit" || + !Number.isSafeInteger(value.right.value) || + value.right.value <= 0 || + value.right.value > MAX_OFFSET + ) + return null; + return { id: s.localId, k: value.right.value, wraps }; +} + +/** Typed-array and string accesses of a while loop that happen at a + * counter below a loop-invariant bound. A counter is a number local that + * the loop changes only by positive integer increments, written as + * statements of the body, of its blocks and branches, or of nested while + * loops. The condition of the loop, an `if` or a `&&` chain that compares + * the counter below a bound (`while (pos < end && bytes[pos] === 32) + * pos++`, or `s.charCodeAt(pos)` of a string) makes it below that bound + * until the next increment on the path. The counter starts at its entry + * value and only grows (a `| 0` increment never wraps when every bound is + * at most 2^31 minus the increments), so an access at `counter + d` is in + * bounds for the whole loop when the entry value is at least `-d` and + * `bound + d < length` (`+ 1` for `<=`). */ +function findWhileCounterAccesses( + loop: IrStmt & { kind: "while" }, + host: { + counterLocal: (id: string) => boolean; + /** A key for a bound expression whose value the loop never changes. */ + boundKey: (e: IrExpr) => string | null; + }, +): { accesses: Map; counters: Map } { + const steps = new Map(); + const refused = new Set(); + // Increments count only as statements of the loop's straight-line + // structure; any other write refuses the binding. + const writes = (list: readonly IrStmt[], direct: boolean): void => { + for (const s of list) { + const step = counterStep(s); + if (step && direct) { + const prior = steps.get(step.id); + steps.set(step.id, { + sum: (prior?.sum ?? 0) + step.k, + wraps: (prior?.wraps ?? false) || step.wraps, + }); + continue; + } + if (step) refused.add(step.id); + const nested = + direct && + (s.kind === "if" || s.kind === "while" || (s.kind === "block" && !s.labels?.length)); + if (s.kind === "assign" || s.kind === "varDecl" || s.kind === "forOf") refused.add(s.localId); + if (s.kind === "tryCatch" && s.catchLocalId) refused.add(s.catchLocalId); + everyStmtChild( + s, + (e) => (exprWrites(e), true), + (c) => (writes([c], nested), true), + ); + } + }; + const exprWrites = (e: IrExpr): void => { + if (e.kind === "assignExpr" || e.kind === "incDec") refused.add(e.localId); + everyExprChild( + e, + (c) => (exprWrites(c), true), + (c) => (writes([c], false), true), + ); + }; + exprWrites(loop.cond); + writes(loop.body, true); + const counter = (id: string): boolean => + steps.has(id) && !refused.has(id) && host.counterLocal(id); + + type Fact = { bound: IrExpr; key: string; adj: number }; + type Clean = ReadonlyMap; + // The bounds a condition puts on counters while it holds. + const facts = (cond: IrExpr, into: Map): void => { + if (cond.kind === "toBool") return facts(cond.operand, into); + if (cond.kind === "logical" && cond.op === "&&") { + facts(cond.left, into); + facts(cond.right, into); + return; + } + if (cond.kind !== "bin") return; + const below = cond.op === "<" || cond.op === "<=", + above = cond.op === ">" || cond.op === ">="; + const c = below ? cond.left : above ? cond.right : null, + bound = below ? cond.right : cond.left; + if (c?.kind !== "varRef" || !counter(c.localId) || into.has(c.localId)) return; + const key = host.boundKey(bound); + if (key === null) return; + into.set(c.localId, { bound, key, adj: cond.op === "<=" || cond.op === ">=" ? 1 : 0 }); + }; + // The facts that hold after either of two paths. + const meet = (a: Clean | null, b: Clean | null): Clean | null => { + if (a === null) return b; + if (b === null) return a; + const out = new Map(); + for (const [id, fact] of a) if (b.get(id) === fact) out.set(id, fact); + return out; + }; + const withFacts = (clean: Clean, cond: IrExpr): Clean => { + const out = new Map(clean); + facts(cond, out); + return out; + }; + const accesses = new Map(); + const record = (node: Node, index: IrExpr, clean: Clean): void => { + let ref: IrExpr = index, + offset = 0; + if (index.kind === "bin" && (index.op === "+" || index.op === "-")) { + const literal = + index.right.kind === "numLit" ? index.right : index.op === "+" ? index.left : null; + const other = literal === index.right ? index.left : index.right; + if (literal?.kind !== "numLit" || !Number.isSafeInteger(literal.value)) return; + if (Math.abs(literal.value) > MAX_OFFSET || Object.is(literal.value, -0)) return; + offset = index.op === "-" ? -literal.value : literal.value; + ref = other; + } + if (ref.kind !== "varRef") return; + const fact = clean.get(ref.localId); + if (fact) accesses.set(node, { counter: ref.localId, index, ref, ...fact, offset }); + }; + const expr = (e: IrExpr, clean: Clean): void => { + if (e.kind === "bytesIntrinsic") { + const numeric = byteNumberAccess(e); + const index = numeric + ? e.args[numeric.offsetArg] + : e.method === "get" + ? e.args[0] + : undefined; + if (index) record(e, index, clean); + } + if ( + e.kind === "strIntrinsic" && + (e.method === "charCodeAt" || e.method === "charAt") && + e.args[0] + ) + record(e, e.args[0], clean); + if (e.kind === "logical" && e.op === "&&") { + expr(e.left, clean); + expr(e.right, withFacts(clean, e.left)); + return; + } + if (e.kind === "ternary") { + expr(e.cond, clean); + expr(e.then, withFacts(clean, e.cond)); + expr(e.else_, clean); + return; + } + everyExprChild( + e, + (c) => (expr(c, clean), true), + (c) => (stmts([c], clean), true), + ); + }; + // Walks statements in order; the result is the clean state where the + // list falls through, or null when it never does. + const stmts = (list: readonly IrStmt[], clean: Clean): Clean | null => { + let current: Clean | null = clean; + for (const s of list) { + if (current === null) return null; + const step = counterStep(s); + if (step) { + const next: Map = new Map(current); + next.delete(step.id); + current = next; + continue; + } + switch (s.kind) { + case "if": { + expr(s.cond, current); + const then = stmts(s.then, withFacts(current, s.cond)); + const otherwise = stmts(s.else_ ?? [], current); + current = meet(then, otherwise); + break; + } + case "while": { + // A pass of the nested loop may follow its own increments. + const advanced: Map = new Map(current); + for (const id of writtenCounters(s.body)) advanced.delete(id); + expr(s.cond, advanced); + stmts(s.body, withFacts(advanced, s.cond)); + current = advanced; + break; + } + case "block": + if (!s.labels?.length) { + current = stmts(s.body, current); + break; + } + everyStmtChild( + s, + (e) => (expr(e, current!), true), + (c) => (stmts([c], current!), true), + ); + break; + case "bytesSet": + record(s, s.index, current); + everyStmtChild( + s, + (e) => (expr(e, current!), true), + (c) => (stmts([c], current!), true), + ); + break; + case "break": + case "continue": + case "return": + case "throw": + everyStmtChild( + s, + (e) => (expr(e, current!), true), + (c) => (stmts([c], current!), true), + ); + current = null; + break; + default: + everyStmtChild( + s, + (e) => (expr(e, current!), true), + (c) => (stmts([c], current!), true), + ); + break; + } + } + return current; + }; + const writtenCounters = (list: readonly IrStmt[]): Set => { + const out = new Set(); + const visit = (s: IrStmt): boolean => { + const step = counterStep(s); + if (step) out.add(step.id); + return everyStmtChild(s, () => true, visit); + }; + list.every(visit); + return out; + }; + expr(loop.cond, new Map()); + stmts(loop.body, withFacts(new Map(), loop.cond)); + + // Each counter's bounds: the loop's and those of nested while loops that + // advance it. + const counters = new Map(); + const loops: (IrStmt & { kind: "while" })[] = [loop]; + const collectLoops = (s: IrStmt): boolean => { + if (s.kind === "while") loops.push(s); + return everyStmtChild(s, () => true, collectLoops); + }; + loop.body.every(collectLoops); + for (const [id, step] of steps) { + if (!counter(id)) continue; + let bounds: WhileCounter["bounds"] = []; + for (const l of loops) { + if (l !== loop && !writtenCounters(l.body).has(id)) continue; + const fact = withFacts(new Map(), l.cond).get(id); + if (!fact) { + bounds = null; + break; + } + bounds.push({ bound: fact.bound, adj: fact.adj }); + } + counters.set(id, { ...step, bounds }); + } + return { accesses, counters }; +} + /** Branches to `fast` when every access the version proves is in bounds for * every iteration and every view it proves thread-owned has no shared * storage, and to `slow` otherwise (including a counted loop whose @@ -535,14 +999,20 @@ export function emitBytesLoopVersionGuard( } return value; }); - const lengths = receivers.map((receiver) => { - const len = emitBytesHeaderLength(host, receiver); + const lengths = receivers.map((receiver, index) => { + const len = version.receivers[index]!.string + ? emitStringByteLength(host, receiver) + : emitBytesHeaderLength(host, receiver); if (host.sizeType === "i64") return len; const wide = B.tmp(); B.line(`${wide} = zext ${host.sizeType} ${len} to i64`); return wide; }); const conditions: string[] = []; + // A string's byte length is its UTF-16 length when it is ASCII. + version.receivers.forEach(({ string }, index) => { + if (string) conditions.push(emitStringAsciiFlag(host, receivers[index]!)); + }); for (const index of version.ownedReceivers) { const owned = B.tmp(); B.line(`${owned} = icmp eq ptr ${emitBytesSharedStorage(host, receivers[index]!)}, null`); @@ -553,6 +1023,39 @@ export function emitBytesLoopVersionGuard( B.line(`${ok} = icmp ugt i64 ${lengths[check.receiver]}, ${check.end}`); conditions.push(ok); } + for (const counter of version.counters) { + const value = host.emitExpr(counter.ref); + // A counter held as an integer (an int32 slot) is exact already and + // needs at most its lower bound tested. + const integer = exactInteger(host, value, counter.ref); + if (integer && integer.range.max <= SAFE) { + if (integer.range.min < counter.min) { + const ok = B.tmp(); + B.line(`${ok} = icmp sge i64 ${widenInteger(host, integer)}, ${counter.min}`); + conditions.push(ok); + } + } else conditions.push(emitExactIntegerTest(host, value.name, counter.min, SAFE).ok); + for (const wrap of counter.wrapBounds) { + const bound = host.emitExpr(wrap.bound); + const boundInteger = exactInteger(host, bound, wrap.bound); + if (boundInteger && boundInteger.range.max <= wrap.limit) continue; + const ok = B.tmp(); + B.line(`${ok} = fcmp ole double ${bound.name}, ${f64Lit(wrap.limit)}`); + conditions.push(ok); + } + } + for (const check of version.bounded) { + // `bound <= length - slack` in doubles: both sides are exact (a length + // is below 2^53 and a slack below 2^32), and NaN fails. + const bound = host.emitExpr(check.bound).name; + const length = B.tmp(), + room = B.tmp(), + ok = B.tmp(); + B.line(`${length} = uitofp i64 ${lengths[check.receiver]} to double`); + B.line(`${room} = fsub double ${length}, ${f64Lit(check.slack)}`); + B.line(`${ok} = fcmp ole double ${bound}, ${room}`); + conditions.push(ok); + } if (counted && version.affine.length > 0) { // The first and last counter values, in ascending order. The last value // of a non-unit step is exact: `start + ⌊(limit ∓ 1 - start) / step⌋ * @@ -599,7 +1102,7 @@ export function emitBytesLoopVersionGuard( for (const term of check.terms) { const value = host.emitExpr(term.expr); if (version.guarded.has(term.key)) { - const tested = emitGuardedTerm(host, value.name); + const tested = emitExactIntegerTest(host, value.name, GUARDED_TERM.min, GUARDED_TERM.max); conditions.push(tested.ok); const sum = B.tmp(); B.line(`${sum} = ${term.sign > 0 ? "add" : "sub"} i64 ${base}, ${tested.integer}`); @@ -653,10 +1156,15 @@ export function emitBytesLoopVersionGuard( else B.condBr(valid, fast, slow); } -/** Tests that a guarded term is an exact integer within GUARDED_TERM (not - * -0; NaN fails the ordered comparisons) and gives its i64. The integer is - * frozen, so it is a fixed value even when the test fails. */ -function emitGuardedTerm(host: LlvmEmitterContext, value: string): { ok: string; integer: string } { +/** Tests that a double is an exact integer within [min, max] (not -0; NaN + * fails the ordered comparisons) and gives its i64. The integer is frozen, + * so it is a fixed value even when the test fails. */ +function emitExactIntegerTest( + host: LlvmEmitterContext, + value: string, + min: number, + max: number, +): { ok: string; integer: string } { const B = host.B; const low = B.tmp(), high = B.tmp(), @@ -668,8 +1176,8 @@ function emitGuardedTerm(host: LlvmEmitterContext, value: string): { ok: string; backBits = B.tmp(), exact = B.tmp(), ok = B.tmp(); - B.line(`${low} = fcmp oge double ${value}, ${f64Lit(GUARDED_TERM.min)}`); - B.line(`${high} = fcmp ole double ${value}, ${f64Lit(GUARDED_TERM.max)}`); + B.line(`${low} = fcmp oge double ${value}, ${f64Lit(min)}`); + B.line(`${high} = fcmp ole double ${value}, ${f64Lit(max)}`); B.line(`${inRange} = and i1 ${low}, ${high}`); B.line(`${raw} = fptosi double ${value} to i64`); B.line(`${integer} = freeze i64 ${raw}`); diff --git a/packages/compiler/src/backend/llvm/checked-value-lifetimes.ts b/packages/compiler/src/backend/llvm/checked-value-lifetimes.ts index 5f8119079..4169e86d8 100644 --- a/packages/compiler/src/backend/llvm/checked-value-lifetimes.ts +++ b/packages/compiler/src/backend/llvm/checked-value-lifetimes.ts @@ -5,6 +5,8 @@ export function preservesDynTest(test: string): boolean { switch (test) { case "truthy": + case "classCapsule": + case "funcKind": case "nullish": case "promise": case "buffer": diff --git a/packages/compiler/src/backend/llvm/classes.test.ts b/packages/compiler/src/backend/llvm/classes.test.ts index 2d95e942c..bee954afb 100644 --- a/packages/compiler/src/backend/llvm/classes.test.ts +++ b/packages/compiler/src/backend/llvm/classes.test.ts @@ -1,5 +1,14 @@ import { expect, test } from "vitest"; -import { DYN, F64, STRING, VOID, type IrModule } from "../../ir/ir.js"; +import { + DYN, + F64, + STRING, + VOID, + type IrExpr, + type IrFunction, + type IrModule, + type IrType, +} from "../../ir/ir.js"; import { assignCompactLayouts, buildClassGraph, @@ -159,6 +168,43 @@ test("ProgramLifetime hierarchies are never freed and take no reference counts", expect(body(mangleClassRelease("Owner"))).toContain("load i32, ptr %bufp"); }); +test("arrays of ProgramLifetime instances tell the runtime their teardown releases nothing", () => { + const kids = { kind: "array" as const, elem: { kind: "object" as const, className: "Node" } }; + const owners = { kind: "array" as const, elem: { kind: "object" as const, className: "Owner" } }; + const mod: IrModule = { + irVersion: 17, + sourceFile: loc.file, + entry: "main", + functions: [ + { + name: "main", + params: [], + returnType: VOID, + locals: [], + body: [ + { kind: "exprStmt", expr: { kind: "arrayLit", elems: [], type: kids, loc }, loc }, + { kind: "exprStmt", expr: { kind: "arrayLit", elems: [], type: owners, loc }, loc }, + ], + loc, + }, + ], + classes: [ + { name: "Node", fields: [], methods: [], loc }, + { name: "Owner", fields: [{ name: "nodes", type: kids }], methods: [], loc }, + ], + contracts: { programLifetime: ["Node"] }, + }; + const llvm = emitLlvmModule(mod, { pointerBits: 64 }); + const ops = (cls: string): string => + new RegExp(`^@sc_arrops\\.[^\\n]*${mangleClassRelease(cls)}[^\\n]*$`, "m").exec(llvm)?.[0] ?? + ""; + // SCR_ARR_OPS_RELEASE_NOTHING for Node; Owner's elements are released. + expect(ops("Node")).toMatch( + /= internal global \{ ptr, ptr, ptr, i32, i32 \} .*, i32 0, i32 2 \}$/, + ); + expect(ops("Owner")).toMatch(/= internal global %ScrArrOps .*, i32 0 \}$/); +}); + test("classes whose trace visits nothing start acyclic in a traced hierarchy", () => { const mapper = { kind: "object" as const, className: "Mapper" }; const mod: IrModule = { @@ -222,12 +268,13 @@ test("ProgramLifetime classes pack their fields after a 32-bit header", () => { contracts: { programLifetime: ["Node"] }, }; const llvm = emitLlvmModule(mod, { pointerBits: 64 }); - // Fields go widest first, each at the first offset that fits it. Node's - // weight (an int32 slot: nothing stores a fraction) takes the word the - // header shares (24 bytes, not 32); Leaf keeps Node's offsets and puts its - // bool next to Node's (32 bytes, not 48). - expect(llvm).toContain(`%${mangleClassStruct("Node")} = type { i32, i32, ptr, i8 }`); - expect(llvm).toContain(`%${mangleClassStruct("Leaf")} = type { i32, i32, ptr, i8, i8, ptr }`); + // Scalars narrower than 8 bytes go first, then the rest, widest first, + // each at the first offset that fits it. Node's weight (an int32 slot: + // nothing stores a fraction) takes the word the header shares and its + // flag the next one (24 bytes, not 32); Leaf keeps Node's offsets and puts + // its bool next to Node's (32 bytes, not 48). + expect(llvm).toContain(`%${mangleClassStruct("Node")} = type { i32, i32, i8, ptr }`); + expect(llvm).toContain(`%${mangleClassStruct("Leaf")} = type { i32, i32, i8, i8, ptr, ptr }`); // Without the contract, the layout is the declared one. const mortal = emitLlvmModule({ ...mod, contracts: {} }, { pointerBits: 64 }); expect(mortal).toContain(`%${mangleClassStruct("Leaf")} = type { i64, ptr, i8, i32, ptr, i8 }`); @@ -332,6 +379,99 @@ test("references between ProgramLifetime instances are 32-bit indexes into their expect(classFieldIndex(leafMeta, "label").compressed).toBe(false); }); +test("arrays of ProgramLifetime instances keep 32-bit references in 4-byte slots", () => { + const node = { kind: "object" as const, className: "Node" }; + const nodes = { kind: "array" as const, elem: node }; + const ref = (localId: string, type: IrType): IrExpr => ({ kind: "varRef", localId, type, loc }); + const fn = (name: string, body: IrExpr, returnType: IrType): IrFunction => ({ + name, + params: [ + { name: "a", localId: "a", type: nodes }, + { name: "v", localId: "v", type: node }, + ], + returnType, + locals: [ + { id: "a", name: "a", type: nodes, mutable: false }, + { id: "v", name: "v", type: node, mutable: false }, + ], + body: [{ kind: "return", value: body, loc }], + loc, + }); + const mod: IrModule = { + irVersion: 17, + sourceFile: loc.file, + entry: "main", + functions: [ + { name: "main", params: [], returnType: VOID, locals: [], body: [], loc }, + fn( + "first", + { + kind: "arrayGet", + arr: ref("a", nodes), + index: { kind: "numLit", value: 0, type: F64, loc }, + type: node, + loc, + }, + node, + ), + fn( + "add", + { + kind: "arrIntrinsic", + method: "push", + receiver: ref("a", nodes), + args: [ref("v", node)], + type: F64, + loc, + }, + F64, + ), + fn("make", { kind: "arrayLit", elems: [ref("v", node)], type: nodes, loc }, nodes), + fn( + "find", + { + kind: "arrIntrinsic", + method: "indexOf", + receiver: ref("a", nodes), + args: [ref("v", node)], + type: F64, + loc, + }, + F64, + ), + ], + classes: [{ name: "Node", fields: [{ name: "kids", type: nodes }], methods: [], loc }], + contracts: { programLifetime: ["Node"] }, + }; + const body = (llvm: string, name: string): string => + llvm.split(/^define /m).find((f) => f.includes(`; ${name}\n`)) ?? ""; + const llvm = emitLlvmModule(mod, { pointerBits: 64, objectAudit: false, lowRegion: true }); + // The element type's ops say so (SCR_ARR_OPS_COMPACT), so the runtime makes + // the arrays SCR_ELEM_CREF. + expect(llvm).toMatch( + /^@sc_arrops\.\S+\.c = internal global \{ ptr, ptr, ptr, i32, i32 \} \{ [^}]*, i32 0, i32 1 \}$/m, + ); + // A read loads the 32-bit reference and shifts it; an append stores one. + const first = body(llvm, "first"); + expect(first).toMatch(/getelementptr inbounds i32, ptr %\S+, i64 \S+\n\s+%\S+ = load i32/); + // The states follow 4-byte slots. + expect(first).toContain("getelementptr inbounds [5 x i8]"); + expect(first).toMatch(/= inttoptr i64 %\S+ to ptr/); + expect(body(llvm, "add")).toMatch( + /= lshr i64 %\S+, 3\n\s+%\S+ = trunc i64 %\S+ to i32\n\s+store i32/, + ); + // indexOf scans the 4-byte slots for the needle's reference. + expect(llvm).toMatch(/call double @sc_arr_index_of_cref\(ptr %\S+, ptr %\S+\)/); + expect(/^define internal double @sc_arr_index_of_cref[^]*?^}/m.exec(llvm)?.[0]).toContain( + "load i32, ptr %qp0", + ); + // Elsewhere the arrays keep 8-byte slots. + const elsewhere = emitLlvmModule(mod, { pointerBits: 64, objectAudit: false }); + expect(elsewhere).not.toMatch(/sc_arrops\.\S+\.c /); + expect(body(elsewhere, "first")).not.toContain("inttoptr"); + expect(elsewhere).not.toContain("sc_arr_index_of_cref"); +}); + test("a recycled block is cleared from the end of a ProgramLifetime instance's 32-bit header", () => { const BOOL = { kind: "bool" as const }; const mod: IrModule = { diff --git a/packages/compiler/src/backend/llvm/classes.ts b/packages/compiler/src/backend/llvm/classes.ts index 3073bd1bd..8f179828d 100644 --- a/packages/compiler/src/backend/llvm/classes.ts +++ b/packages/compiler/src/backend/llvm/classes.ts @@ -703,11 +703,13 @@ export function classFieldTypes(host: ShapeHost, cls: IrClassDef, int32?: Int32S /** Gives the classes `compact` admits (whole hierarchies: the test is the * root's) the 32-bit header and packed fields. A class keeps its base's - * field offsets, and places each of its own fields, widest first, at the - * first offset after the header that fits it: in a hole another field's - * alignment left, in the base's tail padding, or at the end. So 32-bit and - * smaller fields share 8-byte words. The struct lists the header and the - * fields by offset, which LLVM's layout of the type reproduces (checked). */ + * field offsets, and places each of its own fields at the first offset + * after the header that fits it: in a hole another field's alignment left, + * in the base's tail padding, or at the end. Scalars narrower than 8 bytes + * (flags, ids, counts) go first, widest first, so they share the header's + * cache line; then the other fields, widest first. So 32-bit and smaller + * fields share 8-byte words. The struct lists the header and the fields by + * offset, which LLVM's layout of the type reproduces (checked). */ export function assignCompactLayouts( metaMap: ReadonlyMap, compact: (root: LlClassMeta) => boolean, @@ -751,10 +753,12 @@ export function assignCompactLayouts( ); return COMPACT_REF; }); + const smallScalar = (f: { type: string; field: number; size: number }): number => + f.size < 8 && f.type !== "ptr" && !compressed[f.field] ? 0 : 1; const own = types .map((type, field) => ({ type, field, ...fieldStorage(type) })) .slice(baseCount) - .sort((a, b) => b.size - a.size || a.field - b.field); + .sort((a, b) => smallScalar(a) - smallScalar(b) || b.size - a.size || a.field - b.field); for (const f of own) { const sorted = [...elems].sort((a, b) => a.offset - b.offset); let at = -1; @@ -860,9 +864,9 @@ export function emitClassShapes( nullable?: NullableRefFields, int32?: Int32Slots, ): { typeDefs: string[]; defs: string[] } { - // Whether compact instances live in the program-lifetime region (any - // field holds a 32-bit reference to one; emitter.ts assignCompactLayouts). - const regionRefs = [...metaMap.values()].some((m) => m.compressed.some(Boolean)); + // Whether compact instances live in the program-lifetime region, where + // fields and arrays can refer to them in 32 bits (LlEmitter.compactRegion). + const regionRefs = host.compactRegion?.() === true; const typeDefs: string[] = []; const defs: string[] = []; const emitted = (mod.classes ?? []).filter((c) => !c.runtime); diff --git a/packages/compiler/src/backend/llvm/common.ts b/packages/compiler/src/backend/llvm/common.ts index 822ee29e2..ac4e28708 100644 --- a/packages/compiler/src/backend/llvm/common.ts +++ b/packages/compiler/src/backend/llvm/common.ts @@ -10,9 +10,10 @@ import type { import type { LlvmEmitterContext } from "./expr-context.js"; /** Field numbers of %ScrArr, which mirrors scr_runtime.h's ScrArr field for - * field (its static assertions pin `len`, `cap`, `cyc`, `filled` and - * `data`). `len` and `cap` are i32 on every target; the dense states end - * the allocation after the `cap` value slots at `data` (scrArrStates). */ + * field (its static assertions pin `len`, `cap`, `cyc`, `spare`, `filled` + * and `data`). `len` and `cap` are i32 on every target; the dense states of + * an array with states (SCR_ARR_STATES in `spare`) end the allocation after + * the `cap` value slots at `data` (scrArrStates). */ export const SCR_ARR_FIELD = { rc: 0, len: 1, @@ -24,6 +25,10 @@ export const SCR_ARR_FIELD = { data: 7, } as const; +/** The `spare` flag of a %ScrArr whose dense slots carry state bytes + * (SCR_ARR_STATES in scr_array.h). */ +export const SCR_ARR_STATES = 16; + /** Loads the i32 `len` or `cap` of a %ScrArr at `ptr` into `dest` as a * size-type count. */ export function loadArrCount( diff --git a/packages/compiler/src/backend/llvm/constant-callbacks.ts b/packages/compiler/src/backend/llvm/constant-callbacks.ts index 8a898d6a0..cf5c1b062 100644 --- a/packages/compiler/src/backend/llvm/constant-callbacks.ts +++ b/packages/compiler/src/backend/llvm/constant-callbacks.ts @@ -252,11 +252,13 @@ export function findConstantCallbacks( // entries actually used as indirect callees in each body. A module-wide // global/function cross product would dominate large-source emission. const globalTargets = new Map(); + const result = new Map>(); + // Only indirect callees get targets: without one, nothing resolves. + if (![...indirectCallees.values()].some((callees) => callees.size > 0)) return result; for (const [id, value] of globals) { const target = resolve(null, value); if (target) globalTargets.set(id, target); } - const result = new Map>(); for (const fn of mod.functions) { const needed = indirectCallees.get(fn.name)!; if (!needed.size) continue; diff --git a/packages/compiler/src/backend/llvm/dense-array-access.test.ts b/packages/compiler/src/backend/llvm/dense-array-access.test.ts index 1b84cf0a0..6a0123559 100644 --- a/packages/compiler/src/backend/llvm/dense-array-access.test.ts +++ b/packages/compiler/src/backend/llvm/dense-array-access.test.ts @@ -316,6 +316,55 @@ test.each([32, 64] as const)( }, ); +test("arrays without state bytes skip the state stores", () => { + const read: IrExpr = { kind: "arrayGet", arr: ref("s", words), index, type: STRING, loc }; + const write: IrStmt = { + kind: "arraySet", + arr: ref("s", words), + index, + value: { kind: "strLit", value: "x", type: STRING, loc }, + loc, + }; + const push: IrStmt = { + kind: "exprStmt", + expr: { + kind: "arrIntrinsic", + method: "push", + receiver: ref("a", nums), + args: [{ kind: "numLit", value: 2.5, type: F64, loc }], + type: F64, + loc, + }, + loc, + }; + const llvm = emitLlvmModule( + module("plain", STRING, [push, write, { kind: "return", value: read, loc }]), + ); + const fn = body(llvm, "plain"); + // SCR_ARR_STATES (16) in `spare`, field 5. + const flag = String.raw`getelementptr inbounds %ScrArr, ptr %t\d+, i32 0, i32 5\n %t\d+ = load i16, ptr %t\d+, !tbaa !\d+\n %t\d+ = and i16 %t\d+, 16\n %t\d+ = icmp ne i16 %t\d+, 0\n`; + // A push marks the state only when the array has states. + expect(fn).toMatch( + new RegExp( + `${flag} br i1 %t\\d+, label %arr\\.push\\.state\\d+, label %arr\\.push\\.marked\\d+\\narr\\.push\\.state\\d+:\\n store i8 1, ptr `, + ), + ); + // Above the prefix, an array without states has no elements (its prefix + // is its length): a store there appends at the length and replaces no + // value. + expect(fn).toMatch( + new RegExp( + `${flag} br i1 %t\\d+, label %arr\\.set\\.states\\d+, label %arr\\.set\\.plain\\d+`, + ), + ); + expect(fn).toMatch( + /^arr\.set\.plain\d+:\n %t\d+ = icmp eq i64 .*\n br i1 %t\d+, label %arr\.set\.values\d+, label %arr\.set\.slow\d+$/m, + ); + expect(fn).toMatch(/phi i1 \[ %t\d+, %arr\.set\.states\d+ \], \[ false, %arr\.set\.values\d+ \]/); + // Reads test no flag: they reach the state check only above the prefix. + expect(fn).not.toMatch(/label %arr\.get\.state\d+/); +}); + test("state and presence queries answer dense slots inline", () => { const state: IrExpr = { kind: "arrayState", arr: ref("b", flags), index, type: F64, loc }; const has: IrExpr = { kind: "arrayHas", arr: ref("s", words), index, type: BOOL, loc }; diff --git a/packages/compiler/src/backend/llvm/dense-array-access.ts b/packages/compiler/src/backend/llvm/dense-array-access.ts index 4488f90cb..95612e57c 100644 --- a/packages/compiler/src/backend/llvm/dense-array-access.ts +++ b/packages/compiler/src/backend/llvm/dense-array-access.ts @@ -14,23 +14,48 @@ * state bytes altogether: the runtime keeps every state below it a present * value, so reads load the slot directly and writes replace it without * touching its state. Other dense indices take the state-checked path, - * whose writes extend the bound when they continue it. */ + * whose writes extend the bound when they continue it. An array without + * state bytes (SCR_ARR_STATES clear in `spare`) keeps its bound at its + * length, so reads never reach the state check for it; a write past the + * bound tests the flag and, without states, appends at the length. */ import { isRefCounted, type IrExpr, type IrType } from "../../ir/ir.js"; import { f64Lit, loadArrCount, SCR_ARR_FIELD, + SCR_ARR_STATES, scrArrStatePtr, scrArrStates, sizeToDouble, storeArrCount, } from "./common.js"; import type { LlValue, LlvmEmitterContext } from "./expr-context.js"; -import { exactInteger, widenInteger } from "./integer-values.js"; +import { compactRefLoadLines, compactRefStoreLines } from "./classes.js"; +import { elemAccess } from "./shapes.js"; +import { indexInteger } from "./integer-values.js"; +import { integerOrNaNIndex } from "./wrapping-sums.js"; const SCR_ARR_VALUE = 1; +/** Whether the array at `array` has state bytes, as an i1. `spare` changes + * only in runtime calls, so its loads take their own alias tag. */ +export function emitArrayHasStates(host: LlvmEmitterContext, array: string): string { + const B = host.B; + const sparePtr = B.tmp(), + spare = B.tmp(), + bit = B.tmp(), + has = B.tmp(); + B.line( + `${sparePtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 ${SCR_ARR_FIELD.spare}`, + ); + host.markMemoryPointer(sparePtr, "array:spare"); + B.line(`${spare} = load i16, ptr ${sparePtr}${host.fieldAliasAttachment(sparePtr)}`); + B.line(`${bit} = and i16 ${spare}, ${SCR_ARR_STATES}`); + B.line(`${has} = icmp ne i16 ${bit}, 0`); + return has; +} + /** Call-site effects of the non-retaining read fallbacks: they only read * module-visible memory, or trap without returning to the caller (the trap * writes runtime-private stderr/TLS state). Declaring this lets LLVM keep @@ -74,13 +99,21 @@ function emitDenseGuard( ): DenseGuard { const B = host.B; const size = host.sizeType; - const integer = exactInteger(host, index, indexExpr); + const integer = indexInteger(host, index, indexExpr); + const nanIndex = integer === null ? integerOrNaNIndex(host, index, indexExpr) : null; let wide: string; let integral: string | null = null; - if (integer) { - // A proven integer needs no double round-trip. Negative signed values - // compare above any length or capacity as unsigned 64-bit integers. - wide = widenInteger(host, integer); + if (integer !== null) { + // A proven integer, or a wide view that equals the double at every + // index below a length, needs no double round-trip. Negative signed + // values compare above any length or capacity as unsigned 64-bit + // integers, and every index that fails the comparisons takes the + // double's path. + wide = integer; + } else if (nanIndex) { + // An exact integer or NaN is an index exactly when it is ordered. + wide = nanIndex.wide; + integral = nanIndex.ordered; } else { // freeze keeps NaN/out-of-range conversions from producing poison; the // exact round-trip comparison then rejects every value that is not an @@ -209,26 +242,52 @@ function statePointer( return statePtr; } -/** The dense slot type: boolean arrays store one byte per element, every - * other element kind an 8-byte slot. */ -function slotType(acc: ArrayAccess): "i8" | "i64" { - return acc === "bool" ? "i8" : "i64"; +/** How an array's dense slots hold its elements: the access kind (`i32` + * for int32 arrays, ir/int32-arrays.ts), or `cref` for references to compact + * class instances (SCR_ELEM_CREF: a 4-byte slot holding the address over 8; + * LlEmitter.compactArrayElem). */ +type SlotKind = ArrayAccess | "cref"; + +function slotKind(host: LlvmEmitterContext, acc: ArrayAccess, element: IrType): SlotKind { + return acc === "ref" && host.compactArrayElem(element) ? "cref" : acc; +} + +/** The dense slot type: boolean arrays store one byte per element, compact + * references and int32 arrays four, every other element kind an 8-byte slot. */ +function slotType(kind: SlotKind): "i8" | "i32" | "i64" { + return kind === "bool" ? "i8" : kind === "cref" || kind === "i32" ? "i32" : "i64"; } /** Bytes per dense slot (scr_arr_slot_width). */ -export function slotWidth(acc: ArrayAccess): number { - return acc === "bool" ? 1 : 8; +export function slotWidth(kind: SlotKind): number { + return kind === "bool" ? 1 : kind === "cref" || kind === "i32" ? 4 : 8; +} + +/** Bytes per dense slot of an array of `element` (scr_arr_slot_width). */ +export function denseSlotWidth(host: LlvmEmitterContext, element: IrType): number { + return slotWidth(slotKind(host, elemAccess(element), element)); +} + +/** The runtime accessor family of an access: int32 arrays answer through + * the double-valued entry points, which convert their slots. */ +export function runtimeAccess(acc: ArrayAccess): "f64" | "bool" | "ref" { + return acc === "i32" ? "f64" : acc; +} + +/** The LLVM type of an access's values at the emitter's interface. */ +function valueType(acc: ArrayAccess): "double" | "i1" | "ptr" { + return acc === "f64" || acc === "i32" ? "double" : acc === "bool" ? "i1" : "ptr"; } function valuePointer( host: LlvmEmitterContext, guard: Pick, - acc: ArrayAccess, + kind: SlotKind, ): string { const B = host.B; const valuePtr = B.tmp(); B.line( - `${valuePtr} = getelementptr inbounds ${slotType(acc)}, ptr ${guard.data}, ${host.sizeType} ${guard.offset}`, + `${valuePtr} = getelementptr inbounds ${slotType(kind)}, ptr ${guard.data}, ${host.sizeType} ${guard.offset}`, ); host.markMemoryPointer(valuePtr, "array:elements"); return valuePtr; @@ -241,7 +300,13 @@ function loadState(host: LlvmEmitterContext, guard: DenseGuard): string { return state; } -export type ArrayAccess = "f64" | "bool" | "ref"; +/** The ScrElemKind of int32 arrays (scr_array.h). */ +export const SCR_ELEM_I32 = 7; + +/** How a dense array's slots are read and written: doubles, booleans, + * references, or the int32 slots of SCR_ELEM_I32 arrays (ir/int32-arrays.ts), + * whose values are doubles at the emitter's interface. */ +export type ArrayAccess = "f64" | "bool" | "ref" | "i32"; /** Before a self-append (`a[i] = a[i] + suffix`) replaces a reference * element: when `index` lies in the hole-free prefix and the slot still @@ -297,14 +362,33 @@ export function emitDenseArrayGet( retain: boolean, proven = false, ): string { + return emitDenseArrayRead(host, array, index, indexExpr, acc, element, getter, retain, proven) + .value; +} + +/** emitDenseArrayGet, with the int32 of an int32 array's element: the + * slot itself on the dense path, the runtime getter's double converted + * (exactly: the element is an int32) on the other. */ +export function emitDenseArrayRead( + host: LlvmEmitterContext, + array: string, + index: LlValue, + indexExpr: IrExpr | undefined, + acc: ArrayAccess, + element: IrType, + getter: string, + retain: boolean, + proven = false, +): { value: string; int32: string | null } { const B = host.B; - const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr"; + const ty = valueType(acc); const ret = acc === "bool" ? "zeroext i1" : ty; + const kind = slotKind(host, acc, element); host.declare(`declare ${ret} @${getter}(ptr, double)`); if (acc === "ref" && retain && !isRefCounted(element)) { const t = B.tmp(); B.line(`${t} = call ${ty} @${getter}(ptr ${array}, double ${index.name})`); - return t; + return { value: t, int32: null }; } const slow = B.newLabel("arr.get.slow"), join = B.newLabel("arr.get.join"), @@ -322,7 +406,7 @@ export function emitDenseArrayGet( slow, true, valueLabel, - slotWidth(acc), + slotWidth(kind), proven, induction, ); @@ -333,12 +417,18 @@ export function emitDenseArrayGet( B.condBr(present, valueLabel, slow); } B.startBlock(valueLabel); - const ptr = valuePointer(host, guard, acc); + const ptr = valuePointer(host, guard, kind); let fast: string; + let fastInt: string | null = null; let fastLabel = valueLabel; if (acc === "f64") { fast = B.tmp(); B.line(`${fast} = load double, ptr ${ptr}${host.fieldAliasAttachment(ptr)}`); + } else if (acc === "i32") { + fastInt = B.tmp(); + fast = B.tmp(); + B.line(`${fastInt} = load i32, ptr ${ptr}${host.fieldAliasAttachment(ptr)}`); + B.line(`${fast} = sitofp i32 ${fastInt} to double`); } else if (acc === "bool") { const raw = B.tmp(); fast = B.tmp(); @@ -347,7 +437,13 @@ export function emitDenseArrayGet( } else { const raw = B.tmp(), nonnull = B.tmp(); - B.line(`${raw} = load ptr, ptr ${ptr}${host.fieldAliasAttachment(ptr)}`); + if (kind === "cref") { + const ref = B.tmp(); + B.line(`${ref} = load i32, ptr ${ptr}${host.fieldAliasAttachment(ptr)}`); + for (const line of compactRefLoadLines(ref, raw, `${raw}.r`)) B.line(line.trimStart()); + } else { + B.line(`${raw} = load ptr, ptr ${ptr}${host.fieldAliasAttachment(ptr)}`); + } B.line(`${nonnull} = icmp ne ptr ${raw}, null`); fastLabel = B.newLabel("arr.get.ref"); B.condBr(nonnull, fastLabel, slow); @@ -359,11 +455,21 @@ export function emitDenseArrayGet( const slowValue = B.tmp(); const attrs = acc === "ref" && retain ? " cold" : READ_FALLBACK; B.line(`${slowValue} = call ${ty} @${getter}(ptr ${array}, double ${index.name})${attrs}`); + let slowInt: string | null = null; + if (fastInt !== null) { + slowInt = B.tmp(); + B.line(`${slowInt} = fptosi double ${slowValue} to i32`); + } B.br(join); B.startBlock(join); const result = B.tmp(); B.line(`${result} = phi ${ty} [ ${fast}, %${fastLabel} ], [ ${slowValue}, %${slow} ]`); - return result; + let int32: string | null = null; + if (fastInt !== null && slowInt !== null) { + int32 = B.tmp(); + B.line(`${int32} = phi i32 [ ${fastInt}, %${fastLabel} ], [ ${slowInt}, %${slow} ]`); + } + return { value: result, int32 }; } /** `scr_arr_get_number`: a numeric element, or NaN for every missing, @@ -374,13 +480,14 @@ export function emitDenseArrayNumber( index: LlValue, indexExpr: IrExpr | undefined, proven = false, + acc: "f64" | "i32" = "f64", ): string { return emitDenseArrayGet( host, array, index, indexExpr, - "f64", + acc, { kind: "f64" }, "scr_arr_get_number", false, @@ -435,7 +542,9 @@ export function emitDenseArrayState( * value's reference. A dense index stores the slot, marks it present, and * extends the length when it lands at or past it; a replaced present * reference is released after the new edge is published (a release can - * collect cycles). Every other index takes the unchanged runtime call. */ + * collect cycles). Every other index takes the unchanged runtime call. + * `int32`, for an int32 array, is the value as an i32 when the caller has + * it at hand. */ export function emitDenseArraySet( host: LlvmEmitterContext, array: string, @@ -445,10 +554,12 @@ export function emitDenseArraySet( element: IrType, value: string, proven = false, + int32: string | null = null, ): void { const B = host.B; - const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr"; - const setter = `scr_arr_set_${acc}`; + const ty = valueType(acc); + const kind = slotKind(host, acc, element); + const setter = `scr_arr_set_${runtimeAccess(acc)}`; host.declare(`declare void @${setter}(ptr, double, ${acc === "bool" ? "i1 zeroext" : ty})`); const call = (attrs: string): void => B.line(`call void @${setter}(ptr ${array}, double ${index.name}, ${ty} ${value})${attrs}`); @@ -456,6 +567,13 @@ export function emitDenseArraySet( call(""); return; } + // An int32 array's stores are proven int32s (ir/int32-arrays.ts); both + // dense paths below store the converted value. + let int32Value: string | null = int32; + if (acc === "i32" && int32Value === null) { + int32Value = B.tmp(); + B.line(`${int32Value} = fptosi double ${value} to i32`); + } const slow = B.newLabel("arr.set.slow"), join = B.newLabel("arr.set.join"), filledLabel = B.newLabel("arr.set.filled"); @@ -467,33 +585,70 @@ export function emitDenseArraySet( slow, false, filledLabel, - slotWidth(acc), + slotWidth(kind), proven, ); const { offset, len } = guard; const storeValue = (valuePtr: string): void => { if (acc === "f64") { B.line(`store double ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); + } else if (acc === "i32") { + B.line(`store i32 ${int32Value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); } else if (acc === "bool") { const bits = B.tmp(); B.line(`${bits} = zext i1 ${value} to i8`); B.line(`store i8 ${bits}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); + } else if (kind === "cref") { + const ref = B.tmp(); + for (const line of compactRefStoreLines(value, ref, `${ref}.r`)) B.line(line.trimStart()); + B.line(`store i32 ${ref}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); } else { B.line(`store ptr ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); } }; + const stated = B.newLabel("arr.set.states"), + plain = B.newLabel("arr.set.plain"), + plainStore = B.newLabel("arr.set.values"), + stored = B.newLabel("arr.set.stored.slot"); + B.condBr(emitArrayHasStates(host, array), stated, plain); + B.startBlock(stated); const statePtr = statePointer(host, guard); - const valuePtr = valuePointer(host, guard, acc); - let oldState: string | null = null; - let old: string | null = null; - if (acc === "ref") { - oldState = B.tmp(); - old = B.tmp(); + const valuePtr = valuePointer(host, guard, kind); + let statedOld: string | null = null; + let statedHad: string | null = null; + // Compact instances are never released: a replaced one needs nothing. + if (kind === "ref") { + const oldState = B.tmp(); + statedOld = B.tmp(); + statedHad = B.tmp(); B.line(`${oldState} = load i8, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`); - B.line(`${old} = load ptr, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); + B.line(`${statedOld} = load ptr, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); + B.line(`${statedHad} = icmp eq i8 ${oldState}, ${SCR_ARR_VALUE}`); } storeValue(valuePtr); B.line(`store i8 ${SCR_ARR_VALUE}, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`); + const statedEnd = B.currentLabel(); + B.br(stored); + // Without states the bound is the length, so a store here appends at + // the length (it replaces no value) or leaves holes, which the runtime + // gives states. + B.startBlock(plain); + const appends = B.tmp(); + B.line(`${appends} = icmp eq ${host.sizeType} ${offset}, ${len}`); + B.condBr(appends, plainStore, slow); + B.startBlock(plainStore); + storeValue(valuePointer(host, guard, kind)); + const plainEnd = B.currentLabel(); + B.br(stored); + B.startBlock(stored); + let old: string | null = null; + let hadValue: string | null = null; + if (kind === "ref") { + old = B.tmp(); + hadValue = B.tmp(); + B.line(`${old} = phi ptr [ ${statedOld}, %${statedEnd} ], [ null, %${plainEnd} ]`); + B.line(`${hadValue} = phi i1 [ ${statedHad}, %${statedEnd} ], [ false, %${plainEnd} ]`); + } // A store at the bound continues the hole-free prefix. const filledWide = sizedFilled(host, guard.filled), continues = B.tmp(); @@ -537,9 +692,7 @@ export function emitDenseArraySet( storeArrCount(host, nextLen, lenPtr, host.fieldAliasAttachment(lenPtr)); B.br(storedLabel); B.startBlock(storedLabel); - if (acc === "ref" && oldState && old) { - const hadValue = B.tmp(); - B.line(`${hadValue} = icmp eq i8 ${oldState}, ${SCR_ARR_VALUE}`); + if (kind === "ref" && hadValue && old) { const releaseLabel = B.newLabel("arr.set.release"); B.condBr(hadValue, releaseLabel, join); B.startBlock(releaseLabel); @@ -549,9 +702,9 @@ export function emitDenseArraySet( // Below the bound the slot holds a present value: replace it, leaving // the state alone, and release a replaced reference after the store. B.startBlock(filledLabel); - const filledValuePtr = valuePointer(host, guard, acc); + const filledValuePtr = valuePointer(host, guard, kind); let filledOld: string | null = null; - if (acc === "ref") { + if (kind === "ref") { filledOld = B.tmp(); B.line( `${filledOld} = load ptr, ptr ${filledValuePtr}${host.fieldAliasAttachment(filledValuePtr)}`, @@ -578,10 +731,12 @@ export function emitDenseArrayPop( host: LlvmEmitterContext, array: string, acc: ArrayAccess, + element: IrType, empty: string, slow: string, ): string { const B = host.B; + const kind = slotKind(host, acc, element); const size = host.sizeType; const header = (field: number): string => { const ptr = B.tmp(); @@ -615,19 +770,42 @@ export function emitDenseArrayPop( states = B.tmp(); B.line(`${data} = load ptr, ptr ${dataPtr}${host.fieldAliasAttachment(dataPtr)}`); loadArrCount(host, cap, capPtr, host.fieldAliasAttachment(capPtr)); - scrArrStates(host, states, data, cap, slotWidth(acc)); - const valuePtr = valuePointer(host, { offset: last, data }, acc); + scrArrStates(host, states, data, cap, slotWidth(kind)); + const valuePtr = valuePointer(host, { offset: last, data }, kind); const statePtr = statePointer(host, { offset: last, states }); - const slotTy = slotType(acc); + const slotTy = slotType(kind); let raw = B.tmp(); B.line(`${raw} = load ${slotTy}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); if (slotTy === "i8") { const wide = B.tmp(); B.line(`${wide} = zext i8 ${raw} to i64`); raw = wide; + } else if (slotTy === "i32") { + if (kind === "cref") { + // The 8-byte slot of a compact reference is its address. + const wide = B.tmp(), + address = B.tmp(); + B.line(`${wide} = zext i32 ${raw} to i64`); + B.line(`${address} = shl nuw nsw i64 ${wide}, 3`); + raw = address; + } else { + // The slot bits of an int32 element's double, as the f64 path returns them. + const number = B.tmp(), + bits = B.tmp(); + B.line(`${number} = sitofp i32 ${raw} to double`); + B.line(`${bits} = bitcast double ${number} to i64`); + raw = bits; + } } B.line(`store ${slotTy} 0, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); + // Only an array with states has a state byte to clear. + const clear = B.newLabel("arr.pop.state"), + cleared = B.newLabel("arr.pop.cleared"); + B.condBr(emitArrayHasStates(host, array), clear, cleared); + B.startBlock(clear); B.line(`store i8 0, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`); + B.br(cleared); + B.startBlock(cleared); let last32 = last; if (size === "i64") { last32 = B.tmp(); @@ -648,12 +826,14 @@ export function emitDenseArrayPush( host: LlvmEmitterContext, array: string, acc: ArrayAccess, + element: IrType | null, value: string, ): string { const B = host.B; const size = host.sizeType; - const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr"; - const pusher = `scr_arr_push_${acc}`; + const kind = element === null ? acc : slotKind(host, acc, element); + const ty = valueType(acc); + const pusher = `scr_arr_push_${runtimeAccess(acc)}`; host.declare(`declare double @${pusher}(ptr, ${acc === "bool" ? "i1 zeroext" : ty})`); const header = (field: number): string => { const ptr = B.tmp(); @@ -685,20 +865,35 @@ export function emitDenseArrayPush( B.line(`${spare} = sub nuw ${size} ${cap}, ${len}`); B.line(`${near} = getelementptr inbounds i8, ptr ${data}, ${size} ${spare}`); B.line( - `${statePtr} = getelementptr inbounds [${slotWidth(acc)} x i8], ptr ${near}, ${size} ${cap}, ${size} -1`, + `${statePtr} = getelementptr inbounds [${slotWidth(kind)} x i8], ptr ${near}, ${size} ${cap}, ${size} -1`, ); host.markMemoryPointer(statePtr, "array:present"); - const valuePtr = valuePointer(host, { offset: len, data }, acc); + const valuePtr = valuePointer(host, { offset: len, data }, kind); if (acc === "f64") { B.line(`store double ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); + } else if (acc === "i32") { + const int32 = B.tmp(); + B.line(`${int32} = fptosi double ${value} to i32`); + B.line(`store i32 ${int32}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); } else if (acc === "bool") { const bits = B.tmp(); B.line(`${bits} = zext i1 ${value} to i8`); B.line(`store i8 ${bits}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); + } else if (kind === "cref") { + const ref = B.tmp(); + for (const line of compactRefStoreLines(value, ref, `${ref}.r`)) B.line(line.trimStart()); + B.line(`store i32 ${ref}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); } else { B.line(`store ptr ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); } + // Only an array with states has a state byte to mark. + const mark = B.newLabel("arr.push.state"), + marked = B.newLabel("arr.push.marked"); + B.condBr(emitArrayHasStates(host, array), mark, marked); + B.startBlock(mark); B.line(`store i8 ${SCR_ARR_VALUE}, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`); + B.br(marked); + B.startBlock(marked); const next = B.tmp(), fastValue = B.tmp(); B.line(`${next} = add nuw ${size} ${len}, 1`); @@ -716,6 +911,7 @@ export function emitDenseArrayPush( B.line(`${bumped} = select i1 ${packed}, i32 ${nextFilled}, i32 ${filled}`); B.line(`store i32 ${bumped}, ptr ${filledPtr}${host.fieldAliasAttachment(filledPtr)}`); B.line(`${fastValue} = ${sizeToDouble(size)} ${size} ${next} to double`); + const fastEnd = B.currentLabel(); B.br(join); B.startBlock(slow); const slowValue = B.tmp(); @@ -723,6 +919,6 @@ export function emitDenseArrayPush( B.br(join); B.startBlock(join); const result = B.tmp(); - B.line(`${result} = phi double [ ${fastValue}, %${fast} ], [ ${slowValue}, %${slow} ]`); + B.line(`${result} = phi double [ ${fastValue}, %${fastEnd} ], [ ${slowValue}, %${slow} ]`); return result; } diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index 5c9d8e60f..94581cfa0 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -1,6 +1,7 @@ +import { emitPerKeyOrder } from "./key-orders.js"; import { typedRefConstructor } from "./shapes.js"; import { bindLiveArrayOps } from "./expr-stream-bridges.js"; -import { BYTES_ELEM_NUM, SCR_ARR_FIELD, f64Lit, sizeToDouble } from "./common.js"; +import { BYTES_ELEM_NUM, SCR_ARR_FIELD, SCR_ARR_STATES, f64Lit, sizeToDouble } from "./common.js"; import { emitUnionPeek, emitUnionTag } from "./union-repr.js"; import { INLINE_UNION_FIELD, @@ -37,6 +38,8 @@ import { accessorSlotProp, DYN_HANDLE_KINDS, isDirentShape, + KEY_ORDER_FIELD, + shapeKeyOrders, isDynTypedRefType, isRefCounted, isUnitType, @@ -153,6 +156,8 @@ export interface DynHost extends WalkerHost { /** See moduleUsesDynBuiltinStream. */ usesDynBuiltinStream?(): boolean; liveDynUnionRefAdapter(t: IrType & { kind: "union" }): string; + /** The module has declaration-typed boundary casts (IrModule.declBoundary). */ + declBoundary?(): boolean; dynPromiseAdapter(t: IrType): string; isErrorClass(className: string): boolean; classSubtypes(className: string): readonly string[]; @@ -196,6 +201,55 @@ export class LlDyn { constructor(private readonly host: DynHost) {} + /** Journal replay (emit-journal.ts): builds or finds the helper a chunk + * emitter recorded, and answers its name. */ + replayHelper(op: string, args: readonly unknown[]): string { + switch (op) { + case "dyn.collectionDynOps": + return this.collectionDynOps(args[0] as IrType & { kind: "map" | "set" }); + case "dyn.dynMatchHelper": + return this.dynMatchHelper(args[0] as IrType, args[1] as boolean); + case "dyn.jsonSchema": + return this.jsonSchema(args[0] as IrType); + case "dyn.recordNameTable": { + const shapeId = args[0] as string; + return this.recordNameTable(shapeId, this.host.recordsById.get(shapeId)!).names; + } + case "dyn.jsonParseHelper": + return this.jsonParseHelper(args[0] as IrType)!; + case "dyn.dynCheckHelper": + return this.dynCheckHelper(args[0] as IrType, args[1] as boolean); + case "dyn.toDynHelper": + return this.toDynHelper(args[0] as IrType); + case "dyn.promiseDynAdapterHelper": + return this.promiseDynAdapterHelper(args[0] as IrType); + case "dyn.dynFuncThunkHelper": + return this.dynFuncThunkHelper(args[0] as IrType & { kind: "func" }); + case "dyn.dynFuncBoxHelper": + return this.dynFuncBoxHelper(args[0] as IrType & { kind: "func" }); + case "dyn.dynFuncAdapterHelper": + return this.dynFuncAdapterHelper(args[0] as IrType & { kind: "func" }); + case "dyn.dynToStrHelper": + return this.dynToStrHelper(); + case "dyn.dynIterNHelper": + return this.dynIterNHelper(); + case "dyn.dynDestrCheckHelper": + return this.dynDestrCheckHelper(); + case "dyn.dynKeyGetHelper": + return this.dynKeyGetHelper(); + case "dyn.dynComputedKeyGetHelper": + return this.dynComputedKeyGetHelper(); + case "dyn.caughtToDynHelper": + return this.caughtToDynHelper(); + case "dyn.messageListenerHelper": + return this.messageListenerHelper(args[0] as IrType & { kind: "func" }) ?? ""; + case "dyn.messageReaderHelper": + return this.messageReaderHelper(args[0] as IrType); + default: + throw new InternalCompilerError(`llvm emitter bug: no dyn helper replay ${op}`); + } + } + /** The types a dyn matcher or builder for `t` checks structurally: record * fields and index values, array elements and union arms. */ private structuralChildren(t: IrType): readonly IrType[] { @@ -385,6 +439,7 @@ export class LlDyn { if (prior) return prior; const name = `sc_map_dyn_${this.mapDynOps.size}`; this.mapDynOps.set(key, name); + this.host.journalAllocated?.(name, "dyn.collectionDynOps", [t]); const keyType = t.kind === "map" ? t.key : t.elem; const valueType: IrType = t.kind === "map" ? t.value : { kind: "f64" }; const emitSlot = (type: IrType, role: string): void => { @@ -442,6 +497,7 @@ export class LlDyn { `@${name} = internal constant { ptr, ptr, ptr, ptr, ptr, ptr } { ptr ${this.host.cstr(key)}, ptr @${name}_key_box, ptr @${name}_key_unbox, ptr @${this.dynMatchHelper(keyType)}, ptr @${name}_value_box, ptr @${name}_value_unbox }`, "", ); + this.host.journalBuilt?.(); return name; } @@ -840,6 +896,7 @@ export class LlDyn { if (existing) return existing; const name = `sc_dm_${this.dynMatchers.size}`; this.dynMatchers.set(matcherKey, name); + this.host.journalAllocated?.(name, "dyn.dynMatchHelper", [t, preserveRefs]); const recursive = this.isRecursiveShape(t); const wrapped = recursive || this.holdsRecursiveShape(t); const B = new BlockBuilder(); @@ -1055,9 +1112,10 @@ export class LlDyn { } B.condBr(readable, l0, fail); B.startBlock(l0); - // dyn ('unknown') fields match ANY value, present or missing. + // dyn ('unknown') fields match ANY value, present or missing. The + // order index is no key. for (const f of shape.fields) { - if (f.type.kind === "dyn") continue; + if (f.type.kind === "dyn" || f.name === KEY_ORDER_FIELD) continue; const { value: m, release } = this.memberRead(B, f.name, f.type); const lNext = B.newLabel("dm.n"); this.pendingBail( @@ -1200,6 +1258,7 @@ export class LlDyn { ``, ); if (wrapped) this.emitVisitWrapper(name, t, recursive ? "match" : "visit"); + this.host.journalBuilt?.(); return name; } @@ -1239,9 +1298,10 @@ export class LlDyn { return ( shape.fields.every( (f) => - !f.name.startsWith("%") && - f.name !== "__proto__" && - this.jsonSchemaSupported(f.type, visiting, true), + f.name === KEY_ORDER_FIELD || + (!f.name.startsWith("%") && + f.name !== "__proto__" && + this.jsonSchemaSupported(f.type, visiting, true)), ) && (!index || this.jsonSchemaSupported(index, visiting)) ); @@ -1293,6 +1353,7 @@ export class LlDyn { const S = host.sizeType; const name = `sc_jsch_${this.jsonSchemas.size}`; this.jsonSchemas.set(key, name); + this.host.journalAllocated?.(name, "dyn.jsonSchema", [t]); let body: string; switch (t.kind) { case "f64": @@ -1326,7 +1387,9 @@ export class LlDyn { const base = host.nullableUnions.fieldBase(t.shapeId); const index = new Map(shape.fields.map((f, i) => [f.name, i + base])); // The runtime names kept-source keys by this index (record-sources.ts). - const order = recordSourceFieldOrder(shape); + // The order index is no key: parsed values keep the table's order + // and their source's own order. + const order = recordSourceFieldOrder(shape).filter((name) => name !== KEY_ORDER_FIELD); if (recordMayKeepSource(shape)) this.keepsRecordSources = true; const fieldTy = `{ ptr, ${S}, ${S}, ptr }`; const entries = order.map((fieldName) => { @@ -1364,6 +1427,7 @@ export class LlDyn { `@${name} = internal constant { ${S}, ${S}, ptr, ptr, ptr, ptr, ptr, ptr, ${S} } { ${body} }`, ``, ); + this.host.journalBuilt?.(); return name; } @@ -1453,6 +1517,7 @@ export class LlDyn { const base = `sc_rnames_${this.recordNameTables.size}`; const table = { names: `@${base}`, lens: `@${base}_lens` }; this.recordNameTables.set(shapeId, table); + this.host.journalAllocated?.(base, "dyn.recordNameTable", [shapeId]); const n = shape.fields.length; const names = recordSourceFieldOrder(shape); this.defs.push( @@ -1460,6 +1525,7 @@ export class LlDyn { `@${base}_lens = internal constant [${n} x ${S}] [${names.map((name) => `${S} ${Buffer.byteLength(name, "utf8")}`).join(", ")}]`, ``, ); + this.host.journalBuilt?.(); return table; } @@ -1478,6 +1544,7 @@ export class LlDyn { const host = this.host; const name = `sc_jp_${this.jsonParsers.size}`; this.jsonParsers.set(key, name); + this.host.journalAllocated?.(name, "dyn.jsonParseHelper", [t]); const schema = this.jsonSchema(t); const check = this.dynCheckHelper(t); host.declare(`declare ptr @scr_json_parse_schema(ptr, ptr)`); @@ -1504,6 +1571,7 @@ export class LlDyn { `}`, ``, ); + this.host.journalBuilt?.(); return name; } @@ -1519,6 +1587,7 @@ export class LlDyn { if (existing) return existing; const name = `sc_dc_${this.dynBuilders.size}`; this.dynBuilders.set(builderKey, name); + this.host.journalAllocated?.(name, "dyn.dynCheckHelper", [t, preserveRefs]); const recursive = this.isRecursiveShape(t); const memo = !recursive && this.holdsRecursiveShape(t) && this.valTy(t) === "ptr"; const host = this.host; @@ -1541,6 +1610,7 @@ export class LlDyn { `}`, ``, ); + this.host.journalBuilt?.(); return name; } if (isRefCounted(t) && t.kind !== "dyn") { @@ -1719,6 +1789,32 @@ export class LlDyn { B.line(`store ${member.type} ${oldValue}, ptr ${checkedPtr}`); }); if (t.kind !== "record") { + // Whether the dense slots carry states (SCR_ARR_STATES in + // `spare`) moves with them; the rest of `spare` stays. + const cachedSpare = B.tmp(), + checkedSpare = B.tmp(); + const sparePtr = (array: string): string => { + const ptr = B.tmp(); + B.line( + `${ptr} = getelementptr inbounds %ScrArr, ptr ${array}, i64 0, i32 ${SCR_ARR_FIELD.spare}`, + ); + return ptr; + }; + const cachedSparePtr = sparePtr(cached), + checkedSparePtr = sparePtr(checked); + B.line(`${cachedSpare} = load i16, ptr ${cachedSparePtr}`); + B.line(`${checkedSpare} = load i16, ptr ${checkedSparePtr}`); + const exchange = (own: string, other: string, ptr: string): void => { + const kept = B.tmp(), + moved = B.tmp(), + next = B.tmp(); + B.line(`${kept} = and i16 ${own}, ${~SCR_ARR_STATES}`); + B.line(`${moved} = and i16 ${other}, ${SCR_ARR_STATES}`); + B.line(`${next} = or i16 ${kept}, ${moved}`); + B.line(`store i16 ${next}, ptr ${ptr}`); + }; + exchange(cachedSpare, checkedSpare, cachedSparePtr); + exchange(checkedSpare, cachedSpare, checkedSparePtr); // Sparse slots and numeric properties live out of line; each // array keeps its own match metadata. host.declare("declare void @scr_arr_swap_indexed_ext(ptr, ptr)"); @@ -2069,7 +2165,10 @@ export class LlDyn { } B.line(`%r0 = call ptr @${mangleRecordNew(t.shapeId)}()`); if (recursive) this.recordBuilt(B, name, "%r0"); + // The order index stays 0: the source's own order is kept below + // when it differs from the table. for (const f of shape.fields) { + if (f.name === KEY_ORDER_FIELD) continue; const { value: m, release } = this.memberRead(B, f.name, f.type); this.pendingBail( B, @@ -2537,6 +2636,7 @@ export class LlDyn { ``, ); if (recursive || memo) this.emitVisitWrapper(name, t, recursive ? "build" : "memo"); + this.host.journalBuilt?.(); return name; } @@ -2551,6 +2651,7 @@ export class LlDyn { if (existing) return existing; const name = `sc_td_${this.toDynFns.size}`; this.toDynFns.set(key, name); + this.host.journalAllocated?.(name, "dyn.toDynHelper", [t]); const host = this.host; const B = new BlockBuilder(); let sourceAccessor: { name: string; release: string } | null = null; @@ -2800,70 +2901,79 @@ export class LlDyn { host.declare(`declare ptr @scr_dyn_new_obj()`); B.line(`${d} = call ptr @scr_dyn_new_obj()`); } - // Keys insert in DECLARED order (JS insertion order); internal - // '%'-fields follow so a record→dyn→record round trip keeps them. + // Keys insert in DECLARED order (JS insertion order), or in the + // value's own order for a shape with orderVariants; internal + // '%'-fields follow so a record→dyn→record round trip keeps them + // (all but the order index, which is no key). const byName = new Map(shape.fields.map((f) => [f.name, f])); // An accessor property is one key whose getter and setter are the // slot closures (see accessorSlotProp), in accessorOrder's place. - const order = shape.accessorOrder ?? shape.declaredOrder ?? shape.fields.map((f) => f.name); - const inOrder = new Set(order); - const accessors = new Set( - shape.accessorOrder === undefined - ? [] - : shape.fields.flatMap((f) => { - const slot = accessorSlotProp(f.name); - return slot === null ? [] : [slot.prop]; - }), - ); - type DynField = { name: string; type: IrType } | { accessor: string }; - const dynFields: DynField[] = [ - ...order.flatMap((n) => { - if (accessors.has(n)) return [{ accessor: n } as DynField]; - const f = byName.get(n); - return f === undefined ? [] : [f as DynField]; - }), - ...shape.fields.filter( - (f) => - !inOrder.has(f.name) && !(accessors.size > 0 && accessorSlotProp(f.name) !== null), - ), - ]; - for (const entry of dynFields) { - if ("accessor" in entry) { - host.declare( - `declare void @scr_dyn_obj_set_accessor(ptr, ptr, ${host.sizeType}, ptr, ptr)`, - ); - const boxed = (kind: "get" | "set"): string => { - const slot = byName.get(`%${kind}:${entry.accessor}`); - if (slot === undefined) return "null"; - const fv = loadFieldOf(slot.name, slot.type); - const conv = B.tmp(); - B.line( - `${conv} = call ptr @${this.toDynHelper(slot.type)}(${this.valTy(slot.type)} ${fv})`, - ); - return conv; - }; - const getter = boxed("get"); - const setter = boxed("set"); - B.line( - `call void @scr_dyn_obj_set_accessor(ptr ${d}, ptr ${host.cstr(entry.accessor)}, ${host.sizeType} ${Buffer.byteLength(entry.accessor, "utf8")}, ptr ${getter}, ptr ${setter}) ; ${llvmCommentText(entry.accessor)}`, - ); - continue; - } - const f = entry; - const klen = Buffer.byteLength(f.name, "utf8"); - const fv = loadFieldOf(f.name, f.type); - // An ABSENT optional field contributes no key. - const lSkip = f.type.kind === "union" ? this.skipIfAbsent(B, fv, f.type) : null; - const conv = B.tmp(); - B.line(`${conv} = call ptr @${this.toDynHelper(f.type)}(${this.valTy(f.type)} ${fv})`); - B.line( - `call void @scr_dyn_obj_set(ptr ${d}, ptr ${host.cstr(f.name)}, ${host.sizeType} ${klen}, ptr ${conv}) ; ${llvmCommentText(f.name)}`, + const orders = + shape.accessorOrder !== undefined ? [shape.accessorOrder] : shapeKeyOrders(shape); + const perOrder = (order: readonly string[]): void => { + const inOrder = new Set(order); + const accessors = new Set( + shape.accessorOrder === undefined + ? [] + : shape.fields.flatMap((f) => { + const slot = accessorSlotProp(f.name); + return slot === null ? [] : [slot.prop]; + }), ); - if (lSkip !== null) { - B.br(lSkip); - B.startBlock(lSkip); + type DynField = { name: string; type: IrType } | { accessor: string }; + const dynFields: DynField[] = [ + ...order.flatMap((n) => { + if (accessors.has(n)) return [{ accessor: n } as DynField]; + const f = byName.get(n); + return f === undefined ? [] : [f as DynField]; + }), + ...shape.fields.filter( + (f) => + !inOrder.has(f.name) && + f.name !== KEY_ORDER_FIELD && + !(accessors.size > 0 && accessorSlotProp(f.name) !== null), + ), + ]; + for (const entry of dynFields) { + if ("accessor" in entry) { + host.declare( + `declare void @scr_dyn_obj_set_accessor(ptr, ptr, ${host.sizeType}, ptr, ptr)`, + ); + const boxed = (kind: "get" | "set"): string => { + const slot = byName.get(`%${kind}:${entry.accessor}`); + if (slot === undefined) return "null"; + const fv = loadFieldOf(slot.name, slot.type); + const conv = B.tmp(); + B.line( + `${conv} = call ptr @${this.toDynHelper(slot.type)}(${this.valTy(slot.type)} ${fv})`, + ); + return conv; + }; + const getter = boxed("get"); + const setter = boxed("set"); + B.line( + `call void @scr_dyn_obj_set_accessor(ptr ${d}, ptr ${host.cstr(entry.accessor)}, ${host.sizeType} ${Buffer.byteLength(entry.accessor, "utf8")}, ptr ${getter}, ptr ${setter}) ; ${llvmCommentText(entry.accessor)}`, + ); + continue; + } + const f = entry; + const klen = Buffer.byteLength(f.name, "utf8"); + const fv = loadFieldOf(f.name, f.type); + // An ABSENT optional field contributes no key. + const lSkip = f.type.kind === "union" ? this.skipIfAbsent(B, fv, f.type) : null; + const conv = B.tmp(); + B.line(`${conv} = call ptr @${this.toDynHelper(f.type)}(${this.valTy(f.type)} ${fv})`); + B.line( + `call void @scr_dyn_obj_set(ptr ${d}, ptr ${host.cstr(f.name)}, ${host.sizeType} ${klen}, ptr ${conv}) ; ${llvmCommentText(f.name)}`, + ); + if (lSkip !== null) { + B.br(lSkip); + B.startBlock(lSkip); + } } - } + }; + if (orders.length === 1) perOrder(orders[0]!); + else emitPerKeyOrder(B, shape, base, "%v", perOrder); // A Dirent row takes Node's Dirent prototype (the type probes) and // its Symbol(type) key; %dtype stays readable but non-enumerable. if (isDirentShape(shape)) { @@ -3157,6 +3267,7 @@ export class LlDyn { ``, ); } + this.host.journalBuilt?.(); return name; } @@ -3173,6 +3284,7 @@ export class LlDyn { if (existing) return existing; const name = `sc_pda_${this.promiseDynAdapters.size}`; this.promiseDynAdapters.set(key, name); + this.host.journalAllocated?.(name, "dyn.promiseDynAdapterHelper", [inner]); const host = this.host; const B = new BlockBuilder(); host.declare(`declare void @scr_promise_fulfill_ref(ptr, ptr, ptr, ptr, ptr)`); @@ -3250,6 +3362,7 @@ export class LlDyn { `}`, ``, ); + this.host.journalBuilt?.(); return name; } @@ -3262,6 +3375,7 @@ export class LlDyn { if (this.dynToStrFn) return this.dynToStrFn; const name = "sc_ds"; this.dynToStrFn = name; + this.host.journalAllocated?.(name, "dyn.dynToStrHelper", []); const host = this.host; host.declare(`declare void @scr_jb_init(ptr)`); host.declare(`declare ptr @scr_jb_finish(ptr)`); @@ -3566,6 +3680,7 @@ export class LlDyn { ``, ); } + this.host.journalBuilt?.(); return name; } @@ -3577,6 +3692,7 @@ export class LlDyn { if (this.caughtToDynFn) return this.caughtToDynFn; const name = "sc_cd"; this.caughtToDynFn = name; + this.host.journalAllocated?.(name, "dyn.caughtToDynHelper", []); const host = this.host; const B = new BlockBuilder(); const kp = B.tmp(); @@ -3682,6 +3798,7 @@ export class LlDyn { `}`, ``, ); + this.host.journalBuilt?.(); return name; } @@ -3695,6 +3812,7 @@ export class LlDyn { this.helperMemo.set(name, name); const B = new BlockBuilder(); const host = this.host; + this.host.journalAllocated?.(name, "dyn.dynComputedKeyGetHelper", []); const stringHelper = this.dynKeyGetHelper(); host.declare(`declare ptr @scr_dyn_property_key_value(ptr)`); host.declare(`declare ptr @scr_dyn_symbol_key_get(ptr, ptr, i1 zeroext)`); @@ -3732,6 +3850,7 @@ export class LlDyn { "}", "", ); + this.host.journalBuilt?.(); return name; } @@ -3741,6 +3860,7 @@ export class LlDyn { if (existing) return existing; const name = "sc_dyn_key_get"; this.dynBuilders.set(memoKey, name); + this.host.journalAllocated?.(name, "dyn.dynKeyGetHelper", []); const host = this.host; const B = new BlockBuilder(); const kd = this.kindOf(B, "%d"); @@ -4134,6 +4254,7 @@ export class LlDyn { `}`, ``, ); + this.host.journalBuilt?.(); return name; } @@ -4145,6 +4266,7 @@ export class LlDyn { if (existing) return existing; const name = "sc_dyn_destr_check"; this.dynBuilders.set(memoKey, name); + this.host.journalAllocated?.(name, "dyn.dynDestrCheckHelper", []); const host = this.host; host.declare(`declare void @scr_jb_init(ptr)`); host.declare(`declare ptr @scr_jb_finish(ptr)`); @@ -4199,6 +4321,7 @@ export class LlDyn { `}`, ``, ); + this.host.journalBuilt?.(); return name; } @@ -4210,6 +4333,7 @@ export class LlDyn { if (existing) return existing; const name = "sc_dyn_iter_n"; this.dynBuilders.set(memoKey, name); + this.host.journalAllocated?.(name, "dyn.dynIterNHelper", []); const host = this.host; host.declare(`declare ptr @scr_dyn_new_arr()`); host.declare(`declare void @scr_dyn_arr_push(ptr, ptr)`); @@ -4414,6 +4538,7 @@ export class LlDyn { `}`, ``, ); + this.host.journalBuilt?.(); return name; } @@ -4422,6 +4547,31 @@ export class LlDyn { /** The dyn argument spelling of one static value: dyn passes through * (+1), funcs box anonymously, everything else rides toDyn. `expr` is * BORROWED in every arm. Emits into the given builder. */ + /** A live capsule of a record or array value (+1; the value is + * borrowed), as `dynFrom` with `liveRef` builds it; null for other + * types. Closures boxed in a declaration-typed boundary program return + * their records and arrays this way. */ + private liveToDyn(B: BlockBuilder, t: IrType, expr: string): string | null { + const host = this.host; + if (t.kind === "union") { + const arms = host.unionsById.get(t.unionId)?.arms ?? []; + if (!arms.some((arm) => arm.kind === "record" || arm.kind === "array")) return null; + const boxed = B.tmp(); + B.line(`${boxed} = call ptr @${host.liveDynUnionRefAdapter(t)}(ptr ${expr})`); + return boxed; + } + if ((t.kind !== "record" && t.kind !== "array") || !streamTypedRefEligible(t)) return null; + const key = typeKey(t); + const adapter = host.liveDynRefAdapter(t); + const rc = vAdapters(host, t); + const boxed = B.tmp(); + B.line( + `${boxed} = call ptr ${typedRefConstructor(host, t)}(ptr ${expr}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit})`, + ); + for (const line of bindLiveArrayOps(host, adapter, boxed)) B.line(line); + return boxed; + } + private toDynExpr(B: BlockBuilder, t: IrType, expr: string): string { if (t.kind === "dyn") return this.retainDyn(B, expr); if (t.kind === "func") { @@ -4445,6 +4595,7 @@ export class LlDyn { if (existing) return existing; const name = `sc_dfk_${this.dynFuncThunks.size}`; this.dynFuncThunks.set(key, name); + this.host.journalAllocated?.(name, "dyn.dynFuncThunkHelper", [t]); const host = this.host; const B = new BlockBuilder(); const argNames: string[] = []; @@ -4552,6 +4703,26 @@ export class LlDyn { }, "ptr null", ); + // Declaration-typed boundary programs: an argument package code + // passes keeps its identity (scr_dyn_decl_adopt). + const capsule = this.host.declBoundary?.() === true ? this.liveToDyn(B, p, a) : null; + if (capsule !== null) { + host.declare(`declare void @scr_dyn_decl_adopt(ptr, ptr)`); + host.declare(`declare void @scr_dyn_release(ptr)`); + B.line(`call void @scr_dyn_decl_adopt(ptr ${ad}, ptr ${capsule})`); + B.line(`call void @scr_dyn_release(ptr ${capsule})`); + this.pendingBail( + B, + "dfk.adopt", + () => { + t.params.slice(0, i + 1).forEach((q, j) => { + const name = j === i ? a : argNames[j]; + if (isRefCounted(q)) B.line(`call void ${releaseSym(host, q)}(ptr ${name})`); + }); + }, + "ptr null", + ); + } argNames.push(a); } }); @@ -4584,7 +4755,10 @@ export class LlDyn { const r = B.tmp(); B.line(`${r} = call ${retTy} ${fn}(${callArgs})`); this.pendingBail(B, "dfkr", () => {}, "ptr null"); - const out = this.toDynExpr(B, t.ret, r); + const out = + this.host.declBoundary?.() === true + ? (this.liveToDyn(B, t.ret, r) ?? this.toDynExpr(B, t.ret, r)) + : this.toDynExpr(B, t.ret, r); if (isRefCounted(t.ret)) B.line(`call void ${releaseSym(host, t.ret)}(ptr ${r})`); B.terminate(`ret ptr ${out}`); } @@ -4594,6 +4768,7 @@ export class LlDyn { `}`, ``, ); + this.host.journalBuilt?.(); return name; } @@ -4604,6 +4779,7 @@ export class LlDyn { if (existing) return existing; const name = `sc_dfb_${this.dynFuncBoxes.size}`; this.dynFuncBoxes.set(key, name); + this.host.journalAllocated?.(name, "dyn.dynFuncBoxHelper", [t]); const host = this.host; const thunk = this.dynFuncThunkHelper(t); host.declare(`declare ptr @scr_closure_retain_v(ptr)`); @@ -4618,6 +4794,7 @@ export class LlDyn { `}`, ``, ); + this.host.journalBuilt?.(); return name; } @@ -4640,11 +4817,15 @@ export class LlDyn { (t.ret.kind === "void" || t.ret.kind === "f64" || t.ret.kind === "bool") && this.messageReadable(param, new Set()); if (!supported) { + // A signature without an entry still takes a number. this.messageListeners.set(key, null); + this.host.journalAllocated?.("", "dyn.messageListenerHelper", [t]); + this.host.journalBuilt?.(); return null; } const name = `sc_dml_${this.messageListeners.size}`; this.messageListeners.set(key, name); + this.host.journalAllocated?.(name, "dyn.messageListenerHelper", [t]); const B = new BlockBuilder(); B.entryAllocas.push(`%fail = alloca i1`); B.line(`store i1 false, ptr %fail`); @@ -4676,6 +4857,7 @@ export class LlDyn { `}`, ``, ); + this.host.journalBuilt?.(); return name; } @@ -4737,6 +4919,7 @@ export class LlDyn { if (existing) return existing; const name = `sc_dmr_${this.messageReaders.size}`; this.messageReaders.set(key, name); + this.host.journalAllocated?.(name, "dyn.messageReaderHelper", [t]); const host = this.host; const S = host.sizeType; if (t.kind !== "record") host.declare(`declare i32 @scr_message_kind(ptr, ${S})`); @@ -4909,6 +5092,7 @@ export class LlDyn { `}`, ``, ); + this.host.journalBuilt?.(); return name; } @@ -4920,6 +5104,7 @@ export class LlDyn { if (existing) return existing; const name = `sc_dfa_${this.dynFuncAdapters.size}`; this.dynFuncAdapters.set(key, name); + this.host.journalAllocated?.(name, "dyn.dynFuncAdapterHelper", [t]); const host = this.host; const B = new BlockBuilder(); host.declare(`declare ptr @scr_box_get_ref(ptr)`); @@ -5022,6 +5207,7 @@ export class LlDyn { `}`, ``, ); + this.host.journalBuilt?.(); return name; } } diff --git a/packages/compiler/src/backend/llvm/emit-function.ts b/packages/compiler/src/backend/llvm/emit-function.ts index 9e8f7d701..b6df7bcca 100644 --- a/packages/compiler/src/backend/llvm/emit-function.ts +++ b/packages/compiler/src/backend/llvm/emit-function.ts @@ -16,9 +16,7 @@ import { withInitializerBindings, withInitializerLocals, } from "../../ir/initializer-bindings.js"; -import { findUnaliasedLocalArrays } from "../../ir/array-length-stability.js"; import { constructorClass } from "../../ir/final-fields.js"; -import { findInitializedByteLoopBindings } from "./byte-windows.js"; import { findLoopArrayBorrows } from "./loop-array-borrows.js"; import { findCallArrayReads, findLocalArrayReads } from "./local-array-reads.js"; import { findScalarStringSlices, findScratchStringSlices } from "./string-slices.js"; @@ -55,11 +53,15 @@ import { INT32_ABI_SUFFIX, SCALAR_UNION_RESULT } from "./int32-abi.js"; import { f64Lit } from "./common.js"; import { RESULT_ENTRY_PREFIX } from "./record-results.js"; import { emitIntegerClone, emitIntegerDispatcher } from "./integer-clones.js"; +import { beginFunctionLoops, LOOPS } from "./loop-state.js"; +import { runningExtremumUpdates } from "./running-extrema.js"; +import { findIntegerOrNaNLocals } from "./wrapping-sums.js"; export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { const B = new BlockBuilder(); emitter.B = B; emitter.currentFunctionName = fn.name; + emitter.findInt32Allocations(fn); emitter.currentTier = fn.tier; // Fiber switches restore the active exception cell before control // returns to a synchronous frame (emitPendingCheck). @@ -81,6 +83,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { emitter.currentLocals.set(id, plain); emitter.lazyCaptureBoxes.set(id, ""); } + emitter.runningExtrema = emitter.debug === null ? runningExtremumUpdates(fn) : new Set(); emitter.currentConstantCallbacks = emitter.debug === null ? (emitter.constantCallbacks.get(fn.name) ?? new Map()) : new Map(); emitter.knownCallbacks.clear(); @@ -99,7 +102,6 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { const initializerBindings = emitter.initializerBindings.get(fn.name) ?? []; const numericFn = withInitializerBindings(fn, initializerBindings); emitter.numericLocals = new Map(numericFn.locals.map((l) => [l.id, l])); - emitter.unaliasedArrays = findUnaliasedLocalArrays(fn); emitter.borrowedParameters.clear(); emitter.referenceWrites?.enter(fn); emitter.reboundOwnerSlots.clear(); @@ -132,30 +134,18 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { emitter.integerLoopBindings.clear(); emitter.integerTemps.clear(); emitter.integerTempsEnabled = emitter.debug === null && !fn.async && !fn.generator; - emitter.countedLoopsEnabled = emitter.debug === null && !fn.async && !fn.generator; - emitter.speculativeLoopsEnabled = emitter.countedLoopsEnabled; + beginFunctionLoops(emitter, fn, numericFn); + emitter.speculativeLoopsEnabled = LOOPS.of(emitter).countedLoopsEnabled; emitter.speculativeFunction = fn.name; emitter.speculation = null; emitter.speculationResume = null; + emitter.int32Kinds = null; + emitter.int32KindResume = null; emitter.finalFieldBorrows = !fn.async && !fn.generator && constructorClass(fn.name) === null && emitter.finalFields.size > 0; - emitter.byteWindowsEnabled = emitter.countedLoopsEnabled; - emitter.bytesLoopVersionsEnabled = emitter.countedLoopsEnabled; - emitter.bytesLoopEntries = null; - emitter.bytesLoopFunction = fn; - emitter.denseLoopsEnabled = emitter.countedLoopsEnabled; - emitter.denseLoop = null; - emitter.denseLoopEntries = - emitter.denseLoopsEnabled && numericFn.locals.some((l) => l.type.kind === "array") - ? findInitializedByteLoopBindings(fn) - : new Map(); - emitter.byteWindowEntries = - emitter.byteWindowsEnabled && numericFn.locals.some((l) => l.type.kind === "bytes") - ? findInitializedByteLoopBindings(fn) - : new Map(); emitter.integerViews.clear(); emitter.fieldPointerTags.clear(); emitter.compressedFieldPtrs.clear(); @@ -231,6 +221,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { emitter.integerRanges = rangeFacts.ranges; emitter.integerLoopRanges = rangeFacts.loopRanges; emitter.int32Elements = slotFacts.elements ?? null; + emitter.integerOrNaNLocals = findIntegerOrNaNLocals(emitter, numericFn); emitter.bytesBounds = findBytesBounds(numericFn, emitter.integerRanges); emitter.bytesOwned = new Set(); // Debug builds keep every number local a double for the debugger's @@ -413,7 +404,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { if (local.boxed || emitter.captureIds.has(local.id)) slots.add(`%${mangleLocal(local.id)}`); B.volatileSlots = slots; // Without optimization a versioned copy only adds code. - emitter.bytesLoopVersionsEnabled = false; + LOOPS.of(emitter).bytesLoopVersionsEnabled = false; } // Captured bindings come in through the environment — borrowed for the // whole call (the closure owns them): bound here, never released here. diff --git a/packages/compiler/src/backend/llvm/emit-journal.ts b/packages/compiler/src/backend/llvm/emit-journal.ts new file mode 100644 index 000000000..ad89d2671 --- /dev/null +++ b/packages/compiler/src/backend/llvm/emit-journal.ts @@ -0,0 +1,237 @@ +/* Chunked function emission (emit-module.ts). An emitter that emits one + * contiguous range of a module's functions records here the helpers and + * sites it numbers from a counter (`sc_td_${size}` and the like). The + * emitter that merges the ranges replays each range's records in order: + * replay calls the same entry point with the same arguments, so every + * helper gets the number and the definition the serial emission gives it, + * and the range's text is renamed to those numbers. */ +import type { LlEmitter } from "./emitter.js"; + +/** One event of a range's emission, in order. */ +export type JournalEntry = + /** A helper or site the range's emitter numbered: the name it gave it + * (a counter prefix and a number), and the replay entry point + * (registerReplay) with its arguments, which are plain IR values. */ + | { + readonly kind: "op"; + readonly name: string; + readonly op: string; + readonly args: readonly unknown[]; + } + /** The first request of a key of a module-level table made outside any + * replayable definition, with the value the range's emitter stored. */ + | { + readonly kind: "entry"; + readonly table: string; + readonly key: string; + readonly value: unknown; + }; + +export class EmitJournal { + readonly entries: JournalEntry[] = []; + /** The replayable definitions being built, innermost last. */ + private readonly open: string[] = []; + + /** The definitions still open (empty between functions). */ + get unclosed(): readonly string[] { + return this.open; + } + /** Keys already recorded per table. */ + private readonly recorded = new Map>(); + + /** A keyed helper, recorded when its number is taken, before anything its + * definition creates in turn: that keeps the serial numbering order. + * Replaying a helper that exists answers its name, so helpers created + * inside another helper's definition are recorded too. Opens the + * definition, which the creating entry point closes with `built`. */ + allocated(name: string, op: string, args: readonly unknown[]): void { + this.entries.push({ kind: "op", name, op, args }); + this.open.push(op); + } + + /** Opens a definition whose helper is not replayed itself (its + * entry point records the request with `used`). */ + opened(op: string): void { + this.open.push(op); + } + + /** A request, made outside any definition, of the helper `name` that an + * entry point answered; recorded once per name. Replay makes the same + * request. */ + used(name: string, op: string, args: readonly unknown[]): void { + if (this.open.length !== 0) return; + if (this.usedNames.has(name)) return; + this.usedNames.add(name); + this.entries.push({ kind: "op", name, op, args }); + } + private readonly usedNames = new Set(); + + /** Closes the definition `allocated` or `opened` opened. */ + built(): void { + if (this.open.pop() === undefined) this.overclosed = true; + } + + /** A `built` closed a definition no `allocated` opened. */ + overclosed = false; + + /** An unkeyed site (one per use). Sites taken while a definition is built + * are taken again when the merging emitter replays that definition. */ + site(name: string, op: string, args: readonly unknown[] = []): void { + if (this.open.length === 0) this.entries.push({ kind: "op", name, op, args }); + } + + /** A request of `key` in a module-level table. Only the first request + * made outside any definition is recorded: requests made while a + * definition is built happen again when it is replayed. */ + entry(table: string, key: string, value: unknown): void { + if (this.open.length !== 0) return; + let keys = this.recorded.get(table); + if (keys === undefined) this.recorded.set(table, (keys = new Set())); + if (keys.has(key)) return; + keys.add(key); + this.entries.push({ kind: "entry", table, key, value }); + } +} + +/** A range emitter's result (LlEmitter.chunkResult): plain data, so a + * worker process can write it. */ +export interface ChunkResult { + readonly entries: readonly JournalEntry[]; + readonly flags: { + readonly usesCoroutines: boolean; + readonly usesTimers: boolean; + readonly usesGenSlots: boolean; + readonly needsUnionNone: boolean; + readonly needsNullableAbsent: boolean; + readonly needsOom: boolean; + readonly needsBadTag: boolean; + readonly needsBadKey: boolean; + readonly needsRetainBox: boolean; + }; + readonly publishedNamesSym: string | null; + readonly rcHelpers: readonly string[]; + readonly immortalValues: readonly string[]; + readonly fnValues: readonly string[]; + readonly arraySearchKinds: readonly string[]; + readonly recordCloneShapes: readonly string[]; + readonly awaitCallees: readonly string[]; + readonly adoptCallees: readonly string[]; + readonly directAwaitCallees: readonly string[]; + readonly inlineAwaitCallees: readonly string[]; +} + +type Replay = (emitter: LlEmitter, args: readonly unknown[]) => string; +const replays = new Map(); + +/** Registers the entry point that creates (or finds) a numbered helper or + * site from its recorded arguments and answers its name. */ +export function registerReplay(op: string, replay: Replay): void { + replays.set(op, replay); +} + +/** Replays a range's entries on the merging emitter, in record order, and + * records the renames from the range's names to its own in `names`. */ +export function replayJournal( + emitter: LlEmitter, + entries: readonly JournalEntry[], + names: Map, + tags: Map, +): void { + for (const entry of entries) { + if (entry.kind === "entry") { + emitter.applyJournalEntry(entry.table, entry.key, entry.value, names, tags); + continue; + } + const replay = replays.get(entry.op); + if (replay === undefined) + throw new Error(`llvm emitter bug: no replay for journal entry ${entry.op}`); + const own = stripSigil(replay(emitter, entry.args)); + const local = stripSigil(entry.name); + if (own !== local) names.set(local, own); + } +} + +/** The rename of a range's text: numbered names (`names`, without the + * sigil) and field alias tags (`tags`). One scan for each kind of name: + * a numbered name starts `sc_` after a character that cannot be part of a + * name, and ends at the last digit after its counter prefix. */ +export function renameChunk( + names: ReadonlyMap, + tags: ReadonlyMap, +): (text: string) => string { + const prefixes = new Set(); + for (const name of names.keys()) prefixes.add(name.replace(/\d+$/, "")); + if (prefixes.size === 0 && tags.size === 0) return (text) => text; + return (text) => { + let out = prefixes.size === 0 ? text : renameNames(text, names, prefixes); + if (tags.size > 0) out = renameTags(out, tags); + return out; + }; +} + +function renameNames( + text: string, + names: ReadonlyMap, + prefixes: ReadonlySet, +): string { + const parts: string[] = []; + let copied = 0; + for (let at = text.indexOf("sc_"); at >= 0; at = text.indexOf("sc_", at + 3)) { + if (at > 0 && isNameChar(text.charCodeAt(at - 1))) continue; + // The counter prefix ends where the digits start; the number ends at + // the last digit. + let end = at + 3; + while (end < text.length && isNameChar(text.charCodeAt(end)) && !isDigit(text.charCodeAt(end))) + end++; + if (end === text.length || !isDigit(text.charCodeAt(end))) continue; + if (!prefixes.has(text.slice(at, end))) continue; + let last = end; + while (last < text.length && isDigit(text.charCodeAt(last))) last++; + const own = names.get(text.slice(at, last)); + if (own === undefined) continue; + parts.push(text.slice(copied, at), own); + copied = last; + } + if (copied === 0) return text; + parts.push(text.slice(copied)); + return parts.join(""); +} + +function renameTags(text: string, tags: ReadonlyMap): string { + const marker = "!tbaa !"; + const parts: string[] = []; + let copied = 0; + for (let at = text.indexOf(marker); at >= 0; at = text.indexOf(marker, at + marker.length)) { + const start = at + marker.length; + let end = start; + while (end < text.length && isDigit(text.charCodeAt(end))) end++; + const own = tags.get(Number(text.slice(start, end))); + if (own === undefined) continue; + parts.push(text.slice(copied, start), String(own)); + copied = end; + } + if (copied === 0) return text; + parts.push(text.slice(copied)); + return parts.join(""); +} + +/** `[-$._A-Za-z0-9]`: a character of an LLVM identifier. */ +function isNameChar(code: number): boolean { + return ( + (code >= 48 && code <= 57) || + (code >= 65 && code <= 90) || + (code >= 97 && code <= 122) || + code === 95 || + code === 36 || + code === 45 || + code === 46 + ); +} + +function isDigit(code: number): boolean { + return code >= 48 && code <= 57; +} + +function stripSigil(name: string): string { + return name.startsWith("@") ? name.slice(1) : name; +} diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts index 3d9d41072..16a1b1601 100644 --- a/packages/compiler/src/backend/llvm/emit-module.ts +++ b/packages/compiler/src/backend/llvm/emit-module.ts @@ -2,6 +2,10 @@ import { relieveHeapPressure } from "../../heap-pressure.js"; import { emitWasmModules } from "./wasm-modules.js"; import { type LlEmitter, ffiCallbackDummyLl, llStrBytes } from "./emitter.js"; +import { type ChunkResult, EmitJournal } from "./emit-journal.js"; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { JS_IC_WORDS } from "./js-ic.js"; import { mangleAsyncSpawn, @@ -24,7 +28,7 @@ import { } from "../../ir/ir.js"; import { FN_ATTRS, - limitInlineDynReleases, + limitInlineReleases, releaseSym, typedRefConstructor, vAdapters, @@ -35,9 +39,11 @@ import { emitClassMembershipHelper } from "./classes.js"; import { type RegexMatcherCode, compileRegexMatcher } from "./regex-matchers.js"; import { MAP_ITER_DECL, + MAP_SIZE_DECL, MAP_ITER_NUM_DECL, SCR_MAP_NUM_DEAD, NUMBER_MAP_ENTRY_DECL, + STRING_MAP_ENTRY_C_DECL, STRING_MAP_ENTRY_DECL, } from "./map-number-lookup.js"; import { RUNTIME_ABI_MARKER } from "../runtime-abi.js"; @@ -59,16 +65,150 @@ import { STRING_READ_HELPERS, stringScanDecl } from "./string-reads.js"; import { emitLibraryIdentityLines } from "../library-identity-markers.js"; import { InternalCompilerError } from "../../errors.js"; +/** The function bodies emitted in `chunks` contiguous ranges, each by its + * own emitter (LlEmitter.chunkEmitter), then merged in function order: the + * text one emitter gives. */ +function emitFunctionsInChunks(emitter: LlEmitter, chunks: number): string[] { + const functions = emitter.mod.functions; + const size = Math.ceil(functions.length / chunks); + const ranges: { chunk: LlEmitter; texts: string[] }[] = []; + for (let start = 0; start < functions.length; start += size) { + const chunk = emitter.chunkEmitter(); + const texts = functions.slice(start, start + size).map((fn) => { + const text = chunk.emitFunction(fn); + if (chunk.journal!.overclosed) + throw new Error(`llvm emitter bug: journal definition closed twice in ${fn.name}`); + if (chunk.journal!.unclosed.length > 0) + throw new Error( + `llvm emitter bug: unclosed journal definitions after ${fn.name}: ${chunk.journal!.unclosed.join(", ")}`, + ); + return text; + }); + ranges.push({ chunk, texts }); + } + const fnDefs: string[] = []; + for (const { chunk, texts } of ranges) { + // The plain-data result a worker process would write, round-tripped. + const result = JSON.parse(JSON.stringify(chunk.chunkResult())) as ChunkResult; + const rename = emitter.mergeChunk(result); + for (const text of texts) { + const def = rename(text); + emitter.fnDefText.set(functions[fnDefs.length]!.name, def); + fnDefs.push(def); + } + } + return fnDefs; +} + +/** Modules with fewer functions emit in one process: forking and merging + * would cost more than the workers save (SCRIPTC_EMIT_WORKER_FUNCTIONS + * lowers it, for tests). */ +function workerMinFunctions(): number { + const value = Number(process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"] ?? "2000"); + return Number.isFinite(value) && value > 0 ? value : 2000; +} + +/** The function bodies emitted on forked worker processes, `workers` at a + * time: the function list splits into contiguous ranges, each emitted in + * its own process, which inherits this emitter as it is at the fork and + * records a journal (emit-journal.ts). Ranges merge in function order, so + * the text is the serial text. A range whose worker fails is emitted here + * when its turn comes. Null without fork primitives (Node, Windows, inside + * a worker) or when the module is small. */ +function emitFunctionsOnWorkers(emitter: LlEmitter, workers: number): string[] | null { + const fork = emitter.targetOptions.forkPrimitives; + const functions = emitter.mod.functions; + if (fork === undefined || workers < 2) return null; + if (functions.length < workerMinFunctions()) return null; + // More ranges than workers: a range's merge overlaps the later ranges' + // emission, and a slow range holds back less. + const count = Math.min(functions.length, workers * 3); + const size = Math.ceil(functions.length / count); + const dir = mkdtempSync(join(tmpdir(), "scriptc-emit-")); + const pids: number[] = []; + const start = (range: number): void => { + const pid = fork.fork(); + if (pid === 0) { + let code = 1; + try { + emitter.journal = new EmitJournal(); + const texts: string[] = []; + for (const fn of functions.slice(range * size, (range + 1) * size)) { + texts.push(emitter.emitFunction(fn)); + relieveHeapPressure(); + } + writeFileSync(join(dir, `${range}.json`), JSON.stringify(emitter.chunkResult())); + // Function text never holds NUL, so it separates the functions. + writeFileSync(join(dir, `${range}.ll`), texts.join("\0")); + code = 0; + } catch { + // The parent emits the range itself. + } + fork.exit(code); + } + pids.push(pid); + }; + const fnDefs: string[] = []; + try { + for (let range = 0; range < Math.min(workers, count); range++) start(range); + for (let range = 0; range < count; range++) { + const pid = pids[range]!; + const status = pid > 0 ? fork.wait(pid) : -1; + // The next worker starts from this emitter as merged so far, which + // the journal allows: it records requests, not table contents. + const merged = status === 0 ? readRange(dir, range) : null; + const slice = functions.slice(range * size, (range + 1) * size); + if (merged !== null && merged.texts.length === slice.length) { + const rename = emitter.mergeChunk(merged.result); + slice.forEach((fn, index) => { + const def = rename(merged.texts[index]!); + emitter.fnDefText.set(fn.name, def); + fnDefs.push(def); + }); + } else + for (const fn of slice) { + const def = emitter.emitFunction(fn); + emitter.fnDefText.set(fn.name, def); + fnDefs.push(def); + relieveHeapPressure(); + } + if (pids.length < count) start(pids.length); + } + } finally { + rmSync(dir, { recursive: true, force: true }); + } + return fnDefs; +} + +/** A worker's range result, or null when its files are missing. */ +function readRange(dir: string, range: number): { result: ChunkResult; texts: string[] } | null { + try { + const result = JSON.parse(readFileSync(join(dir, `${range}.json`), "utf8")) as ChunkResult; + const texts = readFileSync(join(dir, `${range}.ll`), "utf8").split("\0"); + return { result, texts }; + } catch { + return null; + } +} + export function emitModuleParts(emitter: LlEmitter): string[] { // Function bodies first (the literal/unit/fn-value tables fill as they // emit), then the file assembles around them — the runtime ABI’s order. - const fnDefs: string[] = []; - for (const fn of emitter.mod.functions) { - const def = emitter.emitFunction(fn); - fnDefs.push(def); - emitter.fnDefText.set(fn.name, def); - relieveHeapPressure(); - } + // Debug metadata is numbered per emitter and not journaled, so a debug + // build emits every body here. + const ranges = emitter.debug === null; + const chunks = emitter.targetOptions.functionChunks ?? 1; + const fnDefs = + (ranges && chunks > 1 ? emitFunctionsInChunks(emitter, chunks) : null) ?? + (ranges ? emitFunctionsOnWorkers(emitter, emitter.targetOptions.functionWorkers ?? 1) : null) ?? + []; + if (fnDefs.length === 0) + for (const fn of emitter.mod.functions) { + const def = emitter.emitFunction(fn); + fnDefs.push(def); + emitter.fnDefText.set(fn.name, def); + relieveHeapPressure(); + } emitter.selectStacklessAsync(fnDefs); const errorViews: string[] = []; for (const property of ["name", "message"]) @@ -456,15 +596,18 @@ export function emitModuleParts(emitter: LlEmitter): string[] { // RC fast paths test the traced byte (shapes.ts). ...(emitter.rcHelpers === null ? [] : [`%ScrMapRc = type { ${emitter.sizeType}, i8, i8, i8 }`]), // The complete ScrMap and its entries, read by the inline number-key - // lookup (sc_map_entry_f64) and the inline iteration steps - // (sc_map_iter_entry, sc_map_iter_entry_num): field 8 is the i32 entry + // lookup (sc_map_entry_f64), the inline iteration steps + // (sc_map_iter_entry, sc_map_iter_entry_num) and Map.size + // (sc_map_size): field 8 is the i32 entry count, field 9 the i32 live // count, field 10 the i64 absent-key filter, entries is field 11, // nbuckets and buckets fields 13 and 14, the direct index and its i32 // length fields 15 and 16. ...(emitter.decls.has(NUMBER_MAP_ENTRY_DECL) || emitter.decls.has(STRING_MAP_ENTRY_DECL) || + emitter.decls.has(STRING_MAP_ENTRY_C_DECL) || emitter.decls.has(MAP_ITER_DECL) || - emitter.decls.has(MAP_ITER_NUM_DECL) + emitter.decls.has(MAP_ITER_NUM_DECL) || + emitter.decls.has(MAP_SIZE_DECL) ? [ `%ScrMapIx = type { ${emitter.sizeType}, i8, i8, i8, i8, i32, ptr, ptr, i32, i32, i64, ptr, i32, i32, ptr, ptr, i32, i32 }`, ] @@ -479,6 +622,11 @@ export function emitModuleParts(emitter: LlEmitter): string[] { emitter.decls.has(MAP_ITER_NUM_DECL) ? [`%ScrMapEntry = type { i64, i64, i64 }`, `%ScrMapNumEntry = type { i64, i64 }`] : []), + // A compact-value map's { key, hash, val } entry (scr_map.h ScrMapCEntry): + // a 32-bit hash and the value as an address over 8. + ...(emitter.decls.has(STRING_MAP_ENTRY_C_DECL) + ? [`%ScrMapCEntry = type { i64, i32, i32 }`] + : []), // The runtime error prefix { rc, vt, name, message, code, cause } and the // class-object shape { rc, pre, post, ctor, name } — field reads on // builtin errors and classval loads GEP through these. @@ -545,7 +693,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] { ``, `@scr_error_vts = external ${tl}global [${RUNTIME_ERROR_CLASSES.size} x %ScrVt]`, `declare void @scr_init()`, - ...(emitter.usesCompactRefs ? [`declare void @scr_plr_init()`] : []), + ...(emitter.compactRegion ? [`declare void @scr_plr_init()`] : []), `declare void @scr_lib_init(i32, ptr)`, ...(emitter.mod.workers ? [`declare void @scr_runtime_workers_v8()`] : []), ...(emitter.stackChecks?.size ? [`declare void @scr_stack_guard_init()`] : []), @@ -699,21 +847,19 @@ export function emitModuleParts(emitter: LlEmitter): string[] { for (const code of regexMatchers.values()) out.push(...code.lines, ``); for (const [, inst] of emitter.templateStringsInstances) { // One immortal ScrArr per tagged-template site: a data global of - // interned cooked-string literals followed by their states (the - // runtime finds them before data + cap * 9), and the ScrArr header over - // it (rc == SIZE_MAX, len == cap == filled, SCR_ELEM_STR = 2, no REF - // entry points). Every dense slot is present; reads retain immortal - // strings — a no-op. Slots are 8 bytes on every target: on 32-bit - // targets each holds the pointer in its low half and zero in its high - // half, so the states start where the runtime looks for them. + // interned cooked-string literals, and the ScrArr header over it + // (rc == SIZE_MAX, len == cap == filled, SCR_ELEM_STR = 2, no REF entry + // points, no state bytes: every dense slot is present). Reads retain + // immortal strings — a no-op. Slots are 8 bytes on every target: on + // 32-bit targets each holds the pointer in its low half and zero in its + // high half. const n = inst.slots.length; - const present = n === 0 ? "zeroinitializer" : `[ ${inst.slots.map(() => "i8 1").join(", ")} ]`; const wide = emitter.sizeType === "i64"; const slot = wide ? "ptr" : "<{ ptr, i32 }>"; const slotValue = (s: string): string => wide ? `ptr ${s}` : `<{ ptr, i32 }> <{ ptr ${s}, i32 0 }>`; out.push( - `@${inst.sym}_data = internal constant <{ [${n} x ${slot}], [${n} x i8] }> <{ [${n} x ${slot}] [ ${inst.slots.map(slotValue).join(", ")} ], [${n} x i8] ${present} }>, align 8`, + `@${inst.sym}_data = internal constant [${n} x ${slot}] [ ${inst.slots.map(slotValue).join(", ")} ], align 8`, `@${inst.sym} = internal global %ScrArr { ${emitter.sizeType} -1, i32 ${n}, i32 ${n}, i8 2, i8 0, i16 0, i32 ${n}, ptr @${inst.sym}_data }`, ); } @@ -727,6 +873,8 @@ export function emitModuleParts(emitter: LlEmitter): string[] { out.push(`@${sym} = internal constant { ptr, ptr, ptr, ptr, ptr, ptr, ptr } { ${body} }`); } if (emitter.mapOpsConsts.size > 0) out.push(``); + for (const sym of emitter.methodIdSlots.values()) out.push(`@${sym} = internal global i32 -1`); + if (emitter.methodIdSlots.size > 0) out.push(``); for (const line of ffiCallbacks.globals) out.push(line); if (ffiCallbacks.globals.length > 0) out.push(``); @@ -840,7 +988,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] { ` call void @scr_init()`, // Compressed fields hold addresses in the program-lifetime region, which // is reserved below 32 GiB before compiled code runs. - ...(emitter.usesCompactRefs ? [` call void @scr_plr_init()`] : []), + ...(emitter.compactRegion ? [` call void @scr_plr_init()`] : []), // Each context (the main thread, every worker) installs its stack // guard before running compiled code that checks it. ...(emitter.stackChecks?.size ? [` call void @scr_stack_guard_init()`] : []), @@ -1293,6 +1441,18 @@ export function helperDefs(emitter: LlEmitter): string[] { ``, ); } + // Map.size and Set.size read the live entry count (scr_map_size). + if (emitter.decls.has(MAP_SIZE_DECL)) + defs.push( + `define internal double @sc_map_size(ptr %m) ${FN_ATTRS} {`, + `entry:`, + ` %np = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 9`, + ` %n = load i32, ptr %np`, + ` %nf = uitofp i32 %n to double`, + ` ret double %nf`, + `}`, + ``, + ); // String-key Map/Set lookups test the map's absent-key filter inline // (scr_map.c scr_map_filter_bits): a key whose two filter bits are not // both set is absent, so most misses on small and medium maps return @@ -1301,36 +1461,69 @@ export function helperDefs(emitter: LlEmitter): string[] { // and the filter reads its bits 44..55, which are the capacity word's // own. A key without a cached hash, and every possible hit, take the // runtime lookup. - if (emitter.decls.has(STRING_MAP_ENTRY_DECL)) { + // A compact-value map's lookup (sc_map_entry_str_c) tests the same filter + // and calls scr_map_entry_str_c; its iteration step (sc_map_iter_entry_c) + // reads 16-byte entries and their 32-bit hash. + const stringMapEntry = (helper: string, runtime: string): string[] => [ + `define internal ptr @${helper}(ptr %m, ptr %k) ${FN_ATTRS} {`, + `entry:`, + ...(emitter.sizeType === "i64" + ? [ + ` %fp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 10`, + ` %filter = load i64, ptr %fp`, + ` %capp = getelementptr inbounds %ScrStr, ptr %k, i32 0, i32 2`, + ` %cap = load i64, ptr %capp`, + ` %cached = icmp uge i64 %cap, 4294967296`, + ` br i1 %cached, label %test, label %slow`, + `test:`, + ` %s1 = lshr i64 %cap, 44`, + ` %i1 = and i64 %s1, 63`, + ` %b1 = shl i64 1, %i1`, + ` %s2 = lshr i64 %cap, 50`, + ` %i2 = and i64 %s2, 63`, + ` %b2 = shl i64 1, %i2`, + ` %bits = or i64 %b1, %b2`, + ` %have = and i64 %filter, %bits`, + ` %maybe = icmp eq i64 %have, %bits`, + ` br i1 %maybe, label %slow, label %absent`, + `absent:`, + ` ret ptr null`, + ] + : [` br label %slow`]), + `slow:`, + ` %r = call ptr @${runtime}(ptr %m, ptr %k)`, + ` ret ptr %r`, + `}`, + ``, + ]; + if (emitter.decls.has(STRING_MAP_ENTRY_DECL)) + defs.push(...stringMapEntry("sc_map_entry_str", "scr_map_entry_str")); + if (emitter.decls.has(STRING_MAP_ENTRY_C_DECL)) { + const sz = emitter.sizeType; defs.push( - `define internal ptr @sc_map_entry_str(ptr %m, ptr %k) ${FN_ATTRS} {`, + ...stringMapEntry("sc_map_entry_str_c", "scr_map_entry_str_c"), + `define internal ptr @sc_map_iter_entry_c(ptr %m, double %i) ${FN_ATTRS} {`, `entry:`, - ...(emitter.sizeType === "i64" - ? [ - ` %fp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 10`, - ` %filter = load i64, ptr %fp`, - ` %capp = getelementptr inbounds %ScrStr, ptr %k, i32 0, i32 2`, - ` %cap = load i64, ptr %capp`, - ` %cached = icmp uge i64 %cap, 4294967296`, - ` br i1 %cached, label %test, label %slow`, - `test:`, - ` %s1 = lshr i64 %cap, 44`, - ` %i1 = and i64 %s1, 63`, - ` %b1 = shl i64 1, %i1`, - ` %s2 = lshr i64 %cap, 50`, - ` %i2 = and i64 %s2, 63`, - ` %b2 = shl i64 1, %i2`, - ` %bits = or i64 %b1, %b2`, - ` %have = and i64 %filter, %bits`, - ` %maybe = icmp eq i64 %have, %bits`, - ` br i1 %maybe, label %slow, label %absent`, - `absent:`, - ` ret ptr null`, - ] - : [` br label %slow`]), - `slow:`, - ` %r = call ptr @scr_map_entry_str(ptr %m, ptr %k)`, - ` ret ptr %r`, + ` %np = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 8`, + ` %n = load i32, ptr %np`, + ` %nf = uitofp i32 %n to double`, + ` %lo = fcmp oge double %i, 0.0`, + ` %hi = fcmp olt double %i, %nf`, + ` %in = and i1 %lo, %hi`, + ` br i1 %in, label %load, label %none`, + `load:`, + ` %ix = fptoui double %i to ${sz}`, + ` %ep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 11`, + ` %entries = load ptr, ptr %ep`, + ` %e = getelementptr inbounds %ScrMapCEntry, ptr %entries, ${sz} %ix`, + ` %hp = getelementptr inbounds %ScrMapCEntry, ptr %e, i32 0, i32 1`, + ` %h = load i32, ptr %hp`, + ` %live = icmp ne i32 %h, 0`, + ` br i1 %live, label %yes, label %none`, + `yes:`, + ` ret ptr %e`, + `none:`, + ` ret ptr null`, `}`, ``, ); @@ -2278,7 +2471,7 @@ export function emitFfiCallbackDefs(emitter: LlEmitter): { globals: string[]; de export function emitParts(emitter: LlEmitter): string[] { const parts = [ - ...limitInlineDynReleases(emitter.emitModuleParts()), + ...limitInlineReleases(emitter.emitModuleParts(), emitter.shapeHost.rcMode), ...emitter.disposeObserverGates(), ]; if (!emitter.executableTls) return parts; diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index dc91da2fb..c2846a9d4 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -32,12 +32,19 @@ import { allocateLocalUnion, storeLocalUnion } from "./local-union-storage.js"; import { boxAccess, boxNewCall, elemAccess } from "./shapes.js"; import { dynHoleRead } from "./loop-array-borrows.js"; import { endsWithJump, matchStringSelfConcat } from "../../ir/analysis.js"; -import { emitDiscardedSplice } from "./expr-containers.js"; +import { charAtUnitRead, emitDiscardedSplice } from "./expr-containers.js"; import { InternalCompilerError } from "../../errors.js"; -import { emitDenseArrayGet, emitDenseArrayPop, emitDenseArraySet } from "./dense-array-access.js"; +import { + emitDenseArrayGet, + emitDenseArrayPop, + emitDenseArraySet, + SCR_ELEM_I32, +} from "./dense-array-access.js"; import { emitBorrowedInput } from "./borrowed-inputs.js"; -import { f64Lit, llvmCommentText, loadArrCount, sizeToDouble } from "./common.js"; -import { RESULT_DST_ID, RESULT_ENTRY_PREFIX } from "./record-results.js"; +import { f64Lit, llvmCommentText, loadArrCount, SCR_ARR_FIELD, sizeToDouble } from "./common.js"; +import { planInt32KindLoop, type Int32KindLoopPlan } from "./int32-kind-loops.js"; +import { withinInt32 } from "../../ir/integer-ranges.js"; +import { resultSlotStore } from "../result-slots.js"; import { matchIntegerArrayForLoop, matchIntegerBytesForLoop, @@ -51,7 +58,7 @@ import { emitCountedLoopSafety, emitPairedLoopSafety, } from "./counted-loops.js"; -import { emitBytesLoopVersionGuard } from "./bytes-loop-versions.js"; +import { emitBytesLoopVersionGuard, type BytesLoopVersion } from "./bytes-loop-versions.js"; import { mangleLocal } from "../mangle.js"; import { LlvmUnsupportedError } from "./unsupported.js"; import type { LlValue } from "./expr-context.js"; @@ -76,6 +83,7 @@ import { stringParts, } from "./string-construction.js"; import { emitStringInputs } from "./string-lifetimes.js"; +import { LOOPS } from "./loop-state.js"; /** A callback loop version copies the loop; keep the copies small. */ const MAX_CALLBACK_VERSION_COST = 400; @@ -410,7 +418,21 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { const v = emitter.emitExpr(s.value); if (s.arr.type.kind !== "array") throw new InternalCompilerError("llvm emitter bug: arraySet on non-array"); - const acc = elemAccess(s.arr.type.elem); + if (emitter.checkedInt32Array(s.arr)) { + // An int32 array with checked stores: the int32 arm stores an int32 + // value, and widens the array for any other first. + emitter.emitPublishedArrayGuard(arr.name, s.arr.type, 0, idx.name); + const elem = s.arr.type.elem; + const proven = emitter.provenDenseIndex(s.arr, s.index); + emitter.emitByElementKind(arr.name, null, (acc, widened) => { + if (acc === "i32") emitter.emitCheckedInt32Store(arr.name, v.name, s.value, widened); + emitDenseArraySet(emitter, arr.name, idx, s.index, acc, elem, v.name, proven); + return null; + }); + break; + } + const acc = emitter.arrayAccess(s.arr, s.arr.type.elem); + const int32 = emitter.emitInt32KindStoreCheck(s.arr, v, s.value); emitter.emitPublishedArrayGuard(arr.name, s.arr.type, 0, idx.name); if (acc === "ref") emitter.toHeap(v); emitDenseArraySet( @@ -422,6 +444,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { s.arr.type.elem, v.name, emitter.provenDenseIndex(s.arr, s.index), + int32, ); break; } @@ -439,6 +462,11 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { const length = emitter.emitExpr(s.length); if (s.arr.type.kind !== "array") throw new InternalCompilerError("llvm emitter bug: arraySetLength on non-array"); + if (s.reserve === true) { + emitter.declare(`declare void @scr_arr_reserve_len(ptr, double)`); + B.line(`call void @scr_arr_reserve_len(ptr ${arr.name}, double ${length.name})`); + break; + } emitter.emitPublishedArrayGuard(arr.name, s.arr.type, s.pop === true ? 2 : 3); emitter.declare(`declare void @scr_arr_set_len(ptr, double)`); // Assigning the current length (a reused work array reset to where @@ -472,12 +500,19 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { B.condBr(dropsLast, pop, call); B.startBlock(pop); const elem = s.arr.type.elem; - const acc = elemAccess(elem); - const raw = emitDenseArrayPop(emitter, arr.name, acc, call, call); - if (acc === "ref" && isRefCounted(elem)) { - const old = B.tmp(); - B.line(`${old} = inttoptr i64 ${raw} to ptr`); - emitter.releaseHeap(old, elem); + if (emitter.checkedInt32Array(s.arr)) { + emitter.emitByElementKind(arr.name, null, (acc) => { + emitDenseArrayPop(emitter, arr.name, acc, elem, call, call); + return null; + }); + } else { + const acc = emitter.arrayAccess(s.arr, elem); + const raw = emitDenseArrayPop(emitter, arr.name, acc, elem, call, call); + if (acc === "ref" && isRefCounted(elem)) { + const old = B.tmp(); + B.line(`${old} = inttoptr i64 ${raw} to ptr`); + emitter.releaseHeap(old, elem); + } } B.br(done); B.startBlock(call); @@ -573,13 +608,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { ? emitter.classFieldPtr(obj.name, s.className, s.field, "compressed") : emitter.recordFieldPtr(obj.name, s.shapeId, s.field); // A result-slot entry fills zeroed caller storage, each field once. - if ( - s.kind === "recordSet" && - s.obj.kind === "varRef" && - s.obj.localId === RESULT_DST_ID && - emitter.currentFunctionName.startsWith(RESULT_ENTRY_PREFIX) - ) - emitter.initField(ptr, type, inline, v); + if (resultSlotStore(emitter.currentFunctionName, s)) emitter.initField(ptr, type, inline, v); else emitter.replaceField(ptr, type, inline, v); if ( s.kind === "fieldSet" && @@ -786,7 +815,12 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { const trueLabel = B.newLabel("if.t"); const joinLabel = B.newLabel("if.j"); const falseLabel = s.else_ ? B.newLabel("if.f") : joinLabel; - B.condBr(cond, trueLabel, falseLabel); + // A running maximum or minimum is rarely updated; the weights keep + // the branch, where a select would make each element wait on the last. + const weights = emitter.runningExtrema.has(s) + ? `, !prof !{!"branch_weights", i32 1, i32 1000}` + : ""; + B.condBr(cond, trueLabel, falseLabel, weights); B.startBlock(trueLabel); emitter.emitBlock(s.then); B.br(joinLabel); @@ -804,6 +838,16 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { emitCallbackVersionedLoop(emitter, s, callbacks); break; } + const version = + LOOPS.of(emitter).bytesLoopVersionsEnabled && + LOOPS.of(emitter).bytesLoopVersioning.size < MAX_BYTES_LOOP_VERSION_DEPTH && + !LOOPS.of(emitter).bytesLoopVersioning.has(s) + ? emitter.matchBytesLoopVersion(s, null) + : null; + if (version) { + emitVersionedLoop(emitter, s, version); + break; + } const workerBudget = emitter.workerLoopBudget(s); const le = B.newLabel("loop.e"); emitter.emitLoopCopies(s, true, le, (exit) => { @@ -886,33 +930,33 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { integerArrayLoop ?? (emitter.debug === null ? matchIntegerBytesForLoop(s, emitter.numericLocals) : null); const countedLoop = - !integerLoop && emitter.countedLoopsEnabled + !integerLoop && LOOPS.of(emitter).countedLoopsEnabled ? matchIntegerCountedForLoop( s, emitter.numericLocals, emitter.integerRanges, - emitter.unaliasedArrays, + LOOPS.of(emitter).unaliasedArrays, ) : null; // Two counters closing in on each other (`lo++, hi--`) take the same // integer storage as a counted loop's one. const pairedLoop = - !integerLoop && !countedLoop && emitter.countedLoopsEnabled + !integerLoop && !countedLoop && LOOPS.of(emitter).countedLoopsEnabled ? matchIntegerPairedForLoop(s, emitter.numericLocals, emitter.integerRanges) : null; // Byte windows and dense loops reason about one counter. const window = - countedLoop && countedLoop.followers.length === 0 && emitter.byteWindowsEnabled + countedLoop && countedLoop.followers.length === 0 && LOOPS.of(emitter).byteWindowsEnabled ? matchByteWindow( s, countedLoop, emitter.numericLocals, emitter.captureIds, emitter.integerRanges, - emitter.byteWindowEntries.get(s) ?? new Set(), + LOOPS.of(emitter).byteWindowEntries.get(s) ?? new Set(), ) : null; - const denseShape = !emitter.denseLoopsEnabled + const denseShape = !LOOPS.of(emitter).denseLoopsEnabled ? null : integerArrayLoop ? { @@ -929,8 +973,8 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { s, denseShape, emitter.numericLocals, - emitter.unaliasedArrays, - emitter.denseLoopEntries.get(s) ?? new Set(), + LOOPS.of(emitter).unaliasedArrays, + LOOPS.of(emitter).denseLoopEntries.get(s) ?? new Set(), ) : null; if (dense) { @@ -939,18 +983,18 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { slow = B.newLabel("dense.loop.slow"), done = B.newLabel("dense.loop.done"); B.condBr(valid, fast, slow); - emitter.denseLoopsEnabled = false; + LOOPS.of(emitter).denseLoopsEnabled = false; try { B.startBlock(slow); emitter.emitStmt(s); B.br(done); B.startBlock(fast); - emitter.denseLoop = dense; + LOOPS.of(emitter).denseLoop = dense; emitter.emitStmt(s); B.br(done); } finally { - emitter.denseLoop = null; - emitter.denseLoopsEnabled = true; + LOOPS.of(emitter).denseLoop = null; + LOOPS.of(emitter).denseLoopsEnabled = true; } B.startBlock(done); break; @@ -963,7 +1007,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { B.condBr(valid, fast, slow); const originalRanges = emitter.integerRanges, originalBounds = emitter.bytesBounds; - emitter.byteWindowsEnabled = false; + LOOPS.of(emitter).byteWindowsEnabled = false; try { B.startBlock(slow); emitter.emitStmt(s); @@ -976,64 +1020,39 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { } finally { emitter.integerRanges = originalRanges; emitter.bytesBounds = originalBounds; - emitter.byteWindowsEnabled = true; + LOOPS.of(emitter).byteWindowsEnabled = true; } B.startBlock(done); break; } const versionable = - emitter.bytesLoopVersionsEnabled && - emitter.bytesLoopVersioning.size < MAX_BYTES_LOOP_VERSION_DEPTH && - !emitter.bytesLoopVersioning.has(s); + LOOPS.of(emitter).bytesLoopVersionsEnabled && + LOOPS.of(emitter).bytesLoopVersioning.size < MAX_BYTES_LOOP_VERSION_DEPTH && + !LOOPS.of(emitter).bytesLoopVersioning.has(s); // A loop to a typed array's or an array's length keeps its integer // induction. Its counted form only gives the version guard the // counter's iteration space, so the guard also covers accesses of // other typed arrays at the counter, such as `x[i]` and `y[i]` in // `for (let i = 0; i < out.length; i++) out[i] = x[i] * y[i]`. const versionCounted = - versionable && !countedLoop && integerLoop && emitter.countedLoopsEnabled + versionable && !countedLoop && integerLoop && LOOPS.of(emitter).countedLoopsEnabled ? matchIntegerCountedForLoop( s, emitter.numericLocals, emitter.integerRanges, - emitter.unaliasedArrays, + LOOPS.of(emitter).unaliasedArrays, ) : countedLoop; const version = versionable ? emitter.matchBytesLoopVersion(s, versionCounted) : null; if (version) { - const fast = B.newLabel("bytes.version.fast"), - slow = B.newLabel("bytes.version.slow"), - done = B.newLabel("bytes.version.done"); - emitBytesLoopVersionGuard(emitter, version, fast, slow); - const originalBounds = emitter.bytesBounds, - originalOwned = emitter.bytesOwned, - originalRanges = emitter.integerRanges; - emitter.bytesLoopVersioning.add(s); - try { - B.startBlock(slow); - emitter.emitStmt(s); - B.br(done); - B.startBlock(fast); - emitter.bytesBounds = new Set([...originalBounds, ...version.bounds]); - if (version.owned.size > 0) - emitter.bytesOwned = new Set([...originalOwned, ...version.owned]); - if (version.ranges.size > 0) - emitter.integerRanges = new Map([...originalRanges, ...version.ranges]); - if (version.counted?.guarded) emitter.provenCountedGuards.add(s); - emitter.emitStmt(s); - B.br(done); - } finally { - emitter.bytesBounds = originalBounds; - emitter.bytesOwned = originalOwned; - emitter.integerRanges = originalRanges; - emitter.provenCountedGuards.delete(s); - emitter.bytesLoopVersioning.delete(s); - } - B.startBlock(done); + emitVersionedLoop(emitter, s, version); break; } let countedJoin: string | null = null; - if ((countedLoop?.guarded && !emitter.provenCountedGuards.has(s)) || pairedLoop?.guarded) { + if ( + (countedLoop?.guarded && !LOOPS.of(emitter).provenCountedGuards.has(s)) || + pairedLoop?.guarded + ) { const safe = countedLoop ? emitCountedLoopSafety(emitter, countedLoop) : emitPairedLoopSafety(emitter, pairedLoop!); @@ -1042,9 +1061,9 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { countedJoin = B.newLabel("counted.done"); B.condBr(safe, fast, slow); B.startBlock(slow); - emitter.countedLoopsEnabled = false; + LOOPS.of(emitter).countedLoopsEnabled = false; emitter.emitStmt(s); - emitter.countedLoopsEnabled = true; + LOOPS.of(emitter).countedLoopsEnabled = true; B.br(countedJoin); B.startBlock(fast); } @@ -1074,8 +1093,8 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { slot: integerSlot, type: emitter.sizeType, range: - emitter.denseLoop?.counter === integerLoop.localId - ? emitter.denseLoop.range + LOOPS.of(emitter).denseLoop?.counter === integerLoop.localId + ? LOOPS.of(emitter).denseLoop!.range : { min: 0, max: integerArrayLoop ? 4294967294 : Number.MAX_SAFE_INTEGER - 1 }, }); } else if (countedLoop) { @@ -1092,8 +1111,8 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { type: "i64", signed: true, range: - emitter.denseLoop?.counter === countedLoop.localId - ? emitter.denseLoop.range + LOOPS.of(emitter).denseLoop?.counter === countedLoop.localId + ? LOOPS.of(emitter).denseLoop!.range : countedLoop.range, }); for (const follower of countedLoop.followers) { @@ -1836,7 +1855,7 @@ export function emitTryCatch(emitter: LlEmitter, s: IrStmt & { kind: "tryCatch" * case, refcounted/boxed case-body locals are NULL-reset up front and * the scope-exit releases rely on NULL tolerance. */ export function emitSwitch(emitter: LlEmitter, s: IrStmt & { kind: "switch" }): void { - const unitSwitch = charAtUnitSwitch(s); + const unitSwitch = charAtUnitSwitch(emitter, s); if (unitSwitch !== null) return emitSwitch(emitter, unitSwitch); const B = emitter.B; const discKind = s.disc.type.kind; @@ -2040,6 +2059,45 @@ const SPECULATION_BAILS = 8; * when speculative-integers.ts plans a fast integer copy. Both copies * share the loop's setup and exit; the ordinary copy comes first so the * fast copy is only emitted when every resume point exists. */ +/** A typed-array loop version: the guard, then the checked copy, which + * runs whenever the guard fails, and the copy whose accesses the guard + * proves in bounds (or thread-owned). */ +function emitVersionedLoop( + emitter: LlEmitter, + s: IrStmt & { kind: "for" | "while" }, + version: BytesLoopVersion, +): void { + const B = emitter.B; + const fast = B.newLabel("bytes.version.fast"), + slow = B.newLabel("bytes.version.slow"), + done = B.newLabel("bytes.version.done"); + emitBytesLoopVersionGuard(emitter, version, fast, slow); + const originalBounds = emitter.bytesBounds, + originalOwned = emitter.bytesOwned, + originalRanges = emitter.integerRanges; + LOOPS.of(emitter).bytesLoopVersioning.add(s); + try { + B.startBlock(slow); + emitter.emitStmt(s); + B.br(done); + B.startBlock(fast); + emitter.bytesBounds = new Set([...originalBounds, ...version.bounds]); + if (version.owned.size > 0) emitter.bytesOwned = new Set([...originalOwned, ...version.owned]); + if (version.ranges.size > 0) + emitter.integerRanges = new Map([...originalRanges, ...version.ranges]); + if (version.counted?.guarded) LOOPS.of(emitter).provenCountedGuards.add(s); + emitter.emitStmt(s); + B.br(done); + } finally { + emitter.bytesBounds = originalBounds; + emitter.bytesOwned = originalOwned; + emitter.integerRanges = originalRanges; + LOOPS.of(emitter).provenCountedGuards.delete(s); + LOOPS.of(emitter).bytesLoopVersioning.delete(s); + } + B.startBlock(done); +} + export function emitLoopCopies( emitter: LlEmitter, loop: Extract, @@ -2069,9 +2127,26 @@ export function emitLoopCopies( ) : null; if (!plan) { - const proven = allowed ? planProvenLoop(emitter, loop) : null; - if (proven) emitProvenLoop(emitter, proven, exit, emitCopy); - else emitCopy(exit); + const ordinary = (exit: string): void => { + const proven = allowed ? planProvenLoop(emitter, loop) : null; + if (proven) emitProvenLoop(emitter, proven, exit, emitCopy); + else emitCopy(exit); + }; + const kinds = + allowed && + emitter.speculativeLoopsEnabled && + emitter.int32Slots.checkedInt32Arrays.size > 0 && + emitter.int32Kinds === null && + emitter.speculation === null && + emitter.speculationResume === null + ? planInt32KindLoop( + loop, + (id) => emitter.checkedInt32Local(id), + (e) => withinInt32(emitter.integerRanges.get(e)), + ) + : null; + if (kinds) emitInt32KindLoop(emitter, kinds, exit, kinds.writes.size > 0 ? emitCopy : ordinary); + else ordinary(exit); return; } const B = emitter.B; @@ -2095,8 +2170,7 @@ export function emitLoopCopies( // count. After SPECULATION_BAILS bailing entries in a row, the site runs // the ordinary copy only. The count is a hint: both copies give the same // results, and a race between workers loses at most an update. - const bails = `@scr.spec.bails.${emitter.speculativeLoopSites++}`; - emitter.declare(`${bails} = internal global i8 0`); + const bails = emitter.speculativeLoopSite(); const count = B.tmp(), trying = B.tmp(); B.line(`${count} = load atomic i8, ptr ${bails} unordered, align 1`); @@ -2215,6 +2289,124 @@ export function emitLoopCopies( } } +/** An int32 kind loop (int32-kind-loops.ts). The entry test reads the kind + * of each of the plan's arrays: when every one holds int32 slots the fast + * copy runs, when every one holds doubles the wide copy, which reads and + * writes them as double arrays, and otherwise (a plan with several arrays) + * the mixed copy, which tests their kinds at each access. A failed store of + * the fast copy re-executes in the third copy, which finishes its + * iteration with kind tests and then enters the mixed copy, or the wide + * copy when the plan has one array (it just widened). Each copy is emitted + * by `ordinary`: the loop's plain copy when a store may fail, since a + * proven integer loop keeps locals in shadows of its own copy that another + * copy cannot resume from. */ +function emitInt32KindLoop( + emitter: LlEmitter, + plan: Int32KindLoopPlan, + exit: string, + ordinary: (exit: string) => void, +): void { + const B = emitter.B; + const fn = emitter.currentFunctionName; + const keys = new Set([...plan.arrays.keys()].map((id) => `${fn}\0${id}`)); + const several = keys.size > 1; + let allInt32 = "true", + allWide = "true"; + const and = (all: string, next: string): string => { + if (all === "true") return next; + const both = B.tmp(); + B.line(`${both} = and i1 ${all}, ${next}`); + return both; + }; + for (const ref of plan.arrays.values()) { + const arr = emitter.emitStableReceiver(ref, []); + const kindPtr = B.tmp(), + kind = B.tmp(), + isInt32 = B.tmp(); + B.line( + `${kindPtr} = getelementptr inbounds %ScrArr, ptr ${arr.name}, i32 0, i32 ${SCR_ARR_FIELD.elem}`, + ); + emitter.markMemoryPointer(kindPtr, "array:header"); + B.line(`${kind} = load i8, ptr ${kindPtr}${emitter.fieldAliasAttachment(kindPtr)}`); + B.line(`${isInt32} = icmp eq i8 ${kind}, ${SCR_ELEM_I32}`); + allInt32 = and(allInt32, isInt32); + if (several) { + const isWide = B.tmp(); + B.line(`${isWide} = xor i1 ${isInt32}, true`); + allWide = and(allWide, isWide); + } + } + const fast = B.newLabel("kind.fast"), + wide = B.newLabel("kind.wide"), + mixed = several ? B.newLabel("kind.mixed") : null; + if (mixed === null) B.condBr(allInt32, fast, wide); + else { + const other = B.newLabel("kind.other"); + B.condBr(allInt32, fast, other); + B.startBlock(other); + B.condBr(allWide, wide, mixed); + } + const resume = new Map(); + for (const s of plan.writes) resume.set(s, { label: B.newLabel("kind.resume"), placed: false }); + // The header the third copy enters: the mixed copy's, or the wide copy's. + let header: string | null = null; + const capture = (label: string): void => { + header = label; + }; + B.startBlock(wide); + if (resume.size > 0 && mixed === null) emitter.speculationHeader = capture; + emitter.int32Kinds = { int32: new Set(), wide: keys, resume: new Map() }; + try { + ordinary(exit); + } finally { + emitter.speculationHeader = null; + emitter.int32Kinds = null; + } + if (mixed !== null) { + B.startBlock(mixed); + if (resume.size > 0) emitter.speculationHeader = capture; + try { + ordinary(exit); + } finally { + emitter.speculationHeader = null; + } + } + if (resume.size > 0) { + if (header === null) + throw new InternalCompilerError("llvm emitter bug: int32 kind loop copy without a header"); + // A failed store finishes its iteration here, as in a speculative + // integer loop's third copy. + B.startBlock(B.newLabel("kind.tail")); + emitter.speculationResume = resume; + emitter.speculationHeader = () => { + B.br(header!); + B.startBlock(B.newLabel("kind.unused")); + }; + try { + ordinary(exit); + } finally { + emitter.speculationResume = null; + emitter.speculationHeader = null; + } + } + B.startBlock(fast); + if (![...resume.values()].every((r) => r.placed)) { + // A write the third copy never reached: this copy tests the kinds. + ordinary(exit); + return; + } + emitter.int32Kinds = { + int32: keys, + wide: new Set(), + resume: new Map([...resume].map(([s, r]) => [s, r.label])), + }; + try { + ordinary(exit); + } finally { + emitter.int32Kinds = null; + } +} + /** At the top of a loop copy's header: lets a speculative loop find its * ordinary copy's header, and keeps its third copy out of the loop. */ function enterLoopHeader(emitter: LlEmitter, header: string): void { @@ -2490,7 +2682,10 @@ function nullableStringSwitch( * one-unit string literals, as a switch on `s.charCodeAt(i)` with the units * as cases: no character string is built. An index out of range answers * NaN, which matches no case, as "" matches no one-unit literal. */ -function charAtUnitSwitch(s: IrStmt & { kind: "switch" }): (IrStmt & { kind: "switch" }) | null { +function charAtUnitSwitch( + emitter: LlEmitter, + s: IrStmt & { kind: "switch" }, +): (IrStmt & { kind: "switch" }) | null { const disc = s.disc; if (disc.kind !== "strIntrinsic" || disc.method !== "charAt" || disc.args.length !== 1) return null; @@ -2503,7 +2698,7 @@ function charAtUnitSwitch(s: IrStmt & { kind: "switch" }): (IrStmt & { kind: "sw return null; return { ...s, - disc: { ...disc, method: "charCodeAt", type: F64 }, + disc: charAtUnitRead(emitter, disc), cases: s.cases.map((c) => c.test?.kind === "strLit" ? { diff --git a/packages/compiler/src/backend/llvm/emitter-state.ts b/packages/compiler/src/backend/llvm/emitter-state.ts new file mode 100644 index 000000000..62dc47b4e --- /dev/null +++ b/packages/compiler/src/backend/llvm/emitter-state.ts @@ -0,0 +1,24 @@ +/* Per-feature emitter state. A feature defines its state in its own file + * with defineModuleState and reads it through the returned key, so adding + * or extending a feature does not edit LlEmitter's field list or + * constructor. */ +import type { LlEmitter } from "./emitter.js"; + +/** A feature's state on an emitter, read with `key.of(emitter)`. */ +export interface EmitterStateKey { + of(emitter: LlEmitter): T; +} + +let moduleSlots = 0; + +/** State that lives for the whole module, created on first read. */ +export function defineModuleState(create: (emitter: LlEmitter) => T): EmitterStateKey { + const slot = moduleSlots++; + return { + of(emitter) { + const states = emitter.moduleStates; + if (slot >= states.length || states[slot] === undefined) states[slot] = create(emitter); + return states[slot] as T; + }, + }; +} diff --git a/packages/compiler/src/backend/llvm/emitter.test.ts b/packages/compiler/src/backend/llvm/emitter.test.ts index f413d6153..4793663aa 100644 --- a/packages/compiler/src/backend/llvm/emitter.test.ts +++ b/packages/compiler/src/backend/llvm/emitter.test.ts @@ -13,6 +13,7 @@ import { type IrType, } from "../../ir/ir.js"; import { emitLlvmModule, emitLlvmModuleSource, LlEmitter } from "./emitter.js"; +import { INLINE_STR_RELEASES_PER_FUNCTION } from "./shapes.js"; /** A pending-exception check: the inline active-cell test or a runtime call. */ const PENDING_CHECK = /@scr_exc_(?:active|pending)\b/; @@ -201,17 +202,24 @@ test("detached exit defines the runtime opt-in symbol only when requested", () = expect(detached.replace("@scr_detached_exit = constant i8 1\n", "")).toBe(plain); }); -test("inline RC inlines every fast path at speed and keeps string and box releases as calls in release", () => { +test("inline RC inlines every fast path at speed and keeps box releases as calls in release", () => { for (const boxed of [false, true]) { const module = moduleFor(Array.from({ length: 4 }, call), { boxed }); const release = emitLlvmModule(module); expect(release).not.toContain("@sc_rc_"); expect(release).not.toContain("%ScrMapRc"); - // The release subset inlines string retains and dyn retains and releases - // only; these string and box releases stay runtime calls. + // The release subset inlines string and dyn retains and releases and Map + // retains only: box releases stay runtime calls, and so do the string + // releases of a function with more than INLINE_STR_RELEASES_PER_FUNCTION. const subset = emitLlvmModule(module, { inlineRc: "release" }); - expect(subset).not.toContain("@sc_rc_release_str"); - expect(subset).not.toContain("@sc_rc_release_box"); + expect(subset).not.toContain("call void @sc_rc_release_box("); + const sites = (ir: string, fn: string): number => ir.split(`call void @${fn}(ptr`).length - 1; + if (!boxed) { + expect(sites(subset, "sc_rc_release_str")).toBe(0); + // Every site calls the runtime, and so does the unused helper's slow path. + expect(sites(subset, "scr_str_release")).toBe(sites(release, "scr_str_release") + 1); + expect(sites(release, "scr_str_release")).toBeGreaterThan(INLINE_STR_RELEASES_PER_FUNCTION); + } const speed = emitLlvmModule(module, { inlineRc: "speed" }); const helper = boxed ? "sc_rc_release_box" : "sc_rc_release_str"; const runtime = boxed ? "scr_box_release" : "scr_str_release"; @@ -451,6 +459,33 @@ test("class brand probes borrow a local only across an inert literal key", () => expect(computed).toContain("call ptr @scr_dyn_retain_v("); }); +test("class constructor probes test the function tag and borrow across a literal key", () => { + const dyn: IrType = { kind: "dyn" }; + const key: IrExpr = { kind: "strLit", value: "classval:Example", type: STRING, loc: receiverLoc }; + const probe: IrExpr = { + kind: "libCall", + fn: "dyn.classIs", + args: [ref(dyn), key], + type: BOOL, + loc: receiverLoc, + }; + const borrowed = work(probe, dyn); + expect(borrowed).not.toContain("call ptr @scr_dyn_retain_v("); + expect(borrowed).toContain("icmp ne ptr"); + expect(borrowed).toContain("icmp eq i32 %t"); + expect(borrowed).toMatch(/icmp eq i32 %t\d+, 8\b/); + expect(borrowed).toContain("call zeroext i1 @scr_dyn_class_is_key("); + const computed = work( + { + ...probe, + args: [ref(dyn), { kind: "toString", operand: ref(dyn), type: STRING, loc: receiverLoc }], + }, + dyn, + ); + expect(computed).toContain("call ptr @scr_dyn_retain_v("); + expect(computed).toContain("call zeroext i1 @scr_dyn_class_is_key("); +}); + test("class brand probes compare the program's key constant before the key text", () => { const dyn: IrType = { kind: "dyn" }; const key: IrExpr = { kind: "strLit", value: "object:Example", type: STRING, loc: receiverLoc }; diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index bfde59a30..c0cc9fef2 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -57,7 +57,6 @@ import { } from "../../ir/ir.js"; import { moduleRuntimeFeatures, type RuntimeFeatures } from "../../ir/runtime-features.js"; import { captureFreeFunctions, observesStacks } from "../stack-observations.js"; -import { type DenseArrayLoop } from "./dense-array-loops.js"; import { findInitializedByteLoopBindings } from "./byte-windows.js"; import { matchBytesLoopVersion } from "./bytes-loop-versions.js"; import { globalSlotAlias } from "./bytes-alias.js"; @@ -71,10 +70,29 @@ import { import { scalarizeNumericRecords } from "../../ir/scalar-records.js"; import { addRecordResultEntries } from "./record-results.js"; import { specializeCallbackCalls } from "../../ir/callback-specialization.js"; +import { + type ChunkResult, + EmitJournal, + registerReplay, + renameChunk, + replayJournal, +} from "./emit-journal.js"; import { specializeNumericCalls } from "../../ir/numeric-call-specialization.js"; +import { versionLoopBoundParameters } from "../../ir/parameter-versions.js"; +import { + allocatedLiteral, + fillAllocationLength, + selectInt32Arrays, +} from "../../ir/int32-arrays.js"; import { everyExpr, everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse.js"; -import { INT32_RANGE, type IntegerRange, type IntegerRanges } from "../../ir/integer-ranges.js"; +import { + INT32_RANGE, + withinInt32, + type IntegerRange, + type IntegerRanges, +} from "../../ir/integer-ranges.js"; import { unshareNodes } from "../../ir/unshare.js"; +import { runPairedWhileLoopsAsFor } from "../../ir/paired-while-loops.js"; import { copyStableCaptures } from "../../ir/capture-values.js"; import { resolveSelfCaptures } from "../../ir/self-captures.js"; import { analyzeInt32Slots, dynamicFieldStores, type Int32Slots } from "../../ir/int32-slots.js"; @@ -83,6 +101,7 @@ import { foldConstantGlobals } from "../../ir/constant-globals.js"; import { dropUnwrittenPropertyBags } from "../../ir/property-bags.js"; import { freezeInitOnlyGlobals } from "../../ir/init-only-globals.js"; import { hoistFinalFieldReads } from "../../ir/field-values.js"; +import { holdCharLocalsAsUnits } from "../../ir/char-unit-locals.js"; import { findInt32Abis, INT32_ABI_SUFFIX, @@ -92,7 +111,7 @@ import { } from "./int32-abi.js"; import { type Int32Local } from "./int32-locals.js"; import { findIntegerClones, NO_INTEGER_CLONES, type IntegerClones } from "./integer-clones.js"; -import { exactInteger, narrowInteger } from "./integer-values.js"; +import { exactInteger, narrowInteger, widenInteger } from "./integer-values.js"; import { findInitializerBindings } from "../../ir/initializer-bindings.js"; import { findConstantNumericTables, type ConstantNumericTable } from "../../ir/constant-tables.js"; import { @@ -139,7 +158,12 @@ import { } from "./inline-unions.js"; import { OptionalArrayReads, type LocalArrayRead } from "./local-array-reads.js"; import { CheckedNarrows } from "./checked-narrows.js"; -import { emitDenseArraySet, emitDenseSlotUnlink } from "./dense-array-access.js"; +import { + emitDenseArraySet, + emitDenseSlotUnlink, + SCR_ELEM_I32, + type ArrayAccess, +} from "./dense-array-access.js"; import { matchMapRead, type MapReadLifetimes } from "./map-read-lifetimes.js"; import { type MapEntryFusion } from "./map-entry-fusion.js"; import { emitBorrowedFieldSequence, guardedValue } from "./borrowed-receivers.js"; @@ -160,7 +184,7 @@ import { } from "./string-slices.js"; import { LazyCaptures } from "./lazy-captures.js"; import { StackCaptures } from "./stack-captures.js"; -import { emitFieldAbsentTest, f64Lit } from "./common.js"; +import { emitFieldAbsentTest, f64Lit, SCR_ARR_FIELD } from "./common.js"; import { ffiExtendsNarrowIntegers } from "../targets.js"; import { emitLiteralExpr, @@ -175,8 +199,10 @@ import { emitDynamicExpr } from "./expr-dynamic.js"; import { emitIntrinsicExpr, emitSerializationExpr, emitAsyncExpr } from "./expr-async.js"; import { PublishWalkers } from "./publish.js"; import { computeImmutableFields, stableGlobals, type ImmutableFields } from "../immutability.js"; +import { computeCycleMutability, hierarchyUnitOfClass } from "../cycle-mutability.js"; import { computePublishedTypes, type PublishedTypes } from "../publication.js"; import { emitExpr } from "./expr-dispatch.js"; +import { LOOPS } from "./loop-state.js"; import { dynKind, @@ -392,9 +418,32 @@ export interface LlScopeEntry { lazy?: boolean; } +/** Process primitives for forked emission workers. */ +export interface ForkPrimitives { + /** 0 in the worker, the worker's pid in the parent, -1 on failure. */ + fork(): number; + /** Ends the worker without running exit handlers. */ + exit(code: number): void; + /** The worker's exit status, 128 plus a signal number, or -1. */ + wait(pid: number): number; +} + export interface LlvmTargetOptions { /** C ABI target; omitted direct emission uses the host ABI. */ targetTriple?: string; + /** Emit the function bodies in this many contiguous ranges, each by its + * own emitter, and merge them in function order (emit-journal.ts). The + * output is the same as one emitter's. Debug builds (`debugSources`) + * emit in one emitter. */ + functionChunks?: number; + /** Emit the function bodies of a large module on this many forked worker + * processes at a time (emit-module.ts), forked with `forkPrimitives`. The + * output is the same as one emitter's. Debug builds (`debugSources`) + * emit in one emitter. */ + functionWorkers?: number; + /** The host's process primitives, where it can fork (the native + * compiler's). */ + forkPrimitives?: ForkPrimitives | undefined; /** Exact frontend sources for dev-build locations and variable metadata. */ debugSources?: ReadonlyMap; /** Pointer width of the target C ABI. Native targets are 64-bit today. */ @@ -442,33 +491,75 @@ export function lowRegionTarget(platform: string, sanitize: boolean): boolean { return platform === "linux" && !sanitize; } -export function emitLlvmModule(input: IrModule, options: LlvmTargetOptions = {}): string { +/** Function versions on loop-bounding number parameters + * (ir/parameter-versions.ts). Library builds have no int32 entries, and a + * module that observes call stacks could see the version's frame. */ +function versionParameters(mod: IrModule): IrModule { + if (mod.lib !== undefined || observesStacks(mod)) return mod; + return versionLoopBoundParameters(mod, mangleFunction); +} + +/** The int32 arrays the int32 slot analysis keeps in `mod` (proven and + * checked), under the inputs LlEmitter gives it (selectInt32Arrays). */ +function provenInt32Arrays(mod: IrModule): ReadonlySet { + const slots = analyzeInt32Slots(mod, { + dynamicStores: dynamicFieldStores(mod), + initializerBindings: findInitializerBindings(mod), + }); + return new Set([...slots.int32Arrays, ...slots.checkedInt32Arrays]); +} + +/** The emitter for `input` after the module passes every emission runs. */ +function moduleEmitter(input: IrModule, options: LlvmTargetOptions): LlEmitter { // Facts are keyed by node identity: emit the same module whether it was // lowered in memory or read back from its serialized form. const mod = unshareNodes(input); // Keep source storage intact for debugger inspection in dev builds. return new LlEmitter( options.debugSources === undefined - ? addRecordResultEntries( - scalarizeNumericRecords( - specializeCallbackCalls( - specializeNumericCalls( - foldConstantGlobals( - copyStableCaptures( - resolveSelfCaptures( - hoistFinalFieldReads(freezeInitOnlyGlobals(dropUnwrittenPropertyBags(mod))), + ? selectInt32Arrays( + versionParameters( + addRecordResultEntries( + scalarizeNumericRecords( + specializeCallbackCalls( + specializeNumericCalls( + foldConstantGlobals( + copyStableCaptures( + resolveSelfCaptures( + hoistFinalFieldReads( + freezeInitOnlyGlobals( + holdCharLocalsAsUnits( + dropUnwrittenPropertyBags(runPairedWhileLoopsAsFor(mod)), + ), + ), + ), + ), + ), ), + mangleFunction, ), + mangleFunction, ), - mangleFunction, ), - mangleFunction, ), ), + provenInt32Arrays, ) : mod, options, - ).emit(); + ); +} + +export function emitLlvmModule(input: IrModule, options: LlvmTargetOptions = {}): string { + return moduleEmitter(input, options).emit(); +} + +/** emitLlvmModule's text as parts that concatenate to it, without the copy + * that joining them makes: a large program's text is 150 MB. */ +export function emitLlvmModuleParts(input: IrModule, options: LlvmTargetOptions = {}): string[] { + return moduleEmitter(input, options) + .emitParts() + .flatMap((part, index) => (index === 0 ? [part] : ["\n", part])); } /** Keep large translation units below the host's single-string limit. */ @@ -478,29 +569,7 @@ export function emitLlvmModuleSource( ): string | readonly string[] { // IR validation, contracts and the frontend's disposal run after lowering without checks. relieveHeapPressure(true); - const mod = unshareNodes(input); - const parts = new LlEmitter( - options.debugSources === undefined - ? addRecordResultEntries( - scalarizeNumericRecords( - specializeCallbackCalls( - specializeNumericCalls( - foldConstantGlobals( - copyStableCaptures( - resolveSelfCaptures( - hoistFinalFieldReads(freezeInitOnlyGlobals(dropUnwrittenPropertyBags(mod))), - ), - ), - ), - mangleFunction, - ), - mangleFunction, - ), - ), - ) - : mod, - options, - ).emitParts(); + const parts = moduleEmitter(input, options).emitParts(); const length = parts.reduce((total, part) => total + part.length, Math.max(0, parts.length - 1)); if (length <= 256 * 1024 * 1024) return parts.join("\n"); return parts.flatMap((part, index) => (index === 0 ? [part] : ["\n", part])); @@ -616,6 +685,8 @@ function countLocalMentions(body: readonly IrStmt[], id: string): number { } export class LlEmitter { + /** Per-feature state (emitter-state.ts), indexed by each key's slot. */ + readonly moduleStates: unknown[] = []; localArrayReads = new Map(); loopArrayBorrows: ReadonlySet = new Set(); scalarStringSlices = new Map(); @@ -641,9 +712,11 @@ export class LlEmitter { * 32-bit references to compact class instances (LlClassMeta.compressed): * loadField/loadRef decode them and storeField/storeRef encode them. */ readonly compressedFieldPtrs = new Set(); - /** Whether a class has compressed fields: main reserves the region before - * compiled code runs (scr_plr_init). */ - usesCompactRefs = false; + /** Compact class instances live in the program-lifetime region below + * 32 GiB (LlvmTargetOptions.lowRegion, and the program has compact + * classes): main reserves it before compiled code runs (scr_plr_init), and + * fields and arrays of compactRefType hold 32-bit references. */ + compactRegion = false; /** Native class fields with different property names cannot occupy the * same slot of one object. Inherited views of the same property share a @@ -660,6 +733,7 @@ export class LlEmitter { tag = 2 + this.fieldAliasTags.size * 2; this.fieldAliasTags.set(field, tag); } + this.journal?.entry("tag", field, tag); this.fieldPointerTags.set(ptr, tag); } @@ -672,14 +746,237 @@ export class LlEmitter { tag = 2 + this.fieldAliasTags.size * 2; this.fieldAliasTags.set(category, tag); } + this.journal?.entry("tag", category, tag); return `, !tbaa !${tag}`; } + /** The current function's int32 array allocations: the array literals + * its int32 arrays are written with. They construct SCR_ELEM_I32 arrays. */ + int32Allocations = new Set(); + /** The fill temporaries (`new Array(n).fill(v)`) of those allocations: + * the same arrays, so int32 too. */ + int32ArrayTemps = new Set(); + /** The same for the current function's int32 arrays with checked stores, + * which are only allocated by fills (ir/int32-arrays.ts + * checkableInt32Allocations): their temporaries' literals, with the fill's + * length (a literal constructs SCR_ELEM_I32 storage when that length is at + * least CHECKED_INT32_MIN_LENGTH, SCR_ELEM_F64 otherwise), and their + * temporaries. */ + checkedAllocations = new Map(); + checkedArrayTemps = new Set(); + /** The fill calls (`.fill(v)`) of those allocations: a value that is not + * an int32 widens the new array first. */ + checkedFills = new Set(); + + /** Collect the int32 allocations of `fn`, about to be emitted. */ + findInt32Allocations(fn: IrFunction): void { + this.int32Allocations.clear(); + this.int32ArrayTemps.clear(); + this.checkedAllocations.clear(); + this.checkedArrayTemps.clear(); + this.checkedFills.clear(); + const { int32Arrays, checkedInt32Arrays } = this.int32Slots; + if (int32Arrays.size === 0 && checkedInt32Arrays.size === 0) return; + everyStmtList(fn.body, { + stmt: (s) => { + if (s.kind !== "varDecl" && s.kind !== "assign") return true; + const key = `${fn.name}\0${s.localId}`; + const checked = checkedInt32Arrays.has(key); + if (!checked && !int32Arrays.has(key)) return true; + const value = s.kind === "varDecl" ? s.init : s.value; + const allocation = value && allocatedLiteral(value); + if (!allocation) return true; + const length = checked ? fillAllocationLength(value) : null; + if (length !== null) this.checkedAllocations.set(allocation.literal, length); + else if (!checked) this.int32Allocations.add(allocation.literal); + if (allocation.temporary !== null) + (checked ? this.checkedArrayTemps : this.int32ArrayTemps).add(allocation.temporary); + if (checked && value !== allocation.literal) this.checkedFills.add(value!); + return true; + }, + expr: () => true, + }); + } + + /** Whether `arr` is one of the current function's int32 arrays + * (ir/int32-arrays.ts, kept by the int32 slot analysis). */ + int32Array(arr: IrExpr): boolean { + if (arr.kind !== "varRef") return false; + if (this.int32KindArray(arr)) return true; + if (this.int32Slots.int32Arrays.size === 0) return false; + return ( + this.int32ArrayTemps.has(arr.localId) || + this.int32Slots.int32Arrays.has(`${this.currentFunctionName}\0${arr.localId}`) + ); + } + + /** Whether `arr` is one of the current function's int32 arrays with + * checked stores: int32 slots until a store widens it to doubles. Every + * access tests its kind (emitByElementKind), except in the fast copy of + * an int32 kind loop, which reads and writes it as an int32 array. */ + checkedInt32Array(arr: IrExpr): boolean { + if (arr.kind !== "varRef" || this.int32KindArray(arr)) return false; + if (this.int32Kinds?.wide.has(`${this.currentFunctionName}\0${arr.localId}`)) return false; + return this.checkedInt32Local(arr.localId); + } + + /** Whether the local `localId` holds one of the current function's int32 + * arrays with checked stores. */ + checkedInt32Local(localId: string): boolean { + if (this.int32Slots.checkedInt32Arrays.size === 0) return false; + return ( + this.checkedArrayTemps.has(localId) || + this.int32Slots.checkedInt32Arrays.has(`${this.currentFunctionName}\0${localId}`) + ); + } + + /** Whether `arr` is an int32 array with checked stores that the fast copy + * of the int32 kind loop being emitted holds as an int32 array. */ + int32KindArray(arr: IrExpr): boolean { + return ( + this.int32Kinds !== null && + arr.kind === "varRef" && + this.int32Kinds.int32.has(`${this.currentFunctionName}\0${arr.localId}`) + ); + } + + /** Before a store of `value` (of `expr`) into `arr`, in the fast copy of + * an int32 kind loop: continue when it is an int32 that is not -0, and + * otherwise leave for the statement's resume point, where it re-executes + * with kind tests. Returns the value as an i32 when it is at hand as an + * integer, or null when the store converts the double itself. */ + emitInt32KindStoreCheck(arr: IrExpr, value: LlValue, expr: IrExpr): string | null { + if (!this.int32KindArray(arr) || withinInt32(this.integerRanges.get(expr))) return null; + const integer = exactInteger(this, value, expr); + if (integer !== null && withinInt32(integer.range)) return narrowInteger(this, integer); + const resume = this.int32KindResume; + if (resume === null) + throw new InternalCompilerError("llvm emitter bug: an int32 kind store without a resume"); + const B = this.B; + const ok = B.tmp(), + int = B.tmp(); + if (integer !== null) { + // An exact integer: a range test on its 64-bit form. + const wide = widenInteger(this, integer), + low = B.tmp(), + high = B.tmp(); + B.line(`${low} = icmp sge i64 ${wide}, -2147483648`); + B.line(`${high} = icmp sle i64 ${wide}, 2147483647`); + B.line(`${ok} = and i1 ${low}, ${high}`); + const store = B.newLabel("arr.store.i32"); + B.condBr(ok, store, resume); + B.startBlock(store); + B.line(`${int} = trunc i64 ${wide} to i32`); + return int; + } + this.declare("declare i32 @llvm.fptosi.sat.i32.f64(double)"); + const back = B.tmp(), + exact = B.tmp(), + bits = B.tmp(), + notNegativeZero = B.tmp(); + B.line(`${int} = call i32 @llvm.fptosi.sat.i32.f64(double ${value.name})`); + B.line(`${back} = sitofp i32 ${int} to double`); + B.line(`${exact} = fcmp oeq double ${back}, ${value.name}`); + B.line(`${bits} = bitcast double ${value.name} to i64`); + B.line(`${notNegativeZero} = icmp ne i64 ${bits}, -9223372036854775808`); + B.line(`${ok} = and i1 ${exact}, ${notNegativeZero}`); + const store = B.newLabel("arr.store.i32"); + B.condBr(ok, store, resume); + B.startBlock(store); + return int; + } + + /** Emit an access to the checked int32 array `array` once per element + * kind: `arm("i32", widened)` while it holds int32 slots and + * `arm("f64", widened)` after it widened, joined after both. `widened` is + * the label of the double arm, where an int32 arm's store of a value that + * is not an int32 continues after widening the array. Returns the phi of + * the arms' results (of LLVM type `ty`), or null when `ty` is null. */ + emitByElementKind( + array: string, + ty: string | null, + arm: (acc: "i32" | "f64", widened: string) => string | null, + ): string | null { + const B = this.B; + const narrow = B.newLabel("arr.kind.i32"), + wide = B.newLabel("arr.kind.f64"), + join = B.newLabel("arr.kind.join"); + const kindPtr = B.tmp(), + kind = B.tmp(), + isInt32 = B.tmp(); + B.line( + `${kindPtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 ${SCR_ARR_FIELD.elem}`, + ); + this.markMemoryPointer(kindPtr, "array:header"); + B.line(`${kind} = load i8, ptr ${kindPtr}${this.fieldAliasAttachment(kindPtr)}`); + B.line(`${isInt32} = icmp eq i8 ${kind}, ${SCR_ELEM_I32}`); + B.condBr(isInt32, narrow, wide); + B.startBlock(narrow); + const narrowValue = arm("i32", wide); + const narrowEnd = B.isTerminated() ? null : B.currentLabel(); + if (narrowEnd !== null) B.br(join); + B.startBlock(wide); + const wideValue = arm("f64", wide); + const wideEnd = B.isTerminated() ? null : B.currentLabel(); + if (wideEnd !== null) B.br(join); + B.startBlock(join); + // Arms that end in their own branches (a read's shared join) leave this + // join without predecessors. + if (narrowEnd === null && wideEnd === null) B.terminate("unreachable"); + if (ty === null) return null; + const arms: string[] = []; + if (narrowEnd !== null) arms.push(`[ ${narrowValue}, %${narrowEnd} ]`); + if (wideEnd !== null) arms.push(`[ ${wideValue}, %${wideEnd} ]`); + const result = B.tmp(); + B.line(`${result} = phi ${ty} ${arms.join(", ")}`); + return result; + } + + /** Before storing the double `value` (of `expr`) into an int32 arm + * (emitByElementKind) of the checked int32 array `array`: continue in this + * arm when it is an int32 that is not -0, or widen the array and continue + * at `widened`. A value proven an int32 needs no test. */ + emitCheckedInt32Store(array: string, value: string, expr: IrExpr, widened: string): void { + if (withinInt32(this.integerRanges.get(expr))) return; + const B = this.B; + this.declare("declare i32 @llvm.fptosi.sat.i32.f64(double)"); + this.declare("declare void @scr_arr_widen_i32(ptr)"); + const int = B.tmp(), + back = B.tmp(), + exact = B.tmp(), + bits = B.tmp(), + notNegativeZero = B.tmp(), + ok = B.tmp(); + B.line(`${int} = call i32 @llvm.fptosi.sat.i32.f64(double ${value})`); + B.line(`${back} = sitofp i32 ${int} to double`); + B.line(`${exact} = fcmp oeq double ${back}, ${value}`); + B.line(`${bits} = bitcast double ${value} to i64`); + B.line(`${notNegativeZero} = icmp ne i64 ${bits}, -9223372036854775808`); + B.line(`${ok} = and i1 ${exact}, ${notNegativeZero}`); + const store = B.newLabel("arr.store.i32"), + widen = B.newLabel("arr.widen"); + B.condBr(ok, store, widen); + B.startBlock(widen); + B.line(`call void @scr_arr_widen_i32(ptr ${array})`); + B.br(widened); + B.startBlock(store); + } + + /** The access an element operation on `arr` (of element type `elem`) + * takes: int32 arrays read and write their 4-byte slots. */ + arrayAccess(arr: IrExpr, elem: IrType): ArrayAccess { + if (this.checkedInt32Array(arr)) + throw new InternalCompilerError( + "llvm emitter bug: a checked int32 array without its kind test", + ); + return this.int32Array(arr) ? "i32" : elemAccess(elem); + } + /** Whether an element access at `index` of `arr` lies in the hole-free * prefix: the counter of a dense array loop's fast copy indexing one of * the arrays its entry test covered. */ provenDenseIndex(arr: IrExpr, index: IrExpr): boolean { - const loop = this.denseLoop; + const loop = LOOPS.of(this).denseLoop; return ( loop !== null && arr.kind === "varRef" && @@ -782,6 +1079,10 @@ export class LlEmitter { readonly cstrs = new Map(); /** Interned ScrMapOps descriptors (scr_map.h): initializer → symbol. */ readonly mapOpsConsts = new Map(); + /** Method-id slots of checked-dynamic method calls + * (scr_dyn_invoke_prepared_id): method name → symbol of an i32 the + * runtime fills with the name's id on the first call. */ + readonly methodIdSlots = new Map(); /** Type-directed walker functions (JSON serializers, the indent * rewriter, union ToString/join) — interned per typeKey/unionId, defs * flushed with the shape helpers. */ @@ -831,6 +1132,9 @@ export class LlEmitter { readonly callLifetimes: CallLifetimes; readonly constantCallbacks: Map>; currentConstantCallbacks: ReadonlyMap = new Map(); + /** The current function's running maximum and minimum updates, whose + * branches are weighted as rarely taken (running-extrema.ts). */ + runningExtrema: ReadonlySet = new Set(); /** Callback bindings whose closures an enclosing callback loop version * proved to hold their known function: calls take the direct entry with * no fallback (emitCallbackVersionedLoop). */ @@ -972,6 +1276,11 @@ export class LlEmitter { get usesDynBuiltinStream(): boolean { return this.runtimeFeatures.dynBuiltinStream; } + /** A checked-dynamic for-await may hold a native stream: the program + * links the stream unit (and the dyn-async one the loop needs). */ + get dynForAwaitStreams(): boolean { + return this.runtimeFeatures.stream && this.runtimeFeatures.dynAsync; + } /** Runtime-arm dispatchers for live union values. */ readonly liveDynUnionRefAdapters = new Map(); readonly dynPromiseAdapters = new Map(); @@ -1073,14 +1382,6 @@ export class LlEmitter { * immortal from construction (classes.ts programLifetimeClass). */ readonly programLifetime: ReadonlySet; numericLocals = new Map(); - /** Local arrays of the current function that no other binding can hold - * (ir/array-length-stability.ts). */ - unaliasedArrays: ReadonlySet = new Set(); - /** Dense array loops (dense-array-loops.ts) run in this function. */ - denseLoopsEnabled = false; - denseLoopEntries: ReturnType = new Map(); - /** The fast copy of a dense array loop being emitted. */ - denseLoop: DenseArrayLoop | null = null; captureIds = new Set(); /** Active induction bindings retain their width independently of size_t. */ integerLoopBindings = new Map< @@ -1098,7 +1399,6 @@ export class LlEmitter { * async or generator body resumes with its locals in its frame, which * an entry alloca is not part of. */ integerTempsEnabled = false; - countedLoopsEnabled = false; /** Speculative integer loops (speculative-integers.ts) run in this function. */ speculativeLoopsEnabled = false; /** The fast copy of a speculative integer loop being emitted: its @@ -1122,6 +1422,14 @@ export class LlEmitter { * (`enterLoopHeader`): records the ordinary copy's header, or sends the * third copy's header there. */ speculationHeader: ((header: string) => void) | null = null; + /** A copy of an int32 kind loop (int32-kind-loops.ts) being emitted: the + * int32 arrays with checked stores it reads and writes as int32 arrays + * (the fast copy) or as double arrays (an ordinary copy entered only + * after they widened), as `function\0local` keys, and where each of the + * fast copy's writes resumes in the third copy. */ + int32Kinds: { int32: Set; wide: Set; resume: Map } | null = null; + /** The resume label of the fast-copy write being emitted. */ + int32KindResume: string | null = null; /** The function being emitted, for speculative globals. */ speculativeFunction = ""; /** Every function that writes each number global (computed on demand). */ @@ -1139,20 +1447,14 @@ export class LlEmitter { int32Elements: ((arr: IrExpr) => boolean) | null = null; /** Sums whose result only feeds 32-bit conversions (wrapping-sums.ts). */ wrappingSums = new Set(); - byteWindowsEnabled = true; - byteWindowEntries: ReturnType = new Map(); - /** Typed-array loop versioning (bytes-loop-versions.ts) for the current - * function: off in debug and suspending functions and inside a copy. */ - bytesLoopVersionsEnabled = false; - /** Loops being versioned around the statement being emitted: a loop is - * never versioned inside its own copies, and versions nest boundedly. */ - readonly bytesLoopVersioning = new Set(); - bytesLoopEntries: ReturnType | null = null; - bytesLoopFunction: IrFunction | null = null; - /** Counted loops whose integer-form safety test an enclosing version - * guard already passed. */ - readonly provenCountedGuards = new Set(); + /** Number locals of the current function that hold exact integers or NaN + * (findIntegerOrNaNLocals in wrapping-sums.ts). */ + integerOrNaNLocals: ReadonlyMap = new Map(); bytesBounds: ReadonlySet = new Set(); + /** The charCodeAt reads standing for charAt reads the emitter rewrote + * (one-unit tests and switches, charAtUnitRead), with the charAt each + * stands for: a loop version's proof for the charAt covers its read. */ + readonly charAtUnitReads = new Map(); /** Worker programs: typed-array accesses whose views an enclosing version * guard proved thread-owned. */ bytesOwned: ReadonlySet = new Set(); @@ -1223,13 +1525,29 @@ export class LlEmitter { readonly chainSlots = new Map(); logArgSlots = 0; readonly stackTraces: boolean; + /** The options this emitter was built with (chunk emitters reuse them). */ + readonly targetOptions: LlvmTargetOptions; + /** Records the module-level effects of emitting a range of functions, + * which the emitter that merges the range replays (emit-journal.ts). Null + * when this emitter emits the whole module itself. */ + journal: EmitJournal | null; + /** The emitter's hosts forward to `journal` (journal set, or workers). */ + private readonly journaled: boolean; /** Stack-trace mode: functions no capture can include (no frame push). */ readonly unframedFunctions: ReadonlySet; constructor( readonly mod: IrModule, options: LlvmTargetOptions, + journal: EmitJournal | null = null, ) { + this.targetOptions = options; + this.journal = journal; + this.journaled = + journal !== null || + ((options.functionWorkers ?? 1) > 1 && + options.forkPrimitives !== undefined && + options.debugSources === undefined); this.stackTraces = observesStacks(mod); this.nullableUnions = new NullableUnions(mod, options.debugSources === undefined); this.mockTimers = this.runtimeFeatures.mockTimers; @@ -1312,8 +1630,10 @@ export class LlEmitter { // and per async fiber; WASI, Windows and library builds have none. const triple = (options.targetTriple ?? "").toLowerCase(); const windows = triple === "" ? process.platform === "win32" : triple.includes("windows"); + this.publishedTypes = computePublishedTypes(mod); const mt = computeMayThrow(mod, { stackChecks: !this.wasi && mod.lib === undefined && !windows, + published: this.publishedTypes, }); this.mayThrow = mt.fns; this.workerEntryPolls = mt.workerEntryPolls ?? null; @@ -1322,11 +1642,12 @@ export class LlEmitter { // runtime moves deep prefix nests to a fresh stack using the guard, // which Windows lacks. this.stacklessAsyncAllowed = this.stacklessGenerators && mod.lib === undefined && !windows; - this.publishedTypes = computePublishedTypes(mod); this.unframedFunctions = this.stackTraces ? captureFreeFunctions(mod, this.stackChecks, this.publishedTypes) : new Set(); - this.immutableFields = computeImmutableFields(mod, dynamicStores); + // One field-mutability answer serves immutable fields and traced shapes. + const cycleMutability = computeCycleMutability(mod, hierarchyUnitOfClass(mod), dynamicStores); + this.immutableFields = computeImmutableFields(mod, dynamicStores, cycleMutability); this.constructorFunctions = new Set((mod.classes ?? []).map((c) => `%${c.name}.constructor`)); this.indirectMayThrow = mt.indirect || mod.workers === true; for (const cls of mod.classes ?? []) { @@ -1451,7 +1772,7 @@ export class LlEmitter { this.receiverReaders = new AmbientReceiverReaders(this.fnByName); this.stackCaptures = new StackCaptures(this.fnByName, this.callLifetimes, this.stackCallbacks); this.lazyCaptures = new LazyCaptures(this.fnByName); - const traced = computeTraced(mod, dynamicStores); + const traced = computeTraced(mod, dynamicStores, cycleMutability); this.tracedShapes = traced.shapes; this.tracedUnions = traced.unions; // A shape union's value is whichever record it holds, and the collector @@ -1476,6 +1797,22 @@ export class LlEmitter { } } this.shapeHost = { + // Journal hooks only on emitters that can emit ranges (chunk + // emitters, and emitters whose workers take a journal after the + // fork), so the host's optional calls cost nothing otherwise. + journalAllocated: !this.journaled + ? undefined + : (name, op, args) => this.journal?.allocated(name, op, args), + journalBuilt: !this.journaled ? undefined : () => this.journal?.built(), + journalOpened: !this.journaled ? undefined : (op) => this.journal?.opened(op), + journalUsed: !this.journaled + ? undefined + : (name, op, args) => this.journal?.used(name, op, args), + journalEntry: !this.journaled + ? undefined + : (table, key, value) => this.journal?.entry(table, key, value), + compactArrayElem: (elem) => this.compactArrayElem(elem), + compactRegion: () => this.compactRegion, sizeType: this.sizeType, cycleBufOffset: this.cycleBufOffset, tracedShapes: this.tracedShapes, @@ -1514,9 +1851,15 @@ export class LlEmitter { liveDynRefAdapter: (type) => this.liveDynRefAdapter(type), usesDynBuiltinStream: () => this.usesDynBuiltinStream, liveDynUnionRefAdapter: (type) => this.liveDynUnionRefAdapter(type), + declBoundary: () => this.mod.declBoundary === true, dynPromiseAdapter: (type) => this.dynPromiseAdapter(type), isErrorClass: (name) => this.classMeta.get(name)?.root.def.name === "%Error", classRootOf: (name) => this.classMeta.get(name)?.root.def.name, + releasesNothing: (t) => { + if (t.kind !== "object" || (this.shapeHost.regions ?? null) !== null) return false; + const meta = this.classMeta.get(t.className); + return meta !== undefined && programLifetimeClass(this.shapeHost, meta); + }, classSubtypes: (name) => { const target = this.classMetaOf(name); const intervals = classMembershipIntervals(this.classMeta, target.def.name); @@ -1605,22 +1948,19 @@ export class LlEmitter { ? (cls, i, isCompact) => { const f = cls.fields[i]!; if (this.inlineUnions.classField(cls.name, f.name)) return false; - const t = f.type; - if (t.kind === "object") return isCompact(t.className); - if (t.kind !== "union") return false; - const single = this.nullableUnions.single(t.unionId); - if (single !== null) - return ( - single.nullTag < 0 && - single.arm.kind === "object" && - isCompact(single.arm.className) - ); - const nullable = this.nullableFields.get(cls.name, f.name); + if (this.compactRefType(f.type, isCompact)) return true; + const nullable = + f.type.kind === "union" && this.nullableUnions.single(f.type.unionId) === null + ? this.nullableFields.get(cls.name, f.name) + : null; return nullable !== null && isCompact(nullable.arm.className); } : undefined, ); - this.usesCompactRefs = [...this.classMeta.values()].some((m) => m.compressed.some(Boolean)); + this.compactRegion = + mod.lib === undefined && + options.lowRegion === true && + [...this.classMeta.values()].some((m) => m.compact); for (const [name, rec] of RUNTIME_ERROR_CLASSES) { const meta = this.classMeta.get(name); if (!meta) break; // hand-written IR without the builtin defs: no stamps @@ -2232,7 +2572,7 @@ export class LlEmitter { B.condBr(thrown, rejected, clean); B.startBlock(clean); if (acc === "ref") this.toHeap(value); - this.arrPush(values, acc, value.name); + this.arrPush(values, acc, value.name, elem); B.br(next); B.startBlock(rejected); const promise = B.tmp(); @@ -2348,6 +2688,7 @@ export class LlEmitter { }; this.literals.set(text, lit); } + this.journal?.entry("literal", text, lit); const name = `@${lit.sym}`; this.immortalValues.add(name); return name; @@ -2358,6 +2699,7 @@ export class LlEmitter { literalEquals(text: string): string { const literal = this.internLiteral(text); const name = `@sc_streq_${literal.slice("@sc_lit_".length)}`; + this.journal?.entry("literalEquality", text, name); if (!this.literalEqualities.has(text)) { this.literalEqualities.set(text, name); if (Buffer.byteLength(text, "utf8") > INLINE_LITERAL_BYTES) { @@ -2384,6 +2726,7 @@ export class LlEmitter { lit = { sym: this.contentSymbol("sc_bigint_", key), value }; this.bigintLiterals.set(key, lit); } + this.journal?.entry("bigint", key, { sym: lit.sym, value: lit.value.toString() }); const name = `@${lit.sym}`; this.immortalValues.add(name); return name; @@ -2397,9 +2740,21 @@ export class LlEmitter { c = { sym: this.contentSymbol("sc_cs_", text), len: Buffer.byteLength(text, "utf8") }; this.cstrs.set(text, c); } + this.journal?.entry("cstr", text, c); return `@${c.sym}`; } + /** The method-id slot for a constant method name (methodIdSlots). */ + methodIdSlot(name: string): string { + let sym = this.methodIdSlots.get(name); + if (!sym) { + sym = this.contentSymbol("sc_mid_", name); + this.methodIdSlots.set(name, sym); + } + this.journal?.entry("methodIdSlot", name, sym); + return `@${sym}`; + } + needBadTag(): void { this.needsBadTag = true; } @@ -2413,6 +2768,7 @@ export class LlEmitter { sym = this.contentSymbol("sc_map_ops_", body); this.mapOpsConsts.set(body, sym); } + this.journal?.entry("mapOps", body, sym); return `@${sym}`; } @@ -2439,6 +2795,7 @@ export class LlEmitter { sym = `sc_unit_${this.unitInstances.size}`; this.unitInstances.set(key, sym); } + this.journal?.entry("unit", key, sym); const name = `@${sym}`; this.immortalValues.add(name); return name; @@ -2452,6 +2809,7 @@ export class LlEmitter { sym = `sc_retype_guard_${this.retypeGuards.size}`; this.retypeGuards.set(unionId, sym); } + this.journal?.entry("retype", unionId, sym); return `@${sym}`; } @@ -2473,6 +2831,7 @@ export class LlEmitter { sym = `sc_absent_${this.absentInstances.size}`; this.absentInstances.set(unionId, sym); } + this.journal?.entry("absent", unionId, sym); const name = `@${sym}`; this.immortalValues.add(name); return name; @@ -2520,6 +2879,7 @@ export class LlEmitter { } declare(decl: string): void { + this.journal?.entry("decl", decl, null); this.decls.add(decl); } @@ -3585,6 +3945,30 @@ export class LlEmitter { return { ptr: p, type, inline: this.inlineUnions.classField(className, field), compressed }; } + /** Whether values of `t` are NULL or a compact class instance: `C`, + * `C | undefined` or `C | null` for a compact class `C`. `C | null | + * undefined` is not: its null is a sentinel outside the region. */ + compactRefType( + t: IrType, + isCompact: (className: string) => boolean = (name) => + this.classMeta.get(name)?.compact === true, + ): boolean { + if (t.kind === "object") return isCompact(t.className); + if (t.kind !== "union") return false; + const single = this.nullableUnions.single(t.unionId); + return ( + single !== null && + single.nullTag < 0 && + single.arm.kind === "object" && + isCompact(single.arm.className) + ); + } + + /** Whether arrays of `elem` hold 32-bit references (SCR_ELEM_CREF). */ + compactArrayElem(elem: IrType): boolean { + return this.compactRegion && this.compactRefType(elem); + } + /** Loads the reference a field pointer addresses: a pointer, or the * decoded 32-bit reference of a compressed field (compressedFieldPtrs). * `tagged`: the load carries the field's alias tags. */ @@ -3640,6 +4024,7 @@ export class LlEmitter { const meta = this.classMetaOf(className); this.classObjs.set(className, { nameSym: this.internLiteral(classJsName(meta.def)) }); } + this.journal?.entry("classObj", className, this.classObjs.get(className)); return mangleClassObj(className); } @@ -3849,13 +4234,13 @@ export class LlEmitter { /** A binding's storage: a module global, a plain local slot, or a boxed * local (the slot holds the capture BOX; access goes through it). */ matchBytesLoopVersion( - s: IrStmt & { kind: "for" }, + s: IrStmt & { kind: "for" | "while" }, counted: IntegerCountedForLoop | null, ): ReturnType { - const fn = this.bytesLoopFunction; + const fn = LOOPS.of(this).bytesLoopFunction; if (!fn) return null; - this.bytesLoopEntries ??= findInitializedByteLoopBindings(fn); - const initialized = this.bytesLoopEntries.get(s); + LOOPS.of(this).bytesLoopEntries ??= findInitializedByteLoopBindings(fn); + const initialized = LOOPS.of(this).bytesLoopEntries!.get(s); if (!initialized) return null; const invariantGlobals = new Set(this.stableGlobals); for (const global of this.initializerBindings.get(fn.name) ?? []) @@ -3869,6 +4254,7 @@ export class LlEmitter { proven: this.bytesBounds, workers: this.mod.workers === true, owned: this.bytesOwned, + scalarStrings: new Set(this.stringSlices.keys()), }); } @@ -4032,7 +4418,7 @@ export class LlEmitter { const acc = boxAccess(t); const cell = B.tmp(); B.line(`${cell} = ${arrNewCall(this.shapeHost, t, "1")} ; TDZ cell`); - this.arrPush(cell, acc === "bool" ? "bool" : "f64", value); + this.arrPush(cell, acc === "bool" ? "bool" : "f64", value, null); this.declare(`declare void @scr_box_set_ref(ptr, ptr)`); B.line(`call void @scr_box_set_ref(ptr ${box}, ptr ${cell})`); } @@ -4347,6 +4733,198 @@ export class LlEmitter { return emitFunction(this, fn); } + /** What this range emitter (chunkEmitter) hands the merging emitter, as + * plain data a worker process can write: its journal and the sets and + * flags its functions set. */ + chunkResult(): ChunkResult { + return { + entries: this.journal!.entries, + flags: { + usesCoroutines: this.usesCoroutines, + usesTimers: this.usesTimers, + usesGenSlots: this.usesGenSlots, + needsUnionNone: this.needsUnionNone, + needsNullableAbsent: this.needsNullableAbsent, + needsOom: this.needsOom, + needsBadTag: this.needsBadTag, + needsBadKey: this.needsBadKey, + needsRetainBox: this.needsRetainBox, + }, + publishedNamesSym: this.publishedNamesSym, + rcHelpers: this.rcHelpers === null ? [] : [...this.rcHelpers], + immortalValues: [...this.immortalValues], + fnValues: [...this.fnValues], + arraySearchKinds: [...this.arraySearchKinds], + recordCloneShapes: [...this.recordCloneShapes], + awaitCallees: [...this.awaitCallees], + adoptCallees: [...this.adoptCallees], + directAwaitCallees: [...this.directAwaitCallees], + inlineAwaitCallees: [...this.inlineAwaitCallees], + }; + } + + /** Merges a range emitter's result (chunkResult) into this emitter, in + * the range's order, and answers the rename of the range's text to this + * emitter's numbered helpers. Ranges merge in function order. */ + mergeChunk(chunk: ChunkResult): (text: string) => string { + const names = new Map(); + const tags = new Map(); + // The range's helpers and table entries first, in its order; then the + // sets and flags, which replayed definitions must find as serial + // emission left them (a helper set guards the declarations its first + // request adds). + replayJournal(this, chunk.entries, names, tags); + const flags = chunk.flags; + this.usesCoroutines ||= flags.usesCoroutines; + this.usesTimers ||= flags.usesTimers; + this.usesGenSlots ||= flags.usesGenSlots; + this.needsUnionNone ||= flags.needsUnionNone; + this.needsNullableAbsent ||= flags.needsNullableAbsent; + this.needsOom ||= flags.needsOom; + this.needsBadTag ||= flags.needsBadTag; + this.needsBadKey ||= flags.needsBadKey; + this.needsRetainBox ||= flags.needsRetainBox; + this.publishedNamesSym ??= chunk.publishedNamesSym; + if (this.rcHelpers !== null) for (const key of chunk.rcHelpers) this.rcHelpers.add(key); + for (const name of chunk.immortalValues) this.immortalValues.add(name); + for (const name of chunk.fnValues) this.fnValues.add(name); + for (const kind of chunk.arraySearchKinds) this.arraySearchKinds.add(kind as ArraySearchKind); + for (const shapeId of chunk.recordCloneShapes) this.recordCloneShapes.add(shapeId); + for (const name of chunk.awaitCallees) this.awaitCallees.add(name); + for (const name of chunk.adoptCallees) this.adoptCallees.add(name); + for (const name of chunk.directAwaitCallees) this.directAwaitCallees.add(name); + for (const name of chunk.inlineAwaitCallees) this.inlineAwaitCallees.add(name); + return renameChunk(names, tags); + } + + /** Adds a content-named constant a chunk interned. Its symbol is this + * emitter's own: a digest collision retries in first-use order, which a + * chunk alone does not know. */ + private addContentEntry( + into: Map, + key: string, + entry: V, + prefix: string, + names: Map, + ): void { + let own = into.get(key); + if (own === undefined) { + own = { ...entry, sym: this.contentSymbol(prefix, key) }; + into.set(key, own); + } + if (own.sym !== entry.sym) names.set(entry.sym, own.sym); + } + + /** Applies a chunk's table entry (EmitJournal.entry) at its place in the + * chunk's order: a key this emitter lacks is added, numbered after its + * own entries where the table numbers them, and the chunk's name for it + * is renamed to this emitter's. */ + applyJournalEntry( + table: string, + key: string, + value: unknown, + names: Map, + tags: Map, + ): void { + switch (table) { + case "decl": + this.decls.add(key); + return; + case "literal": + this.addContentEntry( + this.literals, + key, + value as { sym: string; len: number }, + "sc_lit_", + names, + ); + return; + case "literalEquality": + addEntry(this.literalEqualities, key, value as string); + return; + case "bigint": + { + const entry = value as { sym: string; value: string }; + this.addContentEntry( + this.bigintLiterals, + key, + { sym: entry.sym, value: BigInt(entry.value) }, + "sc_bigint_", + names, + ); + } + return; + case "cstr": + this.addContentEntry( + this.cstrs, + key, + value as { sym: string; len: number }, + "sc_cs_", + names, + ); + return; + case "mapOps": { + let own = this.mapOpsConsts.get(key); + if (own === undefined) { + own = this.contentSymbol("sc_map_ops_", key); + this.mapOpsConsts.set(key, own); + } + if (own !== value) names.set(value as string, own); + return; + } + case "methodIdSlot": { + let own = this.methodIdSlots.get(key); + if (own === undefined) { + own = this.contentSymbol("sc_mid_", key); + this.methodIdSlots.set(key, own); + } + if (own !== value) names.set(value as string, own); + return; + } + case "classObj": + addEntry(this.classObjs, key, value as { nameSym: string }); + return; + case "retainWrapper": + addEntry(this.nullableUnions.retainWrappers, key, value as string); + return; + case "unit": + return addNumbered(this.unitInstances, key, value as string, "sc_unit_", names); + case "absent": + return addNumbered(this.absentInstances, key, value as string, "sc_absent_", names); + case "retype": + return addNumbered(this.retypeGuards, key, value as string, "sc_retype_guard_", names); + case "slots": + return addNumbered(this.threadSlotsBlocks, key, value as string, "@sc_slots_", names); + case "regex": + return addNumberedSymbol(this.regexInstances, key, value as never, "sc_re_", names); + case "templateStrings": + return addNumberedSymbol( + this.templateStringsInstances, + key, + value as never, + "sc_tsa_", + names, + ); + case "tag": { + let own = this.fieldAliasTags.get(key); + if (own === undefined) { + own = 2 + this.fieldAliasTags.size * 2; + this.fieldAliasTags.set(key, own); + } + if (own !== value) tags.set(value as number, own); + return; + } + default: + throw new InternalCompilerError(`llvm emitter bug: no journal table ${table}`); + } + } + + /** An emitter for a range of this module's functions: the same facts, + * empty module-level tables, and a journal of what the range adds. */ + chunkEmitter(): LlEmitter { + return new LlEmitter(this.mod, { ...this.targetOptions, functionChunks: 1 }, new EmitJournal()); + } + /** The double-ABI entry of a function with an i32 entry: every caller * that is not a direct call (closures, dispatch, adapters, the runtime) * enters here. A pending exception passes through to its caller. */ @@ -4965,10 +5543,14 @@ export class LlEmitter { const loop = s.kind === "while" || s.kind === "doWhile" || s.kind === "for" || s.kind === "forOf"; if (loop) this.loopDepth++; + // An int32 kind loop's fast-copy write leaves for its resume point. + const kindResume = this.int32KindResume; + this.int32KindResume = this.int32Kinds?.resume.get(s) ?? null; try { this.emitStmtBody(s); } finally { this.B.debugLocation = previous; + this.int32KindResume = kindResume; if (loop) this.loopDepth--; } } @@ -5429,6 +6011,7 @@ export class LlEmitter { sym = `@sc_slots_${this.threadSlotsBlocks.size}`; this.threadSlotsBlocks.set(name, sym); } + this.journal?.entry("slots", name, sym); return sym; } @@ -5578,6 +6161,20 @@ export class LlEmitter { if (this.publishWalkersCache !== null) return this.publishWalkersCache; const byPre = [...this.classMeta.values()].sort((a, b) => a.pre - b.pre); this.publishWalkersCache = new PublishWalkers({ + // Journal hooks only on emitters that can emit ranges (chunk + // emitters, and emitters whose workers take a journal after the + // fork), so the host's optional calls cost nothing otherwise. + journalAllocated: !this.journaled + ? undefined + : (name, op, args) => this.journal?.allocated(name, op, args), + journalBuilt: !this.journaled ? undefined : () => this.journal?.built(), + journalOpened: !this.journaled ? undefined : (op) => this.journal?.opened(op), + journalUsed: !this.journaled + ? undefined + : (name, op, args) => this.journal?.used(name, op, args), + journalEntry: !this.journaled + ? undefined + : (table, key, value) => this.journal?.entry(table, key, value), declare: (decl) => this.declare(decl), cstr: (text) => this.cstr(text), sizeType: this.sizeType, @@ -6064,12 +6661,27 @@ export class LlEmitter { } jsIcSite(): string { - return `@sc_js_ic_${this.jsIcSiteCount++}`; + const name = `@sc_js_ic_${this.jsIcSiteCount++}`; + this.journal?.site(name, "jsIcSite"); + return name; } /** A fresh read-site cache for one constant-key dyn read. */ dynReadIcSite(): string { - return `@sc_dyn_read_ic_${this.dynReadIcSites++}`; + const name = `@sc_dyn_read_ic_${this.dynReadIcSites++}`; + this.journal?.site(name, "dynReadIcSite"); + return name; + } + + /** A fresh speculative loop site: the global that counts its bailing + * entries in a row. */ + speculativeLoopSite(): string { + const name = `@sc_spec_bails_${this.speculativeLoopSites++}`; + this.journal?.site(name, "speculativeLoopSite"); + this.journal?.opened("speculativeLoopSite"); + this.declare(`${name} = internal global i8 0`); + this.journal?.built(); + return name; } dynKind(d: string): string { @@ -6146,7 +6758,10 @@ export class LlEmitter { return streamDataAdapter(this, cbT); } - streamDoneFnFor(kind: "w" | "f" | "d" | "t" | "l", doneT: IrType & { kind: "func" }): string { + streamDoneFnFor( + kind: "w" | "f" | "d" | "t" | "l" | "c", + doneT: IrType & { kind: "func" }, + ): string { return streamDoneFnFor(this, kind, doneT); } @@ -6170,7 +6785,7 @@ export class LlEmitter { return zlibBytesThunkFor(this, cbT); } - streamCbThunkFor(kind: "r" | "w" | "f" | "d" | "t" | "l" | "e", cbT: IrType): string { + streamCbThunkFor(kind: "r" | "w" | "f" | "d" | "t" | "l" | "e" | "c" | "v", cbT: IrType): string { return streamCbThunkFor(this, kind, cbT); } @@ -6182,8 +6797,8 @@ export class LlEmitter { return tagInSet(this, uName, unionId, tags); } - arrPush(arr: string, acc: "f64" | "bool" | "ref", value: string): string { - return arrPush(this, arr, acc, value); + arrPush(arr: string, acc: ArrayAccess, value: string, element: IrType | null): string { + return arrPush(this, arr, acc, value, element); } emitArrayCopyLoop(dst: string, src: string, acc: "f64" | "bool" | "ref"): void { @@ -6471,3 +7086,76 @@ export function adoptingAwaitOf(value: IrExpr | null): ExprOf<"awaitExpr"> | nul }); return found; } + +/** Adds `key` to `into` when it lacks it. */ +function addEntry(into: Map, key: string, value: V): void { + if (!into.has(key)) into.set(key, value); +} + +/** Adds `key` to a numbered table when it lacks it, numbering it after the + * table's own entries (`prefix` and its index), and records the rename of + * the chunk's `name` for it. */ +function addNumbered( + into: Map, + key: string, + name: string, + prefix: string, + names: Map, +): void { + let own = into.get(key); + if (own === undefined) { + own = `${prefix}${into.size}`; + into.set(key, own); + } + if (own !== name) names.set(name.replace(/^@/, ""), own.replace(/^@/, "")); +} + +/** addNumbered for entries that keep their numbered name in `sym`. */ +function addNumberedSymbol( + into: Map, + key: string, + entry: V, + prefix: string, + names: Map, +): void { + let own = into.get(key); + if (own === undefined) { + own = { ...entry, sym: `${prefix}${into.size}` }; + into.set(key, own); + } + if (own.sym !== entry.sym) names.set(entry.sym, own.sym); +} + +registerReplay("jsIcSite", (emitter) => emitter.jsIcSite()); +registerReplay("dynReadIcSite", (emitter) => emitter.dynReadIcSite()); +registerReplay("speculativeLoopSite", (emitter) => emitter.speculativeLoopSite()); +for (const op of [ + "dyn.collectionDynOps", + "dyn.dynMatchHelper", + "dyn.jsonSchema", + "dyn.recordNameTable", + "dyn.jsonParseHelper", + "dyn.dynCheckHelper", + "dyn.toDynHelper", + "dyn.promiseDynAdapterHelper", + "dyn.dynFuncThunkHelper", + "dyn.dynFuncBoxHelper", + "dyn.dynFuncAdapterHelper", + "dyn.dynToStrHelper", + "dyn.dynIterNHelper", + "dyn.dynDestrCheckHelper", + "dyn.dynKeyGetHelper", + "dyn.dynComputedKeyGetHelper", + "dyn.caughtToDynHelper", + "dyn.messageListenerHelper", + "dyn.messageReaderHelper", +]) + registerReplay(op, (emitter, args) => emitter.dyn.replayHelper(op, args)); +for (const op of [ + "walkers.jsonWriteHelper", + "walkers.recordFieldWriter", + "walkers.unionJoinHelper", +]) + registerReplay(op, (emitter, args) => emitter.walkers.replayHelper(op, args)); +for (const op of ["publish.walker", "publish.refusal"]) + registerReplay(op, (emitter, args) => emitter.publishWalkers.replayHelper(op, args)); diff --git a/packages/compiler/src/backend/llvm/expr-bytes.ts b/packages/compiler/src/backend/llvm/expr-bytes.ts index 062bfdfbe..41ddf2b9d 100644 --- a/packages/compiler/src/backend/llvm/expr-bytes.ts +++ b/packages/compiler/src/backend/llvm/expr-bytes.ts @@ -7,7 +7,7 @@ import { F64_INF, exactIndexLines, f64Lit, sizeToDouble } from "./common.js"; import { exactInteger, widenInteger } from "./integer-values.js"; import { byteNumberAccess } from "../../ir/byte-numbers.js"; import { emitByteNumber } from "./byte-numbers.js"; -import { integerOrNaNRange } from "./wrapping-sums.js"; +import { integerOrNaNIndex, integerOrNaNRange } from "./wrapping-sums.js"; import { bytesElementAlias, bytesHeaderAlias } from "./bytes-alias.js"; const BYTES_NUM_KIND: Record = { @@ -119,12 +119,28 @@ export function emitBytesIndex( B.line(`${narrow} = trunc i64 ${wideIndex} to ${host.sizeType}`); return narrow; } - const exact = exactIndexLines(index, len, host.sizeType, () => B.tmp()); - for (const line of exact.lines) B.line(line); + let ok: string; + let wide: string; + const nanIndex = integerOrNaNIndex(host, value, expr); + if (nanIndex) { + // An exact integer or NaN: in range when ordered and below the length. + const wideLength = host.sizeType === "i64" ? len : B.tmp(); + if (host.sizeType !== "i64") B.line(`${wideLength} = zext ${host.sizeType} ${len} to i64`); + const below = B.tmp(); + ok = B.tmp(); + B.line(`${below} = icmp ult i64 ${nanIndex.wide}, ${wideLength}`); + B.line(`${ok} = and i1 ${nanIndex.ordered}, ${below}`); + wide = nanIndex.wide; + } else { + const exact = exactIndexLines(index, len, host.sizeType, () => B.tmp()); + for (const line of exact.lines) B.line(line); + ok = exact.ok; + wide = exact.wide; + } const invalid = B.newLabel("bytes.index.invalid"); const valid = B.newLabel("bytes.index.valid"); - B.condBr(exact.ok, valid, invalid); - let idx = exact.wide; + B.condBr(ok, valid, invalid); + let idx = wide; B.startBlock(invalid); if (skipInvalid) B.br(skipInvalid); @@ -138,7 +154,7 @@ export function emitBytesIndex( B.startBlock(valid); if (host.sizeType !== "i64") { idx = B.tmp(); - B.line(`${idx} = trunc i64 ${exact.wide} to ${host.sizeType}`); + B.line(`${idx} = trunc i64 ${wide} to ${host.sizeType}`); } return idx; } @@ -455,11 +471,13 @@ export function emitToUint32( B.line(`${out} = select i1 ${ordered}, i32 ${low}, i32 0`); return out; } + // Below 2^63 in magnitude, the i64 conversion truncates exactly (every + // double above 2^53 is an integer), and its low 32 bits are ToUint32. const aboveMin = B.tmp(); const belowMax = B.tmp(); const fast = B.tmp(); - B.line(`${aboveMin} = fcmp oge double ${value}, ${f64Lit(-9007199254740992)}`); - B.line(`${belowMax} = fcmp ole double ${value}, ${f64Lit(9007199254740992)}`); + B.line(`${aboveMin} = fcmp oge double ${value}, ${f64Lit(-9223372036854775808)}`); + B.line(`${belowMax} = fcmp olt double ${value}, ${f64Lit(9223372036854775808)}`); B.line(`${fast} = and i1 ${aboveMin}, ${belowMax}`); const fastLabel = B.newLabel("uint32.coerce.fast"); @@ -475,8 +493,8 @@ export function emitToUint32( B.br(done); B.startBlock(slowLabel); - // Only NaN, the infinities and magnitudes above 2^53 reach this block. - // Such a finite double is the integer mantissa * 2^shift with shift >= 1, + // Only NaN, the infinities and magnitudes from 2^63 reach this block. + // Such a finite double is the integer mantissa * 2^shift with shift >= 11, // so its low 32 bits are the shifted mantissa's (none once shift >= 64), // negated for a negative value. NaN and the infinities have the maximum // exponent, a shift of 972, and convert to 0 like ToUint32. Integer @@ -1100,7 +1118,9 @@ export function emitInlineAtomicsOp( if (op !== 0 && op !== 1 && op !== 3 && ATOMIC_RMW[op] === undefined) return null; const elem = array.type.elem; const B = host.B; - const receiver = host.emitExpr(array).name; + // The instruction and scr_atomics_op borrow the array: an unchanged local + // owns it across the later operands and the operation. + const receiver = emitBorrowedInput(host, array).name; const idxValue = host.emitExpr(index); const operand = host.emitExpr(value); const swap = host.emitExpr(replacement); diff --git a/packages/compiler/src/backend/llvm/expr-callbacks.ts b/packages/compiler/src/backend/llvm/expr-callbacks.ts index b91cdc543..398aebb21 100644 --- a/packages/compiler/src/backend/llvm/expr-callbacks.ts +++ b/packages/compiler/src/backend/llvm/expr-callbacks.ts @@ -14,6 +14,7 @@ import { LlvmUnsupportedError } from "./unsupported.js"; import type { LlvmEmitterContext } from "./expr-context.js"; import { unionPeekLines, unionTagLines } from "./union-repr.js"; import { inlineUnionCopyLines } from "./inline-unions.js"; +import { registerReplay } from "./emit-journal.js"; /** Text-template union construction around an owned (+1) reference payload: * a fresh box, or the payload itself for a nullable union. */ @@ -55,6 +56,7 @@ export function raceAdapterFor(host: LlvmEmitterContext, from: IrType, to: IrTyp if (sym) return sym; sym = `sc_race_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "raceAdapterFor", [from, to]); const toArms = to.kind === "union" ? (host.unionsById.get(to.unionId)?.arms ?? []) : []; if (from.kind === "void" && !toArms.some((a) => a.kind === "undefinedT")) { // A Promise entry (the frontend admits void entries without an @@ -70,6 +72,7 @@ export function raceAdapterFor(host: LlvmEmitterContext, from: IrType, to: IrTyp `}`, ``, ); + host.journal?.built(); return sym; } if (to.kind !== "union") @@ -98,6 +101,7 @@ export function raceAdapterFor(host: LlvmEmitterContext, from: IrType, to: IrTyp const tag = tagOf(UNDEFINED_T); d.push(` ${fulfill(host.unitInstanceRef(to.unionId, tag))}`, ` ret void`, `}`, ``); host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } if (from.kind !== "union") { @@ -138,6 +142,7 @@ export function raceAdapterFor(host: LlvmEmitterContext, from: IrType, to: IrTyp } d.push(` ${fulfill("%u")}`, ` ret void`, `}`, ``); host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } // Sub-union re-tag: switch over the entry's arms, rebuild under the @@ -200,6 +205,7 @@ export function raceAdapterFor(host: LlvmEmitterContext, from: IrType, to: IrTyp ``, ); host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } @@ -213,6 +219,7 @@ export function genResultThunkFor( if (sym) return sym; sym = mangleGenResThunk(host.resolveThunks.size); host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "genResultThunkFor", [genT, recT]); const shape = host.recordsById.get(recT.shapeId); const base = host.nullableUnions.fieldBase(recT.shapeId); const valueT = shape?.fields.find((f) => f.name === "value")?.type; @@ -299,6 +306,7 @@ export function genResultThunkFor( ``, ]; host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } if (valueT.kind !== "union") throw new LlvmUnsupportedError(`genResume:${valueT.kind}`); @@ -456,6 +464,7 @@ export function genResultThunkFor( ``, ); host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } @@ -467,6 +476,7 @@ export function childExitThunkFor(host: LlvmEmitterContext, param: IrType): stri if (sym) return sym; sym = `sc_cx_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "childExitThunkFor", [param]); const def = host.unionsById.get(param.unionId); const f64Tag = def ? def.arms.findIndex((a) => a.kind === "f64") : -1; const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1; @@ -494,6 +504,7 @@ export function childExitThunkFor(host: LlvmEmitterContext, param: IrType): stri `}`, ``, ); + host.journal?.built(); return sym; } @@ -510,6 +521,7 @@ export function childExitSignalThunkFor( if (sym) return sym; sym = `sc_cx_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "childExitSignalThunkFor", [codeParam, sigParam]); const codeDef = host.unionsById.get(codeParam.unionId); const f64Tag = codeDef ? codeDef.arms.findIndex((a) => a.kind === "f64") : -1; const codeNullTag = codeDef ? codeDef.arms.findIndex((a) => a.kind === "nullT") : -1; @@ -560,6 +572,7 @@ export function childExitSignalThunkFor( `}`, ``, ); + host.journal?.built(); return sym; } @@ -569,6 +582,7 @@ export function execFileThunkFor(host: LlvmEmitterContext, cbT: IrType & { kind: if (sym) return sym; sym = `sc_exec_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "execFileThunkFor", [cbT]); const errorParam = cbT.params[0]; let errorTag = -1; let nullTag = -1; @@ -627,6 +641,7 @@ export function execFileThunkFor(host: LlvmEmitterContext, cbT: IrType & { kind: ``, ); host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } @@ -639,6 +654,7 @@ export function ipcMessageThunkFor( if (sym) return sym; sym = `sc_ipc_msg_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "ipcMessageThunkFor", [cbT]); const param = cbT.params[0]; const d: string[] = [ `define internal void @${sym}(ptr %cb, ptr %message) ${FN_ATTRS} { ; IPC message ${typeKey(cbT)}`, @@ -679,6 +695,7 @@ export function ipcMessageThunkFor( } d.push(` ret void`, `}`, ``); host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } @@ -688,6 +705,7 @@ export function ipcSendThunkFor(host: LlvmEmitterContext, cbT: IrType & { kind: if (sym) return sym; sym = `sc_ipc_send_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "ipcSendThunkFor", [cbT]); const param = cbT.params[0]; const d: string[] = [ `define internal void @${sym}(ptr %cb, ptr %error) ${FN_ATTRS} { ; IPC send callback ${typeKey(cbT)}`, @@ -736,6 +754,7 @@ export function ipcSendThunkFor(host: LlvmEmitterContext, cbT: IrType & { kind: } d.push(` ret void`, `}`, ``); host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } @@ -747,6 +766,7 @@ export function childDataThunkFor(host: LlvmEmitterContext, param: IrType): stri if (sym) return sym; sym = `sc_cd_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "childDataThunkFor", [param]); const def = host.unionsById.get(param.unionId); const bytesTag = def ? def.arms.findIndex((a) => a.kind === "bytes" && a.elem === "u8") : -1; if (bytesTag < 0) @@ -768,6 +788,7 @@ export function childDataThunkFor(host: LlvmEmitterContext, param: IrType): stri `}`, ``, ); + host.journal?.built(); return sym; } @@ -780,6 +801,7 @@ export function childDataDynThunkFor(host: LlvmEmitterContext, chunk: "bytes" | if (sym) return sym; sym = `sc_cdd_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "childDataDynThunkFor", [chunk]); const box = chunk === "bytes" ? "scr_dyn_new_buffer" : "scr_dyn_new_str"; host.declare(`declare ptr @${box}(ptr)`); host.resolveThunkDefs.push( @@ -793,6 +815,7 @@ export function childDataDynThunkFor(host: LlvmEmitterContext, chunk: "bytes" | `}`, ``, ); + host.journal?.built(); return sym; } @@ -813,6 +836,7 @@ export function emitterFixedAdapter( if (sym) return { fn: sym, shim }; sym = `sc_ee_ad_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "emitterFixedAdapter", [cbT]); host.declare(`declare ptr @scr_box_get_ref(ptr)`); host.declare(`declare void @scr_closure_release(ptr)`); const params = cursor ? "ptr %args" : cbT.params.map((_, i) => `ptr %a${i}`).join(", "); @@ -857,6 +881,7 @@ export function emitterFixedAdapter( } d.push(` call void @scr_closure_release(ptr %orig)`, ` ret void`, `}`, ``); host.resolveThunkDefs.push(...d); + host.journal?.built(); return { fn: sym, shim }; } @@ -882,6 +907,7 @@ export function emitterOnceAdapter( if (sym) return { fn: sym, shim }; sym = `sc_ee_once_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "emitterOnceAdapter", [boxerT, fire]); host.declare(`declare ptr @scr_box_get_ref(ptr)`); host.declare(`declare void @scr_closure_release(ptr)`); host.declare(`declare void @${fire}(ptr, ptr)`); @@ -921,6 +947,7 @@ export function emitterOnceAdapter( ``, ); host.resolveThunkDefs.push(...d); + host.journal?.built(); return { fn: sym, shim }; } @@ -990,6 +1017,7 @@ export function closeBindThunkFor( if (sym) return sym; sym = `sc_ncb_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "closeBindThunkFor", [cbUnion, retServer]); const def = host.unionsById.get(cbUnion.unionId); const funcTag = def ? def.arms.findIndex((a) => a.kind === "func") : -1; const funcArm = funcTag >= 0 ? (def!.arms[funcTag] as IrType & { kind: "func" }) : null; @@ -1079,6 +1107,7 @@ export function closeBindThunkFor( } d.push(`}`, ``); host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } @@ -1094,6 +1123,7 @@ export function closeOverrideWrapFor( if (sym) return sym; sym = `sc_ncw_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "closeOverrideWrapFor", [cbUnion, retServer]); const def = host.unionsById.get(cbUnion.unionId); const undefTag = def ? def.arms.findIndex((a) => a.kind === "undefinedT") : -1; if (undefTag < 0) @@ -1122,6 +1152,7 @@ export function closeOverrideWrapFor( `}`, ``, ); + host.journal?.built(); return sym; } @@ -1140,6 +1171,7 @@ export function settledStoreFor( if (sym) return sym; sym = `sc_settled_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "settledStoreFor", [from, to]); const def = host.unionsById.get(to.unionId); if (!def) throw new InternalCompilerError("llvm emitter bug: settled store to an unknown union"); const armOf = (field: string): { tag: number; arm: IrType & { kind: "record" } } => { @@ -1230,5 +1262,52 @@ export function settledStoreFor( `}`, ``, ); + host.journal?.built(); return sym; } + +registerReplay("raceAdapterFor", (host, args) => + raceAdapterFor(host, args[0] as IrType, args[1] as IrType), +); +registerReplay("genResultThunkFor", (host, args) => + genResultThunkFor( + host, + args[0] as IrType & { kind: "generator" }, + args[1] as IrType & { kind: "record" }, + ), +); +registerReplay("childExitThunkFor", (host, args) => childExitThunkFor(host, args[0] as IrType)); +registerReplay("childExitSignalThunkFor", (host, args) => + childExitSignalThunkFor(host, args[0] as IrType, args[1] as IrType), +); +registerReplay("execFileThunkFor", (host, args) => + execFileThunkFor(host, args[0] as IrType & { kind: "func" }), +); +registerReplay("ipcMessageThunkFor", (host, args) => + ipcMessageThunkFor(host, args[0] as IrType & { kind: "func" }), +); +registerReplay("ipcSendThunkFor", (host, args) => + ipcSendThunkFor(host, args[0] as IrType & { kind: "func" }), +); +registerReplay("childDataThunkFor", (host, args) => childDataThunkFor(host, args[0] as IrType)); +registerReplay("childDataDynThunkFor", (host, args) => + childDataDynThunkFor(host, args[0] as "bytes" | "str"), +); +registerReplay("closeBindThunkFor", (host, args) => + closeBindThunkFor(host, args[0] as IrType, args[1] as boolean), +); +registerReplay("closeOverrideWrapFor", (host, args) => + closeOverrideWrapFor(host, args[0] as IrType, args[1] as boolean), +); +registerReplay("settledStoreFor", (host, args) => + settledStoreFor(host, args[0] as IrType, args[1] as IrType & { kind: "union" }), +); +registerReplay( + "emitterFixedAdapter", + (host, args) => emitterFixedAdapter(host, args[0] as IrType & { kind: "func" }).fn, +); +registerReplay( + "emitterOnceAdapter", + (host, args) => + emitterOnceAdapter(host, args[0] as IrType & { kind: "func" }, args[1] as string).fn, +); diff --git a/packages/compiler/src/backend/llvm/expr-calls.ts b/packages/compiler/src/backend/llvm/expr-calls.ts index 4bcf2a3d5..a4e7cde2c 100644 --- a/packages/compiler/src/backend/llvm/expr-calls.ts +++ b/packages/compiler/src/backend/llvm/expr-calls.ts @@ -11,6 +11,7 @@ import { type IrExpr, type IrFunction, classValueConstructor, + type IrType, } from "../../ir/ir.js"; import { liveRefView } from "./expr-stream-bridges.js"; import { typedRefConstructor, vAdapters } from "./shapes.js"; @@ -55,6 +56,7 @@ import { import type { Writes } from "./reference-writes.js"; import { emitJsIcCall, jsIcSiteOf } from "./js-ic.js"; import { emitClassMembership, emitDowncastGuard } from "./narrow-guards.js"; +import { registerReplay } from "./emit-journal.js"; export function emitCallExpr( host: LlvmEmitterContext, @@ -815,22 +817,7 @@ export function emitCallExpr( const pushReceiver = (): void => { if (lazyReceiver) { const t = lazyReceiver.type; - const key = typeKey(t); - let recipe = host.typedRefRecipes.get(key); - if (recipe === undefined) { - const adapter = host.liveDynRefAdapter(t); - const rc = vAdapters(host.shapeHost, t); - const ctor = typedRefConstructor(host.shapeHost, t); - const kind = ctor.endsWith("_traced") ? 2 : ctor.endsWith("_observed") ? 1 : 0; - // The box binds the same live view eager boxing attaches, so - // `in`, for-in and Object.keys see the class members. - const view = liveRefView(adapter); - recipe = `@sc_trr_${host.typedRefRecipes.size}`; - host.typedRefRecipes.set(key, recipe); - host.resolveThunkDefs.push( - `${recipe} = internal constant { ptr, ptr, ptr, ${host.sizeType}, ptr, ptr, i32, i32, ptr } { ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit}, i32 ${kind}, i32 ${view?.code ?? 0}, ptr ${view?.ops ?? "null"} }`, - ); - } + const recipe = typedRefRecipe(host, t); host.declare("declare void @scr_dyn_this_push_typed(ptr, ptr)"); B.line(`call void @scr_dyn_this_push_typed(ptr ${lazyReceiver.name}, ptr ${recipe})`); return; @@ -1493,3 +1480,28 @@ function emitNumericIndexRead( B.line(`${result} = phi ptr [ ${fast}, %${fastLabel} ], [ ${read.name}, %${slowLabel} ]`); return host.own({ name: result, type: e.type }); } + +/** The recipe a lazily boxed receiver of type `t` is built from: its RC + * adapters, type key and live view, one per type. */ +function typedRefRecipe(host: LlvmEmitterContext, t: IrType): string { + const key = typeKey(t); + let recipe = host.typedRefRecipes.get(key); + if (recipe !== undefined) return recipe; + const adapter = host.liveDynRefAdapter(t); + const rc = vAdapters(host.shapeHost, t); + const ctor = typedRefConstructor(host.shapeHost, t); + const kind = ctor.endsWith("_traced") ? 2 : ctor.endsWith("_observed") ? 1 : 0; + // The box binds the same live view eager boxing attaches, so `in`, + // for-in and Object.keys see the class members. + const view = liveRefView(adapter); + recipe = `@sc_trr_${host.typedRefRecipes.size}`; + host.typedRefRecipes.set(key, recipe); + host.journal?.allocated(recipe, "typedRefRecipe", [t]); + host.resolveThunkDefs.push( + `${recipe} = internal constant { ptr, ptr, ptr, ${host.sizeType}, ptr, ptr, i32, i32, ptr } { ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit}, i32 ${kind}, i32 ${view?.code ?? 0}, ptr ${view?.ops ?? "null"} }`, + ); + host.journal?.built(); + return recipe; +} + +registerReplay("typedRefRecipe", (host, args) => typedRefRecipe(host, args[0] as IrType)); diff --git a/packages/compiler/src/backend/llvm/expr-containers.ts b/packages/compiler/src/backend/llvm/expr-containers.ts index 8970aa3be..1ba5da818 100644 --- a/packages/compiler/src/backend/llvm/expr-containers.ts +++ b/packages/compiler/src/backend/llvm/expr-containers.ts @@ -2,7 +2,7 @@ import { arraySearchHelperName, arraySearchKind } from "./array-search.js"; import { InternalCompilerError } from "../../errors.js"; import { isStableReceiverOperand, undefinedArmTag } from "../../ir/analysis.js"; -import { type IrExpr, type IrType, isRefCounted, typeEquals, typeKey } from "../../ir/ir.js"; +import { F64, type IrExpr, type IrType, isRefCounted, typeEquals, typeKey } from "../../ir/ir.js"; import { mangleResolveThunk } from "../mangle.js"; import { arrOps, @@ -31,8 +31,10 @@ import { emitMapIterCount, emitMapIterEntry, emitMapIterSlot, + compactMapValues, mapEntryLayout, emitMapIterStart, + emitMapSize, emitNumberMapGetRef, emitNumberMapGetScalar, emitNumberMapHas, @@ -49,6 +51,8 @@ import { STRING_CHAR_AT_INT_MARKER, STRING_CHAR_CODE_AT_DECL, STRING_CHAR_CODE_AT_INT_MARKER, + emitAsciiChar, + emitAsciiCharCode, STRING_UTF16_LEN_DECL, } from "./string-reads.js"; import { exactInteger, widenInteger } from "./integer-values.js"; @@ -56,8 +60,12 @@ import { emitDenseArrayNumber, emitDenseArrayPop, emitDenseArrayPush, + runtimeAccess, + type ArrayAccess, } from "./dense-array-access.js"; +import { INT32_ARRAY_METHODS } from "../../ir/int32-arrays.js"; import { emitUnionWiden } from "./expr-records.js"; +import { registerReplay } from "./emit-journal.js"; export function resolveThunkFor(host: LlvmEmitterContext, inner: IrType): string { const key = typeKey(inner); @@ -65,6 +73,7 @@ export function resolveThunkFor(host: LlvmEmitterContext, inner: IrType): string if (!sym) { sym = mangleResolveThunk(host.resolveThunks.size); host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "resolveThunkFor", [inner]); const v = vAdapters(host.shapeHost, inner); host.declare(`declare void @scr_resolve_ref_impl(ptr, ptr, ptr, ptr, ptr)`); host.resolveThunkDefs.push( @@ -75,6 +84,7 @@ export function resolveThunkFor(host: LlvmEmitterContext, inner: IrType): string `}`, ``, ); + host.journal?.built(); } return sym; } @@ -102,20 +112,25 @@ export function tagInSet( return acc; } +/** `element`: the array's element type, or null for arrays whose + * elements are not compact class references (dense-array-access.ts + * slotKind). */ export function arrPush( host: LlvmEmitterContext, arr: string, - acc: "f64" | "bool" | "ref", + acc: ArrayAccess, value: string, + element: IrType | null, ): string { - return emitDenseArrayPush(host, arr, acc, value); + return emitDenseArrayPush(host, arr, acc, element, value); } /** Move evaluated values through one bounded slot buffer per call site. */ export function emitArrayValues( host: LlvmEmitterContext, arr: string, - acc: "f64" | "bool" | "ref", + acc: ArrayAccess, + element: IrType, values: LlValue[], prepend = false, sharedBuffer?: string, @@ -129,13 +144,16 @@ export function emitArrayValues( return result; } if (values.length === 1) { - if (!prepend) return arrPush(host, arr, acc, values[0]!.name); - const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr"; + if (!prepend) return arrPush(host, arr, acc, values[0]!.name, element); + const flat = runtimeAccess(acc); + const ty = flat === "f64" ? "double" : flat === "bool" ? "i1" : "ptr"; host.declare( - `declare double @scr_arr_unshift_${acc}(ptr, ${ty}${acc === "bool" ? " zeroext" : ""})`, + `declare double @scr_arr_unshift_${flat}(ptr, ${ty}${flat === "bool" ? " zeroext" : ""})`, ); const result = B.tmp(); - B.line(`${result} = call double @scr_arr_unshift_${acc}(ptr ${arr}, ${ty} ${values[0]!.name})`); + B.line( + `${result} = call double @scr_arr_unshift_${flat}(ptr ${arr}, ${ty} ${values[0]!.name})`, + ); return result; } const capacity = Math.min(values.length, 64); @@ -153,8 +171,9 @@ export function emitArrayValues( const slot = B.tmp(), pointer = B.tmp(); const value = values[start + i]!.name; + // The runtime takes double bits for int32 arrays too, and converts. const pack = - acc === "f64" + acc === "f64" || acc === "i32" ? `bitcast double ${value} to i64` : acc === "bool" ? `zext i1 ${value} to i64` @@ -274,6 +293,33 @@ export function emitArrayCopyLoop( host.B.line(`call double @scr_arr_push_spread(ptr ${dst}, ptr ${src})`); } +/** `s.charCodeAt(i)` in place of `s.charAt(i)`, the same index read as a + * code unit. The emitter records the charAt it stands for + * (charAtUnitReads), so a loop version's proof for the charAt covers it. */ +export function charAtUnitRead( + host: LlvmEmitterContext, + charAt: IrExpr & { kind: "strIntrinsic" }, +): IrExpr & { kind: "strIntrinsic" } { + const read: IrExpr & { kind: "strIntrinsic" } = { + kind: "strIntrinsic", + method: "charCodeAt", + receiver: charAt.receiver, + args: charAt.args, + type: F64, + loc: charAt.loc, + }; + host.charAtUnitReads.set(read, charAt); + return read; +} + +/** An enclosing loop version proved the string of `e` ASCII and its index + * below the length (bytes-loop-versions.ts). */ +function provenStringRead(host: LlvmEmitterContext, e: IrExpr): boolean { + if (host.bytesBounds.has(e)) return true; + const origin = host.charAtUnitReads.get(e); + return origin !== undefined && host.bytesBounds.has(origin); +} + export function emitStrIntrinsic( host: LlvmEmitterContext, e: IrExpr & { kind: "strIntrinsic" }, @@ -319,8 +365,10 @@ export function emitStrIntrinsic( const t = B.tmp(); const index = e.args[0] ? exactInteger(host, args[0]!, e.args[0]) : null; if (index) { - host.declare(STRING_CHAR_CODE_AT_INT_MARKER); const wide = widenInteger(host, index); + if (provenStringRead(host, e)) + return { name: emitAsciiCharCode(host, r.name, wide), type: e.type }; + host.declare(STRING_CHAR_CODE_AT_INT_MARKER); B.line(`${t} = call double @sc_str_char_code_at_int(ptr ${r.name}, i64 ${wide})`); return { name: t, type: e.type }; } @@ -333,8 +381,11 @@ export function emitStrIntrinsic( const t = B.tmp(); const index = e.args[0] ? exactInteger(host, args[0]!, e.args[0]) : null; if (index) { - host.declare(STRING_CHAR_AT_INT_MARKER); const wide = widenInteger(host, index); + // The table entry is immortal: nothing owns it. + if (provenStringRead(host, e)) + return host.markImmortal({ name: emitAsciiChar(host, r.name, wide), type: e.type }); + host.declare(STRING_CHAR_AT_INT_MARKER); B.line(`${t} = call ptr @sc_str_char_at_int(ptr ${r.name}, i64 ${wide})`); } else { const at = args[0]?.name ?? f64Lit(0); @@ -563,7 +614,15 @@ export function emitArrIntrinsic( if (e.receiver.type.kind !== "array") throw new InternalCompilerError("llvm emitter bug: arrIntrinsic on non-array"); const elem = e.receiver.type.elem; - const acc = elemAccess(elem); + // Int32 arrays (ir/int32-arrays.ts) reach only these methods; the others + // take the runtime family's flat access. One with checked stores tests + // its kind at each inline access (emitByElementKind) and otherwise takes + // the double entry points, which serve both kinds. + const checked = host.checkedInt32Array(e.receiver); + const access = checked ? "f64" : host.arrayAccess(e.receiver, elem); + if ((access === "i32" || checked) && !INT32_ARRAY_METHODS.has(e.method)) + throw new InternalCompilerError(`llvm emitter bug: ${e.method} on an int32 array`); + const acc = runtimeAccess(access); const accTy = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr"; const accArg = acc === "bool" ? "i1 zeroext" : accTy; const method = e.method; @@ -584,6 +643,13 @@ export function emitArrIntrinsic( e.receiver.kind === "varRef" ? host.constantNumericTables.get(e.receiver.localId) : undefined; + if (!table && elem.kind === "f64" && checked) { + const proven = host.provenDenseIndex(e.receiver, e.args[0]!); + const value = host.emitByElementKind(r.name, "double", (kind) => + emitDenseArrayNumber(host, r.name, index, e.args[0], proven, kind), + )!; + return { name: value, type: e.type }; + } if (!table && elem.kind === "f64") return { name: emitDenseArrayNumber( @@ -592,6 +658,7 @@ export function emitArrIntrinsic( index, e.args[0], host.provenDenseIndex(e.receiver, e.args[0]!), + access === "i32" ? "i32" : "f64", ), type: e.type, }; @@ -628,7 +695,22 @@ export function emitArrIntrinsic( // no-op zero-argument call. const vs = e.args.map((a) => host.emitExpr(a)); host.emitPublishedArrayGuard(r.name, e.receiver.type, 1, f64Lit(0), f64Lit(vs.length)); - return { name: emitArrayValues(host, r.name, acc, vs), type: e.type }; + if (checked && vs.length > 0) { + // Each value appends in its own kind test: one that is not an int32 + // widens the array before it is pushed. + let length = ""; + vs.forEach((v, i) => { + length = host.emitByElementKind(r.name, "double", (kind, widened) => { + if (kind === "i32") host.emitCheckedInt32Store(r.name, v.name, e.args[i]!, widened); + return arrPush(host, r.name, kind, v.name, elem); + })!; + }); + return { name: length, type: e.type }; + } + // In an int32 kind loop's fast copy, every value is checked before + // the first appends. + vs.forEach((v, i) => host.emitInt32KindStoreCheck(e.receiver, v, e.args[i]!)); + return { name: emitArrayValues(host, r.name, access, elem, vs), type: e.type }; } case "pushSpread": { // `a.push(...src)`: append src's elements in order (borrowed src, @@ -660,7 +742,7 @@ export function emitArrIntrinsic( // from right to left so the final front order is source order. const vs = e.args.map((a) => host.emitExpr(a)); host.emitPublishedArrayGuard(r.name, e.receiver.type, 4, f64Lit(0), f64Lit(vs.length)); - return { name: emitArrayValues(host, r.name, acc, vs, true), type: e.type }; + return { name: emitArrayValues(host, r.name, acc, elem, vs, true), type: e.type }; } case "unshiftSpread": { // The runtime snapshots the borrowed source and handles self-spread. @@ -702,7 +784,13 @@ export function emitArrIntrinsic( let slowLabel = ""; if (method === "pop") { slowLabel = B.newLabel("remove.slow"); - denseRaw = emitDenseArrayPop(host, r.name, acc, la, slowLabel); + const empty = la, + slow = slowLabel; + denseRaw = checked + ? host.emitByElementKind(r.name, "i64", (kind) => + emitDenseArrayPop(host, r.name, kind, elem, empty, slow), + )! + : emitDenseArrayPop(host, r.name, access, elem, la, slowLabel); denseLabel = B.currentLabel(); B.br(lp); B.startBlock(slowLabel); @@ -771,7 +859,7 @@ export function emitArrIntrinsic( // by content, everything else by pointer). Strict equality. Arrays of // objects and strings scan inline (array-search.ts). const v = host.emitExpr(e.args[0]!); - const search = acc === "ref" ? arraySearchKind(elem) : null; + const search = acc === "ref" ? arraySearchKind(elem, host.compactArrayElem(elem)) : null; if (search !== null) { host.arraySearchKinds.add(search); const t = B.tmp(); @@ -789,7 +877,7 @@ export function emitArrIntrinsic( // Borrowed needle, SameValueZero (NaN matches NaN). For the inline // searches SameValueZero is strict equality. const v = host.emitExpr(e.args[0]!); - const search = acc === "ref" ? arraySearchKind(elem) : null; + const search = acc === "ref" ? arraySearchKind(elem, host.compactArrayElem(elem)) : null; if (search !== null) { host.arraySearchKinds.add(search); const at = B.tmp(), @@ -891,6 +979,14 @@ export function emitArrIntrinsic( case "fillUndefined": { const args = e.args.map((arg) => host.emitExpr(arg)); host.emitPublishedArrayGuard(r.name, e.receiver.type, 7); + if (host.checkedFills.has(e)) { + // A new int32 array with checked stores, filled with a value that + // may not be an int32: widen it first then. + const filled = B.newLabel("arr.fill.kind"); + host.emitCheckedInt32Store(r.name, args[0]!.name, e.args[0]!, filled); + B.br(filled); + B.startBlock(filled); + } const helper = e.method === "copyWithin" ? "scr_arr_copy_within" @@ -1060,8 +1156,10 @@ function newCollection(host: LlvmEmitterContext, key: IrType, value: IrType): st const kind = mapKeyKindNum(key, arms); const m = B.tmp(); host.declare(`declare ptr @scr_map_new(i32, i32, ptr)`); + // SCR_MAP_VAL_CREF (3): string keys and compact values (compactMapValues). + const valKind = compactMapValues(host, key, value) ? 3 : mapValKindNum(value); B.line( - `${m} = call ptr @scr_map_new(i32 ${kind}, i32 ${mapValKindNum(value)}, ptr ${collectionOps(host, key, value)})`, + `${m} = call ptr @scr_map_new(i32 ${kind}, i32 ${valKind}, ptr ${collectionOps(host, key, value)})`, ); return m; } @@ -1124,6 +1222,9 @@ export function emitMapLikeIntrinsic( const key = receiverType.kind === "map" ? receiverType.key : receiverType.elem; const kAcc = mapKeyAccess(key); const kTy = mapKeyParamType(kAcc); + // The map keeps compact values in 16-byte entries (SCR_MAP_VAL_CREF). + const compact = receiverType.kind === "map" && compactMapValues(host, key, receiverType.value); + const layout = mapEntryLayout(key, compact); const method = e.method; // A published Map or Set rejects mutation (the guard precedes the // operands, which each method evaluates itself). @@ -1287,7 +1388,7 @@ export function emitMapLikeIntrinsic( if (numberKey !== null) { raw = emitNumberMapGetRef(host, r.name, numberKey, value); } else if (stringKey !== null) { - raw = emitStringMapGetRef(host, r.name, stringKey, value); + raw = emitStringMapGetRef(host, r.name, stringKey, value, compact); } else { host.declare(`declare ptr @scr_map_get_${kAcc}_ref(ptr, ${kTy})`); raw = B.tmp(); @@ -1365,7 +1466,7 @@ export function emitMapLikeIntrinsic( return host.wrapNullable(raw, raw, value, valueTag, e.type, undefTag); } if (stringKey !== null && isRefCounted(value)) { - const raw = emitStringMapGetRef(host, r.name, stringKey, value); + const raw = emitStringMapGetRef(host, r.name, stringKey, value, compact); return host.wrapNullable(raw, raw, value, valueTag, e.type, undefTag); } host.declare(`declare ptr @scr_map_get_${kAcc}_ref(ptr, ${kTy})`); @@ -1413,7 +1514,8 @@ export function emitMapLikeIntrinsic( case "has": { const k = borrowInputs ? host.emitReadReceiver(e.args[0]!) : host.emitExpr(e.args[0]!); if (kAcc === "f64") return { name: emitNumberMapHas(host, r.name, k.name), type: e.type }; - if (kAcc === "str") return { name: emitStringMapHas(host, r.name, k.name), type: e.type }; + if (kAcc === "str") + return { name: emitStringMapHas(host, r.name, k.name, compact), type: e.type }; host.declare(`declare zeroext i1 @scr_map_has_${kAcc}(ptr, ${kTy})`); const t = B.tmp(); B.line(`${t} = call zeroext i1 @scr_map_has_${kAcc}(ptr ${r.name}, ${kTy} ${k.name})`); @@ -1426,12 +1528,8 @@ export function emitMapLikeIntrinsic( B.line(`${t} = call zeroext i1 @scr_map_delete_${kAcc}(ptr ${r.name}, ${kTy} ${k.name})`); return { name: t, type: e.type }; } - case "size": { - host.declare(`declare double @scr_map_size(ptr)`); - const t = B.tmp(); - B.line(`${t} = call double @scr_map_size(ptr ${r.name})`); - return { name: t, type: e.type }; - } + case "size": + return { name: emitMapSize(host, r.name), type: e.type }; case "clear": host.declare(`declare void @scr_map_clear(ptr)`); B.line(`call void @scr_map_clear(ptr ${r.name})`); @@ -1442,7 +1540,7 @@ export function emitMapLikeIntrinsic( return { name: emitMapIterStart(host, r.name), type: e.type }; case "iterLive": { const i = host.emitExpr(e.args[0]!); - const entry = emitMapIterEntry(host, r.name, i.name, mapEntryLayout(key)); + const entry = emitMapIterEntry(host, r.name, i.name, layout); const t = B.tmp(); B.line(`${t} = icmp ne ptr ${entry}, null`); return { name: t, type: e.type }; @@ -1461,7 +1559,7 @@ export function emitMapLikeIntrinsic( kAcc === "str" ? "ref" : kAcc, key, { decl: `declare ${retTy} @${symbol}(ptr, double)`, symbol }, - mapEntryLayout(key), + layout, ); return host.own({ name: t, type: e.type }); } @@ -1488,7 +1586,7 @@ export function emitMapLikeIntrinsic( decl: `declare ${vAcc === "bool" ? "zeroext i1" : retTy} @${symbol}(ptr, double)`, symbol, }, - mapEntryLayout(key), + layout, ); return host.own({ name: t, type: e.type }); } @@ -1539,3 +1637,5 @@ export function emitSetNew(host: LlvmEmitterContext, e: IrExpr & { kind: "setNew } return out; } + +registerReplay("resolveThunkFor", (host, args) => resolveThunkFor(host, args[0] as IrType)); diff --git a/packages/compiler/src/backend/llvm/expr-context.ts b/packages/compiler/src/backend/llvm/expr-context.ts index 9dd6bbcac..c6dc506f4 100644 --- a/packages/compiler/src/backend/llvm/expr-context.ts +++ b/packages/compiler/src/backend/llvm/expr-context.ts @@ -15,11 +15,23 @@ export interface LlValue { /** An exact integer snapshot, excluding negative zero, with no discarded * high bits and the proven range of its original value. */ integer?: { name: string; type: "i32" | "i64"; signed: boolean; range: IntegerRange }; + /** An i64 holding the exact integer that the arithmetic producing this + * number computes, when that integer has no proven safe range: its + * absolute value is at most `bound`, and the double equals it whenever + * its absolute value is below `below` (a zero may be -0). Index consumers + * use it in place of a round trip (integer-values.ts `indexInteger`). */ + wideInteger?: WideInteger; slot?: boolean; /** A sequence local held in a capture box; release the box itself. */ boxed?: boolean; } +export interface WideInteger { + name: string; + below: number; + bound: number; +} + export type ExprOf = Extract; export type LibCallExpr = ExprOf<"libCall">; type LibCallPrefixOf = T extends `${infer Prefix}.${string}` ? Prefix : never; @@ -34,4 +46,7 @@ export interface LlStreamTypedRefAdapter { recordOps?: string; /** `@sym` of the ScrDynTypedClassOps table for class instance capsules. */ classOps?: string; + /** `@sym` of the ScrDynTypedBagClassOps table for capsules of bag-tracked + * class instances whose views inherit from prototype data. */ + bagClassOps?: string; } diff --git a/packages/compiler/src/backend/llvm/expr-dynamic.ts b/packages/compiler/src/backend/llvm/expr-dynamic.ts index 58bb90569..07a538184 100644 --- a/packages/compiler/src/backend/llvm/expr-dynamic.ts +++ b/packages/compiler/src/backend/llvm/expr-dynamic.ts @@ -250,8 +250,10 @@ export function emitDynamicExpr( } // Buffer.prototype method names take their scr_bytes_dyn.c entry, which // runs them on Buffer receivers and dispatches any other receiver. - const invoke = DYN_BUFFER_METHODS.get(e.method) ?? "scr_dyn_invoke_prepared"; - host.declare(`declare ptr @${invoke}(ptr, ptr, ptr, ptr, ${host.sizeType}, ptr)`); + // Each call passes its method name's id slot, so only the first call + // looks the name up. + const invoke = DYN_BUFFER_METHODS.get(e.method) ?? "scr_dyn_invoke_prepared_id"; + host.declare(`declare ptr @${invoke}(ptr, ptr, ptr, ptr, ptr, ${host.sizeType}, ptr)`); let name = host.cstr(e.calleeName); if (e.calleeNameValue !== undefined) { const value = emitBorrowedInput(host, e.calleeNameValue); @@ -260,7 +262,7 @@ export function emitDynamicExpr( } const t = B.tmp(); B.line( - `${t} = call ptr @${invoke}(ptr ${recv.name}, ptr ${prepared}, ptr ${host.cstr(e.method)}, ptr ${argsPtr}, ${host.sizeType} ${args.length}, ptr ${name})`, + `${t} = call ptr @${invoke}(ptr ${recv.name}, ptr ${prepared}, ptr ${host.cstr(e.method)}, ptr ${host.methodIdSlot(e.method)}, ptr ${argsPtr}, ${host.sizeType} ${args.length}, ptr ${name})`, ); const out = host.own({ name: t, type: e.type }); host.emitPendingCheck(); @@ -717,6 +719,10 @@ export function emitDynamicExpr( B.line( `${test} = call zeroext i1 @scr_dyn_typed_array_is(ptr ${d.name}, i32 ${BYTES_ELEM_NUM[e.bytesElem ?? "u8"]})`, ); + } else if (e.test === "classCapsule") { + host.declare(`declare zeroext i1 @scr_dyn_is_class_capsule(ptr)`); + test = B.tmp(); + B.line(`${test} = call zeroext i1 @scr_dyn_is_class_capsule(ptr ${d.name})`); } else if (e.test === "truthy") { host.declare(`declare zeroext i1 @scr_dyn_truthy(ptr)`); test = B.tmp(); @@ -837,6 +843,7 @@ export function emitDynamicExpr( undefined: DYN_KIND.UNDEF, null: DYN_KIND.NULL, bytes: DYN_KIND.BYTES, + funcKind: DYN_KIND.FUNC, }; test = oneOf([kindOf[e.test]!]); } @@ -1296,9 +1303,7 @@ function emitUnionEqWrappedScalar( B.startBlock(same); const eq = B.tmp(); if (arm.kind === "f64") { - host.declare(`declare double @scr_union_get_f64(ptr)`); - const v = B.tmp(); - B.line(`${v} = call double @scr_union_get_f64(ptr ${union.name})`); + const v = host.unionGetF64(union.name); if (e.sameValue) { host.declare(`declare zeroext i1 @scr_num_same_value(double, double)`); B.line(`${eq} = call zeroext i1 @scr_num_same_value(double ${v}, double ${plain.name})`); @@ -1306,9 +1311,7 @@ function emitUnionEqWrappedScalar( B.line(`${eq} = fcmp oeq double ${v}, ${plain.name}`); } } else { - host.declare(`declare zeroext i1 @scr_union_get_bool(ptr)`); - const v = B.tmp(); - B.line(`${v} = call zeroext i1 @scr_union_get_bool(ptr ${union.name})`); + const v = host.unionGetBool(union.name); B.line(`${eq} = icmp eq i1 ${v}, ${plain.name}`); } B.line(`store i1 ${eq}, ptr ${slot}`); diff --git a/packages/compiler/src/backend/llvm/expr-primitives.test.ts b/packages/compiler/src/backend/llvm/expr-primitives.test.ts index 41ecd189e..f343c956e 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.test.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.test.ts @@ -200,6 +200,10 @@ test("identity bitwise forms over a proven int32 return the operand's number", ( } // Unproven operands keep the full ToInt32; other operators still compute. expect(body("unknown_or")).toContain("uint32.coerce.slow"); + // The i64 conversion covers every magnitude below 2^63; only NaN, the + // infinities and larger magnitudes take the slow block. + expect(body("unknown_or")).toMatch(/ = fcmp oge double %\S+, 0xC3E0000000000000$/m); + expect(body("unknown_or")).toMatch(/ = fcmp olt double %\S+, 0x43E0000000000000$/m); expect(body("length_and")).toMatch(/ = and i32 .*, 255$/m); }); @@ -308,7 +312,7 @@ test("sums that only feed a bitwise conversion of int32 operands add in 32 bits" }); test.each([32, 64] as const)( - "tagged-template string arrays keep 8-byte slots before their states on %i-bit targets", + "tagged-template string arrays keep 8-byte slots without states on %i-bit targets", (bits) => { const strings: IrExpr = { kind: "templateStrings", @@ -321,14 +325,17 @@ test.each([32, 64] as const)( mod.functions[0]!.body = [{ kind: "exprStmt", expr: strings, loc }]; const llvm = emitLlvmModule(mod, { pointerBits: bits }); const data = /^@sc_tsa_0_data = internal constant .*$/m.exec(llvm)![0]; - // The runtime reads each slot as 8 bytes and finds the states after - // cap * 8 bytes of slots (scr_arr_states_end). - if (bits === 64) expect(data).toContain("<{ [3 x ptr], [3 x i8] }>"); + // The runtime reads each slot as 8 bytes; an array without states + // (`spare` 0 in its header) has no state bytes after them. + if (bits === 64) expect(data).toMatch(/^\S+ = internal constant \[3 x ptr\] /); else { - expect(data).toContain("<{ [3 x <{ ptr, i32 }>], [3 x i8] }>"); + expect(data).toMatch(/^\S+ = internal constant \[3 x <\{ ptr, i32 \}>\] /); expect(data.match(/<\{ ptr @\S+, i32 0 \}>/g)).toHaveLength(3); } - expect(data).toContain("[3 x i8] [ i8 1, i8 1, i8 1 ]"); + expect(data).not.toContain("x i8]"); + expect(llvm).toMatch( + /^@sc_tsa_0 = internal global %ScrArr \{ i\d+ -1, i32 3, i32 3, i8 2, i8 0, i16 0, i32 3, /m, + ); }, ); diff --git a/packages/compiler/src/backend/llvm/expr-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts index 354004e9e..1eec6a965 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.ts @@ -1,13 +1,14 @@ /* Focused LLVM expression emission extracted from emitter.ts. */ import { InternalCompilerError } from "../../errors.js"; import { matchStringSelfConcat, undefinedArmTag } from "../../ir/analysis.js"; -import { F64, isRefCounted, type IrExpr, type IrType } from "../../ir/ir.js"; +import { isRefCounted, type IrExpr, type IrStrCmpOp, type IrType } from "../../ir/ir.js"; import { mangleRecordClone, mangleRecordNew } from "../mangle.js"; import { arrNewCall, elemAccess } from "./shapes.js"; import { LlvmUnsupportedError } from "./unsupported.js"; import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js"; import { BYTES_ELEM_NUM, closureIdentityEqual, f64Lit } from "./common.js"; +import { CHECKED_INT32_MIN_LENGTH } from "../../ir/int32-arrays.js"; import { emitStringInputs } from "./string-lifetimes.js"; import { emitStringParts, numberPart, stringParts } from "./string-construction.js"; import { emitBorrowedInputs } from "./borrowed-inputs.js"; @@ -16,23 +17,33 @@ import { int32Number, integerNumber, narrowInteger, + wideArithmetic, widenInteger, } from "./integer-values.js"; import { bitwiseIdentityOperand, + INT32_RANGE, integerArithmeticRange, isBitwiseOp, withinInt32, withinUint32, } from "../../ir/integer-ranges.js"; -import { emitArrayValues } from "./expr-containers.js"; +import { charAtUnitRead, emitArrayValues } from "./expr-containers.js"; import { constantDivisor, emitCheckedRemainder, emitPowerOfTwoRemainder, emitSignedIntegerRemainder, } from "./integer-remainder.js"; -import { emitDenseArrayGet, emitDenseArrayState, slotWidth } from "./dense-array-access.js"; +import { + denseSlotWidth, + emitDenseArrayRead, + emitDenseArrayState, + runtimeAccess, + SCR_ELEM_I32, + slotWidth, + type ArrayAccess, +} from "./dense-array-access.js"; import { emitWrappingSum, markWrappingOperands } from "./wrapping-sums.js"; export function emitLiteralExpr( @@ -337,7 +348,14 @@ export function emitOperatorExpr( if (arith[e.op] !== undefined) { B.line(`${t} = ${arith[e.op]} double ${l.name}, ${r.name}`); const uint32 = emitWrappingSum(host, e, l, r, li, ri); - if (uint32 !== undefined) return { name: t, type: e.type, uint32 }; + // The exact integer beside the double, for index consumers. + const wideInteger = wideArithmetic(host, e.op, l, li, r, ri); + if (uint32 !== undefined || wideInteger) { + const value: LlValue = { name: t, type: e.type }; + if (uint32 !== undefined) value.uint32 = uint32; + if (wideInteger) value.wideInteger = wideInteger; + return value; + } } else B.line(`${t} = fcmp ${cmp[e.op]} double ${l.name}, ${r.name}`); } else if (bit[e.op] !== undefined) { if (e.left.type.kind !== "f64" || e.right.type.kind !== "f64") { @@ -563,14 +581,16 @@ export function regexLiteralTemplate( }; host.regexInstances.set(key, re); } + host.journal?.entry("regex", key, re); return `@${re.sym}`; } -/** A string equality between `s.charAt(i)` and a literal of exactly one - * UTF-16 code unit: the charAt call and the unit. */ +/** A string equality or comparison between `s.charAt(i)` and a literal of + * exactly one UTF-16 code unit: the charAt call, the unit, and whether the + * charAt is the left operand. */ function charAtUnitTest( - e: ExprOf<"strEq">, -): { charAt: ExprOf<"strIntrinsic">; code: number } | null { + e: ExprOf<"strEq" | "strCmp">, +): { charAt: ExprOf<"strIntrinsic">; code: number; left: boolean } | null { let text: string; let other: IrExpr; if (e.right.kind === "strLit") { @@ -585,7 +605,17 @@ function charAtUnitTest( if (text.length !== 1) return null; if (other.kind !== "strIntrinsic" || other.method !== "charAt" || other.args.length !== 1) return null; - return { charAt: other, code: text.charCodeAt(0) }; + return { charAt: other, code: text.charCodeAt(0), left: other === e.left }; +} + +/** The fcmp predicate of `s.charAt(i) op "c"` on the code unit (or of + * `"c" op s.charAt(i)` when `charAtLeft` is false). A one-unit string + * compares as its unit; an index out of range reads "", which is below + * every one-unit literal, and its unit read is NaN, which the unordered + * predicates count as below. */ +function charAtUnitPredicate(op: IrStrCmpOp, charAtLeft: boolean): string { + const flipped = charAtLeft ? op : ({ "<": ">", "<=": ">=", ">": "<", ">=": "<=" } as const)[op]; + return { "<": "ult", "<=": "ule", ">": "ogt", ">=": "oge" }[flipped]; } export function emitStringExpr( @@ -636,14 +666,7 @@ export function emitStringExpr( // equals no unit, as "" equals no one-unit literal. const unit = charAtUnitTest(e); if (unit !== null) { - const code = host.emitExpr({ - kind: "strIntrinsic", - method: "charCodeAt", - receiver: unit.charAt.receiver, - args: unit.charAt.args, - type: F64, - loc: unit.charAt.loc, - }); + const code = host.emitExpr(charAtUnitRead(host, unit.charAt)); const t = B.tmp(); B.line( `${t} = fcmp ${e.negated ? "une" : "oeq"} double ${code.name}, ${f64Lit(unit.code)}`, @@ -673,6 +696,17 @@ export function emitStringExpr( return { name: t, type: e.type }; } case "strCmp": { + // `s.charAt(i) < "c"` (and `s[i]`) for a one-unit literal compares + // the code unit, as the equality above does. + const unit = charAtUnitTest(e); + if (unit !== null) { + const code = host.emitExpr(charAtUnitRead(host, unit.charAt)); + const t = B.tmp(); + B.line( + `${t} = fcmp ${charAtUnitPredicate(e.op, unit.left)} double ${code.name}, ${f64Lit(unit.code)}`, + ); + return { name: t, type: e.type }; + } const [l, r] = emitStringInputs(host, [e.left, e.right]); const fn = "scr_str_cmp_u16"; host.declare(`declare i32 @${fn}(ptr, ptr)`); @@ -823,6 +857,7 @@ export function emitStringExpr( }; host.templateStringsInstances.set(e.key, inst); } + host.journal?.entry("templateStrings", e.key, inst); return host.own({ name: host.retainFrame(`@${inst.sym}`, e.type), type: e.type }); } case "regexIntrinsic": @@ -869,9 +904,46 @@ export function emitContainerExpr( // the plain elements up to the next one (scr_arr_reserve_spread). const firstSpread = e.elems.findIndex((_, i) => spreadSet.has(i)); const capacity = small ? 0 : (e.capacity ?? (firstSpread < 0 ? e.elems.length : firstSpread)); - B.line(`${arr} = ${arrNewCall(host.shapeHost, elem, String(capacity), small)}`); + // An int32 array's allocation (ir/int32-arrays.ts) constructs + // SCR_ELEM_I32 storage; its elements are proven int32s. The empty + // temporary of a fill that allocates an int32 array with checked + // stores takes SCR_ELEM_I32 storage when the fill's length (which + // evaluates without effects) is at least CHECKED_INT32_MIN_LENGTH, and + // SCR_ELEM_F64 storage otherwise. + const fillLength = host.checkedAllocations.get(e); + if (fillLength !== undefined && e.elems.length === 0) { + const length = host.emitExpr(fillLength); + const long = B.tmp(), + kind = B.tmp(); + B.line(`${long} = fcmp oge double ${length.name}, ${f64Lit(CHECKED_INT32_MIN_LENGTH)}`); + B.line(`${kind} = select i1 ${long}, i32 ${SCR_ELEM_I32}, i32 0`); + host.declare( + small + ? `declare ptr @scr_arr_new_small(i32)` + : `declare ptr @scr_arr_new(i32, ${host.sizeType})`, + ); + B.line( + small + ? `${arr} = call ptr @scr_arr_new_small(i32 ${kind})` + : `${arr} = call ptr @scr_arr_new(i32 ${kind}, ${host.sizeType} ${capacity})`, + ); + return host.own({ name: arr, type: e.type }); + } + const int32 = host.int32Allocations.has(e); + if (int32) { + host.declare( + small + ? `declare ptr @scr_arr_new_small(i32)` + : `declare ptr @scr_arr_new(i32, ${host.sizeType})`, + ); + B.line( + small + ? `${arr} = call ptr @scr_arr_new_small(i32 ${SCR_ELEM_I32})` + : `${arr} = call ptr @scr_arr_new(i32 ${SCR_ELEM_I32}, ${host.sizeType} ${capacity})`, + ); + } else B.line(`${arr} = ${arrNewCall(host.shapeHost, elem, String(capacity), small)}`); const out = host.own({ name: arr, type: e.type }); - const acc = elemAccess(elem); + const acc: ArrayAccess = int32 ? "i32" : elemAccess(elem); let pending: LlValue[] = []; let buffer: string | undefined; const flush = (): void => { @@ -879,7 +951,7 @@ export function emitContainerExpr( buffer = B.tmp(); B.entryAllocas.push(`${buffer} = alloca [${Math.min(e.elems.length, 64)} x i64]`); } - if (pending.length > 0) emitArrayValues(host, arr, acc, pending, false, buffer); + if (pending.length > 0) emitArrayValues(host, arr, acc, elem, pending, false, buffer); pending = []; }; e.elems.forEach((el, i) => { @@ -892,7 +964,8 @@ export function emitContainerExpr( B.line( `call void @scr_arr_reserve_spread(ptr ${arr}, ptr ${v.name}, ${host.sizeType} ${plain})`, ); - host.emitArrayCopyLoop(arr, v.name, acc); + // A literal with spreads is never an int32 allocation. + host.emitArrayCopyLoop(arr, v.name, runtimeAccess(acc)); return; } pending.push(host.emitExpr(el)); @@ -924,7 +997,7 @@ export function emitContainerExpr( } const bound = B.tmp(); B.line(`${bound} = fsub double ${n.name}, ${f64Lit(1)}`); - B.countedLoop(bound, () => host.arrPush(arr, acc, fill), "ole"); + B.countedLoop(bound, () => host.arrPush(arr, acc, fill, elem), "ole"); return out; } case "arrayGet": { @@ -937,33 +1010,65 @@ export function emitContainerExpr( // Ref-element reads return +1 (the inline dense path retains like // the runtime getter); own registers the owned temp in the frame. const elem = e.arr.type.elem; - const acc = elemAccess(elem); - const t = emitDenseArrayGet( + if (host.checkedInt32Array(e.arr)) { + const proven = host.provenDenseIndex(e.arr, e.index); + const value = host.emitByElementKind( + arr.name, + "double", + (kind) => + emitDenseArrayRead( + host, + arr.name, + idx, + e.index, + kind, + elem, + "scr_arr_get_f64", + true, + proven, + ).value, + )!; + return { name: value, type: e.type }; + } + const acc = host.arrayAccess(e.arr, elem); + const read = emitDenseArrayRead( host, arr.name, idx, e.index, acc, elem, - `scr_arr_get_${acc}`, + `scr_arr_get_${runtimeAccess(acc)}`, true, host.provenDenseIndex(e.arr, e.index), ); - return host.own({ name: t, type: e.type }); + if (read.int32 !== null) + // An int32 array's element: its i32 is exact for index consumers. + return { + name: read.value, + type: e.type, + uint32: read.int32, + integer: { name: read.int32, type: "i32", signed: true, range: INT32_RANGE }, + }; + return host.own({ name: read.value, type: e.type }); } case "arrayHas": { const arr = host.emitStableReceiver(e.arr, [e.index]); const idx = host.emitExpr(e.index); if (e.arr.type.kind !== "array") throw new InternalCompilerError("llvm emitter bug: arrayHas on non-array"); - const t = emitDenseArrayState( - host, - arr.name, - idx, - e.index, - "has", - slotWidth(elemAccess(e.arr.type.elem)), - ); + const t = host.checkedInt32Array(e.arr) + ? host.emitByElementKind(arr.name, "i1", (kind) => + emitDenseArrayState(host, arr.name, idx, e.index, "has", slotWidth(kind)), + )! + : emitDenseArrayState( + host, + arr.name, + idx, + e.index, + "has", + host.int32Array(e.arr) ? slotWidth("i32") : denseSlotWidth(host, e.arr.type.elem), + ); return { name: t, type: e.type }; } case "arrayState": { @@ -971,14 +1076,18 @@ export function emitContainerExpr( const idx = host.emitExpr(e.index); if (e.arr.type.kind !== "array") throw new InternalCompilerError("llvm emitter bug: arrayState on non-array"); - const t = emitDenseArrayState( - host, - arr.name, - idx, - e.index, - "state", - slotWidth(elemAccess(e.arr.type.elem)), - ); + const t = host.checkedInt32Array(e.arr) + ? host.emitByElementKind(arr.name, "double", (kind) => + emitDenseArrayState(host, arr.name, idx, e.index, "state", slotWidth(kind)), + )! + : emitDenseArrayState( + host, + arr.name, + idx, + e.index, + "state", + host.int32Array(e.arr) ? slotWidth("i32") : denseSlotWidth(host, e.arr.type.elem), + ); return { name: t, type: e.type }; } case "arrIntrinsic": diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.test.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.test.ts index 5d8f5f26f..244b3afea 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.test.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.test.ts @@ -141,7 +141,7 @@ test("live record capsules bind field hooks in snapshot order", () => { const llvm = emitLlvmModule(module, { pointerBits: 64 }); const tables = [ ...llvm.matchAll( - /^@(\S+)_rops = internal global \{ i64, ptr, ptr, ptr, i8, ptr \} \{ i64 (\d+), /gm, + /^@(\S+)_rops = internal global \{ i64, ptr, ptr, ptr, i8, ptr, ptr \} \{ i64 (\d+), /gm, ), ]; // root and leaf get field hooks; the record with an index-like field name diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index a0a0cf24a..aee7be180 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -13,6 +13,8 @@ import { classFieldAttributesName, isClassOwnEnumerableFieldName, isDirentShape, + KEY_ORDER_FIELD, + shapeKeyOrders, isDynTypedRefType, isRefCounted, shapeHasAccessorSlots, @@ -44,6 +46,8 @@ import type { NullableRefField } from "./nullable-fields.js"; import { emitUnionPeek, emitUnionTag } from "./union-repr.js"; import { inlineUnionCopyLines } from "./inline-unions.js"; import { recordMayKeepSource } from "./record-sources.js"; +import { emitPerKeyOrder } from "./key-orders.js"; +import { registerReplay } from "./emit-journal.js"; /** An owned (+1) union value for a nullable-pointer class field slot `p` * (the unit arm's immortal instance, or a fresh box around a retained @@ -91,6 +95,7 @@ export function dynPromiseAdapter(host: LlvmEmitterContext, inner: IrType): stri if (existing) return existing; const sym = `sc_dpa_${host.dynPromiseAdapters.size}`; host.dynPromiseAdapters.set(key, sym); + host.journal?.allocated(sym, "dynPromiseAdapter", [inner]); host.declare(`declare ptr @scr_promise_payload_ref(ptr)`); host.declare(`declare void @scr_dyn_release_v(ptr)`); host.declare(`declare void @scr_promise_reject_pending(ptr)`); @@ -146,6 +151,7 @@ export function dynPromiseAdapter(host: LlvmEmitterContext, inner: IrType): stri `}`, ``, ); + host.journal?.built(); return sym; } @@ -602,6 +608,7 @@ export function liveDynUnionRefAdapter( const sym = `sc_ldu_${host.liveDynUnionRefAdapters.size}`; host.liveDynUnionRefAdapters.set(key, sym); + host.journal?.allocated(sym, "liveDynUnionRefAdapter", [t]); const adapters = new Map(); for (const { arm, tag } of mutableArms) { @@ -684,6 +691,7 @@ export function liveDynUnionRefAdapter( `}`, ``, ); + host.journal?.built(); return sym; } @@ -732,6 +740,11 @@ export function streamTypedRefMaterializeAdapter( const snapshot = `sc_ldr_${host.liveDynRefAdapters.size}_materialize`; const adapter: LlStreamTypedRefAdapter = { snapshot, commit: "null" }; host.liveDynRefAdapters.set(key, adapter); + host.journal?.allocated( + snapshot.replace(/_materialize$/, ""), + "streamTypedRefMaterializeAdapter", + [t], + ); if (isDynTypedRefType(t)) { const fields = host.classMeta.get(t.className)?.def.fields; if ( @@ -744,6 +757,7 @@ export function streamTypedRefMaterializeAdapter( ) { host.declare(`declare ptr @scr_dyn_class_view_unavailable(ptr)`); adapter.snapshot = "scr_dyn_class_view_unavailable"; + host.journal?.built(); return adapter; } } @@ -753,6 +767,7 @@ export function streamTypedRefMaterializeAdapter( // class view ops are published first. const opsMeta = isDynTypedRefType(t) ? host.classMeta.get(t.className) : undefined; if (opsMeta && liveClassOpsEligible(opsMeta)) adapter.classOps = `@${snapshot}_cops`; + else if (opsMeta && liveBagClassOpsEligible(opsMeta)) adapter.bagClassOps = `@${snapshot}_bops`; adapter.commit = `@${snapshot}_commit`; adapter.commit = host.streamTypedRefCommitAdapter(t, snapshot); const B = new BlockBuilder(); @@ -765,6 +780,7 @@ export function streamTypedRefMaterializeAdapter( ); } if (adapter.classOps !== undefined) liveClassOps(host, snapshot, meta); + if (adapter.bagClassOps !== undefined) liveBagClassOps(host, snapshot, meta, t); if (meta.hierarchy && meta.children.length) { const pre = B.tmp(); for (const line of classPreLines(meta, classStructSym(t.className), "%p", pre, host.sizeType)) @@ -836,11 +852,7 @@ export function streamTypedRefMaterializeAdapter( B.line(`call void @scr_dyn_release_v(ptr ${prototype})`); B.line(`call void @scr_dyn_release_v(ptr ${attached})`); } - for (const field of meta.def.fields.filter( - (f) => - isClassOwnEnumerableFieldName(f.name) && - !(meta.root.def.name === "%Error" && (f.name === "message" || f.name === "name")), - )) { + for (const field of classViewFields(meta)) { const afterField = meta.def.tracksOwnFields ? B.newLabel("field.after") : null; if (afterField) { host.declare(`declare ptr @scr_dyn_obj_get(ptr, ptr, ${host.sizeType})`); @@ -854,32 +866,7 @@ export function streamTypedRefMaterializeAdapter( B.condBr(present, yes, afterField); B.startBlock(yes); } - const { index, compressed } = classFieldIndex(meta, field.name, "compressed"); - const fieldPtr = B.tmp(); - let fieldValue = B.tmp(); - B.line( - `${fieldPtr} = getelementptr inbounds %${classStructSym(t.className)}, ptr %p, i64 0, i32 ${index}`, - ); - if (host.int32Slots.isField(t.className, field.name)) { - const raw = B.tmp(); - B.line(`${raw} = load i32, ptr ${fieldPtr}`); - B.line(`${fieldValue} = sitofp i32 ${raw} to double`); - } else if (host.inlineUnions.classField(t.className, field.name)) - fieldValue = loadStructField(B, host.sizeType, fieldPtr, field.type, true); - else if (compressed) { - const idx = B.tmp(); - B.line(`${idx} = load i32, ptr ${fieldPtr}`); - for (const line of compactRefLoadLines(idx, fieldValue, `${fieldValue}.r`)) - B.line(line.trimStart()); - } else B.line(`${fieldValue} = load ${llFieldType(field.type, false)}, ptr ${fieldPtr}`); - if (llFieldType(field.type, false) === "i8") { - const boolValue = B.tmp(); - B.line(`${boolValue} = trunc i8 ${fieldValue} to i1`); - fieldValue = boolValue; - } - const nullable = host.nullableFields.get(t.className, field.name); - if (nullable) fieldValue = nullableFieldUnion(host, B, fieldValue, nullable); - const boxed = host.streamTypedRefBoxValue(B, field.type, fieldValue); + const { boxed, nullable, fieldValue } = classFieldDynValue(host, B, meta, t, field); // Attributes copied from the bag do not stop the view taking the value. const setter = withAttributes ? "scr_dyn_obj_set_field" : "scr_dyn_obj_set"; if (withAttributes) @@ -1054,52 +1041,68 @@ export function streamTypedRefMaterializeAdapter( const out = B.tmp(); B.line(`${out} = call ptr @scr_dyn_new_obj()`); const byName = new Map(shape.fields.map((field) => [field.name, field])); - const order = shape.declaredOrder ?? shape.fields.map((field) => field.name); - const inOrder = new Set(order); - const fields = [ - ...order.map((name) => byName.get(name)).filter((field) => field !== undefined), - ...shape.fields.filter((field) => !inOrder.has(field.name)), - ]; + // A shape with orderVariants has one field list per key order; its + // order index is no key. + const orders = shapeKeyOrders(shape); + const fieldsOf = (order: readonly string[]): IrRecordShape["fields"][number][] => { + const inOrder = new Set(order); + return [ + ...order.map((name) => byName.get(name)).filter((field) => field !== undefined), + ...shape.fields.filter( + (field) => !inOrder.has(field.name) && field.name !== KEY_ORDER_FIELD, + ), + ]; + }; // Published before the field converters, like the array hooks below. // A Dirent row reads through its finished view (the prototype and // the non-enumerable %dtype). const dirent = isDirentShape(shape); - if (!dirent && liveRecordOpsEligible(fields)) { - adapter.recordOps = `@${snapshot}_rops`; - liveRecordOps(host, t, snapshot, fields); - } - for (const field of fields) { - const index = shape.fields.indexOf(field) + host.nullableUnions.fieldBase(t.shapeId); - const fieldPtr = B.tmp(); - B.line( - `${fieldPtr} = getelementptr inbounds %${mangleRecordStruct(t.shapeId)}, ptr %p, i64 0, i32 ${index}`, - ); - const fieldValue = loadStructField( - B, - host.sizeType, - fieldPtr, - field.type, - host.inlineUnions.record(field.type), - ); - // An ABSENT optional field contributes no key. - const undefinedTag = undefinedArmTag(field.type, host.unionsById); - let skip: string | null = null; - if (undefinedTag >= 0 && field.type.kind === "union") { - const absent = host.fieldAbsentTestIn(B, fieldValue, field.type.unionId); - skip = B.newLabel("live.record.absent"); - const set = B.newLabel("live.record.set"); - B.condBr(absent, skip, set); - B.startBlock(set); - } - const boxed = host.streamTypedRefBoxValue(B, field.type, fieldValue); - B.line( - `call void @scr_dyn_obj_set(ptr ${out}, ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")}, ptr ${boxed})`, - ); - if (skip !== null) { - B.br(skip); - B.startBlock(skip); + if (!dirent && orders.every((order) => liveRecordOpsEligible(fieldsOf(order)))) { + if (orders.length === 1) { + adapter.recordOps = `@${snapshot}_rops`; + liveRecordOps(host, t, snapshot, fieldsOf(orders[0]!)); + } else { + orders.forEach((order, i) => + liveRecordOps(host, t, `${snapshot}_o${i}`, fieldsOf(order)), + ); + adapter.recordOps = `@${snapshot}_rops`; + liveRecordOrderSelect(host, shape, snapshot); } } + emitPerKeyOrder(B, shape, host.nullableUnions.fieldBase(t.shapeId), "%p", (order) => { + for (const field of fieldsOf(order)) { + const index = shape.fields.indexOf(field) + host.nullableUnions.fieldBase(t.shapeId); + const fieldPtr = B.tmp(); + B.line( + `${fieldPtr} = getelementptr inbounds %${mangleRecordStruct(t.shapeId)}, ptr %p, i64 0, i32 ${index}`, + ); + const fieldValue = loadStructField( + B, + host.sizeType, + fieldPtr, + field.type, + host.inlineUnions.record(field.type), + ); + // An ABSENT optional field contributes no key. + const undefinedTag = undefinedArmTag(field.type, host.unionsById); + let skip: string | null = null; + if (undefinedTag >= 0 && field.type.kind === "union") { + const absent = host.fieldAbsentTestIn(B, fieldValue, field.type.unionId); + skip = B.newLabel("live.record.absent"); + const set = B.newLabel("live.record.set"); + B.condBr(absent, skip, set); + B.startBlock(set); + } + const boxed = host.streamTypedRefBoxValue(B, field.type, fieldValue); + B.line( + `call void @scr_dyn_obj_set(ptr ${out}, ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")}, ptr ${boxed})`, + ); + if (skip !== null) { + B.br(skip); + B.startBlock(skip); + } + } + }); if (dirent) { host.declare(`declare void @scr_dyn_dirent_finish(ptr)`); B.line(`call void @scr_dyn_dirent_finish(ptr ${out})`); @@ -1185,6 +1188,7 @@ export function streamTypedRefMaterializeAdapter( `}`, ``, ); + host.journal?.built(); return adapter; } @@ -1361,7 +1365,35 @@ function liveRecordOps( ``, `@${names} = internal constant [${fields.length} x ptr] [${literals.join(", ")}]`, // Mutable: the runtime fills the trailing key-array slot on first use. - `@${ops} = internal global { ${host.sizeType}, ptr, ptr, ptr, i8, ptr } { ${host.sizeType} ${fields.length}, ptr @${names}, ptr @${has}, ptr @${get}, i8 ${optional.length === 0 ? 1 : 0}, ptr null }`, + `@${ops} = internal global { ${host.sizeType}, ptr, ptr, ptr, i8, ptr, ptr } { ${host.sizeType} ${fields.length}, ptr @${names}, ptr @${has}, ptr @${get}, i8 ${optional.length === 0 ? 1 : 0}, ptr null, ptr null }`, + ``, + ); +} + +/** `@_rops` for a shape with orderVariants: a record-ops table + * whose `select` hook answers the `_o_rops` table (liveRecordOps) of the + * record's current key order, which a write of an optional key can change + * after the record is boxed. */ +function liveRecordOrderSelect( + host: LlvmEmitterContext, + shape: IrRecordShape, + snapshot: string, +): void { + const B = new BlockBuilder(); + const out = B.slot(); + B.entryAllocas.push(`${out} = alloca ptr`); + emitPerKeyOrder(B, shape, host.nullableUnions.fieldBase(shape.id), "%p", (_, i) => { + B.line(`store ptr @${snapshot}_o${i}_rops, ptr ${out}`); + }); + const ops = B.tmp(); + B.line(`${ops} = load ptr, ptr ${out}`); + B.terminate(`ret ptr ${ops}`); + host.resolveThunkDefs.push( + `define internal ptr @${snapshot}_select(ptr %p) ${FN_ATTRS} { ; live record ops by key order`, + B.render(), + `}`, + ``, + `@${snapshot}_rops = internal global { ${host.sizeType}, ptr, ptr, ptr, i8, ptr, ptr } { ${host.sizeType} 0, ptr null, ptr null, ptr null, i8 0, ptr null, ptr @${snapshot}_select }`, ``, ); } @@ -1432,14 +1464,157 @@ function liveClassOps(host: LlvmEmitterContext, snapshot: string, meta: LlClassM ); } +/** The checked-dynamic value a class instance's view holds for `field`, read + * from its native slot of `%p`: `boxed` (+1), and the nullable union + * `fieldValue` the caller releases when `nullable` is set. */ +function classFieldDynValue( + host: LlvmEmitterContext, + B: BlockBuilder, + meta: LlClassMeta, + t: Extract, + field: LlClassMeta["def"]["fields"][number], +): { boxed: string; nullable: NullableRefField | null; fieldValue: string } { + const { index, compressed } = classFieldIndex(meta, field.name, "compressed"); + const fieldPtr = B.tmp(); + let fieldValue = B.tmp(); + B.line( + `${fieldPtr} = getelementptr inbounds %${classStructSym(t.className)}, ptr %p, i64 0, i32 ${index}`, + ); + if (host.int32Slots.isField(t.className, field.name)) { + const raw = B.tmp(); + B.line(`${raw} = load i32, ptr ${fieldPtr}`); + B.line(`${fieldValue} = sitofp i32 ${raw} to double`); + } else if (host.inlineUnions.classField(t.className, field.name)) + fieldValue = loadStructField(B, host.sizeType, fieldPtr, field.type, true); + else if (compressed) { + const idx = B.tmp(); + B.line(`${idx} = load i32, ptr ${fieldPtr}`); + for (const line of compactRefLoadLines(idx, fieldValue, `${fieldValue}.r`)) + B.line(line.trimStart()); + } else B.line(`${fieldValue} = load ${llFieldType(field.type, false)}, ptr ${fieldPtr}`); + if (llFieldType(field.type, false) === "i8") { + const boolValue = B.tmp(); + B.line(`${boolValue} = trunc i8 ${fieldValue} to i1`); + fieldValue = boolValue; + } + const nullable = host.nullableFields.get(t.className, field.name); + if (nullable) fieldValue = nullableFieldUnion(host, B, fieldValue, nullable); + return { boxed: host.streamTypedRefBoxValue(B, field.type, fieldValue), nullable, fieldValue }; +} + +/** The fields an instance's view takes from their native slots, in order: + * its own enumerable fields, except an Error's message and name. */ +function classViewFields(meta: LlClassMeta): LlClassMeta["def"]["fields"] { + return meta.def.fields.filter( + (f) => + isClassOwnEnumerableFieldName(f.name) && + !(meta.root.def.name === "%Error" && (f.name === "message" || f.name === "name")), + ); +} + +/** Classes whose instance views the materializer builds from the property + * bag and the prototype data alone (see liveBagClassOps): JavaScript classes + * tracking their own fields, outside the Error and runtime families, with no + * subclass whose view another materializer builds. */ +function liveBagClassOpsEligible(meta: LlClassMeta): boolean { + return ( + meta.def.tracksOwnFields === true && + (meta.def.prototypeDataHelper !== undefined || + meta.def.instancePrototypeHelper !== undefined) && + !meta.def.runtime && + !meta.root.def.runtime && + meta.root.def.name !== "%Error" && + !(meta.hierarchy && meta.children.length > 0) && + meta.def.fields.some((field) => field.name === DYN_CLASS_PROPERTIES) && + !meta.def.fields.some( + (field) => + field.name === CLASS_ATTRIBUTES || + meta.def.fields.some((f) => f.name === classFieldAttributesName(field.name)), + ) + ); +} + +/** The hooks of a bag-tracked class instance capsule + * (ScrDynTypedBagClassOps). The materializer gives the instance's view + * exactly the bag's string-keyed properties, the values of the declared + * fields among them from their native slots, and the prototype these hooks + * return, so a method call resolves without building the view. */ +function liveBagClassOps( + host: LlvmEmitterContext, + snapshot: string, + meta: LlClassMeta, + t: Extract, +): void { + const struct = classStructSym(t.className); + const bag = new BlockBuilder(); + const slot = bag.tmp(), + value = bag.tmp(); + bag.line( + `${slot} = getelementptr inbounds %${struct}, ptr %p, i64 0, i32 ${classFieldIndex(meta, DYN_CLASS_PROPERTIES).index}`, + ); + bag.line(`${value} = load ptr, ptr ${slot}`); + bag.terminate(`ret ptr ${value}`); + const proto = new BlockBuilder(); + const prototype = proto.tmp(); + if (meta.def.instancePrototypeHelper) { + const receiver = proto.tmp(); + proto.line(`${receiver} = call ptr ${vAdapters(host.shapeHost, t).retain}(ptr %p)`); + proto.line( + `${prototype} = call ptr @${mangleFunction(meta.def.instancePrototypeHelper)}(ptr ${receiver})`, + ); + } else proto.line(`${prototype} = call ptr @${mangleFunction(meta.def.prototypeDataHelper!)}()`); + proto.terminate(`ret ptr ${prototype}`); + // Field `%i` of the view's field list, as the materializer boxes it. + const fields = classViewFields(meta); + const get = new BlockBuilder(); + const cases = fields.map((_, i) => get.newLabel(`field.${i}`)); + const none = get.newLabel("field.none"); + get.terminate( + `switch ${host.sizeType} %i, label %${none} [ ${cases.map((label, i) => `${host.sizeType} ${i}, label %${label}`).join(" ")} ]`, + ); + fields.forEach((field, i) => { + get.startBlock(cases[i]!); + const { boxed, nullable, fieldValue } = classFieldDynValue(host, get, meta, t, field); + if (nullable) { + host.declare(`declare void @scr_union_release(ptr)`); + get.line(`call void @scr_union_release(ptr ${fieldValue})`); + } + get.terminate(`ret ptr ${boxed}`); + }); + get.startBlock(none); + get.terminate(`ret ptr null`); + const names = fields.map((field) => `ptr ${host.internLiteral(field.name)}`); + host.resolveThunkDefs.push( + `define internal ptr @${snapshot}_field(ptr %p, ${host.sizeType} %i) ${FN_ATTRS} { ; view field of ${meta.def.name}`, + get.render(), + `}`, + ``, + `@${snapshot}_fnames = internal constant [${names.length} x ptr] ${names.length ? `[${names.join(", ")}]` : "zeroinitializer"}`, + ); + host.resolveThunkDefs.push( + `define internal ptr @${snapshot}_bag(ptr %p) ${FN_ATTRS} { ; property bag of ${meta.def.name}`, + bag.render(), + `}`, + ``, + `define internal ptr @${snapshot}_proto(ptr %p) ${FN_ATTRS} { ; view prototype of ${meta.def.name}`, + proto.render(), + `}`, + ``, + `@${snapshot}_bops = internal constant { ptr, ptr, ${host.sizeType}, ptr, ptr } { ptr @${snapshot}_bag, ptr @${snapshot}_proto, ${host.sizeType} ${fields.length}, ptr @${snapshot}_fnames, ptr @${snapshot}_field }`, + ``, + ); +} + /** The live view a capsule of this adapter binds: the runtime bind call * and its ops constant, with the view code a deferred receiver recipe - * records (1 array, 2 record, 3 class). */ + * records (1 array, 2 record, 3 class, 4 bag-tracked class). */ export function liveRefView( adapter: LlStreamTypedRefAdapter, -): { bind: string; ops: string; code: 1 | 2 | 3 } | null { +): { bind: string; ops: string; code: 1 | 2 | 3 | 4 } | null { if (adapter.classOps !== undefined) return { bind: "scr_dyn_typed_ref_bind_class", ops: adapter.classOps, code: 3 }; + if (adapter.bagClassOps !== undefined) + return { bind: "scr_dyn_typed_ref_bind_bag_class", ops: adapter.bagClassOps, code: 4 }; if (adapter.recordOps !== undefined) return { bind: "scr_dyn_typed_ref_bind_record", ops: adapter.recordOps, code: 2 }; if (adapter.arrayOps !== undefined) @@ -1469,6 +1644,7 @@ export function streamFromArrayAdapter( if (existing) return existing; const sym = `sc_sfa_${host.streamFromArrayAdapters.size}`; host.streamFromArrayAdapters.set(key, sym); + host.journal?.allocated(sym, "streamFromArrayAdapter", [t]); const B = new BlockBuilder(); const unionDef = elem.kind === "union" ? host.unionsById.get(elem.unionId) : undefined; if (elem.kind === "union" && !unionDef) { @@ -1585,5 +1761,17 @@ export function streamFromArrayAdapter( `}`, ``, ); + host.journal?.built(); return sym; } + +registerReplay("dynPromiseAdapter", (host, args) => dynPromiseAdapter(host, args[0] as IrType)); +registerReplay("liveDynUnionRefAdapter", (host, args) => + liveDynUnionRefAdapter(host, args[0] as IrType & { kind: "union" }), +); +registerReplay("streamTypedRefMaterializeAdapter", (host, args) => + streamTypedRefMaterializeAdapter(host, args[0] as IrType).snapshot.replace(/_materialize$/, ""), +); +registerReplay("streamFromArrayAdapter", (host, args) => + streamFromArrayAdapter(host, args[0] as IrType & { kind: "array" }), +); diff --git a/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts b/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts index 0570d0ea7..14a2e3432 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts @@ -4,6 +4,7 @@ import { type IrType, isRefCounted, typeKey } from "../../ir/ir.js"; import { FN_ATTRS, releaseSym, retainSym, traceArg } from "./shapes.js"; import type { LlvmEmitterContext } from "./expr-context.js"; import { unionPeekLines, unionTagLines } from "./union-repr.js"; +import { registerReplay } from "./emit-journal.js"; export function streamDataAdapter( host: LlvmEmitterContext, @@ -14,6 +15,7 @@ export function streamDataAdapter( if (sym) return sym; sym = `sc_ee_dad_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "streamDataAdapter", [cbT]); const p = cbT.params[0]; if (cbT.params.length > 1) { throw new InternalCompilerError( @@ -207,12 +209,13 @@ export function streamDataAdapter( d.push(`}`, ``); } host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } export function streamDoneFnFor( host: LlvmEmitterContext, - kind: "w" | "f" | "d" | "t" | "l", + kind: "w" | "f" | "d" | "t" | "l" | "c", doneT: IrType & { kind: "func" }, ): string { const key = `sd:${kind}:${typeKey(doneT)}`; @@ -220,6 +223,7 @@ export function streamDoneFnFor( if (sym) return sym; sym = `sc_sd_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "streamDoneFnFor", [kind, doneT]); const errT = doneT.params[0]; let errTag = -1; if (errT !== undefined) { @@ -256,19 +260,21 @@ export function streamDoneFnFor( } } const entry = - kind === "w" - ? "scr_stream_write_done" - : kind === "f" - ? "scr_stream_final_done" - : kind === "d" - ? "scr_stream_destroy_done" - : kind === "t" - ? valueData - ? "scr_stream_transform_done_value" - : "scr_stream_transform_done" - : valueData - ? "scr_stream_flush_done_value" - : "scr_stream_flush_done"; + kind === "c" + ? "scr_stream_construct_done" + : kind === "w" + ? "scr_stream_write_done" + : kind === "f" + ? "scr_stream_final_done" + : kind === "d" + ? "scr_stream_destroy_done" + : kind === "t" + ? valueData + ? "scr_stream_transform_done_value" + : "scr_stream_transform_done" + : valueData + ? "scr_stream_flush_done_value" + : "scr_stream_flush_done"; const twoSlot = (kind === "t" || kind === "l") && !valueData; host.declare( `declare void @${entry}(ptr, ptr${twoSlot ? ", ptr, ptr" : valueData ? ", ptr" : ""})`, @@ -347,6 +353,7 @@ export function streamDoneFnFor( d.push(` call void ${releaseSym(host.shapeHost, dataT)}(ptr %d)`); d.push(` ret void`, `}`, ``); host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } @@ -360,6 +367,7 @@ export function fsRenameThunkFor(host: LlvmEmitterContext, cbT: IrType & { kind: if (sym) return sym; sym = `sc_fsren_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "fsRenameThunkFor", [cbT]); const param = cbT.params[0]!; const d: string[] = [ `define internal void @${sym}(ptr %cb, ptr %err) ${FN_ATTRS} { ; fs.rename callback ${typeKey(cbT)}`, @@ -415,6 +423,7 @@ export function fsRenameThunkFor(host: LlvmEmitterContext, cbT: IrType & { kind: ``, ); host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } @@ -447,6 +456,7 @@ export function cryptoIntThunkFor( if (sym) return sym; sym = `sc_cryptoint_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "cryptoIntThunkFor", [cbT]); const d: string[] = [ `define internal void @${sym}(ptr %cb, double %value) ${FN_ATTRS} { ; randomInt callback ${typeKey(cbT)}`, `entry:`, @@ -489,6 +499,7 @@ export function cryptoIntThunkFor( ``, ); host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } @@ -507,6 +518,7 @@ export function zlibBytesThunkFor( if (sym) return sym; sym = `sc_${family}bytes_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "zlibBytesThunkFor", [cbT, arrayBuffer, family, valueMode]); const d: string[] = [ `define internal void @${sym}(ptr %cb, ptr %err, ptr %value) ${FN_ATTRS} { ; zlib bytes callback ${typeKey(cbT)}`, `entry:`, @@ -618,12 +630,13 @@ export function zlibBytesThunkFor( ``, ); host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } export function streamCbThunkFor( host: LlvmEmitterContext, - kind: "r" | "w" | "f" | "d" | "t" | "l" | "e", + kind: "r" | "w" | "f" | "d" | "t" | "l" | "e" | "c" | "v", cbT: IrType, ): string { if (cbT.kind !== "func") @@ -633,14 +646,17 @@ export function streamCbThunkFor( if (sym) return sym; sym = `sc_scb_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "streamCbThunkFor", [kind, cbT]); const runtimeParams = kind === "r" ? ["ptr %s", "double %size"] : kind === "w" || kind === "t" ? ["ptr %s", "ptr %chunk"] - : kind === "d" || kind === "e" - ? ["ptr %s", "ptr %err"] - : ["ptr %s"]; + : kind === "v" + ? ["ptr %s", "ptr %chunks"] + : kind === "d" || kind === "e" + ? ["ptr %s", "ptr %err"] + : ["ptr %s"]; const declared = cbT.params; const hasThis = declared[0] !== undefined && declared[0].kind === "object"; if (declared.length === 0) { @@ -649,7 +665,9 @@ export function streamCbThunkFor( ); } const off = hasThis ? 1 : 0; - const full = (kind === "r" ? 1 : kind === "w" || kind === "t" ? 3 : kind === "d" ? 2 : 1) + off; + const full = + (kind === "r" ? 1 : kind === "w" || kind === "t" ? 3 : kind === "d" || kind === "v" ? 2 : 1) + + off; if (declared.length > full) { throw new InternalCompilerError( `llvm emitter bug: stream '${kind}' callback declares ${declared.length} params (frontend must fence)`, @@ -697,6 +715,31 @@ export function streamCbThunkFor( } continue; } + if (kind === "v" && pos === 0) { + // writev's chunks: the batch array (borrowed from the runtime), as + // a dyn value or converted to the declared type + if (p.kind === "dyn") { + host.declare(`declare ptr @scr_dyn_retain_v(ptr)`); + d.push(` %vc${i} = call ptr @scr_dyn_retain_v(ptr %chunks)`); + passed.push(`ptr %vc${i}`); + } else { + const helper = host.dyn.dynCheckHelper(p, false); + const ty = host.llType(p); + host.declare(`declare zeroext i1 @scr_exc_pending()`); + if (hasThis) host.declare(`declare void @scr_stream_release_v(ptr)`); + d.push( + ` %vt${i} = call ${ty === "i1" ? "zeroext i1" : ty} @${helper}(ptr %chunks, ptr null)`, + ` %vtbad${i} = call zeroext i1 @scr_exc_pending()`, + ` br i1 %vtbad${i}, label %vtfail${i}, label %vtok${i}`, + `vtfail${i}:`, + ...(hasThis ? [` call void @scr_stream_release_v(ptr %sr)`] : []), + ` ret void`, + `vtok${i}:`, + ); + passed.push(`${ty} %vt${i}`); + } + continue; + } const isChunkPos = (kind === "w" || kind === "t") && pos === 0; const isEncPos = (kind === "w" || kind === "t") && pos === 1; const isErrPos = (kind === "d" || kind === "e") && pos === 0; @@ -705,7 +748,7 @@ export function streamCbThunkFor( ? false : kind === "w" || kind === "t" ? pos === 2 - : kind === "f" || kind === "l" + : kind === "f" || kind === "l" || kind === "c" ? pos === 0 : pos === 1; if (p.kind === "dyn") { @@ -751,7 +794,7 @@ export function streamCbThunkFor( d.push(` br label %ego${i}`, `ego${i}:`, ` %edv${i} = load ptr, ptr %edslot${i}`); passed.push(`ptr %edv${i}`); } else if (isDonePos) { - const glue = `scr_stream_done_dyn_${kind}`; + const glue = `scr_stream_done_dyn_${kind === "v" ? "w" : kind}`; host.declare(`declare ptr @${glue}(ptr, ptr, ${host.sizeType})`); host.declare(`declare ptr @scr_dyn_new_func(ptr, ptr, i32, ptr, ptr)`); const clo = doneClosure(`@${glue}`, `dd${i}`); @@ -872,7 +915,8 @@ export function streamCbThunkFor( ); passed.push(`ptr %euv${i}`); } else if (isDonePos) { - const doneKind = kind as "w" | "f" | "d" | "t" | "l"; // "e" has no done position + // "e" has no done position; writev completes as a write + const doneKind = (kind === "v" ? "w" : kind) as "w" | "f" | "d" | "t" | "l" | "c"; if (p.kind !== "func") throw new InternalCompilerError("llvm emitter bug: stream done callback not a func"); const doneFn = host.streamDoneFnFor(doneKind, p); @@ -901,5 +945,35 @@ export function streamCbThunkFor( } d.push(` ret void`, `}`, ``); host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } + +registerReplay("streamDataAdapter", (host, args) => + streamDataAdapter(host, args[0] as IrType & { kind: "func" }), +); +registerReplay("streamDoneFnFor", (host, args) => + streamDoneFnFor( + host, + args[0] as "w" | "f" | "d" | "t" | "l", + args[1] as IrType & { kind: "func" }, + ), +); +registerReplay("fsRenameThunkFor", (host, args) => + fsRenameThunkFor(host, args[0] as IrType & { kind: "func" }), +); +registerReplay("cryptoIntThunkFor", (host, args) => + cryptoIntThunkFor(host, args[0] as IrType & { kind: "func" }), +); +registerReplay("zlibBytesThunkFor", (host, args) => + zlibBytesThunkFor( + host, + args[0] as IrType & { kind: "func" }, + args[1] as boolean, + args[2] as string, + args[3] as BytesThunkValue, + ), +); +registerReplay("streamCbThunkFor", (host, args) => + streamCbThunkFor(host, args[0] as "r" | "w" | "f" | "d" | "t" | "l" | "e", args[1] as IrType), +); diff --git a/packages/compiler/src/backend/llvm/int32-kind-loops.ts b/packages/compiler/src/backend/llvm/int32-kind-loops.ts new file mode 100644 index 000000000..12e38c1f0 --- /dev/null +++ b/packages/compiler/src/backend/llvm/int32-kind-loops.ts @@ -0,0 +1,135 @@ +import type { IrExpr, IrStmt } from "../../ir/ir.js"; +import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; +import { MAX_LOOP_NODES, SIMPLE_STATEMENTS } from "./speculative-integers.js"; + +/** Int32 kind loops. + * + * An int32 array with checked stores (ir/int32-slots.ts) holds int32 slots + * (or doubles, when its fill was short) until a store of a value that is + * not an int32 widens it to doubles in place, so an access to it tests its + * element kind first (LlEmitter.emitByElementKind). An innermost loop that + * accesses such arrays tests their kinds once on entry instead + * (emitLoopCopies). The fast copy runs when every one of them holds int32 + * slots and reads and writes them as int32 arrays; the wide copy runs when + * every one holds doubles and accesses them as double arrays; a loop over + * several arrays whose kinds differ runs the mixed copy, which tests the + * kinds at each access. A store of the fast copy whose value is not an + * int32 branches to the same statement in a third copy, which re-executes + * it with kind tests (widening the array) and finishes the iteration, then + * enters the mixed copy, or the wide copy when the loop has one array. + * Re-execution is exact because such a store's statement evaluates nothing + * but locals, literals, number arithmetic and element reads before it + * writes. + * + * Nothing else can widen these arrays: they never reach code other than + * the compiled element operations of their own function + * (ir/int32-arrays.ts). */ +export interface Int32KindLoopPlan { + /** The arrays the fast copy reads and writes as int32 arrays, by local, + * with one of their references in the loop. */ + arrays: Map; + /** Element writes and pushes whose value may not be an int32: the + * statements the fast copy may leave at. */ + writes: Set; +} + +type KindLoop = Extract; + +/** A number or boolean expression whose evaluation has no effect: locals, + * literals, arithmetic, comparisons and element reads of local arrays. */ +function isPure(e: IrExpr): boolean { + if (e.type.kind !== "f64" && e.type.kind !== "bool") return false; + switch (e.kind) { + case "numLit": + case "boolLit": + case "varRef": + return true; + case "bin": + return ( + (e.left.type.kind === "f64" || e.left.type.kind === "bool") && + isPure(e.left) && + isPure(e.right) + ); + case "unary": + return isPure(e.operand); + case "ternary": + return isPure(e.cond) && isPure(e.then) && isPure(e.else_); + case "arrayGet": + case "arrayHas": + return e.arr.kind === "varRef" && isPure(e.index); + default: + return false; + } +} + +/** The plan for `loop`, or null. `checked` names the int32 array locals + * with checked stores of the function; `provenInt32` the values the static + * range analysis proves int32s (stores that never leave the fast copy). */ +export function planInt32KindLoop( + loop: KindLoop, + checked: (localId: string) => boolean, + provenInt32: (e: IrExpr) => boolean, +): Int32KindLoopPlan | null { + const arrays = new Map(); + const writes = new Set(); + const assigned = new Set(); + let nodes = 0; + let rejected = false; + const reject = (): false => { + rejected = true; + return false; + }; + const array = (e: IrExpr): string | null => + e.kind === "varRef" && checked(e.localId) ? e.localId : null; + const push = (e: IrExpr): (IrExpr & { kind: "arrIntrinsic" }) | null => + e.kind === "arrIntrinsic" && e.method === "push" && array(e.receiver) !== null ? e : null; + + const expr = (e: IrExpr): boolean => { + if (rejected) return false; + nodes++; + if (e.kind === "varRef" && checked(e.localId) && !arrays.has(e.localId)) + arrays.set(e.localId, e); + // A push the statement walk did not take as a write: its values must + // be proven int32s, since it cannot leave the fast copy. + const pushed = push(e); + if (pushed && !pushed.args.every(provenInt32)) return reject(); + return everyExprChild(e, expr, nested); + }; + // Statements inside expressions run mid-expression, where the fast copy + // cannot leave: they may not write these arrays. + const nested = (s: IrStmt): boolean => { + nodes++; + if (s.kind === "assign" || s.kind === "varDecl") assigned.add(s.localId); + if (s.kind === "arraySet" && array(s.arr) !== null && !provenInt32(s.value)) return reject(); + return everyStmtChild(s, expr, nested); + }; + const stmt = (s: IrStmt): boolean => { + if (rejected) return false; + nodes++; + if (!SIMPLE_STATEMENTS.has(s.kind)) return reject(); + if (s.kind === "assign" || s.kind === "varDecl") assigned.add(s.localId); + if (s.kind === "arraySet" && array(s.arr) !== null && !provenInt32(s.value)) { + if (!isPure(s.index) || !isPure(s.value)) return reject(); + writes.add(s); + return everyStmtChild(s, expr, stmt); + } + const pushed = s.kind === "exprStmt" ? push(s.expr) : null; + if (pushed && !pushed.args.every(provenInt32)) { + if (!pushed.args.every(isPure)) return reject(); + writes.add(s); + expr(pushed.receiver); + pushed.args.forEach(expr); + return !rejected; + } + return everyStmtChild(s, expr, stmt); + }; + + if (loop.cond) expr(loop.cond); + if (loop.kind === "for" && loop.update) stmt(loop.update); + for (const s of loop.body) stmt(s); + if (rejected || nodes > MAX_LOOP_NODES || arrays.size === 0) return null; + // A loop that rebinds one of the arrays could reach another array of + // either kind. + for (const id of arrays.keys()) if (assigned.has(id)) return null; + return { arrays, writes }; +} diff --git a/packages/compiler/src/backend/llvm/integer-or-nan-indexes.test.ts b/packages/compiler/src/backend/llvm/integer-or-nan-indexes.test.ts new file mode 100644 index 000000000..93a0a73ed --- /dev/null +++ b/packages/compiler/src/backend/llvm/integer-or-nan-indexes.test.ts @@ -0,0 +1,130 @@ +import { expect, test } from "vitest"; +import { F64, arrayOf, bytesOf, type IrExpr, type IrModule, type IrStmt } from "../../ir/ir.js"; +import { emitLlvmModule } from "./emitter.js"; + +const loc = { file: "integer-or-nan-indexes.ts", start: 0, end: 0 }; +const nums = arrayOf(F64); +const u8 = bytesOf("u8"); +const ref = (localId: string, type: IrExpr["type"]): IrExpr => ({ + kind: "varRef", + localId, + type, + loc, +}); +const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc }); +/** `t[i]!`: the element, or NaN for an index that misses. */ +const byteRead = (index: IrExpr): IrExpr => ({ + kind: "bytesIntrinsic", + method: "get", + receiver: ref("t", u8), + args: [index], + type: F64, + loc, + invalidNaN: true, +}); + +function module(body: IrStmt[]): IrModule { + return { + irVersion: 17, + sourceFile: loc.file, + entry: "__main", + unions: [], + functions: [ + { name: "__main", params: [], returnType: F64, locals: [], body: [], loc }, + { + name: "read", + params: [ + { name: "a", localId: "a", type: nums }, + { name: "t", localId: "t", type: u8 }, + { name: "i", localId: "i", type: F64 }, + ], + returnType: F64, + locals: [ + { id: "a", name: "a", type: nums, mutable: false }, + { id: "t", name: "t", type: u8, mutable: false }, + { id: "i", name: "i", type: F64, mutable: false }, + { id: "c", name: "c", type: F64, mutable: true }, + ], + body, + loc, + }, + ], + }; +} + +function body(llvm: string): string { + return /^define internal [^\n]*@sc_bf_read(?:\.i32)?\([^]*?^}/m.exec(llvm)![0]; +} + +const count = (text: string, pattern: RegExp): number => text.match(pattern)?.length ?? 0; + +test("an index bound to a byte read that may miss checks only that it is ordered", () => { + const llvm = emitLlvmModule( + module([ + { kind: "varDecl", localId: "c", init: byteRead(ref("i", F64)), loc }, + { + kind: "return", + value: { + kind: "bin", + op: "+", + left: { kind: "arrayGet", arr: ref("a", nums), index: ref("c", F64), type: F64, loc }, + right: { + kind: "bytesIntrinsic", + method: "get", + receiver: ref("t", u8), + args: [{ kind: "bin", op: "+", left: ref("c", F64), right: num(1), type: F64, loc }], + type: F64, + loc, + invalidNaN: true, + }, + type: F64, + loc, + }, + loc, + }, + ]), + ); + const fn = body(llvm); + // Only the parameter index `i` round-trips through a double. + expect(count(fn, /fcmp oeq double/g)).toBe(1); + expect(count(fn, /fcmp ord double/g)).toBe(2); +}); + +test("a local also written an ordinary number keeps the exact index check", () => { + const llvm = emitLlvmModule( + module([ + { kind: "varDecl", localId: "c", init: byteRead(ref("i", F64)), loc }, + { kind: "assign", localId: "c", value: ref("i", F64), loc }, + { + kind: "return", + value: { kind: "arrayGet", arr: ref("a", nums), index: ref("c", F64), type: F64, loc }, + loc, + }, + ]), + ); + const fn = body(llvm); + expect(count(fn, /fcmp oeq double/g)).toBe(2); + expect(fn).not.toContain("fcmp ord double"); +}); + +test("a local whose writes grow from its own value keeps the exact index check", () => { + const llvm = emitLlvmModule( + module([ + { kind: "varDecl", localId: "c", init: byteRead(ref("i", F64)), loc }, + { + kind: "assign", + localId: "c", + value: { kind: "bin", op: "+", left: ref("c", F64), right: num(1), type: F64, loc }, + loc, + }, + { + kind: "return", + value: { kind: "arrayGet", arr: ref("a", nums), index: ref("c", F64), type: F64, loc }, + loc, + }, + ]), + ); + const fn = body(llvm); + expect(count(fn, /fcmp oeq double/g)).toBe(2); + expect(fn).not.toContain("fcmp ord double"); +}); diff --git a/packages/compiler/src/backend/llvm/integer-values.ts b/packages/compiler/src/backend/llvm/integer-values.ts index 63e279d04..ddbcdd8da 100644 --- a/packages/compiler/src/backend/llvm/integer-values.ts +++ b/packages/compiler/src/backend/llvm/integer-values.ts @@ -1,6 +1,6 @@ import type { IrExpr, IrType } from "../../ir/ir.js"; import type { IntegerRange } from "../../ir/integer-ranges.js"; -import type { LlValue, LlvmEmitterContext } from "./expr-context.js"; +import type { LlValue, LlvmEmitterContext, WideInteger } from "./expr-context.js"; export type LlInteger = NonNullable; @@ -51,6 +51,89 @@ export function exactInteger( return { name, type: "i64", signed: true, range }; } +/** Doubles hold every integer of smaller magnitude exactly. */ +const EXACT_DOUBLES = 2 ** 53; +/** Wide views stay within this magnitude: a product of two int32s reaches + * 2^62, one exact term of at most 2^53 may be added to it, and no i64 + * result wraps. */ +const WIDE_BOUND = 2 ** 62 + 2 ** 53; +/** Array and string indexes are below 2^32 - 1: a wide view exact below + * this many is exact at every index a length can admit. */ +const INDEX_LIMIT = 2 ** 32; + +function magnitude(range: IntegerRange): number { + return Math.max(Math.abs(range.min), Math.abs(range.max)); +} + +/** The wide view of `l op r` for `+`, `-` and `*` when the exact operands + * have no proven result range (their product or sum may pass 2^53). + * + * - A product of two exact integers equals its double whenever its + * magnitude is below 2^53: the double product of integers is exact there. + * - A sum or difference of two exact integers likewise. + * - A sum or difference of a wide view (exact below T) and an exact integer + * of magnitude at most B equals its double below min(T - B, 2^53): the + * view's magnitude is then below T, so its double is exact, and the + * double sum of two exact integers below 2^53 is exact. + * + * Every i64 result stays within 2^62 + 2^53 in magnitude, so none wraps. A view + * exact below fewer than 2^32 helps no index and is not kept. */ +export function wideArithmetic( + host: LlvmEmitterContext, + op: string, + l: LlValue, + li: LlInteger | null, + r: LlValue, + ri: LlInteger | null, +): WideInteger | null { + if (op !== "+" && op !== "-" && op !== "*") return null; + const view = (value: LlValue, integer: LlInteger | null): WideInteger | null => + integer + ? { name: "", below: Infinity, bound: magnitude(integer.range) } + : (value.wideInteger ?? null); + const lv = view(l, li), + rv = view(r, ri); + if (!lv || !rv) return null; + let below: number, bound: number; + if (op === "*") { + if (lv.below !== Infinity || rv.below !== Infinity) return null; + below = EXACT_DOUBLES; + bound = lv.bound * rv.bound; + } else if (lv.below === Infinity && rv.below === Infinity) { + below = EXACT_DOUBLES; + bound = lv.bound + rv.bound; + } else if (lv.below === Infinity || rv.below === Infinity) { + const [exact, wide] = lv.below === Infinity ? [lv, rv] : [rv, lv]; + below = Math.min(wide.below - exact.bound, EXACT_DOUBLES); + bound = exact.bound + wide.bound; + } else return null; + if (!(bound <= WIDE_BOUND) || below < INDEX_LIMIT) return null; + const operand = (value: LlValue, integer: LlInteger | null): string => + integer ? widenInteger(host, integer) : value.wideInteger!.name; + const left = operand(l, li), + right = operand(r, ri); + const name = host.B.tmp(); + host.B.line( + `${name} = ${op === "+" ? "add" : op === "-" ? "sub" : "mul"} nsw i64 ${left}, ${right}`, + ); + return { name, below, bound }; +} + +/** An i64 index for a consumer that compares it, unsigned, with a length or + * capacity below 2^32 and takes the double's path otherwise: the exact + * integer of `value`, or its wide view, which equals the double at every + * such index. Null when the index needs the double round trip. */ +export function indexInteger( + host: LlvmEmitterContext, + value: LlValue, + expr?: IrExpr, +): string | null { + const integer = exactInteger(host, value, expr); + if (integer) return widenInteger(host, integer); + const wide = value.wideInteger; + return wide && wide.below >= INDEX_LIMIT ? wide.name : null; +} + export function widenInteger(host: LlvmEmitterContext, value: LlInteger): string { if (value.type === "i64") return value.name; const wide = host.B.tmp(); diff --git a/packages/compiler/src/backend/llvm/js-ic.ts b/packages/compiler/src/backend/llvm/js-ic.ts index 45c18b803..7516673cd 100644 --- a/packages/compiler/src/backend/llvm/js-ic.ts +++ b/packages/compiler/src/backend/llvm/js-ic.ts @@ -732,7 +732,7 @@ function emitKeyed(host: Host, e: ExprOf<"call">, callee: IrFunction): LlValue { /** dynInvoke's method read on an ordinary object through the site's * cache, else scr_dyn_prepare_method. Returns the owned method (or NULL, - * which scr_dyn_invoke_prepared answers with the full dispatch). */ + * which scr_dyn_invoke_prepared_id answers with the full dispatch). */ export function emitJsIcMethod(host: Host, e: ExprOf<"dynInvoke">, receiver: string): string { const B = host.B; const method = e.method; diff --git a/packages/compiler/src/backend/llvm/key-orders.ts b/packages/compiler/src/backend/llvm/key-orders.ts new file mode 100644 index 000000000..9dbda1414 --- /dev/null +++ b/packages/compiler/src/backend/llvm/key-orders.ts @@ -0,0 +1,49 @@ +/* Key-order dispatch for shapes whose values record the order their keys + * were written in (IrRecordShape.orderVariants): an emitter that walks a + * record's keys emits one walk per order and branches on the value's + * KEY_ORDER_FIELD. */ +import type { IrRecordShape } from "../../ir/ir.js"; +import { KEY_ORDER_FIELD, shapeKeyOrders } from "../../ir/ir.js"; +import { mangleRecordStruct } from "../mangle.js"; +import type { BlockBuilder } from "./blocks.js"; + +/** Emits `walk(order, index)` for record `rec` of `shape`, once per order behind a + * switch on the value's order index when the shape has several. Each walk + * starts in its own block, and the code after this call continues in the + * block they all branch to. */ +export function emitPerKeyOrder( + B: BlockBuilder, + shape: IrRecordShape, + fieldBase: number, + rec: string, + walk: (order: readonly string[], index: number) => void, +): void { + const orders = shapeKeyOrders(shape); + if (orders.length === 1) { + walk(orders[0]!, 0); + return; + } + const index = shape.fields.findIndex((f) => f.name === KEY_ORDER_FIELD) + fieldBase; + const p = B.tmp(); + const n = B.tmp(); + const i = B.tmp(); + B.line( + `${p} = getelementptr inbounds %${mangleRecordStruct(shape.id)}, ptr ${rec}, i64 0, i32 ${index} ; .${KEY_ORDER_FIELD}`, + ); + B.line(`${n} = load double, ptr ${p}`); + B.line(`${i} = fptosi double ${n} to i32`); + const labels = orders.map(() => B.newLabel("ko")); + const join = B.newLabel("ko.join"); + B.terminate( + `switch i32 ${i}, label %${labels[0]} [ ${labels + .slice(1) + .map((label, k) => `i32 ${k + 1}, label %${label}`) + .join(" ")} ]`, + ); + orders.forEach((order, k) => { + B.startBlock(labels[k]!); + walk(order, k); + B.br(join); + }); + B.startBlock(join); +} diff --git a/packages/compiler/src/backend/llvm/lib-connect.ts b/packages/compiler/src/backend/llvm/lib-connect.ts index ce9c40e21..4d151fc7e 100644 --- a/packages/compiler/src/backend/llvm/lib-connect.ts +++ b/packages/compiler/src/backend/llvm/lib-connect.ts @@ -3,6 +3,7 @@ import { typeKey } from "../../ir/ir.js"; import type { LibCallExpr, LlValue, LlvmEmitterContext } from "./expr-context.js"; import { abiValue, callbackType, finishRuntimeCall, ptr } from "./lib-abi.js"; import { FN_ATTRS } from "./shapes.js"; +import { registerReplay } from "./emit-journal.js"; /** CONNECT transfers owned request/socket/response values to the listener. */ function connectAdapter(host: LlvmEmitterContext, cb: LlValue, h2: boolean): string { @@ -18,6 +19,7 @@ function connectAdapter(host: LlvmEmitterContext, cb: LlValue, h2: boolean): str if (previous !== undefined) return previous; const symbol = `sc_connect_${host.resolveThunks.size}`; host.resolveThunks.set(key, symbol); + host.journal?.allocated(symbol, "connectAdapter", [cb, h2]); const retain = h2 ? "scr_http_res_retain_v" : "scr_net_sock_retain_v"; const release = h2 ? "scr_http_res_release_v" : "scr_net_sock_release_v"; host.declare(`declare ptr @${retain}(ptr)`); @@ -37,6 +39,7 @@ function connectAdapter(host: LlvmEmitterContext, cb: LlValue, h2: boolean): str "}", "", ); + host.journal?.built(); return symbol; } @@ -70,3 +73,7 @@ export function emitConnectListener(host: LlvmEmitterContext, e: LibCallExpr): L a[2]!, ]); } + +registerReplay("connectAdapter", (host, args) => + connectAdapter(host, args[0] as LlValue, args[1] as boolean), +); diff --git a/packages/compiler/src/backend/llvm/lib-datagram.ts b/packages/compiler/src/backend/llvm/lib-datagram.ts index 184fdb99e..07de4e54e 100644 --- a/packages/compiler/src/backend/llvm/lib-datagram.ts +++ b/packages/compiler/src/backend/llvm/lib-datagram.ts @@ -11,6 +11,7 @@ import { ptr, } from "./lib-abi.js"; import { FN_ATTRS } from "./shapes.js"; +import { registerReplay } from "./emit-journal.js"; function messageAdapter(host: LlvmEmitterContext, type: IrType): string { if (type.kind !== "record") @@ -20,6 +21,7 @@ function messageAdapter(host: LlvmEmitterContext, type: IrType): string { if (previous !== undefined) return previous; const symbol = `sc_dgram_message_${host.resolveThunks.size}`; host.resolveThunks.set(key, symbol); + host.journal?.allocated(symbol, "messageAdapter", [type]); const shape = host.recordsById.get(type.shapeId); if (shape === undefined) throw new InternalCompilerError("missing datagram remote info shape"); const lines: string[] = []; @@ -48,6 +50,7 @@ function messageAdapter(host: LlvmEmitterContext, type: IrType): string { "}", "", ); + host.journal?.built(); return symbol; } @@ -67,6 +70,7 @@ function lookupAdapter(host: LlvmEmitterContext, cb: LlValue): string { if (previous !== undefined) return previous; const symbol = `sc_dns_lookup_${host.resolveThunks.size}`; host.resolveThunks.set(key, symbol); + host.journal?.allocated(symbol, "lookupAdapter", [cb]); host.declare("declare ptr @scr_error_new(i32, ptr)"); host.declare("declare ptr @scr_error_retain_v(ptr)"); host.declare("declare void @scr_error_release_v(ptr)"); @@ -93,6 +97,7 @@ function lookupAdapter(host: LlvmEmitterContext, cb: LlValue): string { "}", "", ); + host.journal?.built(); return symbol; } @@ -105,6 +110,10 @@ export function emitDatagramLibCall(host: LlvmEmitterContext, e: LibCallExpr): L "dgram.sendStr": "scr_dgram_send_str", "dgram.sendBytes": "scr_dgram_send_bytes", "dgram.sendChk": "scr_dgram_send_chk", + "dgram.unixSocket": "scr_dgram_unix_socket", + "dgram.unixConnect": "scr_dgram_unix_connect", + "dgram.unixSend": "scr_dgram_unix_send", + "dgram.unixClose": "scr_dgram_unix_close", "dgram.ref": "scr_dgram_ref", "dgram.unref": "scr_dgram_unref", "dgram.setBroadcast": "scr_dgram_set_broadcast", @@ -250,3 +259,6 @@ export function emitDatagramLibCall(host: LlvmEmitterContext, e: LibCallExpr): L throw new InternalCompilerError(`unhandled datagram call ${e.fn}`); } } + +registerReplay("messageAdapter", (host, args) => messageAdapter(host, args[0] as IrType)); +registerReplay("lookupAdapter", (host, args) => lookupAdapter(host, args[0] as LlValue)); diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index 24f2bf019..eaf8f59f6 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -297,6 +297,41 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll B.line(`${result} = load i1, ptr ${slot}`); return { name: result, type: e.type }; } + if (e.fn === "dyn.classIs") { + // Only a class constructor's function value can match. Expose the + // function tag to LLVM so a chain of constructor probes (a static + // property bag's dispatch) costs one test for every other value, and + // borrow the receiver across a literal key as the capsule probes do. + const literal = e.args[1]!.kind === "strLit"; + const value = literal ? host.emitReadReceiver(e.args[0]!) : host.emitExpr(e.args[0]!); + const key = host.emitExpr(e.args[1]!); + const slot = B.slot(); + B.entryAllocas.push(`${slot} = alloca i1`); + B.line(`store i1 false, ptr ${slot}`); + const present = B.tmp(); + const inspect = B.newLabel("ctor.inspect"); + const compare = B.newLabel("ctor.compare"); + const done = B.newLabel("ctor.done"); + B.line(`${present} = icmp ne ptr ${value.name}, null`); + B.condBr(present, inspect, done); + B.startBlock(inspect); + const kind = host.dynKind(value.name); + const fn = B.tmp(); + B.line(`${fn} = icmp eq i32 ${kind}, ${DYN_KIND.FUNC}`); + B.condBr(fn, compare, done); + B.startBlock(compare); + host.declare("declare zeroext i1 @scr_dyn_class_is_key(ptr, ptr)"); + const matches = B.tmp(); + B.line( + `${matches} = call zeroext i1 @scr_dyn_class_is_key(ptr ${value.name}, ptr ${key.name})`, + ); + B.line(`store i1 ${matches}, ptr ${slot}`); + B.br(done); + B.startBlock(done); + const result = B.tmp(); + B.line(`${result} = load i1, ptr ${slot}`); + return { name: result, type: e.type }; + } if (e.fn === "crypto.randomFillCb" || e.fn === "crypto.randomFillCbDyn") { host.usesTimers = true; const args = e.args.map((arg) => host.emitExpr(arg)); @@ -553,7 +588,11 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll // which also takes those streams. e.fn === "stream.unboxDyn" && host.usesDynBuiltinStream ? "scr_dyn_builtin_stream_unbox" - : LIB_FN_SYMS[e.fn]); + : // for await over a checked-dynamic value iterates a native + // stream behind it (scr_dyn_for_await_stream.c) + e.fn === "dyn.asyncIteratorOpen" && host.dynForAwaitStreams + ? "scr_stream_dyn_async_iterator_open" + : 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) @@ -734,7 +773,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { if (e.fn === "process.nextTick") return host.emitAsyncContextLibCall(e); if ( e.fn === "sp.pipeline" || - e.fn === "sp.pipelineStages" || + e.fn.startsWith("sp.pipelineStages") || e.fn.startsWith("sp.pipelineOpts") || e.fn.startsWith("sp.finishedOpts") ) @@ -858,6 +897,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { return host.emitGenericLibCall(e); case "weakMap": case "weakSet": + case "finalizationRegistry": case "ffi": case "sharedArrayBuffer": case "arrayBuffer": diff --git a/packages/compiler/src/backend/llvm/lib-filesystem.ts b/packages/compiler/src/backend/llvm/lib-filesystem.ts index 6d5fe652d..7d9be632d 100644 --- a/packages/compiler/src/backend/llvm/lib-filesystem.ts +++ b/packages/compiler/src/backend/llvm/lib-filesystem.ts @@ -163,7 +163,7 @@ function emitObjEntryPairs(host: LlvmEmitterContext, e: LibCallExpr): LlValue { B.line(`${row} = call ptr @${mangleRecordNew(rowT.shapeId)}()`); host.storeField(host.recordFieldPtr(row, rowT.shapeId, "0").ptr, STRING, key, false); host.storeField(host.recordFieldPtr(row, rowT.shapeId, "1").ptr, DYN, item, false); - host.arrPush(arr, "ref", row); // push takes ownership of the row + host.arrPush(arr, "ref", row, null); // push takes ownership of the row const next = B.tmp(); B.line(`${next} = add ${size} ${i}, 1`); B.line(`store ${size} ${next}, ptr ${iSlot}`); @@ -536,7 +536,7 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr): const parent = B.tmp(); B.line(`${parent} = call ptr @scr_fs_glob_parent(ptr ${snap}, ${host.sizeType} ${i}) ; +1`); B.line(`store ptr ${parent}, ptr ${host.recordFieldPtr(row, recT.shapeId, "parentPath").ptr}`); - host.arrPush(arr, "ref", row); // push takes ownership of the row + host.arrPush(arr, "ref", row, null); // push takes ownership of the row const i2 = B.tmp(); B.line(`${i2} = add ${host.sizeType} ${i}, 1`); B.line(`store ${host.sizeType} ${i2}, ptr ${iSlot}`); @@ -620,7 +620,7 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr): B.line( `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 + host.arrPush(arr, "ref", row, null); // push takes ownership of the row const i2 = B.tmp(); B.line(`${i2} = add ${host.sizeType} ${i}, 1`); B.line(`store ${host.sizeType} ${i2}, ptr ${iSlot}`); @@ -955,7 +955,7 @@ export function emitPathUrlLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll B.startBlock(lj); const rows = B.tmp(); B.line(`${rows} = load ptr, ptr ${rowsSlot}`); - host.arrPush(rows, "ref", row); // push takes ownership of the row + host.arrPush(rows, "ref", row, null); // push takes ownership of the row B.line(`call void @scr_arr_release(ptr ${rows})`); B.line(`call void @scr_str_release(ptr ${nm})`); const i2 = B.tmp(); diff --git a/packages/compiler/src/backend/llvm/lib-http2.ts b/packages/compiler/src/backend/llvm/lib-http2.ts index 791987f86..321f1d005 100644 --- a/packages/compiler/src/backend/llvm/lib-http2.ts +++ b/packages/compiler/src/backend/llvm/lib-http2.ts @@ -13,6 +13,7 @@ import { requestHandler, } from "./lib-abi.js"; import { FN_ATTRS } from "./shapes.js"; +import { registerReplay } from "./emit-journal.js"; /** The runtime owns the answer closure; its arguments arrive owned from the caller. */ function sniAnswer(host: LlvmEmitterContext, type: IrType): string { @@ -22,6 +23,7 @@ function sniAnswer(host: LlvmEmitterContext, type: IrType): string { if (previous !== undefined) return previous; const symbol = `sc_sni_answer_${host.resolveThunks.size}`; host.resolveThunks.set(key, symbol); + host.journal?.allocated(symbol, "sniAnswer", [type]); const params = ["ptr %self"], body: string[] = []; let hasError = "false", @@ -82,6 +84,7 @@ function sniAnswer(host: LlvmEmitterContext, type: IrType): string { "}", "", ); + host.journal?.built(); return symbol; } @@ -318,3 +321,5 @@ export function emitHttp2LibCall(host: LlvmEmitterContext, e: LibCallExpr): LlVa const handler = thunk === null ? [a[1]!] : callbackAdapter(host, cb, thunk, params); return finishRuntimeCall(host, e, runtime, [a[0]!, ...handler, ...a.slice(2)]); } + +registerReplay("sniAnswer", (host, args) => sniAnswer(host, args[0] as IrType)); diff --git a/packages/compiler/src/backend/llvm/lib-lookup.ts b/packages/compiler/src/backend/llvm/lib-lookup.ts index c6bc4d97b..56e94461f 100644 --- a/packages/compiler/src/backend/llvm/lib-lookup.ts +++ b/packages/compiler/src/backend/llvm/lib-lookup.ts @@ -1,9 +1,10 @@ import { InternalCompilerError } from "../../errors.js"; -import { typeKey } from "../../ir/ir.js"; +import { type IrType, typeKey } from "../../ir/ir.js"; import { mangleRecordRelease, mangleRecordStruct } from "../mangle.js"; import type { LibCallExpr, LlValue, LlvmEmitterContext } from "./expr-context.js"; import { abiValue, callbackType, finishRuntimeCall, ptr } from "./lib-abi.js"; import { FN_ATTRS } from "./shapes.js"; +import { registerReplay } from "./emit-journal.js"; export function emitLookupConnect(host: LlvmEmitterContext, e: LibCallExpr): LlValue { const args = e.args.map((arg) => host.emitExpr(arg)), @@ -12,6 +13,18 @@ export function emitLookupConnect(host: LlvmEmitterContext, e: LibCallExpr): LlV answer = lookup.params[2]; if (answer?.kind !== "func" || answer.params.length !== 2) throw new InternalCompilerError("lookup answer must have two parameters"); + const symbol = lookupAnswer(host, answer); + host.usesTimers = true; + host.toHeap(cb); + return finishRuntimeCall(host, e, "scr_net_connect_lookup", [ + ...args.map((arg) => abiValue(host, arg)), + ptr("@" + symbol), + ]); +} + +/** The thunk that hands a lookup's answer (error, addresses) to the + * runtime, one per answer signature. */ +function lookupAnswer(host: LlvmEmitterContext, answer: IrType & { kind: "func" }): string { const error = answer.params[0]!, addresses = answer.params[1]!; if (error.kind !== "union" || addresses.kind !== "array" || addresses.elem.kind !== "record") @@ -26,6 +39,7 @@ export function emitLookupConnect(host: LlvmEmitterContext, e: LibCallExpr): LlV if (symbol === undefined) { symbol = `sc_lookup_answer_${host.resolveThunks.size}`; host.resolveThunks.set(key, symbol); + host.journal?.allocated(symbol, "lookupAnswer", [answer]); for (const declaration of [ "declare ptr @scr_str_retain_v(ptr)", "declare void @scr_str_release(ptr)", @@ -84,11 +98,11 @@ export function emitLookupConnect(host: LlvmEmitterContext, e: LibCallExpr): LlV "}", "", ); + host.journal?.built(); } - host.usesTimers = true; - host.toHeap(cb); - return finishRuntimeCall(host, e, "scr_net_connect_lookup", [ - ...args.map((arg) => abiValue(host, arg)), - ptr("@" + symbol), - ]); + return symbol; } + +registerReplay("lookupAnswer", (host, args) => + lookupAnswer(host, args[0] as IrType & { kind: "func" }), +); diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 9a65d9a24..7dd4370c5 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -44,10 +44,12 @@ export const LIB_FN_SYMS: Record = { "weakSet.is": "scr_weak_set_is", "weakMap.new": "scr_weak_map_new", "weakSet.new": "scr_weak_set_new", + "finalizationRegistry.new": "scr_finalization_registry_new", + "finalizationRegistry.is": "scr_finalization_registry_is", "dyn.fromEntries": "scr_dyn_from_entries", "bytes.constructor": "scr_bytes_constructor", "bytes.construct": "scr_bytes_construct", - "bytes.instanceOf": "scr_bytes_instanceof", + "bytes.instanceOf": "scr_bytes_instanceof_at", "arrayBuffer.constructor": "scr_array_buffer_constructor", "sharedArrayBuffer.new": "scr_shared_array_buffer_new", "sharedArrayBuffer.is": "scr_shared_array_buffer_is", @@ -132,6 +134,9 @@ export const LIB_FN_SYMS: Record = { "error.setCodeDyn": "scr_error_set_code_dyn", "error.assign": "scr_error_assign_dyn", "error.hasKey": "scr_error_has_key", + "error.ownAccessor": "scr_error_own_accessor", + "error.accessorGet": "scr_error_accessor_get", + "error.accessorSet": "scr_error_accessor_set", "error.hasCode": "scr_error_has_code", "error.defineCause": "scr_error_define_cause", "error.deleteCause": "scr_error_delete_cause", @@ -579,6 +584,10 @@ export const LIB_FN_SYMS: Record = { "crypto.ecdhNew": "scr_crypto_ecdh_new", "crypto.ecdhConvertKey": "scr_crypto_ecdh_convert_key", "crypto.dhNew": "scr_crypto_dh_new", + "crypto.checkPrimeSync": "scr_crypto_check_prime_sync", + "crypto.checkPrimeCb": "scr_crypto_check_prime_async", + "crypto.generatePrimeSync": "scr_crypto_generate_prime_sync", + "crypto.generatePrimeCb": "scr_crypto_generate_prime_async", "crypto.dhGroupNew": "scr_crypto_dh_group_new", "crypto.hmacNewStr": "scr_crypto_hmac_new_str", "crypto.hmacNewBytes": "scr_crypto_hmac_new_bytes", @@ -754,6 +763,7 @@ export const LIB_FN_SYMS: Record = { "cp.spawnSync": "scr_spawn_sync", "cp.spawnSyncOpts": "scr_spawn_sync_opts", "cp.spawnSyncStdioStr": "scr_spawn_sync_stdio_str", + "cp.spawnSyncEnv": "scr_spawn_sync_env", "cp.execSync": "scr_exec_sync", "cp.execCapture": "scr_exec_capture", "spawnRes.stdout": "scr_spawn_res_stdout", @@ -966,6 +976,7 @@ 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.mathFold": "scr_dyn_math_fold", "dyn.destructureRest": "scr_dyn_destructure_rest", "dyn.iterator": "scr_dyn_iterator", "dyn.asyncIteratorOpen": "scr_dyn_async_iterator_open", @@ -1121,11 +1132,24 @@ export const LIB_FN_SYMS: Record = { // chaining forms answer the receiver +1. The throwing members // (listeners and option callbacks run synchronously inside) ride the // generic pending check; loop liveness rides USES_TIMERS_LIB_FNS. - "readable.push": "scr_stream_push", + "readable.push": "scr_stream_push_view", "readable.pushStr": "scr_stream_push_str", "readable.pushStrEnc": "scr_stream_push_str_enc", "readable.pushEncoding": "scr_stream_set_push_encoding", "stream.objectMode": "scr_stream_object_mode", + "stream.decodeStrings": "scr_stream_decode_strings", + "stream.duplexFromGen": "scr_stream_duplex_from_gen", + "stream.duplexFromDyn": "scr_stream_duplex_from_dyn", + "stream.duplexifyReadable": "scr_stream_duplexify_readable", + "stream.duplexifyWritable": "scr_stream_duplexify_writable", + "stream.duplexifyPair": "scr_stream_duplexify_pair", + "stream.duplexFromPromise": "scr_stream_duplex_from_promise", + "stream.duplexFromAsyncFn": "scr_stream_duplex_from_async_fn", + "stream.duplexFromGenSignal": "scr_stream_duplex_from_gen_signal", + "stream.duplexFromAsyncFnSignal": "scr_stream_duplex_from_async_fn_signal", + "fetch.readableFromWeb": "scr_stream_readable_from_web", + "writable.writeEncoding": "scr_stream_write_encoding", + "writable.setDefaultEncoding": "scr_stream_set_default_encoding", "stream.chunkDone": "scr_stream_chunk_done", "readable.pushNull": "scr_stream_push_null", "readable.pushDyn": "scr_stream_push_dyn", @@ -1189,6 +1213,8 @@ export const LIB_FN_SYMS: Record = { "readable.opMap": "scr_stream_op_map", "readable.opFilter": "scr_stream_op_filter", "readable.opFlatMap": "scr_stream_op_flat_map", + "readable.opFlatMapAsyncGen": "scr_stream_op_flat_map_async_gen", + "readable.opFlatMapSyncGen": "scr_stream_op_flat_map_sync_gen", "readable.opDrop": "scr_stream_op_drop", "readable.opTake": "scr_stream_op_take", "readable.opToArray": "scr_stream_op_to_array", @@ -1197,6 +1223,11 @@ export const LIB_FN_SYMS: Record = { "readable.opEvery": "scr_stream_op_every", "readable.opFind": "scr_stream_op_find", "readable.opReduce": "scr_stream_op_reduce", + "readable.opSignalList": "scr_stream_op_signal_list", + "readable.opReduceSignal": "scr_stream_op_reduce_signal", + "readable.opSignalOf": "scr_stream_op_signal_of", + "readable.opOnceOptions": "scr_stream_op_once_options", + "readable.opSignalOpts": "scr_stream_op_signal_opts", "readable.pipe": "scr_stream_pipe", "readable.pipeGeneral": "scr_stream_pipe_general", "writable.cork": "scr_stream_cork", @@ -1525,11 +1556,13 @@ export const LIB_FN_SYMS: Record = { "dyn.assignPrototype": "scr_dyn_assign_prototype", "dyn.definePrototypeProps": "scr_dyn_define_prototype_props", "dyn.bagGet": "scr_dyn_bag_get", + "dyn.bagCreateField": "scr_dyn_bag_create_field", "dyn.bagSet": "scr_dyn_bag_set", "dyn.objCreate": "scr_dyn_obj_create", "dyn.nameClassPrototype": "scr_dyn_name_class_prototype", "dyn.objCreateWithProperties": "scr_dyn_obj_create_with_properties", "dyn.getPrototype": "scr_dyn_get_prototype", + "dyn.getPrototypeForIdentity": "scr_dyn_get_prototype_for_identity", "dyn.setPrototype": "scr_dyn_set_prototype", "dyn.getOwnPropertyNames": "scr_dyn_get_own_property_names", "dyn.ownKeys": "scr_dyn_own_keys", @@ -1543,6 +1576,7 @@ export const LIB_FN_SYMS: Record = { "dyn.objValueList": "scr_dyn_obj_value_list", "dyn.objEntries": "scr_dyn_obj_entries", "dyn.structuredClone": "scr_structured_clone", + "dyn.declAdopt": "scr_dyn_decl_adopt", "dyn.cloneMissing": "scr_structured_clone_missing", "dyn.cloneTransferFail": "scr_structured_clone_transfer_fail", "dyn.cloneTransferFailWith": "scr_structured_clone_transfer_fail_with", @@ -1556,6 +1590,8 @@ export const LIB_FN_SYMS: Record = { "regex.resultProp": "scr_regex_result_prop", "array.namedKeys": "scr_arr_named_keys", "array.hasNamedKey": "scr_arr_has_named_key", + "array.hasProperties": "scr_arr_has_properties", + "array.undefinedKey": "scr_arr_undefined_key", // 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. @@ -1600,6 +1636,8 @@ export function mockTimerSym( export const USES_TIMERS_LIB_FNS = new Set([ "crypto.generateKeyPairCb", + "crypto.checkPrimeCb", + "crypto.generatePrimeCb", "process.emitWarning", // Constructor-function streams defer their events on the stream ticks. "dyn.builtinStreamClass", @@ -1700,6 +1738,8 @@ export const USES_TIMERS_LIB_FNS = new Set([ "readable.opMap", "readable.opFilter", "readable.opFlatMap", + "readable.opFlatMapAsyncGen", + "readable.opFlatMapSyncGen", "readable.opDrop", "readable.opTake", "readable.opToArray", @@ -1708,6 +1748,7 @@ export const USES_TIMERS_LIB_FNS = new Set([ "readable.opEvery", "readable.opFind", "readable.opReduce", + "readable.opReduceSignal", "readable.resume", "readable.pipe", "readable.pipeGeneral", @@ -1729,6 +1770,19 @@ export const USES_TIMERS_LIB_FNS = new Set([ "stream.setDestroy", "stream.setTransform", "stream.setFlush", + "stream.setConstruct", + "stream.duplexFromGen", + "stream.duplexFromDyn", + "stream.duplexifyReadable", + "stream.duplexifyWritable", + "stream.duplexifyPair", + "stream.duplexFromPromise", + "stream.duplexFromAsyncFn", + "stream.duplexFromGenSignal", + "stream.duplexFromAsyncFnSignal", + "fetch.readableFromWeb", + "stream.setWritev", + "stream.setWritevDefault", "process.activeResources", "stream.finished", "stream.finishedDyn", @@ -1738,8 +1792,10 @@ export const USES_TIMERS_LIB_FNS = new Set([ "sp.finished", "sp.pipeline", "sp.pipelineStages", + "sp.pipelineStagesOpts", "stream.pipelineStages", "stream.pipelineStagesDyn", + "stream.composeStages", "sp.pipelineOpts", "sp.pipelineOptsSignal", "sp.finishedOpts", diff --git a/packages/compiler/src/backend/llvm/lib-stream.ts b/packages/compiler/src/backend/llvm/lib-stream.ts index 6d6c6f99c..5fd4af1a9 100644 --- a/packages/compiler/src/backend/llvm/lib-stream.ts +++ b/packages/compiler/src/backend/llvm/lib-stream.ts @@ -187,18 +187,24 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV e.fn === "stream.setFinal" || e.fn === "stream.setDestroy" || e.fn === "stream.setTransform" || - e.fn === "stream.setFlush" + e.fn === "stream.setFlush" || + e.fn === "stream.setConstruct" || + e.fn === "stream.setWritev" || + e.fn === "stream.setWritevDefault" ) { // The underscore-method assignment surface: the runtime slot swaps // its closure (+1 moves in) and invoke thunk — the next // _read/_write/... dispatch uses it, Node's timing. - const kindOf: Record = { + const kindOf: Record = { "stream.setRead": "r", "stream.setWrite": "w", "stream.setFinal": "f", "stream.setDestroy": "d", "stream.setTransform": "t", "stream.setFlush": "l", + "stream.setConstruct": "c", + "stream.setWritev": "v", + "stream.setWritevDefault": "v", }; const symOf: Record = { "stream.setRead": "scr_stream_set_read", @@ -207,6 +213,9 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV "stream.setDestroy": "scr_stream_set_destroy", "stream.setTransform": "scr_stream_set_transform", "stream.setFlush": "scr_stream_set_flush", + "stream.setConstruct": "scr_stream_set_construct", + "stream.setWritev": "scr_stream_set_writev", + "stream.setWritevDefault": "scr_stream_set_writev_default", }; const recv = host.emitExpr(e.args[0]!); const cb = host.emitExpr(e.args[1]!); @@ -692,12 +701,15 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV } if ( e.fn === "sp.pipelineStages" || + e.fn === "sp.pipelineStagesOpts" || e.fn === "stream.pipelineStages" || - e.fn === "stream.pipelineStagesDyn" + e.fn === "stream.pipelineStagesDyn" || + e.fn === "stream.composeStages" ) { // (n, k, positions, flags, mask, s1..sn, stage1..stagek, helpers..., - // box?, (signal, abort)?[, cb]): the streams, the stage closures and - // the helpers ride stack arrays; absent groups pass null. + // box?, (signal, abort)?[, cb | end, signal?]): the streams, the + // stage closures and the helpers ride stack arrays; absent groups + // pass null. const [nArg, kArg, positionsArg, flagsArg, maskArg] = e.args; if ( nArg?.kind !== "numLit" || @@ -710,7 +722,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV const n = nArg.value; const k = kArg.value; const flags = flagsArg.value; - const helperCount = 3 * pipelinePumpedRuns(n + k, positionsArg.value); + const helperCount = 3 * pipelinePumpedRuns(n + k, positionsArg.value, flags); const args = e.args.map((a) => host.emitExpr(a)); const packed = (from: number, count: number): string => { if (count === 0) return "null"; @@ -740,6 +752,21 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV if (e.fn === "sp.pipelineStages") { host.declare(`declare ptr @scr_sp_pipeline_stages(${params})`); B.line(`${t} = call ptr @scr_sp_pipeline_stages(${head})`); + } else if (e.fn === "sp.pipelineStagesOpts") { + const end = args[at++]!.name; + const outer = take((flags & 64) !== 0); + let add = "null"; + if (outer !== "null") { + host.declare(`declare ptr @scr_events_add_abort_listener(ptr, ptr)`); + add = "@scr_events_add_abort_listener"; + } + host.declare(`declare ptr @scr_sp_pipeline_stages_opts(${params}, i1, ptr, ptr)`); + B.line( + `${t} = call ptr @scr_sp_pipeline_stages_opts(${head}, i1 ${end}, ptr ${outer}, ptr ${add})`, + ); + } else if (e.fn === "stream.composeStages") { + host.declare(`declare ptr @scr_stream_compose_stages(${params})`); + B.line(`${t} = call ptr @scr_stream_compose_stages(${head})`); } else if (e.fn === "stream.pipelineStagesDyn") { host.declare(`declare ptr @scr_stream_pipeline_stages_dyn(${params}, ptr)`); B.line( diff --git a/packages/compiler/src/backend/llvm/local-array-reads.test.ts b/packages/compiler/src/backend/llvm/local-array-reads.test.ts index b09029865..7af495d42 100644 --- a/packages/compiler/src/backend/llvm/local-array-reads.test.ts +++ b/packages/compiler/src/backend/llvm/local-array-reads.test.ts @@ -940,3 +940,17 @@ test("a dense read tests the hole-free prefix before the capacity", () => { expect(ir.lastIndexOf("local.array.range", firstCap)).toBeGreaterThan(firstFilled); } }); + +test("an optional read at a proven integer index skips the double round trip", () => { + const module = fixture(); + const call = module.functions[2]!.body[0]!; + if (call.kind !== "varDecl" || call.init?.kind !== "call") throw new Error("missing call"); + call.init.args[1] = { kind: "bin", op: "|", left: ref("i", F64), right: num(0), type: F64, loc }; + expect(candidates(module).get("value")?.borrow).toBe(true); + expect(validateModule(module)).toEqual([]); + // The int32 index is the dense offset on 64-bit targets; a 32-bit target + // converts the double, as a borrowed read does. + expect(workBody(module, 64)).not.toContain("local.array.exact"); + expect(workBody(module, 64)).toContain("local.array.value"); + expect(workBody(module, 32)).toContain("local.array.exact"); +}); diff --git a/packages/compiler/src/backend/llvm/local-array-reads.ts b/packages/compiler/src/backend/llvm/local-array-reads.ts index cdd00c4e7..1c540295a 100644 --- a/packages/compiler/src/backend/llvm/local-array-reads.ts +++ b/packages/compiler/src/backend/llvm/local-array-reads.ts @@ -13,8 +13,15 @@ import { analyzeCallLifetimes, type CallLifetimes } from "./call-lifetimes.js"; import type { LlValue, LlvmEmitterContext } from "./expr-context.js"; import { ReferenceEffects } from "./reference-effects.js"; import { SCR_ARR_FIELD, loadArrCount, scrArrStateAt } from "./common.js"; -import { exactInteger, widenInteger } from "./integer-values.js"; +import { indexInteger } from "./integer-values.js"; import { READ_FALLBACK } from "./dense-array-access.js"; +import { compactRefLoadLines } from "./classes.js"; + +/** The dense slot load of a reference read from an array of type + * `arrayType`: a pointer, or a compact class reference (SCR_ELEM_CREF). */ +function referenceLoad(host: LlvmEmitterContext, arrayType: IrType): "ptr" | "cref" { + return arrayType.kind === "array" && host.compactArrayElem(arrayType.elem) ? "cref" : "ptr"; +} export interface LocalArrayRead { type: IrType; @@ -469,7 +476,8 @@ function emitDenseReferenceArrayRead( no: string, slow: string, join: string, - loadType: "ptr" | "i64" | "i8" = "ptr", + /** `i32`: an int32 array's slot, presented as the int32. */ + loadType: "ptr" | "cref" | "i64" | "i8" | "i32" = "ptr", ): void { const B = host.B; const header = (field: number): string => { @@ -555,7 +563,14 @@ function emitDenseReferenceArrayRead( const statePtr = B.tmp(), denseState = B.tmp(), densePresent = B.tmp(); - scrArrStateAt(host, statePtr, data, cap, loadType === "i8" ? 1 : 8, offset); + scrArrStateAt( + host, + statePtr, + data, + cap, + loadType === "i8" ? 1 : loadType === "cref" || loadType === "i32" ? 4 : 8, + offset, + ); host.markMemoryPointer(statePtr, "array:present"); B.line(`${denseState} = load i8, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`); B.line(`${densePresent} = icmp eq i8 ${denseState}, 1`); @@ -564,13 +579,21 @@ function emitDenseReferenceArrayRead( B.startBlock(valueLabel); const valuePtr = B.tmp(), raw = B.tmp(); - // Boolean arrays store one byte per element; every other kind an 8-byte slot. - const slotType = loadType === "i8" ? "i8" : "i64"; + // Boolean arrays store one byte per element, compact references and int32 + // arrays four, every other kind an 8-byte slot. + const slotType = + loadType === "i8" ? "i8" : loadType === "cref" || loadType === "i32" ? "i32" : "i64"; B.line( `${valuePtr} = getelementptr inbounds ${slotType}, ptr ${data}, ${host.sizeType} ${offset}`, ); host.markMemoryPointer(valuePtr, "array:elements"); - B.line(`${raw} = load ${loadType}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); + if (loadType === "cref") { + const ref = B.tmp(); + B.line(`${ref} = load i32, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); + for (const line of compactRefLoadLines(ref, raw, `${raw}.r`)) B.line(line.trimStart()); + } else { + B.line(`${raw} = load ${loadType}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); + } present(raw); B.br(join); } @@ -598,6 +621,18 @@ export function emitBorrowedArrayRead( ); host.markMemoryPointer(dataPtr, "array:header"); B.line(`${data} = load ptr, ptr ${dataPtr}${host.fieldAliasAttachment(dataPtr)}`); + if (referenceLoad(host, read.arr.type) === "cref") { + // Compact references: 4-byte slots holding the address over 8. + const ref = B.tmp(); + B.line( + `${valuePtr} = getelementptr inbounds i32, ptr ${data}, ${host.sizeType} ${loopIndex}`, + ); + host.markMemoryPointer(valuePtr, "array:elements"); + B.line(`${ref} = load i32, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); + for (const line of compactRefLoadLines(ref, value, `${value}.r`)) B.line(line.trimStart()); + B.line(`store ptr ${value}, ptr ${slot}`); + return; + } B.line(`${valuePtr} = getelementptr inbounds i64, ptr ${data}, ${host.sizeType} ${loopIndex}`); host.markMemoryPointer(valuePtr, "array:elements"); B.line(`${value} = load ptr, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); @@ -605,11 +640,11 @@ export function emitBorrowedArrayRead( return; } const index = host.emitExpr(read.index); - // A proven integer index (an int32 local, say) needs no double - // round-trip; a negative one compares above the capacity as unsigned. - const exact = - loopIndex === null && host.sizeType === "i64" ? exactInteger(host, index, read.index) : null; - const integerIndex = loopIndex ?? (exact ? widenInteger(host, exact) : null); + // A proven integer index (an int32 local, say), or a wide view exact at + // every index below the capacity, needs no double round-trip; a negative + // one compares above the capacity as unsigned. + const integerIndex = + loopIndex ?? (host.sizeType === "i64" ? indexInteger(host, index, read.index) : null); const slow = B.newLabel("borrow.array.slow"), join = B.newLabel("borrow.array.join"); emitDenseReferenceArrayRead( @@ -622,6 +657,7 @@ export function emitBorrowedArrayRead( slow, slow, join, + referenceLoad(host, read.arr.type), ); B.startBlock(slow); host.declare("declare ptr @scr_arr_borrow_ref(ptr, double)"); @@ -644,8 +680,12 @@ export function emitLocalArrayRead( ): { slot: string; type: IrType } | null { const B = host.B; const array = host.emitStableReceiver(read.array, [read.index]); - const integerIndex = inline ? host.emitIntegerLoopIndex(read.index) : null; + const loopIndex = inline ? host.emitIntegerLoopIndex(read.index) : null; const index = host.emitExpr(read.index); + // As in emitBorrowedArrayRead: an exact integer or wide view needs no + // round trip, and only the loop's own counter is an induction variable. + const integerIndex = + loopIndex ?? (inline && host.sizeType === "i64" ? indexInteger(host, index, read.index) : null); const nullable = read.type.kind === "union" ? host.nullableUnions.get(read.type.unionId) : null; if (nullable) { // A nullable union is the element pointer itself: borrowed from an @@ -665,12 +705,12 @@ export function emitLocalArrayRead( array, index, integerIndex, - integerIndex !== null, + loopIndex !== null, store, no, slow, join, - "ptr", + referenceLoad(host, read.array.type), ); else B.br(slow); B.startBlock(slow); @@ -702,8 +742,15 @@ export function emitLocalArrayRead( const no = B.newLabel("local.array.missing"), join = B.newLabel("local.array.join"); const scalar = isScalarElement(read.element); + // An int32 array's arms join on the int32 element, converted to the + // payload's double once at the join, so a consumer converting the payload + // back to an integer folds to the element. + const int32 = scalar && read.element.kind === "f64" && host.int32Array(read.array); + const element = int32 ? B.slot() : null; + if (element) B.entryAllocas.push(`${element} = alloca i32`); const storeValue = (value: string): void => { - if (scalar) { + if (element) B.line(`store i32 ${value}, ptr ${element}`); + else if (scalar) { // Slot bits for f64; a canonical 0/1 for bool, like scr_union_new_bool. let bits = value; if (read.element.kind === "bool") { @@ -720,18 +767,54 @@ export function emitLocalArrayRead( } B.line(`store i32 ${read.presentTag}, ptr ${tag}`); }; - if (inline) + if (inline && scalar && host.checkedInt32Array(read.array)) + // An int32 array with checked stores: its int32 arm stores the element's + // double bits, as the double arm does. + host.emitByElementKind(array.name, null, (kind) => { + const present = + kind === "i32" + ? (value: string): void => { + const number = B.tmp(), + bits = B.tmp(); + B.line(`${number} = sitofp i32 ${value} to double`); + B.line(`${bits} = bitcast double ${number} to i64`); + storeValue(bits); + } + : storeValue; + const loadType = kind === "i32" ? "i32" : "i64"; + const induction = loopIndex !== null; + emitDenseReferenceArrayRead( + host, + array, + index, + integerIndex, + induction, + present, + no, + slow, + join, + loadType, + ); + return null; + }); + else if (inline) emitDenseReferenceArrayRead( host, array, index, integerIndex, - integerIndex !== null, + loopIndex !== null, storeValue, no, slow, join, - !scalar ? "ptr" : read.element.kind === "bool" ? "i8" : "i64", + !scalar + ? referenceLoad(host, read.array.type) + : read.element.kind === "bool" + ? "i8" + : int32 + ? "i32" + : "i64", ); else B.br(slow); B.startBlock(slow); @@ -758,7 +841,8 @@ export function emitLocalArrayRead( B.line( `${value} = call double @scr_arr_get_f64(ptr ${array.name}, double ${index.name})${READ_FALLBACK}`, ); - B.line(`${bits} = bitcast double ${value} to i64`); + if (element) B.line(`${bits} = fptosi double ${value} to i32`); + else B.line(`${bits} = bitcast double ${value} to i64`); } else { host.declare(`declare zeroext i1 @scr_arr_get_bool(ptr, double)`); B.line( @@ -766,7 +850,8 @@ export function emitLocalArrayRead( ); B.line(`${bits} = zext i1 ${value} to i64`); } - B.line(`store i64 ${bits}, ptr ${payload}`); + if (element) B.line(`store i32 ${bits}, ptr ${element}`); + else B.line(`store i64 ${bits}, ptr ${payload}`); B.line(`store i32 ${read.presentTag}, ptr ${tag}`); } else { const value = B.tmp(), @@ -780,10 +865,20 @@ export function emitLocalArrayRead( } B.br(join); B.startBlock(no); - B.line(`store i64 0, ptr ${payload}`); + if (element) B.line(`store i32 0, ptr ${element}`); + else B.line(`store i64 0, ptr ${payload}`); B.line(`store i32 ${read.missingTag}, ptr ${tag}`); B.br(join); B.startBlock(join); + if (element) { + const value = B.tmp(), + number = B.tmp(), + bits = B.tmp(); + B.line(`${value} = load i32, ptr ${element}`); + B.line(`${number} = sitofp i32 ${value} to double`); + B.line(`${bits} = bitcast double ${number} to i64`); + B.line(`store i64 ${bits}, ptr ${payload}`); + } B.line(`store ptr ${box}, ptr ${localSlot}`); return read.borrow || scalar ? null : { slot: payload, type: read.element }; } diff --git a/packages/compiler/src/backend/llvm/loop-state.ts b/packages/compiler/src/backend/llvm/loop-state.ts new file mode 100644 index 000000000..ce9189a45 --- /dev/null +++ b/packages/compiler/src/backend/llvm/loop-state.ts @@ -0,0 +1,76 @@ +/* The loop optimizations' per-function state: counted loops, dense array + * loops, byte windows and typed-array loop versioning, and the unaliased + * local arrays they rely on. */ +import { findUnaliasedLocalArrays } from "../../ir/array-length-stability.js"; +import type { IrFunction, IrStmt } from "../../ir/ir.js"; +import { findInitializedByteLoopBindings } from "./byte-windows.js"; +import type { DenseArrayLoop } from "./dense-array-loops.js"; +import type { LlEmitter } from "./emitter.js"; +import { defineModuleState } from "./emitter-state.js"; + +/** Sets the loop state for `fn` at the start of its emission; `numericFn` + * is `fn` with its initializer bindings (emitFunction). */ +export function beginFunctionLoops( + emitter: LlEmitter, + fn: IrFunction, + numericFn: IrFunction, +): void { + const loops = LOOPS.of(emitter); + loops.unaliasedArrays = findUnaliasedLocalArrays(fn); + loops.countedLoopsEnabled = emitter.debug === null && !fn.async && !fn.generator; + loops.byteWindowsEnabled = loops.countedLoopsEnabled; + loops.bytesLoopVersionsEnabled = loops.countedLoopsEnabled; + loops.bytesLoopEntries = null; + loops.bytesLoopFunction = fn; + loops.denseLoopsEnabled = loops.countedLoopsEnabled; + loops.denseLoop = null; + loops.denseLoopEntries = + loops.denseLoopsEnabled && numericFn.locals.some((l) => l.type.kind === "array") + ? findInitializedByteLoopBindings(fn) + : new Map(); + loops.byteWindowEntries = + loops.byteWindowsEnabled && numericFn.locals.some((l) => l.type.kind === "bytes") + ? findInitializedByteLoopBindings(fn) + : new Map(); +} + +/** The state LOOPS holds on each emitter. */ +export interface LoopState { + /** Local arrays of the current function that no other binding can hold + * (ir/array-length-stability.ts). */ + unaliasedArrays: ReadonlySet; + /** Dense array loops (dense-array-loops.ts) run in this function. */ + denseLoopsEnabled: boolean; + denseLoopEntries: ReturnType; + /** The fast copy of a dense array loop being emitted. */ + denseLoop: DenseArrayLoop | null; + countedLoopsEnabled: boolean; + byteWindowsEnabled: boolean; + byteWindowEntries: ReturnType; + /** Typed-array loop versioning (bytes-loop-versions.ts) for the current + * function: off in debug and suspending functions and inside a copy. */ + bytesLoopVersionsEnabled: boolean; + /** Loops being versioned around the statement being emitted: a loop is + * never versioned inside its own copies, and versions nest boundedly. */ + readonly bytesLoopVersioning: Set; + bytesLoopEntries: ReturnType | null; + bytesLoopFunction: IrFunction | null; + /** Counted loops whose integer-form safety test an enclosing version + * guard already passed. */ + readonly provenCountedGuards: Set; +} + +export const LOOPS = defineModuleState(() => ({ + unaliasedArrays: new Set(), + denseLoopsEnabled: false, + denseLoopEntries: new Map(), + denseLoop: null, + countedLoopsEnabled: false, + byteWindowsEnabled: true, + byteWindowEntries: new Map(), + bytesLoopVersionsEnabled: false, + bytesLoopVersioning: new Set(), + bytesLoopEntries: null, + bytesLoopFunction: null, + provenCountedGuards: new Set(), +})); diff --git a/packages/compiler/src/backend/llvm/map-number-lookup.test.ts b/packages/compiler/src/backend/llvm/map-number-lookup.test.ts index 5dc3589e4..989b8b3b0 100644 --- a/packages/compiler/src/backend/llvm/map-number-lookup.test.ts +++ b/packages/compiler/src/backend/llvm/map-number-lookup.test.ts @@ -181,3 +181,63 @@ test("hashed number-key lookups probe inline and call the runtime only for wide ); } }); + +/** work(map): map.size, and keep(map): map returned as an owned value. */ +function sizeFixture(): IrModule { + const map: IrType = { kind: "map", key: F64, value: F64 }; + const params = [{ localId: "map", name: "map", type: map }]; + const locals = params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: true })); + const size: IrExpr = { + kind: "mapIntrinsic", + method: "size", + receiver: ref("map", map), + args: [], + type: F64, + loc, + }; + return { + irVersion: 17, + sourceFile: loc.file, + entry: "main", + records: [], + unions: [], + functions: [ + { name: "main", params: [], locals: [], returnType: VOID, body: [], loc }, + { + name: "work", + params, + returnType: F64, + loc, + locals, + body: [{ kind: "return", value: size, loc }], + }, + { + name: "keep", + params, + returnType: map, + loc, + locals, + body: [{ kind: "return", value: ref("map", map), loc }], + }, + ], + }; +} + +test("Map.size and Map retains need no runtime call in release builds", () => { + const mod = sizeFixture(); + expect(validateModule(mod)).toEqual([]); + for (const bits of [32, 64] as const) { + const module = emitLlvmModule(mod, { pointerBits: bits, inlineRc: "release" }); + const fn = (name: string) => + new RegExp(`^define internal [^\\n]*@sc_(?:b)?f_${name}(?:\\.i32)?\\([^]*?^}`, "m").exec( + module, + )![0]; + // Field 9 is the live entry count (scr_map.h). + expect(module).toMatch(/^define internal double @sc_map_size\(ptr %m\)/m); + expect(module).toContain(`getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 9`); + expect(fn("work")).toContain("call double @sc_map_size("); + expect(module).not.toContain("call double @scr_map_size("); + expect(fn("keep")).toContain("@sc_rc_retain_map("); + expect(module).not.toContain("call ptr @scr_map_retain_v("); + } +}); diff --git a/packages/compiler/src/backend/llvm/map-number-lookup.ts b/packages/compiler/src/backend/llvm/map-number-lookup.ts index 1244bd9c3..6d219b50a 100644 --- a/packages/compiler/src/backend/llvm/map-number-lookup.ts +++ b/packages/compiler/src/backend/llvm/map-number-lookup.ts @@ -15,6 +15,12 @@ * retain inline rather than through the runtime's indirect adapter call, * and scalar reads and has() share the one lookup. * + * A string-key map whose values are compact class instances + * (SCR_MAP_VAL_CREF, LlEmitter.compactArrayElem of the value type) keeps + * 16-byte { key, hash, val } entries with the value as an address over 8: + * its reads go through sc_map_entry_str_c and sc_map_iter_entry_c, and + * decode the 4-byte value. + * * The iteration steps behind for-of, forEach and the other entry walks read * the entries array through the internal sc_map_iter_entry and * sc_map_iter_count helpers: a live entry's key or value loads in place, @@ -24,6 +30,7 @@ */ import type { IrType } from "../../ir/ir.js"; import type { LlvmEmitterContext } from "./expr-context.js"; +import { mapKeyAccess } from "./shapes.js"; export const NUMBER_MAP_ENTRY_DECL = "declare ptr @scr_map_entry_f64(ptr, double)"; @@ -47,18 +54,59 @@ export function emitNumberMapSlot(host: LlvmEmitterContext, entry: string, type: } export const STRING_MAP_ENTRY_DECL = "declare ptr @scr_map_entry_str(ptr, ptr)"; +/** Declared by reads of a compact-value map; its presence makes the emitter + * define sc_map_entry_str_c and sc_map_iter_entry_c (emit-module.ts). */ +export const STRING_MAP_ENTRY_C_DECL = "declare ptr @scr_map_entry_str_c(ptr, ptr)"; -/** The entry for a string `key`, or null when absent. Valid until the next mutation. */ -export function emitStringMapEntry(host: LlvmEmitterContext, map: string, key: string): string { - host.declare(STRING_MAP_ENTRY_DECL); +/** Whether a Map with key type `key` and value type `value` keeps compact + * values (SCR_MAP_VAL_CREF): string keys and a value type whose instances + * are compact (LlEmitter.compactArrayElem). */ +export function compactMapValues(host: LlvmEmitterContext, key: IrType, value: IrType): boolean { + // SCR_MAP_KEY_STR (shapes.ts mapKeyKindNum 1): string access, not a union. + return key.kind !== "union" && mapKeyAccess(key) === "str" && host.compactArrayElem(value); +} + +/** The entry for a string `key`, or null when absent. Valid until the next + * mutation. `compact`: the map keeps compact values (a %ScrMapCEntry). */ +export function emitStringMapEntry( + host: LlvmEmitterContext, + map: string, + key: string, + compact = false, +): string { + host.declare(compact ? STRING_MAP_ENTRY_C_DECL : STRING_MAP_ENTRY_DECL); const entry = host.B.tmp(); - host.B.line(`${entry} = call ptr @sc_map_entry_str(ptr ${map}, ptr ${key})`); + host.B.line( + `${entry} = call ptr @${compact ? "sc_map_entry_str_c" : "sc_map_entry_str"}(ptr ${map}, ptr ${key})`, + ); return entry; } +/** A found compact-value entry's value as a pointer: its 4-byte form, an + * address over 8 (scr_map.h ScrMapCEntry). */ +export function emitCompactMapSlot(host: LlvmEmitterContext, entry: string): string { + const B = host.B; + const slot = B.tmp(), + ref = B.tmp(), + wide = B.tmp(), + addr = B.tmp(), + value = B.tmp(); + B.line(`${slot} = getelementptr inbounds %ScrMapCEntry, ptr ${entry}, i32 0, i32 2`); + B.line(`${ref} = load i32, ptr ${slot}`); + B.line(`${wide} = zext i32 ${ref} to i64`); + B.line(`${addr} = shl nuw nsw i64 ${wide}, 3`); + B.line(`${value} = inttoptr i64 ${addr} to ptr`); + return value; +} + /** Map.has / Set.has under a string key. */ -export function emitStringMapHas(host: LlvmEmitterContext, map: string, key: string): string { - const entry = emitStringMapEntry(host, map, key); +export function emitStringMapHas( + host: LlvmEmitterContext, + map: string, + key: string, + compact = false, +): string { + const entry = emitStringMapEntry(host, map, key, compact); const found = host.B.tmp(); host.B.line(`${found} = icmp ne ptr ${entry}, null`); return found; @@ -92,12 +140,18 @@ export function emitStringMapGetRef( map: string, key: string, value: IrType, + compact = false, ): string { - return emitMapEntryGetRef(host, emitStringMapEntry(host, map, key), value); + return emitMapEntryGetRef(host, emitStringMapEntry(host, map, key, compact), value, compact); } /** The +1 value of a found entry, or null for a missing (null) entry. */ -function emitMapEntryGetRef(host: LlvmEmitterContext, entry: string, value: IrType): string { +function emitMapEntryGetRef( + host: LlvmEmitterContext, + entry: string, + value: IrType, + compact = false, +): string { const B = host.B; const found = B.tmp(); B.line(`${found} = icmp ne ptr ${entry}, null`); @@ -106,7 +160,10 @@ function emitMapEntryGetRef(host: LlvmEmitterContext, entry: string, value: IrTy join = B.newLabel("mapn.join"); B.condBr(found, hit, miss); B.startBlock(hit); - const retained = host.retainFrame(emitNumberMapSlot(host, entry, "ptr"), value); + const retained = host.retainFrame( + compact ? emitCompactMapSlot(host, entry) : emitNumberMapSlot(host, entry, "ptr"), + value, + ); B.br(join); B.startBlock(miss); B.br(join); @@ -199,20 +256,25 @@ export const SCR_MAP_NUM_DEAD = "9218868437227405313"; /** The entry layout a Map or Set of key type `key` stores: * - "num": number keys, whose entries are { key, val } (scr_map.h ScrMapNumEntry); + * - "centry": string keys with compact values (compactMapValues), whose + * entries are { key, hash, val } (ScrMapCEntry); * - "entry": every other typed key, whose entries are { key, val, hash }; * - "either": checked (`unknown`) keys. A `Map` or * `Set` cast from a checked value keeps the collection's own - * storage (scr_dyn_native_collection_check), which can have number keys. */ -export type MapEntryLayout = "num" | "entry" | "either"; + * storage (scr_dyn_native_collection_check), which can have number keys + * or compact values. */ +export type MapEntryLayout = "num" | "centry" | "entry" | "either"; -export function mapEntryLayout(key: IrType): MapEntryLayout { +export function mapEntryLayout(key: IrType, compact = false): MapEntryLayout { + if (compact) return "centry"; return key.kind === "f64" ? "num" : key.kind === "dyn" ? "either" : "entry"; } /** The entry at an iteration index, or null for a tombstone or an index at * or past the entry count (the scr_map_iter_live test). With the "either" * layout, the map's key kind (%ScrMapIx field 1, SCR_MAP_KEY_F64 = 0) picks - * the step at run time. */ + * the step at run time; in a program with compact class instances, so does + * its value kind (field 2, SCR_MAP_VAL_CREF = 3). */ export function emitMapIterEntry( host: LlvmEmitterContext, map: string, @@ -220,14 +282,20 @@ export function emitMapIterEntry( layout: MapEntryLayout, ): string { const B = host.B; - const step = (num: boolean): string => { - host.declare(num ? MAP_ITER_NUM_DECL : MAP_ITER_DECL); + const step = (kind: "num" | "centry" | "entry"): string => { + host.declare(kind === "num" ? MAP_ITER_NUM_DECL : MAP_ITER_DECL); + if (kind === "centry") host.declare(STRING_MAP_ENTRY_C_DECL); const entry = B.tmp(); - const helper = num ? "sc_map_iter_entry_num" : "sc_map_iter_entry"; + const helper = + kind === "num" + ? "sc_map_iter_entry_num" + : kind === "centry" + ? "sc_map_iter_entry_c" + : "sc_map_iter_entry"; B.line(`${entry} = call ptr @${helper}(ptr ${map}, double ${index})`); return entry; }; - if (layout !== "either") return step(layout === "num"); + if (layout !== "either") return step(layout); host.declare(MAP_ITER_DECL); const kindPtr = B.tmp(), kind = B.tmp(), @@ -240,16 +308,31 @@ export function emitMapIterEntry( join = B.newLabel("mapit.layout"); B.condBr(isNum, numLabel, entryLabel); B.startBlock(numLabel); - const numEntry = step(true); + const numEntry = step("num"); const numEnd = B.currentLabel(); B.br(join); B.startBlock(entryLabel); - const otherEntry = step(false); - const entryEnd = B.currentLabel(); + const arms: { value: string; label: string }[] = [{ value: numEntry, label: numEnd }]; + if (host.shapeHost.compactRegion?.() === true) { + const valPtr = B.tmp(), + valKind = B.tmp(), + isCompact = B.tmp(); + B.line(`${valPtr} = getelementptr inbounds %ScrMapIx, ptr ${map}, i32 0, i32 2`); + B.line(`${valKind} = load i8, ptr ${valPtr}`); + B.line(`${isCompact} = icmp eq i8 ${valKind}, 3`); + const compactLabel = B.newLabel("mapit.centry"), + wideLabel = B.newLabel("mapit.wide"); + B.condBr(isCompact, compactLabel, wideLabel); + B.startBlock(compactLabel); + arms.push({ value: step("centry"), label: B.currentLabel() }); + B.br(join); + B.startBlock(wideLabel); + } + arms.push({ value: step("entry"), label: B.currentLabel() }); B.br(join); B.startBlock(join); const entry = B.tmp(); - B.line(`${entry} = phi ptr [ ${numEntry}, %${numEnd} ], [ ${otherEntry}, %${entryEnd} ]`); + B.line(`${entry} = phi ptr ${arms.map((a) => `[ ${a.value}, %${a.label} ]`).join(", ")}`); return entry; } @@ -270,6 +353,16 @@ export function emitMapIterCount(host: LlvmEmitterContext, map: string): string return count; } +export const MAP_SIZE_DECL = "declare double @scr_map_size(ptr)"; + +/** Map.size / Set.size: the live entry count, read inline (sc_map_size). */ +export function emitMapSize(host: LlvmEmitterContext, map: string): string { + host.declare(MAP_SIZE_DECL); + const size = host.B.tmp(); + host.B.line(`${size} = call double @sc_map_size(ptr ${map})`); + return size; +} + /** The key (field 0) or value (field 1) of the live entry at an iteration * index, read inline: a number as double, a boolean as i1, and a string or * reference retained with `type`'s retain (+1), the adapter the collection @@ -294,18 +387,24 @@ export function emitMapIterSlot( join = B.newLabel("mapit.join"); B.condBr(found, hit, miss); B.startBlock(hit); - const slot = B.tmp(); - B.line(`${slot} = getelementptr inbounds %ScrMapEntry, ptr ${entry}, i32 0, i32 ${field}`); const llTy = access === "f64" ? "double" : access === "bool" ? "i1" : "ptr"; - let loaded = B.tmp(); - if (access === "bool") { - const bits = loaded; - B.line(`${bits} = load i64, ptr ${slot}`); - loaded = B.tmp(); - B.line(`${loaded} = icmp ne i64 ${bits}, 0`); + let loaded: string; + if (layout === "centry" && field === 1) { + // A compact value; its type's retain emits no counting. + loaded = host.retainFrame(emitCompactMapSlot(host, entry), type); } else { - B.line(`${loaded} = load ${llTy}, ptr ${slot}`); - if (access === "ref") loaded = host.retainFrame(loaded, type); + const slot = B.tmp(); + B.line(`${slot} = getelementptr inbounds %ScrMapEntry, ptr ${entry}, i32 0, i32 ${field}`); + loaded = B.tmp(); + if (access === "bool") { + const bits = loaded; + B.line(`${bits} = load i64, ptr ${slot}`); + loaded = B.tmp(); + B.line(`${loaded} = icmp ne i64 ${bits}, 0`); + } else { + B.line(`${loaded} = load ${llTy}, ptr ${slot}`); + if (access === "ref") loaded = host.retainFrame(loaded, type); + } } const hitEnd = B.currentLabel(); B.br(join); diff --git a/packages/compiler/src/backend/llvm/map-read-lifetimes.ts b/packages/compiler/src/backend/llvm/map-read-lifetimes.ts index 005f88f13..6b58d314a 100644 --- a/packages/compiler/src/backend/llvm/map-read-lifetimes.ts +++ b/packages/compiler/src/backend/llvm/map-read-lifetimes.ts @@ -15,6 +15,7 @@ import { emitNumberMapGetScalar, emitStringMapGetScalar, emitStringMapGetRef, + compactMapValues, } from "./map-number-lookup.js"; import { mapKeyAccess, mapKeyParamType } from "./shapes.js"; import { rememberMapEntry } from "./map-entry-fusion.js"; @@ -323,7 +324,10 @@ export function emitStackMapRead(host: LlvmEmitterContext, read: LocalMapRead): } else if (isRefCounted(read.value)) { const stringKey = entryStringKey(key); let raw: string; - if (stringKey !== null) raw = emitStringMapGetRef(host, receiver.name, stringKey, read.value); + const mapType = read.receiver.type; + const compact = mapType.kind === "map" && compactMapValues(host, mapType.key, mapType.value); + if (stringKey !== null) + raw = emitStringMapGetRef(host, receiver.name, stringKey, read.value, compact); else { raw = B.tmp(); host.declare(`declare ptr @scr_map_get_${access}_ref(ptr, ${keyType})`); diff --git a/packages/compiler/src/backend/llvm/nullable-unions.ts b/packages/compiler/src/backend/llvm/nullable-unions.ts index 8729e9649..2c355f551 100644 --- a/packages/compiler/src/backend/llvm/nullable-unions.ts +++ b/packages/compiler/src/backend/llvm/nullable-unions.ts @@ -279,6 +279,8 @@ function shapeUnion( * tag and payload inside the C map. */ function boxedUnionIds(mod: IrModule): Set { const boxed = new Set(); + // Only the module's union definitions consult the result. + if (!mod.unions?.length) return boxed; const mark = (t: IrType): void => { if (t.kind === "union") boxed.add(t.unionId); }; diff --git a/packages/compiler/src/backend/llvm/publish.ts b/packages/compiler/src/backend/llvm/publish.ts index 2f45c3cd2..60270adda 100644 --- a/packages/compiler/src/backend/llvm/publish.ts +++ b/packages/compiler/src/backend/llvm/publish.ts @@ -31,6 +31,13 @@ import type { InlineUnionFields } from "./inline-unions.js"; import { LlvmUnsupportedError } from "./unsupported.js"; export interface PublishHost { + /** Chunk emitters only (emit-journal.ts): EmitJournal's allocated, built, + * opened, used and entry. */ + journalAllocated?: ((name: string, op: string, args: readonly unknown[]) => void) | undefined; + journalBuilt?: (() => void) | undefined; + journalOpened?: ((op: string) => void) | undefined; + journalUsed?: ((name: string, op: string, args: readonly unknown[]) => void) | undefined; + journalEntry?: ((table: string, key: string, value: unknown) => void) | undefined; declare(decl: string): void; /** An interned NUL-terminated constant. */ cstr(text: string): string; @@ -103,6 +110,14 @@ export class PublishWalkers { constructor(private readonly host: PublishHost) {} + /** Journal replay (emit-journal.ts): builds or finds the walker a chunk + * emitter recorded, and answers its name. */ + replayHelper(op: string, args: readonly unknown[]): string { + if (op === "publish.walker") + return this.walker(args[0] as IrType, args[1] as string, args[2] as SrcLoc)!; + return this.refusal(args[0] as string, args[1] as string); + } + /** The definitions emitted so far (module-level LLVM text). */ defs(): string[] { return this.defsOut; @@ -119,6 +134,7 @@ export class PublishWalkers { } const fn = this.walker(t, "the published value", loc); if (fn === null) return []; + this.host.journalUsed?.(fn, "publish.walker", [t, "the published value", loc]); this.host.declare(`declare void @scr_publish(ptr, ptr)`); return [` call void @scr_publish(ptr ${v}, ptr ${fn})`]; } @@ -156,8 +172,10 @@ export class PublishWalkers { if (known !== undefined) return known; const sym = `@sc_pub_${this.counter++}`; this.walkers.set(key, sym); + this.host.journalOpened?.("publish.walker"); const body = this.body(t, sym, path, loc); this.defsOut.push(...body, ``); + this.host.journalBuilt?.(); return sym; } @@ -172,6 +190,7 @@ export class PublishWalkers { if (known !== undefined) return known; const sym = `@sc_pub_${this.counter++}`; this.refusals.set(key, sym); + this.host.journalOpened?.("publish.refusal"); this.host.declare(`declare void @scr_pub_refuse_at(ptr, ptr, ptr)`); this.defsOut.push( `define internal void ${sym}(ptr %o, ptr %ctx) ${FN_ATTRS} { ; refuse ${llvmComment(detail)}`, @@ -181,6 +200,7 @@ export class PublishWalkers { `}`, ``, ); + this.host.journalBuilt?.(); return sym; } diff --git a/packages/compiler/src/backend/llvm/record-results.ts b/packages/compiler/src/backend/llvm/record-results.ts index 47a7983d2..db25016cd 100644 --- a/packages/compiler/src/backend/llvm/record-results.ts +++ b/packages/compiler/src/backend/llvm/record-results.ts @@ -12,20 +12,9 @@ import { import { dynamicFieldStores } from "../../ir/int32-slots.js"; import { everyExpr, everyStmtList, transformStmtList } from "../../ir/traverse.js"; import { InlineUnionFields } from "./inline-unions.js"; +import { RESULT_DST_ID, resultEntryName } from "../result-slots.js"; -/** Prefix of a function's result-slot entry: `%into.(...params, dst)` - * writes the function's fresh result object into `dst` and returns nothing. */ -export const RESULT_ENTRY_PREFIX = "%into."; - -export function resultEntryName(name: string): string { - return `${RESULT_ENTRY_PREFIX}${name}`; -} - -/** The local id of a result-slot entry's destination parameter. The - * caller passes zeroed frame storage, and on every path the entry stores - * each of its fields at most once, so a store into it is an - * initialization: there is no old value to release. */ -export const RESULT_DST_ID = "%into.dst"; +export { RESULT_DST_ID, RESULT_ENTRY_PREFIX, resultEntryName } from "../result-slots.js"; function distinctNames(names: readonly string[]): boolean { return new Set(names).size === names.length; diff --git a/packages/compiler/src/backend/llvm/reference-writes.ts b/packages/compiler/src/backend/llvm/reference-writes.ts index d31f14d12..df2ec5032 100644 --- a/packages/compiler/src/backend/llvm/reference-writes.ts +++ b/packages/compiler/src/backend/llvm/reference-writes.ts @@ -107,6 +107,7 @@ const INERT_LIB_CALLS = new Set([ "regex.resultProp", "array.namedKeys", "array.hasNamedKey", + "array.hasProperties", "process.cwd", "process.argv", "process.execArgv", @@ -118,6 +119,7 @@ const INERT_LIB_CALLS = new Set([ "worker.parentPort", "worker.root", "cp.spawnSyncOpts", + "cp.spawnSyncEnv", ]); /** jsval conversions (ir/jsval.ts): no user code, no stores. */ diff --git a/packages/compiler/src/backend/llvm/running-extrema.test.ts b/packages/compiler/src/backend/llvm/running-extrema.test.ts new file mode 100644 index 000000000..4ce307c5c --- /dev/null +++ b/packages/compiler/src/backend/llvm/running-extrema.test.ts @@ -0,0 +1,97 @@ +import { expect, test } from "vitest"; +import { BOOL, F64, type IrExpr, type IrFunction, type IrStmt } from "../../ir/ir.js"; +import { runningExtremumUpdates } from "./running-extrema.js"; + +const loc = { file: "extrema.ts", start: 0, end: 0 }; +const ref = (localId: string): IrExpr => ({ kind: "varRef", localId, type: F64, loc }); +const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc }); +const assign = (localId: string, value: IrExpr): IrStmt => ({ + kind: "assign", + localId, + value, + loc, +}); +const decl = (localId: string): IrStmt => ({ kind: "varDecl", localId, init: num(0), loc }); +const compare = (op: "<" | ">", left: IrExpr, right: IrExpr): IrExpr => ({ + kind: "bin", + op, + left, + right, + type: BOOL, + loc, +}); +/** `if (v > max) max = v;` */ +const update = (): IrStmt & { kind: "if" } => ({ + kind: "if", + cond: compare(">", ref("v"), ref("max")), + then: [assign("max", ref("v"))], + else_: null, + loc, +}); +const loop = (body: IrStmt[]): IrStmt => ({ + kind: "while", + cond: compare(">", ref("n"), num(0)), + body, + loc, +}); +const fn = (body: IrStmt[], captures: string[] = []): IrFunction => ({ + name: "scan", + params: [], + locals: ["v", "max", "n"].map((id) => ({ id, name: id, type: F64, mutable: true })), + captures: captures.map((localId) => ({ localId, name: localId, type: F64 })), + returnType: F64, + body, + loc, +}); + +test("a loop's update of an accumulator declared outside it qualifies", () => { + const s = update(); + expect(runningExtremumUpdates(fn([decl("max"), loop([decl("v"), s])])).has(s)).toBe(true); +}); + +test("the local may be on either side of the comparison", () => { + const s = update(); + s.cond = compare("<", ref("max"), ref("v")); + expect(runningExtremumUpdates(fn([decl("max"), loop([s])])).has(s)).toBe(true); +}); + +test("a local declared in the loop is not an accumulator", () => { + const s = update(); + expect(runningExtremumUpdates(fn([loop([decl("max"), s])])).size).toBe(0); +}); + +test("another write in the loop disqualifies the update", () => { + const s = update(); + const body = [decl("max"), loop([assign("max", num(1)), s])]; + expect(runningExtremumUpdates(fn(body)).size).toBe(0); +}); + +test("the assigned value must be the compared expression", () => { + const s = update(); + s.then = [assign("max", num(5))]; + expect(runningExtremumUpdates(fn([decl("max"), loop([s])])).size).toBe(0); +}); + +test("outside a loop only a captured binding qualifies", () => { + const captured = update(); + expect(runningExtremumUpdates(fn([captured], ["max"])).has(captured)).toBe(true); + expect(runningExtremumUpdates(fn([update()])).size).toBe(0); +}); + +test("an else branch disqualifies the update", () => { + const s = update(); + s.else_ = [assign("v", num(0))]; + expect(runningExtremumUpdates(fn([decl("max"), loop([s])])).size).toBe(0); +}); + +test("an update nested in another branch does not qualify", () => { + const s = update(); + const guard: IrStmt = { + kind: "if", + cond: compare(">", ref("v"), num(0)), + then: [s], + else_: null, + loc, + }; + expect(runningExtremumUpdates(fn([decl("max"), loop([guard])])).size).toBe(0); +}); diff --git a/packages/compiler/src/backend/llvm/running-extrema.ts b/packages/compiler/src/backend/llvm/running-extrema.ts new file mode 100644 index 000000000..1ddea603d --- /dev/null +++ b/packages/compiler/src/backend/llvm/running-extrema.ts @@ -0,0 +1,110 @@ +// Running maximum and minimum updates, `if (v > max) { max = v; ... }`. +// With the accumulator in a register LLVM turns such an update into a +// select (`maxsd`, or a compare and mask), so every element waits on the +// previous one's. The update gets rarer as a scan goes on, so a branch +// predicts well; emitStmtBody gives these `if`s branch weights that say the +// update is rarely taken, and LLVM keeps the branch. A clamp of a value read +// in the same iteration is not an accumulator and keeps its select: its +// branch would go whichever way the data goes. An update nested in another +// branch keeps its select too: kept, its branch can stop LLVM from turning +// the enclosing branch into selects, and that branch may follow the data. + +import type { IrExpr, IrFunction, IrStmt } from "../../ir/ir.js"; +import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; + +type Loop = IrStmt & { kind: "for" | "while" | "doWhile" | "forOf" }; + +const RELATIONAL = new Set(["<", "<=", ">", ">="]); + +/** The `if` statements of `fn` that update a running maximum or minimum: + * no `else`, a numeric `<`, `<=`, `>` or + * `>=` between a local and another expression, and a then-block that + * assigns that expression to the local. The local must carry across + * iterations: inside a loop the `if` is a statement of the innermost loop's + * body, the local is declared outside that loop, and the loop writes it only + * in this `if`; outside any loop the `if` is a statement of the function + * body and the local is a binding the function captures and writes only in + * this `if` (a callback that a loop elsewhere calls). */ +export function runningExtremumUpdates(fn: IrFunction): Set { + const found = new Set(); + const captured = new Set((fn.captures ?? []).map((c) => c.localId)); + const loops: Loop[] = []; + const candidates: { s: IrStmt; local: string; loop: Loop | undefined }[] = []; + const expr = (e: IrExpr): boolean => everyExprChild(e, expr, stmt); + const stmt = (s: IrStmt): boolean => { + if (s.kind === "if") { + const local = updatedExtremum(s); + if (local !== undefined) candidates.push({ s, local, loop: loops[loops.length - 1] }); + } + const loop = + s.kind === "for" || s.kind === "while" || s.kind === "doWhile" || s.kind === "forOf"; + if (loop) loops.push(s); + everyStmtChild(s, expr, stmt); + if (loop) loops.pop(); + return true; + }; + for (const s of fn.body) stmt(s); + for (const { s, local, loop } of candidates) { + const scope = loop ?? (captured.has(local) ? fn.body : undefined); + const body = loop ? loop.body : fn.body; + if (scope !== undefined && body.includes(s) && writes(scope, local) === 1) found.add(s); + } + return found; +} + +/** The local an `if` updates to a new maximum or minimum, if it is one. */ +function updatedExtremum(s: IrStmt & { kind: "if" }): string | undefined { + if (s.else_ !== null && s.else_.length > 0) return undefined; + const c = s.cond; + if (c.kind !== "bin" || !RELATIONAL.has(c.op)) return undefined; + if (c.left.type.kind !== "f64" || c.right.type.kind !== "f64") return undefined; + for (const [local, other] of [ + [c.left, c.right], + [c.right, c.left], + ]) { + if (local!.kind !== "varRef") continue; + const id = local!.localId; + const assigns = s.then.some( + (x) => x.kind === "assign" && x.localId === id && sameExpr(x.value, other!), + ); + if (assigns) return id; + } + return undefined; +} + +/** Declarations and writes of `local` under `scope`. A declaration counts + * as a write too, so a local declared inside a loop does not qualify. */ +function writes(scope: IrStmt | IrStmt[], local: string): number { + let n = 0; + const expr = (e: IrExpr): boolean => { + if ((e.kind === "assignExpr" || e.kind === "incDec") && e.localId === local) n++; + return everyExprChild(e, expr, stmt); + }; + const stmt = (s: IrStmt): boolean => { + if ((s.kind === "assign" || s.kind === "varDecl" || s.kind === "forOf") && s.localId === local) + n++; + else if (s.kind === "tryCatch" && s.catchLocalId === local) n++; + return everyStmtChild(s, expr, stmt); + }; + if (Array.isArray(scope)) for (const s of scope) stmt(s); + else stmt(scope); + return n; +} + +/** Structural equality of two small expressions (`v`, `a[i]`), ignoring + * source locations and types; larger expressions do not compare equal. */ +function sameExpr(a: IrExpr, b: IrExpr): boolean { + let budget = 64; + const same = (x: unknown, y: unknown): boolean => { + if (--budget < 0) return false; + if (typeof x !== "object" || x === null || typeof y !== "object" || y === null) return x === y; + if (Array.isArray(x) !== Array.isArray(y)) return false; + const keys = Object.keys(x).filter((k) => k !== "loc" && k !== "type"); + if (keys.length !== Object.keys(y).filter((k) => k !== "loc" && k !== "type").length) + return false; + return keys.every((k) => + same((x as Record)[k], (y as Record)[k]), + ); + }; + return same(a, b); +} diff --git a/packages/compiler/src/backend/llvm/shapes.test.ts b/packages/compiler/src/backend/llvm/shapes.test.ts index 347099c16..f7327a688 100644 --- a/packages/compiler/src/backend/llvm/shapes.test.ts +++ b/packages/compiler/src/backend/llvm/shapes.test.ts @@ -6,8 +6,9 @@ import { emitRecordShapes, inlineRcDecls, INLINE_DYN_RELEASES_PER_FUNCTION, + INLINE_STR_RELEASES_PER_FUNCTION, inlineRcOption, - limitInlineDynReleases, + limitInlineReleases, releaseSym, retainSym, type ShapeHost, @@ -132,10 +133,10 @@ describe("LLVM runtime RC symbols", () => { ]); }); - test("release builds inline only string retains and dyn retains and releases", () => { + test("release builds inline only string and dyn retains and releases and Map retains", () => { const { host } = declarationHost(64, "release"); expect(retainSym(host, { kind: "string" })).toBe("@sc_rc_retain_str"); - expect(releaseSym(host, { kind: "string" })).toBe("@scr_str_release"); + expect(releaseSym(host, { kind: "string" })).toBe("@sc_rc_release_str"); expect(retainSym(host, { kind: "dyn" })).toBe("@sc_rc_retain_dyn"); expect(releaseSym(host, { kind: "dyn" })).toBe("@sc_rc_release_dyn"); expect(releaseSym(host, { kind: "union", unionId: "u0" } as never)).toBe("@scr_union_release"); @@ -251,12 +252,12 @@ describe("LLVM runtime RC symbols", () => { }); }); -describe("inline dyn release budget", () => { - const body = (name: string, sites: number): string => +describe("inline release budgets", () => { + const body = (name: string, sites: number, family = "dyn"): string => [ `define internal void @${name}(ptr %v) #0 {`, "entry:", - ...Array.from({ length: sites }, () => " call void @sc_rc_release_dyn(ptr %v)"), + ...Array.from({ length: sites }, () => ` call void @sc_rc_release_${family}(ptr %v)`), " ret void", "}", "", @@ -266,7 +267,9 @@ describe("inline dyn release budget", () => { const small = body("small", INLINE_DYN_RELEASES_PER_FUNCTION); const large = body("large", INLINE_DYN_RELEASES_PER_FUNCTION + 1); // main's lines are separate parts. - const limited = limitInlineDynReleases([`; header\n${small}`, ...large.split("\n")]).join("\n"); + const limited = limitInlineReleases([`; header\n${small}`, ...large.split("\n")], "speed").join( + "\n", + ); expect(limited.startsWith("; header\n")).toBe(true); expect(limited).toContain(small); const out = limited.slice(limited.indexOf("@large(")); @@ -275,6 +278,20 @@ describe("inline dyn release budget", () => { INLINE_DYN_RELEASES_PER_FUNCTION + 1, ); }); + + test("a function over the string budget calls the runtime string release at every site", () => { + const small = body("small", INLINE_STR_RELEASES_PER_FUNCTION, "str"); + const large = body("large", INLINE_STR_RELEASES_PER_FUNCTION + 1, "str"); + const limited = limitInlineReleases([small, large], "release").join("\n"); + expect(limited).toContain(small); + // Speed emission keeps every inline string release. + expect(limitInlineReleases([small, large], "speed").join("\n")).toBe([small, large].join("\n")); + const out = limited.slice(limited.indexOf("@large(")); + expect(out).not.toContain("@sc_rc_release_str("); + expect(out.split("call void @scr_str_release(ptr %v)").length - 1).toBe( + INLINE_STR_RELEASES_PER_FUNCTION + 1, + ); + }); }); describe("emitted object releases", () => { diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index 6de92f667..d13f80a78 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -68,9 +68,26 @@ 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 { + /** Chunk emitters only (emit-journal.ts): EmitJournal's allocated, built, + * opened, used and entry. */ + journalAllocated?: ((name: string, op: string, args: readonly unknown[]) => void) | undefined; + journalBuilt?: (() => void) | undefined; + journalOpened?: ((op: string) => void) | undefined; + journalUsed?: ((name: string, op: string, args: readonly unknown[]) => void) | undefined; + journalEntry?: ((table: string, key: string, value: unknown) => void) | undefined; /** 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; + /** Whether releasing a value of this type does nothing: instances of a + * program-lifetime class without lifetime regions (classes.ts + * programLifetimeClass). */ + releasesNothing?(t: IrType): boolean; + /** Whether arrays of `elem` hold 32-bit references to compact class + * instances (SCR_ELEM_CREF; LlEmitter.compactArrayElem). */ + compactArrayElem?(elem: IrType): boolean; + /** Whether compact class instances live in the program-lifetime region + * (LlEmitter.compactRegion). */ + compactRegion?(): boolean; /** Byte offset from an object pointer back to ScrCycHdr.buf. */ readonly cycleBufOffset: number; declare(decl: string): void; @@ -386,6 +403,7 @@ function nullableRetainSym(host: ShapeHost, arm: IrType): string { const sym = `@sc_nretain_${arm.kind}`; if (!host.nullableUnions.retainWrappers.has(sym)) host.nullableUnions.retainWrappers.set(sym, retainSym(host, arm)); + host.journalEntry?.("retainWrapper", sym, host.nullableUnions.retainWrappers.get(sym)); return sym; } @@ -564,14 +582,18 @@ export function inlineRcFamily(host: ShapeHost, stem: string, op: RcOp): InlineR } /** Which RC fast paths an emission inlines. `speed` mirrors every family - * with a fast path. `release` mirrors only the string retain and the dyn - * retain and release: the operations that dominate dynamic-value and string - * code, at almost no size cost. String releases stay calls in `release` - * because string-heavy programs have many release sites. */ + * with a fast path. `release` mirrors only the string retain and release, + * the Map retain and the dyn retain and release: the operations that + * dominate dynamic-value, string and Map code (a Map read from an array or + * another Map). String-heavy programs have many string release sites, so a + * function with more than INLINE_STR_RELEASES_PER_FUNCTION of them calls the + * runtime release at each (limitInlineReleases). */ export type InlineRcMode = "release" | "speed"; const RELEASE_RC_OPS = new Set([ "str:retain", + "str:release", + "map:retain", "dyn:retain", "dyn:release", "js:retain", @@ -654,12 +676,34 @@ function inlineRcName(family: string, op: RcOp): string { * the inline helper mirrors, at every site. */ export const INLINE_DYN_RELEASES_PER_FUNCTION = 256; -/** Apply {@link INLINE_DYN_RELEASES_PER_FUNCTION} to each function of an - * emitted module, given as parts joined by newlines. A function can span - * parts (main's lines) and a part can hold several functions. */ -export function limitInlineDynReleases(parts: readonly string[]): string[] { - const call = `call void @${inlineRcName("dyn", "release")}(`; - const runtime = `call void @${INLINE_RC_FAMILIES["dyn"]!.release}(`; +/** Inlined string releases one function body keeps in `release` emission, + * which inlines string releases (RELEASE_RC_OPS); the bodies with the most of them, such as + * builders of string-heavy records and module initializers, would grow the + * most for the least gain, so a body with more calls scr_str_release at + * every site. */ +export const INLINE_STR_RELEASES_PER_FUNCTION = 8; + +/** Apply {@link INLINE_DYN_RELEASES_PER_FUNCTION}, and in `release` emission + * {@link INLINE_STR_RELEASES_PER_FUNCTION}, to each function of an emitted + * module, given as parts joined by newlines. A function can span parts + * (main's lines) and a part can hold several functions. */ +export function limitInlineReleases( + parts: readonly string[], + mode: InlineRcMode | undefined, +): string[] { + const dyn = limitInlineReleaseFamily(parts, "dyn", INLINE_DYN_RELEASES_PER_FUNCTION); + return mode === "release" + ? limitInlineReleaseFamily(dyn, "str", INLINE_STR_RELEASES_PER_FUNCTION) + : dyn; +} + +function limitInlineReleaseFamily( + parts: readonly string[], + family: "dyn" | "str", + max: number, +): string[] { + const call = `call void @${inlineRcName(family, "release")}(`; + const runtime = `call void @${INLINE_RC_FAMILIES[family]!.release}(`; // The function each release site belongs to: the last `define` line // before it, counted over the joined text. const sites = ( @@ -685,9 +729,7 @@ export function limitInlineDynReleases(parts: readonly string[]): string[] { const counts = new Map(); let fn = 0; for (const part of parts) fn = sites(part, (_, f) => counts.set(f, (counts.get(f) ?? 0) + 1), fn); - const over = new Set( - [...counts].filter(([, n]) => n > INLINE_DYN_RELEASES_PER_FUNCTION).map(([f]) => f), - ); + const over = new Set([...counts].filter(([, n]) => n > max).map(([f]) => f)); if (over.size === 0) return [...parts]; fn = 0; return parts.map((part) => { @@ -1076,18 +1118,41 @@ export function arrNewCall( /** The %ScrArrOps descriptor (retain, release, trace, registry id) of a REF * element type, defined once per module and shared by all its arrays. It is - * writable: the runtime stores its registry id on the first array. */ + * writable: the runtime stores its registry id on the first array. Its tail + * padding is the runtime's flags word (0); arrays of compact class + * references (ShapeHost.compactArrayElem) spell the word out as + * SCR_ARR_OPS_COMPACT, and on 64-bit targets arrays whose element release + * does nothing (ShapeHost.releasesNothing) as SCR_ARR_OPS_RELEASE_NOTHING, + * so that destroying them only frees their storage. */ export function arrOps(host: ShapeHost, elem: IrType): string { const v = vAdapters(host, elem); const trace = traceArg(host, elem); const part = (sym: string): string => sym.replace(/^@/, "").replace(/[^A-Za-z0-9_.]/g, "_"); const name = `@sc_arrops.${part(v.retain)}.${part(v.release)}.${part(trace)}`; + if (host.compactArrayElem?.(elem) === true) { + host.declare( + `${name}.c = internal global { ptr, ptr, ptr, i32, i32 } { ptr ${v.retain}, ptr ${v.release}, ptr ${trace}, i32 0, i32 ${SCR_ARR_OPS_COMPACT} }`, + ); + return `${name}.c`; + } + const nullable = host.nullableUnions.of(elem); + const payload = nullable?.repr === "single" ? nullable.arm : elem; + if (host.sizeType === "i64" && host.releasesNothing?.(payload) === true) { + host.declare( + `${name}.n = internal global { ptr, ptr, ptr, i32, i32 } { ptr ${v.retain}, ptr ${v.release}, ptr ${trace}, i32 0, i32 ${SCR_ARR_OPS_RELEASE_NOTHING} }`, + ); + return `${name}.n`; + } host.declare( `${name} = internal global %ScrArrOps { ptr ${v.retain}, ptr ${v.release}, ptr ${trace}, i32 0 }`, ); return name; } +/** scr_array.h SCR_ARR_OPS_COMPACT and SCR_ARR_OPS_RELEASE_NOTHING. */ +const SCR_ARR_OPS_COMPACT = 1; +const SCR_ARR_OPS_RELEASE_NOTHING = 2; + /* ── capture boxes ────────────────────────────────────────────────────── */ /** Box construction call text (boxNewC's dispatch): plain-kind boxes for diff --git a/packages/compiler/src/backend/llvm/speculative-integers.ts b/packages/compiler/src/backend/llvm/speculative-integers.ts index a309a9785..00fabb788 100644 --- a/packages/compiler/src/backend/llvm/speculative-integers.ts +++ b/packages/compiler/src/backend/llvm/speculative-integers.ts @@ -35,12 +35,12 @@ export interface SpeculativeLoopPlan { type SpeculativeLoop = Extract; /** The fast copy duplicates the loop; keep the duplicated code small. */ -const MAX_LOOP_NODES = 400; +export const MAX_LOOP_NODES = 400; /** Statements whose emission is plain control flow or a single write. * Nested loops, switches and try statements keep the loop on the ordinary * path (their emission keeps state that the two copies would not share). */ -const SIMPLE_STATEMENTS = new Set([ +export const SIMPLE_STATEMENTS: ReadonlySet = new Set([ "varDecl", "assign", "exprStmt", diff --git a/packages/compiler/src/backend/llvm/string-reads.ts b/packages/compiler/src/backend/llvm/string-reads.ts index 03a9fa282..441c925f3 100644 --- a/packages/compiler/src/backend/llvm/string-reads.ts +++ b/packages/compiler/src/backend/llvm/string-reads.ts @@ -17,6 +17,7 @@ * is thread-local. */ +import type { LlvmEmitterContext } from "./expr-context.js"; import { exactIndexLines, sizeToDouble } from "./common.js"; import { wtf8Bytes } from "../literals.js"; import { literalCapWord, literalCapWord32 } from "./string-key-hash.js"; @@ -37,6 +38,66 @@ export const STRING_CHAR_AT_DECL = "declare ptr @scr_str_char_at(ptr, double)"; /** Requests the integer-index charAt helper, as the marker above does. */ export const STRING_CHAR_AT_INT_MARKER = "; uses @sc_str_char_at_int"; +/** The proven-ASCII flag of string `s`, as an i1. */ +export function emitStringAsciiFlag(host: LlvmEmitterContext, s: string): string { + const B = host.B; + const wordPointer = B.tmp(), + word = B.tmp(), + ascii = B.tmp(); + B.line(`${wordPointer} = getelementptr inbounds %ScrStr, ptr ${s}, i32 0, i32 2`); + B.line(`${word} = load i32, ptr ${wordPointer}`); + B.line(`${ascii} = icmp slt i32 ${word}, 0`); + return ascii; +} + +/** The byte length of string `s`, as the target's size type. */ +export function emitStringByteLength(host: LlvmEmitterContext, s: string): string { + const B = host.B; + const lengthPointer = B.tmp(), + length = B.tmp(); + B.line(`${lengthPointer} = getelementptr inbounds %ScrStr, ptr ${s}, i32 0, i32 1`); + B.line(`${length} = load ${host.sizeType}, ptr ${lengthPointer}`); + return length; +} + +/** Unit `index` (an i64) of a proven-ASCII string `s` whose length covers + * it: one byte load, as a double. */ +export function emitAsciiCharCode(host: LlvmEmitterContext, s: string, index: string): string { + const B = host.B; + const data = B.tmp(), + pointer = B.tmp(), + byte = B.tmp(), + code = B.tmp(); + B.line(`${data} = getelementptr inbounds %ScrStr, ptr ${s}, i32 1`); + B.line(`${pointer} = getelementptr inbounds i8, ptr ${data}, i64 ${index}`); + B.line(`${byte} = load i8, ptr ${pointer}`); + B.line(`${code} = uitofp i8 ${byte} to double`); + return code; +} + +/** The one-character string of unit `index` (an i64) of a proven-ASCII + * string `s` whose length covers it: the immortal `@sc_ascii_chars` entry + * for its byte, as the inline charAt helpers answer. */ +export function emitAsciiChar(host: LlvmEmitterContext, s: string, index: string): string { + // The table is defined with sc_str_char_at. + host.declare(STRING_CHAR_AT_DECL); + const B = host.B; + const sz = host.sizeType; + const data = B.tmp(), + pointer = B.tmp(), + byte = B.tmp(), + code = B.tmp(), + entry = B.tmp(); + B.line(`${data} = getelementptr inbounds %ScrStr, ptr ${s}, i32 1`); + B.line(`${pointer} = getelementptr inbounds i8, ptr ${data}, i64 ${index}`); + B.line(`${byte} = load i8, ptr ${pointer}`); + B.line(`${code} = zext i8 ${byte} to i64`); + B.line( + `${entry} = getelementptr inbounds [128 x { ${sz}, ${sz}, ${sz}, [2 x i8] }], ptr @sc_ascii_chars, i64 0, i64 ${code}`, + ); + return entry; +} + /** Load the proven-ASCII flag of `%s` into `%ascii`. */ function asciiTest(): string[] { return [ diff --git a/packages/compiler/src/backend/llvm/walkers.ts b/packages/compiler/src/backend/llvm/walkers.ts index 46dbc86e1..331ea2199 100644 --- a/packages/compiler/src/backend/llvm/walkers.ts +++ b/packages/compiler/src/backend/llvm/walkers.ts @@ -1,3 +1,4 @@ +import { emitPerKeyOrder } from "./key-orders.js"; import { f64Lit } from "./common.js"; import { InternalCompilerError } from "../../errors.js"; /* Structure-walking helper EMITTERS for the LLVM backend — the .ll mirror @@ -62,6 +63,21 @@ export class LlWalkers { constructor(private readonly host: WalkerHost) {} + /** Journal replay (emit-journal.ts): builds or finds the helper a chunk + * emitter recorded, and answers its name. */ + replayHelper(op: string, args: readonly unknown[]): string { + switch (op) { + case "walkers.jsonWriteHelper": + return this.jsonWriteHelper(args[0] as IrType); + case "walkers.recordFieldWriter": + return this.recordFieldWriter(args[0] as string); + case "walkers.unionJoinHelper": + return this.unionJoinHelper(args[0] as string); + default: + throw new InternalCompilerError(`llvm emitter bug: no walker replay ${op}`); + } + } + private get S(): "i32" | "i64" { return this.host.sizeType; } @@ -191,6 +207,7 @@ export class LlWalkers { if (existing) return existing; const name = `sc_jw_${this.jsonWriters.size}`; this.jsonWriters.set(key, name); + this.host.journalAllocated?.(name, "walkers.jsonWriteHelper", [t]); const B = new BlockBuilder(); switch (t.kind) { case "f64": @@ -244,6 +261,7 @@ export class LlWalkers { `}`, ``, ); + this.host.journalBuilt?.(); return name; } @@ -311,78 +329,80 @@ export class LlWalkers { if (cyclic) this.jbLeave(B); return; } - // Fields serialize in DECLARED order (JS insertion order); internal + // Fields serialize in DECLARED order (JS insertion order), or in the + // value's own order for a shape with orderVariants; internal // '%'-fields stay hidden — jsonWriteHelper's contract, ported comment // and all. - const order = shape.declaredOrder ?? shape.fields.map((f) => f.name); - const inOrder = new Set(order); - if (shape.fields.some((f) => !inOrder.has(f.name) && !f.name.startsWith("%"))) { - throw new InternalCompilerError( - `llvm emitter bug: declaredOrder of shape ${shapeId} omits a non-internal field`, - ); - } - const byName = new Map(shape.fields.map((f) => [f.name, f])); - // Function-valued properties are omitted, as JSON.stringify omits them. - const emitFields = order - .map((n) => byName.get(n)) - .filter((f) => f !== undefined) - .filter((f) => f.type.kind !== "func"); - const droppable = - emitFields.some((f) => undefinedArmTag(f.type, this.host.unionsById) >= 0) || - !!shape.indexValue; - this.putc(B, "%b", "123"); // '{' - if (!droppable) { - emitFields.forEach((f, i) => { - this.puts(B, "%b", `${i > 0 ? "," : ""}${jsonObjectKeyLabel(f.name)}`); - if (edgeable(f.type)) this.jbEdgeProp(B, f.name); - const v = this.loadField(B, "%v", shapeId, fieldIndex.get(f.name)!, f.type); - B.line( - `call void @${this.jsonWriteHelper(f.type)}(ptr %b, ${this.valTy(f.type)} ${v}) ; ${llvmCommentText(f.name)}`, + emitPerKeyOrder(B, shape, fieldBase, "%v", (order) => { + const inOrder = new Set(order); + if (shape.fields.some((f) => !inOrder.has(f.name) && !f.name.startsWith("%"))) { + throw new InternalCompilerError( + `llvm emitter bug: declaredOrder of shape ${shapeId} omits a non-internal field`, ); - }); - } else { - const first = B.slot(); - B.entryAllocas.push(`${first} = alloca i1`); - B.line(`store i1 true, ptr ${first}`); - const comma = (): void => { - const isf = B.tmp(); - const lc = B.newLabel("jwc.c"); - const lj = B.newLabel("jwc.j"); - B.line(`${isf} = load i1, ptr ${first}`); - B.condBr(isf, lj, lc); - B.startBlock(lc); - this.putc(B, "%b", "44"); // ',' - B.br(lj); - B.startBlock(lj); - B.line(`store i1 false, ptr ${first}`); - }; - for (const f of emitFields) { - const utag = undefinedArmTag(f.type, this.host.unionsById); - const v = this.loadField(B, "%v", shapeId, fieldIndex.get(f.name)!, f.type); - let skip: string | null = null; - if (utag >= 0 && f.type.kind === "union") { - // Undefined-valued field: dropped, like Node. - const tag = this.unionTag(B, v, f.type.unionId); - const isu = B.tmp(); - B.line(`${isu} = icmp eq i32 ${tag}, ${utag}`); - const lw = B.newLabel("jwf.w"); - skip = B.newLabel("jwf.s"); - B.condBr(isu, skip, lw); - B.startBlock(lw); - } - comma(); - this.puts(B, "%b", jsonObjectKeyLabel(f.name)); - if (edgeable(f.type)) this.jbEdgeProp(B, f.name); - B.line( - `call void @${this.jsonWriteHelper(f.type)}(ptr %b, ${this.valTy(f.type)} ${v}) ; ${llvmCommentText(f.name)}`, - ); - if (skip !== null) { - B.br(skip); - B.startBlock(skip); - } } - if (shape.indexValue) this.emitOverflowEntries(B, shape, first, edgeable(shape.indexValue)); - } + const byName = new Map(shape.fields.map((f) => [f.name, f])); + // Function-valued properties are omitted, as JSON.stringify omits them. + const emitFields = order + .map((n) => byName.get(n)) + .filter((f) => f !== undefined) + .filter((f) => f.type.kind !== "func"); + const droppable = + emitFields.some((f) => undefinedArmTag(f.type, this.host.unionsById) >= 0) || + !!shape.indexValue; + this.putc(B, "%b", "123"); // '{' + if (!droppable) { + emitFields.forEach((f, i) => { + this.puts(B, "%b", `${i > 0 ? "," : ""}${jsonObjectKeyLabel(f.name)}`); + if (edgeable(f.type)) this.jbEdgeProp(B, f.name); + const v = this.loadField(B, "%v", shapeId, fieldIndex.get(f.name)!, f.type); + B.line( + `call void @${this.jsonWriteHelper(f.type)}(ptr %b, ${this.valTy(f.type)} ${v}) ; ${llvmCommentText(f.name)}`, + ); + }); + } else { + const first = B.slot(); + B.entryAllocas.push(`${first} = alloca i1`); + B.line(`store i1 true, ptr ${first}`); + const comma = (): void => { + const isf = B.tmp(); + const lc = B.newLabel("jwc.c"); + const lj = B.newLabel("jwc.j"); + B.line(`${isf} = load i1, ptr ${first}`); + B.condBr(isf, lj, lc); + B.startBlock(lc); + this.putc(B, "%b", "44"); // ',' + B.br(lj); + B.startBlock(lj); + B.line(`store i1 false, ptr ${first}`); + }; + for (const f of emitFields) { + const utag = undefinedArmTag(f.type, this.host.unionsById); + const v = this.loadField(B, "%v", shapeId, fieldIndex.get(f.name)!, f.type); + let skip: string | null = null; + if (utag >= 0 && f.type.kind === "union") { + // Undefined-valued field: dropped, like Node. + const tag = this.unionTag(B, v, f.type.unionId); + const isu = B.tmp(); + B.line(`${isu} = icmp eq i32 ${tag}, ${utag}`); + const lw = B.newLabel("jwf.w"); + skip = B.newLabel("jwf.s"); + B.condBr(isu, skip, lw); + B.startBlock(lw); + } + comma(); + this.puts(B, "%b", jsonObjectKeyLabel(f.name)); + if (edgeable(f.type)) this.jbEdgeProp(B, f.name); + B.line( + `call void @${this.jsonWriteHelper(f.type)}(ptr %b, ${this.valTy(f.type)} ${v}) ; ${llvmCommentText(f.name)}`, + ); + if (skip !== null) { + B.br(skip); + B.startBlock(skip); + } + } + if (shape.indexValue) this.emitOverflowEntries(B, shape, first, edgeable(shape.indexValue)); + } + }); this.putc(B, "%b", "125"); // '}' if (cyclic) this.jbLeave(B); } @@ -457,6 +477,7 @@ export class LlWalkers { throw new InternalCompilerError(`llvm emitter bug: field writer of unknown shape ${shapeId}`); const name = `sc_jwf_${this.recordFieldWriters.size}`; this.recordFieldWriters.set(shapeId, name); + this.host.journalAllocated?.(name, "walkers.recordFieldWriter", [shapeId]); const fieldBase = this.host.nullableUnions.fieldBase(shapeId); const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + fieldBase])); const byName = new Map(shape.fields.map((f) => [f.name, f])); @@ -508,6 +529,7 @@ export class LlWalkers { `}`, ``, ); + this.host.journalBuilt?.(); return name; } @@ -852,6 +874,7 @@ export class LlWalkers { if (!def) throw new InternalCompilerError(`llvm emitter bug: join of unknown union ${unionId}`); const name = `sc_uj_${this.unionJoinFns.size}`; this.unionJoinFns.set(unionId, name); + this.host.journalAllocated?.(name, "walkers.unionJoinHelper", [unionId]); const host = this.host; host.declare(`declare void @scr_jb_init(ptr)`); @@ -946,6 +969,7 @@ export class LlWalkers { `}`, ``, ); + this.host.journalBuilt?.(); return name; } } diff --git a/packages/compiler/src/backend/llvm/wrapping-sums.ts b/packages/compiler/src/backend/llvm/wrapping-sums.ts index b00b61cbb..c736fcf72 100644 --- a/packages/compiler/src/backend/llvm/wrapping-sums.ts +++ b/packages/compiler/src/backend/llvm/wrapping-sums.ts @@ -1,4 +1,5 @@ -import type { IrExpr } from "../../ir/ir.js"; +import type { IrExpr, IrFunction, IrStmt } from "../../ir/ir.js"; +import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; import { integerArithmeticRange, type IntegerRange } from "../../ir/integer-ranges.js"; import type { LlValue, LlvmEmitterContext } from "./expr-context.js"; import type { LlInteger } from "./integer-values.js"; @@ -47,6 +48,7 @@ export function integerOrNaNRange(host: LlvmEmitterContext, e: IrExpr): IntegerR return ELEMENT_RANGES.i32!; // A UTF-16 code unit, or NaN past either end. if (e.kind === "strIntrinsic" && e.method === "charCodeAt") return { min: 0, max: 65535 }; + if (e.kind === "varRef") return host.integerOrNaNLocals.get(e.localId) ?? null; if (e.kind === "bin" && (e.op === "+" || e.op === "-")) { const left = integerOrNaNRange(host, e.left); const right = left && integerOrNaNRange(host, e.right); @@ -55,6 +57,34 @@ export function integerOrNaNRange(host: LlvmEmitterContext, e: IrExpr): IntegerR return null; } +/** An index that is an exact integer or NaN (`integerOrNaNRange`): the integer as an i64, and an + * i1 that is false when the index is NaN. An index check then needs no round trip through a double: + * the integer is the value's ToUint32 view where the range proves it holds the whole value, or one + * conversion, which is exact for a safe integer and frozen, since the conversion of NaN is poison. + * Null for any other index. */ +export function integerOrNaNIndex( + host: LlvmEmitterContext, + value: LlValue, + expr: IrExpr | undefined, +): { wide: string; ordered: string } | null { + const range = expr ? integerOrNaNRange(host, expr) : null; + if (!range) return null; + const B = host.B; + const ordered = B.tmp(); + const wide = B.tmp(); + B.line(`${ordered} = fcmp ord double ${value.name}, 0.0`); + if (value.uint32 !== undefined && range.min >= 0 && range.max <= 4294967295) { + B.line(`${wide} = zext i32 ${value.uint32} to i64`); + } else if (value.uint32 !== undefined && range.min >= -2147483648 && range.max <= 2147483647) { + B.line(`${wide} = sext i32 ${value.uint32} to i64`); + } else { + const raw = B.tmp(); + B.line(`${raw} = fptosi double ${value.name} to i64`); + B.line(`${wide} = freeze i64 ${raw}`); + } + return { wide, ordered }; +} + const ELEMENT_RANGES: Partial> = { u8: { min: 0, max: 255 }, u8c: { min: 0, max: 255 }, @@ -131,3 +161,85 @@ export function emitWrappingSum( B.line(`${out} = select i1 ${valid}, i32 ${sum}, i32 0`); return out; } + +/** The number locals of `fn` that only hold exact integers (never -0) or NaN, with the range of + * those integers: locals declared once with an initializer, neither parameters nor captured or + * boxed, whose initializer and every later write `integerOrNaNRange` covers. A local joins once + * the values it is written depend only on locals that have joined, so `x = x + 1` keeps `x` out: + * its values have no bound. Uses the function's `integerRanges` and `int32Elements`. */ +export function findIntegerOrNaNLocals( + host: LlvmEmitterContext, + fn: IrFunction, +): ReadonlyMap { + const found = new Map(); + if (fn.async || fn.generator) return found; + const excluded = new Set(); + for (const param of fn.params) excluded.add(param.localId); + for (const capture of fn.captures ?? []) excluded.add(capture.localId); + for (const capture of fn.classCaptures ?? []) excluded.add(capture.localId); + const candidates = new Set(); + for (const local of fn.locals) + if (local.type.kind === "f64" && !local.boxed && !local.tdz && !excluded.has(local.id)) + candidates.add(local.id); + if (candidates.size === 0) return found; + const declared = new Set(); + const writes = new Map(); + const drop = (id: string): void => { + candidates.delete(id); + writes.delete(id); + }; + const write = (id: string, value: IrExpr): void => { + if (!candidates.has(id)) return; + const list = writes.get(id); + if (list) list.push(value); + else writes.set(id, [value]); + }; + function walkExpr(e: IrExpr): boolean { + if (e.kind === "assignExpr") write(e.localId, e.value); + else if (e.kind === "incDec") drop(e.localId); + return everyExprChild(e, walkExpr, walkStmt); + } + function walkStmt(s: IrStmt): boolean { + if (s.kind === "assign") write(s.localId, s.value); + else if (s.kind === "varDecl") { + if (s.init === null || declared.has(s.localId)) drop(s.localId); + else { + declared.add(s.localId); + write(s.localId, s.init); + } + } else if (s.kind === "forOf") drop(s.localId); + else if (s.kind === "tryCatch" && s.catchLocalId) drop(s.catchLocalId); + return everyStmtChild(s, walkExpr, walkStmt); + } + for (const s of fn.body) walkStmt(s); + // `integerOrNaNRange` reads the joined locals while the next ones are tested. + const previous = host.integerOrNaNLocals; + host.integerOrNaNLocals = found; + try { + let joined = true; + while (joined) { + joined = false; + for (const [id, values] of writes) { + if (found.has(id) || !declared.has(id)) continue; + let range: IntegerRange | null = null; + for (const value of values) { + const next = integerOrNaNRange(host, value); + if (next === null) { + range = null; + break; + } + range = + range === null + ? next + : { min: Math.min(range.min, next.min), max: Math.max(range.max, next.max) }; + } + if (range === null) continue; + found.set(id, range); + joined = true; + } + } + } finally { + host.integerOrNaNLocals = previous; + } + return found; +} diff --git a/packages/compiler/src/backend/may-throw.ts b/packages/compiler/src/backend/may-throw.ts index b72dc545d..a04cce856 100644 --- a/packages/compiler/src/backend/may-throw.ts +++ b/packages/compiler/src/backend/may-throw.ts @@ -35,7 +35,12 @@ export type MayThrowReason = * and a generator's `.return()` unwind through `catch`. */ export function computeMayThrow( mod: IrModule, - options: { stackChecks?: boolean; explain?: boolean } = {}, + options: { + stackChecks?: boolean; + explain?: boolean; + /** computePublishedTypes(mod), when the caller already has it. */ + published?: ReturnType; + } = {}, ): { fns: Set; /** With `explain`: the reason each may-throw function was marked. */ @@ -50,7 +55,8 @@ export function computeMayThrow( const terminations = mod.workers === true ? workerTerminationPolls(mod) : null; const stackChecks = options.stackChecks === true ? stackCheckedFunctions(mod) : null; // Stores that can reach a published object throw the frozen TypeError. - const published = computePublishedTypes(mod); + const published = + options.published !== undefined ? options.published : computePublishedTypes(mod); interface Facts { throws: boolean; callees: string[]; diff --git a/packages/compiler/src/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts index 5782f091d..e018037fd 100644 --- a/packages/compiler/src/backend/native-link-info-core.ts +++ b/packages/compiler/src/backend/native-link-info-core.ts @@ -49,6 +49,7 @@ export interface NativeLinkFeatures { /** Own properties JavaScript adds to children: scr_dyn_handle_props.c. * Absent (false) for programs without them. */ handleProps?: boolean; + declAdopt?: boolean; /** for await over checked-dynamic values: scr_dyn_for_await.c. Absent * (false) for programs without it. */ dynForAwait?: boolean; diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts index f683ede3e..61de82e72 100644 --- a/packages/compiler/src/backend/native/contracts.ts +++ b/packages/compiler/src/backend/native/contracts.ts @@ -122,6 +122,7 @@ export interface CcOptions { streamDyn?: boolean; /** Own properties JavaScript adds to children: compiles scr_dyn_handle_props.c. */ handleProps?: boolean; + declAdopt?: boolean; /** for await over checked-dynamic values: compiles scr_dyn_for_await.c. */ dynForAwait?: boolean; /** events.once with options (events.onceSignal): compiles diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index 714bc09a5..233b9cac2 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -279,8 +279,15 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ unit((u) => u.opts.streamDyn === true, "scr_stream_dyn.c"), // Own properties JavaScript adds to children. unit((u) => u.opts.handleProps === true, "scr_dyn_handle_props.c"), + // The declaration-typed boundary's adoption. + unit((u) => u.opts.declAdopt === true, "scr_decl_adopt.c"), // for await over checked-dynamic values. unit((u) => u.opts.dynForAwait === true, "scr_dyn_for_await.c"), + // for await over checked-dynamic values that may hold native streams. + unit( + (u) => u.opts.dynForAwait === true && u.opts.stream && u.opts.dynAsync, + "scr_dyn_for_await_stream.c", + ), // FileHandle streams reference both the stream unit and FileHandle. unit((u) => u.opts.stream && u.opts.fileHandle, "scr_fs_stream_fh.c"), // The readiness-poller backends (scr_platform.h): kqueue on macOS/BSD, @@ -332,6 +339,8 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ unitArgs((u) => (u.tlsCa && targetPlatform(u.driver) === "win32" ? ["-lcrypt32"] : [])), // Fetch uses the native network, TLS, and HTTP runtime. unit((u) => u.nativeFetch, "scr_fetch.c"), + // Readable.fromWeb reads a web ReadableStream through its reader. + unit((u) => u.nativeFetch && u.opts.stream, "scr_stream_from_web.c"), ]; /** The arguments of the optional runtime inputs, in link order. */ diff --git a/packages/compiler/src/backend/result-slots.ts b/packages/compiler/src/backend/result-slots.ts new file mode 100644 index 000000000..608877f06 --- /dev/null +++ b/packages/compiler/src/backend/result-slots.ts @@ -0,0 +1,29 @@ +import type { IrStmt } from "../ir/ir.js"; + +/** Prefix of a function's result-slot entry: `%into.(...params, dst)` + * writes the function's fresh result object into `dst` and returns nothing + * (llvm/record-results.ts). */ +export const RESULT_ENTRY_PREFIX = "%into."; + +export function resultEntryName(name: string): string { + return `${RESULT_ENTRY_PREFIX}${name}`; +} + +/** The local id of a result-slot entry's destination parameter. The + * caller passes zeroed frame storage, and on every path the entry stores + * each of its fields at most once, so a store into it is an + * initialization: there is no old value to release. */ +export const RESULT_DST_ID = "%into.dst"; + +/** Whether `s`, a statement of the function `fnName`, is a result-slot + * entry's store into its destination. The destination is frame storage of + * a caller that only projects the value (llvm/stack-objects.ts), so the + * store initializes a field of an object no other object can point to. */ +export function resultSlotStore(fnName: string, s: IrStmt): boolean { + return ( + s.kind === "recordSet" && + s.obj.kind === "varRef" && + s.obj.localId === RESULT_DST_ID && + fnName.startsWith(RESULT_ENTRY_PREFIX) + ); +} diff --git a/packages/compiler/src/backend/runtime-abi.ts b/packages/compiler/src/backend/runtime-abi.ts index 3a41d9176..582e516ae 100644 --- a/packages/compiler/src/backend/runtime-abi.ts +++ b/packages/compiler/src/backend/runtime-abi.ts @@ -2,8 +2,8 @@ * of the link contract: a program object keeps this symbol undefined and the * matching runtime defines it, so incompatible manual links fail before the * program can start. */ -export const RUNTIME_ABI_VERSION = 27 as const; -export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v27" as const; +export const RUNTIME_ABI_VERSION = 36 as const; +export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v36" as const; /** Object consumption is useful today, but the complete scr_* surface may * still change before 1.0. The versioned marker prevents accidental mixing; diff --git a/packages/compiler/src/backend/runtime-pack-core.ts b/packages/compiler/src/backend/runtime-pack-core.ts index b59fdc093..75953be59 100644 --- a/packages/compiler/src/backend/runtime-pack-core.ts +++ b/packages/compiler/src/backend/runtime-pack-core.ts @@ -86,6 +86,7 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures { dynTier: boolean; streamDyn: boolean; handleProps: boolean; + declAdopt: boolean; dynForAwait: boolean; eventsOnceSignal: boolean; dynBuffer: boolean; @@ -283,6 +284,7 @@ export function effectiveRuntimeFeatures( dynTier: features.dynTier === true, streamDyn: features.streamDyn === true, handleProps: features.handleProps === true, + declAdopt: features.declAdopt === true, dynForAwait: features.dynForAwait === true, eventsOnceSignal: features.eventsOnceSignal === true, dynBuffer: features.dynBuffer === true, diff --git a/packages/compiler/src/backend/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts index af61e5229..739f6a385 100644 --- a/packages/compiler/src/backend/runtime-pack-targets.test.ts +++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts @@ -52,7 +52,7 @@ async function fixture(target: NativeTargetSpec, workers = false) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" }, + runtime_abi: { version: 36, marker: "scr_runtime_abi_v36" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" }, flavors: { diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts index d09509911..74bc32e73 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -127,7 +127,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" }, + runtime_abi: { version: 36, marker: "scr_runtime_abi_v36" }, compiler: { command: "clang", identity: "fixture clang", diff --git a/packages/compiler/src/backend/stack-observations.ts b/packages/compiler/src/backend/stack-observations.ts index fa67c071f..c448897f1 100644 --- a/packages/compiler/src/backend/stack-observations.ts +++ b/packages/compiler/src/backend/stack-observations.ts @@ -29,35 +29,50 @@ const NON_STACK_KEY_TYPES: ReadonlySet = new Set(["f64", "bigint * containers or calls; a key the program assembles from other strings or * from data it reads keeps the static answer. */ class StackKeys { - private readonly spelled: boolean; - private readonly calls = new Map(); - private readonly named: Set; + private spelled: boolean | undefined; + private census: + | { calls: Map; named: Set } + | undefined; private readonly inits = new Map>(); /** Settled answers by function and local: true where a search reached a * key that can be "stack", false where a search that found none passed. */ private readonly answers = new Map(); - constructor(mod: IrModule) { - this.spelled = spellsStack(mod); - this.named = new Set(moduleFunctionReferences(mod)); + // The module walks run on the first question that needs them: a program + // without a dynamic key read asks none. + constructor(private readonly mod: IrModule) {} + + private spellsStack(): boolean { + return (this.spelled ??= spellsStack(this.mod)); + } + + private callCensus(): { + calls: Map; + named: Set; + } { + if (this.census !== undefined) return this.census; + const mod = this.mod; + const calls = new Map(); + const named = new Set(moduleFunctionReferences(mod)); // Class methods, accessors and constructors are called through dispatch. const classNames = new Set((mod.classes ?? []).map((cls) => cls.name)); for (const fn of mod.functions) { - if (isClassMember(fn.name, classNames)) this.named.add(fn.name); + if (isClassMember(fn.name, classNames)) named.add(fn.name); everyStmtList(fn.body, { stmt: () => true, expr: (expr) => { if (expr.kind === "call") { - const sites = this.calls.get(expr.callee) ?? []; + const sites = calls.get(expr.callee) ?? []; sites.push({ caller: fn, args: expr.args }); - this.calls.set(expr.callee, sites); + calls.set(expr.callee, sites); } const referenced = exprFunctionReference(expr); - if (referenced !== undefined) this.named.add(referenced); + if (referenced !== undefined) named.add(referenced); return true; }, }); } + return (this.census = { calls, named }); } mayBeStack(fn: IrFunction, key: IrExpr): boolean { @@ -71,7 +86,7 @@ class StackKeys { private keyMayBeStack(fn: IrFunction, key: IrExpr, visited: Set): boolean { if (key.kind === "strLit") return key.value === "stack"; - if (!this.spelled) return false; + if (!this.spellsStack()) return false; if (NON_STACK_KEY_TYPES.has(key.type.kind)) return false; if (key.kind === "toString" && NON_STACK_KEY_TYPES.has(key.operand.type.kind)) return false; if (key.kind === "dynFrom" && NON_STACK_KEY_TYPES.has(key.value.type.kind)) return false; @@ -93,8 +108,9 @@ class StackKeys { if (local === undefined || local.mutable) return true; const index = fn.params.findIndex((param) => param.localId === localId); if (index !== -1) { - const sites = this.calls.get(fn.name); - if (this.named.has(fn.name) || sites === undefined) return true; + const { calls, named } = this.callCensus(); + const sites = calls.get(fn.name); + if (named.has(fn.name) || sites === undefined) return true; return sites.some( (site) => site.args.length !== fn.params.length || diff --git a/packages/compiler/src/backend/target-diagnostics.ts b/packages/compiler/src/backend/target-diagnostics.ts index 3e97d0c10..236ce12cf 100644 --- a/packages/compiler/src/backend/target-diagnostics.ts +++ b/packages/compiler/src/backend/target-diagnostics.ts @@ -3,6 +3,7 @@ import type { LlvmUnsupportedError } from "./llvm/emitter.js"; import type { ScrDiagnostic } from "../diagnostics/diagnostic.js"; import { type IrModule, type SrcLoc } from "../ir/ir.js"; import { + DH_LIB_FNS, PKI_LIB_FNS, moduleUsesFetch, moduleUsesWebCryptoAsymmetric, @@ -232,9 +233,12 @@ export function moduleWasiUnavailableSurface( }; return false; } - if (node.fn === "crypto.dhNew" || node.fn === "crypto.dhGroupNew") { + if (DH_LIB_FNS.has(node.fn)) { found = { - surface: "DiffieHellman (the wasm32-wasi runtime does not include the mbedTLS library)", + surface: + node.fn === "crypto.dhNew" || node.fn === "crypto.dhGroupNew" + ? "DiffieHellman (the wasm32-wasi runtime does not include the mbedTLS library)" + : "checkPrime and generatePrime (the wasm32-wasi runtime does not include the mbedTLS library)", loc, }; return false; diff --git a/packages/compiler/src/contracts/check.ts b/packages/compiler/src/contracts/check.ts index c80468961..fe5dc171e 100644 --- a/packages/compiler/src/contracts/check.ts +++ b/packages/compiler/src/contracts/check.ts @@ -285,9 +285,10 @@ class ContractChecker { * the lifetimes of the objects they receive). Publication to other * threads needs no check: an instance carries a reference count of its * own until publication marks it like any other object it reaches - * (classes.ts programLifetimeRc). Static code has no WeakRef or - * FinalizationRegistry; weak collections stay correct because an entry - * keyed by an object that is never freed is never observable as removed. */ + * (classes.ts programLifetimeRc). Static code has no WeakRef, and a + * FinalizationRegistry never runs its cleanup callbacks; weak collections + * stay correct because an entry keyed by an object that is never freed is + * never observable as removed. */ private checkProgramLifetime(): void { if (this.notes.classes.size === 0) return; const roots: string[] = []; diff --git a/packages/compiler/src/contracts/int32-elements.ts b/packages/compiler/src/contracts/int32-elements.ts index 8284d40ab..ca61a8e49 100644 --- a/packages/compiler/src/contracts/int32-elements.ts +++ b/packages/compiler/src/contracts/int32-elements.ts @@ -122,7 +122,8 @@ const COPIES = new Set([ const COPY_IN = new Set(["pushSpread", "unshiftSpread", "spliceInsert"]); /** Library calls that only read array arguments. */ -const READ_ONLY_LIB = /^(console\.|inspect\.|json\.stringify|math\.|insp\.)/; +const READ_ONLY_LIB = + /^(console\.|inspect\.|json\.stringify|math\.|insp\.|array\.(hasProperties|namedKeys|hasNamedKey)$)/; /** Library calls that always throw (the value they stand for never exists). */ const THROWING_LIB = /^error\.\w*Throw$/; @@ -872,6 +873,9 @@ export class Int32ElementContracts { }; const isNumberArray = (t: IrType): boolean => t.kind === "array" && t.elem.kind === "f64"; + // Live views handed straight to a read-only library call (util.inspect + // of an array with properties) are only read through. + const readOnlyViews = new Set(); const expr = (e: IrExpr): void => { switch (e.kind) { case "assignExpr": @@ -1000,6 +1004,7 @@ export class Int32ElementContracts { case "libCall": if (!READ_ONLY_LIB.test(e.fn)) for (const a of e.args) escape(a, a.loc, `the library call ${e.fn}`); + else for (const a of e.args) if (a.kind === "dynFrom") readOnlyViews.add(a); break; case "callValue": for (const a of e.args) escape(a, a.loc, "a call through a function value"); @@ -1008,7 +1013,10 @@ export class Int32ElementContracts { case "dynFrom": // Plain arrays and records cross into dynamic code as deep copies; // class instances and live references stay connected. - if (e.liveRef === true || this.containsObject(e.value.type, new Set())) + if ( + !readOnlyViews.has(e) && + (e.liveRef === true || this.containsObject(e.value.type, new Set())) + ) escape(e.value, e.loc, "dynamic (any or unknown) code"); break; default: diff --git a/packages/compiler/src/coverage/report.ts b/packages/compiler/src/coverage/report.ts index 37f77f4c6..79017c97a 100644 --- a/packages/compiler/src/coverage/report.ts +++ b/packages/compiler/src/coverage/report.ts @@ -318,12 +318,23 @@ export function renderCoverage(input: CoverageInput, opts: CoverageRenderOptions // statements the module carries): they compile, but executing one // throws its SC-coded error — the report lists them so a "compiling" // JS program never hides its dynamic gaps. + // Fences in a module's top-level code run when the module initializes: + // they are listed first, as sites that run at startup. const fences = input.runtimeFences ?? []; - if (fences.length > 0) { + const startup = fences.filter((d) => d.startup === true); + const later = fences.filter((d) => d.startup !== true); + if (startup.length > 0) { out.push( - ` ${c(YELLOW, "deferred to runtime")} ${fences.length} site${fences.length === 1 ? "" : "s"} ${c(DIM, "(JS statements that throw their fence if executed)")}`, + ` ${c(YELLOW, "runs at startup")} ${startup.length} site${startup.length === 1 ? "" : "s"} ${c(DIM, "(JS statements in module top-level code, which runs when the module loads)")}`, ); - renderRows(groupBlockers(fences, "deferred"), YELLOW); + renderRows(groupBlockers(startup, "deferred"), YELLOW); + out.push(""); + } + if (later.length > 0) { + out.push( + ` ${c(YELLOW, "deferred to runtime")} ${later.length} site${later.length === 1 ? "" : "s"} ${c(DIM, "(JS statements that throw their fence if executed)")}`, + ); + renderRows(groupBlockers(later, "deferred"), YELLOW); out.push(""); } diff --git a/packages/compiler/src/diagnostics/diagnostic.ts b/packages/compiler/src/diagnostics/diagnostic.ts index 17cb2efc0..625ee93af 100644 --- a/packages/compiler/src/diagnostics/diagnostic.ts +++ b/packages/compiler/src/diagnostics/diagnostic.ts @@ -92,6 +92,10 @@ export interface ScrDiagnostic { * the error may not reproduce under the project's tsc. Machine-readable * output reports these under the "environment" category. */ typeWorld?: boolean; + /** A runtime fence in a module's top-level code, outside every function + * and class member body: it throws when the module initializes, which is + * program startup for a module the entry imports. */ + startup?: true; } /** SC3002–SC3004 — native-helper target, installation, and execution diff --git a/packages/compiler/src/diagnostics/envelope.ts b/packages/compiler/src/diagnostics/envelope.ts index 59c285ead..b4de8db77 100644 --- a/packages/compiler/src/diagnostics/envelope.ts +++ b/packages/compiler/src/diagnostics/envelope.ts @@ -67,6 +67,9 @@ export interface LocatedDiagnostic extends SourcePosition { hint: string; /** The id of the root-cause group this site belongs to. */ group: string; + /** Deferred scope only: the statement is module top-level code, so it + * runs when its module loads. */ + startup?: true; } export interface DiagnosticGroup { @@ -290,6 +293,7 @@ export function locateDiagnostic( message: diag.message, hint: hintOf(diag), group: groupId(diag, scope), + ...(scope === "deferred" && diag.startup === true ? { startup: true as const } : {}), }; } diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index 5c3904a91..7b7febc06 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -70,6 +70,7 @@ export interface EarlyExecutableNativeFeatures { dynTier?: boolean; streamDyn?: boolean; handleProps?: boolean; + declAdopt?: boolean; dynForAwait?: boolean; eventsOnceSignal?: boolean; dynBuffer?: boolean; diff --git a/packages/compiler/src/frontend/decl-projection.test.ts b/packages/compiler/src/frontend/decl-projection.test.ts new file mode 100644 index 000000000..dee695dd8 --- /dev/null +++ b/packages/compiler/src/frontend/decl-projection.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, test } from "vitest"; +import { applyEdits, isDeclBoundaryFile, isDeclBoundaryTypeName } from "./decl-projection.js"; + +describe("decl-projection", () => { + test("applyEdits closes the wrap that opened last first", () => { + // `x.y` wrapped from 0 (outer) and from 2 (inner), both closing at 5: + // closing parts rank by their wrap's negated start. + const out = applyEdits("x = a.b;", [ + { start: 4, end: 4, text: "(" }, + { start: 7, end: 7, text: " as Outer)", rank: -4 }, + { start: 6, end: 6, text: "(" }, + { start: 7, end: 7, text: " as Inner)", rank: -6 }, + ]); + expect(out).toBe("x = (a.(b as Inner) as Outer);"); + }); + + test("applyEdits keeps the first of overlapping replacements and every line", () => { + const text = "type A = z.infer;\nconst x = 1;"; + const out = applyEdits(text, [ + { start: 9, end: 26, text: "__scriptc_decl_0" }, + { start: 11, end: 16, text: "nope" }, + ]); + expect(out).toBe("type A = __scriptc_decl_0;\nconst x = 1;"); + }); + + test("names", () => { + expect(isDeclBoundaryTypeName("__scriptc_decl_3")).toBe(true); + expect(isDeclBoundaryTypeName("Product")).toBe(false); + expect(isDeclBoundaryFile("/app/src/__scriptc_decl_boundary__.d.ts")).toBe(true); + expect(isDeclBoundaryFile("/app/src/types.d.ts")).toBe(false); + }); +}); diff --git a/packages/compiler/src/frontend/decl-projection.ts b/packages/compiler/src/frontend/decl-projection.ts new file mode 100644 index 000000000..f2340e984 --- /dev/null +++ b/packages/compiler/src/frontend/decl-projection.ts @@ -0,0 +1,857 @@ +/* The declaration-typed boundary's projection pass. + * + * An opted-in npm package compiles from its JavaScript, and the program the + * lowering compiles types it from that JavaScript: where the package's + * published declarations are richer (zod's `z.infer`), user code sees `any`. + * This pass asks the DECLARATION world (the same entry checked against the + * packages' declarations) for the types of those user-code positions. Where + * the declared type is pure data, it is printed as structural TypeScript into + * a generated global declaration file, and the user file is rewritten in the + * lowering world: + * + * - type positions that name package types (`z.infer`) name the + * projected alias instead; + * - package-produced expressions (`S.parse(x)`, `r.data`) are wrapped in + * `( as __scriptc_decl_N)`, the boundary cast; + * - untyped parameters of user callbacks the package calls gain the alias. + * + * The lowering world is then rebuilt from the rewritten text, so the + * checker's own inference carries the projected types through the rest of + * the user code, and the lowering sees an ordinary checked cast at each + * crossing (lowered with `adopt`, see narrowing.ts). No edit adds or removes + * a line break, so positions keep their lines. IR records are structural, + * so a printed type interns to the same shape as any equal type. + * + * Only programs whose user code has such positions are rewritten; every + * other program is untouched (the zero-cost rule). See + * scriptc-perf/results/design-decl-boundary/DESIGN.md. */ + +import * as ts from "./ts7/adapter.js"; + +/** Alias names of projected types start with this prefix; the lowering + * recognizes boundary casts and parameters by it. */ +export const DECL_BOUNDARY_PREFIX = "__scriptc_decl_"; + +/** The generated declaration file's base name. */ +export const DECL_BOUNDARY_FILE = "__scriptc_decl_boundary__.d.ts"; + +export function isDeclBoundaryFile(fileName: string): boolean { + return fileName.split("\\").join("/").endsWith(`/${DECL_BOUNDARY_FILE}`); +} + +export function isDeclBoundaryTypeName(name: string): boolean { + return name.startsWith(DECL_BOUNDARY_PREFIX); +} + +/** True for `T` in `x as T` / `(p: T) =>` when T names a projected alias. */ +export function isDeclBoundaryTypeNode(node: ts.TypeNode | undefined): boolean { + return ( + node !== undefined && + ts.isTypeReferenceNode(node) && + ts.isIdentifier(node.typeName) && + isDeclBoundaryTypeName(node.typeName.text) && + node.typeArguments === undefined + ); +} + +interface Edit { + start: number; + end: number; + text: string; + /** Tie-break among edits at one position (lower first). */ + rank?: number; +} + +export interface DeclProjection { + /** Rewritten text per user file. */ + files: Map; + /** The generated declaration file (path and text). */ + declFile: string; + declText: string; + /** Human-readable plan (SCRIPTC_DEBUG_DECL_BOUNDARY). */ + plan: string[]; +} + +export interface DeclProjectionInput { + jsProgram: ts.Program; + declProgram: ts.Program; + /** User source files to consider (TypeScript, outside node_modules). */ + userFiles: readonly string[]; + /** Directory of the generated declaration file. */ + outDir: string; +} + +function isPackageDeclarationFile(sf: ts.SourceFile | undefined): boolean { + if (sf === undefined || !sf.isDeclarationFile) return false; + const f = sf.fileName.split("\\").join("/"); + // The TypeScript standard library ships inside node_modules too. + if (/(^|\/)lib(\.[\w.-]+)?\.d\.ts$/.test(f) || f.startsWith("bundled:")) return false; + return ( + f.includes("/node_modules/") && + !f.includes("/node_modules/@types/node/") && + !f.includes("/node_modules/typescript/") && + !f.includes("/node_modules/@typescript/") + ); +} + +function declaredInPackage(symbol: ts.Symbol | undefined): boolean { + if (symbol === undefined) return false; + return symbol.declarations.some((d) => isPackageDeclarationFile(ts.getSourceFileOfNode(d))); +} + +/** The parts of a call signature's declaration that pass-through detection + * reads, for the declaration kinds a resolved call signature has. */ +function signatureParts(node: ts.Node | undefined): { + typeParameters: readonly ts.TypeParameterDeclaration[]; + parameters: readonly ts.ParameterDeclaration[]; + type: ts.TypeNode | undefined; +} | null { + if (node === undefined) return null; + if (ts.isFunctionTypeNode(node)) + return { + typeParameters: node.typeParameters?.nodes ?? [], + parameters: node.parameters.nodes, + type: node.type, + }; + if (ts.isFunctionDeclaration(node)) + return { + typeParameters: node.typeParameters?.nodes ?? [], + parameters: node.parameters.nodes, + type: node.type, + }; + if (ts.isMethodDeclaration(node)) + return { + typeParameters: node.typeParameters?.nodes ?? [], + parameters: node.parameters.nodes, + type: node.type, + }; + if (ts.isMethodSignatureDeclaration(node)) + return { + typeParameters: node.typeParameters?.nodes ?? [], + parameters: node.parameters.nodes, + type: node.type, + }; + if (ts.isCallSignatureDeclaration(node)) + return { + typeParameters: node.typeParameters?.nodes ?? [], + parameters: node.parameters.nodes, + type: node.type, + }; + return null; +} + +/** A signature that returns one of its own type parameters which also types + * a parameter (`(obj: T) => T`) hands back the argument + * or something standing in for it, a proxy or a marked object, whose + * identity the package may track (Effect's `Equal.byReference`). Its result + * type comes from the caller's argument, not from data the package + * declares, so the boundary leaves it alone: converting it would build a + * separate native value. */ +function passesArgumentThrough(declaration: ts.Node | undefined): boolean { + const sig = signatureParts(declaration); + if (sig === null || sig.type === undefined || !ts.isTypeReferenceNode(sig.type)) return false; + const ret = sig.type; + if (!ts.isIdentifier(ret.typeName) || ret.typeArguments !== undefined) return false; + const name = ret.typeName.text; + if (!sig.typeParameters.some((tp) => tp.name.text === name)) return false; + return sig.parameters.some( + (param) => + param.type !== undefined && + ts.isTypeReferenceNode(param.type) && + ts.isIdentifier(param.type.typeName) && + param.type.typeName.text === name, + ); +} + +function declaredInUserFile(symbol: ts.Symbol | undefined): boolean { + if (symbol === undefined) return false; + const decls = symbol.declarations; + return ( + decls.length > 0 && + decls.every((d) => { + const sf = ts.getSourceFileOfNode(d); + return !sf.isDeclarationFile && !sf.fileName.split("\\").join("/").includes("/node_modules/"); + }) + ); +} + +const ANYISH = ts.TypeFlags.Any | ts.TypeFlags.Unknown; +const READONLY_CHECK_FLAG = 8; + +/** Prints declaration-world types as structural TypeScript, or answers null + * when a type is not pure data (DESIGN.md §4.3). */ +class TypePrinter { + readonly aliases = new Map(); + readonly declarations: string[] = []; + private readonly inProgress = new Map(); + + constructor(private readonly checker: ts.TypeChecker) {} + + /** The alias naming a projectable type, or null. Top-level any/unknown + * never projects. */ + project(type: ts.Type): string | null { + if (type.flags & ANYISH) return null; + const text = this.print(type, 0); + if (text === null || text === "unknown") return null; + let name = this.aliases.get(text); + if (name === undefined) { + name = `${DECL_BOUNDARY_PREFIX}${this.aliases.size}`; + this.aliases.set(text, name); + this.declarations.push(`type ${name} = ${text};`); + } + return name; + } + + private print(type: ts.Type, depth: number): string | null { + if (depth > 24) return null; + const f = type.flags; + if (f & ANYISH) return "unknown"; + if (f & ts.TypeFlags.String) return "string"; + if (f & ts.TypeFlags.Number) return "number"; + if (f & ts.TypeFlags.Boolean) return "boolean"; + if (f & ts.TypeFlags.Null) return "null"; + if (f & ts.TypeFlags.Undefined) return "undefined"; + if (f & ts.TypeFlags.EnumLiteral || f & ts.TypeFlags.Enum) return null; + if (f & ts.TypeFlags.BooleanLiteral) return type.intrinsicName === "true" ? "true" : "false"; + if (f & ts.TypeFlags.StringLiteral) return JSON.stringify(String(type.value)); + if (f & ts.TypeFlags.NumberLiteral) { + const v = Number(type.value); + return Number.isFinite(v) ? String(v) : null; + } + if (type.isUnionType()) { + const parts: string[] = []; + for (const arm of type.getTypes()) { + const p = this.print(arm, depth + 1); + if (p === null || p === "unknown") return null; + if (!parts.includes(p)) parts.push(p); + } + // `true | false` reads better as boolean, and maps the same. + const t = parts.indexOf("true"); + const fl = parts.indexOf("false"); + if (t >= 0 && fl >= 0) { + parts.splice(Math.max(t, fl), 1); + parts.splice(Math.min(t, fl), 1, "boolean"); + } + return parts.map((p) => (/=>/.test(p) ? `(${p})` : p)).join(" | "); + } + if (type.isIntersectionType()) { + // Brands: drop members that only carry unique-symbol keys. + const kept = type.getTypes().filter((m) => !this.isBrandOnly(m)); + if (kept.length === 1) return this.print(kept[0]!, depth + 1); + if (kept.length === 0) return null; + if (!kept.every((m) => (m.flags & ts.TypeFlags.Object) !== 0)) return null; + return this.printObject(type, depth); + } + if (f & ts.TypeFlags.Object) return this.printObject(type, depth); + return null; + } + + private isBrandOnly(type: ts.Type): boolean { + if (!(type.flags & ts.TypeFlags.Object) || type.isIntersectionType()) return false; + const props = this.checker.getPropertiesOfType(type); + return props.length > 0 && props.every((p) => String(p.escapedName).startsWith("__@")); + } + + private printObject(type: ts.Type, depth: number): string | null { + const checker = this.checker; + if (checker.isArrayType(type)) { + const [elem] = checker.getTypeArguments(type); + if (elem === undefined) return null; + const e = this.print(elem, depth + 1); + if (e === null) return null; + const ro = type.getSymbol()?.escapedName === "ReadonlyArray" ? "readonly " : ""; + return `${ro}(${e})[]`; + } + if (checker.isTupleType(type)) { + const target = type.getTarget() ?? type; + const flags = target.elementFlags ?? []; + if (flags.some((fl) => fl !== ts.ElementFlags.Required)) return null; + const elems = checker.getTypeArguments(type).slice(0, flags.length); + const out: string[] = []; + for (const e of elems) { + const p = this.print(e, depth + 1); + if (p === null) return null; + out.push(p); + } + return `${target.readonly === true ? "readonly " : ""}[${out.join(", ")}]`; + } + const symbol = type.getSymbol(); + if (symbol !== undefined) { + if (symbol.flags & (ts.SymbolFlags.Class | ts.SymbolFlags.Function | ts.SymbolFlags.Method)) + return null; + // Interfaces: only user-declared ones describe plain data reliably + // (package and lib interfaces often describe class instances). + if (symbol.flags & ts.SymbolFlags.Interface && !declaredInUserFile(symbol)) return null; + } + if ( + checker.getCallSignatures(type).length > 0 || + checker.getConstructSignatures(type).length > 0 + ) + return null; + const memo = this.inProgress.get(type.id); + if (memo !== undefined) { + memo.name ??= `${DECL_BOUNDARY_PREFIX}r${type.id}`; + return memo.name; + } + const frame: { name: string | null } = { name: null }; + this.inProgress.set(type.id, frame); + try { + const members: string[] = []; + let readonlyCount = 0; + const props = checker.getPropertiesOfType(type); + for (const prop of props) { + const key = String(prop.escapedName); + if (key.startsWith("__@")) continue; // symbol-keyed (brands) + if ( + prop.flags & + (ts.SymbolFlags.Method | ts.SymbolFlags.GetAccessor | ts.SymbolFlags.SetAccessor) + ) + return null; + const propType = checker.getTypeOfSymbol(prop); + const optional = (prop.flags & ts.SymbolFlags.Optional) !== 0; + let p = this.print(propType, depth + 1); + if (p === null) return null; + const readonly = (prop.checkFlags & READONLY_CHECK_FLAG) !== 0 || hasReadonlyModifier(prop); + if (readonly) readonlyCount++; + const name = /^[A-Za-z_$][\w$]*$/.test(key) ? key : JSON.stringify(key); + if (optional && !/\bundefined\b/.test(p)) p = `${p} | undefined`; + members.push(`${readonly ? "readonly " : ""}${name}${optional ? "?" : ""}: ${p}`); + } + for (const info of checker.getIndexInfosOfType(type)) { + if (!(info.keyType.flags & ts.TypeFlags.String)) return null; + const v = this.print(info.type, depth + 1); + if (v === null) return null; + members.push(`[key: string]: ${v}`); + } + if (members.length === 0) return null; // `{}` admits any non-nullish value + // All-readonly object types may describe frozen values (.readonly()). + if (props.length > 0 && readonlyCount === props.length) return null; + const text = `{ ${members.join("; ")} }`; + if (frame.name !== null) { + this.declarations.push(`type ${frame.name} = ${text};`); + return frame.name; + } + return text; + } finally { + this.inProgress.delete(type.id); + } + } +} + +function hasReadonlyModifier(prop: ts.Symbol): boolean { + return prop.declarations.some( + (d) => + (ts.isPropertySignature(d) || ts.isPropertyDeclaration(d)) && + (d.modifiers?.nodes ?? []).some((m) => m.kind === ts.SyntaxKind.ReadonlyKeyword), + ); +} + +/** Children of a node in source order (forEachChild order). */ +function childrenOf(node: ts.Node): ts.Node[] { + const out: ts.Node[] = []; + ts.forEachChild( + node, + (c) => { + out.push(c); + return undefined; + }, + (list) => { + for (const c of list.nodes) out.push(c); + return undefined; + }, + ); + return out; +} + +function isWriteTarget(node: ts.Node): boolean { + let child = node; + let parent = node.parent; + while (parent !== undefined && ts.isParenthesizedExpression(parent)) { + child = parent; + parent = parent.parent; + } + if (parent === undefined) return false; + if (ts.isBinaryExpression(parent) && parent.left === child) { + const k = parent.operatorToken.kind; + return k >= ts.SyntaxKind.FirstAssignment && k <= ts.SyntaxKind.LastAssignment; + } + if (ts.isPrefixUnaryExpression(parent) || ts.isPostfixUnaryExpression(parent)) { + const op = parent.operator; + return op === ts.SyntaxKind.PlusPlusToken || op === ts.SyntaxKind.MinusMinusToken; + } + if (ts.isDeleteExpression(parent)) return true; + if (ts.isForOfStatement(parent) || ts.isForInStatement(parent)) + return parent.initializer === child; + if (ts.isShorthandPropertyAssignment(parent)) return true; + if ( + ts.isArrayLiteralExpression(parent) || + ts.isPropertyAssignment(parent) || + ts.isSpreadElement(parent) + ) { + // Destructuring assignment targets: conservative, skip. + let up: ts.Node | undefined = parent; + while ( + up !== undefined && + (ts.isArrayLiteralExpression(up) || + ts.isObjectLiteralExpression(up) || + ts.isPropertyAssignment(up) || + ts.isSpreadElement(up)) + ) + up = up.parent; + if ( + up !== undefined && + ts.isBinaryExpression(up) && + up.operatorToken.kind === ts.SyntaxKind.EqualsToken + ) + return true; + } + return false; +} + +function inOptionalChain(node: ts.Node): boolean { + const parent = node.parent; + if (parent === undefined) return false; + if ( + (ts.isPropertyAccessExpression(parent) || + ts.isElementAccessExpression(parent) || + ts.isCallExpression(parent)) && + parent.expression === node && + (parent.questionDotToken !== undefined || (parent.flags & ts.NodeFlags.OptionalChain) !== 0) + ) + return true; + return ( + (ts.isPropertyAccessExpression(node) || + ts.isElementAccessExpression(node) || + ts.isCallExpression(node)) && + (node.flags & ts.NodeFlags.OptionalChain) !== 0 + ); +} + +function isCallee(node: ts.Node): boolean { + const p = node.parent; + return ( + p !== undefined && + (ts.isCallExpression(p) || ts.isNewExpression(p) || ts.isTaggedTemplateExpression(p)) && + (ts.isTaggedTemplateExpression(p) ? p.tag === node : p.expression === node) + ); +} + +function inTypeContext(node: ts.Node): boolean { + for (let p = node.parent; p !== undefined; p = p.parent) { + if (ts.isTypeNode(p) || ts.isTypeQueryNode(p)) return true; + if (ts.isExpressionWithTypeArguments(p) && p.parent && ts.isHeritageClause(p.parent)) + return true; + if (ts.isStatement(p)) return false; + } + return false; +} + +function unwrapTransparent(node: ts.Expression): ts.Expression { + let e = node; + while (ts.isParenthesizedExpression(e) || ts.isNonNullExpression(e) || ts.isAwaitExpression(e)) + e = e.expression; + return e; +} + +/** Plans the projection for one program pair. Answers null when nothing + * in the user files projects (the program stays as it is). */ +export function planDeclProjection(input: DeclProjectionInput): DeclProjection | null { + const jsChecker = input.jsProgram.getTypeChecker(); + const declChecker = input.declProgram.getTypeChecker(); + const printer = new TypePrinter(declChecker); + const plan: string[] = []; + const edits = new Map(); + + const typeAt = (checker: ts.TypeChecker, node: ts.Node): ts.Type | null => { + try { + return checker.getTypeAtLocation(node); + } catch { + return null; + } + }; + const isAnyish = (t: ts.Type | null): boolean => t !== null && (t.flags & ANYISH) !== 0; + const resolveAlias = (s: ts.Symbol | undefined): ts.Symbol | undefined => { + if (s === undefined || !(s.flags & ts.SymbolFlags.Alias)) return s; + try { + return declChecker.getAliasedSymbol(s); + } catch { + return s; + } + }; + const declSymbol = (n: ts.Node): ts.Symbol | undefined => { + try { + return resolveAlias(declChecker.getSymbolAtLocation(n)); + } catch { + return undefined; + } + }; + const namesPackageSymbol = (node: ts.Node): boolean => { + let found = false; + const visit = (n: ts.Node): void => { + if (found) return; + if (ts.isIdentifier(n) && declaredInPackage(declSymbol(n))) { + found = true; + return; + } + for (const c of childrenOf(n)) visit(c); + }; + visit(node); + return found; + }; + + for (const file of input.userFiles) { + const jsSf = input.jsProgram.getSourceFile(file); + const declSf = input.declProgram.getSourceFile(file); + if (jsSf === undefined || declSf === undefined || jsSf.text !== declSf.text) continue; + const fileEdits: Edit[] = []; + /** Candidate expressions (by js node) with their alias. */ + const exprCandidates = new Map(); + const exprOrder: ts.Node[] = []; + + const considerType = (jsNode: ts.Node, declNode: ts.Node): boolean => { + if ( + !ts.isTypeReferenceNode(jsNode) && + !ts.isIndexedAccessTypeNode(jsNode) && + !ts.isExpressionWithTypeArguments(jsNode) + ) + return false; + if (ts.isExpressionWithTypeArguments(jsNode)) { + if (!jsNode.parent || !ts.isHeritageClause(jsNode.parent)) return false; + if (!jsNode.parent.parent || !ts.isInterfaceDeclaration(jsNode.parent.parent)) return false; + } + let jsType: ts.Type | null; + let declType: ts.Type | null; + try { + jsType = ts.isExpressionWithTypeArguments(jsNode) + ? jsChecker.getTypeAtLocation(jsNode) + : jsChecker.getTypeFromTypeNode(jsNode as ts.TypeNode); + declType = ts.isExpressionWithTypeArguments(declNode) + ? declChecker.getTypeAtLocation(declNode) + : declChecker.getTypeFromTypeNode(declNode as ts.TypeNode); + } catch { + return false; + } + if (!isAnyish(jsType) || declType === null) return false; + if (!namesPackageSymbol(declNode)) return false; + const alias = printer.project(declType); + if (alias === null) return false; + const start = ts.tokenStartOfNode(jsNode); + fileEdits.push({ start, end: jsNode.end, text: alias }); + plan.push(`${file}:${start}: type ${jsSf.text.slice(start, jsNode.end)} -> ${alias}`); + return true; + }; + + const considerExpr = (jsNode: ts.Node, declNode: ts.Node): void => { + if (!ts.isExpression(jsNode)) return; + const isCall = + ts.isCallExpression(jsNode) || + ts.isNewExpression(jsNode) || + ts.isTaggedTemplateExpression(jsNode); + const isAccess = + ts.isPropertyAccessExpression(jsNode) || ts.isElementAccessExpression(jsNode); + const isAwait = ts.isAwaitExpression(jsNode); + if (!isCall && !isAccess && !isAwait && !ts.isIdentifier(jsNode)) return; + if (inTypeContext(jsNode) || isCallee(jsNode) || isWriteTarget(jsNode)) return; + if (inOptionalChain(jsNode)) return; + const parent = jsNode.parent; + if (parent !== undefined && ts.isExpressionStatement(parent)) return; + if ( + parent !== undefined && + (ts.isImportSpecifier(parent) || + ts.isImportClause(parent) || + ts.isNamespaceImport(parent) || + ts.isExportSpecifier(parent)) + ) + return; + if (ts.isIdentifier(jsNode)) { + if (parent !== undefined && ts.isPropertyAccessExpression(parent) && parent.name === jsNode) + return; + } + const jsType = typeAt(jsChecker, jsNode); + if (!isAnyish(jsType)) return; + // Package-produced? + const d = declNode as ts.Expression; + let produced = false; + if (ts.isCallExpression(d) || ts.isNewExpression(d)) { + const callee = d.expression; + const target = ts.isPropertyAccessExpression(callee) ? callee.name : callee; + let decl: ts.Node | undefined; + try { + decl = declChecker.getResolvedSignature(d)?.declaration; + } catch { + decl = undefined; + } + produced = + (declaredInPackage(declSymbol(target)) || + (decl !== undefined && isPackageDeclarationFile(ts.getSourceFileOfNode(decl)))) && + !passesArgumentThrough(decl); + } else if (ts.isTaggedTemplateExpression(d)) { + produced = declaredInPackage(declSymbol(d.tag)); + } else if (ts.isPropertyAccessExpression(d) || ts.isElementAccessExpression(d)) { + const recvDecl = typeAt(declChecker, d.expression); + const recvJs = typeAt(jsChecker, (jsNode as ts.PropertyAccessExpression).expression); + const recvProjectable = recvDecl !== null && printer.project(recvDecl) !== null; + produced = !recvProjectable || (recvJs !== null && !isAnyish(recvJs)); + } else if (ts.isAwaitExpression(d)) { + const inner = unwrapTransparent(d.expression); + produced = + (ts.isCallExpression(inner) || ts.isPropertyAccessExpression(inner)) && + (declaredInPackage( + declSymbol( + ts.isCallExpression(inner) + ? ts.isPropertyAccessExpression(inner.expression) + ? inner.expression.name + : inner.expression + : inner.name, + ), + ) || + isAnyish(typeAt(jsChecker, inner))); + } else if (ts.isIdentifier(d)) { + const raw = (() => { + try { + return declChecker.getSymbolAtLocation(d); + } catch { + return undefined; + } + })(); + produced = + raw !== undefined && + (raw.flags & ts.SymbolFlags.Alias) !== 0 && + declaredInPackage(resolveAlias(raw)); + } + if (!produced) return; + // A consumer that takes anything needs no conversion. + try { + const ctx = declChecker.getContextualType(d); + if (ctx !== undefined && ctx.flags & ANYISH) return; + } catch { + // no contextual type + } + const declType = typeAt(declChecker, d); + if (declType === null) return; + const alias = printer.project(declType); + if (alias === null) return; + exprCandidates.set(jsNode, alias); + exprOrder.push(jsNode); + }; + + const annotated = new Set(); + const considerParam = (jsNode: ts.Node, declNode: ts.Node): void => { + if (!ts.isParameter(jsNode) || jsNode.type !== undefined || !ts.isIdentifier(jsNode.name)) + return; + const fn = jsNode.parent; + if (fn === undefined || !(ts.isArrowFunction(fn) || ts.isFunctionExpression(fn))) return; + if (jsNode.dotDotDotToken !== undefined) return; + const jsType = typeAt(jsChecker, jsNode.name); + if (!isAnyish(jsType)) return; + const declType = typeAt(declChecker, (declNode as ts.ParameterDeclaration).name); + if (declType === null) return; + // Only parameters the package's declarations type (a callback the + // package calls), not ones the user typed elsewhere. + try { + const ctxFn = declChecker.getContextualType(declNode.parent as ts.Expression); + if (ctxFn === undefined) return; + const sigs = declChecker.getCallSignatures(ctxFn); + if ( + !sigs.some( + (s) => + s.declaration !== undefined && + isPackageDeclarationFile(ts.getSourceFileOfNode(s.declaration)), + ) + ) + return; + } catch { + return; + } + const alias = printer.project(declType); + if (alias === null) return; + annotated.add(jsNode); + const name = jsNode.name; + const nameStart = ts.tokenStartOfNode(name); + const text = jsSf.text; + // `x => ...` or `async x => ...`: nothing but the modifiers comes + // before the parameter, which then needs parentheses for its type. + const head = ts.isArrowFunction(fn) + ? ((ts.getModifiers(fn) ?? []).at(-1)?.end ?? ts.tokenStartOfNode(fn)) + : nameStart; + const bareArrow = + ts.isArrowFunction(fn) && + fn.parameters.nodes.length === 1 && + text.slice(head, nameStart).trim() === ""; + const afterName = jsNode.questionToken !== undefined ? jsNode.questionToken.end : name.end; + if (bareArrow) { + fileEdits.push({ start: nameStart, end: nameStart, text: "(" }); + fileEdits.push({ start: afterName, end: afterName, text: `: ${alias})` }); + } else { + fileEdits.push({ start: afterName, end: afterName, text: `: ${alias}` }); + } + plan.push(`${file}:${nameStart}: parameter ${name.text}: ${alias}`); + }; + + const walk = (jsNode: ts.Node, declNode: ts.Node): void => { + if (jsNode.kind !== declNode.kind) return; + if (ts.isTypeNode(jsNode) || ts.isExpressionWithTypeArguments(jsNode)) { + if (considerType(jsNode, declNode)) return; + } else { + considerParam(jsNode, declNode); + considerExpr(jsNode, declNode); + } + const a = childrenOf(jsNode); + const b = childrenOf(declNode); + if (a.length !== b.length) return; + for (let i = 0; i < a.length; i++) walk(a[i]!, b[i]!); + }; + walk(jsSf, declSf); + + // Suppress candidates whose receiver chain another candidate types. + const symbolTyped = new Map(); + const willBeTyped = (e: ts.Node, depth = 0): boolean => { + if (depth > 32) return false; + if (exprCandidates.has(e)) return true; + if (ts.isParenthesizedExpression(e) || ts.isNonNullExpression(e) || ts.isAwaitExpression(e)) + return willBeTyped(e.expression, depth + 1); + if (ts.isAsExpression(e)) return false; + if (ts.isPropertyAccessExpression(e) || ts.isElementAccessExpression(e)) + return willBeTyped(e.expression, depth + 1); + if (ts.isIdentifier(e)) { + let sym: ts.Symbol | undefined; + try { + sym = jsChecker.getSymbolAtLocation(e); + } catch { + return false; + } + if (sym === undefined) return false; + const known = symbolTyped.get(sym); + if (known !== undefined) return known; + symbolTyped.set(sym, false); + let result = false; + const decls = sym.declarations; + if (decls.length === 1) { + const decl = decls[0]!; + if ( + ts.isVariableDeclaration(decl) && + decl.initializer !== undefined && + decl.type === undefined + ) + result = willBeTyped(decl.initializer, depth + 1); + else if (ts.isParameter(decl) && annotated.has(decl)) result = true; + else if (ts.isBindingElement(decl)) result = false; + } + symbolTyped.set(sym, result); + return result; + } + return false; + }; + const receiverOf = (e: ts.Node): ts.Node | null => { + if (ts.isPropertyAccessExpression(e) || ts.isElementAccessExpression(e)) return e.expression; + if (ts.isIdentifier(e)) return null; + return null; + }; + const kept: ts.Node[] = []; + for (const e of exprOrder) { + const recv = receiverOf(e); + if (recv !== null && willBeTyped(recv)) continue; + // An identifier candidate that is a user binding never gets here + // (only package imports are candidates). + kept.push(e); + } + // Nested candidates: keep only the innermost (an outer one is typed by + // the inner wrap). + const keptSet = new Set(kept); + const containsKept = (outer: ts.Node): boolean => { + let found = false; + const visit = (n: ts.Node): void => { + if (found) return; + for (const c of childrenOf(n)) { + if (keptSet.has(c)) { + found = true; + return; + } + visit(c); + } + }; + visit(outer); + return found; + }; + for (const e of kept) { + if ( + !ts.isAwaitExpression(e) && + containsKept(e) && + (ts.isPropertyAccessExpression(e) || ts.isElementAccessExpression(e)) + ) { + // `a.b.c` with `a.b` wrapped: the outer access reads typed data. + const recv = receiverOf(e); + if ( + recv !== null && + (keptSet.has(unwrapTransparent(recv as ts.Expression)) || willBeTyped(recv)) + ) + continue; + } + const alias = exprCandidates.get(e)!; + const start = ts.tokenStartOfNode(e); + const stmt = e.parent; + let prefix = "("; + // A statement that would start with `(` after a line without a + // semicolon continues the previous expression: guard it. + let up: ts.Node | undefined = e; + while (up?.parent !== undefined && !ts.isStatement(up.parent)) up = up.parent; + // Only a statement in a statement list can follow such a line; an + // unbraced body (`for (...) S.parse(x).run()`) follows its header, + // and a `;` there would become the body. + const list = up?.parent?.parent; + if ( + up?.parent !== undefined && + ts.isExpressionStatement(up.parent) && + ts.tokenStartOfNode(up.parent) === start && + list !== undefined && + (ts.isBlock(list) || + ts.isSourceFile(list) || + ts.isModuleBlock(list) || + ts.isCaseClause(list) || + ts.isDefaultClause(list)) + ) + prefix = ";("; + void stmt; + fileEdits.push({ start, end: start, text: prefix }); + // Closing parts at one position close inner wraps first. + fileEdits.push({ start: e.end, end: e.end, text: ` as ${alias})`, rank: -start }); + plan.push( + `${file}:${start}: cast ${jsSf.text.slice(start, Math.min(e.end, start + 60)).replace(/\s+/g, " ")} as ${alias}`, + ); + } + + if (fileEdits.length === 0) continue; + edits.set(file, fileEdits); + } + + if (edits.size === 0) return null; + const files = new Map(); + for (const [file, list] of edits) { + const sf = input.jsProgram.getSourceFile(file)!; + files.set(file, applyEdits(sf.text, list)); + } + const declFile = `${input.outDir.split("\\").join("/")}/${DECL_BOUNDARY_FILE}`; + const declText = + "// Generated by scriptc: types projected from npm package declarations.\n" + + printer.declarations.join("\n") + + "\n"; + return { files, declFile, declText, plan }; +} + +/** Applies insertions and replacements; overlapping replacements keep the + * first. Insertions at one position keep their order. */ +export function applyEdits(text: string, edits: readonly Edit[]): string { + const sorted = edits + .map((e, i) => ({ ...e, i })) + .sort( + (a, b) => a.start - b.start || a.end - b.end || (a.rank ?? 0) - (b.rank ?? 0) || a.i - b.i, + ); + let out = ""; + let at = 0; + for (const e of sorted) { + if (e.start < at) continue; // overlaps an earlier replacement + out += text.slice(at, e.start) + e.text; + at = e.end; + } + return out + text.slice(at); +} diff --git a/packages/compiler/src/frontend/dts-paths.ts b/packages/compiler/src/frontend/dts-paths.ts index 3b2f32413..009d1c333 100644 --- a/packages/compiler/src/frontend/dts-paths.ts +++ b/packages/compiler/src/frontend/dts-paths.ts @@ -34,6 +34,31 @@ export function packageShimRoot(): string { : join(installedDeclarationRoot, "shims"); } +/** The directories of files scriptc itself ships, each with the prefix + * that names its files independently of where scriptc is installed + * (compilerOwnedFileName). IR artifacts spell source locations in them this + * way (serializeModuleParts). */ +export function compilerOwnedFileRoots(): readonly { from: string; to: string }[] { + return [{ from: tsgoPath(packageShimRoot()) + "/", to: "scriptc:shims/" }]; +} + +/** The name of a file scriptc itself ships (a package shim or one of the + * shipped declaration files) that does not depend on where scriptc is + * installed: `scriptc:shims/` or + * `scriptc:`. Null for every other file. Names and + * artifacts that would otherwise spell the installation's absolute path use + * it, so two installations of one compiler emit the same output. */ +export function compilerOwnedFileName(file: string): string | null { + const path = tsgoPath(file); + for (const { from, to } of compilerOwnedFileRoots()) { + if (path.startsWith(from)) return to + path.slice(from.length); + } + for (const declaration of [ambientDtsPath(), overridesDtsPath(), fallbackDtsPath()]) { + if (path === declaration) return `scriptc:${path.slice(path.lastIndexOf("/") + 1)}`; + } + return null; +} + /** Path of the shipped ambient declarations — the always-shipped CORE * (comptime, setTimeout). Part of EVERY program scriptc * builds, the project-world preflight program included. */ diff --git a/packages/compiler/src/frontend/input-tracker.test.ts b/packages/compiler/src/frontend/input-tracker.test.ts index 4b6ef6d90..b4f3957f5 100644 --- a/packages/compiler/src/frontend/input-tracker.test.ts +++ b/packages/compiler/src/frontend/input-tracker.test.ts @@ -60,6 +60,28 @@ test("semantic validation reads non-source dependencies once and still invalidat expect(reads.mock.calls.filter(([path]) => path === dependency)).toHaveLength(1); }); +test("an equivalent edit of a file whose raw text the frontend read does not match", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-inputs-")); + scratch.push(dir); + const source = join(dir, "entry.ts"); + const previous = "export const answer = 1;\n"; + await writeFile(source, previous); + const tracker = new FrontendInputTracker(); + tracker.run(() => { + trackedReadFile(source); + tracker.record({ op: "text", path: source }); + }); + const snapshot = tracker.snapshot(); + expect(validFrontendInputSnapshot(snapshot)).toBe(true); + expect(frontendInputsStillMatch(snapshot)).toBe(true); + await writeFile(source, `// comment\n${previous}`); + const equivalent = vi.fn(() => true); + expect( + frontendInputsSemanticallyMatch(snapshot, new Map([[source, previous]]), equivalent), + ).toBeNull(); + expect(equivalent).not.toHaveBeenCalled(); +}); + test("tracked frontend reads invalidate on byte edits", async () => { const dir = await mkdtemp(join(tmpdir(), "scriptc-inputs-")); scratch.push(dir); diff --git a/packages/compiler/src/frontend/input-tracker.ts b/packages/compiler/src/frontend/input-tracker.ts index fa2e92568..d27ab6285 100644 --- a/packages/compiler/src/frontend/input-tracker.ts +++ b/packages/compiler/src/frontend/input-tracker.ts @@ -2,6 +2,7 @@ import { createHash } from "node:crypto"; import { AsyncLocalStorage } from "node:async_hooks"; import { readFileSync, readdirSync, realpathSync, statSync } from "node:fs"; import { basename, dirname, resolve } from "node:path"; +import { observeSourceText } from "./ts7/ast-node.js"; /** * Filesystem observations made while constructing and lowering one frontend @@ -16,7 +17,10 @@ export type FrontendInputProbe = | { op: "kind"; path: string; kind: "file" | "directory" | "other" | "missing" } | { op: "entries"; path: string; files: string[]; directories: string[] } | { op: "entries-error"; path: string } - | { op: "realpath"; path: string; target: string | null }; + | { op: "realpath"; path: string; target: string | null } + /** The frontend's output depends on the file's raw text or line + * positions, not only on its tokens (observeSourceText). */ + | { op: "text"; path: string }; export interface FrontendInputSnapshot { version: 1; @@ -105,6 +109,7 @@ export class FrontendInputTracker { record(probe: FrontendInputProbe): void { const key = `${probe.op}\0${probe.path}`; + if (probe.op === "text" && this.probes.has(key)) return; const previous = this.probes.get(key); if (previous !== undefined && JSON.stringify(previous) !== JSON.stringify(probe)) { // An input changed while this frontend was running. The build may @@ -158,6 +163,13 @@ function record(probe: FrontendInputProbe): void { (synchronousTracker ?? activeTracker.getStore())?.record(probe); } +// Source text the frontend reads raw (call-site text in messages, comptime +// callbacks, line numbers) changes with edits that keep the tokens. +observeSourceText((fileName) => { + const tracker = synchronousTracker ?? activeTracker.getStore(); + if (tracker !== undefined) tracker.record({ op: "text", path: resolve(fileName) }); +}); + /** Mark the active frontend as unsafe to persist because an exact host query * bypassed the tracked filesystem wrappers. No-op outside a frontend run. */ export function markFrontendInputsUnstable(): void { @@ -394,6 +406,9 @@ export function frontendInputsStillMatch( return probe.target === null; } } + case "text": + // The file probe of the same path checks its content. + return true; } }); } @@ -410,36 +425,44 @@ export function frontendInputsSemanticallyMatch( exclusions: FrontendInputExclusions = {}, ): FrontendSemanticMatch | null { if (!validFrontendInputSnapshot(snapshot)) return null; - const sourceProbes = new Map( - snapshot.probes - .filter((probe): probe is Extract => probe.op === "file") - .map((probe) => [probe.path, probe]), - ); + const sourceProbes = new Map>(); + const rawText = new Set(); + for (const probe of snapshot.probes) { + if (probe.op === "file") sourceProbes.set(probe.path, probe); + else if (probe.op === "text") rawText.add(probe.path); + } for (const [path, previous] of previousSources) { if (sourceProbes.get(path)?.digest !== frontendSourceDigest(previous)) return null; } const changed: FrontendSemanticChange[] = []; const currentSources = new Map(); - const adjusted: FrontendInputSnapshot = { - ...snapshot, - probes: snapshot.probes.map((probe) => { - if (probe.op !== "file") return probe; - const previous = previousSources.get(probe.path); - if (previous === undefined || frontendSourceDigest(previous) !== probe.digest) return probe; - let current: string; - try { - current = readFileSync(probe.path, "utf8"); - } catch { - return probe; - } - const currentDigest = frontendSourceDigest(current); - currentSources.set(probe.path, current); - if (currentDigest === probe.digest) return probe; - if (!isSemanticEquivalent(probe.path, previous, current)) return probe; - changed.push({ path: probe.path, previous, current }); - return { ...probe, digest: currentDigest }; - }), - }; + const probes: FrontendInputProbe[] = []; + for (const probe of snapshot.probes) { + const previous = probe.op === "file" ? previousSources.get(probe.path) : undefined; + if (probe.op !== "file" || previous === undefined) { + probes.push(probe); + continue; + } + let current: string; + try { + current = readFileSync(probe.path, "utf8"); + } catch { + return null; + } + const currentDigest = frontendSourceDigest(current); + currentSources.set(probe.path, current); + if (currentDigest === probe.digest) { + probes.push(probe); + continue; + } + // One edit that is not equivalent already rules out the match, and so + // does any edit of a file whose raw text the frontend read. + if (rawText.has(probe.path) || !isSemanticEquivalent(probe.path, previous, current)) + return null; + changed.push({ path: probe.path, previous, current }); + probes.push({ ...probe, digest: currentDigest }); + } + const adjusted: FrontendInputSnapshot = { ...snapshot, probes }; if (!frontendInputsStillMatch(adjusted, exclusions)) return null; for (const [path, previous] of previousSources) { if (!currentSources.has(path)) { @@ -486,6 +509,8 @@ export function validFrontendInputSnapshot(snapshot: unknown): snapshot is Front return true; case "realpath": return value.target === null || typeof value.target === "string"; + case "text": + return true; default: return false; } 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 af7270ea9..2e0f305a0 100644 --- a/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts +++ b/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts @@ -1,4 +1,4 @@ -import { boolLit, strLit } from "../../../ir/build.js"; +import { boolLit, strLit, varRef } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { requestGeneralStreamSurface } from "../general-streams.js"; import { type Lowerer } from "../lowerer.js"; @@ -93,32 +93,47 @@ export function lowerChildMethodCall( '"exit", "close", "error", "message", and "disconnect" are the supported child events (as literals)', ); } - if (!ts.isExpressionStatement(call.parent)) { - lowerer.unsupported( - "SC1090", - call, - `chaining child.${name}(...) (the result is void here — register each listener as its own statement)`, - ); - } - const receiver = lowerer.lowerExpr(access.expression); + // `child.on(...)` returns the child: a chained call or a stored result + // reads the receiver, which evaluates once, before the listener. + let receiver = lowerer.lowerExpr(access.expression); + const chained = ts.isExpressionStatement(call.parent) + ? null + : { + local: lowerer.declareHiddenLocal("%childListenerTarget", receiver.type), + init: receiver, + }; + if (chained) receiver = varRef(chained.local.id, chained.local.type, loc); + const registered = (registration: IrExpr): IrExpr => + chained === null + ? registration + : { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: chained.local.id, init: chained.init, loc }, + { kind: "exprStmt", expr: registration, loc }, + ], + result: varRef(chained.local.id, chained.local.type, loc), + type: chained.local.type, + loc, + }; const once: IrExpr = { kind: "boolLit", value: name === "once", type: BOOL, loc }; if (event === "message") { - return { + return registered({ kind: "libCall", fn: "child.onMessage", args: [receiver, ipcMessageListener(lowerer, call.arguments.nodes[1]!), once], type: VOID, loc, - }; + }); } if (event === "disconnect") { - return { + return registered({ kind: "libCall", fn: "child.onDisconnect", args: [receiver, ipcDisconnectListener(lowerer, call.arguments.nodes[1]!), once], type: VOID, loc, - }; + }); } const listenerNode = stripTypeCasts(call.arguments.nodes[1]!); if ( @@ -163,7 +178,7 @@ export function lowerChildMethodCall( call.arguments.nodes[1]!, ); } - return { + return registered({ kind: "libCall", fn: "child.onDyn", args: [ @@ -173,7 +188,7 @@ export function lowerChildMethodCall( ], type: VOID, loc, - }; + }); } if (event === "spawn") { if (cb.type.kind !== "func" || cb.type.params.length !== 0 || cb.type.ret.kind !== "void") { @@ -182,7 +197,13 @@ export function lowerChildMethodCall( call.arguments.nodes[1]!, ); } - return { kind: "libCall", fn: "child.onSpawn", args: [receiver, cb], type: VOID, loc }; + return registered({ + kind: "libCall", + fn: "child.onSpawn", + args: [receiver, cb], + type: VOID, + loc, + }); } if (cb.type.kind !== "func" || cb.type.params.length > (event === "error" ? 1 : 2)) { lowerer.unsupported( @@ -243,13 +264,13 @@ export function lowerChildMethodCall( `${event} listeners whose signal parameter is not 'Signals | null' (got '${lowerer.fmt(sigParam!)}')`, ); } - return { + return registered({ kind: "libCall", fn: event === "close" ? "child.onClose" : "child.onExit", args: [receiver, cb], type: VOID, loc, - }; + }); } if (param !== undefined && !(param.kind === "object" && param.className === "%Error")) { lowerer.unsupported( @@ -258,7 +279,13 @@ export function lowerChildMethodCall( `error listeners whose parameter is not 'Error' (got '${lowerer.fmt(param)}')`, ); } - return { kind: "libCall", fn: "child.onError", args: [receiver, cb], type: VOID, loc }; + return registered({ + kind: "libCall", + fn: "child.onError", + args: [receiver, cb], + type: VOID, + loc, + }); } // child.kill(signal?) — Node's semantics exactly: the name resolves // through Node's signal table (unknown names throw the ERR_UNKNOWN_SIGNAL diff --git a/packages/compiler/src/frontend/lowering/builtins/child-process.ts b/packages/compiler/src/frontend/lowering/builtins/child-process.ts index ea2dd379d..9096316a3 100644 --- a/packages/compiler/src/frontend/lowering/builtins/child-process.ts +++ b/packages/compiler/src/frontend/lowering/builtins/child-process.ts @@ -115,6 +115,8 @@ export function lowerSpawnSyncCall(lowerer: Lowerer, expr: ts.CallExpression, lo // "pipe"`) — accepted when its TYPE proves every arm is a supported // literal; the runtime maps the value to the modes at the call. let stdioStr: IrExpr | null = null; + // The `env` option's [k,v,...] pairs, which replace the child's environment. + let envPairs: IrExpr | null = null; let plain = true; // no behavior-changing option: the historical libCall if (optsNode) { @@ -227,11 +229,16 @@ export function lowerSpawnSyncCall(lowerer: Lowerer, expr: ts.CallExpression, lo case "windowsHide": lowerer.lowerExpr(m.value); // Node no-op on POSIX break; + case "env": + // A REPLACEMENT environment, as for spawn and execFileSync. + envPairs = recordToEnvPairs(lowerer, m.value); + plain = false; + break; default: lowerer.noLowering( `spawnSync option '${m.name}'`, p, - "encoding, timeout, killSignal, stdio, and windowsHide are the supported options", + "encoding, timeout, killSignal, stdio, env, and windowsHide are the supported options", ); } } @@ -239,6 +246,13 @@ export function lowerSpawnSyncCall(lowerer: Lowerer, expr: ts.CallExpression, lo if (plain) { return { kind: "libCall", fn: "cp.spawnSync", args: [cmd, argv], type: SPAWNRES_T, loc }; } + if (stdioStr !== null && envPairs !== null) { + lowerer.noLowering( + "spawnSync with a runtime stdio value and env", + optsNode!, + 'pass stdio as a "pipe"/"ignore"/"inherit" literal when env is set', + ); + } if (stdioStr !== null) { return { kind: "libCall", @@ -250,7 +264,7 @@ export function lowerSpawnSyncCall(lowerer: Lowerer, expr: ts.CallExpression, lo } return { kind: "libCall", - fn: "cp.spawnSyncOpts", + fn: envPairs !== null ? "cp.spawnSyncEnv" : "cp.spawnSyncOpts", args: [ cmd, argv, @@ -259,6 +273,7 @@ export function lowerSpawnSyncCall(lowerer: Lowerer, expr: ts.CallExpression, lo numLit(inMode, loc), numLit(outMode, loc), numLit(errMode, loc), + ...(envPairs !== null ? [envPairs] : []), ], type: SPAWNRES_T, loc, diff --git a/packages/compiler/src/frontend/lowering/builtins/crypto.ts b/packages/compiler/src/frontend/lowering/builtins/crypto.ts index 7a1791197..6123ee0a3 100644 --- a/packages/compiler/src/frontend/lowering/builtins/crypto.ts +++ b/packages/compiler/src/frontend/lowering/builtins/crypto.ts @@ -702,6 +702,8 @@ export function lowerCryptoModuleCall( } const pki = lowerPkiCall(lowerer, expr, bi.member, args, loc); if (pki) return pki; + const prime = lowerPrimeCall(lowerer, expr, bi.member, args, loc); + if (prime) return prime; if (bi.member === "createSecretKey") { if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement)) lowerer.noLowering( @@ -867,11 +869,29 @@ const IMPLEMENTED_CIPHERS = new Set([ "id-aes192-gcm", "id-aes256-gcm", "chacha20-poly1305", + ...["128", "192", "256"].flatMap((bits) => + ["ecb", "cfb", "cfb8", "cfb1", "ofb"].map((mode) => `aes-${bits}-${mode}`), + ), + "des-ede3-cbc", + "des3", + "des-ede3", + "des-ede3-ecb", + "des-ede3-cfb", + "des-ede3-cfb8", + "des-ede3-cfb1", + "des-ede3-ofb", + "des-ede-cbc", + "des-ede", + "des-ede-ecb", + "des-ede-cfb", + "des-ede-ofb", + "chacha20", ]); const SUPPORTED_CIPHERS_HINT = - "the supported ciphers are aes-128-cbc, aes-192-cbc, aes-256-cbc, aes-128-ctr, aes-192-ctr, " + - "aes-256-ctr, aes-128-gcm, aes-192-gcm, aes-256-gcm, and chacha20-poly1305"; + "the supported ciphers are aes-128, aes-192 and aes-256 in cbc, ctr, gcm, ecb, cfb, cfb8, cfb1 " + + "and ofb modes, des-ede3 in cbc, ecb, cfb, cfb8, cfb1 and ofb modes, des-ede in cbc, ecb, cfb " + + "and ofb modes, chacha20, and chacha20-poly1305"; /** A literal cipher name that Node knows but the runtime does not * implement is refused at compile time. Unknown literal names keep Node's @@ -1346,6 +1366,59 @@ export function isWebCryptoReceiver(lowerer: Lowerer, receiver: ts.Expression): return false; } +/** Node calls checkPrime's and generatePrime's callbacks with undefined as + * the error, where @types/node declares Error | null: whether this + * callback's error parameter can hold undefined. One that cannot receives + * null instead. */ +function takesUndefinedError(lowerer: Lowerer, callback: ts.Expression): boolean { + const type = lowerer.irTypeOf(callback); + if (type.kind !== "func") return true; + const error = type.params[0]; + if (error === undefined || error.kind === "dyn" || error.kind === "undefinedT") return true; + if (error.kind !== "union") return false; + return !!lowerer.unions.get(error.unionId)?.arms.some((arm) => arm.kind === "undefinedT"); +} + +/** checkPrime, checkPrimeSync, generatePrime and generatePrimeSync + * (scr_dh.c): checked-dynamic arguments, which the runtime checks as Node + * does (a function in the options position is the callback), and the + * sync forms' boolean, ArrayBuffer or bigint converted to the call site's + * type. Null for other members. */ +function lowerPrimeCall( + lowerer: Lowerer, + expr: ts.CallExpression, + member: string, + args: readonly ts.Expression[], + loc: SrcLoc, +): IrExpr | null { + const forms: Readonly> = { + checkPrimeSync: ["crypto.checkPrimeSync", 1, 2, "checkPrimeSync(candidate[, options])"], + checkPrime: ["crypto.checkPrimeCb", 1, 3, "checkPrime(candidate[, options], callback)"], + generatePrimeSync: ["crypto.generatePrimeSync", 1, 2, "generatePrimeSync(size[, options])"], + generatePrime: ["crypto.generatePrimeCb", 1, 3, "generatePrime(size[, options], callback)"], + }; + const form = forms[member]; + if (!form) return null; + const [fn, min, max, supported] = form; + if (args.length < min || args.length > max || args.some(ts.isSpreadElement)) + lowerer.noLowering( + `crypto.${member} with ${args.length} arguments`, + expr, + `${supported} is supported`, + ); + const values = Array.from({ length: max }, (_, i) => dynArg(lowerer, args[i], loc)); + if (fn === "crypto.checkPrimeCb" || fn === "crypto.generatePrimeCb") { + const callback = args[args.length - 1]; + const nullError = callback !== undefined && !takesUndefinedError(lowerer, callback); + values.push({ kind: "boolLit", value: nullError, type: BOOL, loc }); + return { kind: "libCall", fn, args: values, type: VOID, loc }; + } + const call: IrExpr = { kind: "libCall", fn, args: values, type: DYN, loc }; + const result = lowerer.mapTypeOf(lowerer.typeOf(expr)); + if (result === null || result.kind === "dyn") return call; + return lowerer.coerceInto(expr, call, result); +} + /** The asymmetric-key and signature members (scr_pki.c): checked-dynamic * arguments, and the result converted to the call site's type * (KeyObject, Sign and Verify are checked-dynamic handles; key pairs, @@ -1382,14 +1455,10 @@ function lowerPkiCall( expr, `${supported} is supported`, ); - // generateKeyPair(type, callback): Node reads a function in the options - // position as the callback. - const shifted = fn === "crypto.generateKeyPairCb" && args.length === 2; + // generateKeyPair(type, callback): the runtime reads a function in the + // options position as the callback, as Node does. const values: IrExpr[] = []; - for (let i = 0; i < max; i++) { - const node = shifted ? (i === 1 ? undefined : i === 2 ? args[1] : args[i]) : args[i]; - values.push(dynArg(lowerer, node, loc)); - } + for (let i = 0; i < max; i++) values.push(dynArg(lowerer, args[i], loc)); if (fn === "crypto.generateKeyPairCb") return { kind: "libCall", fn, args: values, type: VOID, loc }; const call: IrExpr = { kind: "libCall", fn, args: values, type: DYN, loc }; diff --git a/packages/compiler/src/frontend/lowering/builtins/global-namespace-keys.ts b/packages/compiler/src/frontend/lowering/builtins/global-namespace-keys.ts new file mode 100644 index 000000000..bb364ee55 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/builtins/global-namespace-keys.ts @@ -0,0 +1,123 @@ +/* The string-keyed properties Node 24 exposes on its builtin constructor + * and namespace globals, own and inherited, so `"key" in Error` (a feature + * probe such as `"captureStackTrace" in Error`) answers statically. The + * global must be the unshadowed builtin; compiled programs cannot add + * properties to these objects, so the answer is fixed. */ + +const FUNCTION_PROTOTYPE = [ + "length", + "name", + "arguments", + "caller", + "constructor", + "apply", + "bind", + "call", + "toString", +]; +const OBJECT_PROTOTYPE = [ + "constructor", + "__defineGetter__", + "__defineSetter__", + "hasOwnProperty", + "__lookupGetter__", + "__lookupSetter__", + "isPrototypeOf", + "propertyIsEnumerable", + "toString", + "valueOf", + "__proto__", + "toLocaleString", +]; +const CONSTRUCTOR_OWN = ["length", "name", "prototype"]; +const ERROR_OWN = [ + ...CONSTRUCTOR_OWN, + "captureStackTrace", + "prepareStackTrace", + "isError", + "stackTraceLimit", +]; + +/** Own string keys per global, beyond what the prototype chain adds. */ +const CONSTRUCTORS: Readonly> = { + Error: ERROR_OWN, + // The native error constructors inherit from Error. + TypeError: ERROR_OWN, + RangeError: ERROR_OWN, + SyntaxError: ERROR_OWN, + ReferenceError: ERROR_OWN, + EvalError: ERROR_OWN, + URIError: ERROR_OWN, + Object: [ + ...CONSTRUCTOR_OWN, + "assign", + "getOwnPropertyDescriptor", + "getOwnPropertyDescriptors", + "getOwnPropertyNames", + "getOwnPropertySymbols", + "hasOwn", + "is", + "preventExtensions", + "seal", + "create", + "defineProperties", + "defineProperty", + "freeze", + "getPrototypeOf", + "setPrototypeOf", + "isExtensible", + "isFrozen", + "isSealed", + "keys", + "entries", + "fromEntries", + "values", + "groupBy", + ], + Array: [...CONSTRUCTOR_OWN, "isArray", "from", "fromAsync", "of"], + Promise: [ + ...CONSTRUCTOR_OWN, + "all", + "allSettled", + "any", + "race", + "resolve", + "reject", + "withResolvers", + "try", + ], + Number: [ + ...CONSTRUCTOR_OWN, + "isFinite", + "isInteger", + "isNaN", + "isSafeInteger", + "parseFloat", + "parseInt", + "MAX_VALUE", + "MIN_VALUE", + "NaN", + "NEGATIVE_INFINITY", + "POSITIVE_INFINITY", + "MAX_SAFE_INTEGER", + "MIN_SAFE_INTEGER", + "EPSILON", + ], + String: [...CONSTRUCTOR_OWN, "fromCharCode", "fromCodePoint", "raw"], + Map: [...CONSTRUCTOR_OWN, "groupBy"], + Set: CONSTRUCTOR_OWN, + WeakMap: CONSTRUCTOR_OWN, + WeakSet: CONSTRUCTOR_OWN, + Boolean: CONSTRUCTOR_OWN, + Function: CONSTRUCTOR_OWN, + Date: [...CONSTRUCTOR_OWN, "now", "parse", "UTC"], + BigInt: [...CONSTRUCTOR_OWN, "asUintN", "asIntN"], +}; + +/** Whether Node answers `key in ` with true, or null when `name` is + * not a tabled global. */ +export function builtinGlobalHasKey(name: string, key: string): boolean | null { + const own = CONSTRUCTORS[name]; + if (own === undefined) return null; + return own.includes(key) || FUNCTION_PROTOTYPE.includes(key) || OBJECT_PROTOTYPE.includes(key); +} diff --git a/packages/compiler/src/frontend/lowering/builtins/numbers.ts b/packages/compiler/src/frontend/lowering/builtins/numbers.ts index 1ff35fbb5..9764216b7 100644 --- a/packages/compiler/src/frontend/lowering/builtins/numbers.ts +++ b/packages/compiler/src/frontend/lowering/builtins/numbers.ts @@ -1,7 +1,7 @@ import * as ts from "../../ts7/adapter.js"; import { type Lowerer, own } from "../lowerer.js"; import { lowerCheckedPredicateValue } from "../lower-builtin-values.js"; -import { locOf } from "../../program.js"; +import { isJsSourceFile, locOf } from "../../program.js"; import { BOOL, F64, STRING, type IrExpr, type IrLibFn } from "../../../ir/ir.js"; import { lowerBuiltinValuePreservingUndefined, lowerOptionalNumberPredicate } from "./arguments.js"; import { tryLowerExpression } from "../expressions/try-lower-expression.js"; @@ -104,6 +104,13 @@ export function lowerNumberStaticCall( : { kind: "numLit", value: 0, type: F64, loc }; return { kind: "libCall", fn: "num.parseInt", args: [text, radix], type: F64, loc }; } + // JavaScript whose argument does not lower on its own (zod's + // `Number.parseInt(val.toFixed(n).replace(".", ""))` over an untyped + // parameter): the global parser's own JavaScript forms. + if (probed === null && member === "parseInt" && isJsSourceFile(ts.getSourceFileOfNode(call))) { + const parsed = lowerStaticNumberGlobal(lowerer, member, call, call.arguments.nodes, loc); + if (parsed !== null) return parsed; + } const argText = probed !== null ? lowerer.fmt(probed.type) diff --git a/packages/compiler/src/frontend/lowering/builtins/promises.ts b/packages/compiler/src/frontend/lowering/builtins/promises.ts index 75e527aa5..129ac73a3 100644 --- a/packages/compiler/src/frontend/lowering/builtins/promises.ts +++ b/packages/compiler/src/frontend/lowering/builtins/promises.ts @@ -9,6 +9,7 @@ import { isJsSourceFile, locOf } from "../../program.js"; import { BOOL, DYN, + F64, type IrExpr, type IrFunction, type IrStmt, @@ -376,8 +377,14 @@ export function lowerPromiseStaticCall( ); } const entries = lowerer.lowerExpr(argNode); - if (isJsSourceFile(ts.getSourceFileOfNode(call)) && entries.type.kind === "dyn") - return checkedPromiseAll(lowerer, entries, loc); + if (entries.type.kind === "dyn") return checkedPromiseAll(lowerer, entries, loc); + // An 'any'-typed argument that holds a typed value (`const xs: any = + // []`): the combinator reads it as the runtime value it is. + if ( + (lowerer.typeOf(argNode).flags & ts.TypeFlags.Any) !== 0 && + lowerer.dynConvertible(entries.type) + ) + return checkedPromiseAll(lowerer, lowerer.coerceToExpected(entries, DYN), loc); // loadPlugin(p)))` — the loadPlugins shape, where the checker // spells `any[]`): the ENGINE's own Promise.all runs over the @@ -845,3 +852,89 @@ function lowerTypedSettledCombinator( } return { kind: "intrinsic", name: "promise.allSettled", args: [entries], type: resultT, loc }; } + +/** `const pair = await Promise.all([p, q])` where every entry is the same + * Promise and the binding's type is the checker's tuple `[T, T]`: the + * uniform entries settle through one runtime combinator into a T[] (the + * same combinator and turn as the array form), and the binding receives + * the tuple of its elements. Null for every other await. */ +export function lowerAwaitedUniformPromiseAll( + lowerer: Lowerer, + expr: ts.AwaitExpression, +): IrExpr | null { + let operand: ts.Expression = expr.expression; + while (ts.isParenthesizedExpression(operand)) operand = operand.expression; + if ( + !ts.isCallExpression(operand) || + operand.questionDotToken || + !ts.isPropertyAccessExpression(operand.expression) || + operand.arguments.nodes.length !== 1 || + isJsSourceFile(ts.getSourceFileOfNode(expr)) + ) + return null; + const argNode = operand.arguments.nodes[0]!; + if ( + !ts.isArrayLiteralExpression(argNode) || + argNode.elements.nodes.length === 0 || + argNode.elements.nodes.some(ts.isSpreadElement) + ) + return null; + let user: ts.Node = expr; + while (ts.isParenthesizedExpression(user.parent)) user = user.parent; + const decl = user.parent; + if (!ts.isVariableDeclaration(decl) || decl.initializer !== user || !ts.isIdentifier(decl.name)) + return null; + if (lowerer.stdlibGlobalMember(operand.expression, "Promise") !== "all") return null; + const tuple = lowerer.mapTypeOf(lowerer.typeOf(decl.name)); + if (tuple?.kind !== "record") return null; + const shape = lowerer.shapes.get(tuple.shapeId); + if (!shape?.tuple || shape.fields.length !== argNode.elements.nodes.length) return null; + const elems = argNode.elements.nodes.map((el) => lowerer.lowerExpr(el)); + const first = elems[0]!.type; + if ( + first.kind !== "promise" || + first.inner.kind === "void" || + !elems.every((e) => typeEquals(e.type, first)) || + !shape.fields.every((f) => typeEquals(f.type, first.inner)) + ) + return null; + const loc = locOf(expr); + const values = arrayOf(first.inner); + const local = lowerer.declareHiddenLocal("%allValues", values); + const all: IrExpr = { + kind: "intrinsic", + name: "promise.all", + args: [{ kind: "arrayLit", elems, type: arrayOf(first), loc }], + type: { kind: "promise", inner: values }, + loc, + }; + return { + kind: "seqExpr", + stmts: [ + { + kind: "varDecl", + localId: local.id, + init: { kind: "awaitExpr", value: all, type: values, loc }, + loc, + }, + ], + // Shape fields are sorted by name; a tuple field's name is its position. + result: { + kind: "recordLit", + fields: shape.fields.map((f) => ({ + name: f.name, + value: { + kind: "arrayGet", + arr: varRef(local.id, values, loc), + index: { kind: "numLit", value: Number(f.name), type: F64, loc }, + type: f.type, + loc, + }, + })), + type: tuple, + loc, + }, + type: tuple, + loc, + }; +} diff --git a/packages/compiler/src/frontend/lowering/builtins/url.ts b/packages/compiler/src/frontend/lowering/builtins/url.ts index 2a77fa85c..8619b4997 100644 --- a/packages/compiler/src/frontend/lowering/builtins/url.ts +++ b/packages/compiler/src/frontend/lowering/builtins/url.ts @@ -195,7 +195,30 @@ export function lowerSearchParamsNew( ); } const arg = args[0]; - if (arg === undefined || (ts.isIdentifier(arg) && arg.text === "undefined")) { + if (arg === undefined) { + return { kind: "libCall", fn: "sp.new", args: [], type: SEARCH_PARAMS_T, loc }; + } + return lowerSearchParamsInit(lowerer, arg, loc); +} + +/** The URLSearchParams built from one init expression. A conditional + * init (`new URLSearchParams(c ? { a: "1" } : {})`) evaluates its + * condition first and builds from the arm it selects, so each arm may be + * any supported init shape. */ +function lowerSearchParamsInit(lowerer: Lowerer, arg: ts.Expression, loc: SrcLoc): IrExpr { + let inner = arg; + while (ts.isParenthesizedExpression(inner)) inner = inner.expression; + if (ts.isConditionalExpression(inner)) { + return { + kind: "ternary", + cond: lowerer.lowerCondition(inner.condition), + then: lowerSearchParamsInit(lowerer, inner.whenTrue, loc), + else_: lowerSearchParamsInit(lowerer, inner.whenFalse, loc), + type: SEARCH_PARAMS_T, + loc, + }; + } + if (ts.isIdentifier(arg) && arg.text === "undefined") { return { kind: "libCall", fn: "sp.new", args: [], type: SEARCH_PARAMS_T, loc }; } // The object-literal init: `{ a: "1", b: "2" }` appends pairs in diff --git a/packages/compiler/src/frontend/lowering/builtins/webassembly.ts b/packages/compiler/src/frontend/lowering/builtins/webassembly.ts index 56347e437..8e5c3a048 100644 --- a/packages/compiler/src/frontend/lowering/builtins/webassembly.ts +++ b/packages/compiler/src/frontend/lowering/builtins/webassembly.ts @@ -96,9 +96,15 @@ function webAssemblySurface(lowerer: Lowerer, expr: ts.Expression): string | nul isGlobalThis(lowerer, expr.expression) ) { const target = lowerer.checker.getTypeAtLocation(expr.type); - return lowerer.checker.getPropertyOfType(target, "WebAssembly") !== undefined - ? "globalThis.WebAssembly" - : null; + const member = lowerer.checker.getPropertyOfType(target, "WebAssembly"); + // A target built from `typeof globalThis` (`typeof globalThis & { key?: T }`) + // inherits the global's own WebAssembly declaration: that cast names the + // whole global object, whose other properties stay readable and writable. + const globalMember = lowerer.checker.getPropertyOfType( + lowerer.typeOf(bare(expr.expression)), + "WebAssembly", + ); + return member !== undefined && member !== globalMember ? "globalThis.WebAssembly" : null; } const node = bare(expr); if (ts.isIdentifier(node)) { diff --git a/packages/compiler/src/frontend/lowering/class-callbacks.ts b/packages/compiler/src/frontend/lowering/class-callbacks.ts index 6ca685684..0bda0068e 100644 --- a/packages/compiler/src/frontend/lowering/class-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/class-callbacks.ts @@ -6,12 +6,14 @@ import { STRING, VOID, isClassOwnEnumerableFieldName, + typeEquals, type IrExpr, type IrStmt, type SrcLoc, } from "../../ir/ir.js"; import { isJsSourceFile, locOf } from "../program.js"; -import type { Lowerer } from "./lowerer.js"; +import { PoisonError, type Lowerer } from "./lowerer.js"; +import { unsupportedDiag } from "../../diagnostics/diagnostic.js"; import { findMethodOn, type ClassInfo } from "./lower-classes.js"; import { classPropertiesHelper } from "./class-dynamic-dispatch.js"; import { observedPrototypeMethod } from "./class-prototypes.js"; @@ -162,6 +164,7 @@ export function classCallbackCall( fallback: IrExpr, loc: SrcLoc, calleeName = name, + original?: { originalId: () => string | undefined; directCall: () => IrExpr }, ): IrExpr { const bag = lowerer.declareHiddenLocal("%callbackBag", DYN); const present = lowerer.declareHiddenLocal("%callbackPresent", BOOL); @@ -186,27 +189,91 @@ export function classCallbackCall( loc, }; const observed = observedSlot(lowerer, receiver, name, loc); - const result: IrExpr = observed - ? call - : { - kind: "ternary", - cond: varRef(present.id, BOOL, loc), - then: call, - else_: - fallback.type.kind === "void" - ? { - kind: "seqExpr", - stmts: [{ kind: "exprStmt", expr: fallback, loc }], - result: dynUndefinedExpr(loc), - type: DYN, - loc, - } - : lowerer.coerceToExpected(fallback, DYN), - type: DYN, - loc, - }; - const answer = - fallback.type.kind === "void" ? result : lowerer.coerceToExpected(result, fallback.type); + const boxed = + observed || fallback.type.kind === "void" ? null : lowerer.coerceToExpected(fallback, DYN); + let answer: IrExpr; + if (boxed !== null && boxed.type.kind !== "dyn") { + // A result with no checked-dynamic form (an Iterable, whose + // generator arm does not box) keeps its native type: the unchanged + // method's result stays as it is, and a replacement's dynamic result + // takes the checked conversion into that type. + const converted = lowerer.coerceToExpected(call, fallback.type); + if (!typeEquals(converted.type, fallback.type)) { + lowerer.pushDiag( + unsupportedDiag( + "SC1090", + loc, + `calling replaceable method '${calleeName}' with a '${lowerer.fmt(fallback.type)}' result`, + ), + ); + throw new PoisonError(); + } + answer = { + kind: "ternary", + cond: varRef(present.id, BOOL, loc), + then: converted, + else_: fallback, + type: fallback.type, + loc, + }; + } else { + const result: IrExpr = observed + ? call + : { + kind: "ternary", + cond: varRef(present.id, BOOL, loc), + then: call, + else_: boxed ?? { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: fallback, loc }], + result: dynUndefinedExpr(loc), + type: DYN, + loc, + }, + type: DYN, + loc, + }; + answer = + fallback.type.kind === "void" ? result : lowerer.coerceToExpected(result, fallback.type); + } + // Read after observedSlot built the prototype data and its originals. + const originalId = observed ? original?.originalId() : undefined; + if (original && originalId !== undefined) { + // The slot still holds the method's original value: calling it is + // calling the method, so the call stays native (no boxed arguments, + // dynamic call or checked result). A replacement takes the dynamic call. + const unchanged: IrExpr = { + kind: "libCall", + fn: "dyn.sameValue", + args: [varRef(callback.id, DYN, loc), varRef(originalId, DYN, loc)], + type: BOOL, + loc, + }; + answer = + fallback.type.kind === "void" + ? { + kind: "ternary", + cond: unchanged, + then: { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: original.directCall(), loc }], + result: dynUndefinedExpr(loc), + type: DYN, + loc, + }, + else_: call, + type: DYN, + loc, + } + : { + kind: "ternary", + cond: unchanged, + then: original.directCall(), + else_: answer, + type: answer.type, + loc, + }; + } return { kind: "seqExpr", stmts: [ diff --git a/packages/compiler/src/frontend/lowering/class-construction.ts b/packages/compiler/src/frontend/lowering/class-construction.ts index 66ec547bc..7ee53d04e 100644 --- a/packages/compiler/src/frontend/lowering/class-construction.ts +++ b/packages/compiler/src/frontend/lowering/class-construction.ts @@ -25,16 +25,21 @@ const PREFIX = "%class.construct:"; const PACKED = `${PREFIX}packed`; const INSTANCEOF = "%class.instanceof:dynamic"; +/** `value instanceof callee` for an untyped constructor value, through one + * shared helper. `refusal` is the site's message for a runtime + * constructor with no instance test. */ export function checkedClassInstanceOf( lowerer: Lowerer, value: IrExpr, callee: IrExpr, + refusal: string, loc: SrcLoc, ): IrExpr { if (!lowerer.liftedFns.some((fn) => fn.name === INSTANCEOF)) { const params = [ { localId: "value", name: "value", type: DYN }, { localId: "callee", name: "callee", type: DYN }, + { localId: "refusal", name: "refusal", type: STRING }, ]; lowerer.liftedFns.push({ name: INSTANCEOF, @@ -47,7 +52,11 @@ export function checkedClassInstanceOf( value: { kind: "libCall", fn: "bytes.instanceOf", - args: [varRef("value", DYN, loc), varRef("callee", DYN, loc)], + args: [ + varRef("value", DYN, loc), + varRef("callee", DYN, loc), + varRef("refusal", STRING, loc), + ], type: BOOL, loc, }, @@ -57,7 +66,13 @@ export function checkedClassInstanceOf( loc, }); } - return { kind: "call", callee: INSTANCEOF, args: [value, callee], type: BOOL, loc }; + return { + kind: "call", + callee: INSTANCEOF, + args: [value, callee, { kind: "strLit", value: refusal, type: STRING, loc }], + type: BOOL, + loc, + }; } /** Checked construction dispatches to emitted native constructor thunks. */ diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts index b717452c2..1a21a6aa5 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts @@ -458,9 +458,11 @@ test("normalizes base capsules before dispatching each derived member once", () }); expect(dispatch.body.filter((statement) => statement.kind === "if")).toHaveLength(2); const normalize = liftedFns.find((helper) => helper.name === "%dyn.class.normalize")!; + // The capsule branch unboxes its receiver once, then tests each subclass. expect(normalize.body[0]).toMatchObject({ kind: "if", then: [ + { kind: "varDecl", init: { kind: "dynCheck", type: { kind: "object", className: "Base" } } }, { kind: "if", then: [ diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index af3212ef0..a1bc41782 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -85,7 +85,7 @@ import { lowerDynamicEmitterOn, } from "./lower-event-emitter.js"; import { protocolArm, protocolFieldArm } from "./class-protocol-dispatch.js"; -import { STREAM_OPERATORS } from "./lower-stream.js"; +import { STREAM_OPERATORS, operatorSignalOptions, reduceSignalSetup } from "./lower-stream.js"; import { deletedPropertyNames, fixedFieldFamily } from "./js-class-fields.js"; const STREAM_METHODS = new Set([ @@ -350,7 +350,7 @@ export function classInstanceOf( localId: "candidate", value: { kind: "libCall", - fn: "dyn.getPrototype", + fn: "dyn.getPrototypeForIdentity", args: [candidate], type: DYN, loc, @@ -394,6 +394,40 @@ export function classInstanceOf( }; } +/** `value` is the parameter itself, possibly through js.fromDyn. */ +function returnsParam(value: IrExpr, param: string): boolean { + if (value.kind === "varRef") return value.localId === param; + return ( + value.kind === "libCall" && + value.fn === "js.fromDyn" && + value.args.length === 1 && + value.args[0]!.kind === "varRef" && + value.args[0]!.localId === param + ); +} + +/** The kind a class-table branch requires of `param`: a capsule-key test + * (alone or guarding a conjunction) needs a capsule, a class-key test a + * function value; null for any other condition. */ +function branchKind(cond: IrExpr, param: string): "classCapsule" | "funcKind" | null { + if ( + cond.kind === "libCall" && + (cond.fn === "dyn.typedRefIs" || + cond.fn === "dyn.typedRefKeyIs" || + cond.fn === "dyn.classIs") && + cond.args[0]?.kind === "varRef" && + cond.args[0].localId === param + ) { + if (cond.fn === "dyn.classIs") return "funcKind"; + const key = cond.args[1]; + return key?.kind === "strLit" && key.value.startsWith("object:") ? "classCapsule" : null; + } + if (cond.kind === "ternary" && cond.else_.kind === "boolLit" && cond.else_.value === false) + return branchKind(cond.cond, param); + if (cond.kind === "logical" && cond.op === "&&") return branchKind(cond.left, param); + return null; +} + const READ_PROPERTIES = "%dyn.class.readProperties"; const READ_PROPERTIES_OR_EMPTY = "%dyn.class.readPropertiesOrEmpty"; @@ -685,16 +719,154 @@ export function trackClassFieldCreation(lowerer: Lowerer, functions: IrFunction[ if (![...lowerer.classes.values()].some((info) => info.def.tracksOwnFields)) return; const loc = functions[0]!.loc; const name = "%dyn.class.fieldCreated"; - const value = varRef("value", DYN, loc), - key = varRef("key", DYN, loc); - const bag = varRef("bag", DYN, loc); - const created = (receiver: IrExpr, className: string, field: string, loc: SrcLoc): IrStmt => ({ + const key = varRef("key", DYN, loc); + // One helper per class: the instance's bag is its own hidden field, so a + // creation reads it directly instead of boxing the instance and testing + // it against every class's capsule (%dyn.class.properties), which runs + // only to create the bag, once per instance. + const perClass = new Map(); + const helperFor = (className: string): string => { + const known = perClass.get(className); + if (known !== undefined) return known; + const helperName = `${name}:${className}`; + perClass.set(className, helperName); + const self = varRef("this", { kind: "object", className }, loc); + const bagRef = varRef("bag", DYN, loc); + functions.push({ + name: helperName, + params: [ + { localId: "this", name: "this", type: { kind: "object", className } }, + { localId: "key", name: "key", type: DYN }, + ], + returnType: VOID, + locals: [ + { id: "this", name: "this", type: { kind: "object", className }, mutable: false }, + { id: "key", name: "key", type: DYN, mutable: false }, + { id: "bag", name: "bag", type: DYN, mutable: true }, + ], + body: [ + { + kind: "varDecl", + localId: "bag", + init: { kind: "fieldGet", obj: self, className, field: PROPERTY_BAG, type: DYN, loc }, + loc, + }, + { + kind: "if", + cond: { kind: "dynTest", test: "undefined", value: bagRef, type: BOOL, loc }, + then: initializeBag(lowerer, className, self, loc), + else_: null, + loc, + }, + ...defineIfAbsent(lowerer, bagRef, key, loc), + ], + loc, + }); + return helperName; + }; + // String-keyed fields take the runtime's direct insert for an ordinary + // bag; the general path stays for any other bag and for symbol keys. + const stringHelperFor = (className: string): string => { + const known = perClass.get(`str:${className}`); + if (known !== undefined) return known; + const general = helperFor(className); + const helperName = `${name}.string:${className}`; + perClass.set(`str:${className}`, helperName); + const type: IrType = { kind: "object", className }; + const self = varRef("this", type, loc); + const bagRef = varRef("bag", DYN, loc); + const field = varRef("field", STRING, loc); + functions.push({ + name: helperName, + params: [ + { localId: "this", name: "this", type }, + { localId: "field", name: "field", type: STRING }, + ], + returnType: VOID, + locals: [ + { id: "this", name: "this", type, mutable: false }, + { id: "field", name: "field", type: STRING, mutable: false }, + { id: "bag", name: "bag", type: DYN, mutable: true }, + ], + body: [ + { + kind: "varDecl", + localId: "bag", + init: { kind: "fieldGet", obj: self, className, field: PROPERTY_BAG, type: DYN, loc }, + loc, + }, + { + kind: "if", + cond: { kind: "dynTest", test: "undefined", value: bagRef, type: BOOL, loc }, + then: initializeBag(lowerer, className, self, loc), + else_: null, + loc, + }, + { + kind: "if", + cond: { + kind: "libCall", + fn: "dyn.bagCreateField", + args: [bagRef, field], + type: BOOL, + loc, + }, + then: [{ kind: "return", value: null, loc }], + else_: null, + loc, + }, + { + kind: "exprStmt", + expr: { + kind: "call", + callee: general, + args: [self, lowerer.coerceToExpected(field, DYN)], + type: VOID, + loc, + }, + loc, + }, + ], + loc, + }); + return helperName; + }; + const created = (receiver: IrExpr, className: string, field: string, loc: SrcLoc): IrStmt => { + const symbolKey = symbolMemberKey(lowerer, lowerer.classes.get(className)!, field, loc); + if (symbolKey === null) { + return { + kind: "exprStmt", + expr: { + kind: "call", + callee: stringHelperFor(className), + args: [ + receiver.type.kind === "object" && receiver.type.className !== className + ? { kind: "upcast", value: receiver, type: { kind: "object", className }, loc } + : receiver, + { kind: "strLit", value: field, type: STRING, loc }, + ], + type: VOID, + loc, + }, + loc, + }; + } + return createdGeneral(receiver, className, field, loc); + }; + const createdGeneral = ( + receiver: IrExpr, + className: string, + field: string, + loc: SrcLoc, + ): IrStmt => ({ kind: "exprStmt", expr: { kind: "call", - callee: name, + callee: helperFor(className), args: [ - lowerer.coerceToExpected(receiver, DYN), + receiver.type.kind === "object" && receiver.type.className !== className + ? { kind: "upcast", value: receiver, type: { kind: "object", className }, loc } + : receiver, symbolMemberKey(lowerer, lowerer.classes.get(className)!, field, loc) ?? lowerer.coerceToExpected({ kind: "strLit", value: field, type: STRING, loc }, DYN), ], @@ -769,80 +941,103 @@ export function trackClassFieldCreation(lowerer: Lowerer, functions: IrFunction[ }, }); } - functions.push({ - name, - params: [ - { localId: "value", name: "value", type: DYN }, - { localId: "key", name: "key", type: DYN }, - ], - returnType: VOID, - locals: [ - { id: "value", name: "value", type: DYN, mutable: false }, - { id: "key", name: "key", type: DYN, mutable: false }, - { id: "bag", name: "bag", type: DYN, mutable: false }, - ], - body: [ +} + +/** `bag = `, stored in the instance's hidden field: + * the class's own initializer when recorded, else the class-table + * dispatch, which creates it. */ +function initializeBag(lowerer: Lowerer, className: string, self: IrExpr, loc: SrcLoc): IrStmt[] { + const init = lowerer.classBagInitializers?.get(className); + if (init === undefined) { + return [ { - kind: "varDecl", + kind: "assign", localId: "bag", - init: { + value: { kind: "call", callee: classPropertiesHelper(lowerer, loc).name, - args: [value], + args: [lowerer.coerceToExpected(self, DYN)], type: DYN, loc, }, loc, }, - { - kind: "if", - cond: { - kind: "unary", - op: "!", - operand: { kind: "libCall", fn: "dyn.hasOwnComputed", args: [bag, key], type: BOOL, loc }, - type: BOOL, - loc, - }, - then: [ - { - kind: "exprStmt", - expr: { - kind: "libCall", - fn: "dyn.defineProperty", - args: [ - bag, - lowerer.coerceToExpected(key, DYN), - { - kind: "dynObjLit", - fields: [ - { - key: { kind: "strLit", value: "value", type: STRING, loc }, - value: dynUndefinedExpr(loc), - }, - ...["writable", "enumerable", "configurable"].map((key) => ({ - key: { kind: "strLit" as const, value: key, type: STRING, loc }, - value: lowerer.coerceToExpected( - { kind: "boolLit", value: true, type: BOOL, loc }, - DYN, - ), - })), - ], - type: DYN, - loc, - }, - ], - type: DYN, - loc, - }, - loc, - }, - ], - else_: null, + ]; + } + return [ + { + kind: "fieldSet", + obj: self, + className, + field: PROPERTY_BAG, + value: structuredClone(init), + loc, + }, + { + kind: "assign", + localId: "bag", + value: { kind: "fieldGet", obj: self, className, field: PROPERTY_BAG, type: DYN, loc }, + loc, + }, + ]; +} + +/** Whether `value` reads local `id`. */ +function mentionsLocal(value: IrExpr, id: string): boolean { + return JSON.stringify(value).includes(`"localId":${JSON.stringify(id)}`); +} + +/** `if (!hasOwn(bag, key)) defineProperty(bag, key, )`: + * a field's creation in its instance's property bag. */ +function defineIfAbsent(lowerer: Lowerer, bag: IrExpr, key: IrExpr, loc: SrcLoc): IrStmt[] { + return [ + { + kind: "if", + cond: { + kind: "unary", + op: "!", + operand: { kind: "libCall", fn: "dyn.hasOwnComputed", args: [bag, key], type: BOOL, loc }, + type: BOOL, loc, }, - ], - loc, - }); + then: [ + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "dyn.defineProperty", + args: [ + bag, + lowerer.coerceToExpected(key, DYN), + { + kind: "dynObjLit", + fields: [ + { + key: { kind: "strLit", value: "value", type: STRING, loc }, + value: dynUndefinedExpr(loc), + }, + ...["writable", "enumerable", "configurable"].map((key) => ({ + key: { kind: "strLit" as const, value: key, type: STRING, loc }, + value: lowerer.coerceToExpected( + { kind: "boolLit", value: true, type: BOOL, loc }, + DYN, + ), + })), + ], + type: DYN, + loc, + }, + ], + type: DYN, + loc, + }, + loc, + }, + ], + else_: null, + loc, + }, + ]; } /** Calls on native class capsules keep the instance's compiled methods. The @@ -862,6 +1057,9 @@ export class ClassDynamicDispatch { private deletedNames: Set | null | undefined; private readonly boxed = new Set(); private readonly boxedConstructors = new Set(); + /** Every class a boxed value was statically typed as, runtime error + * classes included: the value's capsule can be any of its subclasses. */ + private readonly boxedStatic = new Set(); private readonly dispatches = new Map(); private readonly properties = new Map(); private readonly computed = new Map< @@ -906,8 +1104,20 @@ export class ClassDynamicDispatch { private descriptorHelper: IrFunction | null = null; private prototypeHelper: IrFunction | null = null; private readonly prototypeClasses = new Set(); - private prototypeIdentityHelper: IrFunction | null = null; - private readonly prototypeIdentityClasses = new Set(); + /** The stand-in prototype object of each class without prototype data + * (prototypeStandIn), by class name. */ + private readonly prototypeStandIns = new Set(); + /** The prototype reads of identity walks, by libCall, and the classes + * each one answers for boxed instances. */ + private readonly prototypeIdentityHelpers = new Map< + "dyn.getPrototype" | "dyn.getPrototypeForIdentity", + { fn: IrFunction; classes: Set } + >(); + /** `value instanceof Error` through Error.prototype: boxed instances of + * classes with a heritage evaluated at run time test their prototype + * data's chain. */ + private errorPrototypeHelper: IrFunction | null = null; + private readonly errorPrototypeClasses = new Set(); private readonly instancePrototypes = new Map }>(); private readonly checkedCasts = new Map }>(); /** The for-in key helpers, by libCall (dyn and native string[] keys). */ @@ -956,6 +1166,7 @@ export class ClassDynamicDispatch { if (seenTypes.has(key)) return; seenTypes.add(key); if (type.kind === "object") { + this.boxedStatic.add(type.className); if (isDynTypedRefType(type)) this.boxed.add(type.className); } else if (type.kind === "classval") this.boxedConstructors.add(type.className); else if (type.kind === "array") discover(type.elem); @@ -1087,9 +1298,13 @@ export class ClassDynamicDispatch { }); changed = true; } - if (this.prototypeIdentityHelper && !this.prototypeIdentityClasses.has(className)) { - this.prototypeIdentityClasses.add(className); - const loc = this.prototypeIdentityHelper.loc; + if ( + this.errorPrototypeHelper && + !this.errorPrototypeClasses.has(className) && + hasRuntimeHeritage(info) + ) { + this.errorPrototypeClasses.add(className); + const loc = this.errorPrototypeHelper.loc; const type: IrType = { kind: "object", className }; const value = varRef("value", DYN, loc); const prototype = classPrototypeData(lowerer, info, loc, { @@ -1099,7 +1314,7 @@ export class ClassDynamicDispatch { loc, }); if (prototype) - this.prototypeIdentityHelper.body.unshift({ + this.errorPrototypeHelper.body.unshift({ kind: "if", cond: { kind: "libCall", @@ -1108,7 +1323,19 @@ export class ClassDynamicDispatch { type: BOOL, loc, }, - then: [{ kind: "return", value: prototype, loc }], + then: [ + { + kind: "return", + value: { + kind: "libCall", + fn: "dyn.inheritsErrorPrototype", + args: [prototype], + type: BOOL, + loc, + }, + loc, + }, + ], else_: null, loc, }); @@ -1181,6 +1408,101 @@ export class ClassDynamicDispatch { } } } + // The runtime boxes a class instance as its exact class, so a value + // boxed at a base (or Error) static type reaches the identity views as + // any subclass's capsule. + for (const helper of this.prototypeIdentityHelpers.values()) + for (const [className, info] of lowerer.classes) { + if (helper.classes.has(className)) continue; + if (info.builtinEmitter || info.builtinStream || info.builtinError) continue; + if (!isDynTypedRefType({ kind: "object", className })) continue; + if ( + !this.boxed.has(className) && + ![...this.boxedStatic].some((base) => lowerer.isSubclassOf(className, base)) + ) + continue; + helper.classes.add(className); + const loc = helper.fn.loc; + const type: IrType = { kind: "object", className }; + const value = varRef("value", DYN, loc); + // A class with no prototype data in its chain (one extending + // Error, or with no methods) has no prototype a program can name: + // an object of the class's own stands in, so instances of one class + // share it (`Object.getPrototypeOf(a) === Object.getPrototypeOf(b)`), + // other prototypes differ from it, and an instanceof walk (through + // Object.prototype to null) answers false instead of reaching the + // reflection refusal. A function-local class is a new class at + // each evaluation of its declaration, so its instances answer with + // the prototype of their own class value instead. + const answer = (name: string): IrStmt[] | null => { + const of = lowerer.classes.get(name)!; + const prototype = + hasClassPrototypeData(of) || of.localClass + ? classPrototypeData(lowerer, of, loc, { + kind: "dynCheck", + value, + type: { kind: "object", className: name }, + loc, + }) + : null; + if (prototype) return [{ kind: "return", value: prototype, loc }]; + return hasClassPrototypeData(of) ? null : this.prototypeStandIn(lowerer, name, loc); + }; + const own = answer(className); + if (!own) { + changed = true; + continue; + } + // A capsule boxed at this class can hold a subclass instance: the + // most derived subclass the instance belongs to answers. + const depth = (name: string): number => { + let n = 0; + for (let at = lowerer.classes.get(name)?.base; at; at = at.base) n++; + return n; + }; + const subclasses = [...lowerer.classes.keys()] + .filter( + (name) => + name !== className && + lowerer.isSubclassOf(name, className) && + isDynTypedRefType({ kind: "object", className: name }), + ) + .sort((a, b) => depth(b) - depth(a)); + const then: IrStmt[] = []; + for (const name of subclasses) { + const sub = answer(name); + if (!sub) continue; + then.push({ + kind: "if", + cond: { + kind: "instanceOf", + value: { kind: "dynCheck", value, type, loc }, + className: name, + type: BOOL, + loc, + }, + then: sub, + else_: null, + loc, + }); + } + then.push(...own); + helper.fn.body.unshift({ + kind: "if", + cond: { + kind: "libCall", + fn: "dyn.typedRefIs", + args: [value, { kind: "strLit", value: typeKey(type), type: STRING, loc }], + type: BOOL, + loc, + }, + then, + else_: null, + loc, + }); + changed = true; + } + let live: Map[]> | null = null; for (const plan of this.propertyReceivers(lowerer, false, false)) { const info = plan.info; const className = info.def.name; @@ -1261,7 +1583,11 @@ export class ClassDynamicDispatch { ); const prototypeFor = (current: ClassInfo, receiver: IrExpr, loc: SrcLoc): IrExpr | null => this.bagPrototype(lowerer, current, receiver, loc); - for (const existingInit of this.bagInitializers.get(className) ?? []) { + live ??= this.liveBagInitializers(); + for (const existingInit of [ + ...(this.bagInitializers.get(className) ?? []), + ...(live.get(className) ?? []), + ]) { if (existingInit.value.kind === "dynObjLit" && needsPrototype(info)) { const prototype = prototypeFor(info, existingInit.obj, existingInit.loc); if (prototype) { @@ -1272,6 +1598,11 @@ export class ClassDynamicDispatch { type: DYN, loc: existingInit.loc, }; + // The initializer is a statement of the property bag helpers; + // their reused site summaries do not see an edit inside it. + if (this.propertyBag) this.sites.forget(this.propertyBag); + for (const fn of this.typedPropertyBags.values()) this.sites.forget(fn); + for (const fn of this.exactPropertyBags.values()) this.sites.forget(fn); changed = true; } } @@ -1516,31 +1847,68 @@ export class ClassDynamicDispatch { } return { kind: "call", callee: cast.fn.name, args: [expr.value], type, loc: expr.loc }; } + if (expr.kind === "libCall" && expr.fn === "dyn.inheritsErrorPrototype") { + if (!this.errorPrototypeHelper) { + const loc = expr.loc; + this.errorPrototypeHelper = { + name: "%dyn.class.inheritsErrorPrototype", + params: [{ localId: "value", name: "value", type: DYN }], + locals: [{ id: "value", name: "value", type: DYN, mutable: false }], + returnType: BOOL, + loc, + body: [ + { kind: "return", value: { ...expr, args: [varRef("value", DYN, loc)] }, loc }, + ], + }; + lowerer.liftedFns.push(this.errorPrototypeHelper); + this.generated.add(this.errorPrototypeHelper); + changed = true; + } + return { + kind: "call", + callee: this.errorPrototypeHelper.name, + args: expr.args, + type: BOOL, + loc: expr.loc, + }; + } if ( expr.kind === "libCall" && - expr.fn === "dyn.getPrototype" && + (expr.fn === "dyn.getPrototype" || expr.fn === "dyn.getPrototypeForIdentity") && expr.args[0]?.kind !== "dynObjLit" ) { if (expr.prototypeIdentityOnly) { - if (!this.prototypeIdentityHelper) { + let helper = this.prototypeIdentityHelpers.get(expr.fn); + if (!helper) { const loc = expr.loc; - this.prototypeIdentityHelper = { - name: "%dyn.class.prototypeIdentity", - params: [{ localId: "value", name: "value", type: DYN }], - locals: [{ id: "value", name: "value", type: DYN, mutable: false }], - returnType: DYN, - loc, - body: [ - { kind: "return", value: { ...expr, args: [varRef("value", DYN, loc)] }, loc }, - ], + helper = { + fn: { + name: + expr.fn === "dyn.getPrototype" + ? "%dyn.class.prototypeIdentity" + : "%dyn.class.prototypeChainIdentity", + params: [{ localId: "value", name: "value", type: DYN }], + locals: [{ id: "value", name: "value", type: DYN, mutable: false }], + returnType: DYN, + loc, + body: [ + { + kind: "return", + value: { ...expr, args: [varRef("value", DYN, loc)] }, + loc, + }, + ], + }, + classes: new Set(), }; - lowerer.liftedFns.push(this.prototypeIdentityHelper); - this.generated.add(this.prototypeIdentityHelper); + this.prototypeIdentityHelpers.set(expr.fn, helper); + lowerer.liftedFns.push(helper.fn); + this.generated.add(helper.fn); changed = true; } return { kind: "call", - callee: this.prototypeIdentityHelper.name, + callee: helper.fn.name, args: expr.args, type: DYN, loc: expr.loc, @@ -3488,6 +3856,43 @@ export class ClassDynamicDispatch { /** The receiver's own dynamic properties for a read: undefined while it has * none, and never stored, so reads stay valid on published objects. */ + /** Statements that return `className`'s stand-in prototype: one empty + * object per class, created on first use and kept in a module global. */ + private prototypeStandIn(lowerer: Lowerer, className: string, loc: SrcLoc): IrStmt[] { + const globalId = `%g.%prototype.standIn.${className}`; + const readyId = `${globalId}.ready`; + if (!this.prototypeStandIns.has(className)) { + this.prototypeStandIns.add(className); + lowerer.globalsList.push( + { id: globalId, name: `${className}.prototype`, type: DYN, mutable: true }, + { id: readyId, name: `${className}.prototype.ready`, type: BOOL, mutable: true }, + ); + } + return [ + { + kind: "if", + cond: { kind: "unary", op: "!", operand: varRef(readyId, BOOL, loc), type: BOOL, loc }, + then: [ + { + kind: "assign", + localId: globalId, + value: { kind: "dynObjLit", fields: [], type: DYN, loc }, + loc, + }, + { + kind: "assign", + localId: readyId, + value: { kind: "boolLit", value: true, type: BOOL, loc }, + loc, + }, + ], + else_: null, + loc, + }, + { kind: "return", value: varRef(globalId, DYN, loc), loc }, + ]; + } + private readPropertyBag(lowerer: Lowerer, receiver: IrExpr, loc: SrcLoc): IrExpr { const helper = classPropertiesHelper(lowerer, loc, true); this.generated.add(helper); @@ -4367,6 +4772,27 @@ export class ClassDynamicDispatch { * A class with constructible subclasses that have prototype data selects * it through its `%class.bagPrototype` helper; any other class reads its * own prototype data. */ + /** The lazy bag stores as they stand in the generated bodies, by class. + * Worklist passes that transform every function rebuild statement + * objects, so a recorded initializer can be a detached copy; a store that + * gains its prototype must be updated where it runs. */ + private liveBagInitializers(): Map[]> { + const live = new Map[]>(); + const add = (stmt: IrStmt | undefined): void => { + if (stmt?.kind !== "if") return; + const store = stmt.then[0]; + if (store?.kind !== "fieldSet" || store.field !== PROPERTY_BAG) return; + const list = live.get(store.className); + if (list) list.push(store); + else live.set(store.className, [store]); + }; + // The complete dispatch guards each class's lazy store by its receiver. + for (const stmt of this.propertyBag?.body ?? []) if (stmt.kind === "if") add(stmt.then[0]); + for (const helper of this.typedPropertyBags.values()) add(helper.body[0]); + for (const helper of this.exactPropertyBags.values()) add(helper.body[0]); + return live; + } + private bagPrototype( lowerer: Lowerer, current: ClassInfo, @@ -4443,6 +4869,7 @@ export class ClassDynamicDispatch { finalize(lowerer?: Lowerer): void { // Each table serves every site that reaches it. if (lowerer) for (const fn of this.generated) lowerer.shareHelper(fn); + if (lowerer) this.recordBagInitializers(lowerer); for (const { helper, initialize, orEmpty } of this.readPropertyBags) { const absent = helper.body[0]; if (isEmptyBag(initialize.value) && !orEmpty) helper.body.shift(); @@ -4463,6 +4890,82 @@ export class ClassDynamicDispatch { fn.name === READ_PROPERTIES_OR_EMPTY, ); } + if (lowerer) this.guardClassTables(lowerer); + } + + /** Each tracked class's bag initializer over a typed receiver (`this`), + * for the per-class field-creation helpers (trackClassFieldCreation), + * which create an instance's bag without the class-table dispatch. */ + private recordBagInitializers(lowerer: Lowerer): void { + for (const [className, initializers] of this.bagInitializers) { + const init = initializers[0]; + if (!init || !lowerer.classes.get(className)?.def.tracksOwnFields) continue; + const self = varRef("this", { kind: "object", className }, init.loc); + const value = withTypedReceiver(structuredClone(init.value), self); + if (mentionsLocal(value, "p.0")) continue; + // Lowerer stand-ins in unit tests may not carry the table. + lowerer.classBagInitializers?.set(className, value); + } + } + + /** Fast exits for the generated class tables: every class branch of + * the property-bag, normalization and static-bag helpers tests the + * value's capsule key (or class key), so a value of another kind (a + * plain object, the common case in untyped code) skips straight to the + * table's fallthrough instead of testing every class in the program. */ + private guardClassTables(lowerer: Lowerer): void { + const names = new Set([ + "%dyn.class.properties", + READ_PROPERTIES, + READ_PROPERTIES_OR_EMPTY, + "%dyn.class.normalize", + "%dyn.class.staticProperties", + ]); + for (const fn of lowerer.liftedFns) { + if (!names.has(fn.name) || fn.body.length < 2) continue; + const param = fn.params[0]?.localId; + if (param === undefined) continue; + const last = fn.body[fn.body.length - 1]!; + if (last.kind !== "return" || last.value === null || !returnsParam(last.value, param)) + continue; + let test: "classCapsule" | "funcKind" | null = null; + let ok = true; + for (const stmt of fn.body.slice(0, -1)) { + if ( + stmt.kind === "assign" && + stmt.localId === param && + stmt.value.kind === "call" && + stmt.value.callee === "%dyn.class.normalize" + ) + continue; + const kind = + stmt.kind === "if" && stmt.else_ === null ? branchKind(stmt.cond, param) : null; + if (kind === null || (test !== null && kind !== test)) { + ok = false; + break; + } + test = kind; + } + if (!ok || test === null) continue; + const first = fn.body[0]!; + if (first.kind === "if" && first.cond.kind === "dynTest" && first.cond.test === test) + continue; + const loc = fn.loc; + fn.body.unshift({ + kind: "if", + cond: { + kind: "dynTest", + test, + negated: true, + value: varRef(param, DYN, loc), + type: BOOL, + loc, + }, + then: [structuredClone(last)], + else_: null, + loc, + }); + } } private normalizedDispatches(): IrFunction[] { @@ -4536,10 +5039,17 @@ export class ClassDynamicDispatch { this.normalizationKey = key; const loc = this.normalization.loc; const value = varRef("value", DYN, loc); - this.normalization.body = entries.map(({ capsule, children }): IrStmt => { + // The checked receiver is unboxed once per capsule branch, not once + // per subclass test. + this.normalization.locals = this.normalization.locals.filter( + (local) => !local.id.startsWith("%normalized."), + ); + this.normalization.body = entries.map(({ capsule, children }, index): IrStmt => { const type: IrType = { kind: "object", className: capsule.def.name }; - const receiver: IrExpr = { kind: "dynCheck", value, type, loc }; - return { + const id = `%normalized.${index}`; + this.normalization!.locals.push({ id, name: id, type, mutable: false }); + const receiver: IrExpr = varRef(id, type, loc); + const branch: IrStmt = { kind: "if", cond: { kind: "libCall", @@ -4580,6 +5090,15 @@ export class ClassDynamicDispatch { else_: null, loc, }; + // The capsule test stays the branch condition; its body unboxes. + if (branch.kind === "if") + branch.then.unshift({ + kind: "varDecl", + localId: id, + init: { kind: "dynCheck", value, type, loc }, + loc, + }); + return branch; }); this.normalization.body.push({ kind: "return", value, loc }); } @@ -5640,13 +6159,16 @@ export class ClassDynamicDispatch { const readableT: IrType = { kind: "object", className: "%Readable" }; const op = (fn: string, args: IrExpr[], type: IrType): IrStmt[] => ret({ kind: "libCall", fn: fn as IrLibFn, args: [receiver, ...args], type, loc }); + // Node passes every callback its { signal }; the callback's + // declared parameters are not known here. + const sig = (): IrExpr => operatorSignalOptions(lowerer, arg(1), loc); switch (method) { case "map": - return op("readable.opMap", [arg(0), arg(1)], readableT); + return op("readable.opMap", [arg(0), arg(1), sig()], readableT); case "filter": - return op("readable.opFilter", [arg(0), arg(1)], readableT); + return op("readable.opFilter", [arg(0), arg(1), sig()], readableT); case "flatMap": - return op("readable.opFlatMap", [arg(0), arg(1)], readableT); + return op("readable.opFlatMap", [arg(0), arg(1), sig()], readableT); case "drop": return op("readable.opDrop", [arg(0), arg(1)], readableT); case "take": @@ -5654,25 +6176,49 @@ export class ClassDynamicDispatch { case "toArray": return op("readable.opToArray", [arg(0)], { kind: "promise", inner: DYN }); case "forEach": - return op("readable.opForEach", [arg(0), arg(1)], { kind: "promise", inner: VOID }); + return op("readable.opForEach", [arg(0), arg(1), sig()], { + kind: "promise", + inner: VOID, + }); case "some": - return op("readable.opSome", [arg(0), arg(1)], { kind: "promise", inner: BOOL }); + return op("readable.opSome", [arg(0), arg(1), sig()], { kind: "promise", inner: BOOL }); case "every": - return op("readable.opEvery", [arg(0), arg(1)], { kind: "promise", inner: BOOL }); + return op("readable.opEvery", [arg(0), arg(1), sig()], { + kind: "promise", + inner: BOOL, + }); case "find": - return op("readable.opFind", [arg(0), arg(1)], { kind: "promise", inner: DYN }); - case "reduce": - return op( - "readable.opReduce", - [ - arg(0), - arg(1), - { kind: "boolLit", value: dispatch.source.args.length >= 2, type: BOOL, loc }, - arg(2), - { kind: "numLit", value: 0, type: F64, loc }, - ], - { kind: "promise", inner: DYN }, - ); + return op("readable.opFind", [arg(0), arg(1), sig()], { kind: "promise", inner: DYN }); + case "reduce": { + // the dispatch function's locals: there is no function + // context while dispatch bodies are built + const setup = reduceSignalSetup(lowerer, arg(2), loc, (name, type) => { + const local = { + id: `${name}.${dispatch.fn.locals.length}`, + name, + type, + mutable: false, + }; + dispatch.fn.locals.push(local); + return local; + }); + return [ + ...setup.stmts, + ...op( + "readable.opReduceSignal", + [ + arg(0), + arg(1), + { kind: "boolLit", value: dispatch.source.args.length >= 2, type: BOOL, loc }, + arg(2), + { kind: "numLit", value: 0, type: F64, loc }, + setup.abort, + setup.cbOpts, + ], + { kind: "promise", inner: DYN }, + ), + ]; + } } } if (method === "destroy" && (!incoming.length || incoming[0]?.kind === "unitLit")) @@ -5915,3 +6461,11 @@ const INHERITED_OBJECT_METHODS: ReadonlySet = new Set([ "hasOwnProperty", "propertyIsEnumerable", ]); + +/** Whether a class or one of its bases has a heritage evaluated at run + * time, whose prototype chain may reach Error.prototype. */ +function hasRuntimeHeritage(info: ClassInfo): boolean { + for (let current: ClassInfo | null = info; current; current = current.base) + if (current.callableBase) return true; + return false; +} diff --git a/packages/compiler/src/frontend/lowering/class-iterables.ts b/packages/compiler/src/frontend/lowering/class-iterables.ts index 8c58fc0bd..0f4d6c254 100644 --- a/packages/compiler/src/frontend/lowering/class-iterables.ts +++ b/packages/compiler/src/frontend/lowering/class-iterables.ts @@ -1,13 +1,18 @@ import type { IrExpr, IrType } from "../../ir/ir.js"; import type { Lowerer } from "./lowerer.js"; import { accessorCall } from "./lower-classes.js"; +import { iterableGenerator, iterableUnionOf } from "./iterable-values.js"; /** The generator a class instance's `[Symbol.iterator]` returns, when that - * method is a generator method taking no arguments: `obj[Symbol.iterator]()`, + * method is a generator method taking no arguments (or the iterator an + * Iterable value opens over its current arm): `obj[Symbol.iterator]()`, * called once, as for-of, spread and Array.from call it. Null for other * values, and for classes whose iterator the program can replace at run * time (an instance property, a prototype write). */ export function classGeneratorIterable(lowerer: Lowerer, value: IrExpr): IrExpr | null { + // An Iterable value opens an iterator over its current arm. + const iterable = iterableUnionOf(lowerer, value.type); + if (iterable) return iterableGenerator(lowerer, value, iterable); if (value.type.kind !== "object" || !iteratesThroughGenerator(lowerer, value.type)) return null; const className = value.type.className; const ret = lowerer.findMethodOn(lowerer.classes.get(className)!, "sym:iterator")!.sig.ret; @@ -17,6 +22,7 @@ export function classGeneratorIterable(lowerer: Lowerer, value: IrExpr): IrExpr /** Whether values of `type` iterate through a generator method (the check * classGeneratorIterable makes, from the type alone). */ export function iteratesThroughGenerator(lowerer: Lowerer, type: IrType): boolean { + if (iterableUnionOf(lowerer, type)) return true; if (type.kind !== "object") return false; const info = lowerer.classes.get(type.className); if (!info || info.def.runtime || info.fields.has("sym:iterator")) return false; diff --git a/packages/compiler/src/frontend/lowering/class-method-mutations.ts b/packages/compiler/src/frontend/lowering/class-method-mutations.ts index 7629377d4..c5cd444f8 100644 --- a/packages/compiler/src/frontend/lowering/class-method-mutations.ts +++ b/packages/compiler/src/frontend/lowering/class-method-mutations.ts @@ -390,6 +390,13 @@ export function collectClassMethodMutations( markTarget(node.initializer, node); if (ts.isPropertyAccessExpression(node) && isErrorPrototypeValue(lowerer, node)) lowerer.errorPrototypeValues = true; + // A heritage evaluated at run time may be Error, which links the + // class's instances to Error.prototype. + if ( + (ts.isClassDeclaration(node) || ts.isClassExpression(node)) && + runtimeHeritage(lowerer, node) + ) + lowerer.errorPrototypeValues = true; if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) { const base = unwrap(node.expression); const prototype = @@ -631,3 +638,22 @@ export function collectClassMethodMutations( if (!receiverFields.has(field)) lowerer.receiverFreeCallbackFields.add(field); } } + +/** Whether a class's `extends` expression is a value computed at run time: + * anything other than a name of a class declaration or of a builtin + * constructor (`extends Parent` over `const Parent = params.Parent ?? Object`, + * `extends make()`). */ +function runtimeHeritage( + lowerer: Lowerer, + node: ts.ClassDeclaration | ts.ClassExpression, +): boolean { + const heritage = node.heritageClauses?.nodes.find( + (clause) => clause.token === ts.SyntaxKind.ExtendsKeyword, + )?.types?.nodes[0]?.expression; + if (heritage === undefined) return false; + if (!ts.isIdentifier(heritage) && !ts.isPropertyAccessExpression(heritage)) return true; + let symbol = lowerer.checker.getSymbolAtLocation(heritage); + if (symbol === undefined) return true; + if (symbol.flags & ts.SymbolFlags.Alias) symbol = lowerer.checker.getAliasedSymbol(symbol); + return !(symbol.flags & ts.SymbolFlags.Class) && !lowerer.isStdlibSymbol(symbol); +} diff --git a/packages/compiler/src/frontend/lowering/class-prototypes.ts b/packages/compiler/src/frontend/lowering/class-prototypes.ts index 2a05cf977..cb2ab2280 100644 --- a/packages/compiler/src/frontend/lowering/class-prototypes.ts +++ b/packages/compiler/src/frontend/lowering/class-prototypes.ts @@ -207,12 +207,21 @@ export function classPrototypeData( // promise-returning signature) gets no data slot. const boxedValue = lowerer.coerceToExpected(compiled, DYN); if (boxedValue.type.kind !== "dyn") continue; + const originalId = `%g.${info.def.prototypeDataHelper}.original.${method}`; + lowerer.globalsList.push({ + id: originalId, + name: `${info.def.jsName ?? info.def.name}.prototype.${method}.original`, + type: DYN, + mutable: true, + }); + info.prototypeOriginals ??= new Map(); + info.prototypeOriginals.set(method, originalId); const descriptor: IrExpr = { kind: "dynObjLit", fields: [ { key: { kind: "strLit", value: "value", type: STRING, loc }, - value: boxedValue, + value: varRef(originalId, DYN, loc), }, { key: { kind: "strLit", value: "writable", type: STRING, loc }, @@ -227,6 +236,8 @@ export function classPrototypeData( loc, }; const init = helper.body[0]!; + if (init.kind === "if") + init.then.push({ kind: "assign", localId: originalId, value: boxedValue, loc }); if (init.kind === "if") init.then.push({ kind: "exprStmt", @@ -278,6 +289,16 @@ export function hasClassPrototypeData(info: ClassInfo): boolean { ); } +/** Methods and accessors that live on the class's prototype. */ +function declaresPrototypeMembers(decl: ts.ClassLikeDeclaration): boolean { + return decl.members.nodes.some( + (member) => + (ts.isMethodDeclaration(member) || ts.isAccessor(member)) && + !ts.isPrivateIdentifier(member.name) && + !ts.getModifiers(member)?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword), + ); +} + /** Populate the shared prototype view before exposing it to reflection. * Methods and accessors are non-enumerable and retain declaration identity. */ export function reflectClassPrototype( @@ -307,7 +328,10 @@ export function reflectClassPrototype( // Local JavaScript factories initialize captured prototype members on // their constructor value, so the shared runtime view is already complete. if (info.localClass && info.runtimePrototypeMembers) return prototype; - if (info.localClass) + // A function-local class declaring no prototype members (zod 4's + // `class Definition extends Parent {}`) has nothing to install: its + // shared runtime view is complete once its base's is. + if (info.localClass && declaresPrototypeMembers(info.decl)) return refuse("prototype reflection on a function-local class is not supported yet"); if (info.base && !info.base.builtinError) { reflectClassPrototype( @@ -533,6 +557,15 @@ export function lowerClassPrototypeData( if (name === "constructor") { return reflectClassPrototype(lowerer, info, locOf(expr), classValueRef(lowerer, info, expr)); } + // The whole prototype of a function-local class: the shared view + // Object.getPrototypeOf answers for its instances. + if (name === null && info.localClass) { + const receiver = lowerer.lowerExprExpecting(expr.expression, { + kind: "classval", + className: info.def.name, + }); + return reflectClassPrototype(lowerer, info, locOf(expr), receiver); + } if (name === null || (isCompiledPrototypeMember(lowerer, info, name) && !method)) { lowerer.unsupported( "SC1090", diff --git a/packages/compiler/src/frontend/lowering/class-runtime-statics.ts b/packages/compiler/src/frontend/lowering/class-runtime-statics.ts index 310c8f389..707c916ec 100644 --- a/packages/compiler/src/frontend/lowering/class-runtime-statics.ts +++ b/packages/compiler/src/frontend/lowering/class-runtime-statics.ts @@ -1,6 +1,6 @@ import { dynUndefinedExpr, varRef } from "../../ir/build.js"; import * as ts from "../ts7/adapter.js"; -import { BOOL, DYN, STRING, type IrExpr, type IrStmt } from "../../ir/ir.js"; +import { BOOL, DYN, STRING, type IrExpr, type IrLocal, type IrStmt } from "../../ir/ir.js"; import { locOf } from "../program.js"; import type { Lowerer } from "./lowerer.js"; import { classValueRef, type ClassInfo } from "./lower-classes.js"; @@ -15,9 +15,29 @@ export function hasRuntimeStatics(info: ClassInfo): boolean { /** Constructor descriptors preserve inherited getter receivers and static * initialization order. Factory evaluations additionally own independent - * fields and closures when the same factory is called repeatedly. */ -export function initializeRuntimeStatics(lowerer: Lowerer, info: ClassInfo, value: IrExpr): IrExpr { - if (!info.runtimeStatics && !info.runtimePrototypeMembers) return value; + * fields and closures when the same factory is called repeatedly. + * + * `binding` is the local that holds the class under its own name (a + * function-local class declaration's binding, or the inner name of a + * function-local class expression whose body names itself), declared + * without a value. It is assigned before any static member is defined, so + * static initializers and methods read the class through it. */ +export function initializeRuntimeStatics( + lowerer: Lowerer, + info: ClassInfo, + value: IrExpr, + binding?: IrLocal, +): IrExpr { + if (!info.runtimeStatics && !info.runtimePrototypeMembers) { + if (!binding) return value; + return { + kind: "seqExpr", + stmts: [{ kind: "assign", localId: binding.id, value, loc: value.loc }], + result: varRef(binding.id, binding.type, value.loc), + type: binding.type, + loc: value.loc, + }; + } const loc = value.loc; const local = lowerer.declareHiddenLocal("%staticClass", value.type); const receiver = varRef(local.id, value.type, loc); @@ -28,6 +48,16 @@ export function initializeRuntimeStatics(lowerer: Lowerer, info: ClassInfo, valu : dynUndefinedExpr(loc); const statements: IrStmt[] = [ { kind: "varDecl", localId: local.id, init: value, loc }, + ...(binding + ? [ + { + kind: "assign" as const, + localId: binding.id, + value: lowerer.coerceToExpected(receiver, binding.type), + loc, + }, + ] + : []), { kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.classInherit", args: [boxed, base], type: DYN, loc }, diff --git a/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts b/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts index 53b0748b4..f42663a13 100644 --- a/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts +++ b/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts @@ -1,7 +1,7 @@ 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"; +import { canDynCheckTo, isUnitType, typeEquals, UNDEFINED_T } from "../../../ir/ir.js"; import { type ClassInfo, findMethodOn, findGenericMethodOn } from "../lower-classes.js"; import type { Lowerer } from "../lowerer.js"; @@ -21,6 +21,22 @@ export function widthLiftPlan(lowerer: Lowerer, src: IrType, dst: IrType): Width if (dst.kind === "dyn" && src.kind !== "dyn" && lowerer.dynConvertible(src)) { return { how: "dynIn" }; } + // An 'unknown' (dyn) SOURCE field into a typed slot — a field TypeScript + // types `any` (a JSON.parse read), which the checker lets into any + // declared field type: the checked extraction, the same dynCheck a + // typed binding initialized from that value applies. + if ( + src.kind === "dyn" && + dst.kind !== "dyn" && + dst.kind !== "union" && + canDynCheckTo( + dst, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + ) + ) { + return { how: "dynOut" }; + } if (dst.kind === "union") { if (src.kind === "union") { if (lowerer.unionRetagMappable(src.unionId, dst.unionId)) return { how: "retag" }; @@ -108,6 +124,10 @@ export function widthLiftPlan(lowerer: Lowerer, src: IrType, dst: IrType): Width return { how: "discriminantRoute" }; } } + if (selected.length === 2 && src.kind === "record") { + const route = presenceRoute(lowerer, src.shapeId, selected[0]!, selected[1]!); + if (route) return route; + } if (selected.length !== 1) return null; return { how: "liftWrap", tag: selected[0]!.tag, arm: selected[0]!.arm }; } @@ -202,6 +222,42 @@ export function widthLiftPlan(lowerer: Lowerer, src: IrType, dst: IrType): Width return null; } +/** Two record arms a source record fits, where one arm declares a field + * the other lacks and the source types that field `T | undefined` + * (`{ count, errors: errs.length ? errs : undefined }` into `{ count; + * errors: string[] } | { count }`): the field's value picks the arm at + * run time. Null for any other pair. */ +function presenceRoute( + lowerer: Lowerer, + sourceId: string, + a: { tag: number; arm: IrType }, + b: { tag: number; arm: IrType }, +): WidthLift | null { + const source = lowerer.shapes.get(sourceId); + if (!source || source.tuple || source.indexValue || a.arm.kind !== "record") return null; + if (b.arm.kind !== "record") return null; + const shapeA = lowerer.shapes.get(a.arm.shapeId); + const shapeB = lowerer.shapes.get(b.arm.shapeId); + if (!shapeA || !shapeB) return null; + const optional = (t: IrType): boolean => + t.kind === "union" && lowerer.armTag(t.unionId, UNDEFINED_T) >= 0; + for (const [present, absent, presentShape, absentShape] of [ + [a, b, shapeA, shapeB], + [b, a, shapeB, shapeA], + ] as const) { + const extra = presentShape.fields.filter( + (f) => !absentShape.fields.some((g) => g.name === f.name), + ); + if (extra.length !== 1 || optional(extra[0]!.type)) continue; + if (absentShape.fields.some((g) => !presentShape.fields.some((f) => f.name === g.name))) + continue; + const field = source.fields.find((f) => f.name === extra[0]!.name); + if (!field || !optional(field.type)) continue; + return { how: "presenceRoute", field: field.name, present, absent }; + } + return null; +} + /** True for the unit-only element types (`(null | undefined)[]`, the * `readonly []` mapping): a union whose every arm is a unit. */ export function unitOnlyElem(lowerer: Lowerer, t: IrType): boolean { diff --git a/packages/compiler/src/frontend/lowering/coercions/structural.ts b/packages/compiler/src/frontend/lowering/coercions/structural.ts index 5b5e283b3..4321a5ead 100644 --- a/packages/compiler/src/frontend/lowering/coercions/structural.ts +++ b/packages/compiler/src/frontend/lowering/coercions/structural.ts @@ -6,7 +6,7 @@ import { buildArrayConversion } from "./builders.js"; import type { WidthLift } from "../width-lift.js"; import { InternalCompilerError } from "../../../errors.js"; import type { IrExpr, IrFunction, IrRecordShape, IrStmt, IrType, SrcLoc } from "../../../ir/ir.js"; -import { BOOL, DYN, F64, STRING } from "../../../ir/ir.js"; +import { BOOL, DYN, F64, STRING, UNDEFINED_T } from "../../../ir/ir.js"; import { typeKey } from "../../type-mapper.js"; import { findStaticOn, findGenericStaticOn } from "../lower-classes.js"; import { lowerRecordOvfCaptureHelper } from "../containers/indexed-objects.js"; @@ -155,6 +155,76 @@ export function applyWidthLift( loc, }; } + case "presenceRoute": { + if (dst.kind !== "union" || value.type.kind !== "record") + throw new InternalCompilerError("lowerer bug: presenceRoute lift shape"); + const sourceType = value.type; + const fieldType = lowerer.shapes + .get(sourceType.shapeId) + ?.fields.find((f) => f.name === lift.field)?.type; + if (fieldType?.kind !== "union") + throw new InternalCompilerError("lowerer bug: presenceRoute field shape"); + // One helper per pair: the conversion reads the source twice. + const key = `presence:${typeKey(sourceType)}:${dst.unionId}:${lift.field}`; + let helper = lowerer.valueHelpers.get(key); + if (!helper) { + helper = `%union.presence.${lowerer.valueHelpers.size}`; + lowerer.valueHelpers.set(key, helper); + const source = (): IrExpr => varRef("v.0", sourceType, loc); + const arm = (route: { tag: number; arm: IrType }): IrExpr => { + const inner = lowerer.widthLiftPlan(sourceType, route.arm); + if (!inner) + throw new InternalCompilerError( + "lowerer bug: planned presenceRoute arm stopped lifting", + ); + return { + kind: "unionWrap", + unionId: dst.unionId, + tag: route.tag, + value: lowerer.applyWidthLift(inner, source(), route.arm, loc), + type: dst, + loc, + }; + }; + lowerer.liftedFns.push({ + name: helper, + params: [{ localId: "v.0", name: "v", type: sourceType }], + returnType: dst, + locals: [{ id: "v.0", name: "v", type: sourceType, mutable: false }], + body: [ + { + kind: "return", + value: { + kind: "ternary", + cond: { + kind: "unionIsTag", + unionId: fieldType.unionId, + tag: lowerer.armTag(fieldType.unionId, UNDEFINED_T), + value: { + kind: "recordGet", + obj: source(), + shapeId: sourceType.shapeId, + field: lift.field, + type: fieldType, + loc, + }, + negated: false, + type: BOOL, + loc, + }, + then: arm(lift.absent), + else_: arm(lift.present), + type: dst, + loc, + }, + loc, + }, + ], + loc, + }); + } + return { kind: "call", callee: helper, args: [value], type: dst, loc }; + } case "width": { if (dst.kind !== "record" || value.type.kind !== "record") throw new InternalCompilerError("lowerer bug: width lift shape"); @@ -218,8 +288,18 @@ export function applyWidthLift( case "dynIn": { if (dst.kind !== "dyn") throw new InternalCompilerError("lowerer bug: dynIn lift against a non-dyn slot"); + // The declaration-typed boundary keeps user values' identity when + // they cross into package code (decl-projection.ts). + if ( + lowerer.declBoundary && + !/[\\/]node_modules[\\/]/.test(loc.file) && + liveBoxable(lowerer, value.type) + ) + return { kind: "dynFrom", value, liveRef: true, type: DYN, loc }; return { kind: "dynFrom", value, type: DYN, loc }; } + case "dynOut": + return { kind: "dynCheck", value, type: dst, loc }; case "upcast": { if (dst.kind !== "object" || value.type.kind !== "object") throw new InternalCompilerError("lowerer bug: upcast lift shape"); @@ -703,3 +783,18 @@ function buildRecordProjection( loc, }; } + +/** Records and arrays (and unions holding them), whose identity a live + * capsule keeps. */ +function liveBoxable(lowerer: Lowerer, t: IrType, seen = new Set()): boolean { + if (t.kind === "record") { + const shape = lowerer.shapes.get(t.shapeId); + return shape !== undefined && !shape.tuple; + } + if (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((arm) => liveBoxable(lowerer, arm, seen)) ?? false + ); +} 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 a22a82bb1..b98000fa1 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-callback-call.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-callback-call.ts @@ -1,10 +1,10 @@ import * as ts from "../../ts7/adapter.js"; +import { lowerOptionalIndex } from "../undefined-index.js"; import { locOf } from "../../program.js"; import { varRef } from "../../../ir/build.js"; import { DYN, type IrExpr, type IrStmt, isUnitType } from "../../../ir/ir.js"; import type { Lowerer } from "../lowerer.js"; import { completeFuncValueArgs } from "../call-arguments.js"; -import { lowerOptionalNumber } from "../lower-exprs.js"; import { lowerSafeIndexRead } from "../array-values.js"; /** Retain native function slots during indexed calls. The array remains @@ -37,7 +37,7 @@ export function lowerArrayCallbackCall( return varRef(local.id, local.type, loc); }; const receiver = store(array, "%callbackArray"); - const index = lowerOptionalNumber( + const index = lowerOptionalIndex( lowerer, lowerer.lowerExpr(access.argumentExpression), locOf(access.argumentExpression), diff --git a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts index 612c47ea0..86f214bc1 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts @@ -59,6 +59,7 @@ import { } from "./array-iteration.js"; import { dynamicReceiverThrows } from "./dynamic-receivers.js"; import { defaultAfterUndefined, lowerStaticallyUndefinedArgument } from "../optional-arguments.js"; +import { checkedFilterNarrowing } from "./filter-predicates.js"; /** `a.map(fn)` / `a.filter(fn)` / `a.forEach(fn)` desugar to a direct * call of a synthetic module function — one per method + element/result @@ -123,10 +124,11 @@ export function lowerArrayHofCall( } lowerer.badType(argNode, lowerer.typeOf(argNode)); } - const outElem = + const filtered = method === "filter" ? filterResultElem(lowerer, call, argNode, elem, valueT, booleanFilter) - : callbackArrayElement(lowerer, call, fnRet); + : null; + const outElem = filtered !== null ? filtered.elem : callbackArrayElement(lowerer, call, fnRet); // A filter that re-tags its elements keeps the single union-typed visit. const fast = present || booleanFilter || (method === "filter" && !typeEquals(outElem, elem)) @@ -145,19 +147,33 @@ export function lowerArrayHofCall( present, ); const resultType: IrType = method === "map" || method === "filter" ? arrayOf(outElem) : VOID; - return { + const result: IrExpr = { kind: "call", callee: helper, args: [receiver, fnArg, ...(fast ? [fast] : [])], type: resultType, loc, }; + const narrowing = filtered?.narrowing ?? null; + if (narrowing === null) return result; + return { + kind: "call", + callee: narrowing.helper, + args: [result], + type: arrayOf(narrowing.elem), + loc, + }; } -/** The checker-selected element of a filter result, with the same trust - * boundary as the historical narrowing filter: an inferred inline type - * predicate may prove one source arm; a written predicate annotation may - * not. Boolean is safe when every discarded arm is a nullish unit. */ +/** The element a filter keeps, and the checked conversion of the result + * to the checker's narrower element when the predicate does not prove it. + * An inferred inline type predicate proves one source arm, or a class + * view, so the loop keeps that type directly. A written predicate (`(p): p + * is Cat => ...`, a guard function passed as a value) is an assertion + * TypeScript does not check, and an inferred predicate that keeps several + * arms has no single arm to re-tag: the loop keeps the receiver's element + * and `narrowing` (checkedFilterNarrowing) converts each kept element. + * Boolean is safe when every discarded arm is a nullish unit. */ function filterResultElem( lowerer: Lowerer, call: ts.CallExpression, @@ -165,7 +181,37 @@ function filterResultElem( elem: IrType, valueT: IrType, booleanFilter: boolean, -): IrType { +): { elem: IrType; narrowing: { helper: string; elem: IrType } | null } { + const kept = filterKeptElem(lowerer, call, argNode, elem, valueT, booleanFilter); + if (typeof kept !== "string") return { elem: kept, narrowing: null }; + return { elem, narrowing: { helper: kept, elem: filterCheckerElem(lowerer, call, elem) } }; +} + +/** The checker's spelling of a filter result's element type. */ +function filterTypeText(lowerer: Lowerer, call: ts.CallExpression): string { + const result = lowerer.typeOf(call); + const element = lowerer.checker.isArrayType(result) + ? lowerer.checker.getTypeArguments(result as ts.TypeReference)[0] + : undefined; + return element === undefined ? "" : lowerer.checker.typeToString(element); +} + +/** The checker's element of a filter result, or the receiver's when the + * filter introduces no refinement. */ +function filterCheckerElem(lowerer: Lowerer, call: ts.CallExpression, elem: IrType): IrType { + const result = lowerer.mapTypeOf(lowerer.typeOf(call)); + return result?.kind === "array" ? result.elem : elem; +} + +/** filterResultElem's element, or the name of the checked narrowing helper. */ +function filterKeptElem( + lowerer: Lowerer, + call: ts.CallExpression, + argNode: ts.Expression, + elem: IrType, + valueT: IrType, + booleanFilter: boolean, +): IrType | string { const callT = lowerer.typeOf(call); const result = lowerer.mapTypeOf(callT); let outElem = result?.kind === "array" ? result.elem : elem; @@ -191,23 +237,31 @@ function filterResultElem( "keep the receiver's element type instead — annotate the callback's return ': boolean' " + "(the checker then skips the predicate) or annotate the result with the receiver's own " + "element type — and narrow the elements after"; - if (!booleanFilter) { + const written = + !booleanFilter && + ((!ts.isArrowFunction(argNode) && !ts.isFunctionExpression(argNode)) || + argNode.type !== undefined); + const narrowing = booleanFilter + ? null + : (): string | null => + checkedFilterNarrowing(lowerer, elem, outElem, filterTypeText(lowerer, call), locOf(call)); + if (written) { + const helper = narrowing!(); + if (helper !== null) return helper; if (!ts.isArrowFunction(argNode) && !ts.isFunctionExpression(argNode)) { lowerer.unsupported( "SC1090", argNode, - `narrowing '.filter' through a callback VALUE (only an inline callback whose predicate ` + - `the checker inferred can re-tag — ${annotateEscape})`, - ); - } - if (argNode.type) { - lowerer.unsupported( - "SC1090", - argNode, - `narrowing '.filter' with a hand-written type predicate (a written 'x is T' is an ` + - `unchecked assertion nothing validates at runtime — ${annotateEscape})`, + `narrowing '.filter' to '${lowerer.fmt(outElem)}' through a callback VALUE (no checked ` + + `conversion from '${lowerer.fmt(elem)}' — ${annotateEscape})`, ); } + lowerer.unsupported( + "SC1090", + argNode, + `narrowing '.filter' to '${lowerer.fmt(outElem)}' with a hand-written type predicate (no ` + + `checked conversion from '${lowerer.fmt(elem)}' — ${annotateEscape})`, + ); } const classRefinement = !booleanFilter && outElem.kind === "object" && canAssertClassValue(lowerer, valueT, outElem); @@ -215,6 +269,8 @@ function filterResultElem( !classRefinement && (valueT.kind !== "union" || lowerer.armTag(valueT.unionId, outElem) < 0) ) { + const helper = narrowing?.() ?? null; + if (helper !== null) return helper; const multiArm = outElem.kind === "union"; lowerer.unsupported( "SC1090", @@ -778,6 +834,10 @@ function buildArrayHofFn( loc, }, readLen, + // map's result has the receiver's length, holes where the receiver + // has them. Storage for it is reserved first and the length is set + // after the loop, so a result without holes is filled by appends and + // gets no state bytes. ...(method === "map" ? [ { @@ -785,6 +845,7 @@ function buildArrayHofFn( arr: varRef("out.0", outT, loc), length: varRef("n.0", F64, loc), loc, + reserve: true, } satisfies IrStmt, ] : []), @@ -816,6 +877,16 @@ function buildArrayHofFn( loc, }, ]), + ...(method === "map" + ? [ + { + kind: "arraySetLength", + arr: varRef("out.0", outT, loc), + length: varRef("n.0", F64, loc), + loc, + } satisfies IrStmt, + ] + : []), { kind: "return", value: varRef("out.0", outT, loc), loc }, ]; } else if (method === "filter" && fastT) { @@ -1501,6 +1572,19 @@ export function lowerArrayFlatMapCall( } if (fnRet.kind === "union") { const def = lowerer.unions.get(fnRet.unionId); + const plan = def?.arms.some((a) => a.kind === "array") + ? flatMapUnionPlan(lowerer, call, argNode, fnRet) + : null; + if (plan) { + const helper = flatMapUnionHelper(lowerer, elem, fnRet, arity, plan, loc, present); + return { + kind: "call", + callee: helper, + args: [receiver, fnArg], + type: arrayOf(plan.outElem), + loc, + }; + } if (def?.arms.some((a) => a.kind === "array")) { lowerer.unsupported( "SC1090", @@ -1566,6 +1650,278 @@ export function lowerArrayFlatMapCall( }; } +/** How each arm of a flatMap callback's union result contributes to the + * output: an array arm appends its elements, an array arm the checker + * types `never[]` (an empty literal such as `s ? s : []`) appends nothing, + * and any other arm appends itself. */ +interface FlatMapUnionPlan { + outElem: IrType; + arms: ("spread" | "empty" | "value")[]; +} + +/** The plan for a callback returning `T | U[]`: every non-array arm must + * convert into the result's element type without a check, and every + * array arm's element type must be the result's. Null when an arm does + * not. */ +function flatMapUnionPlan( + lowerer: Lowerer, + call: ts.CallExpression, + argNode: ts.Expression, + fnRet: IrType & { kind: "union" }, +): FlatMapUnionPlan | null { + const out = lowerer.mapTypeOf(lowerer.typeOf(call)); + if (out?.kind !== "array") return null; + const outElem = out.elem; + const arms = lowerer.unions.get(fnRet.unionId)?.arms; + if (!arms) return null; + // The `never[]` constituents of the callback's checked result. + const emptyArms: IrType[] = []; + const signature = lowerer.checker.getCallSignatures(lowerer.typeOf(argNode))[0]; + if (signature) { + const ret = lowerer.checker.getReturnTypeOfSignature(signature); + for (const part of ret.isUnionType() ? ts.constituentTypes(ret) : [ret]) { + if (!lowerer.checker.isArrayType(part)) continue; + const [element] = lowerer.checker.getTypeArguments(part as ts.TypeReference); + if (element === undefined || (element.flags & ts.TypeFlags.Never) === 0) continue; + const mapped = lowerer.mapTypeOf(part); + if (mapped) emptyArms.push(mapped); + } + } + const converts = (t: IrType): boolean => { + if (typeEquals(t, outElem)) return true; + if (outElem.kind !== "union") return false; + return lowerer.armTag(outElem.unionId, t) >= 0; + }; + const plan: FlatMapUnionPlan["arms"] = []; + for (const arm of arms) { + if (arm.kind === "array" && emptyArms.some((e) => typeEquals(e, arm))) plan.push("empty"); + // An array arm copies its elements as they are stored, a present + // undefined included, so its element type is the result's. + else if (arm.kind === "array" && typeEquals(arm.elem, outElem)) plan.push("spread"); + else if (arm.kind !== "array" && converts(arm)) plan.push("value"); + else return null; + } + return { outElem, arms: plan }; +} + +/** The flatMap loop over a union-returning callback: each result tests + * its arm and appends its elements, nothing, or itself (Node's + * IsArray decision per value). */ +function flatMapUnionHelper( + lowerer: Lowerer, + elem: IrType, + fnRet: IrType & { kind: "union" }, + arity: number, + plan: FlatMapUnionPlan, + loc: SrcLoc, + present: boolean, +): string { + // The plan is part of the key: one union result type can carry an empty + // `never[]` arm in one callback and a populated array arm in another. + const key = `flatMapUnion:${typeKey(elem)}:${typeKey(fnRet)}:${typeKey(plan.outElem)}:${plan.arms.join(",")}:${arity}${present ? ":present" : ""}`; + const existing = lowerer.arrHofHelpers.get(key); + if (existing) return existing; + const name = `%arr.flatMap.${lowerer.arrHofHelpers.size}`; + lowerer.arrHofHelpers.set(key, name); + const arrT = arrayOf(elem); + const outT = arrayOf(plan.outElem); + const fnT = funcOf([callbackValueType(lowerer, elem, present), F64, arrT].slice(0, arity), fnRet); + const arms = lowerer.unions.get(fnRet.unionId)!.arms; + const outRef = (): IrExpr => varRef("out.0", outT, loc); + const resultRef = (): IrExpr => varRef("r.0", fnRet, loc); + const outLength = (): IrExpr => ({ + kind: "arrIntrinsic", + method: "length", + receiver: outRef(), + args: [], + type: F64, + loc, + }); + const locals: IrLocal[] = [ + ...arrayCallbackLocals(arrT, fnT), + { id: "out.0", name: "out", type: outT, mutable: false }, + { id: "r.0", name: "r", type: fnRet, mutable: false }, + ]; + const armBody = (tag: number): IrStmt[] => { + const arm = arms[tag]!; + const narrowed: IrExpr = { + kind: "unionNarrow", + unionId: fnRet.unionId, + tag, + value: resultRef(), + type: arm, + loc, + }; + if (plan.arms[tag] === "empty") return []; + if (plan.arms[tag] === "value") { + return [ + arrayValueStore( + lowerer, + outRef(), + outLength(), + lowerer.coerceToExpected( + // A unit arm's value is the unit itself, which has no + // narrowed representation. + isUnitType(arm) + ? { + kind: "unitLit", + unit: arm.kind === "nullT" ? "null" : "undefined", + type: arm, + loc, + } + : narrowed, + plan.outElem, + ), + plan.outElem, + loc, + ), + ]; + } + const innerT = arm as IrType & { kind: "array" }; + const listId = `list.${tag}`; + const lengthId = `m.${tag}`; + const indexId = `j.${tag}`; + const itemId = `inner.${tag}`; + locals.push( + { id: listId, name: "list", type: innerT, mutable: false }, + { id: lengthId, name: "m", type: F64, mutable: false }, + { id: indexId, name: "j", type: F64, mutable: true }, + { id: itemId, name: "inner", type: arrayValueType(lowerer, innerT.elem), mutable: false }, + ); + const list = (): IrExpr => varRef(listId, innerT, loc); + const index = (): IrExpr => varRef(indexId, F64, loc); + return [ + { kind: "varDecl", localId: listId, init: narrowed, loc }, + { + kind: "varDecl", + localId: lengthId, + init: { + kind: "arrIntrinsic", + method: "length", + receiver: list(), + args: [], + type: F64, + loc, + }, + loc, + }, + { + kind: "for", + init: { kind: "varDecl", localId: indexId, init: numLit(0, loc), loc }, + cond: { + kind: "bin", + op: "<", + left: index(), + right: varRef(lengthId, F64, loc), + type: BOOL, + loc, + }, + update: { + kind: "assign", + localId: indexId, + value: { kind: "bin", op: "+", left: index(), right: numLit(1, loc), type: F64, loc }, + loc, + }, + body: [ + { + kind: "if", + cond: arrayIndexPresent(list(), index(), loc), + then: [ + { + kind: "varDecl", + localId: itemId, + init: arrayValueRead(lowerer, list(), index(), innerT.elem, loc), + loc, + }, + arrayValueStore( + lowerer, + outRef(), + outLength(), + varRef(itemId, arrayValueType(lowerer, innerT.elem), loc), + plan.outElem, + loc, + ), + ], + else_: null, + loc, + }, + ], + loc, + }, + ]; + }; + let dispatch: IrStmt[] = armBody(arms.length - 1); + for (let tag = arms.length - 2; tag >= 0; tag--) { + dispatch = [ + { + kind: "if", + cond: { + kind: "unionIsTag", + unionId: fnRet.unionId, + tag, + value: resultRef(), + negated: false, + type: BOOL, + loc, + }, + then: armBody(tag), + else_: dispatch, + loc, + }, + ]; + } + const body: IrStmt[] = [ + { + kind: "varDecl", + localId: "out.0", + init: { kind: "arrayLit", elems: [], type: outT, loc }, + loc, + }, + arrayLengthDeclaration(arrT, loc), + countedFor(loc, varRef("n.0", F64, loc), () => [ + { + kind: "if", + cond: currentArrayIndexPresent(arrT, loc), + then: [ + { + kind: "varDecl", + localId: "r.0", + init: callArrayCallback( + arrT, + fnT, + fnRet, + arity, + callbackValueRead( + lowerer, + varRef("a.0", arrT, loc), + varRef("i.0", F64, loc), + elem, + present, + loc, + ), + loc, + ), + loc, + }, + ...dispatch, + ], + else_: null, + loc, + }, + ]), + { kind: "return", value: outRef(), loc }, + ]; + lowerer.liftedFns.push({ + name, + params: arrayCallbackParams(arrT, fnT), + returnType: outT, + locals, + body, + loc, + }); + return name; +} + /** The flatMap loop (array-returning callback), from existing IR nodes: * * out = []; n = a.length; @@ -2650,6 +3006,7 @@ export function lowerTuplePredicateCall( // the value type an array find narrows from. let foundT: IrType = elem; let retag: string | null = null; + let narrowing: { helper: string; elem: IrType } | null = null; if (method === "filter") { // The element union is built in position order, so the checker's // result element can list the same arms in another order. @@ -2660,9 +3017,11 @@ export function lowerTuplePredicateCall( checkerResult?.kind === "array" && arms(checkerResult.elem).length === arms(elem).length && arms(checkerResult.elem).every((arm) => arms(elem).some((other) => typeEquals(arm, other))); - outElem = unchanged - ? elem - : filterResultElem(lowerer, call, argNode, elem, elem, booleanFilter); + if (!unchanged) { + const filtered = filterResultElem(lowerer, call, argNode, elem, elem, booleanFilter); + outElem = filtered.elem; + narrowing = filtered.narrowing; + } resultT = arrayOf(outElem); } else if (method === "find") { resultT = lowerer.irTypeOf(call); @@ -2795,5 +3154,19 @@ export function lowerTuplePredicateCall( loc, }); } - return { kind: "call", callee: helper, args: [receiver, fnArg], type: resultT, loc }; + const result: IrExpr = { + kind: "call", + callee: helper, + args: [receiver, fnArg], + type: resultT, + loc, + }; + if (narrowing === null) return result; + return { + kind: "call", + callee: narrowing.helper, + args: [result], + type: arrayOf(narrowing.elem), + loc, + }; } diff --git a/packages/compiler/src/frontend/lowering/containers/array-concat.ts b/packages/compiler/src/frontend/lowering/containers/array-concat.ts index b4e285ee9..a7ce0a106 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-concat.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-concat.ts @@ -97,11 +97,15 @@ export function arrayConcatHelper( return [ { kind: "varDecl", localId: "offset.0", init: length(out), loc }, { kind: "varDecl", localId: "count.0", init: length(source), loc }, + // Storage is reserved first and the length is set after the copy, so + // a source without holes is copied by appends and the result gets no + // state bytes. { kind: "arraySetLength", arr: out, length: { kind: "bin", op: "+", left: offset, right: count, type: F64, loc }, loc, + reserve: true, }, countedFor(loc, count, () => [ { @@ -134,6 +138,12 @@ export function arrayConcatHelper( loc, }, ]), + { + kind: "arraySetLength", + arr: out, + length: { kind: "bin", op: "+", left: offset, right: count, type: F64, loc }, + loc, + }, ]; }; const body: IrStmt[] = [ diff --git a/packages/compiler/src/frontend/lowering/containers/array-methods.ts b/packages/compiler/src/frontend/lowering/containers/array-methods.ts index 44182fbda..3ab2e84e2 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-methods.ts @@ -361,6 +361,21 @@ export function lowerArrayMethodCall( if (probe.type.kind === "array") { receiverIr = probe.type; probedUntyped = true; + } else if ( + probe.type.kind === "dyn" && + !isJsSourceFile(ts.getSourceFileOfNode(access)) && + lowerer.isStdlibMember(access) + ) { + // An `unknown[]` (or `any[]`) array stored checked-dynamic — a + // generic body instantiated at `unknown`, a spread of unknown + // elements: the method runs on the runtime array (dynInvoke, JS + // exact), and a typed result is checked like any member read. + const dispatched = lowerDynDispatchMethodCall(lowerer, call, access, probe, true); + if (!dispatched) return null; + const result = lowerer.mapTypeOf(lowerer.typeOf(call)); + return result && result.kind !== "void" && result.kind !== "dyn" + ? lowerer.coerceInto(call, dispatched, result) + : dispatched; } } if (receiverIr?.kind === "union") { diff --git a/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts b/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts index 180f40435..2f839a20b 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts @@ -1263,17 +1263,28 @@ class FusedLoop { loc, }); if (holes) { + // The result keeps the source's holes: storage for its length is + // reserved first and the length is set after the loop, so a result + // without holes is filled by appends and gets no state bytes. + const length = (): IrExpr => ({ + kind: "ternary", + cond: { kind: "bin", op: "<", left: lo, right: hi, type: BOOL, loc }, + then: { kind: "bin", op: "-", left: hi, right: lo, type: F64, loc }, + else_: numLit(0, loc), + type: F64, + loc, + }); stateDecls.push({ kind: "arraySetLength", arr: varRef(out, resultT, loc), - length: { - kind: "ternary", - cond: { kind: "bin", op: "<", left: lo, right: hi, type: BOOL, loc }, - then: { kind: "bin", op: "-", left: hi, right: lo, type: F64, loc }, - else_: numLit(0, loc), - type: F64, - loc, - }, + length: length(), + loc, + reserve: true, + }); + epilogue.push({ + kind: "arraySetLength", + arr: varRef(out, resultT, loc), + length: length(), loc, }); } diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts index fd4420b69..995cb177a 100644 --- a/packages/compiler/src/frontend/lowering/containers/bytes.ts +++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts @@ -1414,7 +1414,8 @@ const BUF_ENCODINGS: Record = { * does not know — the ladder callers (stream options) turn unknown * literals into Node's runtime ERR_UNKNOWN_ENCODING throw. */ export function knownBufEncoding(name: string): string | undefined { - return own(BUF_ENCODINGS, name); + // Buffer.isEncoding: any letter case + return own(BUF_ENCODINGS, name) ?? own(BUF_ENCODINGS, name.toLowerCase()); } /** The literal encoding argument of a Buffer surface, normalized — or a diff --git a/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts b/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts index 9db04e95b..d95640978 100644 --- a/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts +++ b/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts @@ -53,6 +53,8 @@ export function lowerArrayCallback( bindUntyped = false, expectedReturn?: IrType, ): { fnArg: IrExpr & { type: IrType & { kind: "func" } }; arity: number } { + // `xs.map(((x) => x))`: a parenthesized callback is the function itself. + while (ts.isParenthesizedExpression(argNode)) argNode = argNode.expression; // The ambient `Boolean` as the callback (`flags.every(Boolean)`, // `xs.some(Boolean)`, `xs.map(Boolean)`): Boolean(value) answers the // ToBoolean of its FIRST argument and ignores the rest, so the callback diff --git a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts index d3169ec5c..e043daf68 100644 --- a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts @@ -22,6 +22,7 @@ import { isPrimitiveCollectionKey, isUnitType, typeEquals, + UNDEFINED_T, } from "../../../ir/ir.js"; import { COLLECTION_ITERATOR_METHODS, @@ -44,7 +45,7 @@ import { strCharsCall } from "./array-construction.js"; import { arrayElementRead } from "./array-iteration.js"; import { isSearchParamsIterable } from "../builtins/url.js"; import { classGeneratorIterable } from "../class-iterables.js"; -import { generatorDrain } from "../iterator-adapters.js"; +import { generatorDrain, lowerCollectionIteratorValue } from "../iterator-adapters.js"; /** Ambient Map method calls. `get`/`set`/`has`/`delete`/`clear` lower to * mapIntrinsic; `forEach` desugars to a direct call of a synthetic loop @@ -1345,6 +1346,17 @@ export function mapFromSeedValue( const drained = generatorDrain(lowerer, generator, entry, loc); return drained ? mapFromSeedValue(lowerer, drained, mapT) : null; } + // A Set of [key, value] pairs walks its values like a generator of them. + if (seed.type.kind === "set" && seed.type.elem.kind === "record") { + const walk = lowerCollectionIteratorValue( + lowerer, + seed, + "values", + { kind: "generator", yieldT: seed.type.elem, retT: VOID, nextT: UNDEFINED_T }, + loc, + ); + return walk ? mapFromSeedValue(lowerer, walk, mapT) : null; + } if (seed.type.kind === "map") { if (!typeEquals(seed.type, mapT)) return null; return { kind: "mapIntrinsic", method: "clone", receiver: seed, args: [], type: mapT, loc }; diff --git a/packages/compiler/src/frontend/lowering/containers/filter-predicates.ts b/packages/compiler/src/frontend/lowering/containers/filter-predicates.ts new file mode 100644 index 000000000..3934edbf0 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/containers/filter-predicates.ts @@ -0,0 +1,261 @@ +/* Checked narrowing for `.filter` with a written type predicate. + * + * A written predicate (`(p): p is Cat => ...`, or a guard function passed + * as a value) is an assertion TypeScript does not check, so the filter loop + * keeps the receiver's element type and the result converts to the + * predicate's type afterwards, element by element: + * + * - a union element to one of its arms, or to a union of some of its + * arms, by its tag; + * - a checked-dynamic element by dynCheck; + * - a class instance to a subclass by its class; + * - a tuple or record to one with the same fields, field by field. + * + * Each conversion throws the catchable TypeError of a checked extraction + * when a kept element is not of the predicate's type, which is when the + * predicate returned true for a value it does not describe. */ +import { varRef } from "../../../ir/build.js"; +import { + BOOL, + F64, + STRING, + isUnitType, + typeEquals, + type IrExpr, + type IrStmt, + type IrType, + type SrcLoc, +} from "../../../ir/ir.js"; +import { checkedClassAssertion } from "../class-assertions.js"; +import { buildArrayConversion } from "../coercions/builders.js"; +import type { Lowerer } from "../lowerer.js"; +import { keyedHelperName } from "../stable-names.js"; +import { typeKey } from "../../type-mapper.js"; + +/** The interned helper converting a `from[]` filter result to `to[]`, or + * null when an element of `from` has no checked conversion to `to`. + * `typeText` names the predicate's type in the TypeError. */ +export function checkedFilterNarrowing( + lowerer: Lowerer, + from: IrType, + to: IrType, + typeText: string, + loc: SrcLoc, +): string | null { + const element = elementHelper(lowerer, from, to, loc, typeText); + if (element === null) return null; + const key = `filter.narrow:${typeKey(from)}:${typeKey(to)}:${typeText}`; + const existing = lowerer.valueHelpers.get(key); + if (existing) return existing; + const name = keyedHelperName(lowerer, "%arr.narrow", key); + lowerer.valueHelpers.set(key, name); + const outT: IrType = { kind: "array", elem: to }; + lowerer.liftedFns.push( + buildArrayConversion( + name, + { kind: "array", elem: from }, + { kind: "arrayLit", elems: [], type: outT, loc }, + (value, _index, result) => ({ + kind: "arrIntrinsic", + method: "push", + receiver: result, + args: [{ kind: "call", callee: element, args: [value], type: to, loc }], + type: F64, + loc, + }), + loc, + ), + ); + return name; +} + +/** The interned helper converting one `from` value to `to`, or null. */ +function elementHelper( + lowerer: Lowerer, + from: IrType, + to: IrType, + loc: SrcLoc, + typeText = lowerer.fmt(to), +): string | null { + const key = `filter.narrow.value:${typeKey(from)}:${typeKey(to)}:${typeText}`; + const existing = lowerer.valueHelpers.get(key); + if (existing) return existing; + // A recursive type pair converts through the helper being built. + if (lowerer.coercions.planning.has(key)) return null; + lowerer.coercions.planning.add(key); + const input = varRef("v.0", from, loc); + let body: IrStmt[] | null; + try { + body = conversionBody(lowerer, input, from, to, typeText, loc); + } finally { + lowerer.coercions.planning.delete(key); + } + if (body === null) return null; + const name = keyedHelperName(lowerer, "%narrow.checked", key); + lowerer.valueHelpers.set(key, name); + lowerer.liftedFns.push({ + name, + params: [{ localId: "v.0", name: "v", type: from }], + returnType: to, + locals: [{ id: "v.0", name: "v", type: from, mutable: false }], + body, + loc, + }); + return name; +} + +/** A converted value: `value` itself when the types are equal, or a call of + * the conversion helper. */ +function converted( + lowerer: Lowerer, + value: IrExpr, + from: IrType, + to: IrType, + loc: SrcLoc, +): IrExpr | null { + if (typeEquals(from, to)) return value; + const helper = elementHelper(lowerer, from, to, loc); + return helper === null ? null : { kind: "call", callee: helper, args: [value], type: to, loc }; +} + +/** The TypeError a kept element of another type throws. */ +function typeError(message: string, loc: SrcLoc): IrStmt { + return { + kind: "throw", + value: { + kind: "libCall", + fn: "error.new", + args: [{ kind: "strLit", value: message, type: STRING, loc }], + type: { kind: "object", className: "%TypeError" }, + loc, + }, + loc, + }; +} + +function conversionBody( + lowerer: Lowerer, + input: IrExpr, + from: IrType, + to: IrType, + typeText: string, + loc: SrcLoc, +): IrStmt[] | null { + if (typeEquals(from, to)) return [{ kind: "return", value: input, loc }]; + if (from.kind === "dyn") + return [{ kind: "return", value: { kind: "dynCheck", value: input, type: to, loc }, loc }]; + if (from.kind === "object" && to.kind === "object") { + const asserted = checkedClassAssertion(lowerer, input, to, loc); + return asserted === null ? null : [{ kind: "return", value: asserted, loc }]; + } + if (from.kind === "union") + return unionConversion(lowerer, input, from.unionId, to, typeText, loc); + if (from.kind === "record" && to.kind === "record") { + const source = lowerer.shapes.get(from.shapeId); + const target = lowerer.shapes.get(to.shapeId); + if ( + !source || + !target || + source.tuple !== target.tuple || + source.indexValue || + target.indexValue + ) + return null; + if (source.fields.length !== target.fields.length) return null; + const fields: { name: string; value: IrExpr }[] = []; + for (const field of target.fields) { + const own = source.fields.find((f) => f.name === field.name); + if (!own) return null; + const read: IrExpr = { + kind: "recordGet", + obj: input, + shapeId: from.shapeId, + field: field.name, + type: own.type, + loc, + }; + const value = converted(lowerer, read, own.type, field.type, loc); + if (value === null) return null; + fields.push({ + name: field.name, + value: keepAbsence(lowerer, input, from.shapeId, field, value, loc), + }); + } + // The copy keeps the source's own keys in the source's order, as a width + // copy does (key-order-witness.ts). + const copy: IrExpr = { kind: "recordLit", fields, type: to, loc }; + return [{ kind: "return", value: lowerer.keyOrder.copy(copy, from.shapeId), loc }]; + } + return null; +} + +/** A converted field value that keeps the source field's absence: an + * optional field the source object lacks stays absent in the copy rather + * than becoming a property that holds undefined. */ +function keepAbsence( + lowerer: Lowerer, + input: IrExpr, + shapeId: string, + field: { name: string; type: IrType }, + value: IrExpr, + loc: SrcLoc, +): IrExpr { + if (field.type.kind !== "union") return value; + if (!lowerer.unions.get(field.type.unionId)?.arms.some((arm) => arm.kind === "undefinedT")) + return value; + return { + kind: "ternary", + cond: { kind: "recordHas", obj: input, shapeId, field: field.name, type: BOOL, loc }, + then: value, + else_: { kind: "fieldAbsent", unionId: field.type.unionId, type: field.type, loc }, + type: field.type, + loc, + }; +} + +/** A union value to one of its arms, or to a union of some of its arms: + * each arm the target holds returns, any other arm throws. */ +function unionConversion( + lowerer: Lowerer, + input: IrExpr, + unionId: string, + to: IrType, + typeText: string, + loc: SrcLoc, +): IrStmt[] | null { + const source = lowerer.unions.get(unionId); + if (!source) return null; + const targetArms = to.kind === "union" ? (lowerer.unions.get(to.unionId)?.arms ?? null) : [to]; + if (targetArms === null) return null; + const body: IrStmt[] = []; + let kept = 0; + for (let tag = 0; tag < source.arms.length; tag++) { + const arm = source.arms[tag]!; + const targetTag = targetArms.findIndex((t) => typeEquals(t, arm)); + if (targetTag < 0) continue; + kept++; + const payload: IrExpr = isUnitType(arm) + ? { kind: "unitLit", unit: arm.kind === "undefinedT" ? "undefined" : "null", type: arm, loc } + : { kind: "unionNarrow", unionId, tag, value: input, type: arm, loc }; + const value: IrExpr = + to.kind === "union" + ? { kind: "unionWrap", unionId: to.unionId, tag: targetTag, value: payload, type: to, loc } + : payload; + body.push({ + kind: "if", + cond: { kind: "unionIsTag", unionId, tag, negated: false, value: input, type: BOOL, loc }, + then: [{ kind: "return", value, loc }], + else_: null, + loc, + }); + } + // Every arm the target names must be one of the union's own arms. + if (kept === 0 || kept !== targetArms.length) return null; + body.push( + typeError( + `a kept element is not a '${typeText}' (a type predicate returned true for a value of another type)`, + loc, + ), + ); + return body; +} diff --git a/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts b/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts index b3faf649e..668603d47 100644 --- a/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts +++ b/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts @@ -1388,6 +1388,15 @@ export type IndexMergeContributor = * JS own-key order. Explicit values arrive pre-coerced to the target's * value slot. Null when a spread source is outside that matrix (the * caller keeps its fence). */ +/** A value of an index slot's type that holds a key's place until a later + * write replaces it, or null for slot types without a literal. */ +function indexSlotPlaceholder(slot: IrType, loc: SrcLoc): IrExpr | null { + if (slot.kind === "string") return { kind: "strLit", value: "", type: STRING, loc }; + if (slot.kind === "f64") return { kind: "numLit", value: 0, type: slot, loc }; + if (slot.kind === "bool") return { kind: "boolLit", value: false, type: slot, loc }; + return null; +} + export function lowerIndexMergeHelper( lowerer: Lowerer, toId: string, @@ -1410,9 +1419,9 @@ export function lowerIndexMergeHelper( shapeId: string; wrapTag: number; retag: string | null; - fields: Map | null; + fields: Map | null; }[] = []; - for (const c of contributors) { + for (const [ci, c] of contributors.entries()) { // A conditional spread needs the undefined arm as its "absent" // value — a target slot without one can't carry the empty arm. if ( @@ -1426,15 +1435,26 @@ export function lowerIndexMergeHelper( if (!from || from.tuple) return null; if (!from.indexValue) { if (from.fields.some((f) => f.name.startsWith("%"))) return null; - const fields = new Map(); + const fields = new Map(); for (const ff of from.fields) { const lift = typeEquals(ff.type, tIv) ? ({ how: "copy" } as WidthLift) : tIv.kind === "dyn" && (ff.type.kind === "dyn" || lowerer.dynConvertible(ff.type)) ? "dyn" : lowerer.widthLiftPlan(ff.type, tIv); - if (lift === null) return null; - fields.set(ff.name, lift); + if (lift !== null) { + fields.set(ff.name, lift); + continue; + } + // A field a later plain property overrides (`{ ...tags, status: + // String(tags.status) }`): its value never survives, but its key + // keeps the spread's position, so a placeholder of the slot's + // type holds the place until the override writes. + const overridden = contributors + .slice(ci + 1) + .some((later) => later.kind === "field" && later.name === ff.name); + if (!overridden || indexSlotPlaceholder(tIv, loc) === null) return null; + fields.set(ff.name, "placeholder"); } srcPlans.push({ shapeId: c.shapeId, wrapTag: -1, retag: null, fields }); continue; @@ -1569,6 +1589,11 @@ export function lowerIndexMergeHelper( // own planned lift (absent optionals skip — the unset convention); // no overflow exists to walk. if (plan.fields !== null) { + // Keys enter in the source's enumeration order, as its own keys do. + // That order settles only after lowering (key-order witnesses), so + // the writes go into one block that a finalizer orders. + lowerer.keyOrder.observed.add(from.id); + const fieldStmts = new Map(); for (const ff of from.fields) { const lift = plan.fields.get(ff.name)!; const raw: IrExpr = { @@ -1586,13 +1611,16 @@ export function lowerIndexMergeHelper( shapeId: toId, key: { kind: "strLit", value: ff.name, type: STRING, loc }, value: - lift === "dyn" - ? { kind: "dynFrom", value: raw, type: DYN, loc } - : lowerer.applyWidthLift(lift, raw, tIv, loc), + lift === "placeholder" + ? indexSlotPlaceholder(tIv, loc)! + : lift === "dyn" + ? { kind: "dynFrom", value: raw, type: DYN, loc } + : lowerer.applyWidthLift(lift, raw, tIv, loc), overflowOnly: true, loc, }; - body.push( + fieldStmts.set( + ff.name, utag >= 0 && ff.type.kind === "union" ? { kind: "if", @@ -1604,6 +1632,17 @@ export function lowerIndexMergeHelper( : write, ); } + // declaredOrder omits internal '%'-fields, which never copy. + const ordered = (shape: IrRecordShape): IrStmt[] => + (shape.declaredOrder ?? shape.fields.map((f) => f.name)).flatMap((field) => { + const stmt = fieldStmts.get(field); + return stmt ? [stmt] : []; + }); + const group: IrStmt & { kind: "block" } = { kind: "block", body: ordered(from), loc }; + lowerer.shapeOrderHelperFinalizers.push(() => { + group.body = ordered(lowerer.shapes.get(plan.shapeId) ?? from); + }); + body.push(group); return; } const fIv = from.indexValue!; 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 1947f0d75..d55f71ff0 100644 --- a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts +++ b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts @@ -16,6 +16,7 @@ import { type IrExpr, type IrStmt, type IrType, + REGEX, STRING, type SrcLoc, UNDEFINED_T, @@ -920,12 +921,15 @@ export function lowerRegexMethodCall( })(); if ((receiverKind === "string" || nullableStringRecv) && name === "match") { const arg0 = call.arguments.nodes[0]; - if (!arg0 || !isRegexPatternArgument(lowerer, arg0)) return null; // string-pattern match: the SC2020 fence - if (call.arguments.nodes.length !== 1) return null; + if (!arg0 || call.arguments.nodes.length !== 1) return null; + const checkedPattern = isCheckedPatternArgument(lowerer, arg0); + if (!checkedPattern && !isRegexPatternArgument(lowerer, arg0)) return null; // string-pattern match: the SC2020 fence const receiver = nullableStringRecv ? lowerMethodReceiver(lowerer, access.expression, STRING, access.name.text) : lowerReceiver(); - const re = lowerRegexPatternArgument(lowerer, arg0); + const re = checkedPattern + ? lowerCheckedPatternArgument(lowerer, arg0) + : lowerRegexPatternArgument(lowerer, arg0); // RegExpMatchArray | null maps to the string[] | null union by // itself; intern it directly when the checker's spelling doesn't — // or when it maps to something WIDER (an optional-chain call node @@ -983,10 +987,13 @@ export function lowerRegexMethodCall( // 0, exactly Node. Never throws. if (receiverKind === "string" && name === "search") { const arg0 = call.arguments.nodes[0]; - if (!arg0 || !isRegexPatternArgument(lowerer, arg0)) return null; // string-pattern form: the SC2020 fence - if (call.arguments.nodes.length !== 1) return null; + if (!arg0 || call.arguments.nodes.length !== 1) return null; + const checkedPattern = isCheckedPatternArgument(lowerer, arg0); + if (!checkedPattern && !isRegexPatternArgument(lowerer, arg0)) return null; // string-pattern form: the SC2020 fence const receiver = lowerReceiver(); - const re = lowerRegexPatternArgument(lowerer, arg0); + const re = checkedPattern + ? lowerCheckedPatternArgument(lowerer, arg0) + : lowerRegexPatternArgument(lowerer, arg0); return { kind: "regexIntrinsic", method: "search", receiver, args: [re], type: F64, loc }; } if (receiverKind === "string" && (name === "replace" || name === "replaceAll")) { @@ -1936,6 +1943,46 @@ function isRegexPatternArgument(lowerer: Lowerer, node: ts.Expression): boolean ); } +/** True when a `match` or `search` pattern is checker-untyped (`any` or + * `unknown`), so its runtime value decides the protocol. */ +function isCheckedPatternArgument(lowerer: Lowerer, node: ts.Expression): boolean { + if (ts.isSpreadElement(node)) return false; + return (lowerer.typeOf(node).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0; +} + +/** An untyped `match` or `search` pattern as a RegExp, chosen at run time as + * Node does: a RegExp is used as is (its lastIndex state included), and any + * other value builds `new RegExp(value)` (undefined is the empty pattern, + * other values convert with ToString). */ +function lowerCheckedPatternArgument(lowerer: Lowerer, node: ts.Expression): IrExpr { + const loc = locOf(node); + const value = lowerer.lowerExpr(node); + if (value.type.kind === "regex") return value; + const checked = lowerer.coerceInto(node, value, DYN); + const local = lowerer.declareHiddenLocal("%matchPattern", DYN); + const ref = varRef(local.id, DYN, loc); + return { + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: local.id, init: checked, loc }], + result: { + kind: "ternary", + cond: { kind: "libCall", fn: "dyn.nativeRegexIs", args: [ref], type: BOOL, loc }, + then: lowerer.coerceInto(node, ref, REGEX), + else_: { + kind: "libCall", + fn: "regex.newChecked", + args: [ref, dynUndefinedExpr(loc)], + type: REGEX, + loc, + }, + type: REGEX, + loc, + }, + type: REGEX, + loc, + }; +} + /** The pattern of `match` or `search` as a RegExp: an undefined pattern is * `new RegExp(undefined)`, the empty pattern, as in Node. */ function lowerRegexPatternArgument(lowerer: Lowerer, node: ts.Expression): IrExpr { diff --git a/packages/compiler/src/frontend/lowering/dispatch-sites.ts b/packages/compiler/src/frontend/lowering/dispatch-sites.ts index 2618dacf9..b9ab8f069 100644 --- a/packages/compiler/src/frontend/lowering/dispatch-sites.ts +++ b/packages/compiler/src/frontend/lowering/dispatch-sites.ts @@ -11,6 +11,19 @@ import { everyStmtList } from "../../ir/traverse.js"; import { toPrimitiveHint } from "./class-dynamic-dispatch.js"; import { GENERATOR_STEP_METHODS, HELPER_METHODS } from "./generator-dynamic-dispatch.js"; +/** Runtime entry points that step a checked-dynamic iterable's iterator: + * destructuring rest, the collection constructors' and Object.fromEntries' + * seeds, and Math.min/Math.max over a spread. */ +const RUNTIME_STEP_LIB_FNS: ReadonlySet = new Set([ + "dyn.destructureRest", + "dyn.mathFold", + "dyn.nativeSetNew", + "dyn.mapSeedEntries", + "dyn.fromEntries", + "weakMap.new", + "weakSet.new", +]); + /** What one function's body holds for the class dispatch pass and its * caught-value, generator and class construction sub-passes, collected in * one walk. Lists keep the order the walk meets their entries. */ @@ -68,6 +81,8 @@ const DISPATCH_LIB_CALLS = new Set([ "dyn.propertyIsEnumerableComputed", "dyn.defineProperty", "dyn.getPrototype", + "dyn.getPrototypeForIdentity", + "dyn.inheritsErrorPrototype", "dyn.reflectGet", "dyn.reflectSet", ]); @@ -186,7 +201,7 @@ function scanSites(statements: IrStmt[]): FunctionSites { expr.key.kind === "strLit" && HELPER_METHODS.has(expr.key.value)) || expr.kind === "dynIterN" || - (expr.kind === "libCall" && expr.fn === "dyn.destructureRest") + (expr.kind === "libCall" && RUNTIME_STEP_LIB_FNS.has(expr.fn)) ) sites.runtimeSteps = true; switch (expr.kind) { diff --git a/packages/compiler/src/frontend/lowering/divergences.ts b/packages/compiler/src/frontend/lowering/divergences.ts index 1c362cb09..87169539f 100644 --- a/packages/compiler/src/frontend/lowering/divergences.ts +++ b/packages/compiler/src/frontend/lowering/divergences.ts @@ -464,8 +464,11 @@ function identityUse(root: ts.Node, name: string, from = -1): IdentityUse | null cur = cur.expression; return cur; }; + // The text appears only in warnings. A build that reported warnings is + // not reused after a source edit, so the read is not recorded as a source + // text dependency (ts.getTextOfNode would record it). const textOf = (node: ts.Node): string => { - const text = ts.getTextOfNode(node); + const text = ts.getSourceFileOfNode(node).text.slice(ts.tokenStartOfNode(node), node.end); return text.length > 60 ? `${text.slice(0, 57)}...` : text; }; const visit = (node: ts.Node): "skip" | "stop" | undefined => { @@ -724,6 +727,11 @@ function keyOrderFinding( written: readonly string[], assignedNames: ReadonlySet, ): { native: string; node: string; detail: "reordered" | "added-later" } | null { + // Values of a shape with orderVariants enumerate in the order their + // literal wrote (key-order-witness.ts). + const mapped = safely(() => lowerer.mapTypeOf(contextual)); + if (mapped?.kind === "record" && lowerer.shapes.get(mapped.shapeId)?.orderVariants !== undefined) + return null; // 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); diff --git a/packages/compiler/src/frontend/lowering/error-methods.ts b/packages/compiler/src/frontend/lowering/error-methods.ts index b545f4147..067f9c286 100644 --- a/packages/compiler/src/frontend/lowering/error-methods.ts +++ b/packages/compiler/src/frontend/lowering/error-methods.ts @@ -94,12 +94,15 @@ export function refreshErrorPropertyDispatch( lowerer.isSubclassOf(info.def.name, "%Error"), ); const properties: ("name" | "message")[] = ["name", "message"]; + // Test lowerers built from partial objects leave the location undefined. + const accessors = Boolean(lowerer.errorAccessorReflection); for (const property of properties) { if ( - overrides.length && + (overrides.length || accessors) && !lowerer.liftedFns.some((fn) => fn.name === `%error.${property}.read`) ) { - errorPropertyRead(lowerer, varRef("error.0", errorType, overrides[0]!.def.loc), property); + const loc = overrides[0]?.def.loc ?? lowerer.errorAccessorReflection!; + errorPropertyRead(lowerer, varRef("error.0", errorType, loc), property); return true; } } @@ -115,6 +118,7 @@ export function refreshErrorPropertyDispatch( const revision = JSON.stringify([ readers.map((fn) => fn.name), overrides.map((info) => [info.def.name, hasClassPrototypeData(info)]), + accessors, ]); if (lowerer.errorPropertyDispatchRevision === revision) return false; lowerer.errorPropertyDispatchRevision = revision; @@ -183,6 +187,50 @@ export function refreshErrorPropertyDispatch( }, ]; const body: IrStmt[] = [{ kind: "if", cond: own, then: fallback, else_: null, loc }]; + // An own accessor a reflective definition installed (the slot reads as + // absent then): the read calls its getter, the write its setter. + if (accessors) + body.push({ + kind: "if", + cond: { kind: "libCall", fn: "error.ownAccessor", args: [receiver, key], type: BOOL, loc }, + then: write + ? [ + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "error.accessorSet", + args: [ + receiver, + key, + lowerer.coerceToExpected(varRef("value", STRING, loc), DYN), + ], + type: VOID, + loc, + }, + loc, + }, + { kind: "return", value: null, loc }, + ] + : [ + { + kind: "return", + value: lowerer.coerceToExpected( + { + kind: "libCall", + fn: "error.accessorGet", + args: [receiver, key], + type: DYN, + loc, + }, + STRING, + ), + loc, + }, + ], + else_: null, + loc, + }); helper.locals = helper.locals.filter((local) => !local.id.startsWith("descriptor.")); for (const info of overrides) { const member = `${write ? "set" : "get"}:${property}`; diff --git a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts index 8f119504c..6c7ce814e 100644 --- a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts @@ -58,6 +58,41 @@ export function jsHoldsDynObject(lowerer: Lowerer, expr: ts.Expression): boolean return false; } +/** The literal is the source of an array destructuring: an assignment's + * right side or a declaration's initializer under an array pattern. */ +function isPatternSource(expr: ts.ArrayLiteralExpression): boolean { + let node: ts.Node = expr; + while (ts.isParenthesizedExpression(node.parent)) node = node.parent; + const parent = node.parent; + if (ts.isBinaryExpression(parent)) + return ( + parent.right === node && + parent.operatorToken.kind === ts.SyntaxKind.EqualsToken && + ts.isArrayLiteralExpression(parent.left) + ); + return ( + ts.isVariableDeclaration(parent) && + parent.initializer === node && + ts.isArrayBindingPattern(parent.name) + ); +} + +/** An empty TypeScript literal whose own type is `never[]`: tsc's type for + * `[]` with no contextual array type (`return []` in an async function, + * whose contextual type is `string[] | PromiseLike`; `{ files: [] }`). + * It holds no element, as the empty tuple does, so it builds as the + * unit-only element array, and an array slot receives it through the + * checked empty-array lift. never's f64 element mapping would make it a + * `number[]`, which no `string[]` slot accepts. */ +function emptyNeverLiteral(lowerer: Lowerer, expr: ts.ArrayLiteralExpression): boolean { + if (expr.elements.nodes.length > 0 || isJsSourceFile(ts.getSourceFileOfNode(expr))) return false; + const own = lowerer.typeOf(expr); + return ( + lowerer.checker.isArrayType(own) && + (lowerer.checker.getTypeArguments(own as ts.TypeReference)[0]!.flags & ts.TypeFlags.Never) !== 0 + ); +} + /** `[a, b, c]`. The element type comes from the contextual type when tsc * has one (`const a: number[] = []`, arguments, nested literals) and from * the literal's own inferred type otherwise. A bare `[]` with no context @@ -80,6 +115,8 @@ export function lowerArrayLiteral( ? lowerer.typeOf(expr) : (ctxType ?? lowerer.typeOf(expr)); let mapped = expected ?? lowerer.mapTypeOf(tsType); + if (expected === undefined && ctxType === undefined && emptyNeverLiteral(lowerer, expr)) + mapped = arrayOf(unitOnlyUnion(lowerer.unions)); // Inferred JavaScript arrays can contain unrelated constructors with // structurally equal checker types. Their native class ABIs still differ. if ( @@ -107,6 +144,22 @@ export function lowerArrayLiteral( ) mapped = inferred; } + // A destructuring pattern's implied type ends in a rest element + // (`[h, ...t] = [1, 2, 3]` supplies `[number, ...number[]]`), which maps + // to an array. The literal keeps its own fixed tuple layout, read by + // position like any tuple source. + if ( + expected === undefined && + ctxType && + isPatternSource(expr) && + lowerer.checker.isTupleType(ctxType) && + ( + (ctxType as ts.TupleTypeReference).getTarget() as ts.TupleType | undefined + )?.elementFlags?.some((flag) => (flag & ts.ElementFlags.Rest) !== 0) + ) { + const own = lowerer.mapTypeOf(lowerer.typeOf(expr)); + if (own?.kind === "record" && lowerer.shapes.get(own.shapeId)?.tuple) mapped = own; + } // A JS literal whose OWN inferred type is never-tainted // (neverTaintedJsType — the evolving `const gb = []`, the mixed // command tuple `['pwd', []]`) carries no element information: route @@ -131,11 +184,14 @@ export function lowerArrayLiteral( // the literal's own inferred type is the real tuple. if (!mapped || mapped.kind === "union" || mapped.kind === "dyn") { const ctxUnion = mapped?.kind === "union" ? mapped : null; + const ctxUnmapped = mapped === null; mapped = lowerer.mapTypeOf(lowerer.typeOf(expr)); // The never-tainted own type carries no element information here // either (the JS story above) — only a contextual union's single // array arm below may still supply a static home. if (neverTaintedOwnJs) mapped = null; + if (ctxUnmapped && emptyNeverLiteral(lowerer, expr)) + mapped = arrayOf(unitOnlyUnion(lowerer.unions)); // An EMPTY literal in a union slot (`const t: string[] | undefined = // []`, the `[]` default argument against `string[] | undefined`) has // no useful own type — tsc infers `never[]`, whose f64-element diff --git a/packages/compiler/src/frontend/lowering/expressions/binary.ts b/packages/compiler/src/frontend/lowering/expressions/binary.ts index 9702a6dd9..58f6e16b5 100644 --- a/packages/compiler/src/frontend/lowering/expressions/binary.ts +++ b/packages/compiler/src/frontend/lowering/expressions/binary.ts @@ -1,6 +1,11 @@ /* Binary operators: arithmetic, comparisons and equality, logical and nullish pairs, and string conversion for concatenation. */ import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; +import { + lowerOptionalIndex, + splitUndefinedIndex, + undefinedTypedArrayKeyFence, +} from "../undefined-index.js"; import { cleanFuncAdaptable } from "../coercions/function-adapters.js"; import { type Lowerer, jsFuncNameOf } from "../lowerer.js"; import { @@ -1160,12 +1165,26 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr } if (recv?.type.kind === "array" || recv?.type.kind === "bytes") { const receiver = lowerer.declareHiddenLocal("%setArray", recv.type); - const index = lowerOptionalNumber( - lowerer, - lowerer.lowerExpr(expr.left.argumentExpression), - locOf(expr.left.argumentExpression), - expr.left.argumentExpression, - ); + // An undefined index names the "undefined" property + // (undefined-index.ts), which a typed array cannot hold. + const rawIndex = lowerer.lowerExpr(expr.left.argumentExpression); + const split = + recv.type.kind === "bytes" + ? splitUndefinedIndex( + lowerer, + rawIndex, + expr.left.argumentExpression, + locOf(expr.left.argumentExpression), + ) + : null; + const index = + split?.index ?? + lowerOptionalIndex( + lowerer, + rawIndex, + locOf(expr.left.argumentExpression), + expr.left.argumentExpression, + ); if (index.type.kind !== "f64") lowerer.unsupported( "SC1090", @@ -1178,7 +1197,7 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr const result = varRef(value.id, raw.type, loc); const arr = varRef(receiver.id, recv.type, loc); const idx = varRef(key.id, F64, loc); - const write: IrStmt = + const store: IrStmt = recv.type.kind === "array" ? arrayValueStore(lowerer, arr, idx, result, recv.type.elem, loc) : { @@ -1188,10 +1207,20 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr value: lowerer.coerceInto(expr.right, result, F64), loc, }; + const write: IrStmt = split + ? { + kind: "if", + cond: split.isUndefined, + then: [undefinedTypedArrayKeyFence(lowerer, expr.left, loc)], + else_: [store], + loc, + } + : store; return { kind: "seqExpr", stmts: [ { kind: "varDecl", localId: receiver.id, init: recv, loc }, + ...(split?.stmts ?? []), { kind: "varDecl", localId: key.id, init: index, loc }, { kind: "varDecl", localId: value.id, init: raw, loc }, write, diff --git a/packages/compiler/src/frontend/lowering/expressions/element-access.ts b/packages/compiler/src/frontend/lowering/expressions/element-access.ts index 3ca1471ae..0ea94ec7a 100644 --- a/packages/compiler/src/frontend/lowering/expressions/element-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/element-access.ts @@ -1,5 +1,10 @@ /* Element reads and writes (`a[i]`, `a[i] = v`), compound element updates, and keyed record reads. */ import * as ts from "../../ts7/adapter.js"; +import { + lowerOptionalIndex, + splitUndefinedIndex, + undefinedTypedArrayKeyFence, +} from "../undefined-index.js"; import { type Lowerer, jsFuncNameOf, neverTaintedJsType } from "../lowerer.js"; import { BOOL, @@ -65,6 +70,7 @@ import { lowerClassPrototypeComputedAssignment } from "../class-prototypes.js"; import { isClassCallback } from "../class-callbacks.js"; import { isKeyedFieldAccess } from "./keyed-field-targets.js"; import { lowerFieldCompoundValue } from "./updates.js"; +import { readsHeldEvolvingArray } from "../lower-stmts.js"; /** `a[i]` reads. Only f64 indices into array receivers are modeled; JS * string-key element access (`a["length"]`) and string indexing (`s[0]` @@ -560,8 +566,8 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre } } if (receiverIr?.kind !== "array") { + const receiver = tryLowerExpression(lowerer, expr.expression); { - const receiver = tryLowerExpression(lowerer, expr.expression); if ( receiver?.type.kind === "dyn" || (receiver && @@ -612,7 +618,13 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre } } } - if (receiverIr?.kind === "string") { + // A checker-`any` receiver that lowers to a string (an implicit-any + // package parameter specialized for a string argument) indexes the + // string the same way. + if ( + receiverIr?.kind === "string" || + (receiverIr === null && receiver?.type.kind === "string") + ) { // An indexed string property is absent for non-integer and missing // indexes. charAt has different coercion and out-of-range semantics. const recv = lowerer.lowerExpr(expr.expression); @@ -751,12 +763,16 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre : lowerer.maybeNarrow(arr, expr.expression); } + // An alias or container of an evolving array whose reads disagree + // (`rows[0]` after `const rows = [xs]`) holds elements the checker's type + // does not list, so its element read stays checked-dynamic. + const heldEvolving = arr.type.kind === "dyn" && readsHeldEvolvingArray(lowerer, expr.expression); // The checker sees an array but the VALUE lowered checked-dynamic (a // jsdoc-typed default export from a .js module — signature 14): the // read rides the dynCheck boundary when the array shape is one the checked-dynamic tree // can validate, and fences by name when it is not — an arrayGet over a // dyn receiver is never emitted (the validator ICE). - if (arr.type.kind === "dyn") { + if (arr.type.kind === "dyn" && !heldEvolving) { if ( canDynCheckTo( receiverIr, @@ -774,14 +790,22 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre } } // Array reads can themselves supply an index. A missing numeric value - // is undefined, whose property lookup misses just like NaN; keep that - // value instead of rejecting a checker-number index with optional IR. - const index = lowerOptionalNumber( + // is undefined, which names the array's "undefined" property + // (undefined-index.ts). + const index = lowerOptionalIndex( lowerer, lowerer.lowerExpr(expr.argumentExpression), locOf(expr.argumentExpression), expr.argumentExpression, ); + if (heldEvolving) + return { + kind: "dynKeyGet", + value: arr, + key: lowerer.coerceToExpected(index, DYN), + type: DYN, + loc: locOf(expr), + }; if ( arr.type.kind === "array" && (index.type.kind === "dyn" || @@ -1321,6 +1345,15 @@ export function lowerRecordKeyRead( shape.fields.every((f) => typeEquals(f.type, shape.fields[0]!.type)) ) { declared = shape.fields[0]!.type; + } else if (shape.fields.length > 0) { + // Fields of different types (`row[key]` with `key: keyof Row`): the + // checker types the read as the union of the field types the key can + // name. When every declared field surfaces in that union (an arm, or a + // union that widens into it), the per-field dispatch wraps each hit + // into its arm. + const read = lowerer.mapTypeOf(lowerer.typeOf(expr)); + if (read !== null && read.kind === "union" && recordKeyResultOk(lowerer, shape, read)) + declared = read; } // Fields of different types answer a checked-dynamic value where nothing // typed the read: in JavaScript, and where the checker types it 'any' @@ -1509,12 +1542,26 @@ export function lowerElementCompoundParts( "compound assignment through a non-native array or byte view", ); } - const index = lowerOptionalNumber( - lowerer, - lowerer.lowerExpr(target.argumentExpression), - locOf(target.argumentExpression), - target.argumentExpression, - ); + // An undefined index names the "undefined" property (undefined-index.ts), + // which a typed array cannot hold. + const rawIndex = lowerer.lowerExpr(target.argumentExpression); + const split = + receiver.type.kind === "bytes" + ? splitUndefinedIndex( + lowerer, + rawIndex, + target.argumentExpression, + locOf(target.argumentExpression), + ) + : null; + const index = + split?.index ?? + lowerOptionalIndex( + lowerer, + rawIndex, + locOf(target.argumentExpression), + target.argumentExpression, + ); if (index.type.kind !== "f64") lowerer.unsupported("SC1090", target.argumentExpression, "indexing with non-number keys"); const receiverLocal = lowerer.declareHiddenLocal("%compoundArray", receiver.type); @@ -1623,15 +1670,25 @@ export function lowerElementCompoundParts( } const resultLocal = lowerer.declareHiddenLocal("%compoundResult", computed.type); const resultRef = (): IrExpr => varRef(resultLocal.id, computed.type, loc); - const write: IrStmt = + const store: IrStmt = receiver.type.kind === "array" ? arrayValueStore(lowerer, receiverRef(), indexRef(), resultRef(), receiver.type.elem, loc) : { kind: "bytesSet", arr: receiverRef(), index: indexRef(), value: resultRef(), loc }; + const write: IrStmt = split + ? { + kind: "if", + cond: split.isUndefined, + then: [undefinedTypedArrayKeyFence(lowerer, target, loc)], + else_: [store], + loc, + } + : store; return { expr: { kind: "seqExpr", stmts: [ { kind: "varDecl", localId: receiverLocal.id, init: receiver, loc }, + ...(split?.stmts ?? []), { kind: "varDecl", localId: indexLocal.id, init: index, loc }, { kind: "varDecl", localId: oldLocal.id, init: oldValue, loc }, { kind: "varDecl", localId: resultLocal.id, init: computed, loc }, @@ -1858,13 +1915,23 @@ export function lowerElementWrite(lowerer: Lowerer, expr: ts.BinaryExpression): // The checker's flow type is a typed array, but the storage is wider. if (recv.type.kind !== "bytes") return lowerWiderBytesWrite(lowerer, expr, target, recv); // An index the checker types as a number but whose storage may hold - // undefined (a hole read) converts with ToNumber, as the reads do. - const index = lowerOptionalNumber( + // undefined (a hole read) converts with ToNumber, as the reads do; an + // undefined index names a property the typed array cannot hold. + const rawIndex = lowerer.lowerExpr(target.argumentExpression); + const split = splitUndefinedIndex( lowerer, - lowerer.lowerExpr(target.argumentExpression), - locOf(target.argumentExpression), + rawIndex, target.argumentExpression, + locOf(target.argumentExpression), ); + const index = + split?.index ?? + lowerOptionalNumber( + lowerer, + rawIndex, + locOf(target.argumentExpression), + target.argumentExpression, + ); if (index.type.kind === "dyn" && recv.type.kind === "bytes") { return { kind: "exprStmt", @@ -1880,7 +1947,34 @@ export function lowerElementWrite(lowerer: Lowerer, expr: ts.BinaryExpression): lowerer.unsupported("SC1090", target.argumentExpression, "indexing with non-number keys"); } const value = lowerer.lowerExprExpecting(expr.right, F64); - return { kind: "bytesSet", arr: recv, index, value, loc: locOf(expr) }; + if (split === null) return { kind: "bytesSet", arr: recv, index, value, loc: locOf(expr) }; + const loc = locOf(expr); + const receiver = lowerer.declareHiddenLocal("%indexedBytes", recv.type); + const valueLocal = lowerer.declareHiddenLocal("%elementValue", F64); + return { + kind: "block", + body: [ + { kind: "varDecl", localId: receiver.id, init: recv, loc }, + ...split.stmts, + { kind: "varDecl", localId: valueLocal.id, init: value, loc }, + { + kind: "if", + cond: split.isUndefined, + then: [undefinedTypedArrayKeyFence(lowerer, target, loc)], + else_: [ + { + kind: "bytesSet", + arr: varRef(receiver.id, recv.type, loc), + index, + value: varRef(valueLocal.id, F64, loc), + loc, + }, + ], + loc, + }, + ], + loc, + }; } // Tuple element write `t[0] = v`: a positional-field write (recordSet) // — literal indices only, like the read. @@ -2175,7 +2269,7 @@ export function lowerElementWrite(lowerer: Lowerer, expr: ts.BinaryExpression): "assignment through a possibly missing nested array receiver", ); } - const index = lowerOptionalNumber( + const index = lowerOptionalIndex( lowerer, lowerer.lowerExpr(target.argumentExpression), locOf(target.argumentExpression), diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts index 1f74cea42..ef0d8a2d1 100644 --- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts +++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts @@ -1,4 +1,5 @@ /* Identifier references in expression position: lowerExprInner's identifier branch and its helpers. */ +import { predeclareForwardClass } from "../lower-stmts.js"; import { cjsMemberAliasValueOf } from "../cjs-exports-object.js"; import { builtinClassValue, builtinMemberClassKind } from "../builtins/builtin-class-dyn.js"; import * as ts from "../../ts7/adapter.js"; @@ -41,6 +42,7 @@ import { runtimeOptionalUseOf, } from "../lower-exprs.js"; import { runtimeOptionalReceiverRead } from "./narrowing.js"; +import { iterableUnionOf } from "../iterable-values.js"; import { builtinGlobalValueAt } from "./property-access.js"; import { varRef } from "../../../ir/build.js"; import { @@ -69,6 +71,7 @@ import { builtinModuleConstOf, builtinModuleFnOf, builtinModulesArrayLit, + identifierReadSymbol, stdlibGlobalNameOf, } from "../surfaces.js"; import { builtinConstantsObject } from "../lower-builtin-values.js"; @@ -321,6 +324,16 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: // value arm before routing the member/element operation so dynamic // keys and builtin record walkers do not see a union receiver. const narrowed = lowerer.mapTypeOf(lowerer.typeOf(expr)); + // An optional Iterable proven present re-tags into the Iterable. + if ( + g.type.kind === "union" && + narrowed?.kind === "union" && + narrowed.unionId !== g.type.unionId && + iterableUnionOf(lowerer, narrowed) + ) { + const helper = lowerer.narrowedRetagHelper(expr, g.type.unionId, narrowed.unionId, loc); + if (helper) return { kind: "call", callee: helper, args: [ref], type: narrowed, loc }; + } const arithmeticUnion = g.type.kind === "union" && lowerer.unions.get(g.type.unionId)?.arms.length === 2 && @@ -753,7 +766,7 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: return lowerGlobalValue(lowerer, expr); } { - const sym = lowerer.checker.getSymbolAtLocation(expr); + const sym = identifierReadSymbol(lowerer, expr); // The temporal global is typed (TS 7's lib slice) but Node 24 does // not define it: the read throws Node's ReferenceError. if (isTemporalGlobalRoot(lowerer, expr, sym)) { @@ -789,8 +802,6 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: "ArrayBuffer constructor values have no native lowering; use new ArrayBuffer(length) for fixed-length shared storage", WeakRef: "deref()-after-collect exposes GC timing — genuinely dynamic; hold a strong reference instead", - FinalizationRegistry: - "finalization callbacks expose GC timing — genuinely dynamic; release resources explicitly instead", eval: "runtime code evaluation cannot be compiled ahead of time", }; lowerer.noLowering(expr.text, expr, globalHints[expr.text], sym ?? undefined); @@ -849,7 +860,27 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: // reach here — their hooks claim the property/new forms first). { const sym = lowerer.resolveValueSymbol(expr); - const classInfo = sym ? lowerer.classBySymbol.get(sym) : undefined; + let classInfo = sym ? lowerer.classBySymbol.get(sym) : undefined; + // A class local to a function lowered once per instantiation + // (`$constructor` called with different argument types) registers per + // instance under the same symbol: a hoisted function of a later + // instance, lowered before its own class statement, must not reach the + // class an earlier instance collected. + const declaration = sym?.valueDeclaration; + if ( + classInfo && + lowerer.localClassInstantiations.length > 0 && + declaration && + ts.isClassDeclaration(declaration) && + lowerer.classNamer(declaration) !== classInfo.def.name + ) + classInfo = undefined; + // A local class referenced from a function hoisted above its statement: + // its TDZ box, holding the class as a checked-dynamic constructor. + if (!classInfo && sym && predeclareForwardClass(lowerer, sym)) { + const local = lowerer.resolveLocal(expr); + if (local) return { kind: "varRef", localId: local.id, type: local.type, loc }; + } // A GENERIC class name whose CONTEXTUAL type pins one instantiation // (`const B: new (v: string) => Counter = Counter`): the // value is that instantiation's class object — the generic-fn diff --git a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts index 786f3a337..fa8ad686f 100644 --- a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts +++ b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts @@ -13,6 +13,7 @@ import { STRING, type SrcLoc, UNDEFINED_T, + VOID, arrayOf, canAdaptDynFuncTo, canDynCheckTo, @@ -46,6 +47,8 @@ import { } from "../lower-exprs.js"; import { isConstAssertionTypeNode, unitOnlyUnion } from "../../type-mapper.js"; import { assertsGlobalObject, extractionCopiesReference } from "../assertion-identity.js"; +import { iterableSlotValue, iterableUnionOf } from "../iterable-values.js"; +import { isDeclBoundaryTypeNode } from "../../decl-projection.js"; /** `c ? a : b`. An explicit destination constructs each branch at that * layout before joining it. Without one, retain the expression's inferred @@ -692,6 +695,18 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx const hosted = hostedStorageView(lowerer, expr, narrowed, expr.loc); if (hosted) return hosted; } + // An optional Iterable narrowed to the iterable union (`xs !== undefined`): + // re-tag into it, so for-of, spread and the other consumers see the + // Iterable itself. + if ( + narrowed?.kind === "union" && + narrowed.unionId !== expr.type.unionId && + iterableUnionOf(lowerer, narrowed) + ) { + const helper = lowerer.narrowedRetagHelper(node, expr.type.unionId, narrowed.unionId, expr.loc); + if (helper) + return { kind: "call", callee: helper, args: [expr], type: narrowed, loc: expr.loc }; + } // A checker type narrowed to a UNIT arm (the `=== undefined` branch) // also stays union-typed: a unit arm has no payload to extract, and // nothing useful reads such a value anyway. (Standalone undefined maps @@ -1503,6 +1518,12 @@ export function lowerAsExpression( const target = lowerer.mapTypeOf(targetTs) ?? dynFallbackType(lowerer, expr.type, targetTs); if (!target) lowerer.badType(expr.type, targetTs); if (target.kind === "dyn") return inner; // `as unknown`: erasure + // The declaration-typed boundary (decl-projection.ts): an identity- + // preserving checked conversion of a value package code produced. + if (isDeclBoundaryTypeNode(expr.type)) { + const adopted = declBoundaryCast(lowerer, inner, target, locOf(expr)); + if (adopted) return adopted; + } // An all-`unknown`-fields record target (`err as { code?: unknown }` // — the errno-probing idiom): there is nothing to validate (every // field is unknown, exactly what the dyn value already answers) and @@ -1524,6 +1545,10 @@ export function lowerAsExpression( // reach it (assertion-identity.ts). if (extractionCopiesReference(lowerer, target) && assertsGlobalObject(lowerer, expr)) return inner; + // `value as Iterable`: the value's own iterator, stepped lazily with + // each element checked against T (the generator arm's adapter). + const iterable = iterableSlotValue(lowerer, inner, target); + if (iterable) return iterable; if ( target.kind === "void" || !canDynCheckTo( @@ -1586,3 +1611,83 @@ export function lowerAsExpression( } return { kind: "dynCheck", value: inner, type: target, loc: locOf(expr) }; } + +/** True when a type holds a record or an array somewhere (values whose + * identity can be observed). */ +function holdsReference(lowerer: Lowerer, t: IrType, seen = new Set()): 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((arm) => holdsReference(lowerer, arm, seen)) ?? false + ); +} + +/** `( as __scriptc_decl_N)`: check the dynamic value against + * the projected type and build the native value (dynCheck), then adopt: + * every source object or array that something else still references + * becomes a live capsule of the value built from it + * (scr_dyn_decl_adopt), so identity and mutation visibility hold on both + * sides. Scalar targets are ordinary checked casts. */ +function declBoundaryCast( + lowerer: Lowerer, + inner: IrExpr, + target: IrType, + loc: SrcLoc, +): IrExpr | null { + if ( + !canDynCheckTo( + target, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + ) && + !(target.kind === "union" && lowerer.dynConvertible(target)) + ) + return null; + if (!holdsReference(lowerer, target)) { + return { kind: "dynCheck", value: inner, type: target, loc }; + } + const source = lowerer.declareHiddenLocal("%boundarySource", DYN); + const value = lowerer.declareHiddenLocal("%boundaryValue", target); + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: source.id, init: inner, loc }, + { + kind: "varDecl", + localId: value.id, + init: { + kind: "dynCheck", + value: varRef(source.id, DYN, loc), + adopt: true, + type: target, + loc, + }, + loc, + }, + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "dyn.declAdopt", + args: [ + varRef(source.id, DYN, loc), + { + kind: "dynFrom", + value: varRef(value.id, target, loc), + liveRef: true, + type: DYN, + loc, + }, + ], + type: VOID, + loc, + }, + loc, + }, + ], + result: varRef(value.id, target, loc), + type: target, + loc, + }; +} diff --git a/packages/compiler/src/frontend/lowering/expressions/nullish-assignment.ts b/packages/compiler/src/frontend/lowering/expressions/nullish-assignment.ts index 81b3b9d69..9664f591d 100644 --- a/packages/compiler/src/frontend/lowering/expressions/nullish-assignment.ts +++ b/packages/compiler/src/frontend/lowering/expressions/nullish-assignment.ts @@ -100,7 +100,9 @@ export function lowerShortCircuitAssignment(lowerer: Lowerer, expr: ts.BinaryExp write = (value) => eventAccessorWrite(receiver, name, value, lowerer, loc); } else if (ts.isPropertyAccessExpression(target)) { const probed = tryLowerExpression(lowerer, target.expression); - if (probed?.type.kind === "dyn") { + // `value.#x ??= v` on a checked-dynamic value names the brand-checked + // field (fieldTarget), never a property called "#x". + if (probed?.type.kind === "dyn" && !ts.isPrivateIdentifier(target.name)) { const receiver = save(probed, "%shortCircuitReceiver"); const key: IrExpr = { kind: "strLit", value: target.name.text, type: STRING, loc }; storage = DYN; diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index 678b375fe..4cc0bee1a 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -37,6 +37,7 @@ import { fenceSymbolFieldCopy } from "../symbol-fields.js"; import { planUnionRetag } from "../union-retag.js"; import { jsHoldsDynObject, runtimeOptionalStorageOperand } from "../lower-exprs.js"; import { builtinGlobalValue } from "./property-access.js"; +import { holdsEvolvingArray } from "../lower-stmts.js"; /** `{ a: 1, b: "x" }` → recordLit. The record type comes from the * contextual type when tsc has one (annotated declarations, arguments, @@ -397,7 +398,10 @@ export function lowerDynObjectLiteral( ? lowerer.lowerLambda(prop, true) : ts.isShorthandPropertyAssignment(prop) && shorthandBuiltinValue(lowerer, prop) ? shorthandBuiltinValue(lowerer, prop)! - : !boxValue && ts.isObjectLiteralExpression(valueExpr) + : !boxValue && + (ts.isObjectLiteralExpression(valueExpr) || + (ts.isArrayLiteralExpression(valueExpr) && + holdsEvolvingArray(lowerer, valueExpr))) ? lowerer.lowerExprExpecting(valueExpr, DYN) : lowerer.lowerExpr(valueExpr as ts.Expression); raw = fenceClosureProbe(lowerer, valueExpr, undefined, lowerValue) ?? lowerValue(); @@ -799,6 +803,22 @@ export function lowerObjectLiteral( expr: ts.ObjectLiteralExpression, expected?: IrType & { kind: "record" }, ): IrExpr { + // A spread of an untyped (`any`) source into a typed slot: TypeScript + // types such a literal `any`, and the copy holds whatever own properties + // the source has at runtime. Build it as a checked-dynamic object, then + // check it into the expected record like any other `any` value assigned + // to that slot. + if ( + expected && + !isJsSourceFile(ts.getSourceFileOfNode(expr)) && + expr.properties.nodes.some( + (prop) => + ts.isSpreadAssignment(prop) && + (lowerer.typeOf(prop.expression).flags & ts.TypeFlags.Any) !== 0, + ) + ) { + return lowerer.coerceToExpected(lowerDynObjectLiteral(lowerer, expr), expected); + } // Unannotated JavaScript accumulator fields have no element contract. // Preserve their checked arrays instead of assigning never[] a numeric ABI. // An empty object field is a map the program adds keys to. @@ -933,11 +953,11 @@ export function lowerObjectLiteral( // spread's own position (any position: they introduce one fresh // name, collision-fenced below). Everything else stays fenced. const cs = conditionalSpreadOf(prop.expression) ?? andSpreadOf(lowerer, prop.expression); - if (cs === "unsupported" || (cs && cs.props.length !== 1)) { + if (cs === "unsupported" && conditionalRecordSpreadOf(lowerer, prop.expression) === null) { lowerer.unsupported( "SC1090", prop, - "conditional spreads beyond `...(c ? { field: v } : {})` (exactly one property against an empty arm — spell other shapes as optional fields)", + "conditional spreads beyond `...(c ? { field: v } : {})` and `...(c ? record : {})` (one arm must be empty — spell other shapes as optional fields)", ); } // Spread ORDER is fenced in the field-by-field desugar below, not @@ -1721,64 +1741,71 @@ export function lowerObjectLiteral( if (ts.isSpreadAssignment(prop)) { const cs = conditionalSpreadOf(prop.expression) ?? andSpreadOf(lowerer, prop.expression); if (cs && cs !== "unsupported") { - // `...(c ? { k: v } : {})` — ONE conditional field at the - // spread's own position: cond evaluates exactly once, `v` only - // when the non-empty arm is taken (ternary arms are lazy), and - // the empty arm holds the interned undefined arm — exactly the - // omitted-optional-field representation, which is also why the - // target field must be optional (tsc types the idiom that way). - const csProp = cs.props[0]!; // single-prop-checked in the fence pass - const name = csProp.name.text; - const fieldType = fieldTypes.get(name); - if (!fieldType) { - if (shape.indexValue) { + // `...(c ? { k: v } : {})` — one conditional field per carried + // property at the spread's own position: cond evaluates exactly + // once (into a hidden local when several fields test it), each + // `v` only when the non-empty arm is taken (ternary arms are + // lazy), in property order, and the empty arm holds the interned + // undefined arm — exactly the omitted-optional-field + // representation, which is also why the target fields must be + // optional (tsc types the idiom that way). + let cond: IrExpr | null = null; + for (const csProp of cs.props) { + const name = csProp.name.text; + const fieldType = fieldTypes.get(name); + if (!fieldType) { + if (shape.indexValue) { + lowerer.unsupported( + "SC1090", + prop, + "conditional spreads into index-signature keys (overflow entries model presence — write the key in an if statement instead)", + ); + } + shapeMismatch(prop); + } + // An 'unknown' slot holds the checked-dynamic undefined for the + // empty arm, the absent-property read the width copy uses too. + const absent = + lowerer.absentFieldValue(fieldType, locOf(prop)) ?? + (fieldType.kind === "dyn" ? dynUndefinedExpr(locOf(prop)) : null); + if (!absent) { lowerer.unsupported( "SC1090", prop, - "conditional spreads into index-signature keys (overflow entries model presence — write the key in an if statement instead)", + `conditional spreads onto the required field '${name}' (the empty arm leaves it undefined — declare the field optional)`, ); } - shapeMismatch(prop); + if (fields.some((f) => f.name === name)) { + lowerer.unsupported( + "SC1090", + prop, + `conditional spread of '${name}' over an earlier '${name}' (the desugar keeps one entry per name — restructure so each name has one contributor)`, + ); + } + if (cond === null) { + cond = lowerer.lowerCondition(cs.cond); + if (cs.props.length > 1) cond = capture(cond); + } + const valueNode = ts.isPropertyAssignment(csProp) ? csProp.initializer : csProp; + let v = ts.isPropertyAssignment(csProp) + ? lowerer.lowerExpr(csProp.initializer) + : lowerer.lowerShorthandValue(csProp); + 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: { + kind: "ternary", + cond, + then: cs.whenTrue ? v : absent, + else_: cs.whenTrue ? absent : v, + type: fieldType, + loc: locOf(prop), + }, + }); } - // An 'unknown' slot holds the checked-dynamic undefined for the - // empty arm, the absent-property read the width copy uses too. - const absent = - lowerer.absentFieldValue(fieldType, locOf(prop)) ?? - (fieldType.kind === "dyn" ? dynUndefinedExpr(locOf(prop)) : null); - if (!absent) { - lowerer.unsupported( - "SC1090", - prop, - `conditional spreads onto the required field '${name}' (the empty arm leaves it undefined — declare the field optional)`, - ); - } - if (fields.some((f) => f.name === name)) { - lowerer.unsupported( - "SC1090", - prop, - `conditional spread of '${name}' over an earlier '${name}' (the desugar keeps one entry per name — restructure so each name has one contributor)`, - ); - } - const cond = lowerer.lowerCondition(cs.cond); - const valueNode = ts.isPropertyAssignment(csProp) ? csProp.initializer : csProp; - let v = ts.isPropertyAssignment(csProp) - ? lowerer.lowerExpr(csProp.initializer) - : lowerer.lowerShorthandValue(csProp); - 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: { - kind: "ternary", - cond, - then: cs.whenTrue ? v : absent, - else_: cs.whenTrue ? absent : v, - type: fieldType, - loc: locOf(prop), - }, - }); continue; } // `{ ...base, ... }` — field-by-field copy of a known record @@ -1790,10 +1817,13 @@ export function lowerObjectLiteral( // overridden. Field snapshots above preserve the copy order. let srcNode: ts.Expression = prop.expression; while (ts.isParenthesizedExpression(srcNode)) srcNode = srcNode.expression; + const condRecord = conditionalRecordSpreadOf(lowerer, prop.expression); const srcLowered = capture( - prop === expr.properties.nodes[0] && leadingSpreadLowered !== null - ? leadingSpreadLowered - : lowerer.lowerExpr(srcNode), + condRecord !== null + ? lowerConditionalRecordSpread(lowerer, condRecord, locOf(prop)) + : prop === expr.properties.nodes[0] && leadingSpreadLowered !== null + ? leadingSpreadLowered + : lowerer.lowerExpr(srcNode), ); // Array callbacks can receive a record-or-undefined ABI even when // TypeScript describes a required record. Select the copy strategy @@ -2115,7 +2145,12 @@ export function lowerObjectLiteral( orderSegments.push( srcLowered.type.kind === "dyn" ? { kind: "opaque" } - : { kind: "spread", shapeId: srcType.shapeId, maybeAbsent: false }, + : { + kind: "spread", + shapeId: srcType.shapeId, + maybeAbsent: false, + ...(srcLowered.kind === "varRef" ? { value: srcLowered } : {}), + }, ); for (const f of srcShape.fields) { if (laterNames.has(f.name)) continue; @@ -2731,6 +2766,62 @@ function syntacticAndSpread( return { cond, props, whenTrue: true }; } +/** `...(c ? source : {})` (either arm order; the empty arm may also be + * `undefined` or `null`) where `source` maps to a record and is not the + * literal the field desugar handles (conditionalSpreadOf). The spread + * copies the source's fields when the source arm is taken and nothing + * otherwise, which is the copy of a `Record | undefined` spread source. */ +export function conditionalRecordSpreadOf( + lowerer: Lowerer, + expr: ts.Expression, +): { cond: ts.Expression; source: ts.Expression; whenTrue: boolean } | null { + let e: ts.Expression = expr; + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (!ts.isConditionalExpression(e)) return null; + const unwrap = (a: ts.Expression): ts.Expression => { + let x = a; + while (ts.isParenthesizedExpression(x)) x = x.expression; + return x; + }; + const isEmpty = (x: ts.Expression): boolean => + (ts.isObjectLiteralExpression(x) && x.properties.nodes.length === 0) || + x.kind === ts.SyntaxKind.NullKeyword || + (ts.isIdentifier(x) && + x.text === "undefined" && + (lowerer.typeOf(x).flags & ts.TypeFlags.Undefined) !== 0); + const whenTrue = unwrap(e.whenTrue); + const whenFalse = unwrap(e.whenFalse); + const sourceIsTrue = isEmpty(whenFalse) && !isEmpty(whenTrue); + if (!sourceIsTrue && !(isEmpty(whenTrue) && !isEmpty(whenFalse))) return null; + const source = sourceIsTrue ? whenTrue : whenFalse; + if (lowerer.mapTypeOf(lowerer.typeOf(source))?.kind !== "record") return null; + return { cond: e.condition, source, whenTrue: sourceIsTrue }; +} + +/** The spread source of a conditionalRecordSpreadOf match: `c ? source : + * undefined` as a `Record | undefined` value (cond first, the source only + * when its arm is taken). */ +function lowerConditionalRecordSpread( + lowerer: Lowerer, + spread: { cond: ts.Expression; source: ts.Expression; whenTrue: boolean }, + loc: SrcLoc, +): IrExpr { + const cond = lowerer.lowerCondition(spread.cond); + const source = lowerer.lowerExpr(spread.source); + if (source.type.kind !== "record") lowerer.badType(spread.source, lowerer.typeOf(spread.source)); + const type = lowerer.withUndefinedArm(source.type); + const present = lowerer.coerceToExpected(source, type); + const absent = lowerer.wrappedUndefined(type, loc)!; + return { + kind: "ternary", + cond, + then: spread.whenTrue ? present : absent, + else_: spread.whenTrue ? absent : present, + type, + loc, + }; +} + /** A conditional spread's carrier property: `name: value` or shorthand, * with an identifier/string-literal name. */ export type CondSpreadProp = (ts.PropertyAssignment | ts.ShorthandPropertyAssignment) & { @@ -3262,6 +3353,9 @@ export function lowerShorthandValue( } return lowerer.classValueRef(classInfo, propName); } + // A standard-library global (`{ Date }`): the value the long form + // `{ Date: Date }` reads. + if (lowerer.isStdlibSymbol(resolved)) return lowerer.lowerExpr(propName); } lowerer.rejectUnresolvedSymbol( symbol diff --git a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts index 4b8c5692a..104d85a80 100644 --- a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts +++ b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts @@ -1,3 +1,4 @@ +import { isNavigatorGlobal } from "../builtins/navigator.js"; import { isSafeToDiscard } from "./evaluation-safety.js"; import { completeFuncValueArgs } from "../call-arguments.js"; import { dynUndefinedExpr } from "../../../ir/build.js"; @@ -223,6 +224,16 @@ export function lowerOptionalChain( // Native handle methods have no first-class value to guard; when the // method read itself has no lowering, re-dispatch the plain call (which // reports its own diagnostics) instead of failing on the value read. + // `navigator?.userAgent`: Node's navigator global is never nullish and + // has no value form, so the guarded step is the plain member read. + if (isNavigatorGlobal(lowerer, recvNode)) { + lowerer.chainHandled.add(dotNode); + try { + return lowerer.lowerExpr(expr); + } finally { + lowerer.chainHandled.delete(dotNode); + } + } let loweredReceiver: IrExpr | null; if (dotNode === expr && alwaysPresentStdlibMethod(lowerer, recvNode)) { loweredReceiver = tryLowerExpression(lowerer, recvNode); diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index 7f3910c15..7b12297a8 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -306,10 +306,10 @@ export function lowerPropertyAccessExpr( }; } } - if (!expr.questionDotToken) { - const navigator = lowerNavigatorMember(lowerer, expr, NODE_COMPAT_VERSION); - if (navigator) return navigator; - } + // Node's navigator is never nullish, so `navigator?.userAgent` (zod's + // Cloudflare probe) folds like `navigator.userAgent`. + const navigator = lowerNavigatorMember(lowerer, expr, NODE_COMPAT_VERSION); + if (navigator) return navigator; // A dgram socket's method read as a value in JavaScript (`emit: // socket.emit.bind(socket)`): the socket crosses as its checked-dynamic // handle, whose methods are function values that dispatch on their @@ -553,6 +553,19 @@ export function lowerPropertyAccessExpr( expr.name.text, ) ) { + // A name the program stores on the global object + // (`(_a = globalThis).__config ?? (_a.__config = {})`) reads back from + // the global object's own properties. + const globalType = lowerer.typeOf(staticallySelected(lowerer, expr.expression)); + if (globalThisWrittenNames(lowerer, globalType).has(expr.name.text)) { + return { + kind: "dynKeyGet", + value: lowerer.coerceToExpected(lowerer.lowerExpr(expr.expression), DYN), + key: { kind: "strLit", value: expr.name.text, type: STRING, loc }, + type: DYN, + loc, + }; + } return dynUndefinedExpr(loc); } // `Uint8Array.prototype.toBase64` and its siblings, the base64/hex @@ -666,14 +679,20 @@ export function lowerPropertyAccessExpr( ); if (fixedArity) { // A `this` parameter is type-only: callers never pass it. - return { - kind: "numLit", - value: (fn as ts.FunctionLikeDeclaration).parameters.nodes.filter( - (p) => !isThisParameter(p), - ).length, - type: F64, - loc, - }; + const declared = (fn as ts.FunctionLikeDeclaration).parameters.nodes.filter( + (p) => !isThisParameter(p), + ).length; + const count = Math.max(declared, contextualArgumentCount(lowerer, fn) ?? 0); + // Calls through the expression's own name pass the declared count, + // calls through the wider slot pass more: no constant answers both. + if (count > declared && referencesOwnName(lowerer, fn)) { + lowerer.unsupported( + "SC1090", + expr, + "'arguments.length' in a function expression that is called both through its own name and through a function type with more parameters (the count varies per call)", + ); + } + return { kind: "numLit", value: count, type: F64, loc }; } lowerer.unsupported( "SC1090", @@ -4120,6 +4139,7 @@ const GLOBAL_CONSTRUCTOR_VALUES: ReadonlySet = new Set([ "Promise", "WeakMap", "WeakSet", + "FinalizationRegistry", ...ERROR_CONSTRUCTOR_VALUES, ]); @@ -4440,3 +4460,73 @@ function lowerPromiseConstructorRead( } return null; } + +/** Whether a named function expression refers to itself by name in its + * body (a recursive call, or the function escaping under its own name). */ +function referencesOwnName(lowerer: Lowerer, fn: ts.Node): boolean { + if (!ts.isFunctionExpression(fn) || fn.name === undefined || fn.body === undefined) return false; + const self = lowerer.checker.getSymbolAtLocation(fn.name); + if (self === undefined) return false; + const name = fn.name.text; + let found = false; + ts.walkPreorder(fn.body, (node) => { + if (found) return "stop"; + if ( + ts.isIdentifier(node) && + node.text === name && + lowerer.checker.getSymbolAtLocation(node) === self + ) + found = true; + return undefined; + }); + return found; +} + +/** The argument count every caller passes a function expression whose + * value flows into one fixed-arity function type, an array method's + * callback or an annotated slot: that type's parameter count. Null when + * there is no such type or its arity varies. */ +function contextualArgumentCount(lowerer: Lowerer, fn: ts.Node): number | null { + if (!ts.isFunctionExpression(fn)) return null; + const contextual = lowerer.checker.getContextualType(fn); + if (contextual === undefined) return null; + const signatures = lowerer.checker.getCallSignatures( + lowerer.checker.getNonNullableType(contextual), + ); + if (signatures.length !== 1) return null; + const declaration = signatures[0]!.declaration; + if (declaration === undefined) return null; + let count = 0; + for (const parameter of (declaration.parameters?.nodes ?? []) as ts.ParameterDeclaration[]) { + if (isThisParameter(parameter)) continue; + if (parameter.questionToken || parameter.initializer || parameter.dotDotDotToken) return null; + count++; + } + return count; +} + +/** The property names the program assigns on the global object, through + * `globalThis` or a binding of its type (`(_a = globalThis).x = v`). */ +function globalThisWrittenNames(lowerer: Lowerer, globalType: ts.Type): Set { + const known = lowerer.globalThisWrittenNames; + if (known) return known; + const names = new Set(); + lowerer.globalThisWrittenNames = names; + for (const sf of lowerer.program.getImplementationSourceFiles()) { + if (sf.isDeclarationFile || !sf.text.includes("globalThis")) continue; + ts.walkPreorder(sf, (node) => { + if ( + ts.isBinaryExpression(node) && + (node.operatorToken.kind === ts.SyntaxKind.EqualsToken || + node.operatorToken.kind === ts.SyntaxKind.QuestionQuestionEqualsToken || + node.operatorToken.kind === ts.SyntaxKind.BarBarEqualsToken || + node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandEqualsToken) && + ts.isPropertyAccessExpression(node.left) && + lowerer.typeOf(node.left.expression) === globalType + ) + names.add(node.left.name.text); + return undefined; + }); + } + return names; +} diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts index 61558f008..f1590e9cc 100644 --- a/packages/compiler/src/frontend/lowering/expressions/relational.ts +++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts @@ -49,13 +49,53 @@ import { InternalCompilerError } from "../../../errors.js"; import { tryLowerExpression } from "./try-lower-expression.js"; import { isJsSourceFile, locOf } from "../../program.js"; import { checkedClassInstanceOf } from "../class-construction.js"; +import { unsupportedDiag } from "../../../diagnostics/diagnostic.js"; import { globalSymbolKey } from "./global-symbols.js"; +import { builtinGlobalHasKey } from "../builtins/global-namespace-keys.js"; import { foldedStringKeyOf } from "./object-literals.js"; import { NARROW_FIRST, sideEffectFreeOptionValue } from "../surfaces.js"; import { isExecExceptionType, refuseExecExceptionCode } from "../builtins/errors.js"; import { hasRuntimeStatics } from "../class-runtime-statics.js"; import { nullProtoRecordShape, recordNullProtoTest } from "../null-prototype.js"; +/** The class value an `instanceof` right-hand side holds. A JavaScript + * object's property holding a program class (`holder.type`) reads as a + * checked-dynamic value that the checker types as the class: it converts + * to the class value. Runtime-provided classes keep their own lowering. */ +function classValueOperand( + lowerer: Lowerer, + right: ts.Expression, + type: IrType & { kind: "classval" }, +): IrExpr { + if ( + !isJsSourceFile(ts.getSourceFileOfNode(right)) || + ts.isIdentifier(right) || + lowerer.classes.get(type.className)?.def.runtime + ) + return lowerer.lowerExpr(right); + const value = lowerer.lowerExprExpecting(right, DYN); + if (value.type.kind !== "dyn") return value; + return { kind: "dynCheck", value, type, loc: locOf(right) }; +} + +/** The value kinds that never hold an instance of a program class. */ +const NO_CLASS_INSTANCE_KINDS: ReadonlySet = new Set([ + "f64", + "bigint", + "date", + "string", + "bool", + "array", + "map", + "set", + "regex", + "bytes", + "symbol", + "promise", + "undefinedT", + "nullT", +]); + /** `x instanceof C` for a program-declared class C. When x's static * class and C are both in extends-hierarchies the test is dynamic — the * O(1) preorder-interval check against the vtable (`instanceOf` node). @@ -147,6 +187,10 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc loc, }; } + if (lowerer.isStdlibGlobal(expr.right, "FinalizationRegistry")) { + const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN); + return { kind: "libCall", fn: "finalizationRegistry.is", args: [value], type: BOOL, loc }; + } const rhsBuiltin = ts.isIdentifier(expr.right) ? lowerer.builtinImportOf(expr.right) : ts.isPropertyAccessExpression(expr.right) @@ -394,7 +438,58 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc // (itself — no descendants can flow into the slot), so the answer // folds statically exactly like the named-target folds below. const rhsClassval = storedClassValueType(lowerer, expr.right); - if (rhsClassval?.kind === "classval" && !lowerer.caughtLocalOf(expr.left)) { + // A catch binding against a class held by a binding (`var E = class + // extends Error {}`). The binding holds the declared class or one of its + // subclasses. Without subclasses the test is the class's caught test, + // after the right operand's read; otherwise a payload of the declared + // class is tested against the class the binding holds. + const caught = lowerer.caughtLocalOf(expr.left); + const caughtTarget = + rhsClassval?.kind === "classval" && caught + ? lowerer.classes.get(rhsClassval.className) + : undefined; + if (caught && caughtTarget && !caughtTarget.localClass && !caughtTarget.def.runtime) { + const classValue = lowerer.lowerExpr(expr.right); + if (caughtTarget.subclasses.length === 0) { + return { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: classValue, loc }], + result: caughtClassInstanceOf(lowerer, caught.id, caughtTarget, loc), + type: BOOL, + loc, + }; + } + if (classValue.type.kind !== "classval") + lowerer.badType(expr.right, lowerer.typeOf(expr.right)); + const held = lowerer.declareHiddenLocal("%instanceofClass", classValue.type); + const instance: IrType = { kind: "object", className: caughtTarget.def.name }; + return { + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: held.id, init: classValue, loc }], + result: { + kind: "logical", + op: "&&", + left: caughtClassInstanceOf(lowerer, caught.id, caughtTarget, loc), + right: { + kind: "instanceOfValue", + value: { + kind: "caughtNarrow", + value: varRef(caught.id, CAUGHT, loc), + type: instance, + loc, + }, + classValue: varRef(held.id, classValue.type, loc), + type: BOOL, + loc, + }, + type: BOOL, + loc, + }, + type: BOOL, + loc, + }; + } + if (rhsClassval?.kind === "classval" && !caught) { const targetInfo = lowerer.classes.get(rhsClassval.className); if (!targetInfo) { // The type world names a class the lowering never registered @@ -426,7 +521,7 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc "'instanceof' through a runtime-provided class value on an untyped value (give the operand a class type first)", ); } - const classValue = lowerer.lowerExpr(expr.right); + const classValue = classValueOperand(lowerer, expr.right, rhsClassval); return classInstanceOf(lowerer, left, targetInfo, loc, classValue); } // A union of class instances against a runtime class value (`new K()` @@ -435,6 +530,22 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc const tested = unionClassValueInstanceOf(lowerer, left, expr.right, targetInfo, loc); if (tested) return tested; } + // A value whose type holds no class instance (a primitive, a native + // collection, a date, a regex, an array) is never an instance + // of a program class: false, after both operands evaluate. + if (!targetInfo.def.runtime && NO_CLASS_INSTANCE_KINDS.has(left.type.kind)) { + const classValue = classValueOperand(lowerer, expr.right, rhsClassval); + return { + kind: "seqExpr", + stmts: [ + { kind: "exprStmt", expr: left, loc }, + { kind: "exprStmt", expr: classValue, loc }, + ], + result: { kind: "boolLit", value: false, type: BOOL, loc }, + type: BOOL, + loc, + }; + } if (left.type.kind !== "object") { lowerer.unsupported( "SC1090", @@ -449,7 +560,7 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc (lowerer.inHierarchy(targetInfo) && lowerer.inHierarchy(lhsInfo)) || (targetInfo.localClass !== undefined && lhsInfo.localClass !== undefined) ) { - const classValue = lowerer.lowerExpr(expr.right); + const classValue = classValueOperand(lowerer, expr.right, rhsClassval); if (classValue.type.kind !== "classval") lowerer.badType(expr.right, lowerer.typeOf(expr.right)); const writableTest = writableClassValueTest( @@ -487,10 +598,19 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc "statically-decided 'instanceof' on computed operands (bind the values to variables first)", ); } + // A checked-dynamic right-hand side (a constructor read from a table, a + // structural constructor type, a builtin constructor held as `any`): + // the runtime's instanceof over the value, in both source languages. const constructor = tryLowerExpression(lowerer, expr.right); - if (constructor?.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr))) { + if (constructor?.type.kind === "dyn") { const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN); - return checkedClassInstanceOf(lowerer, value, constructor, loc); + return checkedClassInstanceOf( + lowerer, + value, + constructor, + dynInstanceOfRefusal(lowerer, expr), + loc, + ); } // `this instanceof Glob` in a JavaScript constructor function: the // function's prototype against the value's prototype chain. @@ -539,39 +659,8 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc // narrowing then types the branches; reads bridge through caughtRead. const caughtLocal = lowerer.caughtLocalOf(expr.left); if (caughtLocal) { - const caughtInstanceOf = (info: ClassInfo): IrExpr => { - if (!lowerer.inHierarchy(info)) { - const value = varRef(caughtLocal.id, CAUGHT, loc); - return { - kind: "ternary", - cond: { - kind: "caughtTest", - value, - test: "instanceof", - className: info.def.name, - type: BOOL, - loc, - }, - then: { kind: "boolLit", value: true, type: BOOL, loc }, - else_: classInstanceOf( - lowerer, - { kind: "caughtToDyn", value, type: DYN, loc }, - info, - loc, - ), - type: BOOL, - loc, - }; - } - return { - kind: "caughtTest", - value: { kind: "varRef", localId: caughtLocal.id, type: CAUGHT, loc }, - test: "instanceof", - className: info.def.name, - type: BOOL, - loc, - }; - }; + const caughtInstanceOf = (info: ClassInfo): IrExpr => + caughtClassInstanceOf(lowerer, caughtLocal.id, info, loc); let test = caughtInstanceOf(target); // A thrown checked-dynamic object whose prototype chain reaches // Error.prototype (an ES5 constructor function inheriting from it). @@ -966,6 +1055,42 @@ function writableClassValueTest( }; } +/** A catch binding tested against a program class: the runtime test against + * the class's preorder interval (false for non-hierarchy-object payloads). */ +function caughtClassInstanceOf( + lowerer: Lowerer, + caughtId: string, + info: ClassInfo, + loc: SrcLoc, +): IrExpr { + if (!lowerer.inHierarchy(info)) { + const value = varRef(caughtId, CAUGHT, loc); + return { + kind: "ternary", + cond: { + kind: "caughtTest", + value, + test: "instanceof", + className: info.def.name, + type: BOOL, + loc, + }, + then: { kind: "boolLit", value: true, type: BOOL, loc }, + else_: classInstanceOf(lowerer, { kind: "caughtToDyn", value, type: DYN, loc }, info, loc), + type: BOOL, + loc, + }; + } + return { + kind: "caughtTest", + value: { kind: "varRef", localId: caughtId, type: CAUGHT, loc }, + test: "instanceof", + className: info.def.name, + 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. */ @@ -1419,6 +1544,16 @@ export function lowerInExpression( "'in' with computed (non-literal) keys (a const whose type is one string literal folds; a string-typed key answers over index-signature record receivers)", ); } + // A feature probe on a builtin constructor global (`"captureStackTrace" + // in Error`): the global is a fixed object, so the answer is static. + { + let recvNode = expr.right; + while (ts.isParenthesizedExpression(recvNode)) recvNode = recvNode.expression; + if (ts.isIdentifier(recvNode) && lowerer.isStdlibGlobal(recvNode, recvNode.text)) { + const has = builtinGlobalHasKey(recvNode.text, key); + if (has !== null) return { kind: "boolLit", value: has, type: BOOL, loc }; + } + } // A receiver re-read (the static folds below) is safe for plain reads // AND for side-effect-free expressions (an object literal of literals // — the `"a" in { a: true }` shape). @@ -1493,6 +1628,32 @@ export function lowerInExpression( ); if (helper) recv = { kind: "call", callee: helper, args: [recv], type: siteType, loc }; } + // A builtin class held as a brand record (TextEncoder, TextDecoder: only + // `%`-prefixed internal fields): its members are the class's prototype + // members, which the standard library declares, so `"encodeInto" in + // encoder` answers from the checker's type, plus Object.prototype's keys. + // The receiver evaluates for its effects. + if (recv.type.kind === "record") { + const shape = lowerer.shapes.get(recv.type.shapeId); + if (shape && shape.fields.length > 0 && shape.fields.every((f) => f.name.startsWith("%"))) { + const declared = + lowerer.checker.getPropertyOfType(lowerer.typeOf(expr.right), key) !== undefined; + const answer: IrExpr = { + kind: "boolLit", + value: declared || OBJECT_PROTOTYPE_KEYS.has(key), + type: BOOL, + loc, + }; + if (recv.kind === "varRef") return answer; + return { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: recv, loc }], + result: answer, + type: BOOL, + loc, + }; + } + } // Error-rooted receivers (builtin or user subclass — the isErrnoException // predicate's `err instanceof Error && "code" in err` shape): `code` // answers from the runtime error's code slot (stamped by fs/system/ @@ -2000,3 +2161,25 @@ function lowerRuntimeKeyIn( } return { kind: "call", callee: helper, args: [keyIr, recv], type: BOOL, loc }; } + +/** Standard-library constructors the untyped instanceof helper has no + * instance test for: an untyped `x instanceof Object` always refuses. */ +const DYN_INSTANCEOF_REFUSED: ReadonlySet = new Set(["Object", "Function"]); + +/** The SC1090 message `x instanceof C` throws, with the site's position, + * when `C` is a runtime constructor with no instance test. A right-hand + * side that names one of DYN_INSTANCEOF_REFUSED always reaches that + * refusal, so the site is listed with the runtime fences. */ +function dynInstanceOfRefusal(lowerer: Lowerer, expr: ts.BinaryExpression): string { + const diag = unsupportedDiag("SC1090", locOf(expr), "instanceof against this native constructor"); + const symbol = lowerer.checker.getSymbolAtLocation(expr.right); + if ( + symbol !== undefined && + lowerer.isStdlibSymbol(symbol) && + DYN_INSTANCEOF_REFUSED.has(ts.symbolName(symbol)) + ) + lowerer.runtimeFences.push(diag); + const sf = ts.getSourceFileOfNode(expr); + const line = ts.getLineAndCharacterOfPosition(sf, diag.loc.start).line + 1; + return `${diag.message} [${diag.code} at ${diag.loc.file}:${line}]`; +} diff --git a/packages/compiler/src/frontend/lowering/expressions/updates.ts b/packages/compiler/src/frontend/lowering/expressions/updates.ts index 128093dad..04ca23a96 100644 --- a/packages/compiler/src/frontend/lowering/expressions/updates.ts +++ b/packages/compiler/src/frontend/lowering/expressions/updates.ts @@ -1,5 +1,10 @@ /* Increment, decrement and prefix unary operators, and compound field updates. */ import * as ts from "../../ts7/adapter.js"; +import { + lowerOptionalIndex, + splitUndefinedIndex, + undefinedTypedArrayKeyFence, +} from "../undefined-index.js"; import { isStrictCode } from "../strict-mode.js"; import { isPrivateWriteReceiver } from "../private-brands.js"; import type { Lowerer } from "../lowerer.js"; @@ -311,12 +316,27 @@ export function lowerIncDec( (raw.type.kind === "array" || raw.type.kind === "bytes") ) { const receiver = save(raw, "%incrementArray"); - const rawIndex = lowerOptionalNumber( - lowerer, - lowerer.lowerExpr(access.argumentExpression), - locOf(access.argumentExpression), - access.argumentExpression, - ); + // An undefined index names the "undefined" property + // (undefined-index.ts), which a typed array cannot hold. + const loweredIndex = lowerer.lowerExpr(access.argumentExpression); + const split = + raw.type.kind === "bytes" + ? splitUndefinedIndex( + lowerer, + loweredIndex, + access.argumentExpression, + locOf(access.argumentExpression), + ) + : null; + if (split) stmts.push(...split.stmts); + const rawIndex = + split?.index ?? + lowerOptionalIndex( + lowerer, + loweredIndex, + locOf(access.argumentExpression), + access.argumentExpression, + ); const index = save( rawIndex.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr)) ? { kind: "libCall", fn: "dyn.toNumberCoerce", args: [rawIndex], type: F64, loc } @@ -340,7 +360,7 @@ export function lowerIncDec( loc, invalidNaN: true, }; - return finish(read, (value) => + const store = (value: IrExpr): IrStmt => type.kind === "array" ? arrayValueStore( lowerer, @@ -356,8 +376,15 @@ export function lowerIncDec( index, value: lowerer.coerceInto(access, value, F64), loc, - }, - ); + }; + if (!split) return finish(read, store); + return finish(read, (value) => ({ + kind: "if", + cond: split.isUndefined, + then: [undefinedTypedArrayKeyFence(lowerer, access, loc)], + else_: [store(value)], + loc, + })); } // A TS member statically typed number on a checked-dynamic receiver (a // class-backed protocol view): the expanded form `o.v = o.v + 1`, its diff --git a/packages/compiler/src/frontend/lowering/function-returns.ts b/packages/compiler/src/frontend/lowering/function-returns.ts index 9500d6d2d..dde109ea7 100644 --- a/packages/compiler/src/frontend/lowering/function-returns.ts +++ b/packages/compiler/src/frontend/lowering/function-returns.ts @@ -1,3 +1,4 @@ +import { iterableGeneratorArm } from "./iterable-values.js"; import { mapAsyncGeneratorAnnotation } from "../type-mapper.js"; import { dynUndefinedExpr } from "../../ir/build.js"; import * as ts from "../ts7/adapter.js"; @@ -14,6 +15,7 @@ import { import { isJsSourceFile } from "../program.js"; import { PoisonError, dynFallbackType } from "./lowerer.js"; import { functionCanReturnUndefined } from "../function-completion.js"; +import { jsBindingAppendsOnly, jsBindingHasOpenWrites } from "./lower-stmts.js"; import type { FnCtx } from "./function-context.js"; /** Keep the conservative completion rule shared by return construction @@ -123,6 +125,32 @@ export function producesConstructor(lowerer: Lowerer, type: ts.Type): boolean { return false; } +/** Whether a return of `decl` itself (not of a nested function) is a bare + * read of a JavaScript variable whose writes the checker does not + * constrain. A string that is only appended to stays a string. */ +function returnsOpenJsBinding(lowerer: Lowerer, decl: ts.SignatureDeclaration): boolean { + let open = false; + const opens = (value: ts.Expression | undefined): boolean => { + while (value && ts.isParenthesizedExpression(value)) value = value.expression; + if (!value || !ts.isIdentifier(value)) return false; + const binding = lowerer.resolveValueSymbol(value)?.valueDeclaration; + return ( + binding !== undefined && + ts.isVariableDeclaration(binding) && + jsBindingHasOpenWrites(lowerer, binding) && + !jsBindingAppendsOnly(lowerer, binding) + ); + }; + const scan = (node: ts.Node): void => { + if (open || (node !== decl && ts.isFunctionLike(node))) return; + if (ts.isReturnStatement(node) && opens(node.expression)) open = true; + ts.forEachChild(node, scan); + }; + if (ts.isArrowFunction(decl) && decl.body && !ts.isBlock(decl.body)) open = opens(decl.body); + else scan(decl); + return open; +} + /** Map a declaration's return contract while retaining JavaScript * inference fallbacks. Diagnostics preserve the caller's blame node. */ export function declaredReturnType( @@ -147,6 +175,9 @@ export function declaredReturnType( // (`.action(() => { throw ... })`). `never` VALUES stay unmapped. if (retTsType.flags & ts.TypeFlags.Never) return VOID; let mappedReturn = lowerer.mapTypeOf(retTsType); + // A generator declared to return Iterable returns a generator over T. + if (mappedReturn && decl.asteriskToken) + mappedReturn = iterableGeneratorArm(lowerer, mappedReturn); if ( mappedReturn === null && decl.asteriskToken && @@ -208,6 +239,23 @@ export function declaredReturnType( lowerer.unions.get(mappedReturn.unionId)?.arms.some((arm) => arm.kind === "func") ) return DYN; + // A JS function returning a local that holds whatever its writes store + // (jsBindingHasOpenWrites): the inferred result is the initializer's + // type, which the returned value need not have. + // An async function's promise fulfills with that value. + if ( + isJsSourceFile(ts.getSourceFileOfNode(decl)) && + !decl.type && + !decl.asteriskToken && + !hasExplicitJsDocReturn(decl) && + mappedReturn !== null && + mappedReturn.kind !== "dyn" && + !(mappedReturn.kind === "promise" && mappedReturn.inner.kind === "dyn") && + returnsOpenJsBinding(lowerer, decl) + ) + return decl.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) + ? { kind: "promise", inner: DYN } + : DYN; // JS inference assigns Number to arithmetic over any, although the // runtime operands can both be BigInts. Preserve that native result. if ( diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index e27ab7f5f..222cb89f9 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -342,10 +342,15 @@ export function genericCallInstance( const local = localClassResultInstance(lowerer, expr, info, rsig, params, retTs); if (local) return local; // A result instantiated at `any` (a type argument inferred from an - // untyped value, such as an untyped forwarding call) is checked-dynamic. + // untyped value, such as an untyped forwarding call) is checked-dynamic, + // and so is a JavaScript result whose JSDoc names a type the file never + // declares or imports (`@returns {MiddlewareHandler}` in a published + // bundle): the checker's error type is documentation it could not + // resolve, not a contract. const mappedReturn = lowerer.mapTypeOf(retTs) ?? - ((retTs.flags & ts.TypeFlags.Any) !== 0 && !isJsSourceFile(ts.getSourceFileOfNode(info.decl)) + ((retTs.flags & ts.TypeFlags.Any) !== 0 && + (!isJsSourceFile(ts.getSourceFileOfNode(info.decl)) || retTs.isErrorType()) ? DYN : null); if (!mappedReturn) { diff --git a/packages/compiler/src/frontend/lowering/iterable-values.ts b/packages/compiler/src/frontend/lowering/iterable-values.ts new file mode 100644 index 000000000..78de9720c --- /dev/null +++ b/packages/compiler/src/frontend/lowering/iterable-values.ts @@ -0,0 +1,342 @@ +/* `Iterable` values. The type maps to the iterable union + * (type-mapper.ts iterableUnionType): an array, Set, Map or string keeps + * its own arm, so the value stays the same multi-pass object (identity, + * inspection and typeof are the source's own), and every iterator — a + * generator, an array, Map or Set iterator, a checked-dynamic iterator — + * enters the generator arm, single-pass as in JavaScript. + * + * Consumers open an iterator over the current arm the way for-of's + * GetIterator does: an array, Set, Map or string arm starts a fresh live + * walk (the sources' own iterator helpers), the generator arm is itself. + * for-of gives the array arm its own array loop, so iterating an Iterable + * that holds an array runs at array-loop speed. */ +import * as ts from "../ts7/adapter.js"; +import { locOf } from "../program.js"; +import { varRef, numLit } from "../../ir/build.js"; +import { + BOOL, + F64, + iterableArmsElem, + typeEquals, + type IrExpr, + type IrStmt, + type IrType, +} from "../../ir/ir.js"; +import { typeKey } from "../type-mapper.js"; +import { keyedHelperName } from "./stable-names.js"; +import { PoisonError, type Lowerer } from "./lowerer.js"; +import { noLoweringDiag, unsupportedDiag } from "../../diagnostics/diagnostic.js"; +import { generatorLoopBinding, lowerForOfGenerator, type GenType } from "./lower-generators.js"; +import { + iteratorSlotAdapter, + lowerArrayIteratorValue, + lowerCollectionIteratorValue, + lowerStringIteratorValue, +} from "./iterator-adapters.js"; +import { arrayValueRead, arrayValueType } from "./array-values.js"; + +/** An iterable union and the tags of its arms. */ +export interface IterableUnion { + unionId: string; + type: IrType & { kind: "union" }; + elem: IrType; + arms: readonly IrType[]; + /** The generator arm: a sync generator over `elem`. */ + gen: GenType; +} + +/** The iterable union `t` names, or null. */ +export function iterableUnionOf(lowerer: Lowerer, t: IrType): IterableUnion | null { + const info = iterableSlotUnionOf(lowerer, t); + // An optional Iterable (`Iterable | undefined`) is consumed only once + // narrowed to the iterable union itself. + if (info === null || info.arms.some((arm) => arm.kind === "undefinedT" || arm.kind === "nullT")) + return null; + return info; +} + +/** The iterable union `t` names, also an optional or nullable one + * (`Iterable | undefined`, whose unit arms sit beside the iterable + * arms), or null. */ +function iterableSlotUnionOf(lowerer: Lowerer, t: IrType): IterableUnion | null { + if (t.kind !== "union") return null; + const arms = lowerer.unions.get(t.unionId)?.arms; + if (!arms) return null; + const elem = iterableArmsElem(arms); + if (elem === null) return null; + const gen = arms.find((arm) => arm.kind === "generator") as GenType; + return { unionId: t.unionId, type: t, elem, arms, gen }; +} + +/** The generator arm of an iterable union (what a generator function + * declared to return Iterable returns), or `t` itself. */ +export function iterableGeneratorArm(lowerer: Lowerer, t: IrType): IrType { + return iterableUnionOf(lowerer, t)?.gen ?? t; +} + +/** A value flowing into an iterable-typed slot, optional or nullable + * ones included: a source arm wraps as itself; a generator over other + * channel types or a checked-dynamic iterator (a Map or Set iterator) + * adapts into the generator arm. Null when neither applies, leaving the + * ordinary conversion fences to refuse other sources (class iterables, + * typed arrays, other element types). */ +export function iterableSlotValue(lowerer: Lowerer, expr: IrExpr, expected: IrType): IrExpr | null { + const info = iterableSlotUnionOf(lowerer, expected); + if (info === null || expr.type.kind === "union") return null; + const wrap = (value: IrExpr): IrExpr => ({ + kind: "unionWrap", + unionId: info.unionId, + tag: lowerer.armTag(info.unionId, value.type), + value, + type: info.type, + loc: expr.loc, + }); + if (lowerer.armTag(info.unionId, expr.type) >= 0) return wrap(expr); + if (expr.type.kind === "generator" || expr.type.kind === "dyn") { + const adapted = iteratorSlotAdapter(lowerer, expr, info.gen); + if (adapted && typeEquals(adapted.type, info.gen)) return wrap(adapted); + } + // Class instances (their own [Symbol.iterator]) and typed arrays have no + // arm: refuse them here, by name. + if (expr.type.kind === "object" || expr.type.kind === "bytes") { + lowerer.pushDiag( + unsupportedDiag( + "SC1090", + expr.loc, + `an Iterable<${lowerer.fmt(info.elem)}> holding a '${lowerer.fmt(expr.type)}' (an Iterable value holds an array, Set, Map, string, or iterator)`, + ), + ); + throw new PoisonError(); + } + return null; +} + +/** GetIterator on an iterable union value: an interned helper answering a + * generator over the current arm — a fresh live walk of an array, Set, Map + * or string, or the generator arm itself. */ +export function iterableGenerator(lowerer: Lowerer, value: IrExpr, info: IterableUnion): IrExpr { + const loc = value.loc; + const key = `iterableOf:${typeKey(info.type)}`; + let name = lowerer.valueHelpers.get(key); + if (!name) { + const u = varRef("u.0", info.type, loc); + const body: IrStmt[] = []; + let genTag = -1; + info.arms.forEach((arm, tag) => { + if (arm.kind === "generator") { + genTag = tag; + return; + } + const narrowed: IrExpr = { + kind: "unionNarrow", + unionId: info.unionId, + tag, + value: u, + type: arm, + loc, + }; + const walk = + arm.kind === "array" + ? lowerArrayIteratorValue(lowerer, narrowed, "values", info.gen, loc) + : arm.kind === "set" + ? lowerCollectionIteratorValue(lowerer, narrowed, "values", info.gen, loc) + : arm.kind === "map" + ? lowerCollectionIteratorValue(lowerer, narrowed, "entries", info.gen, loc) + : arm.kind === "string" + ? lowerStringIteratorValue(lowerer, narrowed, info.gen, loc) + : null; + if (walk === null || !typeEquals(walk.type, info.gen)) { + lowerer.pushDiag( + noLoweringDiag( + `iterating an Iterable<${lowerer.fmt(info.elem)}> that holds a ${arm.kind}`, + loc, + ), + ); + throw new PoisonError(); + } + body.push({ + kind: "if", + cond: { + kind: "unionIsTag", + unionId: info.unionId, + tag, + negated: false, + value: u, + type: BOOL, + loc, + }, + then: [{ kind: "return", value: walk, loc }], + else_: null, + loc, + }); + }); + body.push({ + kind: "return", + value: { + kind: "unionNarrow", + unionId: info.unionId, + tag: genTag, + value: u, + type: info.gen, + loc, + }, + loc, + }); + name = keyedHelperName(lowerer, "%iterable.of", key); + lowerer.valueHelpers.set(key, name); + lowerer.liftedFns.push({ + name, + params: [{ localId: "u.0", name: "u", type: info.type }], + returnType: info.gen, + locals: [{ id: "u.0", name: "u", type: info.type, mutable: false }], + body, + loc, + }); + } + return { kind: "call", callee: name, args: [value], type: info.gen, loc }; +} + +/** Whether a loop body declares a class (a class has one lowering per + * declaration, so its body cannot lower twice). */ +function declaresClass(body: ts.Statement): boolean { + let found = false; + ts.walkPreorder(body, (node) => { + if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) { + found = true; + return "stop"; + } + return undefined; + }); + return found; +} + +/** `for (x of iterable)` over an iterable union: + * + * { const %src = ; + * if (%src is the array arm) { + * const %arr = ; + * for (let %cur = 0; %cur < %arr.length; %cur++) { const x = %arr[%cur]; } + * } else { + * for (const x of GetIterator(%src)) // the generator for-of + * } + * } + * + * The array arm walks exactly like an array for-of (live length, the + * backend's dense-array loop forms), so an Iterable holding an array + * iterates at array speed; every other arm drives its iterator with the + * generator for-of's IteratorClose rules. The body lowers once for each + * loop; a body that declares a class lowers only once, in the generator + * for-of over every arm. */ +export function lowerForOfIterable( + lowerer: Lowerer, + stmt: ts.ForOfStatement, + iterable: IrExpr, + info: IterableUnion, + labels?: string[], +): IrStmt { + const loc = locOf(stmt); + const generatorLoop = (source: IrExpr): IrStmt => + lowerForOfGenerator( + lowerer, + stmt, + iterableGenerator(lowerer, source, info) as IrExpr & { type: GenType }, + labels, + ); + const arrayTag = info.arms.findIndex((arm) => arm.kind === "array"); + // A union without an array arm (`Set | Generator`) has only the + // generator loop. + if (stmt.awaitModifier || declaresClass(stmt.statement) || arrayTag < 0) + return generatorLoop(iterable); + const arrT = info.arms[arrayTag]!; + lowerer.scopes.push(new Map()); + try { + const src = lowerer.declareHiddenLocal("%iterableSource", info.type); + const srcRef = (): IrExpr => varRef(src.id, info.type, loc); + const arr = lowerer.declareHiddenLocal("%arrayIterator", arrT); + const cursor = lowerer.declareHiddenLocal("%arrayCursor", F64); + cursor.mutable = true; + const arrRef = varRef(arr.id, arrT, loc); + const curRef = varRef(cursor.id, F64, loc); + // The element: a hole reads undefined, which an undefined-armed T + // holds; for other element types the read traps, as typed reads do. + const element: IrExpr = typeEquals(arrayValueType(lowerer, info.elem), info.elem) + ? arrayValueRead(lowerer, arrRef, curRef, info.elem, loc) + : { kind: "arrayGet", arr: arrRef, index: curRef, type: info.elem, loc }; + const arrayLoop = ((): IrStmt => { + lowerer.scopes.push(new Map()); + try { + const binding = generatorLoopBinding(lowerer, stmt, element, info.elem); + const body = lowerer.inCtl("loop", () => lowerer.lowerScopedBlock(stmt.statement), labels); + return { + kind: "for", + init: { kind: "varDecl", localId: cursor.id, init: numLit(0, loc), loc }, + cond: { + kind: "bin", + op: "<", + left: curRef, + right: { + kind: "arrIntrinsic", + method: "length", + receiver: arrRef, + args: [], + type: F64, + loc, + }, + type: BOOL, + loc, + }, + update: { + kind: "assign", + localId: cursor.id, + value: { kind: "bin", op: "+", left: curRef, right: numLit(1, loc), type: F64, loc }, + loc, + }, + body: [...binding, ...body], + ...(labels && { labels }), + loc, + }; + } finally { + lowerer.scopes.pop(); + } + })(); + return { + kind: "block", + body: [ + { kind: "varDecl", localId: src.id, init: iterable, loc }, + { + kind: "if", + cond: { + kind: "unionIsTag", + unionId: info.unionId, + tag: arrayTag, + negated: false, + value: srcRef(), + type: BOOL, + loc, + }, + then: [ + { + kind: "varDecl", + localId: arr.id, + init: { + kind: "unionNarrow", + unionId: info.unionId, + tag: arrayTag, + value: srcRef(), + type: arrT, + loc, + }, + loc, + }, + arrayLoop, + ], + else_: [generatorLoop(srcRef())], + loc, + }, + ], + loc, + }; + } finally { + lowerer.scopes.pop(); + } +} diff --git a/packages/compiler/src/frontend/lowering/key-order-witness.test.ts b/packages/compiler/src/frontend/lowering/key-order-witness.test.ts index de16e5d61..c85902c96 100644 --- a/packages/compiler/src/frontend/lowering/key-order-witness.test.ts +++ b/packages/compiler/src/frontend/lowering/key-order-witness.test.ts @@ -1,5 +1,12 @@ import { expect, test } from "vitest"; -import { F64, type IrExpr, type IrFunction, type IrStmt, type IrType } from "../../ir/ir.js"; +import { + F64, + KEY_ORDER_FIELD, + type IrExpr, + type IrFunction, + type IrStmt, + type IrType, +} from "../../ir/ir.js"; import { observeRecordLiteralRebuilds, transformExpr } from "../../ir/traverse.js"; import { ShapeRegistry } from "../type-mapper.js"; import { @@ -21,7 +28,7 @@ interface Fixture { notes: KeyOrderNotes; record(fields: [string, IrType][], order: string[]): IrType & { kind: "record" }; literal(type: IrType, keys: string[], segments?: KeyOrderSegment[]): IrExpr; - run(body: IrStmt[]): void; + run(body: IrStmt[]): IrFunction; order(type: IrType & { kind: "record" }): string[] | undefined; } @@ -35,6 +42,14 @@ function fixture(): Fixture { classes: new Map(), armTag: (unionId: string, arm: IrType): number => unionId === "u0" && arm.kind === "undefinedT" ? 1 : -1, + recordFieldPresent: (obj: IrExpr, shapeId: string, field: string): IrExpr => ({ + kind: "recordHas", + obj, + shapeId, + field, + type: { kind: "bool" }, + loc, + }), } as unknown as Lowerer; const record = (fields: [string, IrType][], order: string[]): IrType & { kind: "record" } => ({ kind: "record", @@ -55,15 +70,20 @@ function fixture(): Fixture { segments ?? keys.map((name) => ({ kind: "key", name, definite: true })), ); }; - const run = (body: IrStmt[]): void => { + const run = (body: IrStmt[]): IrFunction => { const fn: IrFunction = { name: "main", params: [], returnType: F64, locals: [], body, loc }; applyKeyOrderWitnesses(lowerer, [fn]); + return fn; }; const order = (type: IrType & { kind: "record" }): string[] | undefined => shapes.get(type.shapeId)?.declaredOrder; return { lowerer, shapes, notes, record, literal, run, order }; } +/** The order index a literal stores (KEY_ORDER_FIELD), when it stores one. */ +const orderIndexOf = (lit: IrExpr): IrExpr | undefined => + lit.kind === "recordLit" ? lit.fields.find((x) => x.name === KEY_ORDER_FIELD)?.value : undefined; + const observe = (value: IrExpr): IrStmt => ({ kind: "exprStmt", expr: { kind: "jsonStringify", value, type: { kind: "string" }, loc }, @@ -109,7 +129,7 @@ test("literals that agree with the table leave the same array in place", () => { expect(f.notes.changed.size).toBe(0); }); -test("values built in two orders keep today's table", () => { +test("values built in two orders each record their literal's order", () => { const f = fixture(); const ab = f.record( [ @@ -118,8 +138,71 @@ test("values built in two orders keep today's table", () => { ], ["a", "b"], ); - f.run([observe(f.literal(ab, ["b", "a"])), observe(f.literal(ab, ["a", "b"]))]); + const ba = f.literal(ab, ["b", "a"]); + const aOrder = f.literal(ab, ["a", "b"]); + f.run([observe(ba), observe(aOrder)]); expect(f.order(ab)).toEqual(["a", "b"]); + const shape = f.shapes.get(ab.shapeId)!; + expect(shape.orderVariants).toEqual([ + ["a", "b"], + ["b", "a"], + ]); + expect(shape.fields.map((x) => x.name)).toEqual([KEY_ORDER_FIELD, "a", "b"]); + expect(orderIndexOf(ba)).toMatchObject({ kind: "numLit", value: 1 }); + expect(orderIndexOf(aOrder)).toMatchObject({ kind: "numLit", value: 0 }); +}); + +test("a copy of a value with orders stores its source's index", () => { + const f = fixture(); + const abc = f.record( + [ + ["a", F64], + ["b", F64], + ["c", F64], + ], + ["a", "b", "c"], + ); + const ab = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + const read = (field: string): IrExpr => ({ + kind: "recordGet", + obj: ref("x", abc), + shapeId: abc.shapeId, + field, + type: F64, + loc, + }); + const rest = f.notes.copy( + { + kind: "recordLit", + fields: [ + { name: "a", value: read("a") }, + { name: "b", value: read("b") }, + ], + type: ab, + loc, + }, + abc.shapeId, + ); + f.run([ + decl("x", f.literal(abc, ["c", "b", "a"])), + observe(f.literal(abc, ["a", "b", "c"])), + observe(rest), + ]); + expect(f.shapes.get(ab.shapeId)!.orderVariants).toEqual([ + ["a", "b"], + ["b", "a"], + ]); + expect(orderIndexOf(rest)).toMatchObject({ + kind: "recordGet", + obj: { kind: "varRef", localId: "x" }, + field: KEY_ORDER_FIELD, + }); }); test("an unobserved shape keeps today's table", () => { @@ -157,6 +240,36 @@ test("a synthesized construction pins today's table", () => { expect(f.order(ab)).toEqual(["a", "b"]); }); +test("a runtime builder typed by its use site pins today's table", () => { + const f = fixture(); + const ab = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + // net.serverAddress's signature result is VOID: the call takes its record + // type from its use site, and the runtime builds the record in that order. + const built: IrExpr = { kind: "libCall", fn: "net.serverAddress", args: [], type: ab, loc }; + f.run([observe(f.literal(ab, ["b", "a"])), observe(built)]); + expect(f.order(ab)).toEqual(["a", "b"]); +}); + +test("an always-throwing call typed as the record leaves its literals' order", () => { + const f = fixture(); + const ab = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + const thrown: IrExpr = { kind: "libCall", fn: "error.nodeThrow", args: [], type: ab, loc }; + f.run([observe(f.literal(ab, ["b", "a"])), observe(thrown)]); + expect(f.order(ab)).toEqual(["b", "a"]); +}); + test("a checked conversion alone pins today's table", () => { const f = fixture(); const ab = f.record( @@ -307,7 +420,7 @@ test("straight-line writes of late-only keys keep program order", () => { expect(f.order(o)).toEqual(["id", "y", "x"]); }); -test("late-only keys written through different bindings keep today's table", () => { +test("late-only keys written through different bindings move to the end per value", () => { const f = fixture(); const o = f.record( [ @@ -324,9 +437,13 @@ test("late-only keys written through different bindings keep today's table", () observe(ref("o", o)), ]); expect(f.order(o)).toEqual(["id", "x", "y"]); + expect(f.shapes.get(o.shapeId)!.orderVariants).toEqual([ + ["id", "x", "y"], + ["id", "y", "x"], + ]); }); -test("a late-only key written in a loop over an outer binding keeps today's table", () => { +test("a late-only key written in a loop guards the write with its move", () => { const f = fixture(); const o = f.record( [ @@ -336,7 +453,7 @@ test("a late-only key written in a loop over an outer binding keeps today's tabl ], ["id", "x", "y"], ); - f.run([ + const body: IrStmt[] = [ decl("o", f.literal(o, ["id"])), set(ref("o", o), "y"), { @@ -346,11 +463,18 @@ test("a late-only key written in a loop over an outer binding keeps today's tabl loc, }, observe(ref("o", o)), - ]); + ]; + const fn = f.run(body); expect(f.order(o)).toEqual(["id", "x", "y"]); + const loop = fn.body.find((st) => st.kind === "while") as IrStmt & { kind: "while" }; + const guarded = loop.body[0]!; + expect(guarded.kind).toBe("block"); + const [move, write] = (guarded as IrStmt & { kind: "block" }).body; + expect(move).toMatchObject({ kind: "if", cond: { kind: "unary", op: "!" } }); + expect(write).toMatchObject({ kind: "recordSet", field: "x" }); }); -test("re-adding a key a literal creates earlier keeps today's table", () => { +test("re-adding a key a literal creates earlier moves it to the end of that value", () => { const f = fixture(); const o = f.record( [ @@ -369,4 +493,210 @@ test("re-adding a key a literal creates earlier keeps today's table", () => { }; 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"]); + expect(f.shapes.get(o.shapeId)!.orderVariants).toEqual([ + ["a", "b"], + ["b", "a"], + ]); +}); + +test("a moving write evaluates its value before it tests and moves the key", () => { + const f = fixture(); + const o = f.record( + [ + ["id", F64], + ["x", OPT], + ["y", OPT], + ], + ["id", "x", "y"], + ); + // `o.x = ((o.y = 2), 3)`: y is created first. + const value: IrExpr = { + kind: "seqExpr", + stmts: [set(ref("o", o), "y")], + result: num(3), + type: F64, + loc, + }; + const write: IrStmt = { + kind: "recordSet", + obj: ref("o", o), + shapeId: o.shapeId, + field: "x", + value, + loc, + }; + const fn = f.run([ + decl("o", f.literal(o, ["id"])), + observe(f.literal(o, ["x", "id"])), + write, + observe(ref("o", o)), + ]); + const shape = f.shapes.get(o.shapeId)!; + expect(shape.orderVariants).toBeDefined(); + const block = fn.body[2] as IrStmt & { kind: "block" }; + expect(block.kind).toBe("block"); + // The receiver evaluates first, since the value may assign its binding. + const [receiver, first, guard, written] = block.body; + expect(receiver).toMatchObject({ kind: "varDecl", init: { kind: "varRef", localId: "o" } }); + const target = (receiver as IrStmt & { kind: "varDecl" }).localId; + expect(first).toMatchObject({ kind: "varDecl", init: { kind: "seqExpr" } }); + const local = (first as IrStmt & { kind: "varDecl" }).localId; + expect(guard).toMatchObject({ kind: "if", cond: { kind: "unary", op: "!" } }); + expect(written).toMatchObject({ + kind: "recordSet", + obj: { kind: "varRef", localId: target }, + field: "x", + value: { kind: "varRef", localId: local }, + }); + // The write inside the value moves y in its own block. + const init = (first as IrStmt & { kind: "varDecl" }).init as IrExpr & { kind: "seqExpr" }; + expect(init.stmts[0]).toMatchObject({ kind: "block" }); +}); + +test("a key after a spread that the source may lack picks its order from the source's keys", () => { + const f = fixture(); + const s = f.record( + [ + ["a", F64], + ["b", OPT], + ["c", OPT], + ], + ["a", "b", "c"], + ); + const t = f.record( + [ + ["a", F64], + ["b", F64], + ["c", OPT], + ], + ["a", "b", "c"], + ); + const read = (field: string): IrExpr => ({ + kind: "recordGet", + obj: ref("v", s), + shapeId: s.shapeId, + field, + type: OPT, + loc, + }); + // `{ ...x, b: 1 }`, which evaluates `x` into `v` first + const merged = f.notes.literal( + { + kind: "recordLit", + fields: [ + { name: "a", value: read("a") }, + { name: "c", value: read("c") }, + { name: "b", value: num(1) }, + ], + type: t, + loc, + }, + [ + { + kind: "spread", + shapeId: s.shapeId, + maybeAbsent: false, + value: { kind: "varRef", localId: "v", type: s, loc }, + }, + { kind: "key", name: "b", definite: true }, + ], + ); + const prologue = [decl("v", ref("x", s))]; + f.run([ + decl("x", f.literal(s, ["c", "a"])), + observe(f.literal(s, ["a", "c"])), + observe({ kind: "seqExpr", stmts: prologue, result: merged, type: t, loc }), + ]); + // b keeps its place in the source's order when the source has it, and + // follows the source's keys when it does not. + expect(f.shapes.get(t.shapeId)!.orderVariants).toEqual([ + ["a", "c", "b"], + ["c", "a", "b"], + ["a", "b", "c"], + ]); + expect(prologue[1]).toMatchObject({ + kind: "varDecl", + init: { + kind: "ternary", + cond: { kind: "recordHas", obj: { kind: "varRef", localId: "v" }, field: "b" }, + }, + }); + const snapshot = (prologue[1] as IrStmt & { kind: "varDecl" }).localId; + expect(orderIndexOf(merged)).toMatchObject({ kind: "varRef", localId: snapshot }); +}); + +test("a spread reads its source's order right after the literal evaluates the source", () => { + const f = fixture(); + const s = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + const t = f.record( + [ + ["a", F64], + ["b", F64], + ["first", F64], + ["last", F64], + ], + ["first", "a", "b", "last"], + ); + const read = (localId: string, field: string): IrExpr => ({ + kind: "recordGet", + obj: ref(localId, s), + shapeId: s.shapeId, + field, + type: F64, + loc, + }); + // `{ first: y.a, ...x, last: y.b }`: the literal evaluates `first`, then + // the source into `v` and its fields, then `last`, which reads another + // value of the source's shape. + const lit = f.notes.literal( + { + kind: "recordLit", + fields: [ + { name: "first", value: ref("p", F64) }, + { name: "a", value: ref("va", F64) }, + { name: "b", value: ref("vb", F64) }, + { name: "last", value: read("y", "b") }, + ], + type: t, + loc, + }, + [ + { kind: "key", name: "first", definite: true }, + { + kind: "spread", + shapeId: s.shapeId, + maybeAbsent: false, + value: { kind: "varRef", localId: "v", type: s, loc }, + }, + { kind: "key", name: "last", definite: true }, + ], + ); + const prologue: IrStmt[] = [ + decl("p", read("y", "a")), + decl("v", ref("x", s)), + decl("va", read("v", "a")), + decl("vb", read("v", "b")), + ]; + f.run([ + decl("x", f.literal(s, ["b", "a"])), + decl("y", f.literal(s, ["a", "b"])), + observe({ kind: "seqExpr", stmts: prologue, result: lit, type: t, loc }), + ]); + expect(f.shapes.get(t.shapeId)!.orderVariants).toEqual([ + ["first", "a", "b", "last"], + ["first", "b", "a", "last"], + ]); + // The index is the spread binding's, read before `last` evaluates. + expect(prologue[2]).toMatchObject({ + kind: "varDecl", + init: { kind: "recordGet", obj: { kind: "varRef", localId: "v" }, field: KEY_ORDER_FIELD }, + }); + const snapshot = (prologue[2] as IrStmt & { kind: "varDecl" }).localId; + expect(orderIndexOf(lit)).toMatchObject({ kind: "varRef", localId: snapshot }); }); diff --git a/packages/compiler/src/frontend/lowering/key-order-witness.ts b/packages/compiler/src/frontend/lowering/key-order-witness.ts index 2fc9a6cbd..aa84c68c2 100644 --- a/packages/compiler/src/frontend/lowering/key-order-witness.ts +++ b/packages/compiler/src/frontend/lowering/key-order-witness.ts @@ -41,10 +41,56 @@ * 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 { IrExpr, IrFunction, IrRecordShape, IrStmt, IrType, SrcLoc } from "../../ir/ir.js"; +import { + BOOL, + F64, + KEY_ORDER_FIELD, + shapeHasAccessorSlots, + typeEquals, + UNDEFINED_T, +} from "../../ir/ir.js"; +import { + everyExprChild, + everyStmtChild, + everyTypeChild, + transformStmtList, +} from "../../ir/traverse.js"; import type { Lowerer } from "./lowerer.js"; +import { ALWAYS_THROW_LIB_FNS } from "../../ir/builtin-effects.js"; + +/** The statements a key-order helper runs for record `ref`: `walks[0]`, + * its walk over the shape's table, or, for a shape with orderVariants, a + * branch per order (walks[i] for order i) on the value's KEY_ORDER_FIELD. + * `walks` follows shapeKeyOrders. */ +export function perKeyOrder( + shape: IrRecordShape, + ref: IrExpr, + walks: readonly IrStmt[][], + loc: SrcLoc, +): IrStmt[] { + let tail = walks[0]!; + for (let i = walks.length - 1; i >= 1; i--) { + const index: IrExpr = { + kind: "recordGet", + obj: ref, + shapeId: shape.id, + field: KEY_ORDER_FIELD, + type: F64, + loc, + }; + const cond: IrExpr = { + kind: "bin", + op: "===", + left: index, + right: { kind: "numLit", value: i, type: F64, loc }, + type: BOOL, + loc, + }; + tail = [{ kind: "if", cond, then: walks[i]!, else_: tail, loc }]; + } + return tail; +} /** One property of an object literal, in source order. */ export type KeyOrderSegment = @@ -52,8 +98,9 @@ export type KeyOrderSegment = * 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 } + * can be undefined (`...options` of `T | undefined`). `value` is the + * binding the literal evaluated the source into, when it has one. */ + | { kind: "spread"; shapeId: string; maybeAbsent: boolean; value?: IrExpr & { kind: "varRef" } } /** A spread whose keys are only known at run time. */ | { kind: "opaque" }; @@ -226,6 +273,11 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio const opaqueSeen = new Set(); const runtimeShapes = new Set(); const runtimeSeen = new Set(); + /** Shapes the declaration-typed boundary converts into (adopting casts): + * their values come from package code that builds them in the + * declaration's order, the order their table holds. */ + const declaredShapes = new Set(); + const declaredSeen = new Set(); const producedBy = (type: IrType, opaque: boolean): void => { if (opaque) shapesIn(type, opaqueShapes, opaqueSeen); else shapesIn(type, runtimeShapes, runtimeSeen); @@ -312,7 +364,11 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio // 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); + // An always-throwing call stands in for a value (`error.nodeThrow` + // in a narrowing ternary) and never produces one. Other calls + // whose signature result is VOID are typed by their use site and + // build the record at run time (`server.address()`). + if (!ALWAYS_THROW_LIB_FNS.has(e.fn)) producedBy(e.type, true); return; case "intrinsic": // Intrinsics hand back values built elsewhere, except allSettled's @@ -324,10 +380,15 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio case "strIntrinsic": case "regexIntrinsic": case "bytesIntrinsic": + // Generator results ({ done, value }) and Promise.withResolvers + // records are built by the backend in a fixed layout. + case "genResume": + case "promiseWithResolvers": producedBy(e.type, true); return; case "dynCheck": - if (!orderPreservingClone(e)) producedBy(e.type, false); + if (e.adopt === true) shapesIn(e.type, declaredShapes, declaredSeen); + else if (!orderPreservingClone(e)) producedBy(e.type, false); return; case "dynDestrCheck": case "caughtCheck": @@ -387,6 +448,7 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio f.opaque = true; } for (const id of runtimeShapes) factsOf(id).runtime = true; + for (const id of declaredShapes) if (!runtimeShapes.has(id)) factsOf(id).pinned = true; // ── scope ─────────────────────────────────────────────────────────── // Only plain declared-field shapes with a table take part. @@ -708,4 +770,541 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio shape.declaredOrder = [...shape.declaredOrder!.filter(isArrayIndex), ...resolved.order]; notes.changed.add(id); } + + // ── per-instance orders ───────────────────────────────────────────── + // A shape whose literals write its keys in more than one order has no + // one table. Each value then records which of the orders it was written + // in (KEY_ORDER_FIELD), when every construction's order is known + // statically or copied from a value that records its own: + // - a literal of explicit keys writes its order; + // - a copy from a shape with one resolved order writes that order; + // - a copy from a value of a shape with orders writes the source's + // order, filtered to the copy's keys (rest bindings, width copies); + // - clones copy the field, and every other construction (synthesized + // literals, checked conversions, runtime producers) leaves 0, today's + // table, as before. + // A write of an optional key a value lacks moves the key to the end of + // the value's order, as in Node: the orders include every order such + // writes reach, and each write site updates the index after it evaluates + // the value and before it writes the key. + const MAX_ORDERS = 16; + const unresolved = (id: string): boolean => result.get(id) === "unknown"; + /** `order` over every key of `table`: keys the order lacks follow it. */ + const complete = (order: readonly string[], table: readonly string[]): string[] => { + const keys = new Set(table); + const out = order.filter((k) => keys.has(k)); + const seen = new Set(out); + return [...out, ...table.filter((k) => !seen.has(k))]; + }; + const literalOrder = (segments: readonly KeyOrderSegment[]): string[] | null => { + const out: string[] = []; + for (const segment of segments) { + if (segment.kind !== "key" || !segment.definite) return null; + if (!isArrayIndex(segment.name)) out.push(segment.name); + } + return out; + }; + /** `{ ...value }` of a value of the literal's own shape: a copy of the + * value's order. */ + const selfSpread = (segments: readonly KeyOrderSegment[], id: string): boolean => + segments.length === 1 && + segments[0]!.kind === "spread" && + segments[0]!.shapeId === id && + !segments[0]!.maybeAbsent; + const sameOrder = (a: readonly string[], b: readonly string[]): boolean => + a.join("\0") === b.join("\0"); + const indexOfOrder = (orders: readonly string[][], order: readonly string[]): number => + orders.findIndex((o) => sameOrder(o, order)); + /** The keys writes can add to a value of the shape, or null when a + * clone override can (the clone's order is its source's). */ + const movingKeys = (id: string, f: ShapeFacts, table: readonly string[]): string[] | null => { + const keys = new Set(table); + const moving = new Set(); + for (const w of f.writes) { + if (w.kind === "delete" || !keys.has(w.field) || !maybeAbsent(id, w.field)) continue; + if (w.kind === "clone") return null; + moving.add(w.field); + } + return [...moving]; + }; + /** `values[i]` for the order index `current` holds, as a chain of + * tests. */ + const indexChain = (current: IrExpr, values: readonly number[], loc: SrcLoc): IrExpr => { + let out: IrExpr = { kind: "numLit", value: values[values.length - 1]!, type: F64, loc }; + for (let i = values.length - 2; i >= 0; i--) { + if (out.kind === "numLit" && out.value === values[i]) continue; + out = { + kind: "ternary", + cond: { + kind: "bin", + op: "===", + left: current, + right: { kind: "numLit", value: i, type: F64, loc }, + type: BOOL, + loc, + }, + then: { kind: "numLit", value: values[i]!, type: F64, loc }, + else_: out, + type: F64, + loc, + }; + } + return out; + }; + const moves = new Map>(); + /** Adds to `orders` every order writes of `moving` keys reach from them + * and records each key's transitions; false past MAX_ORDERS. */ + const closeUnderMoves = (id: string, orders: string[][], moving: readonly string[]): boolean => { + const transitions = new Map(moving.map((k) => [k, []])); + for (let i = 0; i < orders.length; i++) { + for (const key of moving) { + const moved = [...orders[i]!.filter((k) => k !== key), key]; + let index = indexOfOrder(orders, moved); + if (index < 0) { + if (orders.length === MAX_ORDERS) return false; + index = orders.push(moved) - 1; + } + transitions.get(key)![i] = index; + } + } + if (moving.length > 0) moves.set(id, transitions); + return true; + }; + // Observed shapes, and the shapes their values copy their orders from. + const wanted = new Set(observed); + const pending = [...observed]; + while (pending.length > 0) { + const f = facts.get(pending.pop()!); + if (f === undefined) continue; + const sources = [ + ...f.copies.map((c) => c.source), + ...f.literals.flatMap((segments) => + segments.flatMap((s) => (s.kind === "spread" ? [s.shapeId] : [])), + ), + ]; + for (const source of sources) { + if (wanted.has(source)) continue; + wanted.add(source); + pending.push(source); + } + } + const orderable = ( + id: string, + f: ShapeFacts, + ): { shape: IrRecordShape; moving: string[] } | null => { + const shape = shapes.get(id); + if (!wanted.has(id) || !unresolved(id) || !inScope(shape)) return null; + const moving = movingKeys(id, f, tableOf(shape)); + return moving === null ? null : { shape, moving }; + }; + const variants = new Map(); + for (const [id, f] of facts) { + const candidate = orderable(id, f); + // A runtime producer builds the shape's values in its fixed layout, + // which has no KEY_ORDER_FIELD. + if (candidate === null || f.literals.length === 0 || f.opaque) continue; + const table = tableOf(candidate.shape); + const orders: string[][] = [table]; + let ok = true; + const add = (order: readonly string[]): void => { + const full = complete(order, table); + if (indexOfOrder(orders, full) < 0) orders.push(full); + }; + for (const segments of f.literals) { + if (selfSpread(segments, id)) continue; + const order = literalOrder(segments); + if (order === null) ok = false; + else add(order); + } + for (const c of f.copies) { + if (c.source === id) continue; + const source = result.get(c.source); + if (source === undefined || source === "unknown") ok = false; + else add(source.order.filter((k) => c.keys.has(k))); + } + if (!ok || orders.length > MAX_ORDERS || !closeUnderMoves(id, orders, candidate.moving)) + continue; + if (orders.length > 1) variants.set(id, orders); + else moves.delete(id); + } + // Shapes built only from values of one shape with orders, all in one + // way: copies (rest bindings, width copies), or literals spreading the + // value between the same explicit keys. Order i is the source's order i + // over the shape's keys, after the keys before the spread and before + // the keys after it. A key after the spread that the source value may + // lack goes where the source has it, or after the source's keys when it + // has not: such a literal's orders are one per source order and set of + // those keys the source has, and `select.index[present][i]` is the order + // for source order i with the keys of bit set `present`. + type Template = { + source: string; + prefix: string[]; + suffix: string[]; + select?: { keys: string[]; index: number[][] }; + }; + const templateOf = (segments: readonly KeyOrderSegment[], id: string): Template | null => { + const prefix: string[] = []; + const suffix: string[] = []; + let source: string | null = null; + for (const segment of segments) { + if (segment.kind === "opaque") return null; + if (segment.kind === "spread") { + if (source !== null || segment.maybeAbsent || segment.shapeId === id) return null; + source = segment.shapeId; + continue; + } + if (!segment.definite) return null; + (source === null ? prefix : suffix).push(segment.name); + } + return source === null ? null : { source, prefix, suffix }; + }; + const templateKey = (t: Template): string => JSON.stringify([t.source, t.prefix, t.suffix]); + const copyTemplate = (source: string): Template => ({ source, prefix: [], suffix: [] }); + const mirrors = new Map(); + for (const [id, f] of facts) { + if (variants.has(id) || f.pinned || f.runtime || f.opaque) continue; + const candidate = orderable(id, f); + if (candidate === null || f.literals.length + f.copies.length === 0) continue; + const templates: (Template | null)[] = f.literals.map((segments) => templateOf(segments, id)); + for (const c of f.copies) templates.push(copyTemplate(c.source)); + const first = templates[0]; + if ( + first === null || + first === undefined || + templates.some((t) => t === null || templateKey(t) !== templateKey(first)) + ) + continue; + const orders = variants.get(first.source); + if (orders === undefined || mirrors.has(first.source)) continue; + const table = tableOf(candidate.shape); + const tested = first.suffix.filter((k) => maybeAbsent(first.source, k)); + if (tested.length > 4) continue; + const mirrorOf = (o: readonly string[], lacks: ReadonlySet): string[] => { + const order = [...first.prefix]; + for (const k of o) if (!lacks.has(k) && !order.includes(k)) order.push(k); + for (const k of first.suffix) if (!order.includes(k)) order.push(k); + return complete(order, table); + }; + let mirrored: string[][]; + let template: Template = first; + if (tested.length === 0) mirrored = orders.map((o) => mirrorOf(o, new Set())); + else { + mirrored = []; + const index: number[][] = []; + for (let present = 0; present < 1 << tested.length; present++) { + const lacks = new Set(tested.filter((_, j) => (present & (1 << j)) === 0)); + const row: number[] = []; + for (const o of orders) { + const order = mirrorOf(o, lacks); + let i = indexOfOrder(mirrored, order); + if (i < 0) i = mirrored.push(order) - 1; + row.push(i); + } + index.push(row); + } + if (mirrored.length > MAX_ORDERS) continue; + template = { + source: first.source, + prefix: first.prefix, + suffix: first.suffix, + select: { keys: tested, index }, + }; + } + if (!closeUnderMoves(id, mirrored, candidate.moving)) continue; + if (mirrored.every((o) => sameOrder(o, mirrored[0]!))) { + // Every order of the source agrees over these keys: one table. + moves.delete(id); + const shape = candidate.shape; + const only: string[] = mirrored[0] ?? table; + if (!sameOrder(only, table)) + shape.declaredOrder = [...shape.declaredOrder!.filter(isArrayIndex), ...only]; + continue; + } + variants.set(id, mirrored); + mirrors.set(id, template); + } + if (variants.size === 0) return; + + // Every construction of those shapes, to give each its order index. A + // copy reads its source's index, so its source must be a plain binding. + const sites = new Map(); + /** The statements a spread literal's sequence evaluates first: the + * spread value's binding, then its fields. */ + const prologues = new Map(); + const siteOf = (e: IrExpr): boolean => { + if (e.kind === "seqExpr" && e.result.kind === "recordLit") prologues.set(e.result, e.stmts); + if (e.kind === "recordLit" && e.type.kind === "record" && variants.has(e.type.shapeId)) { + const list = sites.get(e.type.shapeId) ?? []; + list.push(e); + sites.set(e.type.shapeId, list); + } + return everyExprChild(e, siteOf, siteStmt); + }; + const siteStmt = (st: IrStmt): boolean => everyStmtChild(st, siteOf, siteStmt); + /** The function each literal with a prologue is built in. */ + const owners = new Map(); + for (const fn of fns) { + const before = new Set(prologues.keys()); + for (const st of fn.body) siteStmt(st); + for (const lit of prologues.keys()) if (!before.has(lit)) owners.set(lit, fn); + } + /** Where a literal evaluated its spread of `source`: the declaration the + * spread segment names, in the literal's own prologue. */ + const spreadBinding = ( + lit: IrExpr & { kind: "recordLit" }, + source: string, + ): { stmts: IrStmt[]; at: number; value: IrExpr } | null => { + const stmts = prologues.get(lit) as IrStmt[] | undefined; + if (stmts === undefined) return null; + for (const segment of notes.literalOf(lit) ?? []) { + if (segment.kind !== "spread" || segment.shapeId !== source || !segment.value) continue; + const localId = segment.value.localId; + const at = stmts.findIndex((st) => st.kind === "varDecl" && st.localId === localId); + const decl = stmts[at]; + if (decl?.kind !== "varDecl" || decl.init === null) return null; + return { stmts, at, value: { kind: "varRef", localId, type: decl.init.type, loc: lit.loc } }; + } + return null; + }; + /** The binding a copy or spread reads its fields from. A literal's spread + * names it; every field of a copy reads it. */ + const copySource = (lit: IrExpr & { kind: "recordLit" }, source: string): IrExpr | null => { + if (notes.literalOf(lit) !== undefined) return spreadBinding(lit, source)?.value ?? null; + for (const field of lit.fields) { + const value = field.value; + if (value.kind === "recordGet" && value.shapeId === source && value.obj.kind === "varRef") + return value.obj; + } + for (const st of prologues.get(lit) ?? []) { + if (st.kind !== "varDecl" || st.init === null) continue; + const t = st.init.type; + if (t.kind === "record" && t.shapeId === source) + return { kind: "varRef", localId: st.localId, type: t, loc: lit.loc }; + } + return null; + }; + const orderIndex = (id: string, lit: IrExpr & { kind: "recordLit" }): IrExpr | number | null => { + const orders = variants.get(id)!; + const segments = notes.literalOf(lit); + if (segments !== undefined && selfSpread(segments, id)) { + const obj = copySource(lit, id); + if (obj === null) return null; + return { + kind: "recordGet", + obj, + shapeId: id, + field: KEY_ORDER_FIELD, + type: F64, + loc: lit.loc, + }; + } + const mirror = mirrors.get(id); + if (segments !== undefined && mirror !== undefined) { + const obj = copySource(lit, mirror.source); + if (obj === null) return null; + const sourceIndex: IrExpr = { + kind: "recordGet", + obj, + shapeId: mirror.source, + field: KEY_ORDER_FIELD, + type: F64, + loc: lit.loc, + }; + const select = mirror.select; + if (select === undefined) return sourceIndex; + // Tests the source for each key in turn, then picks by its index. + const pick = (j: number, present: number): IrExpr => { + if (j === select.keys.length) + return indexChain(sourceIndex, select.index[present]!, lit.loc); + return { + kind: "ternary", + cond: lowerer.recordFieldPresent(obj, mirror.source, select.keys[j]!, lit.loc), + then: pick(j + 1, present | (1 << j)), + else_: pick(j + 1, present), + type: F64, + loc: lit.loc, + }; + }; + return pick(0, 0); + } + if (segments !== undefined) { + const order = literalOrder(segments); + return order === null ? null : indexOfOrder(orders, complete(order, orders[0]!)); + } + const source = notes.copyOf(lit); + if (source === undefined) return 0; + if (variants.has(source) && (source === id || mirrors.get(id)?.source === source)) { + const obj = copySource(lit, source); + if (obj === null) return null; + return { + kind: "recordGet", + obj, + shapeId: source, + field: KEY_ORDER_FIELD, + type: F64, + loc: lit.loc, + }; + } + const resolved = result.get(source); + if (resolved === undefined || resolved === "unknown") return null; + const keys = new Set( + lit.fields.filter((x) => x.value.kind !== "fieldAbsent").map((x) => x.name), + ); + return indexOfOrder( + orders, + complete( + resolved.order.filter((k) => keys.has(k)), + orders[0]!, + ), + ); + }; + const plan = new Map(); + const dropped = new Set(); + for (const [id, list] of sites) { + for (const lit of list) { + const index = orderIndex(id, lit); + if (index === null || (typeof index === "number" && index < 0)) dropped.add(id); + else plan.set(lit, index); + } + } + // Write sites of keys that move: each reads and updates the value's + // index before the write. + const writeSites: { st: IrStmt & { kind: "recordSet" }; fn: IrFunction }[] = []; + let writeFn: IrFunction | null = null; + const writeExpr = (e: IrExpr): boolean => everyExprChild(e, writeExpr, writeOf); + const writeOf = (st: IrStmt): boolean => { + if ( + st.kind === "recordSet" && + st.value.kind !== "fieldAbsent" && + moves.get(st.shapeId)?.has(st.field) + ) + writeSites.push({ st, fn: writeFn! }); + return everyStmtChild(st, writeExpr, writeOf); + }; + for (const fn of fns) { + writeFn = fn; + for (const st of fn.body) writeOf(st); + } + // A shape whose copies cannot read their source keeps today's table, and + // so do the shapes that mirror it. + for (const [id, mirror] of mirrors) if (dropped.has(mirror.source)) dropped.add(id); + let temporaries = 0; + for (const [lit, index] of plan) { + const id = (lit.type as IrType & { kind: "record" }).shapeId; + if (dropped.has(id)) continue; + let value: IrExpr = + typeof index === "number" ? { kind: "numLit", value: index, type: F64, loc: lit.loc } : index; + // A spread's order is the source's order at the spread: a later + // property may move the source's keys. Read it right after the literal + // evaluates the source. + const source = mirrors.get(id)?.source; + const binding = typeof index === "number" || !source ? null : spreadBinding(lit, source); + const fn = owners.get(lit); + if (binding && fn) { + const localId = `%keyOrderSource.${temporaries++}`; + fn.locals.push({ id: localId, name: "%keyOrderSource", type: F64, mutable: false }); + binding.stmts.splice(binding.at + 1, 0, { + kind: "varDecl", + localId, + init: value, + loc: lit.loc, + }); + value = { kind: "varRef", localId, type: F64, loc: lit.loc }; + } + lit.fields.push({ name: KEY_ORDER_FIELD, value }); + } + const replacements = new Map>(); + for (const { st, fn } of writeSites) { + if (dropped.has(st.shapeId)) continue; + const transitions = moves.get(st.shapeId)!.get(st.field)!; + const loc = st.loc; + // A receiver other than a binding evaluates once, into a local. So does + // a binding when the value evaluates first (below): the value may + // assign the binding, and the write still goes to the object the + // binding held before. + const pure = + st.value.kind === "numLit" || + st.value.kind === "strLit" || + st.value.kind === "boolLit" || + st.value.kind === "varRef"; + let plain = st.obj; + while (plain.kind === "unionNarrow" || plain.kind === "downcast") plain = plain.value; + const prologue: IrStmt[] = []; + let obj = st.obj; + if (plain.kind !== "varRef" || !pure) { + const localId = `%keyOrderReceiver.${temporaries++}`; + fn.locals.push({ id: localId, name: "%keyOrderReceiver", type: st.obj.type, mutable: false }); + prologue.push({ kind: "varDecl", localId, init: st.obj, loc }); + obj = { kind: "varRef", localId, type: st.obj.type, loc }; + } + const current: IrExpr = { + kind: "recordGet", + obj, + shapeId: st.shapeId, + field: KEY_ORDER_FIELD, + type: F64, + loc, + }; + const next = indexChain(current, transitions, loc); + // The value evaluates before the key is tested and moved: it may add + // other keys to the same value, which then come first. + let value = st.value; + if (!pure) { + const localId = `%keyOrderValue.${temporaries++}`; + fn.locals.push({ id: localId, name: "%keyOrderValue", type: value.type, mutable: false }); + prologue.push({ kind: "varDecl", localId, init: value, loc }); + value = { kind: "varRef", localId, type: value.type, loc }; + } + const absent: IrExpr = { + kind: "unary", + op: "!", + operand: lowerer.recordFieldPresent(obj, st.shapeId, st.field, loc), + type: BOOL, + loc, + }; + const update: IrStmt = { + kind: "recordSet", + obj, + shapeId: st.shapeId, + field: KEY_ORDER_FIELD, + value: next, + loc, + }; + // The write becomes a block that moves the key first. + const write: IrStmt = { + kind: "recordSet", + obj, + shapeId: st.shapeId, + field: st.field, + value, + loc, + }; + const guard: IrStmt = { kind: "if", cond: absent, then: [update], else_: null, loc }; + const block: IrStmt = { kind: "block", body: [...prologue, guard, write], loc }; + let replaced = replacements.get(fn); + if (replaced === undefined) { + replaced = new Map(); + replacements.set(fn, replaced); + } + replaced.set(st, block); + } + for (const [fn, replaced] of replacements) { + fn.body = transformStmtList(fn.body, { + expr: (e) => e, + stmt: (st) => replaced.get(st) ?? st, + }); + } + for (const [id, orders] of variants) { + if (dropped.has(id)) continue; + const shape = shapes.get(id)!; + const indexKeys: string[] = (shape.declaredOrder ?? []).filter(isArrayIndex); + shapes.addKeyOrderField(id); + const tables: string[][] = []; + for (const order of orders) { + const table: string[] = [...indexKeys]; + for (const key of order) table.push(key); + tables.push(table); + } + shape.orderVariants = tables; + } } diff --git a/packages/compiler/src/frontend/lowering/lower-async-iteration.ts b/packages/compiler/src/frontend/lowering/lower-async-iteration.ts index 5b427a750..95916ee9a 100644 --- a/packages/compiler/src/frontend/lowering/lower-async-iteration.ts +++ b/packages/compiler/src/frontend/lowering/lower-async-iteration.ts @@ -359,13 +359,15 @@ function boolRef(localId: string, loc: ReturnType): IrExpr { /** The loop's stream: a JavaScript value typed as a stream can arrive as a * checked-dynamic value (returned through an untyped function), which - * checks back into the stream class. */ + * checks back into the stream class, or as a union with it (an element + * read), which narrows to it. */ function streamSource( lowerer: Lowerer, plan: NodeReadablePlan, loc: ReturnType, ): IrExpr { const v = lowerer.lowerExpr(plan.source); + if (v.type.kind === "union") return lowerer.coerceToExpected(v, plan.type); if (v.type.kind !== "dyn") return v; return { kind: "dynCheck", value: v, type: plan.type, loc }; } diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 39b7207c0..82f5a27c1 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -1,4 +1,5 @@ import { keyedHelperName } from "./stable-names.js"; +import { perKeyOrder } from "./key-order-witness.js"; import { isForwardedTupleSpread, lowerForwardingApply } from "./typed-arguments.js"; import { guardNullProto, nullProtoRecordShape } from "./null-prototype.js"; import { lowerDynStreamMemberCall } from "./dyn-streams.js"; @@ -45,6 +46,7 @@ import { errorToStringCall } from "./error-methods.js"; import { lowerClassPrototypeAssign, lowerClassPrototypeDescriptors } from "./class-prototypes.js"; import { BYTES_ELEMENT_NAME, + BYTES_U8, BIGINT_T, BOOL, CAUGHT, @@ -53,6 +55,7 @@ import { F64, type IrExpr, type IrFunction, + type IrLibFn, type IrLocal, type IrParam, type IrStmt, @@ -71,11 +74,13 @@ import { isDynTypedRefType, isUnitType, shapeHasAccessorSlots, + shapeKeyOrders, typeEquals, RUNTIME_EMITTER_CLASS, } from "../../ir/ir.js"; import { lowerProtocolMethodCall } from "./class-protocol-dispatch.js"; import { isJsSourceFile, locOf, requireSpecOf } from "../program.js"; +import { isPackageShimFile, isShimIntrinsicName } from "../package-shims.js"; import { lowerJsonRequireCall } from "./json-require.js"; import { lowerAssertInstanceCall } from "./assert-instances.js"; import { @@ -264,8 +269,46 @@ import { codeGenerationCalleeOf, lowerCodeGenerationCall } from "./code-generati const ZLIB_STREAM_METHODS: ReadonlySet = new Set(["flush", "params", "close", "reset"]); +/** `Object.defineProperty(target, ...)`, `Object.defineProperties`, + * `Reflect.defineProperty`, and `target.__defineGetter__/__defineSetter__` + * over a target that may hold an Error can install an accessor over an + * error's `name` or `message` (error-methods.ts). */ +function noteErrorAccessorReflection(lowerer: Lowerer, expr: ts.CallExpression): void { + const callee = expr.expression; + if (!ts.isPropertyAccessExpression(callee)) return; + const member = callee.name.text; + const target = + member === "defineProperty" || member === "defineProperties" + ? expr.arguments.nodes[0] + : member === "__defineGetter__" || member === "__defineSetter__" + ? callee.expression + : undefined; + if (!target) return; + const error = lowerer.checker.resolveName("Error", ts.SymbolFlags.Type, target, false); + const errorType = error ? lowerer.checker.getDeclaredTypeOfSymbol(error) : null; + if (mayHoldError(lowerer, lowerer.checker.getTypeAtLocation(target), errorType)) + lowerer.errorAccessorReflection = locOf(expr); +} + +/** An untyped or generic type, a type Error is assignable to (`object`, + * `{}`, `Object`, `{ message?: string }`), or + * an Error-shaped type with both `name` and `message`, alone or in a + * union or intersection. */ +function mayHoldError(lowerer: Lowerer, type: ts.Type, errorType: ts.Type | null): boolean { + if (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.Instantiable)) + return true; + if (type.flags & (ts.TypeFlags.Union | ts.TypeFlags.Intersection)) + return ts.constituentTypes(type).some((part) => mayHoldError(lowerer, part, errorType)); + return ( + (lowerer.checker.getPropertyOfType(type, "message") !== undefined && + lowerer.checker.getPropertyOfType(type, "name") !== undefined) || + (errorType !== null && lowerer.checker.isTypeAssignableTo(errorType, type)) + ); +} + export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const loc = locOf(expr); + if (lowerer.errorAccessorReflection === null) noteErrorAccessorReflection(lowerer, expr); // Object's checker return type is `any`. Claim known object inputs // before generic checked-dynamic dispatch tries to load the constructor. if ( @@ -292,6 +335,12 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const codeGeneration = codeGenerationCalleeOf(lowerer, expr.expression); if (codeGeneration !== null) return lowerCodeGenerationCall(lowerer, expr, codeGeneration); } + // A package shim's runtime intrinsic (a bodiless `declare function` + // named scriptcUnixDgram* in a shipped shim): a direct runtime call. + if (ts.isIdentifier(expr.expression) && isShimIntrinsicName(expr.expression.text)) { + const intrinsic = lowerShimIntrinsicCall(lowerer, expr, expr.expression, loc); + if (intrinsic) return intrinsic; + } // Inline require() of a program module (cjs-inline-require.ts): a // member call over its export table resolves like a binding's member // call after the module's %init at this position; the require itself @@ -1619,7 +1668,16 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { } const accessorTarget = lowerer.fieldTarget(expr.expression); if (accessorTarget?.container === "accessor") { - const callee = lowerer.fieldGetExpr(accessorTarget, locOf(expr.expression), expr.expression); + let callee = lowerer.fieldGetExpr(accessorTarget, locOf(expr.expression), expr.expression); + // A getter returning a callable class instance (chalk's + // `chalk.red("x")`): call through its `%call` field. + if (callee.type.kind === "object") { + const direct = callableInstanceStringCall(lowerer, expr, callee, locOf(expr)); + if (direct) return direct; + callee = lowerer.hybridCallUnwrap(callee); + if (callee.type.kind === "func") + return lowerCallThroughValue(lowerer, expr, callee, locOf(expr)); + } if (callee.type.kind === "func") { const args = completeFuncValueArgs(lowerer, expr, callee.type, locOf(expr)); return { kind: "callValue", callee, args, type: callee.type.ret, loc: locOf(expr) }; @@ -1871,7 +1929,55 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // a non-func IR type was already rejected while lowering the callee. // HYBRID (function-with-properties) values call through their %call slot. let callee = lowerer.lowerExpr(expr.expression); - if (callee.type.kind === "record") callee = lowerer.hybridCallUnwrap(callee); + { + const direct = callableInstanceStringCall(lowerer, expr, callee, loc); + if (direct) return direct; + } + if (callee.type.kind === "record" || callee.type.kind === "object") + callee = lowerer.hybridCallUnwrap(callee); + return lowerCallThroughValue(lowerer, expr, callee, loc); +} + +/** A callable class instance called with one string: its `%call1` method + * when the class has one (the chalk shim's typed fast path), skipping the + * variadic closure's checked-dynamic arguments. */ +function callableInstanceStringCall( + lowerer: Lowerer, + expr: ts.CallExpression, + callee: IrExpr, + loc: SrcLoc, +): IrExpr | null { + if (callee.type.kind !== "object" || expr.arguments.nodes.length !== 1) return null; + const arg = expr.arguments.nodes[0]!; + if (ts.isSpreadElement(arg)) return null; + const info = lowerer.classes.get(callee.type.className); + if (info === undefined || !info.fields.has("%call") || !lowerer.findMethodOn(info, "%call1")) + return null; + const value = lowerer.lowerExpr(arg); + if (value.type.kind === "string") + return lowerer.accessorCall(callee.type.className, "%call1", callee, [value], STRING, loc); + // Any other argument (a number behind a cast): the variadic closure, + // through the checked-dynamic call as lowerCallThroughValue would. + const closure = lowerer.hybridCallUnwrap(callee); + if (closure.type.kind !== "func") return null; + return { + kind: "dynCall", + callee: { kind: "dynFrom", value: closure, type: DYN, loc }, + calleeName: ts.isIdentifier(expr.expression) ? expr.expression.text : "value", + args: [lowerer.coerceToExpected(value, DYN)], + type: DYN, + loc, + }; +} + +/** A call through an evaluated callee value: checked-dynamic callees, + * function unions, typed-rest and runtime-arity closures. */ +function lowerCallThroughValue( + lowerer: Lowerer, + expr: ts.CallExpression, + callee: IrExpr, + loc: SrcLoc, +): IrExpr { // A CHECKED-DYNAMIC callee — `fn(a, b)` where fn is an implicit-any // JS binding (the mustCall body's `fn(...args)`), a dyn capture, or a // keyed read off a dyn value: the dynCall boundary. Arguments convert @@ -2334,6 +2440,20 @@ function lowerBigIntConstructorValue(lowerer: Lowerer, arg: IrExpr, loc: SrcLoc) return { kind: "libCall", fn: "bigint.parse", args: [arg], type: BIGINT_T, loc }; if (arg.type.kind === "f64") return { kind: "libCall", fn: "bigint.fromF64", args: [arg], type: BIGINT_T, loc }; + // Byte storage converts through ToPrimitive, which BigInt then parses: a + // typed array's text is its elements joined with "," (one element reads + // as that element, none as 0n, several throw the SyntaxError for the + // joined text), a Buffer's is its UTF-8 decoding and a DataView's is + // "[object DataView]". The runtime value tells them apart, as for String. + if (arg.type.kind === "bytes") { + const text: IrExpr = { + kind: "toString", + operand: { kind: "dynFrom", value: arg, type: DYN, loc }, + type: STRING, + loc, + }; + return { kind: "libCall", fn: "bigint.parse", args: [text], type: BIGINT_T, loc }; + } if (arg.type.kind === "bool") { return { kind: "ternary", @@ -2374,6 +2494,7 @@ function lowerBigIntConstructorValue(lowerer: Lowerer, arg: IrExpr, loc: SrcLoc) arm.kind === "string" || arm.kind === "f64" || arm.kind === "bool" || + arm.kind === "bytes" || isUnitType(arm), ) ) @@ -3731,18 +3852,22 @@ function lowerDynReceiverMethodCall( } if (access.name.text === "flatMap") return lowerDynArrayFlatMapCall(lowerer, call, access, recv); // String methods claim only names NO other dyn-representable kind's - // prototype declares (Array carries includes/indexOf/slice too): for - // these, "the receiver is a string, or the call throws V8's TypeError" - // IS Node's semantics for every possible dyn value. Shared names would - // need a receiver-kind dispatch — they keep the fence. + // prototype declares (Array carries includes/indexOf/slice too), so no + // other kind inherits them. Shared names would need a receiver-kind + // dispatch — they keep the fence. if (DYN_STRING_ONLY_METHODS.has(access.name.text) && !ownCallable) { - // An object-typed receiver the member is missing from (a JavaScript - // module object whose members are assigned in a function body, - // `common.repeat(" ", n)`) holds a string or an object with that - // member: dispatch on the receiver's runtime kind. + // A checked-dynamic object can still carry its OWN member of that + // name: zod's schemas assign `trim`, `toLowerCase` and `startsWith` + // in their constructors (`z.string().trim().min(1)`), an object's + // prototype can define `trim`, and a JavaScript module object can + // assign `repeat` in a function body (`common.repeat(" ", n)`, an + // object-typed receiver the member is missing from). An untyped + // receiver, or a member the checker cannot type, dispatches on the + // receiver's runtime kind: a string runs the string lowering, anything + // else calls its member as Node does. if ( - !checkerUntyped && - (lowerer.typeOf(access).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0 && + (checkerUntyped || + (lowerer.typeOf(access).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) && !call.questionDotToken && !access.questionDotToken && !call.arguments.nodes.some(ts.isSpreadElement) @@ -3776,7 +3901,14 @@ function lowerDynReceiverMethodCall( } // The argument is a radix for bigint and an encoding for Buffer. // Preserve its runtime value so receiver dispatch can choose correctly. - if (access.name.text === "toString" && call.arguments.nodes.length <= 1) { + // An authored own `toString` member (zod's `errorUtil.toString`, which + // answers string | undefined) is an ordinary method call instead. + if ( + access.name.text === "toString" && + call.arguments.nodes.length <= 1 && + !ownCallable && + !(checkerUntyped && jsAssignsOwnToString(lowerer, access.expression)) + ) { const argument = call.arguments.nodes[0]; return { kind: "libCall", @@ -3886,12 +4018,22 @@ function lowerDynReceiverMethodCall( // The same holds when only the MEMBER is checker-untyped: TypeScript // importing chalk's JavaScript source types the default export as a // plain function, so `chalk.bold` reads as `any` on a typed receiver. + const ownToString = + access.name.text === "toString" && + checkerUntyped && + jsAssignsOwnToString(lowerer, access.expression); const stringOnlyName = (checkerUntyped || (lowerer.typeOf(access).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) && !ownCallable && DYN_STRING_PROTO_ONLY_NAMES.has(access.name.text); - if (DYN_PROTO_METHOD_NAMES.has(access.name.text) && !ownCallable && !stringOnlyName) return null; + if ( + DYN_PROTO_METHOD_NAMES.has(access.name.text) && + !ownCallable && + !ownToString && + !stringOnlyName + ) + return null; // Optional forms (`obj.cb?.()`, `obj?.cb()`) belong to the chain // machinery's short-circuit semantics — not modeled here yet. if (call.questionDotToken || access.questionDotToken) return null; @@ -4177,6 +4319,7 @@ const DYN_DISPATCH_METHODS = new Set([ "shift", "unshift", "slice", + "splice", "at", "indexOf", "lastIndexOf", @@ -4318,6 +4461,9 @@ const DYN_DISPATCH_METHODS = new Set([ "additionalHeaders", "altsvc", "origin", + // FinalizationRegistry's methods (scr_finalization_registry_new). + "register", + "unregister", // Worker and MessagePort methods use the owning context's handle adapter. "postMessage", "terminate", @@ -4338,6 +4484,21 @@ export function lowerDynDispatchMethodCall( lowerer.chainBlocked(call, access) ) return null; + // readable.compose lowers its stages from their syntax, so it needs the + // stream's class: a stream a JavaScript function returns untyped has none. + if (method === "compose") { + const recvT = lowerer.mapTypeOf(lowerer.typeOf(access.expression)); + if ( + recvT?.kind === "object" && + streamSidesOf(lowerer, lowerer.classes.get(recvT.className)) !== null + ) { + lowerer.noLowering( + "readable.compose on a stream held as a checked-dynamic value", + call, + "type the stream where it comes from, such as a JSDoc @returns {Readable} on the function that returns it", + ); + } + } if (call.arguments.nodes.some((arg) => ts.isSpreadElement(arg))) { const loc = locOf(call); const local = lowerer.declareHiddenLocal("%spreadReceiver", DYN); @@ -6175,7 +6336,21 @@ export function lowerPromiseMethodCall( `then handlers whose parameter is not the settled value's type (expected '${lowerer.fmt(inner)}', got '${lowerer.fmt(param)}')`, ); } - const resultT = lowerer.mapTypeOf(lowerer.typeOf(call)); + let resultT = lowerer.mapTypeOf(lowerer.typeOf(call)); + // JavaScript whose declared result type has no mapping (zod's + // `Promise.all(items).then((r) => ParseStatus.mergeArray(status, r))`, + // typed by a .d.ts union): the handler's value settles the promise as + // a checked-dynamic value, checked where it is used. + if ( + resultT?.kind !== "promise" && + isJsSourceFile(ts.getSourceFileOfNode(call)) && + cb.type.kind === "func" && + cb.type.ret.kind !== "promise" && + cb.type.ret.kind !== "void" && + (cb.type.ret.kind === "dyn" || lowerer.dynConvertible(cb.type.ret)) + ) { + resultT = { kind: "promise", inner: DYN }; + } if (resultT?.kind !== "promise") { lowerer.noLowering( "then with this handler's result type", @@ -6668,6 +6843,15 @@ export function lowerFilterNarrowCall( } const narrowed = outElem !== null && !typeEquals(outElem, elem); if (!narrowed && !isBooleanArg) return null; + // A written predicate (`(p): p is Cat => ...`, a guard function value) + // proves nothing at run time: the generic array HOF path filters the + // receiver's elements and converts the kept ones checked + // (filterResultElem). + if ( + !isBooleanArg && + ((!ts.isArrowFunction(argNode) && !ts.isFunctionExpression(argNode)) || argNode.type) + ) + return null; const annotateEscape = "keep the receiver's element type instead — annotate the callback's return ': boolean' " + @@ -6679,6 +6863,9 @@ export function lowerFilterNarrowCall( lowerer.badType(call, callT); // defensive: a narrowed non-union receiver } tag = lowerer.armTag(elem.unionId, outElem); + // Several kept arms: the generic path converts the kept elements to + // the narrower union (filterResultElem). + if (tag < 0 && !isBooleanArg) return null; if (tag < 0) { lowerer.unsupported( "SC1090", @@ -6700,25 +6887,8 @@ export function lowerFilterNarrowCall( return { kind: "call", callee: helper, args: [receiver], type: arrayOf(outElem ?? elem), loc }; } - // Inferred type predicate: inline function literal, NO return - // annotation (a written one is an unchecked assertion), and the - // checker reports a predicate over parameter 0. - if (!ts.isArrowFunction(argNode) && !ts.isFunctionExpression(argNode)) { - lowerer.unsupported( - "SC1090", - argNode, - `narrowing '.filter' through a callback VALUE ` + - `(only an inline callback whose predicate the checker inferred can re-tag — ${annotateEscape})`, - ); - } - if (argNode.type) { - lowerer.unsupported( - "SC1090", - argNode, - `narrowing '.filter' with a hand-written type predicate ` + - `(a written 'x is T' is an unchecked assertion nothing validates at runtime — ${annotateEscape})`, - ); - } + // An inferred type predicate: an inline function literal with no return + // annotation, over parameter 0 (written predicates returned above). // The receiver evaluates FIRST, in the enclosing function, like JS. const receiver = lowerer.lowerExpr(access.expression); const fnArg = lowerer.lowerExpr(argNode); @@ -7029,8 +7199,12 @@ export function recordKeysArrayCall( loc, }, ]; - const order = current.declaredOrder ?? current.fields.map((f) => f.name); - for (const name of order) { + // One walk per key order (see perKeyOrder). + const orders = shapeKeyOrders(current); + const walks = orders.map((): IrStmt[] => []); + for (const [k, name] of orders.flatMap((order, k) => + order.map((name) => [k, name] as const), + )) { const f = current.fields.find((x) => x.name === name)!; const pushStmt: IrStmt = { kind: "exprStmt", @@ -7047,12 +7221,13 @@ export function recordKeysArrayCall( // Fields that can be absent push behind their presence test (the // key exists exactly when Object.keys would list it). const present = lowerer.recordFieldPresent(ref, argIr.shapeId, f.name, loc); - body.push( + walks[k]!.push( present.kind === "recordHas" ? { kind: "if", cond: present, then: [pushStmt], else_: null, loc } : pushStmt, ); } + body.push(...perKeyOrder(current, ref, walks, loc)); body.push({ kind: "return", value: outRef, loc }); fn.body = body; }; @@ -8157,6 +8332,12 @@ function lowerObjectStaticCall( ...(fnName !== null ? { fnName } : {}), }; } + // A class value (`Object.defineProperty(Definition, "name", { value })`) + // boxes to its constructor function, whose own-property table holds the + // definition for every alias of the class. + if (target?.type.kind === "classval" && lowerer.dynConvertible(target.type)) { + target = { kind: "dynFrom", value: target, type: DYN, loc: locOf(call.arguments.nodes[0]!) }; + } const descriptorPrimitive = member === "getOwnPropertyDescriptor" && target && @@ -8552,11 +8733,15 @@ function lowerObjectStaticCall( probed?.type.kind === "union" && isJsSourceFile(ts.getSourceFileOfNode(call)) && lowerer.dynConvertible(probed.type); + // An array walks its live view: the present indices, then its numeric + // and named properties. + const isArray = probed?.type.kind === "array" && lowerer.dynConvertible(probed.type); if ( isDyn || isPrimitive || isFunction || isJsUnion || + isArray || (probed && isDynTypedRefType(probed.type)) ) { const fn = @@ -8566,7 +8751,14 @@ function lowerObjectStaticCall( ? "dyn.objValues" : "dyn.objEntries"; let v = lowerer.lowerExpr(argNode); - if (v.type.kind !== "dyn") v = { kind: "dynFrom", value: v, type: DYN, loc: locOf(call) }; + if (v.type.kind !== "dyn") + v = { + kind: "dynFrom", + value: v, + ...(isArray ? { liveRef: true as const } : {}), + type: DYN, + loc: locOf(call), + }; // In TypeScript the keys come back as a native string[] and the // entries as a native [string, any][]: the checker keeps every later // write to the arrays well typed. JavaScript may store anything into @@ -8738,8 +8930,12 @@ function lowerObjectStaticCall( loc, }, ]; - const order = current.declaredOrder ?? current.fields.map((f) => f.name); - for (const name of order) { + // One walk per key order (see perKeyOrder). + const orders = shapeKeyOrders(current); + const walks = orders.map((): IrStmt[] => []); + for (const [k, name] of orders.flatMap((order, k) => + order.map((name) => [k, name] as const), + )) { const f = current.fields.find((x) => x.name === name)!; const raw: IrExpr = { kind: "recordGet", @@ -8901,8 +9097,9 @@ function lowerObjectStaticCall( loc, }; } - body.push(stmt); + walks[k]!.push(stmt); } + body.push(...perKeyOrder(current, ref, walks, loc)); body.push({ kind: "return", value: outRef, loc }); fn.body = body; }; @@ -9181,7 +9378,16 @@ export function lowerRecordFieldCall( // A HYBRID (function-with-properties) field is callable through its // reserved %call slot — `colors.blue("x")` where blue also carries // `.bold` (the chalk shape). - if (callee.type.kind === "record") callee = lowerer.hybridCallUnwrap(callee); + if (callee.type.kind === "object" && !init) { + const direct = callableInstanceStringCall(lowerer, call, callee, loc); + if (direct) return direct; + // Other argument counts call the %call closure through its value, as + // a call on the builder itself does (`theme.error("a", "b")`). + const closure = lowerer.hybridCallUnwrap(callee); + if (closure !== callee) return lowerCallThroughValue(lowerer, call, closure, loc); + } + if (callee.type.kind === "record" || callee.type.kind === "object") + callee = lowerer.hybridCallUnwrap(callee); // Inferred JavaScript option records can retain checked callables in // fields even when the checker only describes those members as any. // TypeScript reaches here for callable library types that are checked @@ -9843,7 +10049,9 @@ export function lowerObjectMethodCall( if ( info && !found && - (STREAM_API_MEMBERS.has(access.name.text) || STREAM_OPERATORS.has(access.name.text)) && + (STREAM_API_MEMBERS.has(access.name.text) || + STREAM_OPERATORS.has(access.name.text) || + access.name.text === "compose") && streamSidesOf(lowerer, info) !== null ) { const stream = lowerStreamMethodCall(lowerer, call, access, info); @@ -10052,6 +10260,23 @@ export function lowerObjectMethodCall( type: found.sig.ret, loc, }; + // The declaring class's original value, built with the observed slot. + // An override below (a virtual call) keeps the dynamic call: a slot + // reassigned to an ancestor's method must not reach the override. So do + // methods declared in unchecked JavaScript, whose results are inferred, + // and calls with a spread argument: the dynamic call drains the spread + // through the iterator's current `next`, which may be a replacement. + const declarerFile = found.declarer.decl ? ts.getSourceFileOfNode(found.declarer.decl) : null; + const original = + virtual || + call.arguments.nodes.some(ts.isSpreadElement) || + !declarerFile || + (isJsSourceFile(declarerFile) && !lowerer.checkedJsFile(declarerFile)) + ? undefined + : { + originalId: (): string | undefined => found.declarer.prototypeOriginals?.get(method), + directCall: (): IrExpr => fallback, + }; const result = classCallbackCall( lowerer, value, @@ -10060,6 +10285,7 @@ export function lowerObjectMethodCall( fallback, loc, ts.getTextOfNode(access), + original, ); return { kind: "seqExpr", @@ -10939,3 +11165,124 @@ function fenceClassCopyIntoFreshObject( `Object.assign copying a '${lowerer.fmt(rawSource.type)}' instance into a fresh object typed as the class (the copy is a plain object holding the instance's own fields, not an instance — build the object literal from the fields you need)`, ); } + +/** True when an untyped JavaScript receiver names a binding whose declaring + * file assigns it an own `toString` (the TypeScript namespace downlevel + * `errorUtil.toString = (message) => ...` in zod): its calls answer what + * that function returns, not Object.prototype.toString's string. */ +function jsAssignsOwnToString(lowerer: Lowerer, receiver: ts.Expression): boolean { + if (!ts.isIdentifier(receiver)) return false; + const decl = bindingDeclaration(lowerer, receiver); + if (decl === undefined) return false; + const sf = ts.getSourceFileOfNode(decl); + if (!isJsSourceFile(sf)) return false; + let targets = lowerer.ownToStringTargets.get(sf); + if (targets === undefined) { + targets = ownToStringTargets(lowerer, sf); + lowerer.ownToStringTargets.set(sf, targets); + } + return targets.has(decl); +} + +const unparenthesized = (e: ts.Expression): ts.Expression => { + let out = e; + while (ts.isParenthesizedExpression(out)) out = out.expression; + return out; +}; + +/** The declaration of the binding an identifier names, through imports. */ +function bindingDeclaration(lowerer: Lowerer, id: ts.Identifier): ts.Node | undefined { + let sym = lowerer.checker.getSymbolAtLocation(id); + if (sym !== undefined && (sym.flags & ts.SymbolFlags.Alias) !== 0) + sym = lowerer.checker.getAliasedSymbol(sym); + return sym?.valueDeclaration ?? sym?.declarations?.[0]; +} + +/** The declarations of the bindings a file's assignments `x.toString = ...` + * (or `x["toString"] = ...`) give an own `toString`. An assignment through + * the parameter of an immediately called function counts for the binding + * the call passes (`(function (ns) { ns.toString = f; })(ns || (ns = {}))`). */ +function ownToStringTargets(lowerer: Lowerer, sf: ts.SourceFile): Set { + const targets = new Set(); + const visit = (node: ts.Node): void => { + if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken) { + const left = unparenthesized(node.left); + const named = + (ts.isPropertyAccessExpression(left) && left.name.text === "toString") || + (ts.isElementAccessExpression(left) && + ts.isStringLiteralLike(left.argumentExpression) && + left.argumentExpression.text === "toString"); + const target = named + ? unparenthesized( + (left as ts.PropertyAccessExpression | ts.ElementAccessExpression).expression, + ) + : undefined; + if (target !== undefined && ts.isIdentifier(target)) { + const decl = bindingDeclaration(lowerer, target); + if (decl !== undefined) { + targets.add(decl); + const passed = iifeArgumentDeclaration(lowerer, decl); + if (passed !== undefined) targets.add(passed); + } + } + } + ts.forEachChild(node, visit); + }; + visit(sf); + return targets; +} + +/** For a parameter of an immediately called function, the declaration of + * the binding its argument names: `x`, `x || (x = {})` or `(x = {})`. */ +function iifeArgumentDeclaration(lowerer: Lowerer, param: ts.Node): ts.Node | undefined { + if (!ts.isParameter(param)) return undefined; + const fn = param.parent; + if (!ts.isFunctionExpression(fn) && !ts.isArrowFunction(fn)) return undefined; + let callee: ts.Node = fn; + while (ts.isParenthesizedExpression(callee.parent)) callee = callee.parent; + const call = callee.parent; + if (!ts.isCallExpression(call) || call.expression !== callee) return undefined; + const index = fn.parameters.nodes.indexOf(param); + let arg = index < 0 ? undefined : call.arguments.nodes[index]; + if (arg === undefined) return undefined; + arg = unparenthesized(arg); + if (ts.isBinaryExpression(arg) && arg.operatorToken.kind === ts.SyntaxKind.BarBarToken) + arg = unparenthesized(arg.left); + if (ts.isBinaryExpression(arg) && arg.operatorToken.kind === ts.SyntaxKind.EqualsToken) + arg = unparenthesized(arg.left); + return ts.isIdentifier(arg) ? bindingDeclaration(lowerer, arg) : undefined; +} + +/** The runtime calls package shims declare as bodiless functions. */ +const SHIM_INTRINSICS: ReadonlyMap = new Map< + string, + { fn: IrLibFn; args: IrType[]; ret: IrType } +>([ + ["scriptcUnixDgramSocket", { fn: "dgram.unixSocket", args: [], ret: F64 }], + ["scriptcUnixDgramConnect", { fn: "dgram.unixConnect", args: [F64, STRING], ret: F64 }], + ["scriptcUnixDgramSend", { fn: "dgram.unixSend", args: [F64, BYTES_U8], ret: F64 }], + ["scriptcUnixDgramClose", { fn: "dgram.unixClose", args: [F64], ret: F64 }], +]); + +function lowerShimIntrinsicCall( + lowerer: Lowerer, + expr: ts.CallExpression, + callee: ts.Identifier, + loc: SrcLoc, +): IrExpr | null { + const intrinsic = SHIM_INTRINSICS.get(callee.text); + if (intrinsic === undefined) return null; + const decl = lowerer.checker.getSymbolAtLocation(callee)?.valueDeclaration; + if ( + decl === undefined || + !ts.isFunctionDeclaration(decl) || + decl.body || + !isPackageShimFile(ts.getSourceFileOfNode(decl).fileName) || + expr.arguments.nodes.length !== intrinsic.args.length + ) + return null; + const args = expr.arguments.nodes.map((arg, i) => + lowerer.lowerExprExpecting(arg, intrinsic.args[i]!), + ); + return { kind: "libCall", fn: intrinsic.fn, args, type: intrinsic.ret, loc }; +} diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index d8ccc15b7..29afbb4fe 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -5,6 +5,7 @@ import { isDatePrototypeMethod, lowerDateSuperMethodCall, } from "./builtins/date-classes.js"; +import { predeclareForwardClass } from "./lower-stmts.js"; import { lowerDateStaticCall } from "./builtins/dates.js"; import { keyedHelperName } from "./stable-names.js"; import { lowerValueReturningConstructorNew } from "./js-dynamic-objects.js"; @@ -133,6 +134,7 @@ import { STREAM_PROP_MEMBERS, STREAM_COMPLETION_CALLBACK_INDEX, UNDERSCORE_METHODS, + streamExtraCallbacks, lowerStreamNew, lowerStreamSuperCall, streamCtorShape, @@ -184,6 +186,7 @@ import { constructionEscape } from "./construction-escapes.js"; import { checkedClassConstruction, checkedClassConstructionPacked } from "./class-construction.js"; import { initializeRuntimeStatics, hasRuntimeStatics } from "./class-runtime-statics.js"; import { reflectedClassStaticMethodValue } from "./class-method-values.js"; +import { identifierReadSymbol } from "./surfaces.js"; export function returnsOnlyThis(member: ts.MethodDeclaration): boolean { const body = member.body; @@ -236,7 +239,7 @@ export function returnsOnlyThisValue(member: ts.MethodDeclaration): boolean { return valid; } -function functionLocalClass(decl: ts.ClassLikeDeclaration): boolean { +export function functionLocalClass(decl: ts.ClassLikeDeclaration): boolean { for ( let parent: ts.Node | undefined = decl.parent; parent && !ts.isSourceFile(parent); @@ -475,6 +478,10 @@ export interface ClassInfo { prototypeMethodValues?: Map; prototypeReflectionReady?: true; prototypeObservedMethodsReady?: true; + /** The globals holding each observed method's original prototype value + * (set when the prototype data is built), by method name. A typed call + * whose slot still holds that value calls the method natively. */ + prototypeOriginals?: Map; prototypeReflectionRefusal?: string; localClass?: { context: FnCtx | null; @@ -2353,6 +2360,7 @@ export function collectClassShapeInner( const checkedStatics = isJsSourceFile(ts.getSourceFileOfNode(decl)) && ((base && hasRuntimeStatics(base)) || + definedThroughReflection(lowerer, decl) || decl.members.nodes.some( (member) => ts.isAccessor(member) && @@ -2746,11 +2754,11 @@ export function collectClassShapeInner( `overriding the stream member '${name}' (the runtime owns the stream surface)`, ); } - if (name === "_writev" || name === "_construct") { + if (name === "_writev" && !streamExtraCallbacks(streamBase.def.name).includes("writev")) { lowerer.unsupported( "SC1090", memberName, - `declaring '${name}' on a stream subclass (${name === "_writev" ? "batched writes are" : "deferred construction is"} not lowered — writes deliver one chunk at a time)`, + `declaring '_writev' on a ${streamBase.def.name.slice(1)} subclass (batched writes are supported on Writable and Duplex subclasses)`, ); } const accepted = streamCtorShape(streamBase.def.name).accepted; @@ -5474,7 +5482,12 @@ export function classValueRef(lowerer: Lowerer, info: ClassInfo, blame: ts.Node) const display = info.def.name.replace(/^%|^%m\d+\./, ""); fenceDecorationThrows(lowerer, info, blame); if (info.localClass && ts.isIdentifier(blame)) { - const stored = lowerer.resolveLocal(blame); + let stored = lowerer.resolveLocal(blame); + // A reference from a function hoisted above the class statement. + if (!stored) { + const symbol = lowerer.resolveValueSymbol(blame); + if (symbol && predeclareForwardClass(lowerer, symbol)) stored = lowerer.resolveLocal(blame); + } if (stored) return lowerer.coerceToExpected( { kind: "varRef", localId: stored.id, type: stored.type, loc: locOf(blame) }, @@ -6034,6 +6047,28 @@ export function lowerClassExpressionInfo( return info; } +/** True when a named class expression's body refers to the class value by + * its own name (`class Box { static of() { return new Box(); } }`, a + * shorthand property `{ Box }` included); type annotations naming it + * (`again(): Box`) do not count. */ +function classExpressionNamesItself(lowerer: Lowerer, expr: ts.ClassExpression): boolean { + const name = expr.name; + if (!name) return false; + const symbol = lowerer.checker.getSymbolAtLocation(name); + if (!symbol) return false; + let found = false; + for (const member of expr.members.nodes) { + ts.walkPreorder(member, (n) => { + if (found || ts.isTypeNode(n)) return "skip"; + if (ts.isIdentifier(n) && n.text === name.text && identifierReadSymbol(lowerer, n) === symbol) + found = true; + return undefined; + }); + if (found) return true; + } + return false; +} + /** `class {…}` in expression position: a class definition bound to no * statement — once the static side is a value, the expression IS the * definition plus a classRef over it. A DECORATED class expression @@ -6041,8 +6076,16 @@ export function lowerClassExpressionInfo( * mints a class at all: evaluating the expression IS the * ReferenceError, so it lowers to exactly that read — every evaluation * throws identically, which is why the once-evaluated restriction and - * the member fences don't apply. */ -export function lowerClassExpression(lowerer: Lowerer, expr: ts.ClassLikeDeclaration): IrExpr { + * the member fences don't apply. + * + * `binding` is the local of a function-local class declaration's name, + * declared without a value; the class value assigns it before the class's + * static members are defined (see initializeRuntimeStatics). */ +export function lowerClassExpression( + lowerer: Lowerer, + expr: ts.ClassLikeDeclaration, + binding?: IrLocal, +): IrExpr { if ( decoratorNodesOf(expr).length > 0 || expr.members.nodes.some((m) => decoratorNodesOf(m).length > 0) @@ -6058,6 +6101,41 @@ export function lowerClassExpression(lowerer: Lowerer, expr: ts.ClassLikeDeclara } } const info = lowerClassExpressionInfo(lowerer, expr); + // A function-local class expression's own name is a binding of its body: + // declared before the members lower, so their references to the name + // resolve to it. The class value captures it before it is assigned, as + // it captures a class declaration's binding. + if ( + !binding && + info.localClass && + ts.isClassExpression(expr) && + expr.name && + classExpressionNamesItself(lowerer, expr) + ) { + const own = lowerer.declareLocal( + expr.name, + expr.name.text, + { kind: "classval", className: info.def.name }, + true, + ); + const value = lowerClassValue(lowerer, expr, info, own); + return { + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: own.id, init: null, loc: value.loc }], + result: value, + type: value.type, + loc: value.loc, + }; + } + return lowerClassValue(lowerer, expr, info, binding); +} + +function lowerClassValue( + lowerer: Lowerer, + expr: ts.ClassLikeDeclaration, + info: ClassInfo, + binding: IrLocal | undefined, +): IrExpr { if ( !info.localClass && !info.expressionInitializationEmitted && @@ -6104,7 +6182,7 @@ export function lowerClassExpression(lowerer: Lowerer, expr: ts.ClassLikeDeclara const clsRef: IrExpr = { kind: "varRef", localId: clsLocal.id, type: value.type, loc }; const boxed: IrExpr = { kind: "dynFrom", value: clsRef, type: DYN, loc }; info.localPrototypeData = true; - return initializeRuntimeStatics(lowerer, info, { + const built: IrExpr = { kind: "seqExpr", stmts: [ { kind: "varDecl", localId: baseLocal.id, init: base, loc }, @@ -6137,7 +6215,8 @@ export function lowerClassExpression(lowerer: Lowerer, expr: ts.ClassLikeDeclara result: clsRef, type: value.type, loc, - }); + }; + return initializeRuntimeStatics(lowerer, info, built, binding); } if (info.localClass?.baseExpression && info.base && value.kind === "classRef") { const loc = locOf(expr); @@ -6152,7 +6231,7 @@ export function lowerClassExpression(lowerer: Lowerer, expr: ts.ClassLikeDeclara const clsRef: IrExpr = { kind: "varRef", localId: clsLocal.id, type: value.type, loc }; const basePrototype = classPrototypeData(lowerer, info.base, loc, baseRef)!; info.localPrototypeData = true; - return initializeRuntimeStatics(lowerer, info, { + const built: IrExpr = { kind: "seqExpr", stmts: [ { kind: "varDecl", localId: baseLocal.id, init: base, loc }, @@ -6191,13 +6270,15 @@ export function lowerClassExpression(lowerer: Lowerer, expr: ts.ClassLikeDeclara result: clsRef, type: value.type, loc, - }); + }; + return initializeRuntimeStatics(lowerer, info, built, binding); } if (!info.localClass) return value; const classValue = initializeRuntimeStatics( lowerer, info, value.kind === "classRef" ? { ...value, captures: info.localClass.captureSources } : value, + binding, ); // The base resolves statically here, but Node still evaluates the // heritage expression each time the class definition runs, first. @@ -6568,7 +6649,7 @@ function staticCallOn( // functions, called directly as `%C.static:m%n` — with the same // through-a-VALUE shadowing fence as plain statics. const gfound = findGenericStaticOn(lowerer, info, access.name.text); - if (!gfound) return null; + if (!gfound) return classConstructorCall(lowerer, call, access, info); if (throughValue && staticShadowBelow(info, access.name.text)) { lowerer.unsupported( "SC1090", @@ -6661,6 +6742,89 @@ function staticCallOn( return { kind: "call", callee: fnName, args, type: found.method.ret, loc }; } +/** `C.call(thisArg, ...)` / `C.apply(thisArg, args)` on a program class that + * declares no such static: Function.prototype.call/apply invoke the class + * constructor without `new`, which throws Node's TypeError "Class + * constructor C cannot be invoked without 'new'" after the arguments are + * evaluated (and, for apply, after the argument list is read). The boxed + * class value's call path throws exactly that. Neither expando statics nor + * writes through untyped values can give a class an own `call` or `apply`. + * A receiver that may hold a subclass at run time (a class value, `this` in + * a static method) needs every subclass to lack such a static too. The + * receiver must be one the caller does not evaluate again (a name, `this`, + * or a namespace member). Null otherwise. */ +function classConstructorCall( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, + info: ClassInfo, +): IrExpr | null { + const name = access.name.text; + const receiver = access.expression; + if ( + (name !== "call" && name !== "apply") || + !info.decl || + info.def.runtime || + info.builtinError || + info.builtinEmitter || + info.builtinStream || + info.builtinEventTarget || + info.runtimeStatics?.length || + declaresStaticAbove(info, name) || + (exactClassOfReceiver(lowerer, receiver) !== info && + info.subclasses.some((subclass) => declaresStaticBelow(subclass, name))) || + call.arguments.nodes.some(ts.isSpreadElement) || + !( + ts.isIdentifier(receiver) || + receiver.kind === ts.SyntaxKind.ThisKeyword || + (ts.isPropertyAccessExpression(receiver) && nsMemberIdentOf(lowerer, receiver) !== null) + ) + ) + return null; + return { + kind: "dynInvoke", + recv: lowerer.lowerExprExpecting(receiver, DYN), + method: name, + calleeName: ts.getTextOfNode(access), + args: call.arguments.nodes.map((argument) => lowerer.lowerExprExpecting(argument, DYN)), + type: DYN, + loc: locOf(call), + }; +} + +/** Whether `info`'s declaration has a static member that may be named `name` + * (a method, field or accessor of that name, or a computed name), or `info` + * has no declaration to tell. */ +function declaresStatic(info: ClassInfo, name: string): boolean { + if (!info.decl) return true; + return info.decl.members.nodes.some( + (member) => + member.name !== undefined && + ts.canHaveModifiers(member) && + ts.getModifiers(member)?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword) === + true && + (ts.isComputedPropertyName(member.name) || + ((ts.isIdentifier(member.name) || ts.isStringLiteral(member.name)) && + member.name.text === name)), + ); +} + +/** declaresStatic over `info` and its bases, which it inherits statics from. */ +function declaresStaticAbove(info: ClassInfo, name: string): boolean { + for (let c: ClassInfo | null = info; c; c = c.base) if (declaresStatic(c, name)) return true; + return false; +} + +/** declaresStatic over `info` and its subclasses, or runtime statics, which a + * function-local JavaScript class defines at run time. */ +function declaresStaticBelow(info: ClassInfo, name: string): boolean { + return ( + declaresStatic(info, name) || + !!info.runtimeStatics?.length || + info.subclasses.some((subclass) => declaresStaticBelow(subclass, name)) + ); +} + /** `C.m(...)` where `m` is a static method that a function-local * JavaScript class defines at runtime (a factory's `class extends base`), * inherited by `C` through its statically known base chain. When the body @@ -7124,12 +7288,102 @@ export function inHierarchy(lowerer: Lowerer, info: ClassInfo): boolean { * dynamically. Abstract re-declarations don't count (they carry no * implementation; the concrete ones below them do, via the recursion). */ export function overrideBelow(lowerer: Lowerer, info: ClassInfo, name: string): boolean { + if (info.decl !== null && localOverrideBelow(lowerer, info.decl, name)) return true; return info.subclasses.some((s) => { const m = s.methods.get(name); return (m !== undefined && m.abstract !== true) || lowerer.overrideBelow(s, name); }); } +/** The class node an `extends` clause names: a class declaration, or the + * class expression a variable is initialized with. */ +function extendedClassNode(lowerer: Lowerer, decl: ts.ClassLikeDeclaration): ts.Node | null { + const heritage = decl.heritageClauses?.nodes.find( + (clause) => clause.token === ts.SyntaxKind.ExtendsKeyword, + )?.types?.nodes[0]?.expression; + if (!heritage || !ts.isIdentifier(heritage)) return null; + let symbol = lowerer.checker.getSymbolAtLocation(heritage); + if (symbol && symbol.flags & ts.SymbolFlags.Alias) + symbol = lowerer.checker.getAliasedSymbol(symbol); + const target = symbol?.valueDeclaration; + if (!target) return null; + if (ts.isClassDeclaration(target) || ts.isClassExpression(target)) return target; + if (ts.isVariableDeclaration(target) && target.initializer) { + let init: ts.Expression = target.initializer; + while (ts.isParenthesizedExpression(init)) init = init.expression; + if (ts.isClassExpression(init)) return init; + } + return null; +} + +/** The function-local classes that extend the class node `base`. Function + * bodies lower on demand, after code that may already call methods of a + * class their local classes extend, so such a subclass is not yet in its + * base's `subclasses` when those calls lower. */ +function localSubclassesOf(lowerer: Lowerer, base: ts.Node): readonly ts.ClassLikeDeclaration[] { + let index = lowerer.localSubclassIndex; + if (index === null) { + index = new Map(); + const files = lowerer.moduleOrder.length > 0 ? lowerer.moduleOrder : [lowerer.entry]; + for (const sf of files) { + if (!sf.text.includes("extends")) continue; + ts.walkPreorder(sf, (node) => { + if ( + (ts.isClassDeclaration(node) || ts.isClassExpression(node)) && + node.heritageClauses !== undefined && + functionLocalClass(node) + ) { + const extended = extendedClassNode(lowerer, node); + if (extended) { + const list = index!.get(extended); + if (list) list.push(node); + else index!.set(extended, [node]); + } + } + return undefined; + }); + } + lowerer.localSubclassIndex = index; + } + return index.get(base) ?? []; +} + +/** True when a function-local class below the class node `base` declares + * a concrete instance method or accessor `name` (accessors are keyed + * `get:x` and `set:x`). A call on `base`'s class then dispatches on the + * receiver; devirtualization turns it back into a direct call when no such + * override is reachable. */ +function localOverrideBelow( + lowerer: Lowerer, + base: ts.Node, + name: string, + visited?: Set, +): boolean { + const subclasses = localSubclassesOf(lowerer, base); + if (subclasses.length === 0) return false; + visited ??= new Set(); + if (visited.has(base)) return false; + visited.add(base); + const accessor = name.startsWith("get:") ? "get" : name.startsWith("set:") ? "set" : null; + const member = accessor ? name.slice(4) : name; + return subclasses.some( + (sub) => + sub.members.nodes.some( + (m) => + (accessor === null + ? ts.isMethodDeclaration(m) + : accessor === "get" + ? ts.isGetAccessorDeclaration(m) + : ts.isSetAccessorDeclaration(m)) && + m.body !== undefined && + m.name !== undefined && + (ts.isIdentifier(m.name) || ts.isStringLiteral(m.name)) && + m.name.text === member && + !ts.getModifiers(m)?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword), + ) || localOverrideBelow(lowerer, sub, name, visited), + ); +} + /** The nearest GENERIC-method declaration of `name` at/above `info` — * findMethodOn's twin over the genericMethods tables. */ export function findGenericMethodOn( @@ -8987,8 +9241,9 @@ export function superCallStmt( // The SYNTHESIZED constructor of a ctor-less stream subclass: // super() with default options (underscore methods still bind; a // construction passing options requires a declared constructor — - // lowerNew fences that). Zero options ⇒ exactly one init stmt. - return lowerStreamSuperCall( + // lowerNew fences that). Zero options ⇒ the init stmt, then the + // class's _writev/_construct follow-ups. + const stmts = lowerStreamSuperCall( lowerer, info, base, @@ -8996,7 +9251,8 @@ export function superCallStmt( thisLocal, loc, info.decl ?? info.ctor!, - )[0]!; + ); + return stmts.length === 1 ? stmts[0]! : { kind: "block", body: stmts, loc }; } lowerer.noteSuperConstructorEdge(`%${base.def.name}.constructor`); const call: IrStmt = { @@ -10394,6 +10650,45 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { loc, }; } + // A FinalizationRegistry construction: register and unregister keep + // their bookkeeping, and the cleanup callback never runs, which + // ECMAScript allows (scr_finalization_registry_new). + if ( + symbol !== null && + ts.symbolName(symbol) === "FinalizationRegistry" && + lowerer.isStdlibSymbol(symbol) + ) { + const args = expr.arguments?.nodes ?? []; + if (args.some(ts.isSpreadElement)) { + lowerer.unsupported( + "SC1090", + expr, + "FinalizationRegistry construction with spread arguments", + ); + } + const cleanup = args[0] ? lowerer.lowerExprExpecting(args[0], DYN) : dynUndefinedExpr(loc); + const surplus = args.slice(1).map((arg) => lowerer.lowerExpr(arg)); + const created: IrExpr = { + kind: "libCall", + fn: "finalizationRegistry.new", + args: [cleanup], + type: DYN, + loc, + }; + if (surplus.length === 0) return created; + // Surplus arguments evaluate after the cleanup argument and are ignored. + const local = lowerer.declareHiddenLocal("%finalizationCleanup", DYN); + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: local.id, init: cleanup, loc }, + ...surplus.map((value): IrStmt => ({ kind: "exprStmt", expr: value, loc })), + ], + result: { ...created, args: [varRef(local.id, DYN, loc)] }, + type: DYN, + loc, + }; + } // `new Set()`: construct + bulk add from supported values, // including live collection cursors. Duplicates collapse in insertion order. if (ts.isIdentifier(identifier) && lowerer.isStdlibGlobal(identifier, "Set")) { @@ -10605,8 +10900,6 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { Boolean: "boxed wrapper objects have no lowering — use the boolean primitive", WeakRef: "deref()-after-collect exposes GC timing — genuinely dynamic; hold a strong reference instead", - FinalizationRegistry: - "finalization callbacks expose GC timing — genuinely dynamic; release resources explicitly instead", SharedArrayBuffer: "no shared-memory threads exist in a compiled program — Uint8Array is the byte storage", ArrayBuffer: @@ -10990,3 +11283,37 @@ function genericFieldFnNodeOf( } return null; } + +/** Whether a JavaScript class's own binding is the target of + * `Object.defineProperty`/`defineProperties` in its file + * (`Object.defineProperty(Definition, "name", { value })`): its constructor + * then needs a live own-property table. */ +function definedThroughReflection(lowerer: Lowerer, decl: ts.ClassLikeDeclaration): boolean { + const name = decl.name; + const sf = ts.getSourceFileOfNode(decl); + if (name === undefined || !sf.text.includes("definePropert")) return false; + const symbol = lowerer.checker.getSymbolAtLocation(name); + if (symbol === undefined) return false; + let found = false; + ts.walkPreorder(sf, (node) => { + if (found) return "stop"; + if ( + ts.isCallExpression(node) && + ts.isPropertyAccessExpression(node.expression) && + (node.expression.name.text === "defineProperty" || + node.expression.name.text === "defineProperties") && + lowerer.stdlibGlobalMember(node.expression, "Object") !== null + ) { + const target = node.arguments.nodes[0]; + if ( + target !== undefined && + ts.isIdentifier(target) && + target.text === name.text && + lowerer.checker.getSymbolAtLocation(target) === symbol + ) + found = true; + } + return undefined; + }); + return found; +} diff --git a/packages/compiler/src/frontend/lowering/lower-dgram.ts b/packages/compiler/src/frontend/lowering/lower-dgram.ts index aa90fc7c7..0e0ddf975 100644 --- a/packages/compiler/src/frontend/lowering/lower-dgram.ts +++ b/packages/compiler/src/frontend/lowering/lower-dgram.ts @@ -612,8 +612,10 @@ export function lowerDgramMethodCall( const fn: IrLibFn = data.type.kind === "string" ? "dgram.sendStr" : "dgram.sendBytes"; return { kind: "libCall", fn, args: [receiver, data, port, host], type: VOID, loc }; } + // TypeScript reaches the ladder for the completion-callback form + // send(msg, port, address, callback) (the hot-shots package shim). if ( - isJsSourceFile(ts.getSourceFileOfNode(call)) && + (isJsSourceFile(ts.getSourceFileOfNode(call)) || args.length === 4) && args.length <= 6 && !args.some(ts.isSpreadElement) ) { diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index b5acaf9e6..2bde5f2aa 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -1,5 +1,12 @@ import { keyedHelperName } from "./stable-names.js"; +import { + isNewTarget, + lowerNewTarget, + lowerNewTargetComparison, + lowerNewTargetName, +} from "./new-target.js"; import { lowerWebAssemblyValue, refuseWebAssemblyUse } from "./builtins/webassembly.js"; +import { lowerAwaitedUniformPromiseAll } from "./builtins/promises.js"; import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../ir/build.js"; import { InternalCompilerError } from "../../errors.js"; /* Expression lowering: the expression dispatch (lowerExpr), literals @@ -575,6 +582,8 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { // boundary for malformed/upstream ASTs. lowerer.unsupported("SC1090", expr, "top-level await (await outside async functions)"); } + const uniformAll = lowerAwaitedUniformPromiseAll(lowerer, expr); + if (uniformAll) return uniformAll; const value = lowerer.lowerExpr(expr.expression); // A promise-or-absent union (`Promise | undefined` values, calls // of `(...) => Promise | void` callbacks): the await handles @@ -710,7 +719,8 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { if (ts.isPrefixUnaryExpression(expr)) return lowerer.lowerPrefixUnary(expr); // `x++` / `x--` in expression position: yields the OLD value. if (ts.isPostfixUnaryExpression(expr)) return lowerIncDec(lowerer, expr, false); - if (ts.isBinaryExpression(expr)) return lowerer.lowerBinary(expr); + if (ts.isBinaryExpression(expr)) + return lowerNewTargetComparison(lowerer, expr) ?? lowerer.lowerBinary(expr); if (ts.isCallExpression(expr)) { // `h.return?.()` on a helper object: the method exists, so the // optional call is the call. @@ -726,13 +736,15 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { if (ts.isElementAccessExpression(expr)) return lowerer.lowerElementAccess(expr); if (ts.isConditionalExpression(expr)) return lowerTernary(lowerer, expr); - if (ts.isPropertyAccessExpression(expr)) return lowerPropertyAccessExpr(lowerer, expr, loc); + if (ts.isPropertyAccessExpression(expr)) + return lowerNewTargetName(lowerer, expr) ?? lowerPropertyAccessExpr(lowerer, expr, loc); // Meta-properties, named: `new.target` reflects HOW a function was // invoked (compiled functions are never constructors of themselves — // no runtime invocation record exists), and `import.meta`/ // `import.defer` are module-loader surface a native binary does not // carry. + if (isNewTarget(expr)) return lowerNewTarget(lowerer, expr); if (ts.isMetaProperty(expr)) { const name = expr.keywordToken === ts.SyntaxKind.NewKeyword ? "new.target" : `import.${expr.name.text}`; @@ -2092,6 +2104,7 @@ export function typeofAnswer(arm: IrType): string | null { case "array": case "map": case "set": + case "generator": case "regex": case "bytes": case "url": diff --git a/packages/compiler/src/frontend/lowering/lower-functions.ts b/packages/compiler/src/frontend/lowering/lower-functions.ts index 653c0d837..c1cca9c64 100644 --- a/packages/compiler/src/frontend/lowering/lower-functions.ts +++ b/packages/compiler/src/frontend/lowering/lower-functions.ts @@ -1,3 +1,4 @@ +import type { ScrDiagnostic } from "../../diagnostics/diagnostic.js"; import { declareTypedArguments } from "./typed-arguments.js"; import { dynUndefinedExpr, varRef } from "../../ir/build.js"; import { everyStmtList, transformStmtList } from "../../ir/traverse.js"; @@ -17,6 +18,7 @@ import { type IrLocal, type IrParam, type IrStmt, + type IrType, type SrcLoc, VOID, canBoxFuncIntoDyn, @@ -858,3 +860,79 @@ function isBufferCheckOf(e: IrExpr, slotId: string): boolean { e.type.elem === "u8" ); } + +/** The diagnostics of a TypeScript function whose signature did not + * compile, for the coverage remainder: its statements lower for their + * diagnostics only (the remainder's output is discarded), so the report + * lists them with the signature's blocker instead of hiding them behind it. + * Each parameter whose type maps is declared, and a use of any other + * parameter reports SC2004 back to the declaration. The statements already + * count as failed (countSkippedBody), so the pass leaves the stats alone. + * The pass adds diagnostics only: an internal error while it lowers drops + * what it found, and the signature's blocker stands alone as before. */ +export function blockedBodyDiagnostics( + lowerer: Lowerer, + decl: ts.FunctionDeclaration, +): ScrDiagnostic[] { + const body = decl.body; + if (!body || decl.typeParameters || decl.asteriskToken) return []; + if (isJsSourceFile(ts.getSourceFileOfNode(decl))) return []; + const symbol = declSymbolOf(lowerer, decl); + if (!symbol || lowerer.fnSigsBySymbol.has(symbol)) return []; + const diagsBefore = lowerer.diags.length; + const fencesBefore = lowerer.runtimeFences.length; + const suppressStats = lowerer.suppressStats; + lowerer.suppressStats = true; + let declared: IrType = DYN; + lowersCleanly(lowerer, () => { + declared = lowerer.declaredReturnType(decl, decl); + }); + const isAsync = decl.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) ?? false; + const ctx = newFnCtx(false, null, null, lowerer.bodyReturnType(isAsync, declared)); + ctx.isAsync = isAsync; + lowerer.fnStack.push(ctx); + let internal = false; + try { + // A declared `this` parameter gives the body its receiver, as + // lowerFunction declares it. + if (decl.parameters.nodes.some(isThisParameter)) declareCallReceiver(lowerer, decl, false); + for (const param of decl.parameters.nodes) { + if (isThisParameter(param)) continue; + const declaredParam = lowersCleanly(lowerer, () => { + lowerer.declareParams([param], lowerer.paramShapes([param])); + }); + if (declaredParam) continue; + const symbol = ts.isIdentifier(param.name) + ? lowerer.checker.getSymbolAtLocation(param.name) + : undefined; + if (symbol) lowerer.blockedBindings.add(symbol); + } + lowersCleanly(lowerer, () => { + lowerer.lowerStmts(body.statements.nodes); + }); + } catch (error) { + if (!(error instanceof InternalCompilerError)) throw error; + internal = true; + } finally { + lowerer.fnStack.pop(); + lowerer.suppressStats = suppressStats; + } + lowerer.runtimeFences.splice(fencesBefore); + const diags = lowerer.diags.splice(diagsBefore); + return internal || diags.some((d) => d.code === "SC9001") ? [] : diags; +} + +/** Runs `lower`, for blockedBodyDiagnostics: false when it poisons, with the + * diagnostics it recorded dropped (a failed mapping reports what the + * signature's blocker already reports). */ +function lowersCleanly(lowerer: Lowerer, lower: () => void): boolean { + const before = lowerer.diags.length; + try { + lower(); + return true; + } catch (error) { + if (!(error instanceof PoisonError)) throw error; + lowerer.diags.splice(before); + return false; + } +} diff --git a/packages/compiler/src/frontend/lowering/lower-generators.ts b/packages/compiler/src/frontend/lowering/lower-generators.ts index b4e3ac102..80463fa6c 100644 --- a/packages/compiler/src/frontend/lowering/lower-generators.ts +++ b/packages/compiler/src/frontend/lowering/lower-generators.ts @@ -687,8 +687,12 @@ export function lowerForAwaitGenerator( lowerer.scopes.push(new Map()); try { const g = lowerer.declareHiddenLocal("%fag", genT); - const done = lowerer.declareHiddenLocal("%fagdone", BOOL); - done.mutable = true; + // Whether a value has arrived and the body holds the generator open: + // leaving the loop then (break, return, a throw, or a return() at a + // yield in the body) closes it, as AsyncIteratorClose does; an + // exhausted generator and a rejected next() are not closed. + const active = lowerer.declareHiddenLocal("%fagactive", BOOL); + active.mutable = true; const p = lowerer.declareHiddenLocal("%fagpromise", promiseT); const r = lowerer.declareHiddenLocal("%fagresult", recT); const gRef = (): IrExpr => ({ kind: "varRef", localId: g.id, type: genT, loc }); @@ -709,7 +713,14 @@ export function lowerForAwaitGenerator( `for-await over an async generator yielding '${lowerer.fmt(genT.yieldT)}' (no per-element extraction exists)`, ); } + const setActive = (value: boolean): IrStmt => ({ + kind: "assign", + localId: active.id, + value: { kind: "boolLit", value, type: BOOL, loc }, + loc, + }); const head: IrStmt[] = [ + setActive(false), { kind: "varDecl", localId: p.id, @@ -737,19 +748,12 @@ export function lowerForAwaitGenerator( type: BOOL, loc, }, - then: [ - { - kind: "assign", - localId: done.id, - value: { kind: "boolLit", value: true, type: BOOL, loc }, - loc, - }, - { kind: "break", loc }, - ], + then: [{ kind: "break", loc }], else_: null, loc, }, ...generatorLoopBinding(lowerer, stmt, extracted, genT.yieldT), + setActive(true), ]; const body = lowerer.inCtl("loop", () => lowerer.lowerScopedBlock(stmt.statement), labels); const closePromise: IrExpr = { @@ -766,34 +770,40 @@ export function lowerForAwaitGenerator( { kind: "varDecl", localId: g.id, init: iterable, loc }, { kind: "varDecl", - localId: done.id, + localId: active.id, init: { kind: "boolLit", value: false, type: BOOL, loc }, loc, }, { - kind: "while", - cond: { kind: "boolLit", value: true, type: BOOL, loc }, - body: [...head, ...body], - ...(labels && { labels }), - loc, - }, - { - kind: "if", - cond: { - kind: "unary", - op: "!", - operand: { kind: "varRef", localId: done.id, type: BOOL, loc }, - type: BOOL, - loc, - }, - then: [ + kind: "tryCatch", + tryBody: [ { - kind: "exprStmt", - expr: { kind: "awaitExpr", value: closePromise, type: recT, loc }, + kind: "while", + cond: { kind: "boolLit", value: true, type: BOOL, loc }, + body: [...head, ...body], + ...(labels && { labels }), + loc, + }, + ], + catchBody: null, + catchLocalId: null, + // After a throw, an error from closing the generator is dropped. + dropFinallyErrors: true, + finallyBody: [ + { + kind: "if", + cond: { kind: "varRef", localId: active.id, type: BOOL, loc }, + then: [ + { + kind: "exprStmt", + expr: { kind: "awaitExpr", value: closePromise, type: recT, loc }, + loc, + }, + ], + else_: null, loc, }, ], - else_: null, loc, }, ], diff --git a/packages/compiler/src/frontend/lowering/lower-global-value.ts b/packages/compiler/src/frontend/lowering/lower-global-value.ts index 4b8e7b10a..e7ab6b0f0 100644 --- a/packages/compiler/src/frontend/lowering/lower-global-value.ts +++ b/packages/compiler/src/frontend/lowering/lower-global-value.ts @@ -1,6 +1,5 @@ import * as ts from "../ts7/adapter.js"; import { DYN, STRING, arrayOf, type IrExpr } from "../../ir/ir.js"; -import { strLit } from "../../ir/build.js"; import { locOf } from "../program.js"; import type { Lowerer } from "./lowerer.js"; import { TEMPORAL_GLOBAL } from "../builtin-modules.js"; @@ -19,10 +18,14 @@ export function lowerGlobalValue(lowerer: Lowerer, expr: ts.Expression): IrExpr return { kind: "libCall", fn: "global.native", + // One immortal name table per program (keyed by its contents): reading + // the global object in a hot path allocates nothing, and the runtime + // recognizes a table it has already registered. args: [ { - kind: "arrayLit", - elems: names.map((name) => strLit(name, loc)), + kind: "templateStrings", + key: `%globalThis.names:${names.join(",")}`, + cooked: names, type: arrayOf(STRING), loc, }, diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index 47f490c7b..5266d5c4e 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -1,4 +1,5 @@ import { keyedHelperName } from "./stable-names.js"; +import { perKeyOrder } from "./key-order-witness.js"; import { dynUndefinedExpr, boolLit, @@ -40,6 +41,7 @@ import { isDynTypedRefType, recordTextCodecClass, shapeHasAccessorSlots, + shapeKeyOrders, typeKey, } from "../../ir/ir.js"; import type { ClassInfo } from "./lower-classes.js"; @@ -1647,7 +1649,12 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string { }); } body.push(depthGate("[Object]"), ...begin()); - for (const fname of order) { + // One walk per key order (see perKeyOrder). + const orders = shapeKeyOrders(current); + const walks = orders.map((): IrStmt[] => []); + for (const [k, fname] of orders.flatMap((order, k) => + order.map((name) => [k, name] as const), + )) { const ft = byName.get(fname); if (!ft) continue; const show = entry( @@ -1655,12 +1662,13 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string { boolLit(false, loc), ); const present = lowerer.recordFieldPresent(v(), t.shapeId, fname, loc); - body.push( + walks[k]!.push( present.kind === "recordHas" ? { kind: "if", cond: present, then: [show], else_: null, loc } : show, ); } + body.push(...perKeyOrder(current, v(), walks, loc)); body.push( ret(end(strLit("", loc), strLit("{", loc), strLit("}", loc), false, boolLit(false, loc))), ); @@ -1918,13 +1926,21 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string { // A closed record that kept the undeclared properties of its checked // source renders through its live checked view, which holds them in the - // source's own-key order beside the current field values. + // source's own-key order beside the current field values. An array with + // properties besides its elements does too: the view lists them after + // the elements. let prependSourceGate: (() => void) | null = null; - if (t.kind === "record" && recordMayKeepSource(lowerer, t)) { + const sourceTest: IrExpr | null = + t.kind === "record" && recordMayKeepSource(lowerer, t) + ? recordSourceTest(v(), loc) + : t.kind === "array" && lowerer.dynConvertible(t) + ? { kind: "libCall", fn: "array.hasProperties", args: [v()], type: BOOL, loc } + : null; + if (sourceTest !== null) { prependSourceGate = (): void => { body.unshift({ kind: "if", - cond: recordSourceTest(v(), loc), + cond: sourceTest, then: [ ret({ kind: "libCall", diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index 5b78c7278..5238333ad 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -2929,6 +2929,38 @@ export function lowerDefaultExport(lowerer: Lowerer, stmt: ts.ExportAssignment): return { kind: "assign", localId: g.id, value, loc: locOf(stmt) }; } +/** Marks the runtime fences recorded since `fencesBefore` that lie in + * `sf`'s top-level code: they run when the module initializes. A fence + * inside a function, or inside a class member that runs later (a method, + * accessor, constructor or instance field initializer), runs when that + * code is called. */ +function markModuleInitFences(lowerer: Lowerer, sf: ts.SourceFile, fencesBefore: number): void { + for (let i = fencesBefore; i < lowerer.runtimeFences.length; i++) { + const fence = lowerer.runtimeFences[i]!; + if (fence.loc.file !== sf.fileName || runsAfterModuleInit(sf, fence.loc.start)) continue; + lowerer.runtimeFences[i] = { ...fence, startup: true }; + } +} + +/** Whether the code at `pos` sits in a function or a later-running class + * member body. */ +function runsAfterModuleInit(sf: ts.SourceFile, pos: number): boolean { + let later = false; + ts.walkPreorder(sf, (node) => { + if (node.pos > pos || node.end <= pos) return "skip"; + if ( + ts.isFunctionLike(node) || + (ts.isPropertyDeclaration(node) && + !node.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.StaticKeyword)) + ) { + later = true; + return "stop"; + } + return undefined; + }); + return later; +} + export function lowerFileInit( lowerer: Lowerer, sf: ts.SourceFile, @@ -2938,6 +2970,7 @@ export function lowerFileInit( const isAsync = lowerer.asyncInitFiles.has(sf); const ctx = newFnCtx(false, null, null, VOID); ctx.isAsync = isAsync; + const fencesBefore = lowerer.runtimeFences.length; lowerer.fnStack.push(ctx); try { const loc0: SrcLoc = { file: sf.fileName, start: 0, end: 0 }; @@ -3288,6 +3321,7 @@ export function lowerFileInit( }; } finally { lowerer.fnStack.pop(); + markModuleInitFences(lowerer, sf, fencesBefore); } } diff --git a/packages/compiler/src/frontend/lowering/lower-namespaces.ts b/packages/compiler/src/frontend/lowering/lower-namespaces.ts index 19123dae3..ea844ab1e 100644 --- a/packages/compiler/src/frontend/lowering/lower-namespaces.ts +++ b/packages/compiler/src/frontend/lowering/lower-namespaces.ts @@ -38,7 +38,7 @@ * calls). */ import { positionTag, type PositionTagOwner } from "./structural-positions.js"; import * as ts from "../ts7/adapter.js"; -import { packageShimFor } from "../package-shims.js"; +import { isPackageShimFile, isShimIntrinsicName, packageShimFor } from "../package-shims.js"; import type { Lowerer } from "./lowerer.js"; import { boundIdentifiersOf } from "./lowerer.js"; import type { FileParts } from "./lower-modules.js"; @@ -654,6 +654,12 @@ export function ambientUndefinedFnSymbolOf( const sym = lowerer.resolveValueSymbol(ident); if (!sym || !(sym.flags & ts.SymbolFlags.Function)) return null; if (lowerer.isStdlibSymbol(sym)) return null; + // A package shim's runtime intrinsic (lower-calls.ts) is defined. + if ( + isShimIntrinsicName(ident.text) && + sym.declarations.some((d) => isPackageShimFile(ts.getSourceFileOfNode(d).fileName)) + ) + return null; let sawFn = false; for (const d of sym.declarations) { if (ts.isInterfaceDeclaration(d) || ts.isTypeAliasDeclaration(d)) continue; // type-world merge partners diff --git a/packages/compiler/src/frontend/lowering/lower-stdlib.ts b/packages/compiler/src/frontend/lowering/lower-stdlib.ts index 0beb24a0e..e671a0a08 100644 --- a/packages/compiler/src/frontend/lowering/lower-stdlib.ts +++ b/packages/compiler/src/frontend/lowering/lower-stdlib.ts @@ -2496,6 +2496,25 @@ export function lowerStdlibMethodCall( // method, drains into the number[] the fold reads. src = classGeneratorIterable(lowerer, src) ?? src; if (src.type.kind === "generator") src = generatorDrain(lowerer, src, F64, loc) ?? src; + // An untyped iterable (`Math.min(...caps.map(f))` in JavaScript): the + // runtime packs its values and folds their ToNumber like Node. V8 names + // a null or undefined source by its text, calls as `f(...)`. + if (src.type.kind === "dyn") { + const what = ts.isCallExpression(spread.expression) + ? `${ts.getTextOfNode(spread.expression.expression)}(...)` + : ts.getTextOfNode(spread.expression); + return { + kind: "libCall", + fn: "dyn.mathFold", + args: [ + src, + { kind: "boolLit", value: name === "max", type: BOOL, loc }, + { kind: "strLit", value: what, type: STRING, loc }, + ], + type: F64, + loc, + }; + } if (src.type.kind !== "array" || src.type.elem.kind !== "f64") { lowerer.unsupported( "SC1090", diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 213abfec6..66ecea602 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -2,7 +2,7 @@ import { lowerTupleAliasDecl, stripTypeWrappers } from "./typed-arguments.js"; import { declaredMemberOf, isPromisePrototypeRead } from "./expressions/property-access.js"; import { hostsAll } from "./expressions/hosted-narrowing.js"; import { requestGeneralStreamSurface } from "./general-streams.js"; -import { lowerForAwaitDynStream } from "./dyn-streams.js"; +import { lowerForAwaitDynStream, nodeStreamInterfaceSide } from "./dyn-streams.js"; import { lowerForAwaitValueStream } from "./builtins/hash-stream.js"; import { lowerChildPropertyWrite } from "./handle-properties.js"; import { @@ -25,6 +25,7 @@ import { InternalCompilerError } from "../../errors.js"; * flow (if/while/for/for-of/do, switch, try/catch, abrupt completions), * explicit resource management, and blocked-binding poisoning. */ import * as ts from "../ts7/adapter.js"; +import { lowerOptionalIndex } from "./undefined-index.js"; import { processDataMemberValue, programReadsExitCode } from "./builtins/process.js"; import { requireInitFlagStmt } from "./require-initialization.js"; import { isJsonRequireDecl } from "./json-require.js"; @@ -67,9 +68,11 @@ import { F64, type IrExpr, type IrGlobal, + type IrLibFn, type IrLocal, type IrStmt, type IrType, + SEARCH_PARAMS_T, STRING, type SrcLoc, UNDEFINED_T, @@ -106,6 +109,7 @@ import { COMPOUND_ASSIGN_OPS, type CompoundOp, STR_METHODS, + NARROW_FIRST, UNSUPPORTED_STMT, isStdlibMember, sideEffectFreeOptionValue, @@ -163,7 +167,11 @@ import { objectFactorySignature } from "./object-factory-new.js"; import { isClassCallback, lowerClassCallbackAssign } from "./class-callbacks.js"; import { classForInHasKey, classPropertiesHelper } from "./class-dynamic-dispatch.js"; import { genericIfaceBindingKeepsClass, superConditionLead } from "./lower-classes.js"; -import { lowerStreamUnderscoreAssign, streamClassAliasDecl } from "./lower-stream.js"; +import { + lowerStreamUnderscoreAssign, + streamClassAliasDecl, + streamSidesOf, +} from "./lower-stream.js"; import { staticAssertInstanceDecl } from "./assert-instances.js"; import { lowerHttpResPropertyAssignment, @@ -197,7 +205,6 @@ import { lowerElementCompoundStatement, lowerIncDec, lowerIncDecTarget, - lowerOptionalNumber, lowerGroupsProjection, matchResultNamedGroupsOf, runtimeOptionalGuardIds, @@ -239,6 +246,7 @@ import { npmStaticPackageOfPath } from "../npm-static.js"; import { fenceNodeModuleMutation } from "./lower-node-module.js"; import { fencePerfEntryKeyWalk } from "./builtins/performance.js"; import { classGeneratorIterable } from "./class-iterables.js"; +import { iterableUnionOf, lowerForOfIterable } from "./iterable-values.js"; interface UsingRegistration { acquire: IrStmt[]; @@ -1002,6 +1010,44 @@ export function predeclareForwardCapture(lowerer: Lowerer, symbol: ts.Symbol): b return false; } +/** The CLASS-DECLARATION twin of predeclareForwardCapture: a function + * hoisted above a local `class D` (`function make() { return new D(); }` + * reached through an earlier function) captures the class binding, which + * is in its temporal dead zone until the class statement runs. The class + * cannot be collected yet (its heritage may name bindings declared between + * here and the statement), so the binding is pre-declared as a TDZ box + * holding the class as a checked-dynamic constructor; the class statement + * later initializes it, and every reference reads it from there. */ +export function predeclareForwardClass(lowerer: Lowerer, symbol: ts.Symbol): boolean { + const decl = symbol.valueDeclaration; + if (!decl || !ts.isClassDeclaration(decl) || decl.name === undefined) return false; + if (!isJsSourceFile(ts.getSourceFileOfNode(decl))) return false; + for (let i = lowerer.activeStmtLists.length - 1; i >= 0; i--) { + const entry = lowerer.activeStmtLists[i]!; + const idx = entry.stmts.indexOf(decl); + if (idx < 0) continue; + if (idx < entry.index || entry.ctx === lowerer.ctx || entry.ctx.tdzPredeclared.has(symbol)) + return false; + const name = decl.name.text; + const count = entry.ctx.localCounters.get(name) ?? 0; + entry.ctx.localCounters.set(name, count + 1); + const local: IrLocal = { + id: `${name}.${count}`, + name, + type: DYN, + mutable: false, + boxed: true, + tdz: true, + }; + entry.ctx.locals.push(local); + entry.frame.set(symbol, local); + entry.out.push({ kind: "varDecl", localId: local.id, init: null, loc: locOf(decl) }); + entry.ctx.tdzPredeclared.set(symbol, local); + return true; + } + return false; +} + /** The FUNCTION-DECLARATION twin of predeclareForwardCapture: JS hoists a * nested `function f() {}` to scope entry — the binding is LIVE from the * first statement, so a reference lexically above the declaration in the @@ -1212,6 +1258,43 @@ function lowerJsVarFromExisting( }; } +/** Whether a JavaScript declaration's object value changes shape after its + * initializer (jsOpenObjectType, jsObjectBindingExpands) or carries + * methods, so only the checked-dynamic representation holds it. + * `inferred` is the declaration's mapped checker type. */ +function jsDeclarationExpandsObject( + lowerer: Lowerer, + decl: ts.VariableDeclaration, + inferred: IrType | null, +): boolean { + const arms = + inferred?.kind === "union" + ? (lowerer.unions.get(inferred.unionId)?.arms ?? []) + : inferred + ? [inferred] + : []; + return ( + (!decl.type && + !hasJsTypeAnnotation(decl) && + inferred !== null && + jsOpenObjectType(decl, inferred, lowerer.shapes, lowerer.unions).kind === "dyn") || + arms.some((arm) => arm.kind === "record" && jsObjectBindingExpands(lowerer, decl, arm)) || + (isJsSourceFile(ts.getSourceFileOfNode(decl)) && + !decl.type && + !hasJsTypeAnnotation(decl) && + decl.initializer !== undefined && + ts.isObjectLiteralExpression(decl.initializer) && + decl.initializer.properties.nodes.some( + (property) => + ts.isMethodDeclaration(property) || + ts.isAccessor(property) || + (ts.isPropertyAssignment(property) && + (ts.isFunctionExpression(property.initializer) || + ts.isArrowFunction(property.initializer))), + )) + ); +} + /** The `var` symbol's function-scoped binding TYPE — the declared type at * the binding name, with the JS-source fallbacks every mutable binding * takes (an empty-object-literal type is checked-dynamic because tsc @@ -1246,6 +1329,19 @@ function varBindingType(lowerer: Lowerer, nameNode: ts.Identifier): IrType | nul type = staticImportNamespaceType(lowerer, nameNode.parent.initializer) ?? type; } if (!type || type.kind === "void") return null; + // A JavaScript `var` holds whatever its writes store, as a `let` does: + // the checker types it by its initializer and checks no later write + // (`var fraction = 0;`, later `fraction = match[7].slice(0, 3)`), and an + // object it holds may gain members (`mark.snippet = snippet(mark)`). + if ( + type.kind !== "dyn" && + ts.isVariableDeclaration(nameNode.parent) && + nameNode.parent.name === nameNode && + isJsSourceFile(ts.getSourceFileOfNode(nameNode)) && + (jsBindingHasOpenWrites(lowerer, nameNode.parent) || + jsDeclarationExpandsObject(lowerer, nameNode.parent, type)) + ) + return DYN; // A binding readable before its first store holds undefined until then. return lowerer.runtimeOptionalBindingType(nameNode, type); } @@ -1521,8 +1617,24 @@ export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt if (ts.isClassDeclaration(stmt)) { if (!stmt.name) lowerer.unsupported("SC1090", stmt, "anonymous local class declarations"); const info = lowerer.lowerClassExpressionInfo(stmt); + // An earlier hoisted function captured the class: its TDZ box exists. + const symbol = lowerer.checker.getSymbolAtLocation(stmt.name); + const pre = symbol ? lowerer.ctx.tdzPredeclared.get(symbol) : undefined; + if (pre) { + lowerer.ctx.tdzPredeclared.delete(symbol!); + const value = lowerer.coerceToExpected(lowerer.lowerClassExpression(stmt), DYN); + return { kind: "assign", localId: pre.id, value, initializes: true, loc: locOf(stmt) }; + } const type: IrType = { kind: "classval", className: info.def.name }; const local = lowerer.declareLocal(stmt.name, stmt.name.text, type, true); + // Static members of a function-local class run while the class value is + // built and may read the class by name: the class value assigns the + // binding before them. + if (info.localClass && (info.runtimeStatics || info.runtimePrototypeMembers)) + return [ + { kind: "varDecl", localId: local.id, init: null, loc: locOf(stmt) }, + { kind: "exprStmt", expr: lowerer.lowerClassExpression(stmt, local), loc: locOf(stmt) }, + ]; const init = lowerer.lowerClassExpression(stmt); return { kind: "varDecl", localId: local.id, init, loc: locOf(stmt) }; } @@ -3210,6 +3322,20 @@ export function lowerBindingPattern( lowerModuleNamespacePattern(lowerer, pattern, nsSource, isLet, out); return; } + if (srcType.kind === "url") { + lowerUrlObjectPattern(lowerer, pattern, srcRef, isLet, out); + return; + } + if ( + srcType.kind === "union" && + !isJsSourceFile(ts.getSourceFileOfNode(pattern)) && + lowerer.unions + .get(srcType.unionId) + ?.arms.every((arm) => arm.kind === "record" || arm.kind === "object") + ) { + lowerUnionObjectPattern(lowerer, pattern, srcRef, srcType, isLet, out); + return; + } if (srcType.kind !== "record") { lowerer.unsupported( "SC1031", @@ -3424,7 +3550,10 @@ function patternSourceValue( !arms || !present?.length || present.length === arms.length || - (present.length > 1 && !(arrayPattern && present.every((arm) => arm.kind === "array"))) + (present.length > 1 && + !(arrayPattern + ? present.every((arm) => arm.kind === "array") + : present.every((arm) => arm.kind === "record" || arm.kind === "object"))) ) return value; for (const [tag, arm] of arms.entries()) { @@ -3541,6 +3670,149 @@ function lowerModuleNamespacePattern( } } +/** The URL getters an object pattern reads (`const { protocol, hostname } + * = new URL(text)`), each a read of the URL's current serialization. */ +const URL_PATTERN_GETTERS: Record = { + href: "url.href", + origin: "url.origin", + protocol: "url.protocol", + username: "url.username", + password: "url.password", + host: "url.host", + hostname: "url.hostname", + port: "url.port", + pathname: "url.pathname", + search: "url.search", + hash: "url.hash", +}; + +/** An object pattern over a URL: each element reads its getter in pattern + * order, as `url.name` does; `searchParams` binds the URL's live view. + * Methods and rest elements stay fenced. */ +function lowerUrlObjectPattern( + lowerer: Lowerer, + pattern: ts.ObjectBindingPattern, + srcRef: () => IrExpr, + isLet: boolean, + out: IrStmt[], +): void { + for (const el of pattern.elements.nodes) { + if (el.name === undefined) continue; + if (el.dotDotDotToken) { + lowerer.unsupported( + "SC1031", + el, + "rest bindings over URL sources (a URL's properties are prototype getters, which rest elements do not copy)", + ); + } + const prop = el.propertyName ?? el.name; + const propName = + ts.isIdentifier(prop) || + ts.isComputedPropertyName(prop) || + ts.isStringLiteralLike(prop) || + ts.isNumericLiteral(prop) + ? patternKeyNameOf(lowerer, prop as ts.PropertyName) + : null; + const loc = locOf(el); + let value: IrExpr; + if (propName !== null && Object.hasOwn(URL_PATTERN_GETTERS, propName)) { + value = { + kind: "libCall", + fn: URL_PATTERN_GETTERS[propName]!, + args: [srcRef()], + type: STRING, + loc, + }; + } else if (propName === "searchParams") { + value = { + kind: "libCall", + fn: "url.searchParams", + args: [srcRef()], + type: SEARCH_PARAMS_T, + loc, + }; + } else { + lowerer.unsupported( + "SC1031", + el, + propName === null + ? "destructuring with computed keys that do not fold to one property name" + : `destructuring '${propName}' from a URL (${Object.keys(URL_PATTERN_GETTERS).join(", ")} and searchParams are the supported URL properties)`, + ); + } + if (el.initializer) value = applyBindingDefault(lowerer, el, value); + lowerer.bindPatternTarget(el.name, value, isLet, out); + } +} + +/** An object pattern over a union of records and class instances + * (`function f({ kind, path }: Add | Remove)`): each element reads a data + * field every arm declares with one type, through the runtime tag, as + * `value.field` does. Rest elements and fields the arms type differently + * stay fenced. */ +function lowerUnionObjectPattern( + lowerer: Lowerer, + pattern: ts.ObjectBindingPattern, + srcRef: () => IrExpr, + srcType: IrType & { kind: "union" }, + isLet: boolean, + out: IrStmt[], +): void { + const arms = lowerer.unions.get(srcType.unionId)?.arms ?? []; + for (const el of pattern.elements.nodes) { + if (el.name === undefined) continue; + if (el.dotDotDotToken) { + lowerer.unsupported("SC1031", el, `rest bindings over union sources (${NARROW_FIRST})`); + } + const prop = el.propertyName ?? el.name; + const propName = + ts.isIdentifier(prop) || + ts.isComputedPropertyName(prop) || + ts.isStringLiteralLike(prop) || + ts.isNumericLiteral(prop) + ? patternKeyNameOf(lowerer, prop as ts.PropertyName) + : null; + if (propName === null) { + lowerer.unsupported( + "SC1031", + el, + "destructuring with computed keys that do not fold to one property name", + ); + } + let common: IrType | null = null; + for (const arm of arms) { + let ft: IrType | undefined; + if (arm.kind === "record") { + ft = lowerer.shapes.get(arm.shapeId)?.fields.find((f) => f.name === propName)?.type; + } else if (arm.kind === "object") { + ft = lowerer.classes.get(arm.className)?.fields.get(propName); + } + if (!ft || ft.kind === "void" || isUnitType(ft) || (common && !typeEquals(common, ft))) { + common = null; + break; + } + common = ft; + } + if (common === null) { + lowerer.unsupported( + "SC1031", + el, + `destructuring '${propName}' from a union whose arms type it differently (every arm must be a record or class with a same-typed field '${propName}' — ${NARROW_FIRST})`, + ); + } + let value: IrExpr = { + kind: "unionDisc", + unionId: srcType.unionId, + field: propName, + value: srcRef(), + type: common, + loc: locOf(el), + }; + if (el.initializer) value = applyBindingDefault(lowerer, el, value); + lowerer.bindPatternTarget(el.name, value, isLet, out); + } +} + /** Whether a pattern's first element has a computed key that does not * fold to one property name, so the key evaluates at run time. */ function startsWithRuntimeKey(lowerer: Lowerer, pattern: ts.ObjectBindingPattern): boolean { @@ -4694,12 +4966,16 @@ export function bindPatternTarget( } // Preserve checked-value pattern bindings when the inferred type // cannot represent them without a refusal or an alias-breaking copy. + // A name destructured from a container of an evolving array + // (evolvingArrayHold) may hold the array, which the checker types by + // the elements written before it. const dynStays = value.type.kind === "dyn" && - (() => { + ((() => { const mapped = lowerer.mapTypeOf(lowerer.typeOf(name)); return mapped === null || jsBindingKeepsCheckedObject(lowerer, name, mapped); - })(); + })() || + readsHeldEvolvingArray(lowerer, name)); let type = dynStays ? DYN : (lowerer.runtimeOptionalBindingType(name, lowerer.irTypeOf(name)) ?? @@ -4799,9 +5075,10 @@ export function lowerVarDeclList( * `never[]` or `any[]` and each later read by the elements written before * it, and reads inside closures as the unsettled array. When every read * with a settled element type maps to the same array type, the binding has - * that type. When reads disagree or the binding is reassigned, it is - * checked-dynamic, as the same binding is at module scope. Null for other - * bindings. */ + * that type. When reads disagree, a write (`xs.push(v)`, `xs.unshift(v)`, + * `xs.push(...values)`, `xs[i] = v`) stores a value that type's element does + * not hold, or the binding is reassigned, it is checked-dynamic, as the same + * binding is at module scope. Null for other bindings. */ function evolvingArrayType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrType | null { if (!ts.isIdentifier(decl.name) || decl.initializer === undefined) return null; let initializer: ts.Expression = decl.initializer; @@ -4823,14 +5100,20 @@ function evolvingArrayType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrTy }; const scope = owner(decl); let result: IrType | null = null; + // The values the binding's writes store, checked against the element + // type once the reads settle it. + const written: ts.Expression[] = []; // The file's identifiers are indexed by name once (identifiersNamed), so - // each binding resolves only the uses spelled like it. + // each binding resolves only the uses spelled like it. A shorthand + // property (`{ xs }`) is a use too. for (const node of identifiersNamed(lowerer, ts.getSourceFileOfNode(decl), decl.name.text)) { if ( node === decl.name || node.pos < scope.pos || node.end > scope.end || - lowerer.checker.getSymbolAtLocation(node) !== symbol + (ts.isShorthandPropertyAssignment(node.parent) && node.parent.name === node + ? lowerer.checker.getShorthandAssignmentValueSymbol(node.parent) + : lowerer.checker.getSymbolAtLocation(node)) !== symbol ) continue; let target: ts.Node = node; @@ -4842,6 +5125,22 @@ function evolvingArrayType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrTy parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment; if (reassigned) return DYN; + if ( + ts.isPropertyAccessExpression(parent) && + parent.expression === target && + (parent.name.text === "push" || parent.name.text === "unshift") && + ts.isCallExpression(parent.parent) && + parent.parent.expression === parent + ) + written.push(...parent.parent.arguments.nodes); + else if ( + ts.isElementAccessExpression(parent) && + parent.expression === target && + ts.isBinaryExpression(parent.parent) && + parent.parent.left === parent && + parent.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken + ) + written.push(parent.parent.right); const t = lowerer.typeOf(node); if (unsettled(t)) continue; const mapped = lowerer.mapTypeOf(t); @@ -4849,7 +5148,126 @@ function evolvingArrayType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrTy return DYN; result = mapped; } - return result ?? DYN; + if (result === null || result.kind !== "array") return DYN; + // The element types a write stores: its value's, or for a spread + // (`xs.push(...values)`) the source array's element or tuple's elements. + // Another spread source stores elements this check cannot see. + const storedTypes = (value: ts.Expression): ts.Type[] | null => { + if (!ts.isSpreadElement(value)) return [lowerer.typeOf(value)]; + const source = lowerer.typeOf(value.expression); + if (lowerer.checker.isArrayType(source) || lowerer.checker.isTupleType(source)) + return [...lowerer.checker.getTypeArguments(source as ts.TypeReference)]; + return null; + }; + for (const value of written) { + const stored = storedTypes(value); + if (stored === null) return DYN; + for (const type of stored) { + const t = lowerer.mapTypeOf(lowerer.checker.getBaseTypeOfLiteralType(type)); + if (!t || (!typeEquals(t, result.elem) && lowerer.widthLiftPlan(t, result.elem) === null)) + return DYN; + } + } + return result; +} + +/** How a TypeScript expression holds an evolving array binding whose reads + * disagree (evolvingArrayType answers checked-dynamic): "evolving" for the + * binding itself, "alias" for an unannotated binding initialized with it or + * with an alias of it (`const alias = xs`), and "container" for an object or + * array literal with it as a property, element or spread source at any + * depth (`{ items: xs }`, `[xs]`), an unannotated binding initialized with + * or destructured from such a literal, and a property or element read of a + * container (`box.items`). The checker types an alias or container by the + * elements written before it was taken, but it holds the same array. Null + * for other expressions. */ +function evolvingArrayHold( + lowerer: Lowerer, + expr: ts.Expression, + depth = 0, +): "evolving" | "alias" | "container" | null { + while (ts.isParenthesizedExpression(expr)) expr = expr.expression; + if (depth >= 8) return null; + const binding = (symbol: ts.Symbol | undefined): "evolving" | "alias" | "container" | null => { + const source = symbol?.valueDeclaration; + // A name destructured from a container (`const { items } = box`) may + // be the array or another container. + if (source !== undefined && ts.isBindingElement(source) && ts.isIdentifier(source.name)) { + const host = hostVariableDeclarationOf(source.name); + return host !== null && + !host.type && + host.initializer !== undefined && + evolvingArrayHold(lowerer, host.initializer, depth + 1) === "container" + ? "container" + : null; + } + if (source === undefined || !ts.isVariableDeclaration(source) || source.type) return null; + const evolved = evolvingArrayType(lowerer, source); + if (evolved !== null) return evolved.kind === "dyn" ? "evolving" : null; + if (source.initializer === undefined) return null; + const held = evolvingArrayHold(lowerer, source.initializer, depth + 1); + return held === "evolving" ? "alias" : held; + }; + if (ts.isIdentifier(expr)) return binding(lowerer.checker.getSymbolAtLocation(expr)); + if (ts.isPropertyAccessExpression(expr) || ts.isElementAccessExpression(expr)) + return evolvingArrayHold(lowerer, expr.expression, depth + 1) === "container" + ? "container" + : null; + // A spread of the array itself (`{ ...xs }`, `[...xs]`) copies its + // elements, as in Node; a spread of a container copies its references. + if (ts.isObjectLiteralExpression(expr)) { + for (const property of expr.properties.nodes) { + const held = ts.isPropertyAssignment(property) + ? evolvingArrayHold(lowerer, property.initializer, depth + 1) + : ts.isShorthandPropertyAssignment(property) + ? binding(lowerer.checker.getShorthandAssignmentValueSymbol(property)) + : ts.isSpreadAssignment(property) && + evolvingArrayHold(lowerer, property.expression, depth + 1) === "container" + ? "container" + : null; + if (held !== null) return "container"; + } + return null; + } + if (ts.isArrayLiteralExpression(expr)) { + for (const element of expr.elements.nodes) { + const held = ts.isSpreadElement(element) + ? evolvingArrayHold(lowerer, element.expression, depth + 1) === "container" + ? "container" + : null + : evolvingArrayHold(lowerer, element, depth + 1); + if (held !== null) return "container"; + } + } + return null; +} + +/** Whether a TypeScript expression is an alias or container of an evolving + * array binding whose reads disagree (evolvingArrayHold). Its value is + * checked-dynamic, and the checker's type for it lists only the elements + * written before it was taken, so element reads through it stay + * checked-dynamic instead of checking against that type. */ +export function readsHeldEvolvingArray(lowerer: Lowerer, expr: ts.Expression): boolean { + if (isJsSourceFile(ts.getSourceFileOfNode(expr))) return false; + const held = evolvingArrayHold(lowerer, expr); + return held === "alias" || held === "container"; +} + +/** Whether a TypeScript object or array literal holds an evolving array + * binding whose reads disagree (evolvingArrayHold), so it builds as a + * checked-dynamic literal that keeps the array. */ +export function holdsEvolvingArray(lowerer: Lowerer, expr: ts.Expression): boolean { + if (isJsSourceFile(ts.getSourceFileOfNode(expr))) return false; + return evolvingArrayHold(lowerer, expr) === "container"; +} + +/** An unannotated binding initialized with an evolving array binding whose + * reads disagree, or with an alias or container of one (evolvingArrayHold): + * it takes the array's checked-dynamic type, so it sees the later writes. + * Null for other bindings. */ +function evolvingArrayHolderType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrType | null { + if (decl.initializer === undefined) return null; + return evolvingArrayHold(lowerer, decl.initializer) === null ? null : DYN; } /** A single static type for an initializer-less JavaScript `let` whose @@ -5359,26 +5777,7 @@ export function lowerVarDecl( : inferredObjectType ? [inferredObjectType] : []; - const expandsObject = - (!decl.type && - !hasJsTypeAnnotation(decl) && - inferredObjectType !== null && - jsOpenObjectType(decl, inferredObjectType, lowerer.shapes, lowerer.unions).kind === "dyn") || - inferredObjectArms.some( - (arm) => arm.kind === "record" && jsObjectBindingExpands(lowerer, decl, arm), - ) || - (isJsSourceFile(ts.getSourceFileOfNode(decl)) && - !decl.type && - !hasJsTypeAnnotation(decl) && - ts.isObjectLiteralExpression(decl.initializer) && - decl.initializer.properties.nodes.some( - (property) => - ts.isMethodDeclaration(property) || - ts.isAccessor(property) || - (ts.isPropertyAssignment(property) && - (ts.isFunctionExpression(property.initializer) || - ts.isArrowFunction(property.initializer))), - )); + const expandsObject = jsDeclarationExpandsObject(lowerer, decl, inferredObjectType); let init: IrExpr; try { init = @@ -5565,7 +5964,7 @@ export function lowerVarDecl( !decl.type && !isJsSourceFile(ts.getSourceFileOfNode(decl)) ) { - const evolved = evolvingArrayType(lowerer, decl); + const evolved = evolvingArrayType(lowerer, decl) ?? evolvingArrayHolderType(lowerer, decl); if (evolved !== null) { type = evolved; init = lowerer.lowerExprExpecting(decl.initializer, evolved); @@ -6089,18 +6488,15 @@ export function hasJsTypeAnnotation(decl: ts.VariableDeclaration): boolean { ); } -/** Inferred JS scalar initializers do not constrain later writes through - * an any-valued callback. Keep those bindings open, including captured writes. */ -export function jsBindingHasOpenWrites(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean { - if ( - !isJsSourceFile(ts.getSourceFileOfNode(decl)) || - decl.type || - hasJsTypeAnnotation(decl) || - !ts.isIdentifier(decl.name) - ) - return false; - const symbol = lowerer.resolveValueSymbol(decl.name); - if (!symbol) return false; +/** The assignments to a declaration's binding in its function (or file), + * `=` and compound alike. Null when the name has no symbol. */ +function jsBindingWrites( + lowerer: Lowerer, + decl: ts.VariableDeclaration, + name: ts.Identifier, +): ts.BinaryExpression[] | null { + const symbol = lowerer.resolveValueSymbol(name); + if (!symbol) return null; let owner: ts.Node = decl; while (owner.parent && !ts.isFunctionLike(owner) && !ts.isSourceFile(owner)) owner = owner.parent; let writes = lowerer.jsBindingWritesByOwner.get(owner); @@ -6121,9 +6517,36 @@ export function jsBindingHasOpenWrites(lowerer: Lowerer, decl: ts.VariableDeclar writes = indexed; lowerer.jsBindingWritesByOwner.set(owner, writes); } - const candidates = (writes.get(decl.name.text) ?? []).filter( + return (writes.get(name.text) ?? []).filter( (node) => lowerer.resolveValueSymbol(node.left as ts.Identifier) === symbol, ); +} + +/** Whether a JavaScript binding always holds a string: the checker types it + * as one, and every write appends to it (`result += part`), which yields a + * string whatever the part is. */ +export function jsBindingAppendsOnly(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean { + if (!ts.isIdentifier(decl.name)) return false; + if (lowerer.mapTypeOf(lowerer.typeOf(decl.name))?.kind !== "string") return false; + const writes = jsBindingWrites(lowerer, decl, decl.name); + return ( + writes !== null && + writes.every((node) => node.operatorToken.kind === ts.SyntaxKind.PlusEqualsToken) + ); +} + +/** Inferred JS scalar initializers do not constrain later writes through + * an any-valued callback. Keep those bindings open, including captured writes. */ +export function jsBindingHasOpenWrites(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean { + if ( + !isJsSourceFile(ts.getSourceFileOfNode(decl)) || + decl.type || + hasJsTypeAnnotation(decl) || + !ts.isIdentifier(decl.name) + ) + return false; + const candidates = jsBindingWrites(lowerer, decl, decl.name); + if (candidates === null) return false; if ( candidates.some( (node) => @@ -6131,6 +6554,18 @@ export function jsBindingHasOpenWrites(lowerer: Lowerer, decl: ts.VariableDeclar ) ) return true; + // A `+=` of a string turns a number or boolean binding into a string + // (`n += "px"`); the checker keeps the declared type. + if ( + candidates.some( + (node) => + node.operatorToken.kind === ts.SyntaxKind.PlusEqualsToken && + (lowerer.typeOf(node.right).flags & ts.TypeFlags.StringLike) !== 0, + ) + ) { + const declared = lowerer.mapTypeOf(lowerer.typeOf(decl.name)); + if (declared?.kind === "f64" || declared?.kind === "bool") return true; + } // A write that can store null or undefined into a binding the checker // types as a primitive (`offset = match[3] || null`): JavaScript files // are checked without strict null checks, so the declared type does not @@ -8974,7 +9409,7 @@ function lowerAssignTargetInto( if (arr.type.kind === "array") { const recv = lowerer.declareHiddenLocal("%dtRecv", arr.type); out.push({ kind: "varDecl", localId: recv.id, init: arr, loc }); - const index = lowerOptionalNumber( + const index = lowerOptionalIndex( lowerer, lowerer.lowerExpr(t.argumentExpression), locOf(t.argumentExpression), @@ -9451,6 +9886,21 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt { ) { return lowerForAwaitStdin(lowerer, stmt); } + // NodeJS.WritableStream is what @types/node answers for pipeline() + // with a function destination, whose value is a PassThrough. + if (nodeStreamInterfaceSide(lowerer, lowerer.typeOf(stmt.expression)) === "w") { + const mapped = lowerer.mapTypeOf(lowerer.typeOf(stmt.expression)); + const sides = + mapped?.kind === "object" + ? streamSidesOf(lowerer, lowerer.classes.get(mapped.className)) + : null; + if (sides !== "r" && sides !== "rw") + lowerer.noLowering( + "for await over a NodeJS.WritableStream", + stmt.expression, + "type the stream as the readable class it is: pipeline() answers a PassThrough for a function destination (pipeline(...) as PassThrough, or a JSDoc @type)", + ); + } // A checked-dynamic NodeJS.ReadableStream iterates its native stream, // and a Hash, Hmac, Cipheriv or Decipheriv iterates its Transform. const dynStream = @@ -9637,6 +10087,10 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt { } iterSrc ??= stmt.expression; let iterable = lowerer.lowerExpr(iterSrc); + { + const iterableUnion = iterableUnionOf(lowerer, iterable.type); + if (iterableUnion) return lowerForOfIterable(lowerer, stmt, iterable, iterableUnion, labels); + } let tupleSource: { shapeId: string; fields: { name: string; type: IrType }[]; @@ -11484,11 +11938,13 @@ function lowerForIn(lowerer: Lowerer, stmt: ts.ForInStatement): IrStmt { keysT, loc, ); + // The overflow map's hashed key set answers presence directly; a + // fresh key list per visit would make the walk quadratic. const guard = (kRef: IrExpr): IrExpr => ({ - kind: "arrIntrinsic", - method: "includes", - receiver: { kind: "recordOvfKeys", obj: recvRef(), shapeId, type: keysT, loc }, - args: [kRef], + kind: "recordOvfHas", + obj: recvRef(), + shapeId, + key: kRef, type: BOOL, loc, }); diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index 6540b3a99..c37ae9074 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -56,6 +56,7 @@ import { bindingNeverReassigned } from "./binding-analysis.js"; import { extractIteratorValue, genStep, genValue, stepDrivable } from "./lower-generators.js"; import { LiftedFrame, driveGenerator } from "./containers/iterator-helpers.js"; import { lowerArrayIteratorValue, lowerStringIteratorValue } from "./iterator-adapters.js"; +import { staticReadableStreamElementType } from "./lower-stdlib.js"; import { BOOL, DYN, @@ -176,10 +177,25 @@ export const UNDERSCORE_METHODS: ReadonlyMap = new Map([ ["flush", "_flush"], ]); +/** The option callbacks applied after construction rather than through + * the constructor's callback slots (option name → method name): construct + * on every stream class, and writev on the writable classes whose + * machinery hands it batches (Writable and Duplex). */ +export const STREAM_EXTRA_CALLBACKS: ReadonlyMap = new Map([ + ["construct", "_construct"], + ["writev", "_writev"], +]); + +export function streamExtraCallbacks(cls: string): readonly string[] { + return cls === "%Writable" || cls === "%Duplex" ? ["construct", "writev"] : ["construct"]; +} + /** The completion callback's parameter position in each underscore * method that takes one (cbTuples' "fn" positions). */ export const STREAM_COMPLETION_CALLBACK_INDEX: ReadonlyMap = new Map([ ["_write", 2], + ["_writev", 1], + ["_construct", 0], ["_final", 0], ["_destroy", 1], ["_transform", 2], @@ -317,9 +333,11 @@ function lowerStreamCallback( // (`string | null | undefined`) converts from the encoding argument's // value. if ( - ((i === 0 && (chunkValues || widensInto(lowerer, fullTuple[0]!, funcType.params[0]!, loc))) || + ((((i === 0 && + (chunkValues || widensInto(lowerer, fullTuple[0]!, funcType.params[0]!, loc))) || i === 1) && - (which === "write" || which === "transform") && + (which === "write" || which === "transform")) || + (i === 0 && which === "writev")) && funcType.params[i]!.kind !== "func" && canConvertToDyn( funcType.params[i]!, @@ -429,6 +447,16 @@ interface StreamOptions { * right after construction. */ readableObject: boolean; writableObject: boolean; + /** decodeStrings: false (string writes stay strings), applied with + * stream.decodeStrings right after construction. */ + decodeStringsOff: boolean; + /** A writable side's non-utf8 `defaultEncoding` as written, applied + * with writable.writeEncoding. */ + writeEncoding: string | null; + /** The construct and writev callbacks (options or the class's + * _construct/_writev), applied with stream.setWritev and + * stream.setConstruct right after construction. */ + extras: { name: string; cb: IrExpr }[]; /** Present callbacks in canonical order with their flag bits. */ flags: number; cbs: IrExpr[]; @@ -470,14 +498,19 @@ function streamObjectModes( return out; } -/** Whether an options literal sets the writable side's object mode with a - * `true` literal (objectMode, or writableObjectMode on a duplex stream). - * Reports nothing: streamObjectModes fences the other value forms. */ +/** Whether an options literal makes the writable side hold values: its + * object mode set with a `true` literal (objectMode, or writableObjectMode + * on a duplex stream), or decodeStrings: false (strings stay strings). + * Reports nothing: the options parse fences the other value forms. */ function literalWritesValues(opts: ts.ObjectLiteralExpression, duplexish: boolean): boolean { for (const prop of opts.properties?.nodes ?? []) { if (!ts.isPropertyAssignment(prop)) continue; const name = ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name) ? prop.name.text : null; + if (name === "decodeStrings") { + if (prop.initializer.kind !== ts.SyntaxKind.TrueKeyword) return true; + continue; + } if (name !== "objectMode" && !(duplexish && name === "writableObjectMode")) continue; if (prop.initializer.kind !== ts.SyntaxKind.FalseKeyword) return true; } @@ -494,6 +527,8 @@ function cbTuples(lowerer: Lowerer): Record { read: [F64], write: [BYTES, STRING, "fn"], final: ["fn"], + construct: ["fn"], + writev: [DYN, "fn"], destroy: [errorOrNull(lowerer), "fn"], transform: [BYTES, STRING, "fn"], flush: ["fn"], @@ -734,9 +769,14 @@ function parseStreamOptions( signal: null, readableObject: false, writableObject: false, + decodeStringsOff: false, + writeEncoding: null, + extras: [], flags: 0, cbs: [], }; + const extraNames = streamExtraCallbacks(`%${ctorName}`); + const writableClass = ctorName !== "Readable"; const followed = followOptionsArg(lowerer, arg); const optsLit = followed === null ? undefined : followed; if (optsLit !== undefined && !ts.isObjectLiteralExpression(optsLit)) { @@ -756,6 +796,12 @@ function parseStreamOptions( ); out.readableObject = objectModes.readable; out.writableObject = objectModes.writable; + // the chunk an option callback receives can be another value than a + // Buffer (literalWritesValues), read before any callback lowers + const writesValues = + optsLit !== undefined && + ts.isObjectLiteralExpression(optsLit) && + literalWritesValues(optsLit, ctorName !== "Readable" && ctorName !== "Writable"); if (out.readableObject) out.hwmR = numLit(16, loc); if (out.writableObject) out.hwmW = numLit(16, loc); const tuples = cbTuples(lowerer); @@ -772,7 +818,7 @@ function parseStreamOptions( lowerer.unsupported("SC1090", prop, `computed or spread members in ${ctorName} options`); } // Callback members: shorthand methods and function-valued properties. - if (accepted.includes(name)) { + if (accepted.includes(name) || extraNames.includes(name)) { let fnNode: ts.MethodDeclaration | ts.FunctionExpression | ts.ArrowFunction | null = null; if (ts.isMethodDeclaration(prop)) fnNode = prop; else if ( @@ -823,7 +869,7 @@ function parseStreamOptions( }); present.set( name, - lowerStreamCallback(lowerer, name, fnNode, thisType, tuple, out.writableObject), + lowerStreamCallback(lowerer, name, fnNode, thisType, tuple, writesValues), ); } finally { if (pinned) lowerer.stageSourceTypes.delete(pinned); @@ -872,11 +918,17 @@ function parseStreamOptions( // read up front (streamObjectModes) break; case "decodeStrings": { + // WritableState's `options.decodeStrings === false`; a Readable + // ignores the option if (value.kind === ts.SyntaxKind.TrueKeyword) break; // Node's default + if (value.kind === ts.SyntaxKind.FalseKeyword) { + if (writableClass) out.decodeStringsOff = true; + break; + } lowerer.noLowering( - `${ctorName} with decodeStrings: false`, + `${ctorName} with a runtime 'decodeStrings' value`, prop, - "chunks always decode to Buffers here (Node's default)", + "write 'decodeStrings: true' or 'decodeStrings: false'", ); break; } @@ -911,15 +963,20 @@ function parseStreamOptions( out.badEncoding = t.value; break; } - if (t.isStringLiteralType() && ctorName === "Readable") { - out.pushEncoding = knownBufEncoding(t.value)!; + if (t.isStringLiteralType()) { + // the readable side decodes push(string) with it, and the + // writable side encodes write(string) with it (the name as + // written reaches _write when decodeStrings is false) + if (ctorName !== "Writable" && !out.readableObject) + out.pushEncoding = knownBufEncoding(t.value)!; + if (writableClass) out.writeEncoding = t.value; break; } } lowerer.noLowering( - `the ${ctorName} option 'defaultEncoding'`, + `the ${ctorName} option 'defaultEncoding' with a runtime value`, prop, - "utf8 (the default) is the supported write-side string encoding", + "write the encoding as a string literal", ); break; case "signal": { @@ -980,18 +1037,13 @@ function parseStreamOptions( ); break; } - case "construct": - lowerer.noLowering( - `the ${ctorName} option 'construct'`, - prop, - "deferred construction (_construct) is not lowered yet", - ); - break; case "writev": lowerer.noLowering( `the ${ctorName} option 'writev'`, prop, - "batched writes (_writev) are not lowered yet — writes deliver one chunk at a time", + ctorName === "Readable" + ? "a Readable has no writable side" + : "batched writes on a Transform would bypass its transform — writev is supported on Writable and Duplex", ); break; default: @@ -1008,6 +1060,10 @@ function parseStreamOptions( out.cbs.push(cb); } } + for (const name of extraNames) { + const cb = present.get(name) ?? fallbackCb?.(name) ?? null; + if (cb) out.extras.push({ name, cb }); + } return out; } @@ -1029,6 +1085,9 @@ function streamCtorArgs( signal: IrExpr | null; readableObject: boolean; writableObject: boolean; + decodeStringsOff: boolean; + writeEncoding: string | null; + extras: { name: string; cb: IrExpr }[]; } { const shape = streamCtorShape(cls); const o = parseStreamOptions( @@ -1060,9 +1119,72 @@ function streamCtorArgs( signal: o.signal, readableObject: o.readableObject, writableObject: o.writableObject, + decodeStringsOff: o.decodeStringsOff, + writeEncoding: o.writeEncoding, + extras: o.extras, }; } +/** The follow-ups a stream's options apply over the fresh stream `recv` + * after the chained ones: decodeStrings, the writable defaultEncoding, + * then writev and construct (Node's constructors schedule construct + * last). */ +function streamOptionFollowUps( + parsed: { + decodeStringsOff: boolean; + writeEncoding: string | null; + extras: { name: string; cb: IrExpr }[]; + }, + recv: () => IrExpr, + thisType: IrType, + loc: SrcLoc, + writevFn: IrLibFn = "stream.setWritev", +): IrStmt[] { + const out: IrStmt[] = []; + if (parsed.decodeStringsOff) { + out.push({ + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "stream.decodeStrings", + args: [recv(), boolLit(false, loc)], + type: thisType, + loc, + }, + loc, + }); + } + if (parsed.writeEncoding !== null) { + out.push({ + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "writable.writeEncoding", + args: [recv(), strLit(parsed.writeEncoding, loc)], + type: thisType, + loc, + }, + loc, + }); + } + for (const name of ["writev", "construct"]) { + const extra = parsed.extras.find((x) => x.name === name); + if (!extra) continue; + out.push({ + kind: "exprStmt", + expr: { + kind: "libCall", + fn: name === "writev" ? writevFn : "stream.setConstruct", + args: [recv(), extra.cb], + type: VOID, + loc, + }, + loc, + }); + } + return out; +} + /** `new Readable(opts?)` and friends — called from lowerNew once the * callee resolved to a builtin stream class. */ export function lowerStreamNew(lowerer: Lowerer, expr: ts.NewExpression, info: ClassInfo): IrExpr { @@ -1137,12 +1259,29 @@ export function lowerStreamNew(lowerer: Lowerer, expr: ts.NewExpression, info: C loc, }; } - if (parsed.signal === null) return built; - // { signal }: addAbortSignal over the fresh stream (answers it +1). + if (parsed.signal !== null) { + // { signal }: addAbortSignal over the fresh stream (answers it +1). + built = { + kind: "libCall", + fn: "stream.addAbortSignal", + args: [built, parsed.signal], + type: thisType, + loc, + }; + } + if (!parsed.decodeStringsOff && parsed.writeEncoding === null && parsed.extras.length === 0) + return built; + const fresh = lowerer.declareHiddenLocal("%stream", thisType); + const followUps = streamOptionFollowUps( + parsed, + () => varRef(fresh.id, thisType, loc), + thisType, + loc, + ); return { - kind: "libCall", - fn: "stream.addAbortSignal", - args: [built, parsed.signal], + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: fresh.id, init: built, loc }, ...followUps], + result: varRef(fresh.id, thisType, loc), type: thisType, loc, }; @@ -1236,10 +1375,12 @@ function streamMethodWrapper( } else if (!typeEquals(got.type, want)) { // A write or transform encoding of another type (`string | null | // undefined`) converts from the encoding argument's value: an - // object-mode write passes undefined, null or the name. + // object-mode write passes undefined, null or the name. A chunk of + // another type (`string | Buffer` under decodeStrings: false) + // converts from the chunk's value. if ( - i === 1 && - (which === "write" || which === "transform") && + (i === 1 || (i === 0 && (chunkValues || which === "writev"))) && + (which === "write" || which === "transform" || which === "writev") && got.type.kind !== "func" && canConvertToDyn( got.type, @@ -1262,8 +1403,7 @@ function streamMethodWrapper( // or throws a TypeError there; a byte-mode side's chunk is a Buffer. if ( i === 0 && - chunkValues && - (which === "write" || which === "transform") && + (which === "writev" || (chunkValues && (which === "write" || which === "transform"))) && got.type.kind !== "func" && canConvertToDyn( got.type, @@ -1380,7 +1520,7 @@ export function lowerStreamSuperCall( (!ts.isObjectLiteralExpression(optsNode) || literalWritesValues(optsNode, cls !== "%Readable" && cls !== "%Writable")); const fallback = (name: string): IrExpr | null => { - const methodName = UNDERSCORE_METHODS.get(name); + const methodName = UNDERSCORE_METHODS.get(name) ?? STREAM_EXTRA_CALLBACKS.get(name); return methodName ? streamMethodWrapper(lowerer, info, name, methodName, thisType, loc, blame, chunkValues) : null; @@ -1401,6 +1541,13 @@ export function lowerStreamSuperCall( cbs.push(cb); } } + // The class's _writev and _construct ride the same way: an options + // writev or construct (applied by initDyn) wins. + const extras: { name: string; cb: IrExpr }[] = []; + for (const name of streamExtraCallbacks(cls)) { + const cb = fallback(name); + if (cb) extras.push({ name, cb }); + } return [ { kind: "exprStmt", @@ -1413,6 +1560,13 @@ export function lowerStreamSuperCall( }, loc, }, + ...streamOptionFollowUps( + { decodeStringsOff: false, writeEncoding: null, extras }, + thisRef, + thisType, + loc, + "stream.setWritevDefault", + ), ]; } } @@ -1499,6 +1653,7 @@ export function lowerStreamSuperCall( loc, }); } + out.push(...streamOptionFollowUps(parsed, thisRef, thisType, loc)); return out; } @@ -1542,18 +1697,18 @@ export function lowerStreamUnderscoreAssign( const loc = locOf(expr); const cls = base.def.name; const clsName = cls.slice(1); - if (methodName === "_writev" || methodName === "_construct") { + if (methodName === "_construct") { + // Node's constructor reads _construct once; a later assignment has no + // effect there lowerer.noLowering( `assigning '${methodName}' on a ${clsName}`, expr.left, - methodName === "_writev" - ? "batched writes (_writev) are not lowered yet — writes deliver one chunk at a time" - : "deferred construction (_construct) is not lowered yet", + "declare _construct on the class or pass the construct option", ); } - const which = optionByMethod.get(methodName)!; + const which = methodName === "_writev" ? "writev" : optionByMethod.get(methodName)!; const { accepted } = streamCtorShape(cls); - if (!accepted.includes(which)) { + if (!accepted.includes(which) && !streamExtraCallbacks(cls).includes(which)) { lowerer.noLowering( `assigning '${methodName}' on a ${clsName}`, expr.left, @@ -1589,6 +1744,7 @@ export function lowerStreamUnderscoreAssign( destroy: "stream.setDestroy", transform: "stream.setTransform", flush: "stream.setFlush", + writev: "stream.setWritev", }; return { kind: "exprStmt", @@ -1658,11 +1814,27 @@ export function lowerStreamStaticCall( } } } + if (member === "fromWeb" && cls === "%Readable") return lowerReadableFromWeb(lowerer, call, loc); if (member !== "from") { lowerer.noLowering(`${cls.slice(1)}.${member}`, call); } + if (cls === "%Duplex") { + const fromArgs = call.arguments.nodes; + if (fromArgs.length !== 1) { + lowerer.noLowering( + `Duplex.from with ${fromArgs.length} arguments`, + call, + "Duplex.from takes one body", + ); + } + return duplexFromBody(lowerer, fromArgs[0]!, locOf(call)); + } if (cls !== "%Readable") { - lowerer.noLowering(`${cls.slice(1)}.from`, call, "Readable.from is the lowered form"); + lowerer.noLowering( + `${cls.slice(1)}.from`, + call, + "Readable.from and Duplex.from are the lowered forms", + ); } const args = call.arguments.nodes; if (args.length > 2) { @@ -1672,16 +1844,184 @@ export function lowerStreamStaticCall( "Readable.from(iterable, options) is the supported form", ); } - const type: IrType = { kind: "object", className: "%Readable" }; + return lowerFromSource(lowerer, call, args[0], args[1], false); +} + +/** Readable.fromWeb(readableStream, options): Node's adapter over the web + * stream's reader (scr_stream_from_web.c). The options are an object + * literal: objectMode a boolean literal, highWaterMark a number, encoding + * a literal name, and signal a variable or property read, applied with + * addAbortSignal as the constructors do. */ +function lowerReadableFromWeb(lowerer: Lowerer, call: ts.CallExpression, loc: SrcLoc): IrExpr { + const args = call.arguments.nodes; + const readable: IrType = { kind: "object", className: "%Readable" }; + if (args.length === 0 || args.length > 2 || args.some((a) => ts.isSpreadElement(a))) { + lowerer.noLowering( + `Readable.fromWeb with ${args.length} arguments`, + call, + "the supported form is Readable.fromWeb(readableStream, options)", + ); + } + const web = lowerer.lowerExprExpecting(args[0]!, DYN); + let objectMode = false; + let hwm: IrExpr = numLit(-1, loc); + let encoding = ""; + let signal: IrExpr | null = null; + const options = args[1]; + if (options !== undefined) { + if (!ts.isObjectLiteralExpression(options)) { + lowerer.noLowering( + "Readable.fromWeb options that are not an object literal", + options, + "pass { objectMode, highWaterMark, encoding, signal } as an object literal", + ); + } + for (const p of options.properties.nodes) { + const key = + (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) && + ts.isIdentifier(p.name) + ? p.name.text + : null; + const value = + key === null + ? null + : ts.isShorthandPropertyAssignment(p) + ? (p.name as ts.Identifier) + : (p as ts.PropertyAssignment).initializer; + if (key === "objectMode" && value) { + if (value.kind === ts.SyntaxKind.TrueKeyword) objectMode = true; + else if (value.kind !== ts.SyntaxKind.FalseKeyword) + lowerer.noLowering( + "the Readable.fromWeb option 'objectMode' with this value", + p, + "write true or false", + ); + continue; + } + if (key === "highWaterMark" && value) { + hwm = lowerer.lowerExprExpecting(value, F64); + continue; + } + if (key === "encoding" && value) { + const t = lowerer.typeOf(value); + if (t.isStringLiteralType() && knownBufEncoding(t.value) === undefined) { + return nodeThrowExpr( + 1, + "ERR_INVALID_ARG_VALUE", + `The property 'options.encoding' is invalid. Received '${t.value}'`, + readable, + loc, + ); + } + encoding = bufEncoding(lowerer, "the Readable.fromWeb option 'encoding'", value); + continue; + } + if (key === "signal" && value) { + if (!sideEffectFreeOptionValue(value) && !pureRead(value)) { + lowerer.noLowering( + "the Readable.fromWeb option 'signal' with this value", + p, + "pass a variable or property read (const signal = controller.signal)", + ); + } + signal = lowerer.lowerExprExpecting(value, DYN); + continue; + } + lowerer.noLowering( + key === null + ? "Readable.fromWeb options with this member" + : `the Readable.fromWeb option '${key}'`, + p, + "objectMode, highWaterMark, encoding and signal are the Readable.fromWeb options", + ); + } + } + const built: IrExpr = { + kind: "libCall", + fn: "fetch.readableFromWeb", + args: [web, boolLit(objectMode, loc), hwm, strLit(encoding, loc)], + type: readable, + loc, + }; + if (signal === null) return built; + return { + kind: "libCall", + fn: "stream.addAbortSignal", + args: [built, signal], + type: readable, + loc, + }; +} + +/** Readable.from(source, options), and for Duplex.from over an iterable + * (duplex) duplexify.js's from(Duplexify, body, { objectMode: true, + * writable: false }): the same builders with SCR_FROM_DUPLEX, answering a + * Duplexify without a writable side. */ +function lowerFromSource( + lowerer: Lowerer, + at: ts.Node, + srcNode: ts.Expression | undefined, + optsNode: ts.Expression | undefined, + duplex: boolean, +): IrExpr { + const loc = locOf(at); + const call = at; + const type: IrType = { kind: "object", className: duplex ? "%Duplex" : "%Readable" }; + const what = duplex ? "Duplex.from" : "Readable.from"; + // SCR_FROM_DUPLEX + const duplexFlag = duplex ? 32 : 0; const js = isJsSourceFile(ts.getSourceFileOfNode(call)); - const srcNode = args[0]; - const optsNode = args[1]; + // A web ReadableStream is async iterable: from.js reads its iterator. + // Duplex.from takes _duplexify({ readable: Readable.fromWeb(body) }). + const webElem = srcNode ? staticReadableStreamElementType(lowerer, srcNode) : null; + if (srcNode && webElem !== null && duplex) { + const fromWeb: IrExpr = { + kind: "libCall", + fn: "fetch.readableFromWeb", + args: [ + lowerer.lowerExprExpecting(srcNode, DYN), + boolLit(false, loc), + numLit(-1, loc), + strLit("", loc), + ], + type: { kind: "object", className: "%Readable" }, + loc, + }; + return { kind: "libCall", fn: "stream.duplexifyReadable", args: [fromWeb], type, loc }; + } + if (srcNode && webElem !== null) { + const source = lowerWebStreamSource(lowerer, srcNode, webElem, loc); + const helpers = source ? fromGenHelpers(lowerer, source.out, loc) : null; + const opts = optsNode ? fromOptions(lowerer, optsNode) : { flags: 0, hwm: null }; + if (!source || !helpers || !opts || !fromGenChunk(lowerer, webElem)) { + lowerer.noLowering( + `Readable.from over a web ReadableStream of '${lowerer.fmt(webElem)}'`, + srcNode, + ); + } + const hwm = opts!.hwm ? lowerer.lowerExprExpecting(opts!.hwm, F64) : numLit(0, loc); + const gen: IrExpr = { + kind: "callValue", + callee: source!.closure, + args: [], + type: source!.out, + loc, + }; + return { + kind: "libCall", + fn: "readable.fromAsyncGen", + args: [gen, ...helpers!, numLit(opts!.flags, loc), hwm], + type, + loc, + }; + } const srcT = srcNode ? lowerer.mapTypeOf(lowerer.typeOf(srcNode)) : null; const typedElem = (t: IrType | null | undefined): "string" | "bytes" | null => t?.kind === "string" ? "string" : t?.kind === "bytes" && t.elem === "u8" ? "bytes" : null; const single = typedElem(srcT); const elem = srcT?.kind === "array" ? typedElem(srcT.elem) : null; - const opts = optsNode ? fromOptions(lowerer, optsNode) : { flags: 0, hwm: null }; + const opts0 = optsNode ? fromOptions(lowerer, optsNode) : { flags: 0, hwm: null }; + const opts = opts0 ? { ...opts0, flags: opts0.flags | duplexFlag } : opts0; // A class whose [Symbol.iterator]() or [Symbol.asyncIterator]() is a // generator method: from.js calls the method when the stream is built, // then reads the generator like any other. @@ -1779,6 +2119,7 @@ export function lowerStreamStaticCall( // The checked-dynamic form: the runtime takes from.js's ladder // (ERR_INVALID_ARG_TYPE for a non-iterable, the options, null values). const src = srcNode ? lowerer.lowerExprExpecting(srcNode, DYN) : dynUndefinedExpr(loc); + if (duplex) return { kind: "libCall", fn: "stream.duplexFromDyn", args: [src], type, loc }; const o = lowerOperatorValue(lowerer, optsNode, "Readable.from options", loc, true); return { kind: "libCall", fn: "readable.fromDyn", args: [src, o], type, loc }; } @@ -1790,7 +2131,7 @@ export function lowerStreamStaticCall( ); } lowerer.noLowering( - `Readable.from over a '${srcT ? lowerer.fmt(srcT) : "?"}' source`, + `${what} over a '${srcT ? lowerer.fmt(srcT) : "?"}' source`, srcNode ?? call, "the supported sources are a string or Buffer, and arrays, Sets, Maps, generators and class iterables of values that convert to checked-dynamic values", ); @@ -2607,22 +2948,28 @@ export function lowerStreamModuleCall( : null; const stageArgs = opts ? args.slice(0, -1) : args; if (hasPipelineFunctionStage(lowerer, stageArgs)) { - if (opts) { - lowerer.noLowering( - "pipeline with function stages or iterable sources and options", - lastArg, - "pipeline(source, async function* (source) { ... }, destination) without options is supported", - ); - } const staged = lowerPipelineStages(lowerer, stageArgs, loc); + const type = staged.destPromise ?? { kind: "promise", inner: VOID }; // A promise destination's value settles the result (same type). - return staged.wrap({ - kind: "libCall", - fn: "sp.pipelineStages", - args: staged.args, - type: staged.destPromise ?? { kind: "promise", inner: VOID }, - loc, - }); + if (!opts) { + return staged.wrap({ + kind: "libCall", + fn: "sp.pipelineStages", + args: staged.args, + type, + loc, + }); + } + // the options: end, and the signal (flags bit 6) + const flagsArg = staged.args[3]!; + if (flagsArg.kind !== "numLit") + throw new InternalCompilerError("pipeline flags not a literal"); + const args = [...staged.args, boolLit(opts.end, loc)]; + if (opts.signal) { + args[3] = numLit(flagsArg.value | 64, loc); + args.push(opts.signal); + } + return staged.wrap({ kind: "libCall", fn: "sp.pipelineStagesOpts", args, type, loc }); } let streams: IrExpr[]; try { @@ -2803,22 +3150,27 @@ export function lowerStreamModuleCall( } if (hasPipelineFunctionStage(lowerer, args.slice(0, -1))) { const staged = lowerPipelineStages(lowerer, args.slice(0, -1), loc); - if (staged.dest !== 0) { - // callback(err, value): the destination's value reaches the - // callback through the checked-dynamic path. Node answers an - // object-mode PassThrough here; reading it has no lowering. - if (!ts.isExpressionStatement(call.parent)) { - lowerer.noLowering( - "using the PassThrough that pipeline() answers for a function destination", - call, - "read the destination's value in the callback", - ); - } + const cbNode = args[args.length - 1]!; + const cbParams = + ts.isArrowFunction(cbNode) || ts.isFunctionExpression(cbNode) + ? cbNode.parameters.nodes.length + : 0; + if (staged.dest === 2 || (staged.dest === 1 && cbParams > 1)) { + // callback(err, value): the destination's value (undefined for + // an async generator) reaches the callback through the + // checked-dynamic path. const cb = lowerer.lowerExprExpecting(args[args.length - 1]!, DYN); + // SCR_PIPE_TYPED_CB: typed code's callback takes null for no error + const stagedArgs = [...staged.args]; + const flagsArg = stagedArgs[3]!; + if (flagsArg.kind !== "numLit") + throw new InternalCompilerError("pipeline flags not a literal"); + if (!isJsSourceFile(ts.getSourceFileOfNode(call))) + stagedArgs[3] = numLit(flagsArg.value | 128, loc); return staged.wrap({ kind: "libCall", fn: "stream.pipelineStagesDyn", - args: [...staged.args, cb], + args: [...stagedArgs, cb], type: staged.last, loc, }); @@ -2855,7 +3207,12 @@ export function lowerStreamModuleCall( }; } - if (bi.member === "compose") return lowerStreamCompose(lowerer, call, loc); + if (bi.member === "compose") { + if (call.arguments.nodes.some((a) => ts.isSpreadElement(a))) { + lowerer.noLowering("compose with spread arguments", call, "pass the streams directly"); + } + return lowerStreamCompose(lowerer, call.arguments.nodes, loc); + } return null; } @@ -2914,6 +3271,8 @@ type SyncGenType = IrType & { kind: "generator" }; * array, a string or a synchronous generator object), or null. */ function iterableSourceType(lowerer: Lowerer, node: ts.Expression): IrType | null { if (pipelineStageFunction(lowerer, node) !== null) return null; + const webElem = staticReadableStreamElementType(lowerer, node); + if (webElem !== null) return webElem; let t: IrType | null; try { t = lowerer.mapTypeOf(lowerer.typeOf(node)); @@ -2933,12 +3292,245 @@ function iterableSourceType(lowerer: Lowerer, node: ts.Expression): IrType | nul * through an async-from-sync iterator, and an async generator that * yields the same elements resumes its consumer on the same ticks. Null * when the source has no such lowering. */ +/** A web ReadableStream source, as an async generator closure over its + * reader: each pass awaits reader.read() and yields the chunk, as the + * stream's async iterator does; leaving early cancels the stream (its + * iterator's return()), and the reader's lock is released either way. */ +function lowerWebStreamSource( + lowerer: Lowerer, + node: ts.Expression, + elemT: IrType, + loc: SrcLoc, +): { closure: IrExpr; out: SyncGenType } | null { + const resultT = genResultRecord(elemT, VOID, lowerer.shapes, lowerer.unions); + if (!resultT) return null; + const valueField = lowerer.shapes.get(resultT.shapeId)?.fields.find((f) => f.name === "value"); + if (!valueField) return null; + const out: SyncGenType = { + kind: "generator", + async: true, + yieldT: elemT, + retT: VOID, + nextT: UNDEFINED_T, + }; + const genResultT = genResultRecord(elemT, VOID, lowerer.shapes, lowerer.unions); + if (!genResultT) return null; + const value = lowerer.lowerExprExpecting(node, DYN); + const key = `webStreamSource:${typeKey(elemT)}`; + let maker = lowerer.valueHelpers.get(key); + if (!maker) { + maker = `%stream.webStreamSource.${lowerer.valueHelpers.size}`; + const genName = `${maker}.gen`; + const frame = new LiftedFrame(loc); + const srcId = frame.local("src", DYN, false); + const readerId = frame.local("reader", DYN, false); + const activeId = frame.local("active", BOOL, true); + const promiseT: IrType = { kind: "promise", inner: resultT }; + const pId = frame.local("p", promiseT, false); + const rId = frame.local("r", resultT, false); + const reader = (): IrExpr => varRef(readerId, DYN, loc); + const r = (): IrExpr => varRef(rId, resultT, loc); + const setActive = (on: boolean): IrStmt => ({ + kind: "assign", + localId: activeId, + value: { kind: "boolLit", value: on, type: BOOL, loc }, + loc, + }); + const extracted = extractIteratorValue( + lowerer, + elemT, + valueField.type, + { + kind: "recordGet", + obj: r(), + shapeId: resultT.shapeId, + field: "value", + type: valueField.type, + loc, + }, + loc, + ); + if (!extracted) return null; + const invoke = (method: string, args: IrExpr[], calleeName: string): IrExpr => ({ + kind: "dynInvoke", + recv: reader(), + method, + calleeName, + args, + type: DYN, + loc, + }); + const body: IrStmt[] = [ + { + kind: "varDecl", + localId: readerId, + init: { + kind: "dynInvoke", + recv: varRef(srcId, DYN, loc), + method: "getReader", + calleeName: "ReadableStream async iterator", + args: [], + type: DYN, + loc, + }, + loc, + }, + { + kind: "varDecl", + localId: activeId, + init: { kind: "boolLit", value: false, type: BOOL, loc }, + loc, + }, + { + kind: "tryCatch", + tryBody: [ + { + kind: "while", + cond: { kind: "boolLit", value: true, type: BOOL, loc }, + body: [ + setActive(false), + { + kind: "varDecl", + localId: pId, + init: { + kind: "libCall", + fn: "fetch.readerRead", + args: [reader()], + type: promiseT, + loc, + }, + loc, + }, + { + kind: "varDecl", + localId: rId, + init: { kind: "awaitExpr", value: varRef(pId, promiseT, loc), type: resultT, loc }, + loc, + }, + { + kind: "if", + cond: { + kind: "recordGet", + obj: r(), + shapeId: resultT.shapeId, + field: "done", + type: BOOL, + loc, + }, + then: [{ kind: "break", loc }], + else_: null, + loc, + }, + setActive(true), + { + kind: "exprStmt", + expr: { kind: "yieldExpr", value: extracted, type: VOID, loc }, + loc, + }, + ], + loc, + }, + ], + catchBody: null, + catchLocalId: null, + finallyBody: [ + { + kind: "tryCatch", + tryBody: [ + { + kind: "if", + cond: varRef(activeId, BOOL, loc), + then: [ + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "async.awaitDyn", + args: [ + invoke( + "cancel", + [dynUndefinedExpr(loc)], + "ReadableStream async iterator return", + ), + ], + type: DYN, + loc, + }, + loc, + }, + ], + else_: null, + loc, + }, + ], + catchBody: null, + catchLocalId: null, + finallyBody: [ + { + kind: "exprStmt", + expr: invoke("releaseLock", [], "ReadableStream async iterator releaseLock"), + loc, + }, + ], + loc, + }, + ], + loc, + }, + { kind: "return", value: null, loc }, + ]; + const srcLocal = frame.locals.find((l) => l.id === srcId)!; + srcLocal.boxed = true; + lowerer.valueHelpers.set(key, maker); + lowerer.liftedFns.push({ + name: genName, + params: [], + returnType: VOID, + async: true, + captures: [{ localId: srcId, name: "src", type: DYN }], + locals: frame.locals, + body, + generator: { yieldT: elemT, nextT: UNDEFINED_T, resultType: genResultT }, + loc, + }); + const makerSrc: IrLocal = { id: "src.0", name: "src", type: DYN, mutable: false, boxed: true }; + lowerer.liftedFns.push({ + name: maker, + params: [{ localId: makerSrc.id, name: makerSrc.name, type: DYN }], + returnType: funcOf([], out), + locals: [makerSrc], + body: [ + { + kind: "return", + value: { + kind: "closure", + fnName: genName, + captures: [makerSrc.id], + type: funcOf([], out), + loc, + }, + loc, + }, + ], + loc, + }); + } + return { + closure: { kind: "call", callee: maker, args: [value], type: funcOf([], out), loc }, + out, + }; +} + function lowerIterableSource( lowerer: Lowerer, node: ts.Expression, fn: StageFunction | null, loc: SrcLoc, ): { closure: IrExpr; out: SyncGenType } | null { + if (fn === null) { + const webElem = staticReadableStreamElementType(lowerer, node); + if (webElem !== null) return lowerWebStreamSource(lowerer, node, webElem, loc); + } let value: IrExpr; if (fn !== null) { if (fn.parameters.nodes.length > 0) { @@ -3072,7 +3664,7 @@ function lowerIterableSource( interface PipelineStagesLowering { args: IrExpr[]; last: IrType; - dest: 0 | 2; + dest: 0 | 1 | 2; destPromise: (IrType & { kind: "promise" }) | null; wrap: (call: IrExpr) => IrExpr; } @@ -3080,9 +3672,10 @@ interface PipelineStagesLowering { /** pipeline(...) with function stages (pipeline.js). A function may * start the pipeline (a source function, called with `{ signal }`), may * end it (an async function whose promise's value reaches the callback or - * the promise), and runs of async generator functions may sit between - * streams; the last generator of a run that a stream follows is pumped - * into that stream. Each stage's `source` parameter is typed as what the + * the promise, or an async generator function), and runs of async + * generator functions may sit between streams; the last generator of a + * run that a stream follows is pumped into that stream. A function + * destination feeds the object-mode PassThrough that pipeline() answers. Each stage's `source` parameter is typed as what the * pipeline passes it: makeAsyncIterable answers the previous stream * itself (a Readable is async iterable) or the previous stage's * generator. */ @@ -3090,6 +3683,7 @@ function lowerPipelineStages( lowerer: Lowerer, stageArgs: readonly ts.Expression[], loc: SrcLoc, + preLowered?: ReadonlyMap, ): PipelineStagesLowering { if (stageArgs.length > 52) { lowerer.noLowering( @@ -3103,7 +3697,7 @@ function lowerPipelineStages( const helperArgs: IrExpr[] = []; let positions = 0; let mask = 0; - let dest: 0 | 2 = 0; + let dest: 0 | 1 | 2 = 0; let destPromise: (IrType & { kind: "promise" }) | null = null; // What the next function stage receives: null before the first stage, // else the previous stream's type or the previous stage's generator. @@ -3114,14 +3708,19 @@ function lowerPipelineStages( const argOrder: { list: IrExpr[]; index: number }[] = []; for (let j = 0; j < stageArgs.length; j++) { const arg = stageArgs[j]!; - const fn = pipelineStageFunction(lowerer, arg); + // compose's head and tail functions arrive as Duplex.from streams + const fn = preLowered?.has(j) ? null : pipelineStageFunction(lowerer, arg); const syncSourceFn = j === 0 && fn !== null && !ts.isArrowFunction(fn) && fn.asteriskToken !== undefined && !(fn.modifiers?.nodes ?? []).some((m) => m.kind === ts.SyntaxKind.AsyncKeyword); - if (j === 0 && (syncSourceFn || (fn === null && iterableSourceType(lowerer, arg) !== null))) { + if ( + j === 0 && + !preLowered?.has(j) && + (syncSourceFn || (fn === null && iterableSourceType(lowerer, arg) !== null)) + ) { const source = lowerIterableSource(lowerer, arg, syncSourceFn ? fn : null, loc); if (source === null) lowerer.noLowering("pipeline over this source", arg); positions += 1; @@ -3147,7 +3746,7 @@ function lowerPipelineStages( } let stream: IrExpr; try { - stream = lowerStreamArg(lowerer, arg, "pipeline"); + stream = preLowered?.get(j) ?? lowerStreamArg(lowerer, arg, "pipeline"); } catch (e) { if (e instanceof StreamArgTypeThrow) lowerer.noLowering("pipeline over this value", arg); throw e; @@ -3160,15 +3759,6 @@ function lowerPipelineStages( const isLast = j === stageArgs.length - 1; const isAsync = (fn.modifiers?.nodes ?? []).some((m) => m.kind === ts.SyntaxKind.AsyncKeyword); const isGen = !ts.isArrowFunction(fn) && fn.asteriskToken !== undefined; - if (isLast && isGen) { - // pipeline() pumps it into an object-mode PassThrough, whose - // buffering decides when the pipeline finishes - lowerer.noLowering( - "a pipeline destination that is an async generator function", - arg, - "end the pipeline with a stream or an async function (source) { for await (const chunk of source) ... }", - ); - } if (!isAsync || (!isGen && !isLast)) { lowerer.noLowering( isLast @@ -3207,6 +3797,14 @@ function lowerPipelineStages( dest = 2; destPromise = lowered.out; srcT = null; + } else if (isLast) { + // an async generator destination: pumped into the object-mode + // PassThrough that pipeline() answers + const helpers = fromGenHelpers(lowerer, lowered.out, loc); + if (!helpers) lowerer.noLowering("a pipeline destination with this generator type", arg); + helperArgs.push(helpers![0]!, helpers![1]!, helpers![2]!); + dest = 1; + srcT = null; } else { srcT = lowered.out; } @@ -3242,7 +3840,8 @@ function lowerPipelineStages( argDecls.length === 0 ? call : { kind: "seqExpr", stmts: argDecls, result: call, type: call.type, loc }; - // SCR_PIPE_* flags: bits 1-2 the destination, bit 5 the signal and abort. + // SCR_PIPE_* flags: bits 1-2 the destination, bit 5 the signal and + // abort (bit 6, the promise form's options signal, is set by its caller). const flags = (dest << 1) | (mask !== 0 ? 32 : 0); const head: IrExpr[] = [ numLit(streams.length, loc), @@ -3255,7 +3854,7 @@ function lowerPipelineStages( ...helperArgs, ]; const last: IrType = - dest === 2 ? { kind: "object", className: "%PassThrough" } : streams[streams.length - 1]!.type; + dest !== 0 ? { kind: "object", className: "%PassThrough" } : streams[streams.length - 1]!.type; if (mask === 0) return { args: head, last, dest, destPromise, wrap: bindArgs }; // const ac = new AbortController(): its signal goes to the stages, and // finishImpl's ac.abort() runs through the abort closure. @@ -3324,21 +3923,27 @@ function pipelineBoxHelper( /** `() => ac.abort()` over the pipeline's AbortController: an interned * lifted function takes the controller and answers the closure. */ -function pipelineAbortHelper(lowerer: Lowerer, ac: IrExpr, loc: SrcLoc): IrExpr { - const key = "pipelineAbort"; +function pipelineAbortHelper( + lowerer: Lowerer, + ac: IrExpr, + loc: SrcLoc, + withReason = false, +): IrExpr { + const key = withReason ? "pipelineAbortReason" : "pipelineAbort"; let name = lowerer.valueHelpers.get(key); - const abortT = funcOf([], VOID); + const abortT = funcOf(withReason ? [DYN] : [], VOID); if (!name) { name = keyedHelperName(lowerer, "%stream.pipelineAbort", key); lowerer.valueHelpers.set(key, name); const acLocal: IrLocal = { id: "ac.0", name: "ac", type: DYN, mutable: false, boxed: true }; const acRef = varRef(acLocal.id, DYN, loc); + const reasonLocal: IrLocal = { id: "reason.0", name: "reason", type: DYN, mutable: false }; lowerer.liftedFns.push({ name: `${name}.abort`, - params: [], + params: withReason ? [{ localId: reasonLocal.id, name: reasonLocal.name, type: DYN }] : [], returnType: VOID, captures: [{ localId: acLocal.id, name: acLocal.name, type: DYN }], - locals: [acLocal], + locals: withReason ? [acLocal, reasonLocal] : [acLocal], body: [ { kind: "exprStmt", @@ -3347,7 +3952,7 @@ function pipelineAbortHelper(lowerer: Lowerer, ac: IrExpr, loc: SrcLoc): IrExpr recv: acRef, method: "abort", calleeName: "ac.abort", - args: [], + args: withReason ? [varRef(reasonLocal.id, DYN, loc)] : [], type: DYN, loc, }, @@ -3467,6 +4072,373 @@ function lowerStageFunction( } } +/** The async generator a Duplex.from body iterates (duplexify.js's + * fromAsyncGen source), as a closure the runtime calls with next() and + * ack(): each pass awaits next() (the deferred a write or the final + * settles), calls ack() (process.nextTick(cb) and a fresh deferred), + * returns at the done marker, and yields the chunk. */ +function duplexSourceHelper(lowerer: Lowerer, genT: SyncGenType, loc: SrcLoc): IrExpr | null { + const nextT = funcOf([], { kind: "promise", inner: DYN }); + const ackT = funcOf([], VOID); + const type = funcOf([nextT, ackT], genT); + const key = "duplexSource"; + let name = lowerer.valueHelpers.get(key); + if (!name) { + const resultType = genResultRecord(DYN, VOID, lowerer.shapes, lowerer.unions); + if (!resultType) return null; + name = `%stream.duplexSource.${lowerer.valueHelpers.size}`; + const frame = new LiftedFrame(loc); + const nextId = frame.param("next", nextT); + const ackId = frame.param("ack", ackT); + const valueId = frame.local("value", DYN, false); + const value = varRef(valueId, DYN, loc); + const body: IrStmt[] = [ + { + kind: "while", + cond: { kind: "boolLit", value: true, type: BOOL, loc }, + body: [ + { + kind: "varDecl", + localId: valueId, + init: { + kind: "awaitExpr", + value: { + kind: "callValue", + callee: varRef(nextId, nextT, loc), + args: [], + type: { kind: "promise", inner: DYN }, + loc, + }, + type: DYN, + loc, + }, + loc, + }, + { + kind: "exprStmt", + expr: { + kind: "callValue", + callee: varRef(ackId, ackT, loc), + args: [], + type: VOID, + loc, + }, + loc, + }, + { + kind: "if", + cond: { kind: "libCall", fn: "stream.chunkDone", args: [value], type: BOOL, loc }, + then: [{ kind: "return", value: null, loc }], + else_: null, + loc, + }, + { kind: "exprStmt", expr: { kind: "yieldExpr", value, type: VOID, loc }, loc }, + ], + loc, + }, + { kind: "return", value: null, loc }, + ]; + lowerer.valueHelpers.set(key, name); + lowerer.liftedFns.push({ + name, + params: frame.params, + returnType: VOID, + async: true, + locals: frame.locals, + body, + generator: { yieldT: DYN, nextT: UNDEFINED_T, resultType }, + loc, + }); + } + return { kind: "closure", fnName: name, captures: [], type, loc }; +} + +/** Duplex.from(body) (duplexify.js) for the bodies with a lowering: an + * async generator function, whose source generator the Duplex's writes + * feed (stream.duplexFromGen); a duplex stream, which answers itself; and + * the bodies Readable.from reads (iterables, strings, generators), as a + * Duplexify that is not writable. */ +function duplexFromBody(lowerer: Lowerer, node: ts.Expression, loc: SrcLoc): IrExpr { + const duplex: IrType = { kind: "object", className: "%Duplex" }; + const fn = pipelineStageFunction(lowerer, node); + if (fn !== null) { + const isAsync = (fn.modifiers?.nodes ?? []).some((m) => m.kind === ts.SyntaxKind.AsyncKeyword); + const isGen = !ts.isArrowFunction(fn) && fn.asteriskToken !== undefined; + if (!isAsync) { + lowerer.noLowering( + "Duplex.from over a function that is not async", + node, + "write the body as async function* (source) { ... } or async function (source) { ... }", + ); + } + if (fn.parameters.nodes.length > 2) { + lowerer.noLowering( + "a Duplex.from body with parameters after the options argument", + fn.parameters.nodes[2]!, + "the body takes the source and { signal }", + ); + } + const sourceT: SyncGenType = { + kind: "generator", + async: true, + yieldT: DYN, + retT: VOID, + nextT: UNDEFINED_T, + }; + const lowered = lowerStageFunction(lowerer, fn, sourceT); + const params = lowered.closure.type.kind === "func" ? lowered.closure.type.params : []; + // fromAsyncGen calls the body with { signal } of an AbortController + // that destroy aborts with its error. + let signalArgs: IrExpr[] = []; + let signalDecls: IrStmt[] = []; + if (params.length > 1) { + if (params[1]!.kind !== "dyn") { + lowerer.noLowering( + "a Duplex.from body's options argument with this type", + fn.parameters.nodes[1]!, + "type it as { signal: AbortSignal }", + ); + } + const acLocal = lowerer.declareHiddenLocal("%duplexAbort", DYN); + const ac = varRef(acLocal.id, DYN, loc); + signalDecls = [ + { + kind: "varDecl", + localId: acLocal.id, + init: { kind: "libCall", fn: "fetch.abortControllerNew", args: [], type: DYN, loc }, + loc, + }, + ]; + signalArgs = [ + pipelineAbortHelper(lowerer, ac, loc, true), + { kind: "dynKeyGet", value: ac, key: strLit("signal", loc), type: DYN, loc }, + ]; + } + const withSignal = (fnName: IrLibFn, args: IrExpr[]): IrExpr => { + const call: IrExpr = { + kind: "libCall", + fn: signalArgs.length > 0 ? (`${fnName}Signal` as IrLibFn) : fnName, + args: [...args, ...signalArgs], + type: duplex, + loc, + }; + return signalDecls.length > 0 + ? { kind: "seqExpr", stmts: signalDecls, result: call, type: duplex, loc } + : call; + }; + if (!isGen) { + // an async function: a writable Duplexify whose final waits for + // the function's promise + const asyncSource = duplexSourceHelper(lowerer, sourceT, loc); + const out = lowered.out; + if (out.kind !== "promise" || !asyncSource) + return lowerer.noLowering("Duplex.from over a function with this return type", node); + if ( + out.inner.kind !== "void" && + !canConvertToDyn( + out.inner, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + ) + ) { + lowerer.noLowering( + `Duplex.from over a function resolving to '${lowerer.fmt(out.inner)}'`, + node, + ); + } + return withSignal("stream.duplexFromAsyncFn", [ + lowered.closure, + asyncSource, + pipelineBoxHelper(lowerer, out, loc), + ]); + } + const helpers = + lowered.out.kind === "generator" ? fromGenHelpers(lowerer, lowered.out, loc) : null; + const source = duplexSourceHelper(lowerer, sourceT, loc); + if (!helpers || !source) + lowerer.noLowering("Duplex.from over a function with this generator type", node); + return withSignal("stream.duplexFromGen", [lowered.closure, source!, ...helpers!]); + } + // duplexify.js: a stream answers itself (a Duplex) or is wrapped; any + // other body reads as from(Duplexify, body, { objectMode: true, + // writable: false }) + let bodyT: IrType | null; + try { + bodyT = lowerer.mapTypeOf(lowerer.typeOf(node)); + } catch { + bodyT = null; + } + const bodySides = + bodyT?.kind === "object" ? streamSidesOf(lowerer, lowerer.classes.get(bodyT.className)) : null; + let literal: ts.Expression = node; + while (ts.isParenthesizedExpression(literal)) literal = literal.expression; + if (bodySides === null && ts.isObjectLiteralExpression(literal)) + return duplexFromPair(lowerer, literal, loc); + if (bodyT?.kind === "promise") { + // a Duplexify that reads the promise's value + const valueT = bodyT.inner; + if ( + valueT.kind !== "void" && + !canConvertToDyn( + valueT, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + ) + ) { + lowerer.noLowering(`Duplex.from over a promise of '${lowerer.fmt(valueT)}'`, node); + } + const promise = lowerer.lowerExpr(node); + if (promise.type.kind !== "promise") { + // a checked-dynamic promise (an untyped function's result) + return { + kind: "libCall", + fn: "stream.duplexFromDyn", + args: [lowerer.coerceToExpected(promise, DYN)], + type: { kind: "object", className: "%Duplex" }, + loc, + }; + } + return { + kind: "libCall", + fn: "stream.duplexFromPromise", + args: [ + promise, + pipelineBoxHelper(lowerer, promise.type as IrType & { kind: "promise" }, loc), + ], + type: { kind: "object", className: "%Duplex" }, + loc, + }; + } + if (bodySides === null) return lowerFromSource(lowerer, node, node, undefined, true); + let stream: IrExpr; + try { + stream = lowerStreamArg(lowerer, node, "compose"); + } catch (e) { + if (e instanceof StreamArgTypeThrow) return e.expr; + throw e; + } + const sides = + stream.type.kind === "object" + ? streamSidesOf(lowerer, lowerer.classes.get(stream.type.className)) + : null; + // _duplexify({ readable }) / _duplexify({ writable }) + if (sides === "r" || sides === "w") { + return { + kind: "libCall", + fn: sides === "r" ? "stream.duplexifyReadable" : "stream.duplexifyWritable", + args: [stream], + type: duplex, + loc, + }; + } + if (sides !== "rw") { + lowerer.noLowering( + "Duplex.from over a value of this type", + node, + "an async generator function, an iterable, a stream, or a { readable, writable } pair is the lowered body", + ); + } + return stream; +} + +/** Duplex.from({ readable, writable }): each member that is a stream with + * that side is used as it is, any other member is Duplex.from of it, and + * the pair reads and writes through _duplexify (duplexify.js). The + * members evaluate in source order. */ +function duplexFromPair( + lowerer: Lowerer, + literal: ts.ObjectLiteralExpression, + loc: SrcLoc, +): IrExpr { + const duplex: IrType = { kind: "object", className: "%Duplex" }; + const sides: { readable?: IrExpr; writable?: IrExpr } = {}; + // The members in source order; when that is not (readable, writable), + // each value is bound to a local first, keeping the literal's + // evaluation order. + const members: { key: "readable" | "writable"; value: IrExpr }[] = []; + for (const p of literal.properties.nodes) { + const key = + (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) && ts.isIdentifier(p.name) + ? p.name.text + : null; + if (key !== "readable" && key !== "writable") { + lowerer.noLowering( + "Duplex.from over an object with members other than readable and writable", + p, + "pass { readable, writable } with streams or Duplex.from bodies", + ); + } + const value = ts.isShorthandPropertyAssignment(p) + ? (p.name as ts.Identifier) + : (p as ts.PropertyAssignment).initializer; + let valueT: IrType | null; + try { + valueT = lowerer.mapTypeOf(lowerer.typeOf(value)); + } catch { + valueT = null; + } + const valueSides = + valueT?.kind === "object" + ? streamSidesOf(lowerer, lowerer.classes.get(valueT.className)) + : null; + const wanted = key === "readable" ? "r" : "w"; + const direct = valueSides === "rw" || valueSides === wanted; + members.push({ + key: key!, + value: direct + ? lowerStreamArg(lowerer, value, "compose") + : duplexFromBody(lowerer, value, loc), + }); + } + const decls: IrStmt[] = []; + const inOrder = + members.length <= 1 || + (members.length === 2 && members[0]!.key === "readable" && members[1]!.key === "writable"); + for (const m of members) { + if (inOrder) { + sides[m.key] = m.value; + continue; + } + const local = lowerer.declareHiddenLocal(`%duplex${m.key}`, m.value.type); + decls.push({ kind: "varDecl", localId: local.id, init: m.value, loc }); + sides[m.key] = varRef(local.id, m.value.type, loc); + } + const { readable, writable } = sides; + if (readable && writable) { + const call: IrExpr = { + kind: "libCall", + fn: "stream.duplexifyPair", + args: [readable, writable], + type: duplex, + loc, + }; + return decls.length > 0 + ? { kind: "seqExpr", stmts: decls, result: call, type: duplex, loc } + : call; + } + if (decls.length > 0) { + // a repeated member: every value is evaluated, the last one is used + const one = readable ?? writable!; + const call: IrExpr = { + kind: "libCall", + fn: readable ? "stream.duplexifyReadable" : "stream.duplexifyWritable", + args: [one], + type: duplex, + loc, + }; + return { kind: "seqExpr", stmts: decls, result: call, type: duplex, loc }; + } + if (readable) + return { kind: "libCall", fn: "stream.duplexifyReadable", args: [readable], type: duplex, loc }; + if (writable) + return { kind: "libCall", fn: "stream.duplexifyWritable", args: [writable], type: duplex, loc }; + return lowerer.noLowering( + "Duplex.from over an object with neither readable nor writable", + literal, + "pass { readable, writable } with streams or Duplex.from bodies", + ); +} + /** The checker type of a stage's string or Buffer chunks, or null for * other chunk types. */ function stageChunkCheckerType(lowerer: Lowerer, chunk: IrType, at: ts.Node): ts.Type | null { @@ -3481,12 +4453,8 @@ function stageChunkCheckerType(lowerer: Lowerer, chunk: IrType, at: ts.Node): ts * classes fix which positions can be readable and writable; a stream in * 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.nodes; +function lowerStreamCompose(lowerer: Lowerer, args: readonly ts.Expression[], loc: SrcLoc): IrExpr { 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"); - } if (args.length === 0) { return nodeThrowExpr( 1, @@ -3496,9 +4464,47 @@ function lowerStreamCompose(lowerer: Lowerer, call: ts.CallExpression, loc: SrcL loc, ); } + // A web ReadableStream first stage reads as an async iterable (the + // pipeline's source), and alone it is Duplex.from of it; a later stage + // would be Duplex.fromWeb of it. + for (let i = 1; i < args.length; i++) { + const a = args[i]!; + // (typeOf on a stage function's name would report what collecting its + // declared signature deferred) + if (pipelineStageFunction(lowerer, a) !== null) continue; + if (staticReadableStreamElementType(lowerer, a) !== null) { + lowerer.noLowering( + "compose with a web ReadableStream after the first stage", + a, + "a web ReadableStream is supported as the first stage of compose or pipeline, and in Readable.from", + ); + } + } + // Duplex.from(stage) for one stage (duplexify.js), and for a function at + // either end; functions between, an iterable first stage or a source + // function run as the pipeline's function stages (compose.js) + if (args.length === 1) return duplexFromBody(lowerer, args[0]!, loc); + const last = args.length - 1; + const isFn = (i: number): boolean => pipelineStageFunction(lowerer, args[i]!) !== null; + const pre = new Map(); + if (isFn(0)) pre.set(0, duplexFromBody(lowerer, args[0]!, loc)); + if (isFn(last)) pre.set(last, duplexFromBody(lowerer, args[last]!, loc)); + const staged = + args.some((_, i) => !pre.has(i) && isFn(i)) || + (!pre.has(0) && iterableSourceType(lowerer, args[0]!) !== null); + if (staged) { + const lowered = lowerPipelineStages(lowerer, args, loc, pre); + return lowered.wrap({ + kind: "libCall", + fn: "stream.composeStages", + args: lowered.args, + type: duplex, + loc, + }); + } let streams: IrExpr[]; try { - streams = args.map((a) => lowerStreamArg(lowerer, a, "compose")); + streams = args.map((a, i) => pre.get(i) ?? lowerStreamArg(lowerer, a, "compose")); } catch (e) { if (e instanceof StreamArgTypeThrow) return e.expr; throw e; @@ -3541,6 +4547,29 @@ function lowerStreamCompose(lowerer: Lowerer, call: ts.CallExpression, loc: SrcL /* ── the Readable operators ──────────────────────────────────────────── */ /** The Readable operators (scr_stream_ops.c — Node's operators.js). */ +/** A stream member's receiver, lowered: a value the checker types as a + * stream class whose lowering is checked-dynamic (a JavaScript binding + * that holds an untyped call's result) or a union (an element read) + * converts to that class, checked where the member runs. */ +function streamReceiverExpr(lowerer: Lowerer, node: ts.Expression): IrExpr { + const value = lowerer.lowerExpr(node); + if (value.type.kind !== "dyn" && value.type.kind !== "union") return value; + let classT: IrType | null; + try { + classT = lowerer.mapTypeOf(lowerer.typeOf(node)); + } catch { + return value; + } + if ( + classT?.kind !== "object" || + streamSidesOf(lowerer, lowerer.classes.get(classT.className)) === null + ) + return value; + return value.type.kind === "dyn" + ? { kind: "dynCheck", value, type: classT, loc: value.loc } + : lowerer.coerceToExpected(value, classT); +} + export const STREAM_OPERATORS: ReadonlySet = new Set([ "map", "filter", @@ -3555,39 +4584,40 @@ export const STREAM_OPERATORS: ReadonlySet = new Set([ "reduce", ]); -/** An operator callback as a checked-dynamic function value. Callbacks - * that declare the options argument ({ signal }) are refused: the - * AbortSignal Node passes there has no representation. map and flatMap - * callbacks must produce strings or Buffers (flatMap: arrays of them, or - * strings) — or checked-dynamic values, which the runtime checks. */ +/** An operator callback as a checked-dynamic function value, and whether + * it declares the options argument ({ signal }), which the runtime then + * passes. Its results convert to checked-dynamic values (flatMap's are + * iterated with yield*). */ function lowerOperatorCallback( lowerer: Lowerer, node: ts.Expression, member: string, maxParams: number, -): IrExpr { +): { fn: IrExpr; takesOptions: boolean; gen?: IrType & { kind: "generator" } } { const loc = locOf(node); const optionsParam = (at: ts.Node): never => lowerer.noLowering( - `${member} callbacks that declare the options argument`, + `${member} callbacks with parameters after the options argument`, at, - "the { signal } argument has no lowering — take only the chunk", + "the callback takes the chunk and { signal }", ); - if ( - (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) && - node.parameters.nodes.length > maxParams - ) { - optionsParam(node.parameters.nodes[maxParams]!); + let takesOptions = false; + if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) { + if (node.parameters.nodes.length > maxParams + 1) + optionsParam(node.parameters.nodes[maxParams + 1]!); + takesOptions = node.parameters.nodes.length > maxParams; } const v = lowerer.lowerExpr(node); - if (v.type.kind === "dyn") return v; + // A checked-dynamic function's parameters are not known here: it gets + // the { signal } argument, as Node passes it to every callback. + if (v.type.kind === "dyn") return { fn: v, takesOptions: true }; if ( v.type.kind !== "func" && isJsSourceFile(ts.getSourceFileOfNode(node)) && lowerer.dynConvertible(v.type) ) { // JavaScript passes any value; Node's validateFunction throws. - return { kind: "dynFrom", value: v, type: DYN, loc }; + return { fn: { kind: "dynFrom", value: v, type: DYN, loc }, takesOptions: false }; } if (v.type.kind !== "func") { lowerer.noLowering( @@ -3596,25 +4626,8 @@ function lowerOperatorCallback( "pass a function", ); } - if (v.type.params.length > maxParams) optionsParam(node); - if (member === "map" || member === "flatMap") { - const ret = v.type.ret.kind === "promise" ? v.type.ret.inner : v.type.ret; - const chunk = (t: IrType): boolean => - t.kind === "string" || t.kind === "dyn" || (t.kind === "bytes" && t.elem === "u8"); - const ok = - member === "map" - ? chunk(ret) - : ret.kind === "dyn" || ret.kind === "string" || (ret.kind === "array" && chunk(ret.elem)); - if (!ok) { - lowerer.noLowering( - `${member} callbacks that return '${lowerer.fmt(ret)}'`, - node, - member === "map" - ? "map results are stream chunks: strings or Buffers (object-mode streams of other values are not lowered)" - : "flatMap results are arrays of strings or Buffers, or strings", - ); - } - } + if (v.type.params.length > maxParams + 1) optionsParam(node); + if (v.type.params.length > maxParams) takesOptions = true; const getRecord = (id: string) => lowerer.shapes.get(id); const getUnion = (id: string) => lowerer.unions.get(id); if (!canBoxFuncIntoDyn(v.type, getRecord, getUnion)) { @@ -3624,7 +4637,97 @@ function lowerOperatorCallback( "the callback's parameter and result types must cross the checked-dynamic boundary", ); } - return { kind: "dynFrom", value: v, type: DYN, loc }; + const fn: IrExpr = { kind: "dynFrom", value: v, type: DYN, loc }; + // flatMap over a compiled generator: the runtime steps it through the + // helpers over its type + if (member === "flatMap" && v.type.ret.kind === "generator") + return { fn, takesOptions, gen: v.type.ret }; + return { fn, takesOptions }; +} + +/** The operator callbacks' `{ signal }` (operators.js): map and the + * operators over it pass `{ signal: AbortSignal.any([options?.signal] + * .filter(Boolean)) }`. options is the operator's options value. */ +export function operatorSignalOptions(lowerer: Lowerer, options: IrExpr, loc: SrcLoc): IrExpr { + void lowerer; + const list: IrExpr = { + kind: "libCall", + fn: "readable.opSignalList", + args: [options], + type: DYN, + loc, + }; + const signal: IrExpr = { kind: "libCall", fn: "fetch.abortAny", args: [list], type: DYN, loc }; + return { kind: "libCall", fn: "readable.opSignalOpts", args: [signal], type: DYN, loc }; +} + +/** reduce's signal for a reducer that reads it (operators.js): `const ac + * = new AbortController()`, `options.signal.addEventListener('abort', () + * => ac.abort(), { once: true })`, then reduceSignal's ac.abort() closure + * and `{ signal: ac.signal }`. options is the bound options value. */ +export function reduceSignalSetup( + lowerer: Lowerer, + options: IrExpr, + loc: SrcLoc, + declare: (name: string, type: IrType) => IrLocal = (name, type) => + lowerer.declareHiddenLocal(name, type), +): { stmts: IrStmt[]; abort: IrExpr; cbOpts: IrExpr } { + const acLocal = declare("%opAbort", DYN); + const ac = varRef(acLocal.id, DYN, loc); + const sigLocal = declare("%opUserSignal", DYN); + const sig = varRef(sigLocal.id, DYN, loc); + const abort = pipelineAbortHelper(lowerer, ac, loc); + const stmts: IrStmt[] = [ + { + kind: "varDecl", + localId: acLocal.id, + init: { kind: "libCall", fn: "fetch.abortControllerNew", args: [], type: DYN, loc }, + loc, + }, + { + kind: "varDecl", + localId: sigLocal.id, + init: { kind: "libCall", fn: "readable.opSignalOf", args: [options], type: DYN, loc }, + loc, + }, + { + kind: "if", + cond: { kind: "dynTest", test: "truthy", value: sig, type: BOOL, loc }, + then: [ + { + kind: "exprStmt", + expr: { + kind: "dynInvoke", + recv: sig, + method: "addEventListener", + calleeName: "options.signal.addEventListener", + args: [ + { kind: "dynFrom", value: strLit("abort", loc), type: DYN, loc }, + { kind: "dynFrom", value: pipelineAbortHelper(lowerer, ac, loc), type: DYN, loc }, + { kind: "libCall", fn: "readable.opOnceOptions", args: [], type: DYN, loc }, + ], + type: DYN, + loc, + }, + loc, + }, + ], + else_: null, + loc, + }, + ]; + const signal: IrExpr = { + kind: "dynKeyGet", + value: ac, + key: strLit("signal", loc), + type: DYN, + loc, + }; + return { + stmts, + abort, + cbOpts: { kind: "libCall", fn: "readable.opSignalOpts", args: [signal], type: DYN, loc }, + }; } /** An operator's options (or reduce's initial value): an object literal @@ -3663,21 +4766,37 @@ function lowerStreamOperator( ): IrExpr { const loc = locOf(call); const args = call.arguments.nodes; - const recv = lowerer.lowerExpr(access.expression); + // JS order: the receiver, the callback, then the options; the + // callback's { signal } is built over the options value afterwards. + const decls: IrStmt[] = []; + const bind = (value: IrExpr, name: string): IrExpr => { + const local = lowerer.declareHiddenLocal(name, value.type); + decls.push({ kind: "varDecl", localId: local.id, init: value, loc }); + return varRef(local.id, value.type, loc); + }; + const recv = bind(streamReceiverExpr(lowerer, access.expression), "%opRecv"); const readable: IrType = { kind: "object", className: "%Readable" }; const opts = (i: number): IrExpr => lowerOperatorValue(lowerer, args[i], `${member} options`, loc, true); + let takesOptions = false; + let callbackGen: (IrType & { kind: "generator" }) | undefined; const fnArg = (maxParams: number): IrExpr => { if (args[0] === undefined) return dynUndefinedExpr(loc); - return lowerOperatorCallback(lowerer, args[0], member, maxParams); + const lowered = lowerOperatorCallback(lowerer, args[0], member, maxParams); + takesOptions = lowered.takesOptions; + callbackGen = lowered.gen; + return bind(lowered.fn, "%opFn"); + }; + const lib = (fn: string, a: IrExpr[], type: IrType): IrExpr => { + const call: IrExpr = { kind: "libCall", fn: fn as IrLibFn, args: a, type, loc }; + return { kind: "seqExpr", stmts: decls, result: call, type, loc }; + }; + // the options argument (bound), and the callback's { signal } when it + // declares one + const withSignal = (optsValue: IrExpr): IrExpr[] => { + const o = bind(optsValue, "%opOptions"); + return [o, takesOptions ? operatorSignalOptions(lowerer, o, loc) : dynUndefinedExpr(loc)]; }; - const lib = (fn: string, a: IrExpr[], type: IrType): IrExpr => ({ - kind: "libCall", - fn: fn as IrLibFn, - args: a, - type, - loc, - }); switch (member) { case "map": case "filter": @@ -3689,7 +4808,16 @@ function lowerStreamOperator( : member === "filter" ? "readable.opFilter" : "readable.opFlatMap"; - return lib(fnName, [recv, fn, opts(1)], readable); + const rest = withSignal(opts(1)); + const genHelpers = callbackGen ? fromGenHelpers(lowerer, callbackGen, loc) : null; + if (callbackGen && genHelpers) { + return lib( + callbackGen.async ? "readable.opFlatMapAsyncGen" : "readable.opFlatMapSyncGen", + [recv, fn, ...rest, ...genHelpers], + readable, + ); + } + return lib(fnName, [recv, fn, ...rest], readable); } case "drop": case "take": { @@ -3702,15 +4830,24 @@ function lowerStreamOperator( } case "toArray": return lib("readable.opToArray", [recv, opts(0)], { kind: "promise", inner: DYN }); - case "forEach": - return lib("readable.opForEach", [recv, fnArg(1), opts(1)], { kind: "promise", inner: VOID }); + case "forEach": { + const fn = fnArg(1); + return lib("readable.opForEach", [recv, fn, ...withSignal(opts(1))], { + kind: "promise", + inner: VOID, + }); + } case "some": case "every": { const fn = fnArg(1); - return lib(member === "some" ? "readable.opSome" : "readable.opEvery", [recv, fn, opts(1)], { - kind: "promise", - inner: BOOL, - }); + return lib( + member === "some" ? "readable.opSome" : "readable.opEvery", + [recv, fn, ...withSignal(opts(1))], + { + kind: "promise", + inner: BOOL, + }, + ); } case "find": { const t = lowerer.typeOf(call); @@ -3722,7 +4859,11 @@ function lowerStreamOperator( "the result is a checked-dynamic value — use a boolean predicate and narrow the result", ); } - return lib("readable.opFind", [recv, fnArg(1), opts(1)], { kind: "promise", inner: DYN }); + const fn = fnArg(1); + return lib("readable.opFind", [recv, fn, ...withSignal(opts(1))], { + kind: "promise", + inner: DYN, + }); } case "reduce": { const t = lowerer.typeOf(call); @@ -3749,12 +4890,19 @@ function lowerStreamOperator( } } const fn = fnArg(2); - const init = lowerOperatorValue(lowerer, args[1], "reduce's initial value", loc, false); - return lib( - "readable.opReduce", - [recv, fn, init, boolLit(args.length >= 2, loc), opts(2), numLit(kind, loc)], - { kind: "promise", inner }, + const init = bind( + lowerOperatorValue(lowerer, args[1], "reduce's initial value", loc, false), + "%opInit", ); + const o = bind(opts(2), "%opOptions"); + const head = [recv, fn, init, boolLit(args.length >= 2, loc), o, numLit(kind, loc)]; + if (!takesOptions) return lib("readable.opReduce", head, { kind: "promise", inner }); + const setup = reduceSignalSetup(lowerer, o, loc); + decls.push(...setup.stmts); + return lib("readable.opReduceSignal", [...head, setup.abort, setup.cbOpts], { + kind: "promise", + inner, + }); } } throw new InternalCompilerError(`unknown stream operator ${member}`); @@ -3807,7 +4955,7 @@ function pipeIntoStdio( ): IrExpr { const loc = locOf(call); // JS order: the receiver, then the destination, then the options. - const receiverValue = lowerer.lowerExpr(access.expression); + const receiverValue = streamReceiverExpr(lowerer, access.expression); const source = lowerer.declareHiddenLocal("%pipeSource", receiverValue.type); const stdio = lowerer.declareHiddenLocal("%pipeStdio", target.type); const options = call.arguments.nodes[1]; @@ -3929,7 +5077,7 @@ function pipeIntoDyn( ): IrExpr { const loc = locOf(call); noteUntypedStreamFlow(lowerer, null); - const receiverValue = lowerer.lowerExpr(access.expression); + const receiverValue = streamReceiverExpr(lowerer, access.expression); const source = lowerer.declareHiddenLocal("%pipeSource", receiverValue.type); const destination = lowerer.declareHiddenLocal("%pipeDestination", target.type); const stmts: IrStmt[] = [ @@ -4024,6 +5172,80 @@ function pipeIntoDyn( }; } +/** readable.compose(stage, options): compose(this, stage), with + * addAbortSignal(options.signal, composed) when the signal is set + * (operators.js). */ +function lowerReadableCompose( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, +): IrExpr { + const loc = locOf(call); + const args = call.arguments.nodes; + if (args.length === 0 || args.length > 2 || args.some((a) => ts.isSpreadElement(a))) { + lowerer.noLowering( + `readable.compose with ${args.length === 0 ? "no stage" : "these arguments"}`, + call, + "the supported form is readable.compose(stage) or readable.compose(stage, { signal })", + ); + } + let signalNode: ts.Expression | null = null; + const options = args[1]; + if ( + options !== undefined && + options.kind !== ts.SyntaxKind.NullKeyword && + !(ts.isIdentifier(options) && options.text === "undefined") + ) { + if (!ts.isObjectLiteralExpression(options)) { + lowerer.noLowering( + "readable.compose options that are not an object literal", + options, + "pass { signal } as an object literal", + ); + } + for (const p of options.properties.nodes) { + const key = + (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) && + ts.isIdentifier(p.name) + ? p.name.text + : null; + if (key !== "signal") { + lowerer.noLowering( + key === null + ? "readable.compose options with this member" + : `the readable.compose option '${key}'`, + p, + "signal is the readable.compose option", + ); + } + const value = ts.isShorthandPropertyAssignment(p) + ? (p.name as ts.Identifier) + : (p as ts.PropertyAssignment).initializer; + if (value.kind === ts.SyntaxKind.NullKeyword) continue; + if (ts.isIdentifier(value) && value.text === "undefined") continue; + // The signal is read after compose runs, so only values whose + // evaluation has no effects. + if (!sideEffectFreeOptionValue(value) && !pureRead(value)) { + lowerer.noLowering( + "the readable.compose option 'signal' with this value", + p, + "pass a variable or property read (const signal = controller.signal)", + ); + } + signalNode = value; + } + } + const composed = lowerStreamCompose(lowerer, [access.expression, args[0]!], loc); + if (signalNode === null) return composed; + return { + kind: "libCall", + fn: "stream.addAbortSignal", + args: [composed, lowerer.lowerExprExpecting(signalNode, DYN)], + type: composed.type, + loc, + }; +} + export function lowerStreamMethodCall( lowerer: Lowerer, call: ts.CallExpression, @@ -4036,6 +5258,12 @@ export function lowerStreamMethodCall( if (opSides === "r" || opSides === "rw") return lowerStreamOperator(lowerer, call, access, member); } + if (member === "compose") { + const composeSides = streamSidesOf(lowerer, info); + return composeSides === "r" || composeSides === "rw" + ? lowerReadableCompose(lowerer, call, access) + : null; + } if (!STREAM_API_MEMBERS.has(member)) return null; const sides = streamSidesOf(lowerer, info); if (!sides) return null; @@ -4138,7 +5366,7 @@ export function lowerStreamMethodCall( loc, ); } - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); const chunk = lowerer.lowerExprExpecting(args[0]!, STRING); return { kind: "libCall", @@ -4154,7 +5382,7 @@ export function lowerStreamMethodCall( if ((kind === "null" || kind === "union" || kind === "dyn") && member === "unshift") { // unshift(value): an object-mode stream's value at the front, null // the end of the stream, as Node's readableAddChunk takes them - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); const chunk = lowerer.lowerExprExpecting(args[0]!, DYN); return { kind: "libCall", @@ -4164,7 +5392,7 @@ export function lowerStreamMethodCall( loc, }; } - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); if (kind === "null") { return { kind: "libCall", fn: "readable.pushNull", args: [receiver], type: BOOL, loc }; } @@ -4217,7 +5445,23 @@ export function lowerStreamMethodCall( if (member === "read") { requireSide(readableSide, "read"); if (args.length > 1) lowerer.noLowering(`read with ${args.length} arguments`, call); - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); + // JavaScript (and a result the declarations type as any) reads any + // chunk: a Buffer, a string once encoded, or an object-mode value. + const resultTs = lowerer.typeOf(call); + if ( + isJsSourceFile(ts.getSourceFileOfNode(call)) || + (resultTs.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0 + ) { + const sizeDyn = args[0] ? lowerer.lowerExprExpecting(args[0], DYN) : dynUndefinedExpr(loc); + return { + kind: "libCall", + fn: "readable.readDyn", + args: [receiver, sizeDyn], + type: DYN, + loc, + }; + } const size = args[0] ? lowerer.lowerExprExpecting(args[0], F64) : numLit(-1, loc); const type: IrType = unionOf(lowerer, [BYTES, { kind: "nullT" }]); const read: IrExpr = { @@ -4233,7 +5477,7 @@ export function lowerStreamMethodCall( if (member === "pause" || member === "resume") { requireSide(readableSide, member); if (args.length !== 0) lowerer.noLowering(`${member} with arguments`, call); - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); return { kind: "libCall", fn: member === "pause" ? "readable.pause" : "readable.resume", @@ -4246,7 +5490,7 @@ export function lowerStreamMethodCall( if (member === "isPaused") { requireSide(readableSide, "isPaused"); if (args.length !== 0) lowerer.noLowering(`isPaused with arguments`, call); - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); return { kind: "libCall", fn: "readable.isPaused", args: [receiver], type: BOOL, loc }; } @@ -4263,7 +5507,7 @@ export function lowerStreamMethodCall( // Transform, and pipe() answers the value itself (the receiver // evaluates first, as in JS). if (isValueStream(lowerer, args[0]!)) { - const receiverValue = lowerer.lowerExpr(access.expression); + const receiverValue = streamReceiverExpr(lowerer, access.expression); const source = lowerer.declareHiddenLocal("%pipeSource", receiverValue.type); const hashValue = lowerer.lowerExpr(args[0]!); const hash = lowerer.declareHiddenLocal("%pipeHash", hashValue.type); @@ -4313,7 +5557,7 @@ export function lowerStreamMethodCall( ); } const end = pipeEndOption(lowerer, args[1], loc); - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); return { kind: "libCall", fn: generalPipe(lowerer, dst.type), @@ -4326,7 +5570,7 @@ export function lowerStreamMethodCall( if (member === "unpipe") { requireSide(readableSide, "unpipe"); if (args.length > 1) lowerer.noLowering(`unpipe with ${args.length} arguments`, call); - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); if (args[0] !== undefined) { const dst = lowerer.lowerExpr(args[0]); if (dst.type.kind === "dyn") return unpipeDyn(lowerer, receiver, dst, loc); @@ -4387,7 +5631,7 @@ export function lowerStreamMethodCall( if (member === "destroy") { if (args.length > 1) lowerer.noLowering(`destroy with ${args.length} arguments`, call); - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); if (args[0] !== undefined) { const err = lowerer.lowerExpr(args[0]); const rootsAtError = (t: IrType): boolean => { @@ -4439,8 +5683,11 @@ export function lowerStreamMethodCall( // argument reaches an object-mode side's _write as passed, null when // the callback took its place (a byte-mode side encodes a string // chunk with it). - const writeEnc = (encArg: ts.Expression | undefined, cbArg: ts.Expression | undefined) => { - const receiver = lowerer.lowerExpr(access.expression); + const writeEnc = ( + encArg: ts.Expression | undefined, + cbArg: ts.Expression | undefined, + ): IrExpr => { + const receiver = streamReceiverExpr(lowerer, access.expression); const chunk = lowerer.lowerExprExpecting(args[0]!, DYN); const tail: IrExpr[] = []; if (encArg !== undefined) tail.push(lowerer.lowerExprExpecting(encArg, DYN)); @@ -4452,7 +5699,7 @@ export function lowerStreamMethodCall( args: [receiver, numLit(flags, loc), chunk, ...tail], type: BOOL, loc, - } satisfies IrExpr; + }; }; if (kind === "dyn") { if (args.length === 3) return writeEnc(args[1], args[2]); @@ -4470,7 +5717,7 @@ export function lowerStreamMethodCall( } return writeEnc(args[1], undefined); } - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); const chunk = lowerer.lowerExprExpecting(args[0]!, DYN); return { kind: "libCall", fn: "writable.writeDyn", args: [receiver, chunk], type: BOOL, loc }; } @@ -4480,7 +5727,7 @@ export function lowerStreamMethodCall( // lowered value may have collapsed to one arm (checker narrowing) — // re-dispatch on its IR type. if (args.length > 1) lowerer.noLowering("write with a union chunk and more arguments", call); - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); const chunk = lowerer.lowerExpr(args[0]!); const fn = chunk.type.kind === "union" @@ -4503,7 +5750,7 @@ export function lowerStreamMethodCall( return writeEnc(args[1], undefined); cbArg = args[1]; } - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); const chunk = kind === "bytes" ? lowerer.lowerExprExpecting(args[0]!, BYTES) @@ -4538,7 +5785,7 @@ export function lowerStreamMethodCall( encNode = args[1]; cbArg = args[2]; } - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); let flags = 0; const tail: IrExpr[] = []; if (chunkNode !== undefined) { @@ -4585,7 +5832,7 @@ export function lowerStreamMethodCall( if (member === "cork" || member === "uncork") { requireSide(writableSide, member); if (args.length !== 0) lowerer.noLowering(`${member} with arguments`, call); - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); return { kind: "libCall", fn: member === "cork" ? "writable.cork" : "writable.uncork", @@ -4605,7 +5852,7 @@ export function lowerStreamMethodCall( ); } const enc = bufEncoding(lowerer, "setEncoding", args[0]!); - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); return { kind: "libCall", fn: "readable.setEncoding", @@ -4616,18 +5863,20 @@ export function lowerStreamMethodCall( } if (member === "setDefaultEncoding") { - // The write-side default: utf8 IS the default — admit it; other - // encodings would change what write(string) means. + // The write-side default: lowercased and validated (ERR_UNKNOWN_ENCODING, + // thrown) at run time; answers the stream. requireSide(writableSide, "setDefaultEncoding"); - const t = args[0] ? lowerer.typeOf(args[0]) : null; - if (t?.isStringLiteralType() && (t.value === "utf8" || t.value === "utf-8")) { - return lowerer.lowerExpr(access.expression); - } - lowerer.noLowering( - "setDefaultEncoding", - call, - "utf8 (the default, as a compile-time literal) is the supported write-side string encoding", - ); + if (args.length > 1) + lowerer.noLowering(`setDefaultEncoding with ${args.length} arguments`, call); + const receiver = streamReceiverExpr(lowerer, access.expression); + const enc = args[0] ? lowerer.lowerExprExpecting(args[0], DYN) : dynUndefinedExpr(loc); + return { + kind: "libCall", + fn: "writable.setDefaultEncoding", + args: [receiver, enc], + type: receiver.type, + loc, + }; } return null; @@ -4772,7 +6021,7 @@ export function lowerStreamProperty( const propCall = (type: IrType): IrExpr => ({ kind: "libCall", fn: "stream.prop", - args: [lowerer.lowerExpr(expr.expression), strLit(name, loc)], + args: [streamReceiverExpr(lowerer, expr.expression), strLit(name, loc)], type, loc, }); @@ -4795,7 +6044,7 @@ export function lowerStreamProperty( const read: IrExpr = { kind: "libCall", fn: "readable.flowing", - args: [lowerer.lowerExpr(expr.expression)], + args: [streamReceiverExpr(lowerer, expr.expression)], type, loc, }; @@ -4809,7 +6058,7 @@ export function lowerStreamProperty( const read: IrExpr = { kind: "libCall", fn: "stream.errored", - args: [lowerer.lowerExpr(expr.expression)], + args: [streamReceiverExpr(lowerer, expr.expression)], type, loc, }; diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 747e396af..e36ffd09f 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -1,4 +1,6 @@ +import { isDeclBoundaryFile } from "../decl-projection.js"; import { relieveHeapPressure } from "../../heap-pressure.js"; +import { moduleNamespaceObject } from "./module-namespace-elements.js"; import { keyedHelperName } from "./stable-names.js"; import { positionTag, StructuralPositions } from "./structural-positions.js"; import { onlyForwarded, stripTypeWrappers } from "./typed-arguments.js"; @@ -67,6 +69,7 @@ import { valueStreamOf } from "./builtins/hash-stream.js"; import { lowerNetAddressCall, lowerNetAddressNew } from "./builtins/net-address.js"; import { dynUndefinedExpr, nodeThrowExpr, numLit, varRef } from "../../ir/build.js"; import { iteratorSlotAdapter } from "./iterator-adapters.js"; +import { iterableSlotValue } from "./iterable-values.js"; import type { FieldLift } from "./coercions/structural-plans.js"; import { lowerBuiltinCall } from "./builtin-calls.js"; import { CoercionState } from "./coercions/state.js"; @@ -183,7 +186,7 @@ export { newFnCtx, type FnCtx } from "./function-context.js"; */ import { dirname, relative, resolve } from "node:path"; import { bindingSource } from "../binding-source.js"; -import { tsgoPath } from "../dts-paths.js"; +import { compilerOwnedFileName, tsgoPath } from "../dts-paths.js"; import * as ts from "../ts7/adapter.js"; import { isBundledShimForwarder } from "../package-shims.js"; import { erasedTypeOnlyImport } from "../type-only-imports.js"; @@ -230,6 +233,7 @@ import { arrayOf, BOOL, canAdaptDynFuncTo, + canBoxFuncIntoDyn, canDynCheckTo, canConvertToDyn, DYN, @@ -445,6 +449,7 @@ import { lowerNestedFunctionDecl, lowerLambda, lowerFunction, + blockedBodyDiagnostics, } from "./lower-functions.js"; import { lowerArrayMethodCall } from "./containers/array-methods.js"; import { type ArrayHofInfo, fuseArrayPipelines } from "./containers/array-pipelines.js"; @@ -611,6 +616,7 @@ import { lowerForStatement, matchGroupsPatternSource, isGuardedRecordRead, + holdsEvolvingArray, } from "./lower-stmts.js"; import { type FieldTarget, @@ -1347,6 +1353,7 @@ export function lowerToIr( // travel with them: a failed statement is never left unexplained. const covered: LowerResult = { ...result, + diagnostics: [...result.diagnostics, ...remainder.reachedBlockedBodyDiagnostics], stats: { ...result.stats, statementsDynamic }, unreached: { diagnostics: rem.diagnostics, @@ -1918,6 +1925,9 @@ export class Lowerer { /** Identifiers by spelling, per file, for the generic bindings' use * ordering (runsAfterDeclaration). */ readonly bindingIdentifiers = new Map>(); + /** Per JavaScript file, the declarations of the bindings it assigns an + * own `toString` (jsAssignsOwnToString in lower-calls.ts). */ + readonly ownToStringTargets = new Map>(); /** Per-symbol result of the never-reassigned file scan * (bindingNeverReassigned — object-literal generic-method receivers). */ readonly neverReassignedCache = new Map(); @@ -2180,6 +2190,9 @@ export class Lowerer { * and erased instances). */ erasingDepth = 0; localClassInstantiations: { owner: ts.FunctionLikeDeclaration; name: string }[] = []; + /** The function-local classes of the program with an `extends` clause, whether lowering collected + * them yet or not (new-target.ts); found on first use. */ + functionLocalSubclasses: ts.ClassLikeDeclaration[] | undefined = undefined; /** Instance types (by checker type id) of the local classes generic * factory calls return, and the per-instance class each names. */ readonly localClassInstanceTypes = new Map(); @@ -2401,6 +2414,51 @@ export class Lowerer { return fallback ?? null; } + /** A fresh TypeScript data literal (`{ message: "m" }`) passed to a + * checked-dynamic parameter of a callee implemented in JavaScript, whose + * contract lets it reshape the object: a call through an untyped value, an + * untyped parameter of a JavaScript function, or a package-declared + * parameter type (a non-library `.d.ts`) that makes one of the literal's + * properties optional (zod's `normalizeParams` deletes `params.message`). + * No typed alias of a fresh literal exists, so it is built as the plain + * object the callee receives instead of a view whose record layout refuses + * that delete. Standard-library and TypeScript callees, and literals with + * methods or accessors, keep their native storage. */ + private reshapeableContextualLiteral( + literal: ts.ObjectLiteralExpression, + contextual: readonly ts.Type[], + ): boolean { + const call = literal.parent; + if ( + call === undefined || + !(ts.isCallExpression(call) || ts.isNewExpression(call)) || + !call.arguments?.nodes.includes(literal) || + literal.properties.nodes.length === 0 || + !literal.properties.nodes.every( + (prop) => + (ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) && + (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)), + ) + ) + return false; + const declaration = this.checker.getResolvedSignature(call)?.declaration; + const file = declaration ? ts.getSourceFileOfNode(declaration) : undefined; + const untyped = contextual.some((part) => part.flags & ts.TypeFlags.Any); + if (file === undefined) return untyped; + if (isJsSourceFile(file)) return untyped; + if (!file.isDeclarationFile || this.isStdlibFile(file)) return false; + if (untyped) return true; + const objects = contextual.filter((part) => part.flags & ts.TypeFlags.Object); + if (objects.length === 0) return false; + return literal.properties.nodes.some((prop) => { + const name = (prop.name as ts.Identifier | ts.StringLiteral).text; + return objects.every((part) => { + const symbol = this.checker.getPropertyOfType(part, name); + return symbol !== undefined && (symbol.flags & ts.SymbolFlags.Optional) !== 0; + }); + }); + } + /** A TypeScript object literal handed to a JavaScript checked-value * parameter whose inferred type is open (`constructor({ factory, ...options * } = {})` — the rest element admits any property): the literal names a @@ -2964,9 +3022,14 @@ export class Lowerer { * ABI and shared prototype descriptors instead of call-site specialization. */ readonly prototypeMethodAccesses = new Map(); /** Whether the program reads `Error.prototype`, the checked-dynamic - * object an ES5 constructor function can inherit from: only then can a - * value that is not an Error instance be `instanceof Error`. */ + * object an ES5 constructor function can inherit from, or has a class + * whose heritage is evaluated at run time (`extends (Base ?? Error)`): + * only then can a value that is not an Error instance be + * `instanceof Error`. */ errorPrototypeValues = false; + /** The property names the program assigns on the global object + * (property-access.ts), collected on first use. */ + globalThisWrittenNames: Set | null = null; /** Well-known symbol methods (`sym:iterator`) the program may replace * through a symbol-keyed write: an instance or prototype property, or a * reflection call. */ @@ -3190,16 +3253,22 @@ export class Lowerer { * dispatch revision needs no weak-key identity or process-global state. */ errorMethodDispatchRevision: string | null = null; errorPropertyDispatchRevision: string | null = null; + /** The program defines properties by reflection on an untyped or + * Error-shaped target, so an own accessor may replace an error's `name` + * or `message`: typed reads and writes of those slots check for one. + * Holds the first such call's location. */ + errorAccessorReflection: SrcLoc | null = null; /** Structural source positions (structural-positions.ts) for names that * stay the same when unrelated code moves. */ /** Files are named by their path relative to the entry's directory, so - * names do not depend on where the program sits, and default library - * files by their base name: two checkers install them in different - * directories. */ + * names do not depend on where the program sits, default library files + * by their base name (two checkers install them in different + * directories), and the files scriptc ships by their name inside scriptc, + * so names do not depend on where scriptc is installed either. */ readonly positions = new StructuralPositions((sf) => this.program.isSourceFileDefaultLibrary(sf) ? `lib:${sf.fileName.slice(sf.fileName.lastIndexOf("/") + 1)}` - : relative(dirname(this.entry.fileName), sf.fileName), + : (compilerOwnedFileName(sf.fileName) ?? relative(dirname(this.entry.fileName), sf.fileName)), ); /** The outermost node whose token span is exactly `loc`, or null. */ @@ -3231,7 +3300,10 @@ export class Lowerer { * before it. */ closureName(loc: SrcLoc, suffix = "", prefix = "%fn"): string { const node = this.nodeAtLoc(loc); - const where = node === null ? `${loc.file}:${loc.start}:${loc.end}` : this.positions.of(node); + const where = + node === null + ? `${compilerOwnedFileName(loc.file) ?? loc.file}:${loc.start}:${loc.end}` + : this.positions.of(node); const key = `${prefix}|${this.closureRoot}|${where}|${suffix}`; const first = keyedHelperName(this, prefix, key); const seen = this.closureNameCounts.get(first) ?? 0; @@ -3306,6 +3378,9 @@ export class Lowerer { * the class and constructed-class counts, it tells caches over the class * graph when the graph changed. */ subclassLinks = 0; + /** Function-local classes by the class node they extend, built on first + * use (localSubclassesOf in lower-classes.ts). */ + localSubclassIndex: Map | null = null; /** Set while a subclass constructor's super() call notes its base * constructor edge: that edge does not instantiate the base exactly. */ private superConstructorEdge = false; @@ -3369,8 +3444,18 @@ export class Lowerer { if (interned.has(fn.name)) this.sharedHelpers.add(fn); if (this.sharedHelpers.size === 0) return; const shared = new Map([...this.sharedHelpers].map((fn) => [fn.name, fn])); - const earlier = (left: SrcLoc, right: SrcLoc): boolean => - left.file < right.file || (left.file === right.file && left.start < right.start); + // Files scriptc ships compare by their name inside scriptc, so the + // choice does not depend on where scriptc is installed. + const fileKeys = new Map(); + const fileKey = (file: string): string => { + let key = fileKeys.get(file); + if (key === undefined) fileKeys.set(file, (key = compilerOwnedFileName(file) ?? file)); + return key; + }; + const earlier = (left: SrcLoc, right: SrcLoc): boolean => { + if (left.file === right.file) return left.start < right.start; + return fileKey(left.file) < fileKey(right.file); + }; // The earliest use of each shared helper outside the shared helpers, and // which shared helpers each shared helper uses. const uses = new Map(); @@ -3555,6 +3640,21 @@ export class Lowerer { else if (kind === "failed") s.failed += 1; } + /** The program holds the declaration-typed boundary's projection + * (decl-projection.ts): user-file record and array values crossing into + * dynamic slots (package parameters) box as live capsules, so a value + * that comes back through a boundary cast keeps its identity. */ + private declBoundaryMemo: boolean | null = null; + get declBoundary(): boolean { + this.declBoundaryMemo ??= this.program + .getSourceFiles() + .some((sf) => isDeclBoundaryFile(sf.fileName)); + return this.declBoundaryMemo; + } + /** Tracked classes' bag initializers over a typed `this` + * (class-dynamic-dispatch.ts recordBagInitializers). */ + readonly classBagInitializers = new Map(); + constructor( readonly program: ts.Program, readonly entry: ts.SourceFile, @@ -3677,8 +3777,11 @@ export class Lowerer { classImplementedProtocol: (type) => this.classImplementedProtocol(type), isStdlibFile: this.isStdlibFile, isNpmFile: this.isNpmFile, + // The declaration-typed boundary's generated types (decl-projection.ts) + // describe data, like an explicitly mapped external type surface. isExternalTypeFile: (sf) => - this.externalTypeSpecifiersByFile.has(tsgoPath(resolve(sf.fileName))), + this.externalTypeSpecifiersByFile.has(tsgoPath(resolve(sf.fileName))) || + isDeclBoundaryFile(sf.fileName), typeMemo: this.typeMemo, ...(this.generalNodeStreams ? { generalNodeStreams: true } : {}), ...(this.dynNodeStreams ? { dynNodeStreams: true } : {}), @@ -6621,7 +6724,10 @@ export class Lowerer { node.expression.name.text, node.arguments.nodes.length >= 2, ); - const callback = node.arguments.nodes[0]; + // `xs.map(((x) => x))`: the parenthesized callback is the arrow. + let callback = node.arguments.nodes[0]; + while (callback !== undefined && ts.isParenthesizedExpression(callback)) + callback = callback.expression; if (callbackIndices !== null && callback !== undefined && !ts.isSpreadElement(callback)) { const receiverNode = node.expression.expression; const receiverTs = this.typeOf(receiverNode); @@ -6680,13 +6786,26 @@ export class Lowerer { presentSites.set(node, elementsPresent && !reduce); if (elementsPresent && reduce && retypeInlineCallback(callback, callbackIndices)) changed = true; - if ( - (method === "reduce" || method === "reduceRight") && - ((!elementsPresent && node.arguments.nodes.length < 2) || - callbackReturnsOptional(callback)) - ) { - if (promoteHofCallback(callback, [0, 1])) changed = true; - const result = this.runtimeOptionalType(this.irTypeOf(node)); + // The accumulator holds undefined when the callback can return + // it, when an initial value can be missing, or when elements can + // be absent and there is no initial value. It holds another + // primitive when the callback's result can be one at run time + // (`missing[0] ?? "fallback"`). + const initial = node.arguments.nodes[1]; + const optional = + (!elementsPresent && initial === undefined) || + (initial !== undefined && !ts.isSpreadElement(initial) && mayBeOptional(initial)) || + callbackReturnsOptional(callback); + const arithmetic = + reduce && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) + ? lambdaArithmeticReturn(callback) + : null; + if (reduce && (optional || arithmetic !== null)) { + if (optional && promoteHofCallback(callback, [0, 1])) changed = true; + let result = this.irTypeOf(node); + if (arithmetic !== null) + result = joinPrimitiveTypes([result, arithmetic]) ?? result; + if (optional) result = this.runtimeOptionalType(result); siteDependencies?.read(node); const previous = this.runtimeOptionalReduceTypes.get(node); if (!previous || !typeEquals(previous, result)) { @@ -7446,7 +7565,10 @@ export class Lowerer { if (sig && !this.wantBody(sig.name)) continue; const fn = this.lowerFunction(decl); if (fn) functions.push(fn); - else if (this.countsSkips()) this.countSkippedBody(decl); + else if (this.countsSkips()) { + this.countSkippedBody(decl); + if (this.remainder) this.reportBlockedBody(decl); + } } for (const decl of fp.classDecls) { const info = this.classes.get(this.classNamer(decl)); @@ -7679,6 +7801,7 @@ export class Lowerer { entry: ENTRY_NAME, ...(this.usesWorkers ? { workers: true } : {}), ...(this.usesLifetimeRegions ? { lifetimeRegions: true as const } : {}), + ...(this.declBoundary ? { declBoundary: true as const } : {}), ...(this.threadSlotKeys.size > 0 ? { threadSlotKeys: [...this.threadSlotKeys].sort() } : {}), @@ -7777,6 +7900,19 @@ export class Lowerer { } } + /** Coverage remainder: the body diagnostics of a function whose + * signature blocked (blockedBodyDiagnostics). A function reachable emit + * reached (its signature's diagnostics flushed there) reports them with + * the reached diagnostics; any other reports them as unreached. */ + readonly reachedBlockedBodyDiagnostics: ScrDiagnostic[] = []; + private reportBlockedBody(decl: ts.FunctionDeclaration): void { + const diags = blockedBodyDiagnostics(this, decl); + const symbol = declSymbolOf(this, decl); + if (symbol && this.alreadyFlushed.has(symbol)) + this.reachedBlockedBodyDiagnostics.push(...diags); + else for (const d of diags) this.pushDiag(d); + } + /** Whether run() counts a signature-blocked declaration in * stats.functionsSkipped: whole-program passes and the coverage * remainder do; an externally-gated emit pass leaves the counting to the @@ -9300,6 +9436,21 @@ export class Lowerer { * type-mapper.ts's chalk-shape mapping — call paths and func-slot coercions * share it. */ hybridCallUnwrap(expr: IrExpr): IrExpr { + // A callable CLASS instance: a class whose merged interface carries a + // call signature stores its call behavior in a `%call` field (the + // chalk package shim's builder). Calls read the field. + if (expr.type.kind === "object") { + const callType = this.classes.get(expr.type.className)?.fields.get("%call"); + if (callType === undefined || callType.kind !== "func") return expr; + return { + kind: "fieldGet", + obj: expr, + className: expr.type.className, + field: "%call", + type: callType, + loc: expr.loc, + }; + } if (expr.type.kind !== "record") return expr; const shape = this.shapes.get(expr.type.shapeId); const call = shape?.fields.find((f) => f.name === "%call"); @@ -9391,6 +9542,22 @@ export class Lowerer { } if (expr.type.kind === "void" && expected.kind !== "void" && this.neverValued.has(expr)) return this.divergentValue(expr, expected); + // A builtin error constructor global stored in a slot of its class value + // type (`{ Parent: Error }`): the class value itself, which a checked + // cast of the dynamic constructor value could not produce. + if ( + expected.kind === "classval" && + expr.kind === "libCall" && + expr.fn === "dyn.globalConstructor" && + expr.args[0]?.kind === "strLit" && + `%${expr.args[0].value}` === expected.className + ) { + const info = this.classes.get(expected.className); + if (info?.builtinError && info.valueCtorParams !== undefined) { + this.defineClassValueConstruct(info); + return { kind: "classRef", className: info.def.name, type: expected, loc: expr.loc }; + } + } // process.stdout/stderr into a general Writable slot (general-streams.ts). const stdioStream = stdioIntoGeneralStream(this, expr, expected); if (stdioStream) return this.coerceToExpected(stdioStream, expected); @@ -9400,6 +9567,11 @@ export class Lowerer { const adapted = iteratorSlotAdapter(this, expr, expected); if (adapted) return adapted; } + // Iterable-typed slots: other iterators enter the generator arm. + if (expected.kind === "union" && !typeEquals(expr.type, expected)) { + const iterable = iterableSlotValue(this, expr, expected); + if (iterable) return iterable; + } // an any-typed slot marshals implicitly (tsc allows the assignment; // the marshal is where its semantics live). OUT: an 'any' value @@ -9492,6 +9664,13 @@ export class Lowerer { // constructor. `require("events")` is the EventEmitter class itself, // so its module binding is too (`ReleaseEmitter = EventEmitter` over // `const EventEmitter = require("events")`). + if (expr.type.kind === "moduleNs" && isJsSourceFileName(expr.loc.file)) { + const object = moduleNamespaceObject( + this, + expr as IrExpr & { type: IrType & { kind: "moduleNs" } }, + ); + if (object) return object; + } if ( expr.type.kind === "moduleNs" && expr.type.moduleId === "builtin:events" && @@ -9630,9 +9809,27 @@ export class Lowerer { // A HYBRID (function-with-properties) record flowing into a plain // func slot extracts its reserved %call field — the chalk shape's // "the value IS callable" half (type-mapper.ts's hybrid mapping). - if (expected.kind === "func" && expr.type.kind === "record") { + if (expected.kind === "func" && (expr.type.kind === "record" || expr.type.kind === "object")) { const un = this.hybridCallUnwrap(expr); if (un !== expr && typeEquals(un.type, expected)) return un; + // A callable class instance (the chalk shim's variadic builder) in a + // narrower function slot: its call closure crosses the checked + // function boundary, which adapts the arity. + if ( + un !== expr && + expr.type.kind === "object" && + un.type.kind === "func" && + canBoxFuncIntoDyn( + un.type, + (id) => this.shapes.get(id), + (id) => this.unions.get(id), + ) + ) { + return this.coerceToExpected( + { kind: "dynFrom", value: un, type: DYN, loc: un.loc }, + expected, + ); + } } // A zero-param function whose RETURN is a wider record array than the // slot's (`getRoutes: () => cachedRoutes` against `() => Narrow[]`): @@ -10480,6 +10677,46 @@ export class Lowerer { return this.coerceInto(node, value, expected); } + /** The right side of a write to a member of a typed view of the global + * object (`(globalThis as typeof globalThis & { k?: T }).k = value`, also + * through `??=` and `||=`) that is `value` itself or wraps only it in + * parentheses or `satisfies`; null when `value` is not in such a write. */ + private globalViewSlot(value: ts.Expression): ts.Expression | null { + let operand: ts.Expression = value; + while ( + operand.parent !== undefined && + (ts.isParenthesizedExpression(operand.parent) || ts.isSatisfiesExpression(operand.parent)) + ) + operand = operand.parent; + const write = operand.parent; + if ( + write === undefined || + !ts.isBinaryExpression(write) || + write.right !== operand || + (write.operatorToken.kind !== ts.SyntaxKind.EqualsToken && + write.operatorToken.kind !== ts.SyntaxKind.QuestionQuestionEqualsToken && + write.operatorToken.kind !== ts.SyntaxKind.BarBarEqualsToken) + ) + return null; + let target = write.left; + while (ts.isParenthesizedExpression(target)) target = target.expression; + if (!ts.isPropertyAccessExpression(target) && !ts.isElementAccessExpression(target)) + return null; + const receiver = this.typeOf(target.expression); + const isGlobal = (type: ts.Type): boolean => { + const symbol = type.getSymbol(); + return ( + symbol !== undefined && + ts.symbolName(symbol) === "globalThis" && + symbol.declarations.length === 0 + ); + }; + return isGlobal(receiver) || + (receiver.isIntersectionType() && ts.constituentTypes(receiver).some(isGlobal)) + ? operand + : null; + } + /** Lower for a known destination, then apply ordinary value coercion. * Fresh arrays and records use the destination layout directly. */ lowerExprExpecting(node: ts.Expression, expected: IrType | undefined): IrExpr { @@ -10547,6 +10784,21 @@ export class Lowerer { (element) => !ts.isSpreadElement(element) && !ts.isOmittedExpression(element), ) ) { + // A checked slot that the program declares as a native array (a + // member of a typed view of the global object, `typeof globalThis & + // { cache?: number[] }`) is read back through that declaration. Build + // the native array and store it boxed: every typed read then extracts + // that same array (a dynamic literal would be copied on each read, + // losing writes such as push). + const slot = this.globalViewSlot(x); + const slotType = slot ? this.checker.getContextualType(slot) : undefined; + const declared = slotType && this.checker.getNonNullableType(slotType); + const native = + declared && !(declared.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) + ? this.mapTypeOf(declared) + : null; + if (native?.kind === "array") + return this.coerceInto(node, this.lowerArrayLiteral(x, native), DYN); return { kind: "dynArrLit", elems: x.elements.nodes.map((element) => this.lowerExprExpecting(element, DYN)), @@ -10575,7 +10827,9 @@ export class Lowerer { builtinOption("UrlObject") || builtinOption("URLFormatOptions") || !this.mapTypeOf(this.typeOf(x)) || - this.excessJsContextualProperty(x, parts) + this.excessJsContextualProperty(x, parts) || + this.reshapeableContextualLiteral(x, parts) || + holdsEvolvingArray(this, x) ) { return lowerDynObjectLiteral(this, x); } @@ -10790,9 +11044,29 @@ export class Lowerer { // Contextual types (such as Proxy descriptors' symbol-valued fields) // must not force an intermediate record with incompatible field slots. if (this.returnsLiteralIntoDyn(node, expected)) return this.lowerExprExpecting(node, expected); + if (this.returnsFreshCollection(node) && expected.kind !== "void") + return this.lowerExprExpecting(node, expected); return this.asyncReturnValueOf(node, this.lowerExpr(node), expected); } + /** An array literal or a `new Map(...)`, `new Set(...)` or `new + * Array(...)` of the builtins: a fresh collection, never a thenable, so + * an async return takes the declared result type as its construction + * layout the way a synchronous return does (`return []` in a function + * whose inferred result is `Promise`). */ + private returnsFreshCollection(node: ts.Expression): boolean { + let e = node; + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (ts.isArrayLiteralExpression(e)) return true; + return ( + ts.isNewExpression(e) && + ts.isIdentifier(e.expression) && + (this.isStdlibGlobal(e.expression, "Map") || + this.isStdlibGlobal(e.expression, "Set") || + this.isStdlibGlobal(e.expression, "Array")) + ); + } + /** A fresh JS object literal returned into a checked-native (dyn) result, * which lowers straight into that storage. */ private returnsLiteralIntoDyn(node: ts.Expression, expected: IrType): boolean { @@ -10924,7 +11198,11 @@ export class Lowerer { loc, }; } - if (this.adoptsReturnedPromise() && !this.returnsLiteralIntoDyn(node, expected)) { + if ( + this.adoptsReturnedPromise() && + !this.returnsLiteralIntoDyn(node, expected) && + !this.returnsFreshCollection(node) + ) { // The async return lowers its operand untyped first (a promise // adopts before its payload converts); a maybe-promise union needs // the statement form, which branches between adoption and fulfillment. @@ -11387,8 +11665,8 @@ export class Lowerer { } } - lowerClassExpression(expr: ts.ClassLikeDeclaration): IrExpr { - return lowerClassExpression(this, expr); + lowerClassExpression(expr: ts.ClassLikeDeclaration, binding?: IrLocal): IrExpr { + return lowerClassExpression(this, expr, binding); } /* ── the class graph (single inheritance) ─────────────────────────── */ diff --git a/packages/compiler/src/frontend/lowering/module-namespace-elements.ts b/packages/compiler/src/frontend/lowering/module-namespace-elements.ts index d2425750b..cf828c6f0 100644 --- a/packages/compiler/src/frontend/lowering/module-namespace-elements.ts +++ b/packages/compiler/src/frontend/lowering/module-namespace-elements.ts @@ -1,6 +1,13 @@ import { dynUndefinedExpr, strLit, varRef } from "../../ir/build.js"; import * as ts from "../ts7/adapter.js"; -import { DYN, STRING, type IrExpr, type IrFunction, type IrStmt } from "../../ir/ir.js"; +import { + DYN, + STRING, + type IrExpr, + type IrFunction, + type IrStmt, + type IrType, +} from "../../ir/ir.js"; import { locOf } from "../program.js"; import { newFnCtx, type Lowerer } from "./lowerer.js"; @@ -25,12 +32,13 @@ export function lowerModuleNamespaceElement( try { const body: IrStmt[] = []; const symbol = lowerer.checker.getSymbolAtLocation(source); - symbol?.getExports().forEach((exported, key) => { + for (const exported of symbol ? lowerer.checker.getExportsOfModule(symbol) : []) { + const key = ts.symbolName(exported); const target = exported.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(exported) : exported; - if (!(target.flags & ts.SymbolFlags.Value)) return; + if (!(target.flags & ts.SymbolFlags.Value)) continue; const declaration = target.valueDeclaration; const identifier = declaration?.name as ts.Node | undefined; if (!identifier || !ts.isIdentifier(identifier)) { @@ -43,7 +51,7 @@ export function lowerModuleNamespaceElement( kind: "strEq", negated: false, left: varRef("key", STRING, loc), - right: strLit(String(key), loc), + right: strLit(key, loc), type: { kind: "bool" }, loc, }, @@ -51,7 +59,7 @@ export function lowerModuleNamespaceElement( else_: null, loc, }); - }); + } body.push({ kind: "return", value: dynUndefinedExpr(loc), loc }); const fn: IrFunction = { name, @@ -88,3 +96,78 @@ export function lowerModuleNamespaceElement( }; return { kind: "call", callee: name, args: [receiver, text], type: DYN, loc }; } + +/** A module namespace taken as an untyped value (`{ ...ns }`, `{ iso: ns }` + * in JavaScript): an object holding each value export, keyed in the + * namespace's sorted key order. The copy is exact while every export is a + * binding that never changes after initialization (const, function or + * class); null for other namespaces, which keep their fence. */ +export function moduleNamespaceObject( + lowerer: Lowerer, + ns: IrExpr & { type: IrType & { kind: "moduleNs" } }, +): IrExpr | null { + const source = lowerer.sourceFileOfModuleNamespace(ns.type); + if (!source) return null; + const symbol = lowerer.checker.getSymbolAtLocation(source); + if (!symbol) return null; + const exports: { key: string; identifier: ts.Identifier }[] = []; + let exact = true; + // The module's resolved exports, which include `export *` members. + for (const exported of lowerer.checker.getExportsOfModule(symbol)) { + const key = ts.symbolName(exported); + const target = + exported.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(exported) : exported; + if (!(target.flags & ts.SymbolFlags.Value)) continue; + const declaration = target.valueDeclaration; + const identifier = declaration?.name as ts.Node | undefined; + const immutable = + declaration !== undefined && + (ts.isFunctionDeclaration(declaration) || + ts.isClassDeclaration(declaration) || + (ts.isVariableDeclaration(declaration) && + (ts.getCombinedNodeFlags(declaration) & ts.NodeFlags.Const) !== 0)); + if (!identifier || !ts.isIdentifier(identifier) || !immutable) { + exact = false; + break; + } + exports.push({ key, identifier }); + } + if (!exact) return null; + exports.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0)); + const name = `%module.object:${ns.type.moduleId}`; + const loc = ns.loc; + if (!lowerer.liftedFns.some((fn) => fn.name === name)) { + const context = newFnCtx(false, null, null, DYN); + const previousClass = lowerer.currentClass; + lowerer.currentClass = null; + lowerer.fnStack.push(context); + try { + const fields = exports.map(({ key, identifier }) => ({ + key: strLit(key, loc), + value: lowerer.lowerExprExpecting(identifier, DYN), + })); + if (fields.some((field) => field.value.type.kind !== "dyn")) return null; + const fn: IrFunction = { + name, + params: [], + returnType: DYN, + locals: [...context.locals], + body: [{ kind: "return", value: { kind: "dynObjLit", fields, type: DYN, loc }, loc }], + loc, + }; + lowerer.liftedFns.push(fn); + } finally { + lowerer.fnStack.pop(); + lowerer.currentClass = previousClass; + } + } + const call: IrExpr = { kind: "call", callee: name, args: [], type: DYN, loc }; + if (ns.kind === "moduleNsRef") return call; + return { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: ns, loc }], + result: call, + type: DYN, + loc, + }; +} diff --git a/packages/compiler/src/frontend/lowering/new-target.ts b/packages/compiler/src/frontend/lowering/new-target.ts new file mode 100644 index 000000000..83994ca6f --- /dev/null +++ b/packages/compiler/src/frontend/lowering/new-target.ts @@ -0,0 +1,357 @@ +/* `new.target`. Compiled code has no invocation record, but inside a class + * constructor `new.target` is always the class of the object under + * construction: a compiled `new` allocates the instance with its concrete + * class before any constructor of the chain runs, and every constructor of + * the chain receives that instance as `this`. The program's class + * hierarchy is closed, so the value is a static dispatch over the + * constructor's class and its descendants: the deepest class the instance + * is an instance of is its exact class. + * + * The checker rejects `new.target` in methods, accessors, static blocks and + * field initializers. Plain functions (constructible in JavaScript) keep + * the refusal. */ +import * as ts from "../ts7/adapter.js"; +import { BOOL, STRING, type IrExpr, type IrType, type SrcLoc } from "../../ir/ir.js"; +import { varRef } from "../../ir/build.js"; +import { locOf } from "../program.js"; +import type { Lowerer } from "./lowerer.js"; +import type { ClassInfo } from "./lower-classes.js"; +import { classValueRef, functionLocalClass } from "./lower-classes.js"; + +export function isNewTarget(node: ts.Node): node is ts.MetaProperty { + return ( + ts.isMetaProperty(node) && + node.keywordToken === ts.SyntaxKind.NewKeyword && + node.name.text === "target" + ); +} + +/** Where `new.target` is bound: a class constructor, or a plain function + * (refused). */ +type Binding = + | { kind: "constructor"; declaration: ts.ConstructorDeclaration } + | { kind: "function" }; + +function bindingOf(node: ts.Node): Binding { + for (let p = node.parent; p !== undefined; p = p.parent) { + if (ts.isArrowFunction(p)) continue; + if (ts.isConstructorDeclaration(p)) return { kind: "constructor", declaration: p }; + if (ts.isFunctionLike(p) || ts.isSourceFile(p)) return { kind: "function" }; + } + return { kind: "function" }; +} + +interface Dispatch { + info: ClassInfo; + /** The constructor's descendants, deepest first, then its class. A + * generic family stands for its instantiations, also those that later + * bodies instantiate: its interval holds them all. */ + classes: ClassInfo[]; + self: IrExpr | null; +} + +function depth(info: ClassInfo): number { + let d = 0; + for (let c = info.base; c; c = c.base) d++; + return d; +} + +/** The function-local classes of the program that extend a class. */ +function functionLocalSubclasses(lowerer: Lowerer): ts.ClassLikeDeclaration[] { + let found = lowerer.functionLocalSubclasses; + if (found === undefined) { + const classes: ts.ClassLikeDeclaration[] = []; + for (const sf of lowerer.moduleOrder) + ts.walkPreorder(sf, (node) => { + if ( + (ts.isClassDeclaration(node) || ts.isClassExpression(node)) && + baseExpressionOf(node) !== undefined && + functionLocalClass(node) + ) + classes.push(node); + }); + lowerer.functionLocalSubclasses = found = classes; + } + return found; +} + +function baseExpressionOf(node: ts.ClassLikeDeclaration): ts.Expression | undefined { + let base = node.heritageClauses?.nodes.find( + (clause) => clause.token === ts.SyntaxKind.ExtendsKeyword, + )?.types?.nodes[0]?.expression; + while (base !== undefined && ts.isParenthesizedExpression(base)) base = base.expression; + return base; +} + +/** Whether the class `node` extends the class of `info` (for an + * instantiation, its family), directly or through other classes, as far as + * its `extends` clauses name classes. */ +function derivesFrom( + lowerer: Lowerer, + node: ts.ClassLikeDeclaration, + info: ClassInfo, + seen: Set, +): boolean { + if (seen.has(node)) return false; + seen.add(node); + const base = baseExpressionOf(node); + const name = + base === undefined + ? undefined + : ts.isIdentifier(base) + ? base + : ts.isPropertyAccessExpression(base) + ? base.name + : undefined; + let symbol = name === undefined ? undefined : lowerer.checker.getSymbolAtLocation(name); + if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) + symbol = lowerer.checker.getAliasedSymbol(symbol); + if (symbol === undefined) return false; + const named = lowerer.classBySymbol.get(symbol); + if (named !== undefined) + return ( + named === info || + named === info.genericInstance?.family || + lowerer.isSubclassOf(named.def.name, info.def.name) + ); + // A class lowering has not collected: through its declaration. + for (const declaration of symbol.declarations) { + let cls: ts.Node | undefined = declaration; + if (ts.isVariableDeclaration(cls)) { + cls = cls.initializer; + while (cls !== undefined && ts.isParenthesizedExpression(cls)) cls = cls.expression; + } + if ( + cls !== undefined && + (ts.isClassDeclaration(cls) || ts.isClassExpression(cls)) && + derivesFrom(lowerer, cls, info, seen) + ) + return true; + } + return false; +} + +/** `node`'s name, quoted, for diagnostics. */ +function localClassName(node: ts.ClassLikeDeclaration): string { + if (node.name) return `'${node.name.text}'`; + const parent = node.parent; + return parent && ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name) + ? `'${parent.name.text}'` + : "a class expression"; +} + +/** The classes `new.target` can denote in the constructor being lowered, + * or null after a diagnostic. */ +function dispatchOf(lowerer: Lowerer, expr: ts.MetaProperty): Dispatch | null { + const binding = bindingOf(expr); + if (binding.kind !== "constructor") return null; + const info = lowerer.currentClass; + const cls = binding.declaration.parent; + if (!info || (info.decl !== null && info.decl !== cls)) { + lowerer.unsupported( + "SC1090", + expr, + "'new.target' in this constructor (its class has no lowering here)", + ); + return null; + } + // An instantiation's subclasses can also be instantiations of generic + // subclasses of its family (`class G extends Box` under Box). + const family = info.genericInstance?.family; + const descendants: ClassInfo[] = []; + for (const other of lowerer.classes.values()) { + if (other === info) continue; + if ( + lowerer.isSubclassOf(other.def.name, info.def.name) || + (family !== undefined && + other.generic !== undefined && + other !== family && + lowerer.isSubclassOf(other.def.name, family.def.name)) + ) + descendants.push(other); + } + descendants.sort((a, b) => depth(b) - depth(a)); + const classes = [...descendants, info]; + for (const c of classes) { + if (c.localClass) { + lowerer.unsupported( + "SC1090", + expr, + `'new.target' where '${c.def.jsName ?? c.def.name}' is a function-local class (each evaluation creates a new class)`, + ); + return null; + } + } + // Lowering collects a function-local class with the body declaring it, + // which may come after this constructor. + for (const local of functionLocalSubclasses(lowerer)) { + if (!derivesFrom(lowerer, local, info, new Set())) continue; + lowerer.unsupported( + "SC1090", + expr, + `'new.target' where ${localClassName(local)} is a function-local class (each evaluation creates a new class)`, + ); + } + let self: IrExpr | null = null; + if (classes.length > 1) { + const thisLocal = lowerer.resolveThis(expr); + if (!thisLocal || thisLocal.type.kind !== "object") { + lowerer.unsupported( + "SC1090", + expr, + "'new.target' where the constructor's 'this' has no class representation", + ); + return null; + } + self = varRef(thisLocal.id, thisLocal.type, locOf(expr)); + } + return { info, classes, self }; +} + +/** A conditional chain selecting `arm(D)` for the exact class D of the + * instance under construction. */ +function select( + dispatch: Dispatch, + type: IrType, + loc: SrcLoc, + arm: (info: ClassInfo) => IrExpr, +): IrExpr { + const { classes, self } = dispatch; + let result = arm(classes[classes.length - 1]!); + for (let i = classes.length - 2; i >= 0; i--) { + const c = classes[i]!; + result = { + kind: "ternary", + cond: { kind: "instanceOf", value: self!, className: c.def.name, type: BOOL, loc }, + then: arm(c), + else_: result, + type, + loc, + }; + } + return result; +} + +function refuseFunction(lowerer: Lowerer, expr: ts.MetaProperty): never { + return lowerer.unsupported( + "SC1090", + expr, + "'new.target' in a plain function (a constructible function's invocation record does not exist in compiled code; use a class constructor)", + ); +} + +/** `new.target` as a value. */ +export function lowerNewTarget(lowerer: Lowerer, expr: ts.MetaProperty): IrExpr { + const loc = locOf(expr); + if (bindingOf(expr).kind === "function") refuseFunction(lowerer, expr); + const dispatch = dispatchOf(lowerer, expr)!; + const type: IrType = { kind: "classval", className: dispatch.info.def.name }; + for (const c of dispatch.classes) { + if (c.generic) { + lowerer.unsupported( + "SC1090", + expr, + `'new.target' as a value where '${c.def.jsName ?? c.def.name}' is a generic subclass (its instantiations have no common class value; read 'new.target.name' or compare 'new.target' with a class instead)`, + ); + } + if (c !== dispatch.info && !lowerer.ctorAbiEquals(c, dispatch.info)) { + lowerer.unsupported( + "SC1090", + expr, + `'new.target' as a value where '${c.def.jsName ?? c.def.name}' constructs with a different signature than '${dispatch.info.def.jsName ?? dispatch.info.def.name}' (read 'new.target.name' or compare 'new.target' with a class instead)`, + ); + } + } + return select(dispatch, type, loc, (c) => + lowerer.coerceToExpected(classValueRef(lowerer, c, expr), type), + ); +} + +/** `new.target.name`: the exact class's name, without materializing a + * class value (subclasses may construct with other signatures). */ +export function lowerNewTargetName( + lowerer: Lowerer, + access: ts.PropertyAccessExpression, +): IrExpr | null { + if (access.questionDotToken || access.name.text !== "name") return null; + let target: ts.Expression = access.expression; + while (ts.isParenthesizedExpression(target)) target = target.expression; + if (!isNewTarget(target) || bindingOf(target).kind !== "constructor") return null; + const dispatch = dispatchOf(lowerer, target); + if (!dispatch) return null; + const loc = locOf(access); + return select(dispatch, STRING, loc, (c) => { + // An own static `name` field replaces the class's name; a subclass has + // its own name. An instantiation's statics are its family's. + const owner = c.genericInstance?.family ?? c; + const field = owner.staticFields.find((f) => f.name === "name"); + if (field !== undefined) { + if (field.type.kind !== "string") + lowerer.unsupported( + "SC1090", + access, + `'new.target.name' where '${c.def.jsName ?? c.def.name}' declares a static 'name' that is not a string`, + ); + return { kind: "varRef", localId: field.globalId, type: field.type, loc }; + } + if (owner.staticMethods?.has("name") || owner.genericStatics?.has("name")) + lowerer.unsupported( + "SC1090", + access, + `'new.target.name' where '${c.def.jsName ?? c.def.name}' declares a static method 'name'`, + ); + // A generic family's instantiations share its name. + if (c.generic) + return { kind: "strLit", value: c.def.jsName ?? c.generic.baseName, type: STRING, loc }; + return { + kind: "libCall", + fn: "class.name", + args: [classValueRef(lowerer, c, target)], + type: STRING, + loc, + }; + }); +} + +/** `new.target === C` (and `!==`, `==`, `!=`, either operand order) with a + * program class named by the other operand: an exact-class test. */ +export function lowerNewTargetComparison( + lowerer: Lowerer, + expr: ts.BinaryExpression, +): IrExpr | null { + const op = expr.operatorToken.kind; + const negated = + op === ts.SyntaxKind.ExclamationEqualsEqualsToken || + op === ts.SyntaxKind.ExclamationEqualsToken; + if ( + !negated && + op !== ts.SyntaxKind.EqualsEqualsEqualsToken && + op !== ts.SyntaxKind.EqualsEqualsToken + ) + return null; + const strip = (e: ts.Expression): ts.Expression => { + while (ts.isParenthesizedExpression(e)) e = e.expression; + return e; + }; + const left = strip(expr.left); + const right = strip(expr.right); + const target = isNewTarget(left) ? left : isNewTarget(right) ? right : null; + if (!target || bindingOf(target).kind !== "constructor") return null; + const other = target === left ? right : left; + if (!ts.isIdentifier(other)) return null; + // A constructor's new.target is never undefined. + if (other.text === "undefined" && (lowerer.typeOf(other).flags & ts.TypeFlags.Undefined) !== 0) + return { kind: "boolLit", value: negated, type: BOOL, loc: locOf(expr) }; + const symbol = lowerer.resolveValueSymbol(other); + const named = symbol ? lowerer.classBySymbol.get(symbol) : undefined; + if (!named || named.generic || named.localClass) return null; + const dispatch = dispatchOf(lowerer, target); + if (!dispatch) return null; + const loc = locOf(expr); + return select(dispatch, BOOL, loc, (c) => ({ + kind: "boolLit", + value: (c === named) !== negated, + type: BOOL, + loc, + })); +} diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 0167adf12..8b826304b 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -1590,6 +1590,12 @@ export const BUILTIN_MODULE_FNS: Record< createDiffieHellman: { fn: "crypto.dhNew", params: [DYN, DYN, DYN, DYN], result: DYN }, getDiffieHellman: { fn: "crypto.dhGroupNew", params: [DYN], result: DYN }, createDiffieHellmanGroup: { fn: "crypto.dhGroupNew", params: [DYN], result: DYN }, + // Prime checks and generation (lowerPrimeCall): checked-dynamic + // arguments, results converted to the call site's type. + checkPrimeSync: { fn: "crypto.checkPrimeSync", params: [], result: DYN }, + checkPrime: { fn: "crypto.checkPrimeCb", params: [], result: VOID }, + generatePrimeSync: { fn: "crypto.generatePrimeSync", params: [], result: DYN }, + generatePrime: { fn: "crypto.generatePrimeCb", params: [], result: VOID }, }, zlib: { // The default-options convenience family is special-cased in @@ -3099,7 +3105,8 @@ export const BUILTIN_MODULE_FENCE_HINTS: Record< ["getCipherInfo"].map((m) => [ m, "cipher metadata has no lowering — createCipheriv and createDecipheriv support " + - "aes-128/192/256 in cbc, ctr, and gcm modes and chacha20-poly1305", + "aes-128/192/256 in cbc, ctr, gcm, ecb, cfb, cfb8, cfb1, and ofb modes, des-ede3 and " + + "des-ede, chacha20, and chacha20-poly1305", ]), ), setFips: @@ -3368,6 +3375,17 @@ export function isStdlibSymbol(lowerer: Lowerer, symbol: ts.Symbol | undefined): * tamper-guard prologue) registered in stdlibGlobalAliases; * - a const binding of the Map or Set constructor (`const M = Map`, * builtinClassAliasOf). */ +/** The symbol an identifier reads. A shorthand property's name (`{ Date }`) + * reads the binding of the same name, not the property it declares. */ +export function identifierReadSymbol(lowerer: Lowerer, expr: ts.Identifier): ts.Symbol | undefined { + if (expr.parent && ts.isShorthandPropertyAssignment(expr.parent) && expr.parent.name === expr) + return ( + lowerer.checker.getShorthandAssignmentValueSymbol(expr.parent) ?? + lowerer.checker.getSymbolAtLocation(expr) + ); + return lowerer.checker.getSymbolAtLocation(expr); +} + export function stdlibGlobalNameOf(lowerer: Lowerer, expr: ts.Expression): string | null { if (ts.isParenthesizedExpression(expr)) return stdlibGlobalNameOf(lowerer, expr.expression); // The global-object detection chain (`typeof global < "u" ? global : @@ -3379,7 +3397,7 @@ export function stdlibGlobalNameOf(lowerer: Lowerer, expr: ts.Expression): strin return stdlibGlobalNameOf(lowerer, taken ? expr.whenTrue : expr.whenFalse); } if (ts.isIdentifier(expr)) { - const symbol = lowerer.checker.getSymbolAtLocation(expr); + const symbol = identifierReadSymbol(lowerer, expr); // `globalThis` itself: a reserved intrinsic — tsc rejects user // global bindings, but local parameters can shadow it. Its special // symbol has no ordinary declarations for the provenance check. diff --git a/packages/compiler/src/frontend/lowering/undefined-index.ts b/packages/compiler/src/frontend/lowering/undefined-index.ts new file mode 100644 index 000000000..5226d7008 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/undefined-index.ts @@ -0,0 +1,101 @@ +import * as ts from "../ts7/adapter.js"; +import { BOOL, DYN, F64, UNDEFINED_T, type IrExpr, type IrStmt, type SrcLoc } from "../../ir/ir.js"; +import { varRef } from "../../ir/build.js"; +import type { Lowerer } from "./lowerer.js"; +import { lowerOptionalNumber } from "./lower-exprs.js"; +import { keyedHelperName } from "./stable-names.js"; + +/** An element index the checker types as a number can be undefined at run + * time (an element read from a holey array). JavaScript then names the + * property "undefined", not "NaN". */ + +/** The array element index `operand` (the lowered index of `node`) as a + * number. An undefined index is the runtime's "undefined" key + * (`array.undefinedKey`, a NaN the array's property table spells + * "undefined"); other values convert with ToNumber, as lowerOptionalNumber + * does. */ +export function lowerOptionalIndex( + lowerer: Lowerer, + operand: IrExpr, + loc: SrcLoc, + node: ts.Expression, +): IrExpr { + const split = splitUndefinedIndex(lowerer, operand, node, loc); + if (split === null) return lowerOptionalNumber(lowerer, operand, loc, node); + return { + kind: "seqExpr", + stmts: split.stmts, + result: { + kind: "ternary", + cond: split.isUndefined, + then: { kind: "libCall", fn: "array.undefinedKey", args: [], type: F64, loc }, + else_: split.index, + type: F64, + loc, + }, + type: F64, + loc, + }; +} + +/** A typed-array element index that can be undefined: `stmts` evaluate it + * once, `isUndefined` tests it, and `index` is the number a defined index + * converts to. */ +export interface SplitIndex { + stmts: IrStmt[]; + isUndefined: IrExpr; + index: IrExpr; +} + +/** Splits `raw`, the lowered index of `node`, when it is a number that can + * be undefined; null otherwise. */ +export function splitUndefinedIndex( + lowerer: Lowerer, + raw: IrExpr, + node: ts.Expression, + loc: SrcLoc, +): SplitIndex | null { + if (raw.type.kind !== "union") return null; + const undefinedTag = lowerer.armTag(raw.type.unionId, UNDEFINED_T); + if (undefinedTag < 0) return null; + const local = lowerer.declareHiddenLocal("%elementIndex", raw.type); + const stable = varRef(local.id, raw.type, loc); + const index = lowerOptionalNumber(lowerer, stable, loc, node); + if (index.type.kind !== "f64") return null; + return { + stmts: [{ kind: "varDecl", localId: local.id, init: raw, loc }], + isUndefined: { + kind: "unionIsTag", + unionId: raw.type.unionId, + tag: undefinedTag, + negated: false, + value: stable, + type: BOOL, + loc, + }, + index, + }; +} + +/** A typed array has no property named "undefined" to store into: the + * store throws the catchable SC1090 error when it runs. A call, so that it + * can sit among an expression's statements. */ +export function undefinedTypedArrayKeyFence(lowerer: Lowerer, node: ts.Node, loc: SrcLoc): IrStmt { + const line = ts.getLineAndCharacterOfPosition(ts.getSourceFileOfNode(node), loc.start).line + 1; + const message = `typed array properties named 'undefined' (an element index that is undefined) are not supported yet [SC1090 at ${loc.file}:${line}]`; + const key = `index.undefinedFence:${message}`; + let name = lowerer.arrHofHelpers.get(key); + if (name === undefined) { + name = keyedHelperName(lowerer, "%index.undefinedFence", key); + lowerer.arrHofHelpers.set(key, name); + lowerer.liftedFns.push({ + name, + params: [], + locals: [], + returnType: DYN, + body: [{ kind: "runtimeFence", code: "SC1090", message, loc }], + loc, + }); + } + return { kind: "exprStmt", expr: { kind: "call", callee: name, args: [], type: DYN, loc }, loc }; +} diff --git a/packages/compiler/src/frontend/lowering/width-lift.ts b/packages/compiler/src/frontend/lowering/width-lift.ts index e3722bc32..126f80f93 100644 --- a/packages/compiler/src/frontend/lowering/width-lift.ts +++ b/packages/compiler/src/frontend/lowering/width-lift.ts @@ -11,6 +11,16 @@ export type WidthLift = | { how: "retag" } | { how: "unitRetag" } | { how: "liftWrap"; tag: number; arm: IrType } + /** A record into a union of two record arms that differ by one field: + * the source field `field` (`T | undefined`) selects `present` when it + * holds a value and `absent`, which lacks the field, when it is + * undefined. */ + | { + how: "presenceRoute"; + field: string; + present: { tag: number; arm: IrType }; + absent: { tag: number; arm: IrType }; + } | { how: "width" } | { how: "unionWidth" } | { how: "arr" } @@ -20,5 +30,6 @@ export type WidthLift = | { how: "clsWidth" } | { how: "narrow" } | { how: "dynIn" } + | { how: "dynOut" } | { how: "upcast" } | { how: "funcAdapt" }; diff --git a/packages/compiler/src/frontend/package-shims.ts b/packages/compiler/src/frontend/package-shims.ts index 86a41d3a6..a0c2e4cb9 100644 --- a/packages/compiler/src/frontend/package-shims.ts +++ b/packages/compiler/src/frontend/package-shims.ts @@ -62,9 +62,20 @@ interface ImplementationShim { * of it (a __commonJS chunk whose key names the package, its version * and this file) is the package itself. */ mainFile: string; + /** The package's declaration entry relative to its directory, when it + * is not `index.d.ts`. */ + typesFile?: string; + /** An alternative entry for this installation (relative to the shim + * root), or undefined for the version's own entry. */ + variant?: (pkgDir: string) => string | undefined; + /** Every variant entry, for the guard test that compiles each one. */ + variants?: readonly string[]; } -const IMPLEMENTATION_SHIMS: ReadonlyMap = new Map([ +const IMPLEMENTATION_SHIMS: ReadonlyMap = new Map< + string, + ImplementationShim +>([ [ "fs-extra", { @@ -79,6 +90,33 @@ const IMPLEMENTATION_SHIMS: ReadonlyMap = new Map([ ]), }, ], + [ + "chalk", + { + name: "chalk", + defaultExport: true, + mainFile: "source/index.js", + typesFile: "source/index.d.ts", + versions: new Map([[5, "chalk/v5.ts"]]), + }, + ], + [ + "hot-shots", + { + name: "hot-shots", + defaultExport: true, + mainFile: "index.js", + typesFile: "types.d.ts", + versions: new Map([[8, "hot-shots/v8.ts"]]), + // hot-shots' uds protocol loads the optional unix-dgram addon: where + // it is installed, the entry with the native Unix datagram transport. + variant: (pkgDir: string) => + installedPackageDir(join(pkgDir, "index.js"), "unix-dgram") === null + ? undefined + : "hot-shots/v8-unix-dgram.ts", + variants: ["hot-shots/v8-unix-dgram.ts"], + }, + ], [ "undici", { @@ -141,8 +179,10 @@ export function throughBundledShimForwarder(resolved: string): string { * the guard test that compiles each one. */ export function packageShimEntries(): string[] { const entries = new Set(); - for (const shim of IMPLEMENTATION_SHIMS.values()) + for (const shim of IMPLEMENTATION_SHIMS.values()) { for (const entry of shim.versions.values()) entries.add(join(packageShimRoot(), entry)); + for (const entry of shim.variants ?? []) entries.add(join(packageShimRoot(), entry)); + } return [...entries].sort(); } @@ -240,7 +280,8 @@ function shimEntryForPackageDir(pkgDir: string, expectedName: string | null): st if (shim === undefined) return null; const major = Number(/^(\d+)\./.exec(pkg.version)?.[1] ?? NaN); const entry = shim.versions.get(major); - return entry === undefined ? null : join(packageShimRoot(), entry); + if (entry === undefined) return null; + return join(packageShimRoot(), shim.variant?.(pkgDir) ?? entry); } /** The shim module `specifier` resolves to from `fromFile`, or null. Only @@ -280,16 +321,33 @@ export function isShimmedImport(fromFile: string, specifier: string): boolean { return isUnloadedPackage(specifier) || packageShimFor(fromFile, specifier) !== null; } +/** The shimmed package whose declaration entry `path` is, with the + * package directory: `/index.d.ts`, or the entry's own `typesFile`. */ +function shimOfDeclarationPath(path: string): { pkgDir: string; shim: ImplementationShim } | null { + if (!path.endsWith(".d.ts")) return null; + if (path.endsWith("/index.d.ts")) { + const pkgDir = dirname(path); + const shim = IMPLEMENTATION_SHIMS.get(pkgDir.slice(pkgDir.lastIndexOf("/") + 1)); + if (shim !== undefined && shim.typesFile === undefined) return { pkgDir, shim }; + } + for (const shim of IMPLEMENTATION_SHIMS.values()) { + if (shim.typesFile === undefined) continue; + const suffix = `/${shim.name}/${shim.typesFile}`; + if (path.endsWith(suffix)) + return { pkgDir: path.slice(0, path.length - suffix.length + shim.name.length + 1), shim }; + } + return null; +} + /** The fs-shadow answer for a shimmed package's declaration entry: the * installed `index.d.ts` reads as a re-export of the version's shim module, * so the checker types each importer against its own version. Undefined * for every other path. */ export function packageShimDeclarationText(path: string): string | undefined { - if (!path.endsWith("/index.d.ts")) return undefined; - const pkgDir = dirname(path); - const name = pkgDir.slice(pkgDir.lastIndexOf("/") + 1); - const shim = IMPLEMENTATION_SHIMS.get(name); - if (shim === undefined) return undefined; + const located = shimOfDeclarationPath(path); + if (located === null) return undefined; + const { pkgDir, shim } = located; + const name = shim.name; // A DefinitelyTyped twin (`@types/fs-extra`) types a package that ships no // declarations. Nothing at its path names the installed runtime version, // so it reads as the shim only when every supported version shares one @@ -316,6 +374,12 @@ export function isPackageShimFile(fileName: string): boolean { return fileName.startsWith(root.endsWith("/") ? root : `${root}/`); } +/** True for a name a shipped shim may declare as a bodiless runtime + * intrinsic (lower-calls.ts lowers its calls to runtime calls). */ +export function isShimIntrinsicName(name: string): boolean { + return name.startsWith("scriptcUnixDgram"); +} + /** Resets per-compile caches. */ export function clearPackageShimCaches(): void { installedCache.clear(); diff --git a/packages/compiler/src/frontend/pipeline.ts b/packages/compiler/src/frontend/pipeline.ts index 193e8adc0..6da81dfbc 100644 --- a/packages/compiler/src/frontend/pipeline.ts +++ b/packages/compiler/src/frontend/pipeline.ts @@ -685,8 +685,8 @@ function loadFrontend( ...finalLoad.program.getImplementationSourceFiles().filter((sf) => !sf.isDeclarationFile), ].map((sf) => [sf.fileName, sf.text]), ), - lower: (opts) => - lowerToIr(finalLoad.program, finalLoad.entry, finalLoad.moduleOrder, { + lower: (opts) => { + const lowered = lowerToIr(finalLoad.program, finalLoad.entry, finalLoad.moduleOrder, { ...opts, frontendServices: finalLoad.services, startupCrash: finalLoad.startupCrash ?? null, @@ -695,7 +695,13 @@ function loadFrontend( projectCheckJs: finalLoad.typeWorld?.projectCheckJs ?? false, externalTypes: finalLoad.externalTypes, externalTypeSpecifiersByFile: finalLoad.externalTypeSpecifiersByFile, - }), + }); + // The declaration-typed boundary's notes (SC6102) join the + // performance warnings; they never fail a build. + const notes = finalLoad.declBoundaryNotes; + if (notes === undefined) return lowered; + return { ...lowered, performance: [...notes, ...(lowered.performance ?? [])] }; + }, npmStatic: statuses, npmImportSites: npmSites, dispose: finalLoad.dispose, diff --git a/packages/compiler/src/frontend/process-fork.ts b/packages/compiler/src/frontend/process-fork.ts new file mode 100644 index 000000000..744d57aa5 --- /dev/null +++ b/packages/compiler/src/frontend/process-fork.ts @@ -0,0 +1,26 @@ +/** Process primitives for forked lowering workers (unit-journal.ts). The + * native compiler fills these in; under Node they stay unset and reachable + * emit lowers every wave in-process. */ +export interface ProcessFork { + /** fork(): 0 in the worker, the worker's pid in the parent, -1 on failure. */ + fork(): number; + /** Ends the worker without running exit handlers. */ + exit(code: number): void; + /** The worker's exit status, 128 plus a signal number, or -1. */ + wait(pid: number): number; + /** Opens the lock file checker requests from workers take. */ + openLock(path: string): boolean; + lock(): void; + unlock(): void; +} + +export const processFork: { + primitives: ProcessFork | null; + /** True in a worker: checker requests take the lock (rpc-client.ts). */ + worker: boolean; + /** A worker's checker requests and the time it waited for the lock and + * for responses (SCRIPTC_TIMING). */ + requests: number; + lockWaitMs: number; + requestMs: number; +} = { primitives: null, worker: false, requests: 0, lockWaitMs: 0, requestMs: 0 }; diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index 507d3392d..68b649b07 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -139,6 +139,7 @@ import { typeOnlyReachedFiles, } from "./type-only-imports.js"; import { moduleSourceCandidates } from "./module-source-candidates.js"; +import { planDeclProjection, type DeclProjection } from "./decl-projection.js"; import { inferredJsDiagnosticSuppressed } from "./inferred-js-diagnostics.js"; import { containsJsx, @@ -386,6 +387,9 @@ export interface LoadResult { * checkPreflight. */ startupCrash?: StartupCrash | null; configDiags: ScrDiagnostic[]; + /** SC6102 performance notes of the declaration-typed boundary + * (decl-projection.ts): files whose projection was dropped. */ + declBoundaryNotes?: ScrDiagnostic[]; /** The files whose JSX the program lowers (jsx-source.ts), by file name. * The program holds their rewritten text; positions in diagnostics refer * to it. */ @@ -397,6 +401,9 @@ export interface LoadResult { /** The program's pre-emit diagnostics, computed once. */ diagnostics: readonly ts.Diagnostic[]; projectWorld: ts.Program | null; + /** The declaration-typed boundary rewrote user files: errors fall + * back to the declaration world (load.projectWorld()), as without it. */ + declWorldFallback?: true; }; /** How scriptc's type world differs from the project's own tsc setup — * used only to label the TypeScript errors those differences cause. */ @@ -846,6 +853,9 @@ function loadProgram7( externalTypes: ReadonlyMap = new Map(), /** The --npm-static lowering world: root opted-in packages' deep edges. */ rootNpmStaticEdges = false, + /** The declaration-typed boundary's projection (decl-projection.ts): + * answers rewritten user files for the lowering world, or null. */ + projector?: (program: ts.Program) => DeclProjection | null, ): LoadResult & { disposeAll: () => void } { const config = adoptProjectConfig7(host, entryPath); const configuredPaths = config.options["paths"]; @@ -925,6 +935,7 @@ function loadProgram7( // lowering compiles holds them with their JSX lowered. const jsx = lowerProgramJsx7(program, options); let tscWorld: LoadResult["tscWorld"]; + const declBoundaryNotes: ScrDiagnostic[] = []; if (jsx.refusal !== null) { config.diags.push(jsx.refusal); } else if (jsx.sources.size > 0) { @@ -940,6 +951,51 @@ function loadProgram7( }; for (const [file, lowered] of jsx.sources) host.addVirtualFile(file, lowered.text); program = host.createProgram([...programRoots, overridesDtsPath()], options); + } else if (projector !== undefined) { + // The declaration-typed boundary: user files typed from package + // declarations (decl-projection.ts). The tsc gate reads the original + // text; the lowering compiles the rewritten files. + const projection = projector(program); + if (projection !== null) { + const original = program; + const baseline = new Map(); + for (const file of projection.files.keys()) { + const sf = original.getSourceFile(file); + if (sf !== undefined) baseline.set(file, original.getSemanticDiagnostics(sf).length); + } + for (const [file, text] of projection.files) host.addVirtualFile(file, text); + host.addVirtualFile(projection.declFile, projection.declText); + programRoots.push(projection.declFile); + program = host.createProgram([...programRoots, overridesDtsPath()], options); + // A rewrite the lowering world disagrees with (new errors) is + // dropped for that file: the file lowers as it would without it. + const dropped: string[] = []; + for (const file of projection.files.keys()) { + const sf = program.getSourceFile(file); + if (sf === undefined) continue; + if (program.getSemanticDiagnostics(sf).length > (baseline.get(file) ?? 0)) { + dropped.push(file); + host.addVirtualFile(file, original.getSourceFile(file)!.text); + } + } + if (dropped.length > 0) { + program.dispose(); + program = host.createProgram([...programRoots, overridesDtsPath()], options); + for (const file of dropped) declBoundaryNotes.push(declBoundaryDroppedDiag(file)); + } + if (dropped.length === projection.files.size) { + program.dispose(); + programRoots.pop(); + program = original; + } else { + tscWorld = { + program: original, + diagnostics: ts.getPreEmitDiagnostics(original), + projectWorld: null, + declWorldFallback: true, + }; + } + } } const entry = program.getSourceFile(entryPath); if (!entry) throw new Error(`could not load ${entryPath}`); @@ -950,6 +1006,7 @@ function loadProgram7( entry, moduleOrder: [], configDiags: config.diags, + ...(declBoundaryNotes.length > 0 ? { declBoundaryNotes } : {}), typeWorld: { nodeFallback: nodeTypes === null, projectLib: config.projectLib ?? null, @@ -1156,31 +1213,69 @@ export function loadProgram( (npmShadow?.hideFile(path) ?? false) || projectDtsRuntimeSibling(path) !== null, }; const host = services.createProgramHost({ cwd: dirname(entryPath), fsShadow }); + let authoredHost: ts.Ts7Host | null = null; + let authoredLoad: ReturnType | null = null; + const authored = (): ReturnType => { + if (!authoredLoad) { + authoredHost = services.createProgramHost({ cwd: dirname(entryPath) }); + authoredLoad = loadProgram7(authoredHost, entryPath, externalTypes); + } + return authoredLoad; + }; + const declPackages = npmStaticPackages.filter((pkg) => { + const resolved = resolveBareModule(entryPath, pkg, "types-only"); + return resolved !== null && /\.d\.(?:ts|mts|cts)$/.test(resolved.typesFile); + }); + const projector = + declPackages.length === 0 + ? undefined + : (program: ts.Program): DeclProjection | null => { + const userFiles = declBoundaryUserFiles(program, declPackages); + if (userFiles.length === 0) return null; + let projection: DeclProjection | null; + try { + projection = planDeclProjection({ + jsProgram: program, + declProgram: authored().program, + userFiles, + outDir: dirname(entryPath), + }); + } catch (error) { + if (process.env["SCRIPTC_DEBUG_DECL_BOUNDARY"] === "1") throw error; + return null; + } + if (process.env["SCRIPTC_DEBUG_DECL_BOUNDARY"] === "1") { + process.stderr.write( + `[decl-boundary] ${projection === null ? "no projection" : projection.plan.join("\n[decl-boundary] ")}\n`, + ); + if (projection !== null) process.stderr.write(projection.declText); + } + return projection; + }; try { - const load = loadProgram7(host, entryPath, externalTypes, npmStaticPackages.length > 0); - let authoredHost: ts.Ts7Host | null = null; - let authoredLoad: ReturnType | null = null; + const load = loadProgram7( + host, + entryPath, + externalTypes, + npmStaticPackages.length > 0, + projector, + ); return { ...load, services, projectWorld: npmStaticPackages.length === 0 ? load.projectWorld - : () => { + : () => // Check authored code against the package's published declarations. // Runtime bodies still lower from JavaScript in the original world; // inference there must not manufacture errors in valid user code. - if (!authoredLoad) { - authoredHost = services.createProgramHost({ cwd: dirname(entryPath) }); - authoredLoad = loadProgram7(authoredHost, entryPath, externalTypes); - } - return authoredLoad.projectWorld(); - }, + authored().projectWorld(), dispose: () => { try { - authoredLoad?.disposeAll(); + (authoredLoad as ReturnType | null)?.disposeAll(); } finally { - authoredHost?.close(); + (authoredHost as ts.Ts7Host | null)?.close(); try { load.disposeAll(); } finally { @@ -1190,11 +1285,47 @@ export function loadProgram( }, }; } catch (error) { + (authoredLoad as ReturnType | null)?.disposeAll(); + (authoredHost as ts.Ts7Host | null)?.close(); host.close(); throw error; } } +/** User TypeScript files (outside node_modules) that import one of the + * packages whose declarations the boundary projects from. */ +function declBoundaryUserFiles(program: ts.Program, packages: readonly string[]): string[] { + const out: string[] = []; + for (const sf of program.getSourceFiles()) { + const name = sf.fileName.split("\\").join("/"); + if (sf.isDeclarationFile || name.includes("/node_modules/") || !/\.[mc]?tsx?$/.test(name)) + continue; + if (name.endsWith(".tsx")) continue; // JSX files: not projected yet + const text = sf.text; + if ( + packages.some( + (pkg) => + text.includes(`"${pkg}"`) || + text.includes(`'${pkg}'`) || + text.includes(`"${pkg}/`) || + text.includes(`'${pkg}/`), + ) + ) + out.push(sf.fileName); + } + return out; +} + +/** SC6102 — a projected file the lowering world disagreed with. */ +function declBoundaryDroppedDiag(file: string): ScrDiagnostic { + return { + code: "SC6102", + message: + "values from npm packages stay checked-dynamic in this file: the types projected from the packages' declarations do not type-check against their JavaScript", + loc: { file, start: 0, end: 0 }, + }; +} + /** tsc diagnostics (syntax + types), the supported-import fence, and the * module evaluation order: fills load.moduleOrder (the SourceFiles * themselves — the lowering consumes them directly) and returns the @@ -4801,7 +4932,9 @@ function preflight7(load: LoadResult): { tscWorld === undefined ? errorsOf(program) : keptErrors(tscWorld.program, tscWorld.diagnostics); if (tscErrors.length > 0) { const world = - tscWorld === undefined ? load.projectWorld() : (tscWorld.projectWorld ?? tscWorld.program); + tscWorld === undefined || tscWorld.declWorldFallback === true + ? load.projectWorld() + : (tscWorld.projectWorld ?? tscWorld.program); for (const d of errorsOf(world)) { const diag = toPassthrough(d); const lowered = tscWorld === undefined ? undefined : load.jsxSources?.get(diag.loc.file); diff --git a/packages/compiler/src/frontend/token-equivalence.test.ts b/packages/compiler/src/frontend/token-equivalence.test.ts new file mode 100644 index 000000000..880a07221 --- /dev/null +++ b/packages/compiler/src/frontend/token-equivalence.test.ts @@ -0,0 +1,107 @@ +import { expect, test } from "vitest"; +import { tokenEquivalentSource } from "./token-equivalence.js"; + +const equivalent = (previous: string, current: string, path = "/src/a.ts") => + tokenEquivalentSource(path, previous, current); + +test("whitespace and plain comments are trivia", () => { + expect(equivalent("const a = 1;\n", "const a = 1; // one\n")).toBe(true); + expect(equivalent("f(a,b);\n", "f( a, /* first */ b );\n")).toBe(true); + expect(equivalent("const s = `x${a}y`;\n", "const s = `x${ a /* the value */ }y`;\n")).toBe(true); +}); + +test("tokens, literals and line breaks that change semicolon insertion are not", () => { + expect(equivalent("const a = 1;\n", "const a = 2;\n")).toBe(false); + expect(equivalent('const s = "a // b";\n', 'const s = "a // c";\n')).toBe(false); + expect(equivalent("const s = `a // ${b} /* c */`;\n", "const s = `a // ${b} /* d */`;\n")).toBe( + false, + ); + // `return` followed by a line break returns undefined. + expect(equivalent("return a;\n", "return\na;\n")).toBe(false); + expect(equivalent("return a;\n", "return /*\n*/ a;\n")).toBe(false); +}); + +test("comment-like text inside regular expressions is code", () => { + expect(equivalent("const r = /[//]/; x = 1;\n", "const r = /[//]/; x = 2;\n")).toBe(false); + expect(equivalent("if (a) /\\/*/.test(b); c = 1;\n", "if (a) /\\/*/.test(b); c = 2;\n")).toBe( + false, + ); + expect(equivalent("const q = a / b; // ratio\n", "const q = a / b; // the ratio\n")).toBe(true); + expect(equivalent("return /a/; // one\n", "return /a/; // two\n")).toBe(true); + // A `for await` header closes like a `for` header. + expect( + equivalent( + "for await (const x of xs) /[//]/.test(x); c = 1;\n", + "for await (const x of xs) /[//]/.test(x); c = 2;\n", + ), + ).toBe(false); +}); + +test("a / after a line break is never equivalent", () => { + const loop = (body: string) => `a: for (;;) {\n${body}\n}\n`; + // Automatic semicolon insertion decides whether these start a regular + // expression. + expect( + equivalent(loop("break\n/[//]/.test(x); c = 1;"), loop("break\n/[//]/.test(x); c = 2;")), + ).toBe(false); + expect( + equivalent(loop("break a\n/[//]/.test(x); c = 1;"), loop("break a\n/[//]/.test(x); c = 2;")), + ).toBe(false); + expect( + equivalent("type A = B\n/[//]/.test(x); c = 1;\n", "type A = B\n/[//]/.test(x); c = 2;\n"), + ).toBe(false); + // A comment spanning lines separates them too; JavaScript keeps its comments as tokens. + expect( + equivalent( + "x\n/* a\n */ /[//]/.test(y); c = 1;\n", + "x\n/* a\n */ /[//]/.test(y); c = 2;\n", + "/src/a.js", + ), + ).toBe(false); + // A / on the line of the token before it still divides. + expect(equivalent(loop("continue\nx / 2; // one"), loop("continue\nx / 2; // two"))).toBe(true); +}); + +test("code units outside ASCII are equivalent only inside strings, templates and comments", () => { + expect(equivalent('const s = "é"; // one\n', 'const s = "é"; // two\n')).toBe(true); + expect(equivalent("const s = `é${a}`; // one\n", "const s = `é${a}`; // two\n")).toBe(true); + expect(equivalent("const a = 1; // é\n", "const a = 1; // è\n")).toBe(true); + expect(equivalent("const café = 1; // one\n", "const café = 1; // two\n")).toBe(false); + expect( + equivalent( + "ä: for (;;) { break ä\n/[//]/.test(x); c = 1; }\n", + "ä: for (;;) { break ä\n/[//]/.test(x); c = 2; }\n", + ), + ).toBe(false); + expect(equivalent("const r = /é/; // one\n", "const r = /é/; // two\n")).toBe(false); +}); + +test("whitespace outside ASCII is never equivalent", () => { + expect( + equivalent("return\u3000/[//]/.test(x); c = 1;\n", "return\u3000/[//]/.test(x); c = 2;\n"), + ).toBe(false); + expect(equivalent("const a = 1;\u00a0// one\n", "const a = 1;\u00a0// two\n")).toBe(false); +}); + +test("U+2028 and U+2029 end a line comment, so their files are never equivalent", () => { + expect(equivalent("// a\u2028c = 1;\n", "// a\u2028c = 2;\n")).toBe(false); + expect(equivalent("/* a\u2029*/ c = 1;\n", "/* b\u2029*/ c = 1;\n")).toBe(false); +}); + +test("comments that can affect compilation are kept", () => { + expect(equivalent("/** @type {number} */ let a;\n", "/** @type {string} */ let a;\n")).toBe( + false, + ); + expect(equivalent("// @ts-ignore\nf();\n", "f();\n")).toBe(false); + expect(equivalent('/// \n', "\n")).toBe(false); + // Every comment is kept in JavaScript, whose types come from JSDoc. + expect(equivalent("let a; // one\n", "let a; // two\n", "/src/a.js")).toBe(false); +}); + +test("text the scanner cannot classify is never equivalent", () => { + expect(equivalent("x = {} / 2; // a\n", "x = {} / 2; // b\n")).toBe(false); + expect(equivalent("++/a/.lastIndex; // a\n", "++/a/.lastIndex; // b\n")).toBe(false); + expect(equivalent("const \\u0061 = 1; // a\n", "const \\u0061 = 1; // b\n")).toBe(false); + expect(equivalent("const a = ; // a\n", "const a = ; // b\n", "/src/a.tsx")).toBe(false); + expect(equivalent('const s = "open\n', 'const s = "open // \n')).toBe(false); +}); diff --git a/packages/compiler/src/frontend/token-equivalence.ts b/packages/compiler/src/frontend/token-equivalence.ts new file mode 100644 index 000000000..7a0f6cc79 --- /dev/null +++ b/packages/compiler/src/frontend/token-equivalence.ts @@ -0,0 +1,391 @@ +/* Token equivalence of two versions of a source file, for frontend cache + * reuse after edits that touch only comments and whitespace. + * + * Two versions are equivalent when they have the same tokens, in the same + * order, with line breaks between the same pairs of tokens (automatic + * semicolon insertion depends on them), and differ only in whitespace and + * in comments that cannot affect compilation. The scanner answers "not + * equivalent" whenever it cannot classify text with certainty, so a wrong + * answer can only cost a cache hit: + * + * - Every comment in a file that is not TypeScript (`.ts`, `.mts`, `.cts`) + * is kept as a token: JavaScript types come from JSDoc. TypeScript + * comments are kept when they start with `///` (references and + * directives) or contain `@` or `#` (checker directives, JSDoc tags, and + * annotations lowering reads). + * - A `/` starts a regular expression or divides according to the token + * before it. After `)` it divides unless the parenthesis closes an `if`, + * `while`, `for`, `for await`, or `with` header. After `}`, `++` or + * `--` the scanner cannot tell which, so it gives up, as it does for + * escaped identifiers, which can spell keywords. + * - A `/` with a line break (or a comment that spans lines) since the + * previous token gives up: whether automatic semicolon insertion ends the + * statement before it decides how it reads. + * - Template literals track their `${` substitutions on a brace stack. + * - Any code unit outside ASCII gives up, except inside strings, templates + * and comments. So do files that contain whitespace or a line terminator + * outside ASCII anywhere (U+2028 and U+2029 end a line comment), JSX + * files, and unterminated strings, comments, templates and regular + * expressions. + * + * The scanner models only what these rules name; anything else makes the + * versions not equivalent, which rebuilds. */ + +interface Token { + text: string; + comment: boolean; + lineBreakBefore: boolean; +} + +/** Keywords after which `/` starts a regular expression. */ +const EXPRESSION_KEYWORDS = new Set([ + "return", + "typeof", + "instanceof", + "in", + "of", + "new", + "delete", + "void", + "throw", + "case", + "do", + "else", + "yield", + "await", + "default", + "extends", +]); + +/** Whitespace and line terminators outside ASCII. */ +const NON_ASCII_SPACE = /[\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]/; + +/** Keywords whose parenthesized header is followed by a statement. */ +const HEADER_KEYWORDS = new Set(["if", "while", "for", "with"]); + +const PUNCTUATORS = [ + ">>>=", + "...", + "===", + "!==", + "**=", + "<<=", + ">>=", + ">>>", + "&&=", + "||=", + "??=", + "=>", + "==", + "!=", + "<=", + ">=", + "&&", + "||", + "??", + "?.", + "++", + "--", + "+=", + "-=", + "*=", + "%=", + "&=", + "|=", + "^=", + "**", + "<<", + ">>", +]; + +function isTypeScript(path: string): boolean { + return path.endsWith(".ts") || path.endsWith(".mts") || path.endsWith(".cts"); +} + +function isIdentifierCode(code: number): boolean { + return ( + (code >= 97 && code <= 122) || + (code >= 65 && code <= 90) || + (code >= 48 && code <= 57) || + code === 36 || + code === 95 || + code === 92 + ); +} + +function isLineBreak(code: number): boolean { + return code === 10 || code === 13; +} + +function isSpace(code: number): boolean { + return code === 32 || code === 9 || code === 11 || code === 12 || isLineBreak(code); +} + +/** The tokens of `source`, or null when the scanner cannot classify it. */ +function scan(path: string, source: string): Token[] | null { + if (path.endsWith(".tsx") || path.endsWith(".jsx")) return null; + if (NON_ASCII_SPACE.test(source)) return null; + const typeScript = isTypeScript(path); + const tokens: Token[] = []; + // Brace kinds: true for a template substitution, false otherwise. + const braces: boolean[] = []; + // Paren kinds: true for a statement header. + const parens: boolean[] = []; + let lineBreakBefore = false; + // A line break, or a comment spanning lines, since the last token other + // than a comment. + let breakSinceCode = false; + // The previous token and the one before it, and whether a `)` closed a + // statement header. + const last: { token: string | null; before: string | null } = { token: null, before: null }; + let closedHeader = false; + const push = (text: string, comment = false): void => { + tokens.push({ text, comment, lineBreakBefore }); + lineBreakBefore = false; + if (!comment) { + breakSinceCode = false; + last.before = last.token; + last.token = text; + } + }; + let index = 0; + const length = source.length; + // A shebang is one token. + if (source.startsWith("#!")) { + while (index < length && !isLineBreak(source.charCodeAt(index))) index++; + push(source.slice(0, index)); + } + /** Scans template text from `index` (after a backtick or a closing + * substitution brace) through the closing backtick or `${`. */ + const template = (start: number): boolean => { + let position = index; + while (position < length) { + const code = source.charCodeAt(position); + if (code === 92) { + position += 2; + continue; + } + if (code === 96) { + position++; + index = position; + push(source.slice(start, position)); + closedHeader = false; + return true; + } + if (code === 36 && source.charCodeAt(position + 1) === 123) { + position += 2; + index = position; + push(source.slice(start, position)); + braces.push(true); + last.before = last.token; + last.token = "${"; + return true; + } + position++; + } + return false; + }; + while (index < length) { + const code = source.charCodeAt(index); + if (isSpace(code)) { + if (isLineBreak(code)) lineBreakBefore = breakSinceCode = true; + index++; + continue; + } + // Outside strings, templates and comments, the scanner models ASCII only. + if (code > 127) return null; + const next = source.charCodeAt(index + 1); + // Comments. + if (code === 47 && (next === 47 || next === 42)) { + const start = index; + if (next === 47) { + while (index < length && !isLineBreak(source.charCodeAt(index))) index++; + } else { + const end = source.indexOf("*/", index + 2); + if (end < 0) return null; + index = end + 2; + } + const text = source.slice(start, index); + const semantic = + !typeScript || text.startsWith("///") || text.includes("@") || text.includes("#"); + const multiline = /[\r\n]/.test(text); + if (multiline) breakSinceCode = true; + if (semantic) push(text, true); + else if (multiline) lineBreakBefore = true; + continue; + } + // Strings. + if (code === 34 || code === 39) { + const start = index; + index++; + while (index < length) { + const inner = source.charCodeAt(index); + if (inner === 92) { + index += 2; + continue; + } + if (isLineBreak(inner)) return null; + index++; + if (inner === code) break; + if (index >= length) return null; + } + if (source.charCodeAt(index - 1) !== code || index - start < 2) return null; + push(source.slice(start, index)); + closedHeader = false; + continue; + } + // Templates. + if (code === 96) { + const start = index; + index++; + if (!template(start)) return null; + continue; + } + // Identifiers, keywords and numbers. + if (isIdentifierCode(code) || (code === 46 && next >= 48 && next <= 57)) { + const start = index; + index++; + while (index < length) { + const inner = source.charCodeAt(index); + if (isIdentifierCode(inner) || inner === 46) { + // A dot continues only a number (`1.5`), not member access. + if (inner === 46) { + const first = source.charCodeAt(start); + if (!((first >= 48 && first <= 57) || first === 46)) break; + } + index++; + continue; + } + // Exponent signs (`1e-5`). + if ( + (inner === 43 || inner === 45) && + (source.charCodeAt(index - 1) === 101 || source.charCodeAt(index - 1) === 69) + ) { + const first = source.charCodeAt(start); + if ((first >= 48 && first <= 57) || first === 46) { + index++; + continue; + } + } + break; + } + const word = source.slice(start, index); + // An escaped identifier can spell a keyword (`\u0072eturn`). + if (word.includes("\\")) return null; + push(word); + closedHeader = false; + continue; + } + // Division or a regular expression. + if (code === 47) { + if (breakSinceCode) return null; + const before = last.token; + let regex: boolean; + if (before === null) regex = true; + else if (before === "}") return null; + else if (before === ")") regex = closedHeader; + // `++/re/.lastIndex` is valid, `x++ / 2` too. + else if (before === "++" || before === "--") return null; + else if (before === "]") regex = false; + else if (/^[A-Za-z0-9_$\\]/.test(before)) regex = EXPRESSION_KEYWORDS.has(before); + else if (before.startsWith("'") || before.startsWith('"') || before.endsWith("`")) + regex = false; + else if (before.startsWith("/") && before.length > 1 && before !== "/=") regex = false; + else regex = true; + if (regex) { + const start = index; + index++; + let inClass = false; + while (index < length) { + const inner = source.charCodeAt(index); + if (isLineBreak(inner) || inner > 127) return null; + if (inner === 92) { + index += 2; + continue; + } + if (inner === 91) inClass = true; + else if (inner === 93) inClass = false; + else if (inner === 47 && !inClass) break; + index++; + } + if (index >= length) return null; + index++; + while (index < length && isIdentifierCode(source.charCodeAt(index))) index++; + push(source.slice(start, index)); + closedHeader = false; + continue; + } + const text = next === 61 ? "/=" : "/"; + index += text.length; + push(text); + closedHeader = false; + continue; + } + // Brackets. + if (code === 40) { + parens.push( + last.token !== null && + (HEADER_KEYWORDS.has(last.token) || (last.token === "await" && last.before === "for")), + ); + index++; + push("("); + closedHeader = false; + continue; + } + if (code === 41) { + const header = parens.pop() ?? false; + index++; + push(")"); + closedHeader = header; + continue; + } + if (code === 123) { + braces.push(false); + index++; + push("{"); + closedHeader = false; + continue; + } + if (code === 125) { + if (braces.pop() === true) { + const start = index; + index++; + if (!template(start)) return null; + continue; + } + index++; + push("}"); + closedHeader = false; + continue; + } + // Other punctuators. + let text = source[index]!; + for (const punctuator of PUNCTUATORS) + if (source.startsWith(punctuator, index)) { + text = punctuator; + break; + } + index += text.length; + push(text); + closedHeader = false; + } + if (braces.length !== 0 || parens.length !== 0) return null; + return tokens; +} + +/** Whether `current` differs from `previous` only in whitespace and in + * comments that cannot affect compilation. */ +export function tokenEquivalentSource(path: string, previous: string, current: string): boolean { + if (previous === current) return true; + const left = scan(path, previous); + if (left === null) return false; + const right = scan(path, current); + if (right === null || left.length !== right.length) return false; + for (let index = 0; index < left.length; index++) { + const a = left[index]!; + const b = right[index]!; + if (a.text !== b.text || a.comment !== b.comment) return false; + if (index > 0 && a.lineBreakBefore !== b.lineBreakBefore) return false; + } + return true; +} diff --git a/packages/compiler/src/frontend/ts7/ast-node.test.ts b/packages/compiler/src/frontend/ts7/ast-node.test.ts index 6b2725b93..8b391e622 100644 --- a/packages/compiler/src/frontend/ts7/ast-node.test.ts +++ b/packages/compiler/src/frontend/ts7/ast-node.test.ts @@ -169,6 +169,30 @@ function ids( return nodes?.map((n) => (n as OracleNode).index); } +test("generated child getters select the child their name selects", () => { + // These getters adapt their raw child; their slot functions are compared instead. + const adapted = ["body", "initializer", "left", "modifiers", "operand", "questionToken"]; + const names = [...new Set(Object.values(childProperties).flat())]; + const lists = (value: unknown): unknown => (value instanceof AstNodeList ? value.nodes : value); + let compared = 0; + for (const { file } of decoded.values()) { + walkPreorder(file.root, (node) => { + const view = node as unknown as Record; + for (const name of names) { + if (adapted.includes(name)) { + const slot = (view[`${name}Slot`] as () => number).call(node); + expect(slot, `${node.kind}.${name}`).toBe(astChildOrder(node.kind, name)); + } else { + expect(lists(view[name]), `${node.kind}.${name}`).toBe(lists(node.child(name))); + } + compared++; + } + return undefined; + }); + } + expect(compared).toBeGreaterThan(100_000); +}); + test("the writeTargets option answers write targets without their wrappers", () => { const { bytes, file: plain } = decoded.get("targets.ts")!; const file = new AstFile(bytes, undefined, { writeTargets: true }); diff --git a/packages/compiler/src/frontend/ts7/ast-node.ts b/packages/compiler/src/frontend/ts7/ast-node.ts index be2745616..4c7726c00 100644 --- a/packages/compiler/src/frontend/ts7/ast-node.ts +++ b/packages/compiler/src/frontend/ts7/ast-node.ts @@ -17,6 +17,16 @@ import { import type { SourceFile } from "./ast-types.js"; import { SyntaxKind, NodeFlags } from "./enums.js"; +/** Receives the file name of a source file whose raw text or line + * positions a reader takes: the text of a node other than a token + * (getText), a node's text with its trivia (getFullText), or line starts. */ +let sourceTextObserver: ((fileName: string) => void) | undefined; + +/** Installs the observer of raw source text reads (sourceTextObserver). */ +export function observeSourceText(observer: ((fileName: string) => void) | undefined): void { + sourceTextObserver = observer; +} + /** 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 @@ -85,6 +95,8 @@ export class AstFile { private readonly strings = new Map(); private lines: number[] | undefined; private childIndices: Uint32Array | undefined; + /** The root source file's name (AstNode.fileName). */ + private rootFileName: string | undefined; /** Whether nodes answer write targets without their wrappers (see * AstFileOptions.writeTargets). */ @@ -113,7 +125,8 @@ export class AstFile { } node(index: number): AstNode { - if (Object.hasOwn(this.nodes, index)) return this.nodes[index]!; + const existing: AstNode | undefined = this.nodes[index]; + if (existing !== undefined) return existing; if (index === 0 || this.wire.kind(index) === KIND_NODE_LIST) throw new AstDecodeError("expected a node index"); const node = new AstNode(this, index); @@ -140,8 +153,13 @@ export class AstFile { } namedChild(node: AstNode, name: string): number { + return this.namedChildAt(node, astChildOrder(node.kind, name)); + } + + /** The child in wire slot `order` (astChildOrder), or 0 when the slot is + * negative or empty. */ + namedChildAt(node: AstNode, order: number): number { if (node.file !== this) throw new AstDecodeError("named child belongs to another source file"); - const order = astChildOrder(node.kind, name); if (order < 0) return 0; const mask = node.data >>> 30 === 0 ? node.data & 0xff : 0xff; if ((mask & (1 << order)) === 0) return 0; @@ -202,7 +220,23 @@ export class AstFile { return result; } + /** Reports a read of this file's raw text or line positions. */ + noteSourceText(): void { + if (sourceTextObserver !== undefined && this.root.kind === SyntaxKind.SourceFile) + sourceTextObserver(this.root.fileName); + } + + /** The file name of the source file node at `index`; the root's is + * read once. */ + sourceFileName(index: number): string { + if (index === 1 && this.rootFileName !== undefined) return this.rootFileName; + const name = this.wire.string(this.wire.extendedWord(index, 4)); + if (index === 1) this.rootFileName = name; + return name; + } + lineStarts(): number[] { + this.noteSourceText(); if (this.lines === undefined) this.lines = astLineStarts(this.root.text ?? ""); return this.lines; } @@ -327,6 +361,22 @@ export class AstNode { const index = this.file.namedChild(this, name); return index === 0 ? undefined : this.file.node(index); } + /** childNode by wire slot, as the generated getters select it. */ + childNodeAt(order: number): AstNode | undefined { + const index = this.file.namedChildAt(this, order); + return index === 0 ? undefined : this.file.node(index); + } + childListAt(order: number): AstNodeList | undefined { + const index = this.file.namedChildAt(this, order); + return index === 0 ? undefined : this.file.list(index); + } + childAt(order: number): AstNode | AstNodeList | undefined { + const index = this.file.namedChildAt(this, order); + if (index === 0) return undefined; + return this.file.wire.kind(index) === KIND_NODE_LIST + ? this.file.list(index) + : this.file.node(index); + } childList(name: string): AstNodeList | undefined { const index = this.file.namedChild(this, name); return index === 0 ? undefined : this.file.list(index); @@ -462,10 +512,14 @@ export class AstNode { return this.getStart(sourceFile) - this.pos; } getFullText(sourceFile?: AstNode): string { - return ((sourceFile ?? this.file.root).text ?? "").substring(this.pos, this.end); + const source = sourceFile ?? this.file.root; + source.file.noteSourceText(); + return (source.text ?? "").substring(this.pos, this.end); } getText(sourceFile?: AstNode): string { const source = sourceFile ?? this.file.root; + // A token's text is the same in every token-equivalent version. + if (this.kind > SyntaxKind.LastToken) source.file.noteSourceText(); return (source.text ?? "").substring(this.getStart(source), this.end); } @@ -564,18 +618,18 @@ export class AstNode { } get modifierFlags(): number { let result = 0; - for (const modifier of this.childList("modifiers")?.nodes ?? []) + for (const modifier of this.childListAt(this.modifiersSlot())?.nodes ?? []) result |= astModifierFlag(modifier.kind); return result; } get modifiers(): AstModifierList | undefined { - const index = this.file.namedChild(this, "modifiers"); + const index = this.file.namedChildAt(this, this.modifiersSlot()); return index === 0 ? undefined : this.file.modifierList(index); } get body(): AstNode | undefined { - const body = this.childNode("body"); + const body = this.childNodeAt(this.bodySlot()); // TS7 can serialize a reparsed @type callable before a method's // body without setting the type presence bit. The linked Block is // still the real body; keep the JSDoc node out of this view. @@ -597,11 +651,11 @@ export class AstNode { } get questionToken(): AstNode | undefined { - const direct = this.childNode("questionToken"); + const direct = this.childNodeAt(this.questionTokenSlot()); if (direct !== undefined) return direct; // TypeScript 7 shares one postfix slot for optional and definite- // assignment declarations. The familiar helper surface keeps them apart. - const token = this.childNode("postfixToken"); + const token = this.postfixToken; return token?.kind === SyntaxKind.QuestionToken ? token : undefined; } @@ -609,21 +663,21 @@ export class AstNode { // tree; with AstFileOptions.writeTargets these views return the target // without them. get initializer(): AstNode | undefined { - const initializer = this.childNode("initializer"); + const initializer = this.childNodeAt(this.initializerSlot()); return initializer !== undefined && this.file.writeTargets && writesTarget(this, "initializer") ? unwrapTarget(initializer) : initializer; } get left(): AstNode | undefined { - const left = this.childNode("left"); + const left = this.childNodeAt(this.leftSlot()); return left !== undefined && this.file.writeTargets && writesTarget(this, "left") ? unwrapTarget(left) : left; } get operand(): AstNode | undefined { - const operand = this.childNode("operand"); + const operand = this.childNodeAt(this.operandSlot()); return operand !== undefined && this.file.writeTargets && writesTarget(this, "operand") ? unwrapTarget(operand) : operand; @@ -632,281 +686,1173 @@ export class AstNode { // BEGIN GENERATED CHILD GETTERS // Generated from typescript@7.0.2; run scripts/generate-ts7-ast-schema.mjs. get argument(): AstNode | undefined { - return this.childNode("argument"); + switch (this.kind) { + case 206: + return this.childNodeAt(0); + default: + return undefined; + } } get argumentExpression(): AstNode | undefined { - return this.childNode("argumentExpression"); + switch (this.kind) { + case 213: + return this.childNodeAt(2); + default: + return undefined; + } } get arguments(): AstNodeList | undefined { - return this.childList("arguments"); + switch (this.kind) { + case 215: + return this.childListAt(2); + case 214: + return this.childListAt(3); + default: + return undefined; + } } get assertsModifier(): AstNode | undefined { - return this.childNode("assertsModifier"); + switch (this.kind) { + case 183: + return this.childNodeAt(0); + default: + return undefined; + } } get asteriskToken(): AstNode | undefined { - return this.childNode("asteriskToken"); + switch (this.kind) { + case 230: + return this.childNodeAt(0); + case 175: + case 219: + case 263: + return this.childNodeAt(1); + default: + return undefined; + } } get attributes(): AstNode | AstNodeList | undefined { - return this.child("attributes"); + switch (this.kind) { + case 301: + return this.childAt(0); + case 206: + return this.childAt(1); + case 286: + case 287: + return this.childAt(2); + case 273: + case 279: + case 343: + case 346: + return this.childAt(3); + default: + return undefined; + } } get awaitModifier(): AstNode | undefined { - return this.childNode("awaitModifier"); + switch (this.kind) { + case 250: + case 251: + return this.childNodeAt(0); + default: + return undefined; + } } get block(): AstNode | undefined { - return this.childNode("block"); + switch (this.kind) { + case 300: + return this.childNodeAt(1); + default: + return undefined; + } } get caseBlock(): AstNode | undefined { - return this.childNode("caseBlock"); + switch (this.kind) { + case 256: + return this.childNodeAt(1); + default: + return undefined; + } } get catchClause(): AstNode | undefined { - return this.childNode("catchClause"); + switch (this.kind) { + case 259: + return this.childNodeAt(1); + default: + return undefined; + } } get checkType(): AstNode | undefined { - return this.childNode("checkType"); + switch (this.kind) { + case 195: + return this.childNodeAt(0); + default: + return undefined; + } } get children(): AstNode | AstNodeList | undefined { - return this.child("children"); + switch (this.kind) { + case 344: + return this.childAt(0); + case 285: + case 289: + return this.childAt(1); + default: + return undefined; + } } get className(): AstNode | undefined { - return this.childNode("className"); + switch (this.kind) { + case 323: + case 324: + return this.childNodeAt(1); + default: + return undefined; + } } get clauses(): AstNodeList | undefined { - return this.childList("clauses"); + switch (this.kind) { + case 270: + return this.childListAt(0); + default: + return undefined; + } } get closingElement(): AstNode | undefined { - return this.childNode("closingElement"); + switch (this.kind) { + case 285: + return this.childNodeAt(2); + default: + return undefined; + } } get closingFragment(): AstNode | undefined { - return this.childNode("closingFragment"); + switch (this.kind) { + case 289: + return this.childNodeAt(2); + default: + return undefined; + } } get colonToken(): AstNode | undefined { - return this.childNode("colonToken"); + switch (this.kind) { + case 228: + return this.childNodeAt(3); + default: + return undefined; + } } get comment(): AstNodeList | undefined { - return this.childList("comment"); + switch (this.kind) { + case 315: + return this.childListAt(0); + case 322: + case 325: + case 326: + case 327: + case 328: + case 329: + case 330: + return this.childListAt(1); + case 323: + case 324: + case 332: + case 334: + case 335: + case 336: + case 339: + case 341: + case 342: + return this.childListAt(2); + case 331: + case 333: + case 337: + case 338: + case 340: + return this.childListAt(3); + case 343: + return this.childListAt(4); + default: + return undefined; + } } get condition(): AstNode | undefined { - return this.childNode("condition"); + switch (this.kind) { + case 228: + return this.childNodeAt(0); + case 249: + return this.childNodeAt(1); + default: + return undefined; + } } get constraint(): AstNode | undefined { - return this.childNode("constraint"); + switch (this.kind) { + case 337: + return this.childNodeAt(1); + case 169: + return this.childNodeAt(2); + default: + return undefined; + } } get declarationList(): AstNode | undefined { - return this.childNode("declarationList"); + switch (this.kind) { + case 244: + return this.childNodeAt(1); + default: + return undefined; + } } get declarations(): AstNodeList | undefined { - return this.childList("declarations"); + switch (this.kind) { + case 262: + return this.childListAt(0); + default: + return undefined; + } } get defaultType(): AstNode | undefined { - return this.childNode("defaultType"); + switch (this.kind) { + case 169: + return this.childNodeAt(4); + default: + return undefined; + } } get dotDotDotToken(): AstNode | undefined { - return this.childNode("dotDotDotToken"); + switch (this.kind) { + case 203: + case 209: + case 295: + return this.childNodeAt(0); + case 170: + return this.childNodeAt(1); + default: + return undefined; + } } get elementType(): AstNode | undefined { - return this.childNode("elementType"); + switch (this.kind) { + case 189: + return this.childNodeAt(0); + default: + return undefined; + } } get elements(): AstNodeList | undefined { - return this.childList("elements"); + switch (this.kind) { + case 190: + case 207: + case 208: + case 210: + case 276: + case 280: + return this.childListAt(0); + default: + return undefined; + } } get elseStatement(): AstNode | undefined { - return this.childNode("elseStatement"); + switch (this.kind) { + case 246: + return this.childNodeAt(2); + default: + return undefined; + } } get endOfFileToken(): AstNode | undefined { - return this.childNode("endOfFileToken"); + switch (this.kind) { + case 307: + return this.childNodeAt(1); + default: + return undefined; + } } get equalsGreaterThanToken(): AstNode | undefined { - return this.childNode("equalsGreaterThanToken"); + switch (this.kind) { + case 220: + return this.childNodeAt(4); + default: + return undefined; + } } get equalsToken(): AstNode | undefined { - return this.childNode("equalsToken"); + switch (this.kind) { + case 304: + return this.childNodeAt(4); + default: + return undefined; + } } get exclamationToken(): AstNode | undefined { - return this.childNode("exclamationToken"); + switch (this.kind) { + case 261: + return this.childNodeAt(1); + default: + return undefined; + } } get exportClause(): AstNode | undefined { - return this.childNode("exportClause"); + switch (this.kind) { + case 279: + return this.childNodeAt(1); + default: + return undefined; + } } get exprName(): AstNode | undefined { - return this.childNode("exprName"); + switch (this.kind) { + case 187: + return this.childNodeAt(0); + default: + return undefined; + } } get expression(): AstNode | undefined { - return this.childNode("expression"); + switch (this.kind) { + case 168: + case 171: + case 212: + case 213: + case 214: + case 215: + case 218: + case 221: + case 222: + case 223: + case 224: + case 231: + case 234: + case 235: + case 236: + case 239: + case 240: + case 245: + case 246: + case 248: + case 254: + case 255: + case 256: + case 258: + case 284: + case 294: + case 297: + case 298: + case 305: + case 348: + case 349: + return this.childNodeAt(0); + case 217: + case 230: + case 247: + case 295: + return this.childNodeAt(1); + case 250: + case 251: + case 278: + return this.childNodeAt(2); + case 169: + return this.childNodeAt(3); + default: + return undefined; + } } get extendsType(): AstNode | undefined { - return this.childNode("extendsType"); + switch (this.kind) { + case 195: + return this.childNodeAt(1); + default: + return undefined; + } } get falseType(): AstNode | undefined { - return this.childNode("falseType"); + switch (this.kind) { + case 195: + return this.childNodeAt(3); + default: + return undefined; + } } get finallyBlock(): AstNode | undefined { - return this.childNode("finallyBlock"); + switch (this.kind) { + case 259: + return this.childNodeAt(2); + default: + return undefined; + } } get head(): AstNode | undefined { - return this.childNode("head"); + switch (this.kind) { + case 204: + case 229: + return this.childNodeAt(0); + default: + return undefined; + } } get heritageClauses(): AstNodeList | undefined { - return this.childList("heritageClauses"); + switch (this.kind) { + case 232: + case 264: + case 265: + return this.childListAt(3); + default: + return undefined; + } } get importClause(): AstNode | undefined { - return this.childNode("importClause"); + switch (this.kind) { + case 273: + case 343: + case 346: + return this.childNodeAt(1); + default: + return undefined; + } } get incrementor(): AstNode | undefined { - return this.childNode("incrementor"); + switch (this.kind) { + case 249: + return this.childNodeAt(2); + default: + return undefined; + } } get indexType(): AstNode | undefined { - return this.childNode("indexType"); + switch (this.kind) { + case 200: + return this.childNodeAt(1); + default: + return undefined; + } } get jsdocPropertyTags(): AstNode | undefined { - return this.childNode("jsdocPropertyTags"); + switch (this.kind) { + case 317: + return this.childNodeAt(0); + default: + return undefined; + } } get label(): AstNode | undefined { - return this.childNode("label"); + switch (this.kind) { + case 252: + case 253: + case 257: + return this.childNodeAt(0); + default: + return undefined; + } } get literal(): AstNode | undefined { - return this.childNode("literal"); + switch (this.kind) { + case 202: + return this.childNodeAt(0); + case 205: + case 240: + return this.childNodeAt(1); + default: + return undefined; + } } get members(): AstNodeList | undefined { - return this.childList("members"); + switch (this.kind) { + case 188: + return this.childListAt(0); + case 267: + return this.childListAt(2); + case 232: + case 264: + case 265: + return this.childListAt(4); + case 201: + return this.childListAt(5); + default: + return undefined; + } } get moduleReference(): AstNode | undefined { - return this.childNode("moduleReference"); + switch (this.kind) { + case 272: + return this.childNodeAt(2); + default: + return undefined; + } } get moduleSpecifier(): AstNode | undefined { - return this.childNode("moduleSpecifier"); + switch (this.kind) { + case 273: + case 279: + case 343: + case 346: + return this.childNodeAt(2); + default: + return undefined; + } } get name(): AstNode | undefined { - return this.childNode("name"); + switch (this.kind) { + case 237: + case 261: + case 274: + case 275: + case 281: + case 292: + case 302: + case 306: + case 309: + case 319: + case 320: + case 321: + return this.childNodeAt(0); + case 169: + case 172: + case 173: + case 174: + case 178: + case 179: + case 203: + case 232: + case 264: + case 265: + case 266: + case 267: + case 268: + case 271: + case 272: + case 277: + case 282: + case 296: + case 303: + case 304: + case 333: + case 340: + case 345: + return this.childNodeAt(1); + case 170: + case 175: + case 209: + case 212: + case 219: + case 263: + case 331: + case 338: + return this.childNodeAt(2); + default: + return undefined; + } } get nameExpression(): AstNode | undefined { - return this.childNode("nameExpression"); + switch (this.kind) { + case 339: + return this.childNodeAt(1); + default: + return undefined; + } } get nameType(): AstNode | undefined { - return this.childNode("nameType"); + switch (this.kind) { + case 201: + return this.childNodeAt(2); + default: + return undefined; + } } get namedBindings(): AstNode | undefined { - return this.childNode("namedBindings"); + switch (this.kind) { + case 274: + return this.childNodeAt(1); + default: + return undefined; + } } get namespace(): AstNode | undefined { - return this.childNode("namespace"); + switch (this.kind) { + case 296: + return this.childNodeAt(0); + default: + return undefined; + } } get objectAssignmentInitializer(): AstNode | undefined { - return this.childNode("objectAssignmentInitializer"); + switch (this.kind) { + case 304: + return this.childNodeAt(5); + default: + return undefined; + } } get objectType(): AstNode | undefined { - return this.childNode("objectType"); + switch (this.kind) { + case 200: + return this.childNodeAt(0); + default: + return undefined; + } } get openingElement(): AstNode | undefined { - return this.childNode("openingElement"); + switch (this.kind) { + case 285: + return this.childNodeAt(0); + default: + return undefined; + } } get openingFragment(): AstNode | undefined { - return this.childNode("openingFragment"); + switch (this.kind) { + case 289: + return this.childNodeAt(0); + default: + return undefined; + } } get operatorToken(): AstNode | undefined { - return this.childNode("operatorToken"); + switch (this.kind) { + case 227: + return this.childNodeAt(3); + default: + return undefined; + } } get parameterName(): AstNode | undefined { - return this.childNode("parameterName"); + switch (this.kind) { + case 183: + return this.childNodeAt(1); + default: + return undefined; + } } get parameters(): AstNodeList | undefined { - return this.childList("parameters"); + switch (this.kind) { + case 180: + case 181: + case 182: + case 185: + case 318: + return this.childListAt(1); + case 177: + case 186: + case 220: + return this.childListAt(2); + case 178: + case 179: + return this.childListAt(3); + case 174: + case 219: + case 263: + return this.childListAt(4); + case 175: + return this.childListAt(5); + default: + return undefined; + } } get postfixToken(): AstNode | undefined { - return this.childNode("postfixToken"); + switch (this.kind) { + case 172: + case 173: + case 174: + case 303: + case 304: + return this.childNodeAt(2); + case 175: + return this.childNodeAt(3); + default: + return undefined; + } } get properties(): AstNodeList | undefined { - return this.childList("properties"); + switch (this.kind) { + case 211: + case 293: + return this.childListAt(0); + default: + return undefined; + } } get propertyName(): AstNode | undefined { - return this.childNode("propertyName"); + switch (this.kind) { + case 277: + case 282: + return this.childNodeAt(0); + case 209: + return this.childNodeAt(1); + default: + return undefined; + } } get qualifier(): AstNode | undefined { - return this.childNode("qualifier"); + switch (this.kind) { + case 206: + return this.childNodeAt(2); + default: + return undefined; + } } get questionDotToken(): AstNode | undefined { - return this.childNode("questionDotToken"); + switch (this.kind) { + case 212: + case 213: + case 214: + case 216: + return this.childNodeAt(1); + default: + return undefined; + } } get readonlyToken(): AstNode | undefined { - return this.childNode("readonlyToken"); + switch (this.kind) { + case 201: + return this.childNodeAt(0); + default: + return undefined; + } } get right(): AstNode | undefined { - return this.childNode("right"); + switch (this.kind) { + case 167: + return this.childNodeAt(1); + case 227: + return this.childNodeAt(4); + default: + return undefined; + } } get statement(): AstNode | undefined { - return this.childNode("statement"); + switch (this.kind) { + case 247: + return this.childNodeAt(0); + case 248: + case 255: + case 257: + return this.childNodeAt(1); + case 249: + case 250: + case 251: + return this.childNodeAt(3); + default: + return undefined; + } } get statements(): AstNodeList | undefined { - return this.childList("statements"); + switch (this.kind) { + case 242: + case 269: + case 307: + return this.childListAt(0); + case 297: + case 298: + return this.childListAt(1); + default: + return undefined; + } } get tag(): AstNode | undefined { - return this.childNode("tag"); + switch (this.kind) { + case 216: + return this.childNodeAt(0); + default: + return undefined; + } } get tagName(): AstNode | undefined { - return this.childNode("tagName"); + switch (this.kind) { + case 286: + case 287: + case 288: + case 322: + case 323: + case 324: + case 325: + case 326: + case 327: + case 328: + case 329: + case 330: + case 331: + case 332: + case 333: + case 334: + case 335: + case 336: + case 337: + case 338: + case 339: + case 340: + case 341: + case 342: + case 343: + return this.childNodeAt(0); + default: + return undefined; + } } get tags(): AstNodeList | undefined { - return this.childList("tags"); + switch (this.kind) { + case 315: + return this.childListAt(1); + default: + return undefined; + } } get template(): AstNode | undefined { - return this.childNode("template"); + switch (this.kind) { + case 216: + return this.childNodeAt(3); + default: + return undefined; + } } get templateSpans(): AstNodeList | undefined { - return this.childList("templateSpans"); + switch (this.kind) { + case 204: + case 229: + return this.childListAt(1); + default: + return undefined; + } } get thenStatement(): AstNode | undefined { - return this.childNode("thenStatement"); + switch (this.kind) { + case 246: + return this.childNodeAt(1); + default: + return undefined; + } } get thisArg(): AstNode | undefined { - return this.childNode("thisArg"); + switch (this.kind) { + case 349: + return this.childNodeAt(1); + default: + return undefined; + } } get trueType(): AstNode | undefined { - return this.childNode("trueType"); + switch (this.kind) { + case 195: + return this.childNodeAt(2); + default: + return undefined; + } } get tryBlock(): AstNode | undefined { - return this.childNode("tryBlock"); + switch (this.kind) { + case 259: + return this.childNodeAt(0); + default: + return undefined; + } } get tupleNameSource(): AstNode | undefined { - return this.childNode("tupleNameSource"); + switch (this.kind) { + case 238: + return this.childNodeAt(0); + default: + return undefined; + } } get type(): AstNode | undefined { - return this.childNode("type"); + switch (this.kind) { + case 191: + case 192: + case 197: + case 199: + case 205: + case 217: + case 308: + case 311: + case 312: + case 313: + case 314: + return this.childNodeAt(0); + case 235: + case 239: + case 278: + return this.childNodeAt(1); + case 180: + case 181: + case 182: + case 183: + case 185: + case 227: + case 261: + case 318: + return this.childNodeAt(2); + case 172: + case 173: + case 177: + case 186: + case 203: + case 220: + case 266: + case 303: + case 304: + case 345: + return this.childNodeAt(3); + case 170: + case 178: + case 179: + case 201: + return this.childNodeAt(4); + case 174: + case 219: + case 263: + return this.childNodeAt(5); + case 175: + return this.childNodeAt(6); + default: + return undefined; + } } get typeArguments(): AstNodeList | undefined { - return this.childList("typeArguments"); + switch (this.kind) { + case 184: + case 187: + case 215: + case 234: + case 286: + case 287: + return this.childListAt(1); + case 214: + case 216: + return this.childListAt(2); + case 206: + return this.childListAt(3); + default: + return undefined; + } } get typeExpression(): AstNode | undefined { - return this.childNode("typeExpression"); + switch (this.kind) { + case 331: + case 332: + case 334: + case 335: + case 336: + case 338: + case 341: + case 342: + return this.childNodeAt(1); + case 333: + case 340: + return this.childNodeAt(2); + default: + return undefined; + } } get typeName(): AstNode | undefined { - return this.childNode("typeName"); + switch (this.kind) { + case 184: + return this.childNodeAt(0); + default: + return undefined; + } } get typeParameter(): AstNode | undefined { - return this.childNode("typeParameter"); + switch (this.kind) { + case 196: + return this.childNodeAt(0); + case 201: + return this.childNodeAt(1); + default: + return undefined; + } } get typeParameters(): AstNodeList | undefined { - return this.childList("typeParameters"); + switch (this.kind) { + case 180: + case 181: + case 185: + case 318: + return this.childListAt(0); + case 177: + case 186: + case 220: + return this.childListAt(1); + case 178: + case 179: + case 232: + case 264: + case 265: + case 266: + case 337: + case 345: + return this.childListAt(2); + case 174: + case 219: + case 263: + return this.childListAt(3); + case 175: + return this.childListAt(4); + default: + return undefined; + } } get types(): AstNodeList | undefined { - return this.childList("types"); + switch (this.kind) { + case 193: + case 194: + case 299: + return this.childListAt(0); + default: + return undefined; + } } get value(): AstNode | undefined { - return this.childNode("value"); + switch (this.kind) { + case 302: + return this.childNodeAt(1); + default: + return undefined; + } } get variableDeclaration(): AstNode | undefined { - return this.childNode("variableDeclaration"); + switch (this.kind) { + case 300: + return this.childNodeAt(0); + default: + return undefined; + } } get whenFalse(): AstNode | undefined { - return this.childNode("whenFalse"); + switch (this.kind) { + case 228: + return this.childNodeAt(4); + default: + return undefined; + } } get whenTrue(): AstNode | undefined { - return this.childNode("whenTrue"); + switch (this.kind) { + case 228: + return this.childNodeAt(2); + default: + return undefined; + } + } + private bodySlot(): number { + switch (this.kind) { + case 176: + return 1; + case 268: + return 2; + case 177: + return 4; + case 178: + case 179: + case 220: + return 5; + case 219: + case 263: + return 6; + case 175: + return 7; + default: + return -1; + } + } + private initializerSlot(): number { + switch (this.kind) { + case 249: + return 0; + case 250: + case 251: + case 292: + case 306: + return 1; + case 209: + case 261: + return 3; + case 172: + case 173: + case 303: + return 4; + case 170: + return 5; + default: + return -1; + } + } + private leftSlot(): number { + switch (this.kind) { + case 167: + return 0; + case 227: + return 1; + default: + return -1; + } + } + private modifiersSlot(): number { + switch (this.kind) { + case 169: + case 170: + case 172: + case 173: + case 174: + case 175: + case 176: + case 177: + case 178: + case 179: + case 182: + case 186: + case 219: + case 220: + case 227: + case 232: + case 244: + case 263: + case 264: + case 265: + case 266: + case 267: + case 268: + case 271: + case 272: + case 273: + case 278: + case 279: + case 283: + case 303: + case 304: + case 345: + case 346: + return 0; + default: + return -1; + } + } + private operandSlot(): number { + switch (this.kind) { + case 225: + case 226: + return 0; + default: + return -1; + } + } + private questionTokenSlot(): number { + switch (this.kind) { + case 228: + return 1; + case 203: + return 2; + case 170: + case 201: + return 3; + default: + return -1; + } } // END GENERATED CHILD GETTERS // Source-file fields stay on the same nominal class, matching the wire's // single node representation. Their extended-data accesses are checked. get fileName(): string { - return this.file.wire.string(this.file.wire.extendedWord(this.index, 4)); + return this.file.sourceFileName(this.index); } get path(): string { return this.file.wire.string(this.file.wire.extendedWord(this.index, 8)); diff --git a/packages/compiler/src/frontend/ts7/ast-wire.test.ts b/packages/compiler/src/frontend/ts7/ast-wire.test.ts index 03a98a2dc..fd3e611e2 100644 --- a/packages/compiler/src/frontend/ts7/ast-wire.test.ts +++ b/packages/compiler/src/frontend/ts7/ast-wire.test.ts @@ -101,6 +101,30 @@ test("node words respect a byte view's offset, signed spans and unsigned flags", expect(() => file.kind(5)).toThrow(AstDecodeError); }); +test("strings, string offsets and node words read alike at every byte alignment", () => { + const original = response(); + for (let shift = 0; shift < 4; shift++) { + const padded = new Uint8Array(original.length + 8).fill(0xff); + padded.set(original, shift); + const bytes = padded.subarray(shift, shift + original.length); + const file = new AstWireFile(bytes); + expect([file.string(0), file.string(2), file.text(1), file.text(3)]).toEqual([ + "abcfile!", + "file", + "abcfile!", + "file", + ]); + expect([file.kind(3), file.next(3), file.parent(4), file.data(4)]).toEqual([ + AstKind.Identifier, + 4, + 2, + 0x40000002, + ]); + word(bytes, 56, 9); + expect(() => new AstWireFile(bytes)).toThrow("invalid string table offset"); + } +}); + test("truncated headers, sections and node tables are rejected", () => { for (let size = 0; size < HEADER_SIZE; size++) expect(() => new AstWireFile(new Uint8Array(size))).toThrow(AstDecodeError); diff --git a/packages/compiler/src/frontend/ts7/ast-wire.ts b/packages/compiler/src/frontend/ts7/ast-wire.ts index ff70148df..6b2222eae 100644 --- a/packages/compiler/src/frontend/ts7/ast-wire.ts +++ b/packages/compiler/src/frontend/ts7/ast-wire.ts @@ -109,6 +109,20 @@ function invalidNodeIndex(index: number): never { * byte order. */ const LITTLE_ENDIAN = new Uint8Array(new Uint32Array([1]).buffer)[0] === 1; +/** `count` little-endian words of `bytes` from `offset`, which the caller has + * bounds-checked: a view of the response when the words are aligned on a + * little-endian host, a copy of their bytes when they are not, and a decoded + * copy on a big-endian host. Where a section starts depends on the lengths of + * the strings before it (file paths among them), so most files are unaligned. */ +function astWords(bytes: Uint8Array, offset: number, count: number): Uint32Array { + const start = bytes.byteOffset + offset; + if (LITTLE_ENDIAN && start % 4 === 0) return new Uint32Array(bytes.buffer, start, count); + const words = new Uint32Array(count); + if (LITTLE_ENDIAN) new Uint8Array(words.buffer).set(bytes.subarray(offset, offset + count * 4)); + else for (let i = 0; i < count; i++) words[i] = astU32(bytes, offset + i * 4); + return words; +} + function hexWord(value: number): string { const digits = "0123456789abcdef"; let result = ""; @@ -125,14 +139,15 @@ export class AstWireFile { readonly stringCount: number; readonly contentHash: string; readonly parseOptionsKey: string; - private readonly stringOffsets: number; private readonly strings: number; private readonly extended: number; private readonly structured: number; private readonly nodes: number; - /** The node table as 32-bit words: a view of the response when it is - * word-aligned on a little-endian host, otherwise a decoded copy. */ + /** The node table as 32-bit words (astWords). */ private readonly words: Uint32Array; + /** The string table's start and end offsets, two words per string + * (astWords). */ + private readonly stringWords: Uint32Array; /** Decoded strings by string word index / 2. */ private readonly stringCache: (string | undefined)[]; private preorderTree: AstPreorder | null | undefined; @@ -142,34 +157,28 @@ export class AstWireFile { const metadata = astU32(bytes, HEADER_OFFSET_METADATA); if (metadata >>> 24 !== PROTOCOL_VERSION) throw new AstDecodeError(`unsupported protocol version ${metadata >>> 24}`); - this.stringOffsets = astU32(bytes, HEADER_OFFSET_STRING_TABLE_OFFSETS); + const stringOffsets = astU32(bytes, HEADER_OFFSET_STRING_TABLE_OFFSETS); this.strings = astU32(bytes, HEADER_OFFSET_STRING_TABLE); this.extended = astU32(bytes, HEADER_OFFSET_EXTENDED_DATA); this.structured = astU32(bytes, HEADER_OFFSET_STRUCTURED_DATA); this.nodes = astU32(bytes, HEADER_OFFSET_NODES); if ( - this.stringOffsets < HEADER_SIZE || - this.strings < this.stringOffsets || + stringOffsets < HEADER_SIZE || + this.strings < stringOffsets || this.extended < this.strings || this.structured < this.extended || this.nodes < this.structured || this.nodes > bytes.length || - (this.strings - this.stringOffsets) % 8 !== 0 || + (this.strings - stringOffsets) % 8 !== 0 || (this.structured - this.extended) % 4 !== 0 || (bytes.length - this.nodes) % NODE_LEN !== 0 ) { throw new AstDecodeError("invalid section layout"); } this.nodeCount = (bytes.length - this.nodes) / NODE_LEN; - this.stringCount = (this.strings - this.stringOffsets) / 8; - const wordCount = this.nodeCount * NODE_WORDS; - const start = bytes.byteOffset + this.nodes; - if (LITTLE_ENDIAN && start % 4 === 0) { - this.words = new Uint32Array(bytes.buffer, start, wordCount); - } else { - this.words = new Uint32Array(wordCount); - for (let i = 0; i < wordCount; i++) this.words[i] = astU32(bytes, this.nodes + i * 4); - } + this.stringCount = (this.strings - stringOffsets) / 8; + this.words = astWords(bytes, this.nodes, this.nodeCount * NODE_WORDS); + this.stringWords = astWords(bytes, stringOffsets, this.stringCount * 2); this.stringCache = new Array(this.stringCount).fill(undefined); if (this.nodeCount < 2) throw new AstDecodeError("missing root"); if (this.kind(1) === KIND_NODE_LIST || this.parent(1) > 1) @@ -184,8 +193,8 @@ export class AstWireFile { // source text, so pairs overlap and are not globally monotone. Handles // address words (0, 2, 4...), not pair ordinals. for (let i = 0; i < this.stringCount; i++) { - const start = astU32(bytes, this.stringOffsets + i * 8); - const end = astU32(bytes, this.stringOffsets + i * 8 + 4); + const start = this.stringWords[i * 2]!; + const end = this.stringWords[i * 2 + 1]!; if (end < start || end > this.extended - this.strings) throw new AstDecodeError("invalid string table offset"); } @@ -240,12 +249,12 @@ export class AstWireFile { string(index: number): string { if (index >>> 0 !== index || index % 2 !== 0 || index >= this.stringCount * 2) throw new AstDecodeError(`invalid string index ${index}`); - const cached = this.stringCache[index / 2]; + const cached = this.stringCache[index >>> 1]; if (cached !== undefined) return cached; - const start = astU32(this.bytes, this.stringOffsets + index * 4); - const end = astU32(this.bytes, this.stringOffsets + (index + 1) * 4); + const start = this.stringWords[index]!; + const end = this.stringWords[index + 1]!; const text = decodeAstString(this.bytes, this.strings + start, end - start); - this.stringCache[index / 2] = text; + this.stringCache[index >>> 1] = text; return text; } diff --git a/packages/compiler/src/frontend/ts7/checker-backend.test.ts b/packages/compiler/src/frontend/ts7/checker-backend.test.ts index e37357afa..cd37bafff 100644 --- a/packages/compiler/src/frontend/ts7/checker-backend.test.ts +++ b/packages/compiler/src/frontend/ts7/checker-backend.test.ts @@ -24,15 +24,11 @@ function writePackage(directory: string, name: string, executable?: string): voi writeFileSync(join(directory, "lib", executable), ""); } -test("tsc-ts is the default checker except on Windows, and SCRIPTC_CHECKER selects one", () => { - expect(checkerBackend({}, "linux")).toBe("tsc-ts"); - expect(checkerBackend({ SCRIPTC_CHECKER: "" }, "linux")).toBe("tsc-ts"); - expect(checkerBackend({}, "darwin")).toBe("tsc-ts"); - expect(checkerBackend({}, "win32")).toBe("tsgo"); - expect(checkerBackend({ SCRIPTC_CHECKER: "" }, "win32")).toBe("tsgo"); - expect(checkerBackend({ SCRIPTC_CHECKER: "tsc-ts" }, "linux")).toBe("tsc-ts"); - expect(checkerBackend({ SCRIPTC_CHECKER: "tsc-ts" }, "win32")).toBe("tsc-ts"); - expect(checkerBackend({ SCRIPTC_CHECKER: "tsgo" }, "linux")).toBe("tsgo"); +test("tsc-ts is the default checker, and SCRIPTC_CHECKER selects one", () => { + expect(checkerBackend({})).toBe("tsc-ts"); + expect(checkerBackend({ SCRIPTC_CHECKER: "" })).toBe("tsc-ts"); + expect(checkerBackend({ SCRIPTC_CHECKER: "tsc-ts" })).toBe("tsc-ts"); + expect(checkerBackend({ SCRIPTC_CHECKER: "tsgo" })).toBe("tsgo"); expect(() => checkerBackend({ SCRIPTC_CHECKER: "tsc" })).toThrow( 'SCRIPTC_CHECKER must be tsgo or tsc-ts, not "tsc"', ); diff --git a/packages/compiler/src/frontend/ts7/checker-backend.ts b/packages/compiler/src/frontend/ts7/checker-backend.ts index 14418cc94..0b32f24f6 100644 --- a/packages/compiler/src/frontend/ts7/checker-backend.ts +++ b/packages/compiler/src/frontend/ts7/checker-backend.ts @@ -4,17 +4,13 @@ import { fileURLToPath } from "node:url"; /** The TypeScript 7 API servers a compile can check with: tsc-ts, the default, * and tsgo from the `typescript` package, selected with `SCRIPTC_CHECKER=tsgo`. - * Both serve the same API and give the same results. On Windows the default is - * tsgo: the compiler connects to the server over a named pipe there, and tsc-ts's - * API server has no named-pipe transport yet. */ + * Both serve the same API and give the same results. On Windows the Node host + * connects to either over a named pipe the server creates (rpc-process.ts). */ export type CheckerBackend = "tsgo" | "tsc-ts"; -export function checkerBackend( - env: NodeJS.ProcessEnv = process.env, - platform: string = process.platform, -): CheckerBackend { +export function checkerBackend(env: NodeJS.ProcessEnv = process.env): CheckerBackend { const value = env["SCRIPTC_CHECKER"]; - if (value === undefined || value === "") return platform === "win32" ? "tsgo" : "tsc-ts"; + if (value === undefined || value === "") return "tsc-ts"; if (value === "tsc-ts") return "tsc-ts"; if (value === "tsgo") return "tsgo"; throw new Error(`SCRIPTC_CHECKER must be tsgo or tsc-ts, not ${JSON.stringify(value)}`); diff --git a/packages/compiler/src/frontend/ts7/checker.ts b/packages/compiler/src/frontend/ts7/checker.ts index 9550d1dc2..eb2b9e20b 100644 --- a/packages/compiler/src/frontend/ts7/checker.ts +++ b/packages/compiler/src/frontend/ts7/checker.ts @@ -631,6 +631,14 @@ export class CheckerFacade { return aliased; } + /** A module symbol's exports, with the members of its `export *` + * declarations resolved (a symbol's own export table lists only the + * module's own declarations). */ + getExportsOfModule(symbol: Ts7Symbol): readonly Ts7Symbol[] { + this.ensureActive(); + return this.raw.getExportsOfModule(symbol); + } + getDeclaredTypeOfSymbol(symbol: Ts7Symbol): Type { this.ensureActive(); let type = this.cache.declaredTypeOfSymbol.get(symbol); diff --git a/packages/compiler/src/frontend/ts7/lib-diagnostics.test.ts b/packages/compiler/src/frontend/ts7/lib-diagnostics.test.ts new file mode 100644 index 000000000..f041a6cb5 --- /dev/null +++ b/packages/compiler/src/frontend/ts7/lib-diagnostics.test.ts @@ -0,0 +1,221 @@ +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterAll, afterEach, expect, test } from "vitest"; +import type { SourceFile } from "./ast-types.js"; +import { + reusedLibDiagnostics, + setLibDiagnosticsStore, + type LibDiagnosticsStore, +} from "./lib-diagnostics.js"; +import type { Ts7DiagnosticData } from "./session-schema.generated.js"; +import type { Ts7CheckedFileDiagnosticData, Ts7SessionProgram } from "./session.js"; +import { closeSourceParser, parseSourceFile } from "./source-parser-node.js"; + +const dir = mkdtempSync(join(tmpdir(), "scriptc-lib-diagnostics-")); +const lib = join(dir, "lib.es5.d.ts"); +const fallback = join(dir, "scriptc-node-fallback.d.ts"); +const libText = "declare var NaN: number;\ninterface Array { length: number }\n"; +const fallbackText = "export {};\ndeclare global {\n var process: { argv: string[] };\n}\n"; +writeFileSync(lib, libText); +writeFileSync(fallback, fallbackText); +const main = join(dir, "main.ts"); +const util = join(dir, "\u{1F600}.ts"); +const other = join(dir, "�.ts"); + +afterEach(() => setLibDiagnosticsStore(null)); +afterAll(() => { + closeSourceParser(); + rmSync(dir, { recursive: true, force: true }); +}); + +function memoryStore(): LibDiagnosticsStore & { entries: Map } { + const entries = new Map(); + return { + entries, + read: (key) => entries.get(key) ?? null, + write: (key, text) => void entries.set(key, text), + }; +} + +function diagnostic(fileName: string | undefined, pos: number, code = 2322): Ts7DiagnosticData { + return { fileName, pos, end: pos + 1, code, category: 1, text: `at ${pos}` }; +} + +/** A session program whose full-check and checked-file answers are given; it + * records which requests it was asked. */ +function sessionProgram(answers: { + semantic: Map; + global?: Ts7DiagnosticData[]; + checked?: boolean; +}): Ts7SessionProgram & { asked: string[] } { + const asked: string[] = []; + const all = [...answers.semantic.values()].flat(); + return { + asked, + getCompilerOptions: () => ({ strict: true }), + diagnosticData: (method: string, file?: string) => { + asked.push(file === undefined ? method : `${method} ${file}`); + if (method === "getGlobalDiagnostics") return answers.global ?? []; + return file === undefined ? all : (answers.semantic.get(file) ?? []); + }, + checkedFileDiagnosticData: (files: readonly string[]): Ts7CheckedFileDiagnosticData | null => { + asked.push(`checked ${files.join(",")}`); + if (answers.checked === false) return null; + return { files: files.map((file) => answers.semantic.get(file) ?? []), global: [] }; + }, + } as unknown as Ts7SessionProgram & { asked: string[] }; +} + +/** The program's files, parsed: the supplied two and the given others. */ +function files(texts: Record): (name: string) => SourceFile | undefined { + const parsed = new Map([ + [lib, parseSourceFile(lib, libText, "ts")], + [fallback, parseSourceFile(fallback, fallbackText, "ts")], + ]); + for (const [name, text] of Object.entries(texts)) + parsed.set(name, parseSourceFile(name, text, "ts")); + return (name) => parsed.get(name); +} + +const names = [lib, fallback, main]; +const supplied = (name: string): boolean => name === lib || name === fallback; +const moduleFile = files({ [main]: "export const n: number = 'x';\n" }); + +test("without a store the full check answers", () => { + const program = sessionProgram({ semantic: new Map() }); + expect(reusedLibDiagnostics(program, names, supplied, moduleFile)).toBeNull(); + expect(program.asked).toEqual([]); +}); + +test("a stored answer stands in for checking the supplied files, merged by file name", () => { + const store = memoryStore(); + setLibDiagnosticsStore(store, "checker"); + const semantic = new Map([ + [lib, [diagnostic(lib, 1)]], + [fallback, []], + [main, [diagnostic(main, 5), diagnostic(main, 9)]], + ]); + const first = sessionProgram({ semantic }); + const full = reusedLibDiagnostics(first, names, supplied, moduleFile); + // The first build runs the full check and stores the supplied files' answers. + expect(first.asked).toEqual([ + "getGlobalDiagnostics", + "getSemanticDiagnostics", + "checked ", + `getSemanticDiagnostics ${lib}`, + `getSemanticDiagnostics ${fallback}`, + ]); + expect(store.entries.size).toBe(1); + const second = sessionProgram({ semantic }); + const reused = reusedLibDiagnostics(second, names, supplied, moduleFile); + expect(second.asked).toEqual([`checked ${main}`]); + // The files' diagnostics in the order of their names (lib.es5.d.ts, main.ts), as the full answer. + expect(reused).toEqual({ + semantic: [diagnostic(lib, 1), diagnostic(main, 5), diagnostic(main, 9)], + global: [], + }); + expect(full?.semantic).toEqual(reused?.semantic); +}); + +test("files merge in code point order, which differs from UTF-16 order", () => { + const store = memoryStore(); + setLibDiagnosticsStore(store, "checker"); + const all = [lib, fallback, util, other]; + const sources = files({ [util]: "export {};\n", [other]: "export {};\n" }); + const semantic = new Map([ + [util, [diagnostic(util, 1)]], + [other, [diagnostic(other, 2)]], + ]); + reusedLibDiagnostics(sessionProgram({ semantic }), all, supplied, sources); + const reused = reusedLibDiagnostics(sessionProgram({ semantic }), all, supplied, sources); + // U+FFFD sorts before U+1F600 by code point (its surrogates sort after it by UTF-16 unit). + expect(reused?.semantic).toEqual([diagnostic(other, 2), diagnostic(util, 1)]); +}); + +test("a full check with global diagnostics is not stored", () => { + const store = memoryStore(); + setLibDiagnosticsStore(store, "checker"); + const program = sessionProgram({ + semantic: new Map([[main, [diagnostic(main, 1)]]]), + global: [diagnostic(undefined, 0, 2318)], + }); + expect(reusedLibDiagnostics(program, names, supplied, moduleFile)?.global).toEqual([ + diagnostic(undefined, 0, 2318), + ]); + expect(store.entries.size).toBe(0); +}); + +test("scripts reuse the answer unless they declare a global the supplied files declare", () => { + const store = memoryStore(); + setLibDiagnosticsStore(store, "checker"); + const semantic = new Map([[main, []]]); + // A script declaring only new names: it stores the answer, and a later one reuses it. + const fresh = files({ + [main]: "const total = 1;\nfunction run() {}\nif (total) { var later = 2; }\n", + }); + reusedLibDiagnostics(sessionProgram({ semantic }), names, supplied, fresh); + expect(store.entries.size).toBe(1); + const again = sessionProgram({ semantic }); + expect(reusedLibDiagnostics(again, names, supplied, fresh)).not.toBeNull(); + expect(again.asked).toEqual([`checked ${main}`]); + // Scripts declaring a supplied global, at the top level, in a block, in a + // `declare global` of a supplied module, or an ambient module: the full check. + for (const text of [ + "interface Array { extra: T }\n", + "if (true) { var NaN = 1; }\n", + "declare var process: unknown;\n", + 'declare module "fs" {}\n', + ]) { + const program = sessionProgram({ semantic }); + const result = reusedLibDiagnostics(program, names, supplied, files({ [main]: text })); + expect(program.asked[0], text).not.toBe(`checked ${main}`); + if (result !== null) expect(program.asked[0]).toBe("getGlobalDiagnostics"); + } + expect(store.entries.size).toBe(1); +}); + +test("a program whose script declares a supplied global does not store its answer", () => { + const store = memoryStore(); + setLibDiagnosticsStore(store, "checker"); + const program = sessionProgram({ semantic: new Map([[main, []]]) }); + reusedLibDiagnostics(program, names, supplied, files({ [main]: "declare var NaN: number;\n" })); + expect(store.entries.size).toBe(0); +}); + +test("modules that can declare something global, and servers without the method, ask as before", () => { + const store = memoryStore(); + setLibDiagnosticsStore(store, "checker"); + const semantic = new Map([[main, []]]); + reusedLibDiagnostics(sessionProgram({ semantic }), names, supplied, moduleFile); + expect(store.entries.size).toBe(1); + for (const text of [ + "export {};\ndeclare global { var extra: number }\n", + 'export {};\ndeclare module "./other" { const extra: number }\n', + "export as namespace Lib;\nexport const n = 1;\n", + ]) { + const program = sessionProgram({ semantic }); + expect(reusedLibDiagnostics(program, names, supplied, files({ [main]: text }))).toBeNull(); + expect(program.asked).toEqual([]); + } + const old = sessionProgram({ semantic, checked: false }); + expect(reusedLibDiagnostics(old, names, supplied, moduleFile)).toBeNull(); + // Nor does a server without the method store an answer it could not use. + const fresh = memoryStore(); + setLibDiagnosticsStore(fresh, "checker"); + reusedLibDiagnostics(sessionProgram({ semantic, checked: false }), names, supplied, moduleFile); + expect(fresh.entries.size).toBe(0); +}); + +test("a supplied file that changes is a different entry", () => { + const store = memoryStore(); + setLibDiagnosticsStore(store, "checker"); + const semantic = new Map([[main, []]]); + reusedLibDiagnostics(sessionProgram({ semantic }), names, supplied, moduleFile); + writeFileSync(fallback, fallbackText + "// edited\n"); + const program = sessionProgram({ semantic }); + reusedLibDiagnostics(program, names, supplied, moduleFile); + expect(program.asked[0]).toBe("getGlobalDiagnostics"); + expect(store.entries.size).toBe(2); + writeFileSync(fallback, fallbackText); +}); diff --git a/packages/compiler/src/frontend/ts7/lib-diagnostics.ts b/packages/compiler/src/frontend/ts7/lib-diagnostics.ts new file mode 100644 index 000000000..827b337bd --- /dev/null +++ b/packages/compiler/src/frontend/ts7/lib-diagnostics.ts @@ -0,0 +1,324 @@ +/* The semantic diagnostics of the declaration files scriptc supplies (the + * TypeScript libs and scriptc's own declaration files), reused across + * builds. Checking them is most of the full check of a small program. When + * no other file of a program can change what checking those files reports + * (every one is a module without global or module augmentations, or a + * script declaring no global name the supplied files declare), the answer + * depends only on the checker, the compiler options and the files + * themselves, so a stored answer for the same three stands in for checking + * them again: the server checks only the program's other files + * (getDiagnosticsOfCheckedFiles), and the semantic diagnostics merge in the + * order the full check sorts them, by file name. An answer is stored only + * when the full check reports no global diagnostics: the global diagnostics + * of checking the other files are then the full check's. */ +import { createHash } from "node:crypto"; +import { statSync } from "node:fs"; +import type { Node, SourceFile } from "./ast-types.js"; +import type { Ts7DiagnosticData } from "./session-schema.generated.js"; +import type { Ts7SessionProgram } from "./session.js"; +import { NodeFlags, SyntaxKind } from "./enums.js"; +import { walkPreorder } from "./ast.js"; + +/** Key-value storage for reused diagnostics (best effort: failures miss). */ +export interface LibDiagnosticsStore { + read(key: string): string | null; + write(key: string, text: string): void; +} + +const SCHEMA = "scriptc.lib-diagnostics.v1"; + +let configured: { store: LibDiagnosticsStore; checker: string } | null = null; + +/** Has later builds of this process reuse the diagnostics of the supplied + * declaration files through `store`, for the checker identified by + * `checker` (its executable's identity), or stop reusing them (null). */ +export function setLibDiagnosticsStore(store: LibDiagnosticsStore | null, checker = ""): void { + configured = store === null ? null : { store, checker }; +} + +/** A checker executable's identity for the stored diagnostics' key: its + * path and file identity, or the path alone when it cannot be read. */ +export function checkerExecutableIdentity(path: string): string { + try { + const info = statSync(path); + return JSON.stringify([path, info.size, info.mtimeMs, info.ino]); + } catch { + return JSON.stringify([path]); + } +} + +/** The program's semantic and global diagnostics as the full check reports + * them, using stored diagnostics for the supplied declaration files; null + * when they cannot be reused for this program (then the caller asks for + * them as before). `supplied` tells whether a program file is one of the + * supplied declaration files. */ +export function reusedLibDiagnostics( + program: Ts7SessionProgram, + fileNames: readonly string[], + supplied: (fileName: string) => boolean, + sourceFile: (fileName: string) => SourceFile | undefined, +): { semantic: Ts7DiagnosticData[]; global: Ts7DiagnosticData[] } | null { + if (configured === null || process.env["SCRIPTC_LIB_DIAGNOSTICS"] === "0") return null; + const { store, checker } = configured; + const libs: string[] = []; + const own: string[] = []; + for (const name of fileNames) (supplied(name) ? libs : own).push(name); + if (libs.length === 0) return null; + // The global names each of the program's scripts declares; a module that + // declares nothing global contributes none. + const scripts: Set[] = []; + for (const name of own) { + const file = sourceFile(name); + if (file === undefined) return null; + if (file.externalModuleIndicator !== undefined) { + if (!declaresNothingGlobal(file)) return null; + continue; + } + const names = isJsFileName(name) ? null : scriptGlobalNames(file); + if (names === null) return null; + scripts.push(names); + } + // A script's globals cannot change what the supplied files report unless + // one of them is also a global the supplied files declare. + const collides = (globals: ReadonlySet): boolean => + scripts.some((names) => [...names].some((name) => globals.has(name))); + // The options without the configuration file's path: it names where the + // project is, which checking the supplied files does not read (the files a + // project's location brings in are part of the key). + const options = { ...program.getCompilerOptions(), configFilePath: undefined }; + const key = entryKey(checker, options, libs); + if (key === null) return null; + const stored = readEntry(store, key, libs); + if (stored !== null && !collides(stored.globals)) { + const checked = program.checkedFileDiagnosticData(own); + if (checked === null) return null; + const byFile = new Map(stored.files); + own.forEach((name, index) => byFile.set(name, checked.files[index] ?? [])); + return { semantic: inFileOrder(byFile), global: checked.global }; + } + // The full check, as before. Its answers for the supplied files are stored + // when it reports no global diagnostics and the program's scripts declare + // none of the supplied files' globals (so nothing of the program reached + // what those files report). + const global = program.diagnosticData("getGlobalDiagnostics"); + const semantic = program.diagnosticData("getSemanticDiagnostics"); + // A server that cannot check only some files could never use the answer. + if (global.length === 0 && stored === null && program.checkedFileDiagnosticData([]) !== null) { + const globals = suppliedGlobalNames(libs, sourceFile); + if (globals !== null && !collides(globals)) { + const files: Record = {}; + for (const name of libs) files[name] = program.diagnosticData("getSemanticDiagnostics", name); + try { + store.write(key, JSON.stringify({ schema: SCHEMA, globals: [...globals], files })); + } catch { + // Best effort: a failed write is a later miss. + } + } + } + return { semantic, global }; +} + +/** Whether a file cannot add to or change the global declarations the + * supplied files declare: a module with no `declare global`, no module + * augmentation and no UMD `export as namespace` declaration. */ +function declaresNothingGlobal(file: SourceFile): boolean { + if (file.externalModuleIndicator === undefined) return false; + if (file.moduleAugmentations.length > 0) return false; + for (const statement of file.statements.nodes) { + if (statement.kind === SyntaxKind.NamespaceExportDeclaration) return false; + // `declare global` (a module declaration named by the identifier `global`). + if ( + statement.kind === SyntaxKind.ModuleDeclaration && + statement.name?.kind === SyntaxKind.Identifier && + statement.name.text === "global" + ) + return false; + } + return true; +} + +function isJsFileName(name: string): boolean { + return /\.[cm]?jsx?$/.test(name); +} + +/** The names a script file declares in the global scope: its top-level + * declarations, and the `var` and function declarations of its top-level + * blocks; null when it declares an ambient module (`declare module "x"`), + * which can merge with one the supplied files declare. Over-approximates: + * a name more only makes the stored answer apply less often. */ +function scriptGlobalNames(file: SourceFile): Set | null { + const names = new Set(); + let ambientModule = false; + walkPreorder(file, (node) => { + if (node === file) return undefined; + if (node.kind === SyntaxKind.ModuleDeclaration && node.name?.kind === SyntaxKind.StringLiteral) + ambientModule = true; + const topLevel = node.parent === file; + if (topLevel || node.kind === SyntaxKind.FunctionDeclaration) addDeclaredNames(node, names); + else if ( + node.kind === SyntaxKind.VariableDeclarationList && + (node.flags & NodeFlags.BlockScoped) === 0 + ) + for (const declaration of node.declarations?.nodes ?? []) + addBindingNames(declaration.name, names); + // Functions, classes and namespaces have scopes of their own. + return GLOBAL_SCOPE_BOUNDARIES.has(node.kind) ? "skip" : undefined; + }); + return ambientModule ? null : names; +} + +/** The global names the supplied files declare: the declarations of their + * scripts (as scriptGlobalNames finds them, with ambient modules as their + * quoted names) and those of the `declare global` blocks of their modules; + * null when one of them is not available. */ +function suppliedGlobalNames( + libs: readonly string[], + sourceFile: (fileName: string) => SourceFile | undefined, +): Set | null { + const names = new Set(); + for (const name of libs) { + const file = sourceFile(name); + if (file === undefined) return null; + if (file.externalModuleIndicator === undefined) { + walkPreorder(file, (node) => { + if (node === file) return undefined; + if ( + node.kind === SyntaxKind.ModuleDeclaration && + node.name?.kind === SyntaxKind.StringLiteral + ) + names.add(JSON.stringify(node.name.text ?? "")); + if (node.parent === file || node.kind === SyntaxKind.FunctionDeclaration) + addDeclaredNames(node, names); + else if ( + node.kind === SyntaxKind.VariableDeclarationList && + (node.flags & NodeFlags.BlockScoped) === 0 + ) + for (const declaration of node.declarations?.nodes ?? []) + addBindingNames(declaration.name, names); + return GLOBAL_SCOPE_BOUNDARIES.has(node.kind) ? "skip" : undefined; + }); + continue; + } + for (const statement of file.statements.nodes) { + if ( + statement.kind !== SyntaxKind.ModuleDeclaration || + statement.name?.kind !== SyntaxKind.Identifier || + statement.name.text !== "global" + ) + continue; + for (const inner of statement.body?.statements?.nodes ?? []) addDeclaredNames(inner, names); + } + } + return names; +} + +const GLOBAL_SCOPE_BOUNDARIES = new Set([ + SyntaxKind.FunctionDeclaration, + SyntaxKind.FunctionExpression, + SyntaxKind.ArrowFunction, + SyntaxKind.MethodDeclaration, + SyntaxKind.Constructor, + SyntaxKind.GetAccessor, + SyntaxKind.SetAccessor, + SyntaxKind.ClassDeclaration, + SyntaxKind.ClassExpression, + SyntaxKind.ModuleDeclaration, +]); + +/** The names a statement declares in the scope it is in. */ +function addDeclaredNames(node: Node, names: Set): void { + switch (node.kind) { + case SyntaxKind.VariableStatement: + for (const declaration of node.declarationList?.declarations?.nodes ?? []) + addBindingNames(declaration.name, names); + return; + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.ClassDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.TypeAliasDeclaration: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.ModuleDeclaration: + case SyntaxKind.ImportEqualsDeclaration: + if (node.name?.kind === SyntaxKind.Identifier) names.add(node.name.text ?? ""); + return; + } +} + +function addBindingNames(name: Node | undefined, names: Set): void { + if (name === undefined) return; + if (name.kind === SyntaxKind.Identifier) { + names.add(name.text ?? ""); + return; + } + // Object and array binding patterns. + for (const element of name.elements?.nodes ?? []) addBindingNames(element.name, names); +} + +/** The stored diagnostics' key: the checker, the compiler options and the + * supplied files in program order with each one's file identity; null when + * one of them cannot be read. */ +function entryKey(checker: string, options: unknown, libs: readonly string[]): string | null { + const files: unknown[] = []; + for (const name of libs) { + try { + const info = statSync(name); + files.push([name, info.size, info.mtimeMs, info.ino]); + } catch { + return null; + } + } + return createHash("sha256") + .update(JSON.stringify({ schema: SCHEMA, checker, options, files })) + .digest("hex"); +} + +function readEntry( + store: LibDiagnosticsStore, + key: string, + libs: readonly string[], +): { files: Map; globals: Set } | null { + let text: string | null; + try { + text = store.read(key); + } catch { + return null; + } + if (text === null) return null; + let entry: { schema?: unknown; globals?: unknown; files?: Record }; + try { + entry = JSON.parse(text) as typeof entry; + } catch { + return null; + } + if (entry.schema !== SCHEMA || entry.files === undefined || !Array.isArray(entry.globals)) + return null; + const files = new Map(); + for (const name of libs) { + const list = entry.files[name]; + if (!Array.isArray(list)) return null; + files.set(name, list); + } + return { files, globals: new Set(entry.globals.filter((name) => typeof name === "string")) }; +} + +/** Each file's diagnostics (already in the checker's order) in the order the + * checker sorts files: by file name, compared by code point. */ +function inFileOrder(byFile: ReadonlyMap): Ts7DiagnosticData[] { + const names = [...byFile.keys()].sort(compareCodePoints); + const out: Ts7DiagnosticData[] = []; + for (const name of names) for (const d of byFile.get(name)!) out.push(d); + return out; +} + +function compareCodePoints(a: string, b: string): number { + let i = 0; + let j = 0; + while (i < a.length && j < b.length) { + const x = a.codePointAt(i)!; + const y = b.codePointAt(j)!; + if (x !== y) return x < y ? -1 : 1; + i += x > 0xffff ? 2 : 1; + j += y > 0xffff ? 2 : 1; + } + return i < a.length ? 1 : j < b.length ? -1 : 0; +} diff --git a/packages/compiler/src/frontend/ts7/program-host.ts b/packages/compiler/src/frontend/ts7/program-host.ts index 639086e94..ec143e401 100644 --- a/packages/compiler/src/frontend/ts7/program-host.ts +++ b/packages/compiler/src/frontend/ts7/program-host.ts @@ -46,6 +46,10 @@ import { trackedRealpath, } from "../input-tracker.js"; import { packageShimDeclarationText } from "../package-shims.js"; +import { ambientDtsPath, fallbackDtsPath, overridesDtsPath } from "../dts-paths.js"; +import { reusedLibDiagnostics } from "./lib-diagnostics.js"; +import { ts7Diagnostic } from "./diagnostic.js"; +import type { Ts7DiagnosticData } from "./session-schema.generated.js"; /** The compiler options our createProgram accepts: TS7's CompilerOptions * shape (numeric enums for target/module/moduleResolution — the enums module @@ -393,6 +397,22 @@ export class Ts7Program { return this.project.program.getSourceFileMetadata(resolve(fileName)); } + /** The semantic and global diagnostics with the stored diagnostics of the + * declaration files scriptc supplies (lib-diagnostics.ts), or null. */ + reusedLibDiagnostics(): { semantic: Ts7DiagnosticData[]; global: Ts7DiagnosticData[] } | null { + this.snapshot.ensureActive(); + const program = this.project.program; + const declarations = new Set([ambientDtsPath(), overridesDtsPath(), fallbackDtsPath()]); + return reusedLibDiagnostics( + program, + this.getSourceFileNames(), + (name) => + declarations.has(tsgoPath(name)) || + program.getSourceFileMetadataByPath(name)?.isDefaultLibrary === true, + (name) => this.getSourceFile(name), + ); + } + isSourceFileDefaultLibrary(file: SourceFile): boolean { return this.project.program.isSourceFileDefaultLibrary(file); } @@ -423,9 +443,20 @@ export class Ts7Program { * package dropped the aggregate). Order mirrors 5.9.3: config, options/ * program, global, syntactic, semantic. */ export function getPreEmitDiagnostics(program: Ts7Program): readonly Diagnostic[] { + const config = program.getConfigFileParsingDiagnostics(); + const options = program.getProgramDiagnostics(); + const reused = program.reusedLibDiagnostics(); + if (reused !== null) + return [ + ...config, + ...options, + ...reused.global.map(ts7Diagnostic), + ...program.getSyntacticDiagnostics(), + ...reused.semantic.map(ts7Diagnostic), + ]; return [ - ...program.getConfigFileParsingDiagnostics(), - ...program.getProgramDiagnostics(), + ...config, + ...options, ...program.getGlobalDiagnostics(), ...program.getSyntacticDiagnostics(), ...program.getSemanticDiagnostics(), diff --git a/packages/compiler/src/frontend/ts7/session.ts b/packages/compiler/src/frontend/ts7/session.ts index d3f2ae9fe..082bb79a6 100644 --- a/packages/compiler/src/frontend/ts7/session.ts +++ b/packages/compiler/src/frontend/ts7/session.ts @@ -55,6 +55,13 @@ interface ProgramQuery { snapshot?: number; project?: string; file?: SemanticDocument; + files?: string[]; +} + +/** The answer of getDiagnosticsOfCheckedFiles, as the server sends it. */ +export interface Ts7CheckedFileDiagnosticData { + files: Ts7DiagnosticData[][]; + global: Ts7DiagnosticData[]; } /** Platform-independent session over an already connected transport. The @@ -344,6 +351,29 @@ export class Ts7SessionProgram { result.push(ts7Diagnostic(data)); return result; } + /** The diagnostics of `method` for `file`, or for the whole program, as + * the server sends them. */ + diagnosticData(method: string, file?: SemanticDocument): Ts7DiagnosticData[] { + return this.session.request(method, this.query(file)) ?? []; + } + /** The semantic diagnostics of each of `files` and the global diagnostics, + * after the server checks only those files (getDiagnosticsOfCheckedFiles), + * or null when the server does not answer that method. */ + checkedFileDiagnosticData(files: readonly string[]): Ts7CheckedFileDiagnosticData | null { + try { + return this.session.request("getDiagnosticsOfCheckedFiles", { + ...this.query(), + files: [...files], + }); + } catch (error) { + if ( + error instanceof Error && + error.message.includes('unknown API method "getDiagnosticsOfCheckedFiles"') + ) + return null; + throw error; + } + } getSyntacticDiagnostics(file?: SemanticDocument): Ts7Diagnostic[] { return this.diagnostics("getSyntacticDiagnostics", file); } diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index bd2a32c37..8adab0488 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -24,6 +24,7 @@ import { isSupportedMapKey, isSupportedMapValue, isSupportedSetElem, + iterableArmsElem, isUnitType, mapOf, NULL_T, @@ -42,7 +43,7 @@ import { UNDEFINED_T, VOID, } from "../ir/ir.js"; -import { BIGINT_T } from "../ir/ir.js"; +import { BIGINT_T, KEY_ORDER_FIELD } from "../ir/ir.js"; import { literalValues } from "./literal-values.js"; import { remapUnionDiscriminant } from "./union-discriminants.js"; @@ -554,6 +555,21 @@ export class ShapeRegistry { return missing.length === 0 ? order : [...order, ...missing]; } + /** Gives a shape with orderVariants its KEY_ORDER_FIELD, in place, + * after the bodies lowered (key-order-witness.ts adds the field to each + * of the shape's literals). Allocation zeroes it, which every other + * construction keeps. */ + addKeyOrderField(shapeId: string): void { + const shape = this.byId.get(shapeId); + if (!shape || shape.fields.some((f) => f.name === KEY_ORDER_FIELD)) return; + shape.fields = [...shape.fields, { name: KEY_ORDER_FIELD, type: F64 }].sort((a, b) => + a.name < b.name ? -1 : a.name > b.name ? 1 : 0, + ); + this.revision++; + const key = this.keyOf(shape.fields, false, shape.indexValue); + if (!this.byKey.has(key)) this.byKey.set(key, shapeId); + } + /** Adds optional fields to an interned shape in place: members a type * assertion attaches to the shape's values (`r as R & { tag: string }` * followed by `view.tag = ...`; see assertion-members.ts). Every value @@ -1844,10 +1860,16 @@ 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(); + const isIntrinsicGlobal = (symbol: ts.Symbol | undefined): boolean => + symbol !== undefined && + ts.symbolName(symbol) === "globalThis" && + symbol.declarations.length === 0; + if (isIntrinsicGlobal(intrinsic)) return DYN; + // `typeof globalThis & { key?: T }` (the global-singleton spelling) is the + // same global object with declared extra members. if ( - intrinsic !== undefined && - ts.symbolName(intrinsic) === "globalThis" && - intrinsic.declarations.length === 0 + type.isIntersectionType() && + ts.constituentTypes(type).some((part) => isIntrinsicGlobal(part.getSymbol())) ) return DYN; if (ctx.jsDynamicDeclaration !== undefined && intrinsic !== undefined) { @@ -2361,6 +2383,27 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const args = checker.getTypeArguments(ref); if (args.length === 0) return arrayOf(unitOnlyUnion(unions)); if (tupleShape?.elementFlags === undefined) return null; + // A leading-required tuple ending in a rest element whose positions + // all share one type (`[string, ...string[]]`, the non-empty list) + // is an array of that type: every read, write and method behaves as + // the array's, and tsc already proves the required positions exist. + const flags = tupleShape.elementFlags; + if ( + flags.length >= 2 && + (flags[flags.length - 1]! & ts.ElementFlags.Rest) !== 0 && + flags.slice(0, -1).every((f) => (f & ts.ElementFlags.Required) !== 0) + ) { + const elems = args.map((a) => mapType(a, ctx)); + const first = elems[0]; + if ( + first && + first.kind !== "void" && + isSupportedArrayElem(first) && + elems.every((e) => e !== null && typeEquals(e, first)) + ) + return arrayOf(first); + return null; + } // Optional/rest elements have no fixed native tuple layout. if (tupleShape.elementFlags.some((f) => !(f & ts.ElementFlags.Required))) { return null; @@ -3155,6 +3198,8 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // Native WeakMap keeps reference identities in checked-dynamic storage. // The runtime rejects reference kinds without a weak lifetime contract. if (isStdlibInterface("WeakMap") || isStdlibInterface("WeakSet")) return DYN; + // FinalizationRegistry is a native handle with register and unregister. + if (isStdlibInterface("FinalizationRegistry")) return DYN; // Set: Map's sibling — same provenance rule and key domain. // Anything else stays unmapped; // the `new Set` lowering names the offending element type specifically. @@ -3935,6 +3980,14 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { ...channels, }; } + // Iterable: the iterable union (iterableUnionType) — the source value + // itself in its own arm, so an array stays the same multi-pass object and + // a generator stays single-pass. + if (isStdlibInterface("Iterable")) { + const arg = checker.getTypeArguments(widened as ts.TypeReference)[0]; + const elem = arg ? mapType(arg, ctx) : null; + return elem ? iterableUnionType(elem, ctx.shapes, unions) : null; + } // IteratorResult — the checker's type of `g.next()` (an // alias for IteratorYieldResult | IteratorReturnResult): // maps to the SAME interned record genResume answers, so `const r = @@ -4989,7 +5042,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { a.kind === "union" || a.kind === "date" || a.kind === "dyn" || - a.kind === "generator", + // A generator arm only comes from an Iterable (an optional + // one keeps its unit arms beside the iterable union's). + (a.kind === "generator" && iterableArmsElem(arms) === null), ) || !maybePromiseUnionOk(parts, arms, ctx) ) { @@ -5844,6 +5899,60 @@ function constAssertedEmptyArrayProp(p: ts.Symbol): boolean { return underConstAssertion(d); } +/** A TypeScript property assignment `name: []` whose type is `never[]`. */ +function emptyNeverArrayProp(p: ts.Symbol, fieldTs: ts.Type, checker: ts.TypeChecker): boolean { + const d = p.valueDeclaration; + if (d === undefined || !ts.isPropertyAssignment(d)) return false; + if (isJsSourceFile(ts.getSourceFileOfNode(d))) return false; + if (!ts.isArrayLiteralExpression(d.initializer) || d.initializer.elements.nodes.length > 0) + return false; + return ( + checker.isArrayType(fieldTs) && + (checker.getTypeArguments(fieldTs as ts.TypeReference)[0]!.flags & ts.TypeFlags.Never) !== 0 + ); +} + +/** A JavaScript object literal's property typed `never[]` (`{ issues: [] }`). + * Nothing checks a JavaScript file's later writes, which can push any value + * onto it (`ctx.common.issues.push(issue)`), so it holds the checked-dynamic + * array rather than never's f64 element array. */ +function jsNeverArrayProp(p: ts.Symbol, fieldTs: ts.Type, checker: ts.TypeChecker): boolean { + return ( + jsObjectLiteralProperty(p) && + checker.isArrayType(fieldTs) && + (checker.getTypeArguments(fieldTs as ts.TypeReference)[0]!.flags & ts.TypeFlags.Never) !== 0 + ); +} + +/** A JavaScript object literal's property (`{ lastComponent: undefined }`, + * `{ user: null }`). Nothing checks a JavaScript file's later writes, which + * can store any value in it (`path.lastComponent = { ... }`), so a + * property whose only initializer is a unit holds the checked-dynamic value + * rather than the unit-only union. */ +function isUnitOnlyIr(t: IrType, ctx: TypeMapperCtx): boolean { + if (t.kind !== "union") return false; + const def = ctx.unions.get(t.unionId); + return def !== undefined && def.arms.every((arm) => isUnitType(arm)); +} + +/** A property a JavaScript object literal declares. The checker also adds + * each missing property, optional and `undefined`, to every literal of a + * union of literals (`{ _tag, value } | { _tag, error }` gains + * `error?: undefined` and `value?: undefined`); those keep their unit-only + * type, so a literal still converts to the one arm whose properties it + * has. */ +function jsObjectLiteralProperty(p: ts.Symbol): boolean { + const d = p.valueDeclaration; + // `__proto__: null` sets the literal's prototype; it declares no property. + return ( + d !== undefined && + (ts.isPropertyAssignment(d) || ts.isShorthandPropertyAssignment(d)) && + ts.symbolName(p) !== "__proto__" && + (p.flags & ts.SymbolFlags.Optional) === 0 && + isJsSourceFile(ts.getSourceFileOfNode(d)) + ); +} + function mapGenericAwaitedAlias(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const { resolveTypeParam } = ctx; if (!resolveTypeParam) return null; @@ -6397,6 +6506,46 @@ export function unitOnlyUnion(unions: UnionRegistry): IrType { return { kind: "union", unionId: unions.intern(arms) }; } +/** `Iterable`: a union whose arms are the sources that keep their own + * representation — `T[]`, a `Set`, a `Map` (T the `[K, V]` + * tuple), a string (T string) — and a generator over T, which also holds + * every other iterator (Map/Set and array iterators adapt into one). The + * generator arm marks the union as iterable (iterableArmsElem): no other + * union has one. Null when T has no array or generator form. */ +export function iterableUnionType( + elem: IrType, + shapes: ShapeRegistry, + unions: UnionRegistry, +): IrType | null { + if (elem.kind === "void" || elem.kind === "generator" || !isSupportedArrayElem(elem)) return null; + if (!genResultRecord(elem, VOID, shapes, unions)) return null; + const arms: IrType[] = [ + { kind: "array", elem }, + { kind: "generator", yieldT: elem, retT: VOID, nextT: UNDEFINED_T }, + ]; + const elemArms = elem.kind === "union" ? unions.get(elem.unionId)?.arms : undefined; + if (isSupportedSetElem(elem, elemArms)) arms.push({ kind: "set", elem }); + if (elem.kind === "string") arms.push(STRING); + if (elem.kind === "record") { + const shape = shapes.get(elem.shapeId); + const [k, v] = shape?.fields ?? []; + if ( + shape?.tuple && + shape.fields.length === 2 && + k?.name === "0" && + v?.name === "1" && + isSupportedMapKey( + k.type, + k.type.kind === "union" ? unions.get(k.type.unionId)?.arms : undefined, + ) && + isSupportedMapValue(v.type) + ) + arms.push({ kind: "map", key: k.type, value: v.type }); + } + arms.sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1)); + return { kind: "union", unionId: unions.intern(arms) }; +} + /** The IteratorResult record of a generator's value channels: `{ done: * boolean, value: V }` where V is dyn (an any/unknown channel) or the * canonical union of the yield arms, the return arms (when the return @@ -7592,6 +7741,22 @@ function wideningAddedProperties( return added; } +/** Whether `type` is an interface one of whose `extends` clauses names a + * type the checker answers `any` (or an error type) for. */ +function interfaceExtendsAny(type: ts.Type, checker: ts.TypeChecker): boolean { + const symbol = type.getSymbol(); + if (symbol === undefined || (symbol.flags & ts.SymbolFlags.Interface) === 0) return false; + for (const decl of symbol.declarations) { + if (!ts.isInterfaceDeclaration(decl)) continue; + for (const clause of decl.heritageClauses?.nodes ?? []) { + for (const base of clause.types.nodes) { + if ((checker.getTypeAtLocation(base).flags & ts.TypeFlags.Any) !== 0) return true; + } + } + } + return false; +} + function mapRecordTypeInner( widened: ts.Type, ctx: TypeMapperCtx, @@ -7607,6 +7772,13 @@ function mapRecordTypeInner( // names the construct. const widenedSym = widened.getSymbol(); if (widenedSym !== undefined && (widenedSym.flags & ts.SymbolFlags.Enum) !== 0) return null; + // An interface extending a type the checker answers `any` for (in the + // compiled world, a package type computed from members only its .d.ts + // declares, such as `interface P extends z.infer`) has members + // the checker cannot list: its own declared fields are not the whole + // object. The value is checked-dynamic, never a record of the declared + // fields alone. + if (interfaceExtendsAny(widened, checker)) return DYN; // An index signature maps to a hybrid shape: declared fields keep // struct slots, undeclared keys ride the shape's overflow map, valued // uniformly by the signature's value type (`unknown` → dyn — a model-pricing table's @@ -7880,6 +8052,14 @@ function mapRecordTypeInner( ) { pt = arrayOf(unitOnlyUnion(ctx.unions)); } + // A TypeScript property initialized with an empty literal and typed + // `never[]` (`const out = { files: [] }`) holds no element, so it maps + // like the empty tuple: the unit-only element array, which a + // destination's typed array field (`files: string[]`) receives through + // the checked empty-array lift. never's f64 element would make it a + // `number[]` field. + if (pt?.kind === "array" && emptyNeverArrayProp(p, fieldTs, checker)) + pt = arrayOf(unitOnlyUnion(ctx.unions)); if (pt === null && (fieldTs.flags & ts.TypeFlags.Any) !== 0) pt = DYN; // An `any[]` field (`list: any[]` in an SDK's message type, also // optional or nullable) is the checked-dynamic array, exactly as @@ -7903,6 +8083,8 @@ function mapRecordTypeInner( // omits the undefined arm exactly like an omitted optional. if (pt?.kind === "void" && isUnitOnlyTsType(fieldTs, ctx.resolveTypeParam)) pt = unitOnlyUnion(ctx.unions); + if (pt !== null && isUnitOnlyIr(pt, ctx) && jsObjectLiteralProperty(p)) pt = DYN; + if (pt?.kind === "array" && jsNeverArrayProp(p, fieldTs, checker)) pt = DYN; // dyn FIELDS map now (`{ v: unknown }`, `[string, unknown]` tuples): // the slot carries a dyn value exactly like an `unknown`-valued // overflow entry — same RC adapters, same dynFrom conversion on the diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index f0f19a408..586561632 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -9,6 +9,7 @@ import { } from "./library/prepare.js"; import { analyzeWithFrontend } from "./frontend/analysis.js"; import { checkerCacheIdentity } from "./frontend/ts7/checker-backend.js"; +import { setLibDiagnosticsStore } from "./frontend/ts7/lib-diagnostics.js"; import { analyzeWithVerdictCache, fileVerdictStore } from "./coverage/verdict-cache.js"; import { coverageCompilerIdentity } from "./coverage/verdict-startup.js"; import { commentOnlyReplay } from "./coverage/verdict-semantic.js"; @@ -126,6 +127,7 @@ import { moduleRuntimeFeatures } from "./ir/runtime-features.js"; import { serializeModuleParts } from "./ir/serialize.js"; import { readTextParts, writeTextParts } from "./text-parts.js"; import { validateModule } from "./ir/validate.js"; +import { compilerOwnedFileRoots } from "./frontend/dts-paths.js"; import { loadProgram } from "./frontend/program-node.js"; import { runFrontend, type FrontendFactory } from "./frontend/pipeline.js"; import { @@ -600,6 +602,7 @@ async function compileExecutableNative( ...(features.dynTier ? { dynTier: true } : {}), ...(features.streamDyn ? { streamDyn: true } : {}), ...(features.handleProps ? { handleProps: true } : {}), + ...(features.declAdopt ? { declAdopt: true } : {}), ...(features.dynForAwait ? { dynForAwait: true } : {}), ...(features.eventsOnceSignal ? { eventsOnceSignal: true } : {}), ...(features.dynBuffer ? { dynBuffer: true } : {}), @@ -785,7 +788,7 @@ async function prepareExecutableInput( if (outputKind === "ir") { await mkdir(dirname(opts.outPath), { recursive: true }); - await writeTextParts(opts.outPath, serializeModuleParts(mod)); + await writeTextParts(opts.outPath, serializeModuleParts(mod, compilerOwnedFileRoots())); return { ok: true, artifact: { kind: "ir", path: opts.outPath } }; } @@ -885,7 +888,7 @@ async function prepareExecutableInput( if (opts.emitIr) { irPath = defaultSourcePaths.ir; irSourcePath = privateSiblingPath(irPath, "ir"); - await writeTextParts(irSourcePath, serializeModuleParts(mod)); + await writeTextParts(irSourcePath, serializeModuleParts(mod, compilerOwnedFileRoots())); } const nativeFeatures = executableNativeFeatures(mod, backend, opts.optimization ?? "release"); @@ -1159,9 +1162,28 @@ async function prepareExecutableInputInChild( * `requestPath` as `compileTracked` would in-process, and writes what it prepared, without the LLVM * text, to `resultPath`. */ +/** Has the build reuse the diagnostics of the declaration files scriptc + * supplies (lib-diagnostics.ts) through the build cache, when there is one and + * the checker is tsc-ts. */ +async function configureLibDiagnostics(): Promise { + let checker: string | undefined; + try { + checker = checkerCacheIdentity(); + } catch { + // A missing checker fails where the build starts it. + checker = undefined; + } + const root = checker === undefined ? null : await prepareBuildCacheRoot(buildCacheRoot()); + setLibDiagnosticsStore( + root === null ? null : fileVerdictStore(join(root, "lib-diagnostics-v1")), + checker ?? "", + ); +} + export async function runFrontendProcess(requestPath: string, resultPath: string): Promise { const request = JSON.parse(await readFile(requestPath, "utf8")) as FrontendProcessRequest; clearCompileSessionCaches(); + await configureLibDiagnostics(); setProvenanceSources(request.provenance); const frontendInputs = new FrontendInputTracker(); const warnings: CompileWarningSink = { warnings: [], sourceTexts: null }; @@ -1224,6 +1246,7 @@ async function compileTracked( const timing = compilationTiming(); const timingStart = Date.now(); entryPath = resolve(entryPath); + await configureLibDiagnostics(); const outputKind = opts.outputKind ?? "exe"; if ( (opts.backend !== undefined && opts.backend !== "llvm") || @@ -1986,7 +2009,7 @@ async function emitSemanticLibraryHit( let irPath: string | undefined; if (opts.emitIr) { irPath = join(opts.outDir, `${stem}.lib.ir.json`); - await writeTextParts(irPath, serializeModuleParts(mod)); + await writeTextParts(irPath, serializeModuleParts(mod, compilerOwnedFileRoots())); } await compileLibraryNative(profile, llvmPath, archivePath, opts.sanitize ?? false, hit.native); timing("native-archive"); @@ -2272,7 +2295,7 @@ async function compileLibraryTracked( let irPath: string | undefined; if (opts.emitIr) { irPath = join(opts.outDir, `${stem}.lib.ir.json`); - await writeTextParts(irPath, serializeModuleParts(mod)); + await writeTextParts(irPath, serializeModuleParts(mod, compilerOwnedFileRoots())); } const nativeFeatures = libraryNativeFeatures(mod, profile.emission); diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 16db45b6c..0239c1624 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -16,6 +16,12 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ // Node's prime, generator, size, encoding and group-name errors. "crypto.dhNew", "crypto.dhGroupNew", + // Node's candidate, size, options, add and rem errors, and OpenSSL's + // generation errors. + "crypto.checkPrimeSync", + "crypto.checkPrimeCb", + "crypto.generatePrimeSync", + "crypto.generatePrimeCb", // node:test's { timeout } and { concurrency } range checks. "test.register", "test.registerEmpty", @@ -88,6 +94,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "stream.unpipeDyn", "weakMap.new", "weakSet.new", + // Node's "cleanup must be callable" TypeError. + "finalizationRegistry.new", "dyn.fromEntries", "bytes.constructor", "bytes.instanceOf", @@ -340,7 +348,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "sp.finished", "sp.pipeline", "sp.pipelineStages", + "sp.pipelineStagesOpts", "stream.pipelineStages", + "stream.composeStages", "stream.pipelineStagesDyn", "sc.text", "sc.json", @@ -379,6 +389,14 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "writable.writeEnc", "writable.end", "writable.uncork", + "writable.setDefaultEncoding", + "stream.duplexFromGen", + "stream.duplexFromDyn", + "stream.duplexFromPromise", + "stream.duplexFromAsyncFn", + "stream.duplexFromGenSignal", + "stream.duplexFromAsyncFnSignal", + "fetch.readableFromWeb", "stream.destroy", "stream.destroyErr", "stream.iteratorClose", @@ -428,6 +446,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "global.undefRead", // The compiler-resolved Node-parity throw: always throws, catchably. "error.nodeThrow", + // An accessor over an error's own name or message runs user code. + "error.accessorGet", + "error.accessorSet", // USVString coercion runs user toString/valueOf — throws propagate. "dyn.toStringCoerce", "dyn.stringConstructor", @@ -513,6 +534,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "readable.opMap", "readable.opFilter", "readable.opFlatMap", + "readable.opFlatMapAsyncGen", + "readable.opFlatMapSyncGen", "readable.opDrop", "readable.opTake", "rl.createSignal", @@ -590,6 +613,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ // fence; the zero-argument form always throws ERR_MISSING_ARGS. The // DOMException clone shares the option validation. "dyn.structuredClone", + "dyn.declAdopt", "dyn.cloneMissing", "dyn.cloneTransferFail", "dyn.cloneTransferFailWith", @@ -612,6 +636,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "dyn.objCreate", "dyn.objCreateWithProperties", "dyn.getPrototype", + "dyn.getPrototypeForIdentity", "dyn.setPrototype", // Error.captureStackTrace: Node's TypeError for a primitive target, and // the error string conversion of name and message @@ -651,6 +676,8 @@ 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", + // ToNumber of each element can run valueOf + "dyn.mathFold", "dyn.destructureRest", "dyn.arrayFromIterator", "dyn.iterator", @@ -1001,3 +1028,25 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "crypto.createSign", "crypto.createVerify", ]); + +/** libCall members that always throw. Their result type is the replaced + * expression's own and is never materialized: global.undefRead's is the + * declared type of the undefined global; the others stand in for a value + * of the expression they replace. The fs Chk ladders qualify: every + * validation failure throws Node's typed error, and a full pass throws the + * trailing compiler-rendered fence. */ +export const ALWAYS_THROW_LIB_FNS: ReadonlySet = new Set([ + "global.undefRead", + "error.nodeThrow", + "error.argTypeThrow", + "error.propTypeThrow", + "fs.mkdtempChk", + "fs.readFileChk", + "fs.opendirChk", + "fs.watchFileChk", + "fs.lchmodChk", + "fs.readChk", + "fs.streamOptsChk", + "net.connectOptsChk", + "tls.caCertsChk", +]); diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index fcdbe8694..6a645785f 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -170,6 +170,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "fetch.streamFrom": { argTypes: [null], result: DYN }, // The chunk/result record depends on ReadableStream; validated by validate.ts. "fetch.readerRead": { argTypes: [DYN], result: VOID }, + /** Readable.fromWeb (scr_stream_from_web.c): the stream, objectMode, + * highWaterMark (< 0: the mode's default) and encoding ("": none). */ + "fetch.readableFromWeb": { argTypes: [DYN, BOOL, F64, STRING], result: VOID }, "json.parse": { argTypes: [STRING], result: DYN }, "json.parseReviver": { argTypes: [STRING, DYN], result: DYN }, "json.stringifyReplacer": { argTypes: [DYN, DYN, STRING], result: DYN }, @@ -216,6 +219,7 @@ 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.mathFold": { argTypes: [DYN, BOOL, STRING], result: F64 }, "dyn.destructureRest": { argTypes: [DYN, STRING], result: DYN }, "dyn.arrayFromIterator": { argTypes: [DYN], result: DYN }, "dyn.iterator": { argTypes: [DYN, STRING], result: DYN }, @@ -1318,6 +1322,12 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ argTypes: [STRING, arrayOf(STRING), F64, STRING, F64, F64, F64], result: SPAWNRES_T, }, + /** cp.spawnSyncOpts with the `env` option: the trailing [k,v,...] + * pairs replace the child's environment (recordToEnvPairs). */ + "cp.spawnSyncEnv": { + argTypes: [STRING, arrayOf(STRING), F64, STRING, F64, F64, F64, arrayOf(STRING)], + result: SPAWNRES_T, + }, /** cp.spawnSyncOpts with the stdio carried as a RUNTIME string — * "pipe" | "ignore" | "inherit", proven by the call site's TYPE (the * defaultRunner idiom `stdio: options?.stdio ?? "pipe"`); the runtime @@ -1528,6 +1538,13 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ }, "dgram.sendStr": { argTypes: [DGRAMSOCK_T, STRING, F64, STRING], result: VOID }, "dgram.sendBytes": { argTypes: [DGRAMSOCK_T, BYTES_U8, F64, STRING], result: VOID }, + /** Connected AF_UNIX datagram sockets for the hot-shots package shim + * (the unix-dgram addon's surface): fd or result >= 0, -errno on + * failure; send answers 1 when the socket would block. */ + "dgram.unixSocket": { argTypes: [], result: F64 }, + "dgram.unixConnect": { argTypes: [F64, STRING], result: F64 }, + "dgram.unixSend": { argTypes: [F64, BYTES_U8], result: F64 }, + "dgram.unixClose": { argTypes: [F64], result: F64 }, /** The send argument-validation ladder over dyn arguments (Node's * signature shuffle: slice bounds, list/type contracts, port/address * validation, connected-state errors), then the send: lists, connected @@ -2820,6 +2837,16 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ /** getDiffieHellman(groupName) and createDiffieHellmanGroup(name): a * native DiffieHellmanGroup handle. */ "crypto.dhGroupNew": { argTypes: [DYN], result: DYN }, + /** checkPrimeSync(candidate, options) and generatePrimeSync(size, + * options) over checked-dynamic arguments (scr_dh.c): a boolean, and an + * ArrayBuffer or bigint, as checked-dynamic results. The Cb forms take + * the callback and whether its error parameter takes null for Node's + * undefined, and call it in a later macrotask; Node's argument errors + * throw (may-throw). */ + "crypto.checkPrimeSync": { argTypes: [DYN, DYN], result: DYN }, + "crypto.checkPrimeCb": { argTypes: [DYN, DYN, DYN, BOOL], result: VOID }, + "crypto.generatePrimeSync": { argTypes: [DYN, DYN], result: DYN }, + "crypto.generatePrimeCb": { argTypes: [DYN, DYN, DYN, BOOL], result: VOID }, "crypto.hmacNewStr": { argTypes: [STRING, STRING], result: CRYPTOHMAC_T }, "crypto.hmacNewBytes": { argTypes: [STRING, BYTES_U8], result: CRYPTOHMAC_T }, "crypto.hashUpdateStr": { argTypes: [CRYPTOHASH_T, STRING], result: CRYPTOHASH_T }, @@ -3402,6 +3429,14 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * throw-site fields, dynamic writes) or an Object.prototype member. */ "error.hasKey": { argTypes: [null, STRING], result: BOOL }, "error.hasCode": { argTypes: [null], result: BOOL }, + /** Whether the error's own `name` or `message` (the key) is an accessor + * a reflective definition installed. Borrows both; never throws. */ + "error.ownAccessor": { argTypes: [null, STRING], result: BOOL }, + /** That accessor's getter called on the error (owned result), and its + * setter called with the value (V8's TypeError without one). Borrow + * their arguments; throw what the accessor throws. */ + "error.accessorGet": { argTypes: [null, STRING], result: DYN }, + "error.accessorSet": { argTypes: [null, STRING, DYN], result: VOID }, "error.defineCause": { argTypes: [null, DYN], result: VOID }, "error.deleteCause": { argTypes: [null], result: VOID }, /** The compiler-resolved Node-parity throw for always-throwing lowered @@ -3591,9 +3626,11 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "weakSet.is": { argTypes: [DYN], result: BOOL }, "weakMap.new": { argTypes: [DYN], result: DYN }, "weakSet.new": { argTypes: [DYN], result: DYN }, + "finalizationRegistry.new": { argTypes: [DYN], result: DYN }, + "finalizationRegistry.is": { argTypes: [DYN], result: BOOL }, "dyn.fromEntries": { argTypes: [DYN], result: DYN }, "bytes.constructor": { argTypes: [STRING], result: DYN }, - "bytes.instanceOf": { argTypes: [DYN, DYN], result: BOOL }, + "bytes.instanceOf": { argTypes: [DYN, DYN, STRING], result: BOOL }, "bytes.construct": { argTypes: [DYN, DYN, STRING], result: DYN }, /** The ambient receiver — JS `this` in a plain (non-method) function * body: the innermost binding the current firing/dispatch window @@ -3716,6 +3753,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "dyn.assignPrototype": { argTypes: [DYN, DYN, DYN], result: DYN }, "dyn.definePrototypeProps": { argTypes: [DYN, DYN, DYN], result: DYN }, "dyn.bagGet": { argTypes: [DYN, STRING, DYN], result: DYN }, + /** A class field's creation in its instance's property bag: adds the + * string key as a default data property when absent, answering false + * (nothing changed) when the bag needs the general defineProperty path. */ + "dyn.bagCreateField": { argTypes: [DYN, STRING], result: BOOL }, "dyn.bagSet": { argTypes: [DYN, STRING, DYN, DYN], result: VOID }, "dyn.objCreate": { argTypes: [DYN], result: DYN }, // Names a compiled class's prototype object (args[0]) after the class @@ -3723,6 +3764,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "dyn.nameClassPrototype": { argTypes: [DYN, STRING], result: VOID }, "dyn.objCreateWithProperties": { argTypes: [DYN, DYN], result: DYN }, "dyn.getPrototype": { argTypes: [DYN], result: DYN }, + // dyn.getPrototype for walks that compare each prototype with a program + // class's prototype: native builtin values end the walk (null). + "dyn.getPrototypeForIdentity": { argTypes: [DYN], result: DYN }, "dyn.setPrototype": { argTypes: [DYN, DYN], result: DYN }, "dyn.getOwnPropertyNames": { argTypes: [DYN], result: DYN }, "dyn.ownKeys": { argTypes: [DYN], result: DYN }, @@ -3748,6 +3792,14 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * call: always throws Node's TypeError [ERR_MISSING_ARGS] with Node's * own (verbatim, doubly-wrapped) message. */ "dyn.structuredClone": { argTypes: [DYN, DYN], result: DYN }, + /** The declaration-typed boundary's adoption (decl-projection.ts): + * `source` is the dynamic value a boundary cast just converted, and + * `capsule` a live capsule of the native value built from it. Every + * object or array of `source` that something else still references + * becomes, in place, a live capsule of the native value built from it. + * Throws the path TypeError, changing nothing, when a converted object + * is not plain data (a prototype, accessors, symbols, frozen state). */ + "dyn.declAdopt": { argTypes: [DYN, DYN], result: VOID }, "dyn.cloneMissing": { argTypes: [], result: DYN }, /** structuredClone with a NON-EMPTY transfer array of static values: * nothing static is transferable, so the call always throws Node's @@ -3785,6 +3837,13 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ // Whether the array (args[0]) still has the key (args[1]) from its // namedKeys list. Borrowed; never throws. "array.hasNamedKey": { argTypes: [null, STRING], result: BOOL }, + // Whether an array (args[0], any element type) has properties besides its + // elements: noncanonical numeric keys, match metadata or named + // properties. Borrowed; never throws. + "array.hasProperties": { argTypes: [null], result: BOOL }, + // The key of an element access whose index is undefined (the property + // named "undefined"; frontend/lowering/undefined-index.ts). + "array.undefinedKey": { argTypes: [], result: F64 }, // 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 @@ -3930,6 +3989,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "stream.setDestroy": { argTypes: [null, null], result: VOID }, "stream.setTransform": { argTypes: [null, null], result: VOID }, "stream.setFlush": { argTypes: [null, null], result: VOID }, + "stream.setConstruct": { argTypes: [null, null], result: VOID }, + "stream.setWritev": { argTypes: [null, null], result: VOID }, + "stream.setWritevDefault": { argTypes: [null, null], result: VOID }, /** The dyn-options twins (a checked-dynamic options record — the JS * lane's `super(options)` forwarding and `new Readable(dynVar)`): the * option walk runs at RUNTIME with Node's reading rules. newDyn: @@ -3956,6 +4018,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "stream.pipeline": { argTypes: [F64], result: VOID }, "stream.pipelineDyn": { argTypes: [F64], result: VOID }, "stream.pipelineStages": { argTypes: [F64, F64, F64, F64, F64], result: VOID }, + "stream.composeStages": { argTypes: [F64, F64, F64, F64, F64], result: VOID }, "stream.pipelineStagesDyn": { argTypes: [F64, F64, F64, F64, F64], result: VOID }, /** stream.compose(count, s1..sn): the Duplex over a pipeline of the * streams (+1). MAY THROW (ERR_INVALID_ARG_VALUE for a stream that is @@ -4000,6 +4063,12 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ argTypes: [F64, F64, F64, F64, F64], result: { kind: "promise", inner: VOID }, }, + /** sp.pipelineStages with the options argument: the same head, then + * end (a bool) and, with flags bit 6, the options' signal (dyn). */ + "sp.pipelineStagesOpts": { + argTypes: [F64, F64, F64, F64, F64], + result: { kind: "promise", inner: VOID }, + }, /** node:stream/consumers — the promise consumers over the readable * machinery: (s) → a pending promise settled at the terminal point * with the accumulated result (sc.text: the utf8 decode, sc.json: the @@ -4056,6 +4125,28 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ /** The objectMode option's sides (readable, writable), applied right * after construction; answers the receiver (+1, the chaining shape). */ "stream.objectMode": { argTypes: [null, BOOL, BOOL], result: VOID }, + "stream.decodeStrings": { argTypes: [null, BOOL], result: VOID }, + "stream.duplexFromGen": { argTypes: [null, null, null, null, null, null], result: VOID }, + "stream.duplexFromDyn": { argTypes: [DYN], result: VOID }, + /** Duplex.from over a lone Readable or Writable, or a { readable, + * writable } pair of streams (duplexify.js's _duplexify). */ + "stream.duplexifyReadable": { argTypes: [null], result: VOID }, + "stream.duplexifyWritable": { argTypes: [null], result: VOID }, + "stream.duplexifyPair": { argTypes: [null, null], result: VOID }, + /** Duplex.from(promise): the promise and its box helper. */ + "stream.duplexFromPromise": { argTypes: [null, null], result: VOID }, + /** Duplex.from(asyncFunction): the stage, its source helper and the + * promise's box helper. */ + "stream.duplexFromAsyncFn": { argTypes: [null, null, null], result: VOID }, + /** The same for a body that declares the options argument: then the + * AbortController's abort() and its signal. */ + "stream.duplexFromGenSignal": { + argTypes: [null, null, null, null, null, null, null, DYN], + result: VOID, + }, + "stream.duplexFromAsyncFnSignal": { argTypes: [null, null, null, null, DYN], result: VOID }, + "writable.writeEncoding": { argTypes: [null, STRING], result: VOID }, + "writable.setDefaultEncoding": { argTypes: [null, DYN], result: VOID }, /** Whether a readable.nextChunkDyn step answered the end (the done * marker; an object-mode stream can yield undefined). */ "stream.chunkDone": { argTypes: [DYN], result: BOOL }, @@ -4091,17 +4182,39 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * forEach nothing; reduce: [stream, fn, init, hasInit, options, kind] * with the payload kind (0 dyn, 1 number, 2 boolean, 3 string). The * promise-returning ones reject instead of throwing. */ - "readable.opMap": { argTypes: [null, DYN, DYN], result: VOID }, - "readable.opFilter": { argTypes: [null, DYN, DYN], result: VOID }, - "readable.opFlatMap": { argTypes: [null, DYN, DYN], result: VOID }, + "readable.opMap": { argTypes: [null, DYN, DYN, DYN], result: VOID }, + "readable.opFilter": { argTypes: [null, DYN, DYN, DYN], result: VOID }, + "readable.opFlatMap": { argTypes: [null, DYN, DYN, DYN], result: VOID }, + /** flatMap whose callback returns a compiled generator: the helpers + * over its type follow (async: next, read, return, throw; sync: next, + * return, throw). */ + "readable.opFlatMapAsyncGen": { + argTypes: [null, DYN, DYN, DYN, null, null, null, null], + result: VOID, + }, + "readable.opFlatMapSyncGen": { argTypes: [null, DYN, DYN, DYN, null, null, null], result: VOID }, "readable.opDrop": { argTypes: [null, DYN, DYN], result: VOID }, "readable.opTake": { argTypes: [null, DYN, DYN], result: VOID }, "readable.opToArray": { argTypes: [null, DYN], result: { kind: "promise", inner: DYN } }, - "readable.opForEach": { argTypes: [null, DYN, DYN], result: { kind: "promise", inner: VOID } }, - "readable.opSome": { argTypes: [null, DYN, DYN], result: { kind: "promise", inner: BOOL } }, - "readable.opEvery": { argTypes: [null, DYN, DYN], result: { kind: "promise", inner: BOOL } }, - "readable.opFind": { argTypes: [null, DYN, DYN], result: { kind: "promise", inner: DYN } }, + "readable.opForEach": { + argTypes: [null, DYN, DYN, DYN], + result: { kind: "promise", inner: VOID }, + }, + "readable.opSome": { argTypes: [null, DYN, DYN, DYN], result: { kind: "promise", inner: BOOL } }, + "readable.opEvery": { argTypes: [null, DYN, DYN, DYN], result: { kind: "promise", inner: BOOL } }, + "readable.opFind": { argTypes: [null, DYN, DYN, DYN], result: { kind: "promise", inner: DYN } }, + /** The map family's last argument is the callback's { signal } (or + * undefined). reduceSignal adds ac.abort() (a closure) and + * { signal: ac.signal } for a reducer that reads them. */ "readable.opReduce": { argTypes: [null, DYN, DYN, BOOL, DYN, F64], result: VOID }, + "readable.opReduceSignal": { + argTypes: [null, DYN, DYN, BOOL, DYN, F64, null, DYN], + result: VOID, + }, + "readable.opSignalList": { argTypes: [DYN], result: DYN }, + "readable.opSignalOpts": { argTypes: [DYN], result: DYN }, + "readable.opSignalOf": { argTypes: [DYN], result: DYN }, + "readable.opOnceOptions": { argTypes: [], result: DYN }, "readable.resume": { argTypes: [null], result: VOID }, "readable.isPaused": { argTypes: [null], result: BOOL }, "readable.pipe": { argTypes: [null, null, BOOL], result: VOID }, @@ -4809,10 +4922,12 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ export type IrLibFn = keyof typeof LIB_FN_SIGS; /** The function-stage runs of a pipeline of `total` stages that a stream - * follows (positions bit j: stage j is a function): each pumps its last - * generator into that stream through one (next, read, return) triple. */ -export function pipelinePumpedRuns(total: number, positions: number): number { - let runs = 0; + * follows (positions bit j: stage j is a function), and an async + * generator destination's run (flags bits 1-2 = 1, pumped into the + * PassThrough pipeline() answers): each pumps its last generator through + * one (next, read, return) triple. */ +export function pipelinePumpedRuns(total: number, positions: number, flags: number): number { + let runs = ((flags >> 1) & 3) === 1 ? 1 : 0; for (let j = 0; j + 1 < total; j++) { if (Math.floor(positions / 2 ** j) % 2 === 1 && Math.floor(positions / 2 ** (j + 1)) % 2 === 0) runs++; diff --git a/packages/compiler/src/ir/char-unit-locals.test.ts b/packages/compiler/src/ir/char-unit-locals.test.ts new file mode 100644 index 000000000..ad16ecebd --- /dev/null +++ b/packages/compiler/src/ir/char-unit-locals.test.ts @@ -0,0 +1,98 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule } from "../index.js"; +import { holdCharLocalsAsUnits } from "./char-unit-locals.js"; +import type { IrFunction, IrModule } from "./ir.js"; +import { everyStmtList } from "./traverse.js"; + +async function lower(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-char-units-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ir.json"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + return deserializeModule(await readFile(outPath, "utf8")); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +const fn = (mod: IrModule, name: string): IrFunction => mod.functions.find((f) => f.name === name)!; + +/** The kinds of the function's string tests and string intrinsics. */ +function reads(f: IrFunction): string[] { + const out: string[] = []; + everyStmtList(f.body, { + stmt: () => true, + expr: (e) => { + if (e.kind === "strEq" || e.kind === "strCmp") out.push(e.kind); + if (e.kind === "strIntrinsic") out.push(e.method); + return true; + }, + }); + return out; +} + +test("a character local used only in one-unit tests holds its code unit", async () => { + const mod = holdCharLocalsAsUnits( + await lower(` +function classify(s: string, i: number): number { + const c = s.charAt(i); + let n = 0; + if (c === " ") n += 1; + if (c !== "\\t") n += 2; + if ("a" <= c && c <= "z") n += 4; + if (c < "0" || c > "9") n += 8; + switch (c) { + case "5": + n += 16; + break; + default: + n += 32; + } + return n; +} +console.log(classify("a b", 1), classify("x", 0), classify("7", 0), classify("5", 0), classify("", 3)); +`), + ); + const classify = fn(mod, "classify"); + expect(reads(classify)).toEqual(["charCodeAt"]); + expect(classify.locals.find((l) => l.name === "c")!.type).toEqual({ kind: "f64" }); +}); + +test("a character local with any other use stays a string", async () => { + const before = await lower(` +function escapes(s: string, i: number): string { + const c = s.charAt(i); + return c === " " ? "space" : c; +} +function rebound(s: string, i: number): boolean { + let c = s.charAt(i); + if (c === " ") c = s.charAt(i + 1); + return c === "x"; +} +function wide(s: string, i: number): boolean { + const c = s.charAt(i); + return c === "ab" || c < "b"; +} +function against(s: string, t: string, i: number): boolean { + const c = s.charAt(i); + return c < t; +} +function captured(s: string, i: number): () => boolean { + const c = s.charAt(i); + return () => c === "y"; +} +console.log(escapes("a b", 1), rebound(" x", 0), wide("ab", 0), against("a", "b", 0), captured("y", 0)()); +`); + const after = holdCharLocalsAsUnits(before); + for (const name of ["escapes", "rebound", "wide", "against", "captured"]) { + expect(fn(before, name)).toBeDefined(); + expect(fn(after, name)).toBe(fn(before, name)); + } +}); diff --git a/packages/compiler/src/ir/char-unit-locals.ts b/packages/compiler/src/ir/char-unit-locals.ts new file mode 100644 index 000000000..beeeffaf3 --- /dev/null +++ b/packages/compiler/src/ir/char-unit-locals.ts @@ -0,0 +1,194 @@ +import { + BOOL, + F64, + type IrExpr, + type IrFunction, + type IrLocal, + type IrModule, + type IrStmt, +} from "./ir.js"; +import { mapExprChildren, mapStmtChildren } from "./traverse.js"; + +type CharAt = IrExpr & { kind: "strIntrinsic" }; +type UnitUse = IrExpr & { kind: "strEq" | "strCmp" }; + +/** Character locals held as their code unit. + * + * `const c = s[i]` (or `s.charAt(i)`) followed only by tests such as + * `c === " "`, `c >= "a" && c <= "z"` or `switch (c) { case "x": ... }` + * against literals of one UTF-16 code unit builds a one-character string + * and compares strings, though every answer follows from the code unit. + * Such a local holds `s.charCodeAt(i)` instead, and each test compares + * numbers: + * - a one-unit string compares as its unit; + * - charAt reads "" for an index out of range, and charCodeAt reads NaN. + * "" equals no one-unit literal and is below each, while NaN equals no + * unit and fails every ordered comparison, so `c < "x"` becomes + * `!(c >= 120)`. + * + * Only immutable unboxed string locals qualify, with one declaration whose + * initializer is a one-argument charAt, and with no other mention of the + * local. */ +export function holdCharLocalsAsUnits(mod: IrModule): IrModule { + let changed = false; + const functions = mod.functions.map((fn) => { + const next = rewriteFunction(fn); + if (next !== fn) changed = true; + return next; + }); + return changed ? { ...mod, functions } : mod; +} + +function rewriteFunction(fn: IrFunction): IrFunction { + const candidates = new Set( + fn.locals.filter((l) => !l.mutable && !l.boxed && l.type.kind === "string").map((l) => l.id), + ); + if (candidates.size === 0) return fn; + // Every mention of a candidate, in any field, against the mentions the + // rewrite understands. + const mentions = new Map(); + const understood = new Map(); + const declarations = new Map(); + const note = (map: Map, id: string): void => { + map.set(id, (map.get(id) ?? 0) + 1); + }; + const count = (value: unknown): void => { + if (typeof value === "string") { + if (candidates.has(value)) note(mentions, value); + } else if (Array.isArray(value)) { + for (const item of value) count(item); + } else if (value !== null && typeof value === "object") { + const node = value as Record; + const id = understoodMention(node); + if (id !== null && candidates.has(id)) { + note(understood, id); + if (node.kind === "varDecl") note(declarations, id); + } + for (const key in node) if (key !== "loc") count(node[key]); + } + }; + count(fn.body); + const held = new Set(); + for (const id of candidates) + if ( + declarations.get(id) === 1 && + (mentions.get(id) ?? 0) > 0 && + mentions.get(id) === understood.get(id) + ) + held.add(id); + if (held.size === 0) return fn; + + const unitRef = (id: string, loc: IrExpr["loc"]): IrExpr => ({ + kind: "varRef", + localId: id, + type: F64, + loc, + }); + const rewriteExpr = (e: IrExpr): IrExpr => { + if (e.kind === "strEq" || e.kind === "strCmp") { + const use = unitUse(e); + if (use && held.has(use.id)) { + const code: IrExpr = { kind: "numLit", value: use.code, type: F64, loc: e.loc }; + const c = unitRef(use.id, e.loc); + if (e.kind === "strEq") + return { + kind: "bin", + op: e.negated ? "!==" : "===", + left: c, + right: code, + type: BOOL, + loc: e.loc, + }; + // The comparison with the local on the left. + const op = use.left + ? e.op + : ({ "<": ">", "<=": ">=", ">": "<", ">=": "<=" } as const)[e.op]; + if (op === ">" || op === ">=") + return { kind: "bin", op, left: c, right: code, type: BOOL, loc: e.loc }; + const negation: IrExpr = { + kind: "bin", + op: op === "<" ? ">=" : ">", + left: c, + right: code, + type: BOOL, + loc: e.loc, + }; + return { kind: "unary", op: "!", operand: negation, type: BOOL, loc: e.loc }; + } + } + return mapExprChildren(e, rewriteExpr, rewriteStmt); + }; + const rewriteStmt = (s: IrStmt): IrStmt => { + if (s.kind === "varDecl" && held.has(s.localId) && s.init?.kind === "strIntrinsic") + return { + ...s, + init: { + ...s.init, + method: "charCodeAt", + receiver: rewriteExpr(s.init.receiver), + args: s.init.args.map(rewriteExpr), + type: F64, + }, + }; + if (s.kind === "switch" && s.disc.kind === "varRef" && held.has(s.disc.localId)) + return { + ...s, + disc: unitRef(s.disc.localId, s.disc.loc), + cases: s.cases.map((c) => ({ + ...c, + test: + c.test?.kind === "strLit" + ? { kind: "numLit", value: c.test.value.charCodeAt(0), type: F64, loc: c.test.loc } + : c.test, + body: c.body.map(rewriteStmt), + })), + }; + return mapStmtChildren(s, rewriteExpr, rewriteStmt); + }; + return { + ...fn, + body: fn.body.map(rewriteStmt), + locals: fn.locals.map((l) => (held.has(l.id) ? unitLocal(l) : l)), + }; +} + +/** The local, holding the code unit instead of the one-character string. */ +function unitLocal(local: IrLocal): IrLocal { + return { ...local, type: F64 }; +} + +/** The local a node mentions in a form the rewrite handles: the + * declaration from a one-argument charAt, a one-unit test, or a switch + * over one-unit cases. */ +function understoodMention(node: Record): string | null { + // count() visits every object in the body (types, locations, cases), so + // only the node kinds handled here are read as IR nodes + const kind = node.kind; + if (kind !== "varDecl" && kind !== "strEq" && kind !== "strCmp" && kind !== "switch") return null; + const n = node as unknown as IrExpr | IrStmt; + if (n.kind === "varDecl") return n.init && isCharAt(n.init) ? n.localId : null; + if (n.kind === "strEq" || n.kind === "strCmp") return unitUse(n)?.id ?? null; + if (n.kind === "switch") { + if (n.disc.kind !== "varRef") return null; + if (!n.cases.some((c) => c.test !== null)) return null; + const units = n.cases.every( + (c) => c.test === null || (c.test.kind === "strLit" && c.test.value.length === 1), + ); + return units ? n.disc.localId : null; + } + return null; +} + +function isCharAt(e: IrExpr): e is CharAt { + return e.kind === "strIntrinsic" && e.method === "charAt" && e.args.length === 1; +} + +/** A test between a local and a literal of one code unit. */ +function unitUse(e: UnitUse): { id: string; code: number; left: boolean } | null { + const literalRight = e.right.kind === "strLit"; + const literal = literalRight ? e.right : e.left; + const other = literalRight ? e.left : e.right; + if (literal.kind !== "strLit" || literal.value.length !== 1 || other.kind !== "varRef") + return null; + return { id: other.localId, code: literal.value.charCodeAt(0), left: literalRight }; +} diff --git a/packages/compiler/src/ir/function-references.test.ts b/packages/compiler/src/ir/function-references.test.ts index 7bef40aa6..26539b17a 100644 --- a/packages/compiler/src/ir/function-references.test.ts +++ b/packages/compiler/src/ir/function-references.test.ts @@ -172,6 +172,7 @@ const MODULE_FIELDS: Record>, Role> = { "records[].fields[].name": "other", "records[].declaredOrder[]": "other", "records[].accessorOrder[]": "other", + "records[].orderVariants[][]": "other", "unions[].id": "other", "unions[].discriminant.field": "other", "unions[].discriminant.cases[].values[]": "other", @@ -208,6 +209,7 @@ const MODULE_FIELDS: Record>, Role> = { "contracts.int32Returns[]": "fact", "contracts.int32Locals[]": "other", "contracts.int32Elements[]": "other", + "int32ArrayCandidates[]": "other", "contracts.noThrow[]": "fact", "contracts.acyclic[]": "other", "contracts.immortalGlobals[]": "other", diff --git a/packages/compiler/src/ir/int32-arrays.ts b/packages/compiler/src/ir/int32-arrays.ts new file mode 100644 index 000000000..02de7268f --- /dev/null +++ b/packages/compiler/src/ir/int32-arrays.ts @@ -0,0 +1,515 @@ +import { F64, type IrExpr, type IrFunction, type IrModule, type IrStmt } from "./ir.js"; + +type SwitchCase = Extract["cases"][number]; +import { reparseExpr } from "./serialize.js"; +import { + everyExprChild, + everyStmtChild, + everyStmtList, + transformExpr, + transformStmt, + transformStmtList, + type IrTransform, +} from "./traverse.js"; + +/** Local number arrays the backend may store as int32 slots. + * + * A candidate is a local `number[]` of a synchronous function that no + * other binding can hold and only compiled element operations reach: + * every write of the local stores a fresh array (an array literal, or + * `new Array(n).fill(v)`), and every read of it is the receiver of an + * element read, write, state test, `length`, `push` or `pop` (a discarded + * `pop` included), or the array of an element-reader helper call. The + * array never reaches a runtime function that reads its slots, a library + * call, a closure, a return or untyped code. + * + * Candidates are only proposed here. The int32 slot analysis + * (int32-slots.ts) keeps the ones whose every stored element is a proven + * int32 (never -0), assuming the candidates' own elements are int32s, at + * a fixpoint, and the LLVM backend stores those as SCR_ELEM_I32. It keeps + * the ones whose stores are exact integers but not all proven int32s as + * int32 arrays with checked stores: SCR_ELEM_I32 until a store that is not + * an int32 widens them to doubles in place. Their element-reader helper + * calls are inlined first (rewriteCandidates), so no shared helper ever + * receives an int32 array. */ + +/** Array methods a candidate may be the receiver of: the runtime paths + * behind them read and write int32 slots through the double-valued slot + * accessors. */ +export const INT32_ARRAY_METHODS: ReadonlySet = new Set([ + "length", + "push", + "pop", + "getNumber", +]); + +/** An element-reader helper (`%arr.idxOr.*`): `(a, i) => a's state at i is + * a value ? the element : undefined`, as one returned ternary. */ +export function isElementReader(fn: IrFunction): boolean { + if (fn.params.length !== 2 || fn.body.length !== 1) return false; + const [array, index] = fn.params; + const ret = fn.body[0]!; + if (array!.type.kind !== "array" || array!.type.elem.kind !== "f64") return false; + if (index!.type.kind !== "f64" || ret.kind !== "return" || ret.value?.kind !== "ternary") + return false; + const isParam = (e: IrExpr, id: string): boolean => e.kind === "varRef" && e.localId === id; + const { cond, then, else_ } = ret.value; + return ( + cond.kind === "bin" && + cond.op === "===" && + cond.left.kind === "arrayState" && + isParam(cond.left.arr, array!.localId) && + isParam(cond.left.index, index!.localId) && + cond.right.kind === "numLit" && + cond.right.value === 1 && + then.kind === "unionWrap" && + then.value.kind === "arrayGet" && + isParam(then.value.arr, array!.localId) && + isParam(then.value.index, index!.localId) && + else_.kind === "unionWrap" && + else_.value.kind === "unitLit" + ); +} + +/** `new Array(n).fill(v)` as lowering spells it: `fill` over a sequence + * that declares a temporary empty array, sets its length and yields it. */ +function fillAllocation(e: IrExpr): (IrExpr & { kind: "arrIntrinsic" }) | null { + if (e.kind !== "arrIntrinsic" || e.method !== "fill" || e.receiver.kind !== "seqExpr") + return null; + const seq = e.receiver; + const [decl, length] = seq.stmts; + if ( + seq.stmts.length !== 2 || + decl?.kind !== "varDecl" || + decl.init?.kind !== "arrayLit" || + decl.init.elems.length !== 0 || + length?.kind !== "arraySetLength" || + length.arr.kind !== "varRef" || + length.arr.localId !== decl.localId || + seq.result.kind !== "varRef" || + seq.result.localId !== decl.localId + ) + return null; + return e; +} + +/** Fill-form arrays shorter than this hold doubles even when they are int32 + * arrays with checked stores: their doubles fit a typical 32 KiB L1 data + * cache, so 4-byte slots save no memory traffic. */ +export const CHECKED_INT32_MIN_LENGTH = 4096; + +/** The length `n` of `new Array(n).fill(v)`, or null for any other + * allocation. */ +export function fillAllocationLength(e: IrExpr): IrExpr | null { + const fill = fillAllocation(e); + if (!fill || fill.receiver.kind !== "seqExpr") return null; + const set = fill.receiver.stmts[1]!; + return set.kind === "arraySetLength" ? set.length : null; +} + +/** A number expression whose evaluation has no effect: locals, literals and + * arithmetic. */ +function isPureLength(e: IrExpr): boolean { + if (e.type.kind !== "f64") return false; + switch (e.kind) { + case "numLit": + case "varRef": + return true; + case "bin": + return e.left.type.kind === "f64" && isPureLength(e.left) && isPureLength(e.right); + case "unary": + return isPureLength(e.operand); + default: + return false; + } +} + +/** Whether the candidate `id` of `fn` may hold int32 slots with checked + * stores: every array it is assigned is the fill form, with a length that + * may reach CHECKED_INT32_MIN_LENGTH and evaluates without effects (the + * backend evaluates it once more to pick the new array's kind). */ +export function checkableInt32Allocations(fn: IrFunction, id: string): boolean { + let ok = true; + everyStmtList(fn.body, { + stmt: (s) => { + if ((s.kind === "varDecl" || s.kind === "assign") && s.localId === id) { + const value = s.kind === "varDecl" ? s.init : s.value; + const length = value ? fillAllocationLength(value) : null; + if ( + length === null || + !isPureLength(length) || + (length.kind === "numLit" && length.value < CHECKED_INT32_MIN_LENGTH) + ) + ok = false; + } + return ok; + }, + expr: () => ok, + }); + return ok; +} + +/** A fresh number array: a literal without spreads, or the fill form. */ +export function int32ArrayAllocation(e: IrExpr): boolean { + return (e.kind === "arrayLit" && (e.spreads?.length ?? 0) === 0) || fillAllocation(e) !== null; +} + +/** The array literal a candidate's allocation builds (the literal itself, + * or the fill form's temporary's), and the fill form's temporary. */ +export function allocatedLiteral( + e: IrExpr, +): { literal: IrExpr & { kind: "arrayLit" }; temporary: string | null } | null { + if (e.kind === "arrayLit") return { literal: e, temporary: null }; + const fill = fillAllocation(e); + if (!fill || fill.receiver.kind !== "seqExpr") return null; + const decl = fill.receiver.stmts[0]!; + return decl.kind === "varDecl" && decl.init?.kind === "arrayLit" + ? { literal: decl.init, temporary: decl.localId } + : null; +} + +/** `xs.pop()` whose value is unused, as lowering spells it: a sequence + * that binds the array to a hidden local, then only reads that local's + * length and truncates it with pop stores. Returns the hidden local and the + * array read it binds. */ +function discardedPop(e: IrExpr): { local: string; array: IrExpr & { kind: "varRef" } } | null { + if (e.kind !== "seqExpr") return null; + const [bind, ...rest] = e.stmts; + if (bind?.kind !== "varDecl" || bind.init?.kind !== "varRef") return null; + const local = bind.localId; + const isLocal = (x: IrExpr): boolean => x.kind === "varRef" && x.localId === local; + const expr = (x: IrExpr): boolean => { + if (x.kind === "arrIntrinsic" && x.method === "length" && isLocal(x.receiver)) return true; + return !isLocal(x) && everyExprChild(x, expr, stmt); + }; + const stmt = (x: IrStmt): boolean => { + if (x.kind === "arraySetLength" && x.pop === true && isLocal(x.arr)) return expr(x.length); + return ( + !((x.kind === "varDecl" || x.kind === "assign") && x.localId === local) && + everyStmtChild(x, expr, stmt) + ); + }; + return rest.every(stmt) && expr(e.result) ? { local, array: bind.init } : null; +} + +/** Whether evaluating `e` may assign the local `id`. */ +function writesLocal(e: IrExpr, id: string): boolean { + const expr = (x: IrExpr): boolean => + !(x.kind === "assignExpr" && x.localId === id) && everyExprChild(x, expr, stmt); + const stmt = (x: IrStmt): boolean => + !((x.kind === "varDecl" || x.kind === "assign") && x.localId === id) && + everyStmtChild(x, expr, stmt); + return !expr(e); +} + +/** The candidate locals of `fn` (see the module comment). */ +export function int32ArrayCandidates(fn: IrFunction, readers: ReadonlySet): Set { + const result = new Set(); + if (fn.async || fn.generator) return result; + const captured = new Set( + [...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId), + ); + const params = new Set(fn.params.map((p) => p.localId)); + for (const local of fn.locals) + if ( + local.type.kind === "array" && + local.type.elem.kind === "f64" && + !local.boxed && + !local.tdz && + !captured.has(local.id) && + !params.has(local.id) + ) + result.add(local.id); + if (result.size === 0) return result; + const written = new Set(); + // A receiver position: the candidate is read there without escaping. + const receiver = (e: IrExpr): void => { + if (e.kind !== "varRef") expr(e); + }; + const expr = (e: IrExpr): boolean => { + switch (e.kind) { + case "varRef": + result.delete(e.localId); + return true; + case "assignExpr": + result.delete(e.localId); + break; + case "arrayGet": + case "arrayState": + case "arrayHas": + receiver(e.arr); + expr(e.index); + return true; + case "arrIntrinsic": + if (!INT32_ARRAY_METHODS.has(e.method)) break; + receiver(e.receiver); + e.args.forEach(expr); + return true; + case "seqExpr": { + // The pop's hidden local is the array itself; every use of it is a + // length read or a pop store. + const pop = discardedPop(e); + if (!pop) break; + e.stmts.slice(1).every(stmt); + expr(e.result); + return true; + } + case "call": { + if (!readers.has(e.callee) || e.args.length !== 2) break; + const [array, index] = e.args; + // Inlining evaluates the index before the array read. + if (array!.kind === "varRef" && writesLocal(index!, array!.localId)) + result.delete(array!.localId); + receiver(array!); + expr(index!); + return true; + } + } + return everyExprChild(e, expr, stmt); + }; + const stmt = (s: IrStmt): boolean => { + switch (s.kind) { + case "varDecl": + case "assign": { + const value = s.kind === "varDecl" ? s.init : s.value; + if (value && int32ArrayAllocation(value)) written.add(s.localId); + else if (value) result.delete(s.localId); + if (value) expr(value); + return true; + } + case "arraySet": + receiver(s.arr); + expr(s.index); + expr(s.value); + return true; + case "arraySetUndefined": + // Stores a present undefined: a state, and a cleared (+0) slot. + receiver(s.arr); + expr(s.index); + return true; + case "forOf": + result.delete(s.localId); + break; + case "tryCatch": + if (s.catchLocalId) result.delete(s.catchLocalId); + break; + } + return everyStmtChild(s, expr, stmt); + }; + fn.body.every(stmt); + for (const id of result) if (!written.has(id)) result.delete(id); + return result; +} + +/** The element values stored into each candidate: element writes, pushed + * values, literal elements and fill values. */ +export function int32ArrayStores( + fn: IrFunction, + candidates: ReadonlySet, +): Map { + const stores = new Map(); + const add = (id: string, value: IrExpr): void => { + const list = stores.get(id); + if (list) list.push(value); + else stores.set(id, [value]); + }; + const target = (e: IrExpr): string | null => + e.kind === "varRef" && candidates.has(e.localId) ? e.localId : null; + const expr = (e: IrExpr): boolean => { + if (e.kind === "arrIntrinsic" && e.method === "push") { + const id = target(e.receiver); + if (id !== null) for (const arg of e.args) add(id, arg); + } + return everyExprChild(e, expr, stmt); + }; + const stmt = (s: IrStmt): boolean => { + if ((s.kind === "varDecl" || s.kind === "assign") && candidates.has(s.localId)) { + const value = s.kind === "varDecl" ? s.init : s.value; + if (value?.kind === "arrayLit") for (const el of value.elems) add(s.localId, el); + else if (value) { + const fill = fillAllocation(value); + if (fill) add(s.localId, fill.args[0]!); + } + } + if (s.kind === "arraySet") { + const id = target(s.arr); + if (id !== null) add(id, s.value); + } + return everyStmtChild(s, expr, stmt); + }; + fn.body.every(stmt); + return stores; +} + +/** Rewrite a function's candidates for the backend. A discarded pop uses + * the candidate in place of its hidden local. An element-reader helper call + * whose array is a candidate is inlined: the helper's returned ternary, with + * the candidate for its array and the index for its index, so the read means + * the same. An index that is not a plain read or literal is bound to a fresh + * local first: in its own declaration just before the statement when the + * call is the whole value of a declaration or assignment (the form lowering + * gives `a[i]!`, and the one the backend reads without a box), otherwise in a + * sequence around the ternary. Selection keeps out indexes that write the + * array, so evaluating the index before the array read is unobservable. */ +function rewriteCandidates( + fn: IrFunction, + candidates: ReadonlySet, + readers: ReadonlyMap, +): IrFunction { + const locals = [...fn.locals]; + let fresh = 0; + const bind = (index: IrExpr): { decl: IrStmt; ref: IrExpr } => { + const id = `%int32.read.${fresh++}`; + locals.push({ id, name: id, type: F64, mutable: false }); + return { + decl: { kind: "varDecl", localId: id, init: index, loc: index.loc }, + ref: { kind: "varRef", localId: id, type: F64, loc: index.loc }, + }; + }; + const readerCall = (e: IrExpr | null): (IrExpr & { kind: "call" }) | null => + e?.kind === "call" && + e.args.length === 2 && + readers.has(e.callee) && + e.args[0]!.kind === "varRef" && + candidates.has(e.args[0]!.localId) + ? e + : null; + const simple = (e: IrExpr): boolean => e.kind === "varRef" || e.kind === "numLit"; + // The statements of a list, with complex read indexes declared first. + const hoist = (list: IrStmt[]): IrStmt[] => + list.flatMap((s): IrStmt[] => { + if (s.kind !== "varDecl" && s.kind !== "assign") return [s]; + const value = s.kind === "varDecl" ? s.init : s.value; + const call = readerCall(value); + if (!call || simple(call.args[1]!)) return [s]; + const { decl, ref } = bind(call.args[1]!); + const read: IrExpr = { ...call, args: [call.args[0]!, ref] }; + if (s.kind === "varDecl") return [decl, { ...s, init: read }]; + return [decl, { ...s, value: read }]; + }); + const hoistCase = (c: SwitchCase): SwitchCase => ({ ...c, body: hoist(c.body) }); + const lists = (s: IrStmt): IrStmt => { + switch (s.kind) { + case "if": + return { ...s, then: hoist(s.then), else_: s.else_ === null ? null : hoist(s.else_) }; + case "block": + return { ...s, body: hoist(s.body) }; + case "while": + return { ...s, body: hoist(s.body) }; + case "doWhile": + return { ...s, body: hoist(s.body) }; + case "for": + return { ...s, body: hoist(s.body) }; + case "forOf": + return { ...s, body: hoist(s.body) }; + case "switch": + return { ...s, cases: s.cases.map((c) => hoistCase(c)) }; + case "tryCatch": + return { + ...s, + tryBody: hoist(s.tryBody), + catchBody: s.catchBody === null ? null : hoist(s.catchBody), + finallyBody: s.finallyBody === null ? null : hoist(s.finallyBody), + }; + } + return s; + }; + // Hidden pop locals replaced by their candidate array. + const popLocals = new Set(); + const rewrite = (e: IrExpr): IrExpr => { + const pop = discardedPop(e); + if (e.kind === "seqExpr" && pop && candidates.has(pop.array.localId)) { + popLocals.add(pop.local); + const array = pop.array; + const substitute: IrTransform = { + expr: (x) => + x.kind === "varRef" && x.localId === pop.local ? { ...array, loc: x.loc } : x, + stmt: (x) => x, + }; + return { + ...e, + stmts: e.stmts.slice(1).map((x) => transformStmt(x, substitute)), + result: transformExpr(e.result, substitute), + }; + } + const call = readerCall(e); + if (!call) { + if (e.kind === "seqExpr") return { ...e, stmts: hoist(e.stmts) }; + return e; + } + const helper = readers.get(call.callee)!; + const array = call.args[0]!; + const [arrayParam, indexParam] = helper.params; + const ret = helper.body[0]!; + if (ret.kind !== "return" || !ret.value) return e; + let index = call.args[1]!; + const stmts: IrStmt[] = []; + if (!simple(index)) { + const { decl, ref } = bind(index); + stmts.push(decl); + index = ref; + } + const operand = index; + const body = transformExpr(reparseExpr(ret.value), { + expr: (x) => { + if (x.kind !== "varRef") return x; + if (x.localId === arrayParam!.localId) return reparseExpr(array); + if (x.localId === indexParam!.localId) return reparseExpr(operand); + return x; + }, + stmt: (x) => x, + }); + // The ternary is a fresh copy (reparseExpr): it takes the call's location. + const typed = body; + typed.loc = e.loc; + return stmts.length === 0 + ? typed + : { kind: "seqExpr", stmts, result: typed, type: typed.type, loc: e.loc }; + }; + const body = transformStmtList(hoist(fn.body), { expr: rewrite, stmt: lists }); + if (fresh === 0 && popLocals.size === 0) return { ...fn, body }; + return { ...fn, body, locals: locals.filter((local) => !popLocals.has(local.id)) }; +} + +/** Propose the module's int32 array candidates (`function\0local` keys in + * `int32ArrayCandidates`), only those in `keep` when given, and rewrite + * their functions. */ +export function markInt32ArrayCandidates( + mod: IrModule, + keep: ReadonlySet | null = null, +): IrModule { + if (mod.lib !== undefined) return mod; + const readers = new Map(); + for (const fn of mod.functions) if (isElementReader(fn)) readers.set(fn.name, fn); + const names = new Set(readers.keys()); + const keys: string[] = []; + const functions = mod.functions.map((fn) => { + const candidates = int32ArrayCandidates(fn, names); + for (const id of candidates) + if (keep === null || keep.has(`${fn.name}\0${id}`)) keys.push(`${fn.name}\0${id}`); + else candidates.delete(id); + return candidates.size === 0 ? fn : rewriteCandidates(fn, candidates, readers); + }); + return keys.length === 0 ? mod : { ...mod, functions, int32ArrayCandidates: keys }; +} + +/** The module the backend emits: rewritten for exactly the candidates the + * int32 slot analysis (`prove`, returning its int32Arrays) keeps, so a + * module without int32 arrays is `mod` itself. A dropped candidate's + * rewrite is undone by marking again without it, until every candidate + * marked is kept. */ +export function selectInt32Arrays( + mod: IrModule, + prove: (marked: IrModule) => ReadonlySet, +): IrModule { + let keep: ReadonlySet | null = null; + for (let round = 0; round < 4; round++) { + const marked = markInt32ArrayCandidates(mod, keep); + if (marked === mod) return mod; + const kept = prove(marked); + const proposed = marked.int32ArrayCandidates ?? []; + if (proposed.every((key) => kept.has(key))) return marked; + keep = new Set(proposed.filter((key) => kept.has(key))); + } + return mod; +} diff --git a/packages/compiler/src/ir/int32-slots.ts b/packages/compiler/src/ir/int32-slots.ts index 21223862a..df9e3c0a5 100644 --- a/packages/compiler/src/ir/int32-slots.ts +++ b/packages/compiler/src/ir/int32-slots.ts @@ -10,6 +10,7 @@ import { } from "./integer-ranges.js"; import { exprFunctionReference, moduleFunctionReferences } from "./function-references.js"; import { findInitializerBindings, withInitializerBindings } from "./initializer-bindings.js"; +import { checkableInt32Allocations, int32ArrayStores } from "./int32-arrays.js"; import { everyExprChild, everyStmtChild, everyStmtList, everyTypeChild } from "./traverse.js"; /** The storage slot an array-valued expression reads, as an @@ -169,7 +170,10 @@ export class Int32Slots { this.int32Locals.has(`${fnName}\0${localId}`) || this.int32Locals.has(`\0${localId}`) ? INT32_RANGE : null; - if (this.int32Elements.size > 0) result.elements = (arr) => this.hasInt32Elements(fnName, arr); + if (this.int32Elements.size > 0) { + result.elements = (arr) => this.hasInt32Elements(fnName, arr); + if (this.narrowedReads) result.narrowedReads = true; + } const params = this.paramFacts.get(fnName); if (params) result.params = @@ -234,6 +238,28 @@ export class Int32Slots { addInt32Elements(key: string): void { this.int32Elements.add(key); } + dropInt32Elements(key: string): boolean { + return this.int32Elements.delete(key); + } + hasInt32ElementSlot(key: string): boolean { + return this.int32Elements.has(key); + } + /** Whether the module has int32 array candidates (the narrowedReads + * range fact). */ + narrowedReads = false; + /** Local number arrays (`function\0local`) stored as int32 slots: the + * module's int32 array candidates whose every stored element the + * fixpoint proved an int32. */ + readonly int32Arrays = new Set(); + /** Local number arrays that may hold int32 slots with checked stores: the + * candidates whose element stores the fixpoint proved exact integers but + * not int32s, allocated only by fills that may be long. A fill of at least + * CHECKED_INT32_MIN_LENGTH elements creates one with int32 slots and a + * shorter one with doubles. Their elements give no int32 facts; compiled + * accesses test the array's kind (once per innermost loop where they can, + * backend/llvm/int32-kind-loops.ts), and a store that is not an int32 + * widens it first (scr_arr_widen_i32). */ + readonly checkedInt32Arrays = new Set(); /** Whether every present element of the number array `arr` evaluates to * (read inside `fnName`) is an int32. */ hasInt32Elements(fnName: string, arr: IrExpr): boolean { @@ -365,6 +391,7 @@ const DYN_READ_ONLY = new Set([ "dyn.forInStable", "dyn.getOwnPropertyNames", "dyn.getPrototype", + "dyn.getPrototypeForIdentity", "dyn.hasKey", "dyn.hasOwn", "dyn.in", @@ -508,7 +535,10 @@ export function dynamicFieldStores( }; const dynWritten = new Set(); let dynWritesAll = false; - for (const fn of mod.functions) + // A store whose key is not a literal names every field: later stores add + // nothing, so the walk stops there. + for (const fn of mod.functions) { + if (dynWritesAll) break; walkFunction(fn, { expr: (e) => { if (dynWritesAll) return; @@ -524,6 +554,7 @@ export function dynamicFieldStores( } }, }); + } return { all: dynWritesAll, names: dynWritten }; } @@ -536,6 +567,22 @@ export function dynamicallyStoredFamilies( slots: Int32Slots, stores?: DynamicFieldStores, ): Set { + const records = new Map((mod.records ?? []).map((r) => [r.id, r])); + const unions = new Map((mod.unions ?? []).map((u) => [u.id, u])); + // A live dyn view (typed-ref capsule) of an instance writes back into + // its fields only after a runtime store into the capsule; the commit then + // writes every field from the view, which holds the exact snapshot value + // of any property no store named. So reaching dynamic code disqualifies + // exactly the field names some dynamic store in the program can name. + const { all: dynWritesAll, names: dynWritten } = + stores ?? dynamicFieldStores(mod, records, unions); + const storable = (field: { name: string; type: IrType }): boolean => + field.type.kind === "f64" && + !field.name.startsWith("%") && + (dynWritesAll || dynWritten.has(field.name)); + const families = new Set(); + // Without a field some store can name, no class is worth exposing. + if (!(mod.classes ?? []).some((cls) => cls.fields.some(storable))) return families; const classes = new Map((mod.classes ?? []).map((c) => [c.name, c])); const children = new Map(); for (const cls of classes.values()) @@ -554,8 +601,6 @@ export function dynamicallyStoredFamilies( } return out; }; - const records = new Map((mod.records ?? []).map((r) => [r.id, r])); - const unions = new Map((mod.unions ?? []).map((u) => [u.id, u])); const exposedClasses = new Set(); const seenNamed = new Set(); let exposeAll = false; @@ -596,13 +641,6 @@ export function dynamicallyStoredFamilies( }); } }; - // A live dyn view (typed-ref capsule) of an instance writes back into - // its fields only after a runtime store into the capsule; the commit then - // writes every field from the view, which holds the exact snapshot value - // of any property no store named. So reaching dynamic code disqualifies - // exactly the field names some dynamic store in the program can name. - const { all: dynWritesAll, names: dynWritten } = - stores ?? dynamicFieldStores(mod, records, unions); for (const fn of mod.functions) walkFunction(fn, { expr: (e) => { @@ -628,16 +666,10 @@ export function dynamicallyStoredFamilies( if (s.kind === "throw") expose(s.value.type); }, }); - const families = new Set(); for (const cls of classes.values()) { if (!exposeAll && !exposedClasses.has(cls.name)) continue; for (const field of cls.fields) - if ( - field.type.kind === "f64" && - !field.name.startsWith("%") && - (dynWritesAll || dynWritten.has(field.name)) - ) - families.add(slots.family(cls.name, field.name)!); + if (storable(field)) families.add(slots.family(cls.name, field.name)!); } return families; } @@ -747,6 +779,18 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): } for (const key of contracts?.int32Locals ?? []) slots.addInt32Local(key); for (const key of contracts?.int32Elements ?? []) slots.addInt32Elements(key); + // Int32 array candidates (ir/int32-arrays.ts): assumed to hold int32 + // elements until a store the fixpoint cannot prove drops one. + const arrayCandidates = new Map>(); + slots.narrowedReads = (mod.int32ArrayCandidates?.length ?? 0) > 0; + for (const key of mod.int32ArrayCandidates ?? []) { + const cut = key.indexOf("\0"); + const fnName = key.slice(0, cut); + const set = arrayCandidates.get(fnName) ?? new Set(); + set.add(key.slice(cut + 1)); + arrayCandidates.set(fnName, set); + slots.addInt32Elements(`l:${key}`); + } /* ── candidate fields ── */ const children = new Map(); @@ -1026,6 +1070,7 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): return false; }; const hasCandidateWrite = (fn: IrFunction): boolean => { + if (arrayCandidates.has(fn.name)) return true; const box = (localId: string): boolean => slots.hasBox(slots.boxGroup(fn.name, localId)); const returns = slots.hasReturn(fn.name); return !everyStmtList(fn.body, { @@ -1058,13 +1103,49 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): if (params) for (const id of params.keys()) reads(`param:${fn.name}\0${id}`, fn.name); // Range facts are consulted only at writes into candidate slots; slots // are only ever dropped, so a body without such a write needs none. - const ranges: IntegerRanges = hasCandidateWrite(fn) + const candidate = hasCandidateWrite(fn); + const ranges: IntegerRanges = candidate ? analyzeIntegerRanges(numeric(fn), slots.facts(fn.name)) : new Map(); if (ranges.size > 0) lastWrites.set(fn.name, slotWrites(fn, ranges, functions)); else lastWrites.delete(fn.name); const proven: IrExpr[] = []; + // The walk below acts only at writes into candidate slots. + if (!candidate) { + provenByFunction.set(fn.name, proven); + continue; + } const int32 = (e: IrExpr): boolean => withinInt32(ranges.get(e)); + // Every element stored into an int32 array candidate must be an int32 + // under the facts that assume the candidates' own elements are; a + // failing store drops its array's element facts, and the function is + // analyzed again without them. When every failing store is still an + // exact integer under those facts (a counter, `a[i]! + 1`) and the array + // is only ever allocated by a fill that may be long + // (checkableInt32Allocations), it keeps int32 slots with checked stores + // (checkedInt32Arrays), which widen it to doubles in place at the first + // store that is not an int32. + const arrays = arrayCandidates.get(fn.name); + if (arrays) { + let dropped = false; + for (const [id, values] of int32ArrayStores(fn, arrays)) { + const key = `l:${fn.name}\0${id}`; + if (!slots.hasInt32ElementSlot(key) || values.every(int32)) continue; + slots.dropInt32Elements(key); + const checkable = (value: IrExpr): boolean => { + const range = ranges.get(value); + return int32(value) || range != null; + }; + if (values.every(checkable) && checkableInt32Allocations(fn, id)) + slots.checkedInt32Arrays.add(`${fn.name}\0${id}`); + else reason(`array:${fn.name}\0${id}`, "an element store is not int32"); + dropped = true; + } + if (dropped && !queued.has(fn.name)) { + queued.add(fn.name); + queue.push(fn.name); + } + } // An int32 written into a slot still assumed non-negative: widen the // assumption when the value may be negative, and reanalyze its readers. const settle = (slot: string, e: IrExpr): void => { @@ -1133,6 +1214,10 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): } for (const proven of provenByFunction.values()) for (const value of proven) slots.provenWrites.add(value); + for (const [fnName, ids] of arrayCandidates) + for (const id of ids) + if (slots.hasInt32ElementSlot(`l:${fnName}\0${id}`)) + slots.int32Arrays.add(`${fnName}\0${id}`); const ranged = rangedParameters(slots, functions, retry, numeric, (callee) => readers.get(`ret:${callee}`), ); diff --git a/packages/compiler/src/ir/integer-loops.test.ts b/packages/compiler/src/ir/integer-loops.test.ts index b6ac27e3a..2cca43a00 100644 --- a/packages/compiler/src/ir/integer-loops.test.ts +++ b/packages/compiler/src/ir/integer-loops.test.ts @@ -402,6 +402,16 @@ test("paired counters closing in on each other run as integers", () => { guarded: false, range: { min: 0, max: 9 }, }); + // `hi > lo` and `hi >= lo` are `lo < hi` and `lo <= hi`. + for (const op of [">", ">="] as const) { + const reversed = paired(number(0), number(9)); + reversed.cond = { kind: "bin", op, left: ref("hi"), right: ref("lo"), type: BOOL, loc }; + expect(matchIntegerPairedForLoop(reversed, vars)).toMatchObject({ + low: { localId: "lo", step: 1 }, + high: { localId: "hi", step: -1 }, + inclusive: op === ">=", + }); + } // A start without a known range is tested at run time. expect(matchIntegerPairedForLoop(paired(number(0), ref("head")), vars)).toMatchObject({ high: { startGuarded: true }, diff --git a/packages/compiler/src/ir/integer-loops.ts b/packages/compiler/src/ir/integer-loops.ts index c451c0852..d6942718a 100644 --- a/packages/compiler/src/ir/integer-loops.ts +++ b/packages/compiler/src/ir/integer-loops.ts @@ -333,7 +333,12 @@ export function matchIntegerCountedForLoop( ) useful = true; // A string read at the counter indexes with the integer directly. - if (e.kind === "strIntrinsic" && e.method === "charCodeAt" && e.args[0] && counter(e.args[0])) + if ( + e.kind === "strIntrinsic" && + (e.method === "charCodeAt" || e.method === "charAt") && + e.args[0] && + counter(e.args[0]) + ) useful = true; // So does an array element read; only a loop that needs no versioned // copy takes the integer counter for it. @@ -377,9 +382,9 @@ export function matchIntegerCountedForLoop( * * The first counter only grows and the second only shrinks, by positive * literal steps, the body writes neither, and the loop runs while the first - * is below (or at most) the second. Every body entry therefore holds both - * between the first start and the second start: exact integers when the - * starts are. A start without a proven integer range is tested at run time + * is below (or at most) the second, written either way round (`hi > lo`). + * Every body entry therefore holds both between the first start and the + * second start: exact integers when the starts are. A start without a proven integer range is tested at run time * on a versioned loop, as a counted loop's is. */ export interface IntegerPairedCounter { localId: string; @@ -407,10 +412,13 @@ export function matchIntegerPairedForLoop( cond = stmt.cond; if (init?.kind !== "block" || init.body.length !== 2) return null; if (update?.kind !== "block" || update.body.length !== 2) return null; - if (cond?.kind !== "bin" || (cond.op !== "<" && cond.op !== "<=")) return null; - if (cond.left.kind !== "varRef" || cond.right.kind !== "varRef") return null; - const lowId = cond.left.localId, - highId = cond.right.localId; + if (cond?.kind !== "bin" || cond.left.kind !== "varRef" || cond.right.kind !== "varRef") + return null; + const below = cond.op === "<" || cond.op === "<="; + if (!below && cond.op !== ">" && cond.op !== ">=") return null; + // `hi > lo` is `lo < hi`. + const lowId = below ? cond.left.localId : cond.right.localId, + highId = below ? cond.right.localId : cond.left.localId; if (lowId === highId) return null; const counter = (id: string): IntegerPairedCounter | null => { const decl = init.body.find((d) => d.kind === "varDecl" && d.localId === id); @@ -456,7 +464,12 @@ export function matchIntegerPairedForLoop( (isCounter(e.left) || isCounter(e.right))); const expr = (e: IrExpr): boolean => { if (e.kind === "arrayGet" && isCounter(e.index)) useful = true; - if (e.kind === "strIntrinsic" && e.method === "charCodeAt" && e.args[0] && isCounter(e.args[0])) + if ( + e.kind === "strIntrinsic" && + (e.method === "charCodeAt" || e.method === "charAt") && + e.args[0] && + isCounter(e.args[0]) + ) useful = true; if (e.kind === "bytesIntrinsic" && e.method === "get" && e.args[0] && isCounter(e.args[0])) useful = true; @@ -475,7 +488,7 @@ export function matchIntegerPairedForLoop( return { low, high, - inclusive: cond.op === "<=", + inclusive: cond.op === "<=" || cond.op === ">=", guarded, // A loop whose starts already fail the condition never enters the body. range: { diff --git a/packages/compiler/src/ir/integer-ranges.ts b/packages/compiler/src/ir/integer-ranges.ts index 1d295ca24..0e5ca426d 100644 --- a/packages/compiler/src/ir/integer-ranges.ts +++ b/packages/compiler/src/ir/integer-ranges.ts @@ -220,6 +220,11 @@ export interface IntegerSlotFacts { /** Number arrays whose every present element is an int32 (Int32[] * contracts): an element read that found a value reads an int32. */ elements?: (arr: IrExpr) => boolean; + /** Element reads narrowed out of a union (`xs[i]!`, also when a union + * local holds the read) keep the element's range, and a ternary arm that + * always throws yields no value. Only modules with int32 array candidates + * (ir/int32-arrays.ts) set it, so no other program's ranges change. */ + narrowedReads?: boolean; } /** A local read, or a numeric local plus or minus an integer literal @@ -240,6 +245,50 @@ function offsetRef(e: IrExpr): { localId: string; offset: number } | null { return null; } +/** Library calls that always throw. */ +const THROWING_LIB = /^error\.\w*Throw$/; + +function throws(e: IrExpr): boolean { + return e.kind === "libCall" && THROWING_LIB.test(e.fn); +} + +/** The values written to each union-typed local of `fn` by declarations + * and assignments; null for a local that another write reaches or that a + * closure captures. */ +function unionLocalWrites( + fn: IrFunction, + captures: ReadonlySet, +): Map { + const unionLocals = new Set( + fn.locals + .filter((l) => l.type.kind === "union" && !l.boxed && !captures.has(l.id)) + .map((l) => l.id), + ); + const result = new Map(); + const add = (id: string, value: IrExpr | null): void => { + if (!unionLocals.has(id)) return; + const list = result.get(id); + if (list === null) return; + if (value === null) result.set(id, null); + else if (list) list.push(value); + else result.set(id, [value]); + }; + everyStmtList(fn.body, { + stmt: (s) => { + if (s.kind === "varDecl" && s.init) add(s.localId, s.init); + else if (s.kind === "assign") add(s.localId, s.value); + else if (s.kind === "forOf") add(s.localId, null); + else if (s.kind === "tryCatch" && s.catchLocalId) add(s.catchLocalId, null); + return true; + }, + expr: (e) => { + if (e.kind === "assignExpr") add(e.localId, null); + return true; + }, + }); + return result; +} + /** Structured local dataflow. Calls cannot write uncaptured local slots, * but argument expressions can; unknown evaluation forms invalidate all * syntactically written slots before their children are inspected. Loops @@ -272,6 +321,10 @@ export function analyzeIntegerRangeFacts( .map((l) => l.id), ); const loopLocals = new Map(fn.locals.filter((l) => !captures.has(l.id)).map((l) => [l.id, l])); + // With narrowed reads, the values written to union locals (an element + // read held before it is narrowed), or null for a local some other write + // (assignExpr, for-of, catch, capture) reaches. + const unionWrites = slots.narrowedReads ? unionLocalWrites(fn, captures) : null; // A captured binding has no local dataflow: only a proof that covers // every write of the shared box anywhere applies to its reads. const boxedRange = (localId: string): IntegerRange | null => { @@ -490,6 +543,45 @@ export function analyzeIntegerRangeFacts( return range; } + /** narrowedRange through a union local: every value written to it. */ + function narrowedValue( + value: IrExpr, + unionId: string, + tag: number, + ): IntegerRange | null | "never" { + if (value.kind !== "varRef") return narrowedRange(value, unionId, tag); + const written = unionWrites?.get(value.localId); + if (!written || written.length === 0) return null; + let result: IntegerRange | null | "never" = "never"; + for (const w of written) { + const arm = narrowedRange(w, unionId, tag); + if (arm === "never") continue; + if (arm === null) return null; + result = result === "never" ? arm : union(result, arm); + } + return result; + } + /** The range of the values of union tag `tag` that `value` (already + * analyzed) can hold: "never" when no arm holds that tag. */ + function narrowedRange( + value: IrExpr, + unionId: string, + tag: number, + ): IntegerRange | null | "never" { + if (value.kind === "seqExpr") return narrowedRange(value.result, unionId, tag); + if (value.kind === "ternary") { + const yes = narrowedRange(value.then, unionId, tag), + no = narrowedRange(value.else_, unionId, tag); + if (yes === "never") return no; + if (no === "never") return yes; + return yes && no ? union(yes, no) : null; + } + if (value.kind === "unionWrap" && value.unionId === unionId) { + if (value.tag !== tag) return "never"; + return value.value.kind === "arrayGet" ? (ranges.get(value.value) ?? null) : null; + } + return null; + } function remember(e: IrExpr, range: IntegerRange | null): IntegerRange | null { // Shared IR objects have to satisfy the proof at every occurrence. const previous = ranges.get(e); @@ -683,6 +775,10 @@ export function analyzeIntegerRangeFacts( right = expr(e.else_, no); merge(facts, yes, no); if (left && right) range = union(left, right); + // With narrowed reads: an arm that always throws yields no value, + // so the other arm's range is the result's. + else if (slots.narrowedReads && throws(e.then)) range = right; + else if (slots.narrowedReads && throws(e.else_)) range = left; break; } case "logical": { @@ -777,27 +873,45 @@ export function analyzeIntegerRangeFacts( expr(e.obj, facts); range = slotRange(e); break; + case "unionNarrow": + // Only modules with narrowed reads take this rule: the narrow + // evaluates its operand first, and throws for every arm of another + // tag, so an element read `xs[i]!` (a ternary of the wrapped + // element and a wrapped undefined) has the element's range. + if (!slots.narrowedReads || e.type.kind !== "f64") { + range = conservative(e, facts); + break; + } + expr(e.value, facts); + { + const arm = narrowedValue(e.value, e.unionId, e.tag); + range = arm === "never" ? null : arm; + } + break; default: - // Do not assume the traversal order is an evaluation order for an - // unknown node. Every child gets the same conservative entry state. - havoc(facts, [e], facts); - everyExprChild( - e, - (child) => { - expr(child, new Map(facts)); - return true; - }, - (child) => { - body([child], new Map(facts)); - return true; - }, - ); - // A direct call's result carries its whole-program return proof. - range = slotRange(e); + range = conservative(e, facts); break; } return remember(e, range); } + /** An unknown node: do not assume the traversal order is an evaluation + * order. Every child gets the same conservative entry state. */ + function conservative(e: IrExpr, facts: Facts): IntegerRange | null { + havoc(facts, [e], facts); + everyExprChild( + e, + (child) => { + expr(child, new Map(facts)); + return true; + }, + (child) => { + body([child], new Map(facts)); + return true; + }, + ); + // A direct call's result carries its whole-program return proof. + return slotRange(e); + } function induction( s: IrStmt & { kind: "for" }, facts: Facts, diff --git a/packages/compiler/src/ir/ir.test.ts b/packages/compiler/src/ir/ir.test.ts index 52ef9b9c1..d62c9c83c 100644 --- a/packages/compiler/src/ir/ir.test.ts +++ b/packages/compiler/src/ir/ir.test.ts @@ -10,6 +10,7 @@ import { canDynCheckTo, type IrBytesElem, isJsonSafeType, + iterableArmsElem, isJsonStringifySafeType, isJsonStringifyDynamicType, type IrRecordShape, @@ -22,6 +23,29 @@ import { typeKey, } from "./ir.js"; +describe("iterable unions", () => { + const gen = (yieldT: IrType): IrType => ({ + kind: "generator", + yieldT, + retT: { kind: "void" }, + nextT: { kind: "undefinedT" }, + }); + + test("names the element type of an Iterable union", () => { + expect(iterableArmsElem([arrayOf(F64), gen(F64), { kind: "set", elem: F64 }])).toEqual(F64); + expect(iterableArmsElem([arrayOf(STRING), gen(STRING), STRING])).toEqual(STRING); + expect(iterableArmsElem([arrayOf(F64), gen(F64), { kind: "undefinedT" }])).toEqual(F64); + }); + + test("rejects unions without exactly one plain generator arm over the same element", () => { + expect(iterableArmsElem([arrayOf(F64), STRING])).toBeNull(); + expect(iterableArmsElem([arrayOf(STRING), gen(F64)])).toBeNull(); + expect(iterableArmsElem([arrayOf(F64), gen(F64), gen(STRING)])).toBeNull(); + expect(iterableArmsElem([gen(F64), STRING])).toBeNull(); + expect(iterableArmsElem([arrayOf(F64), { ...gen(F64), nextT: F64 } as IrType])).toBeNull(); + }); +}); + describe("IR kind sets", () => { test("keeps procStream as the scalar handle exception", () => { expect(HANDLE_KINDS.has("procStream")).toBe(true); diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 4a395feb2..6d0760b81 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -730,6 +730,32 @@ function isCollectionValueKey(t: IrType): boolean { ); } +/** The element type of an ITERABLE union (`Iterable`, + * iterableUnionType): its one generator arm — a synchronous generator over + * T that returns nothing and takes no `.next()` values — beside source arms + * (T[], Set, a Map of [K, V] entries, a string), and the undefined and + * null arms of an optional Iterable. A generator arm is legal only in such + * a union. Null for every other union. */ +export function iterableArmsElem(arms: readonly IrType[]): IrType | null { + let elem: IrType | null = null; + for (const arm of arms) { + if (arm.kind !== "generator") continue; + if (elem !== null || arm.async || arm.retT.kind !== "void" || arm.nextT.kind !== "undefinedT") + return null; + elem = arm.yieldT; + } + if (elem === null) return null; + for (const arm of arms) { + if (arm.kind === "generator" || arm.kind === "undefinedT" || arm.kind === "nullT") continue; + if (arm.kind === "array" || arm.kind === "set") { + if (!typeEquals(arm.elem, elem)) return null; + } else if (arm.kind === "string") { + if (elem.kind !== "string") return null; + } else if (arm.kind !== "map") return null; + } + return elem; +} + /** Set elements and Map keys share storage, equality and ownership rules. */ export function isSupportedSetElem(t: IrType, unionArms?: IrType[]): boolean { return isSupportedMapKey(t, unionArms); @@ -1018,6 +1044,10 @@ export interface IrModule { entry: string; /** Executable roots run in isolated native worker contexts. */ workers?: boolean; + /** The program has declaration-typed boundary casts (decl-projection.ts): + * closures boxed for package code return records and arrays as live + * capsules, so a value that comes back keeps its identity. */ + declBoundary?: true; /** The program constructs LifetimeRegion (@scriptc/experimental): every * ProgramLifetime instance lives in a counted region (the region.* * intrinsics; backend/llvm/regions.ts). */ @@ -1047,6 +1077,12 @@ export interface IrModule { * write in the program (ir/contract-check.ts). Backends may rely on them * unconditionally. Absent when the program writes no contract. */ contracts?: IrContracts; + /** Local number arrays (`function\0local`) the backend may store as + * int32 slots, proposed by ir/int32-arrays.ts after their element-reader + * calls were inlined; the int32 slot analysis keeps the ones whose every + * stored element it proves an int32, and those whose stores are exact + * integers with checked stores. Absent when there is none. */ + int32ArrayCandidates?: string[]; /** The WebAssembly modules translated into this program (wasm/ * translate.ts), present exactly when the program uses the WebAssembly * API (possibly empty). The backend appends each module's translation @@ -1665,6 +1701,24 @@ export interface IrRecordShape { * an accessor property named once (see accessorSlotProp). The * checked-dynamic copy lists its keys in this order. */ accessorOrder?: string[]; + /** Every order the shape's values enumerate their keys in, when its + * object literals write the keys in more than one order (see + * key-order-witness.ts); declaredOrder is the first. Each value holds the + * index of its order in the hidden number field KEY_ORDER_FIELD: the + * literal that wrote it stores its order's index, a copy its source's, + * and every other construction leaves 0. Each key-order surface + * dispatches on that field. */ + orderVariants?: string[][]; +} + +/** The hidden field of a shape with orderVariants: the index of the value's + * key order. */ +export const KEY_ORDER_FIELD = "%order"; + +/** The orders a record shape's values enumerate their keys in: each order + * of a shape with orderVariants, or its one table. */ +export function shapeKeyOrders(shape: IrRecordShape): readonly (readonly string[])[] { + return shape.orderVariants ?? [shape.declaredOrder ?? shape.fields.map((f) => f.name)]; } /** fs.Dirent rows: the hidden %dtype entry kind (libuv's UV_DIRENT @@ -1943,8 +1997,18 @@ export type IrStmt = * RangeError from the runtime. `pop` marks the stores that implement * `pop()`: a frozen (published) array fails with pop's TypeError rather * than the length assignment's, and pop's `length = 0` store on an empty - * array is dropped unless the array may be frozen. */ - | { kind: "arraySetLength"; arr: IrExpr; length: IrExpr; loc: SrcLoc; pop?: boolean } + * array is dropped unless the array may be frozen. `reserve` marks a + * capacity hint instead: dense storage for `length` elements of a fresh + * array that statements fill next, with no change to its length and no + * observable effect. */ + | { + kind: "arraySetLength"; + arr: IrExpr; + length: IrExpr; + loc: SrcLoc; + pop?: boolean; + reserve?: boolean; + } /** Write an explicit present `undefined` state without shrinking length. */ | { kind: "arraySetUndefined"; arr: IrExpr; index: IrExpr; loc: SrcLoc } /** Delete an indexed/property slot without changing Array.length. */ @@ -2058,8 +2122,9 @@ export type IrStmt = * and whose `code` carries the SC diagnostic code; unwinds exactly like * `throw`. * TypeScript sources produce it only as the tsc-unreachable fallthrough - * trap (appendImplicitUndefinedReturn's SC9002) — their construct fences - * stay compile errors. */ + * trap (appendImplicitUndefinedReturn's SC9002) and for a typed-array + * store through an element index that is undefined at run time + * (undefined-index.ts); their construct fences stay compile errors. */ | { kind: "runtimeFence"; code: string; message: string; loc: SrcLoc } /** Rethrow of a catch binding (`throw e` where e is the binding): * re-raises the SAVED exception exactly — kind and payload preserved, @@ -3380,7 +3445,11 @@ export type IrExpr = | "array" | "truthy" | "error" - | "function"; + | "function" + /** The value is a capsule of a compiled class instance. */ + | "classCapsule" + /** The value is a boxed function or class value (exact kind test). */ + | "funcKind"; bytesElem?: IrBytesElem; negated?: true; value: IrExpr; @@ -3843,10 +3912,19 @@ export type IrExpr = /** `exact`: the value is known to be a capsule this program boxed from * exactly the class `type` names (a `dyn.typedRefKeyIs` test guards the * expression), so the check is one load of the capsule's instance. */ + /** `adopt`: the declaration-typed boundary's cast (decl-projection.ts). + * The conversion preserves identity: a source object or array that + * something else still references (reference count above one) is + * turned, after the whole check succeeded, into a live capsule of the + * value built from it, so every holder sees the same native value; an + * unshared source is simply consumed. Values must be plain data + * (ordinary objects and arrays without accessors, prototypes, symbols + * or frozen state), otherwise the cast throws the path TypeError. */ | { kind: "dynCheck"; value: IrExpr; preserveRefs?: true; + adopt?: true; exact?: true; type: IrType; loc: SrcLoc; diff --git a/packages/compiler/src/ir/js-ic.test.ts b/packages/compiler/src/ir/js-ic.test.ts index 1b566fd00..f71a1e9db 100644 --- a/packages/compiler/src/ir/js-ic.test.ts +++ b/packages/compiler/src/ir/js-ic.test.ts @@ -176,3 +176,64 @@ test("a function holds a site when it calls a classified helper or dispatches a expect(hasJsIcSite({ ...invoker, name: "%init.0" }, sites)).toBe(false); expect(hasJsIcSite({ ...caller, name: "%init.0" }, sites)).toBe(true); }); + +test("bag helpers that exit early for values no class branch matches are still the receiver", () => { + /** `if (!(v)) return `: the class tables' early exit. */ + const exit = (test: "classCapsule" | "funcKind", value: IrExpr, negated = true): IrStmt => ({ + kind: "if", + cond: { + kind: "dynTest", + test, + ...(negated ? { negated: true as const } : {}), + value: ref("v"), + type: BOOL, + loc, + }, + then: [ret(value)], + else_: null, + loc, + }); + const guarded = (name: string, first: IrStmt): IrFunction => + helper(name, ["v"], [first, capsuleBranch("v"), ret(ref("v"))]); + const readBag = guarded("%dyn.class.readPropertiesOrEmpty", exit("classCapsule", ref("v"))); + const writeBag = guarded("%dyn.class.properties", exit("classCapsule", ref("v"))); + const staticBag = guarded("%dyn.class.staticProperties", exit("funcKind", ref("v"))); + const getter = getHelper( + "%dyn.class.property.0", + "x", + [], + call(staticBag.name, call(readBag.name, ref("p.0"))), + ); + const setter = setHelper("%dyn.class.property.1", "y"); + // An exit that returns another value, or a test that is not negated, + // makes the bag differ from the receiver. + const otherExit = guarded( + "%dyn.class.other", + exit("classCapsule", { kind: "dynObjLit", type: DYN, loc } as IrExpr), + ); + const positive = guarded("%dyn.class.positive", exit("classCapsule", ref("v"), false)); + const otherGetter = getHelper("%dyn.class.property.2", "x", [], call(otherExit.name, ref("p.0"))); + const positiveGetter = getHelper( + "%dyn.class.property.3", + "x", + [], + call(positive.name, ref("p.0")), + ); + const sites = new JsIcSites({ + functions: [ + readBag, + writeBag, + staticBag, + otherExit, + positive, + getter, + setter, + otherGetter, + positiveGetter, + ], + }); + expect(sites.site("%dyn.class.property.0")).toEqual({ kind: "get", key: "x" }); + expect(sites.site("%dyn.class.property.1")).toEqual({ kind: "set", key: "y" }); + expect(sites.site("%dyn.class.property.2")).toBeNull(); + expect(sites.site("%dyn.class.property.3")).toBeNull(); +}); diff --git a/packages/compiler/src/ir/js-ic.ts b/packages/compiler/src/ir/js-ic.ts index 316b3c958..752316db3 100644 --- a/packages/compiler/src/ir/js-ic.ts +++ b/packages/compiler/src/ir/js-ic.ts @@ -170,6 +170,18 @@ export class JsIcSites { ); } + /** An early exit for values a class table never matches: `if (!(value)) return `, with no else. A plain object takes it + * and gets itself back. */ + private kindExit(s: IrStmt, param: string): boolean { + if (s.kind !== "if" || s.else_ !== null || s.then.length !== 1) return false; + const { cond } = s; + if (cond.kind !== "dynTest" || cond.negated !== true || !isVar(cond.value, param)) return false; + if (cond.test !== "classCapsule" && cond.test !== "funcKind") return false; + const ret = s.then[0]!; + return ret.kind === "return" && ret.value !== null && this.bagOf(ret.value, param); + } + /** A property-bag helper that returns its argument unless a capsule * branch is taken. */ private identityHelper(name: string): boolean { @@ -190,7 +202,9 @@ export class JsIcSites { const param = fn.params[0]!.localId; const last = fn.body[fn.body.length - 1]; identity = - fn.body.slice(0, -1).every((s) => this.capsuleBranch(s, param)) && + fn.body + .slice(0, -1) + .every((s) => this.capsuleBranch(s, param) || this.kindExit(s, param)) && last?.kind === "return" && last.value !== null && this.bagOf(last.value, param); diff --git a/packages/compiler/src/ir/paired-while-loops.test.ts b/packages/compiler/src/ir/paired-while-loops.test.ts new file mode 100644 index 000000000..e0a69afa3 --- /dev/null +++ b/packages/compiler/src/ir/paired-while-loops.test.ts @@ -0,0 +1,182 @@ +import { expect, test } from "vitest"; +import { + BOOL, + F64, + arrayOf, + type IrExpr, + type IrFunction, + type IrLocal, + type IrModule, + type IrStmt, +} from "./ir.js"; +import { runPairedWhileLoopsAsFor } from "./paired-while-loops.js"; + +const loc = { file: "paired-while-loops.ts", start: 0, end: 0 }; +const number = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc }); +const ref = (localId: string): IrExpr => ({ kind: "varRef", localId, type: F64, loc }); +const array = { kind: "varRef", localId: "a", type: arrayOf(F64), loc } as const; +const element = (index: IrExpr): IrStmt => ({ + kind: "exprStmt", + expr: { kind: "arrayGet", arr: array, index, type: F64, loc }, + loc, +}); +const step = (id: string, op: "+" | "-"): IrStmt => ({ + kind: "assign", + localId: id, + value: { kind: "bin", op, left: ref(id), right: number(1), type: F64, loc }, + loc, +}); +const declare = (id: string, init: IrExpr): IrStmt => ({ kind: "varDecl", localId: id, init, loc }); +const number_ = (id: string): IrLocal => ({ + id, + name: id.split(".")[0]!, + type: F64, + mutable: true, +}); + +function whileLoop( + body: IrStmt[] = [element(ref("lo.0")), element(ref("hi.0"))], + updates: IrStmt[] = [step("lo.0", "+"), step("hi.0", "-")], +): IrStmt & { kind: "while" } { + return { + kind: "while", + cond: { kind: "bin", op: "<", left: ref("lo.0"), right: ref("hi.0"), type: BOOL, loc }, + body: [...body, ...updates], + loc, + }; +} + +function program(body: IrStmt[], extra: Partial = {}): IrModule { + const fn: IrFunction = { + name: "f", + params: [{ localId: "n.0", name: "n", type: F64 }], + returnType: F64, + locals: [ + number_("n.0"), + number_("lo.0"), + number_("hi.0"), + { id: "a", name: "a", type: arrayOf(F64), mutable: false }, + ], + body, + loc, + ...extra, + }; + return { functions: [fn], globals: [] } as unknown as IrModule; +} + +const rewritten = (mod: IrModule): IrStmt[] => runPairedWhileLoopsAsFor(mod).functions[0]!.body; + +test("a two-counter while loop runs as a for loop over new counters", () => { + const mod = program([declare("lo.0", number(0)), declare("hi.0", ref("n.0")), whileLoop()]); + const out = runPairedWhileLoopsAsFor(mod).functions[0]!; + // The declarations stay; the loop's counters are new locals that start + // from them, and the trailing updates are the for loop's update. + expect(out.body.slice(0, 2)).toEqual(mod.functions[0]!.body.slice(0, 2)); + expect(out.body[2]).toMatchObject({ + kind: "for", + init: { + kind: "block", + body: [ + { kind: "varDecl", localId: "lo.1", init: { kind: "varRef", localId: "lo.0" } }, + { kind: "varDecl", localId: "hi.1", init: { kind: "varRef", localId: "hi.0" } }, + ], + }, + cond: { op: "<", left: { localId: "lo.1" }, right: { localId: "hi.1" } }, + update: { kind: "block", body: [{ localId: "lo.1" }, { localId: "hi.1" }] }, + body: [ + { expr: { kind: "arrayGet", index: { localId: "lo.1" } } }, + { expr: { kind: "arrayGet", index: { localId: "hi.1" } } }, + ], + }); + expect(out.locals.map((l) => l.id)).toEqual(["n.0", "lo.0", "hi.0", "a", "lo.1", "hi.1"]); +}); + +test("parameters, reversed comparisons, labels and either update order qualify", () => { + const loop = whileLoop(undefined, [step("hi.0", "-"), step("lo.0", "+")]); + loop.cond = { kind: "bin", op: ">=", left: ref("hi.0"), right: ref("lo.0"), type: BOOL, loc }; + loop.labels = ["outer"]; + const mod = program([declare("lo.0", number(0)), loop], { + params: [{ localId: "hi.0", name: "hi", type: F64 }], + }); + expect(rewritten(mod)[1]).toMatchObject({ + kind: "for", + labels: ["outer"], + cond: { op: ">=", left: { localId: "hi.1" }, right: { localId: "lo.1" } }, + update: { body: [{ localId: "hi.1" }, { localId: "lo.1" }] }, + }); +}); + +test("loops the for form would change, or whose counters are used elsewhere, stay", () => { + const decls = [declare("lo.0", number(0)), declare("hi.0", ref("n.0"))]; + const kept = (body: IrStmt[], extra: Partial = {}): void => { + const mod = program(body, extra); + expect(runPairedWhileLoopsAsFor(mod)).toBe(mod); + }; + // A continue that binds to the loop skips the trailing updates. + kept([...decls, whileLoop([{ kind: "continue", loc }, element(ref("lo.0"))])]); + const labeled = whileLoop([{ kind: "continue", label: "outer", loc }, element(ref("lo.0"))]); + labeled.labels = ["outer"]; + kept([...decls, labeled]); + // A counter read after the loop, written before it, or declared twice. + kept([...decls, whileLoop(), { kind: "return", value: ref("lo.0"), loc }]); + kept([...decls, step("hi.0", "-"), whileLoop()]); + kept([...decls, declare("lo.0", number(1)), whileLoop()]); + // A declaration outside an enclosing loop: a later entry would resume + // from the values the previous run left. + kept([ + ...decls, + { + kind: "while", + cond: { kind: "boolLit", value: true, type: BOOL, loc }, + body: [whileLoop()], + loc, + }, + ]); + // A parameter counter inside another loop, for the same reason. + const outerLoop: IrStmt = { + kind: "while", + cond: { kind: "boolLit", value: true, type: BOOL, loc }, + body: [whileLoop()], + loc, + }; + kept([outerLoop], { + params: [ + { localId: "lo.0", name: "lo", type: F64 }, + { localId: "hi.0", name: "hi", type: F64 }, + ], + }); + // A counter that was never declared, or that this function captures. + kept([declare("lo.0", number(0)), whileLoop()]); + kept([...decls, whileLoop()], { captures: [{ localId: "hi.0", name: "hi", type: F64 }] }); + // The body writes a counter, a counter is boxed, or no counter is an index. + kept([...decls, whileLoop([element(ref("lo.0")), step("hi.0", "-")])]); + const boxed = program([...decls, whileLoop()]); + boxed.functions[0]!.locals[2] = { ...number_("hi.0"), boxed: true }; + expect(runPairedWhileLoopsAsFor(boxed)).toBe(boxed); + kept([...decls, whileLoop([])]); +}); + +test("a continue in a nested loop keeps the outer loop eligible", () => { + const inner: IrStmt = { + kind: "while", + cond: { kind: "boolLit", value: false, type: BOOL, loc }, + body: [{ kind: "continue", loc }], + loc, + }; + // Literal starts have known ranges, so the loop needs no start test and + // may hold a nested loop. + const mod = program([ + declare("lo.0", number(0)), + declare("hi.0", number(9)), + whileLoop([inner, element(ref("lo.0"))]), + ]); + expect(rewritten(mod)[2]).toMatchObject({ kind: "for", body: [{ kind: "while" }, {}] }); + // A start without a known range is tested at run time, on a loop that + // may not nest another. + const guarded = program([ + declare("lo.0", number(0)), + declare("hi.0", ref("n.0")), + whileLoop([inner, element(ref("lo.0"))]), + ]); + expect(runPairedWhileLoopsAsFor(guarded)).toBe(guarded); +}); diff --git a/packages/compiler/src/ir/paired-while-loops.ts b/packages/compiler/src/ir/paired-while-loops.ts new file mode 100644 index 000000000..c096c34fe --- /dev/null +++ b/packages/compiler/src/ir/paired-while-loops.ts @@ -0,0 +1,323 @@ +import { matchIntegerPairedForLoop } from "./integer-loops.js"; +import { analyzeIntegerRanges, type IntegerRange } from "./integer-ranges.js"; +import { + F64, + type IrExpr, + type IrFunction, + type IrLocal, + type IrModule, + type IrStmt, +} from "./ir.js"; +import { everyExprChild, everyStmtChild, mapExprChildren, mapStmtChildren } from "./traverse.js"; + +type WhileLoop = IrStmt & { kind: "while" }; + +/** Two-counter while loops run as two-counter for loops. + * + * let lo = a, hi = b; + * while (lo < hi) { ...; lo++; hi--; } + * + * runs as `for (let l = lo, h = hi; l < h; l++, h--) { ... }`, whose + * counters the backend stores as integers (matchIntegerPairedForLoop). The + * loop qualifies when: + * - its condition compares the two counters (`<`, `<=`, `>` or `>=`); + * - its body ends with one update of each counter; + * - no `continue` in the body binds to it (a for loop's `continue` runs the + * update, a while loop's skips the body's last statements); + * - the function mentions neither counter outside the loop, except in one + * declaration or as a parameter; + * - every entry starts from the declared or passed values: a declaration is + * an earlier statement of the loop's own statement list, which runs before + * each entry, and a loop over a parameter is in no other loop. + * + * The for loop's counters are new locals that start from the counters' + * values on entry, so the declarations stay where they are and nothing + * between them and the loop moves. The original counters are not read + * after the loop, so they need no final value. A loop that the paired match + * would refuse is left as it is. */ +export function runPairedWhileLoopsAsFor(mod: IrModule): IrModule { + let changed = false; + const functions = mod.functions.map((fn) => { + const next = rewriteFunction(fn); + if (next !== fn) changed = true; + return next; + }); + return changed ? { ...mod, functions } : mod; +} + +/** The local a statement steps: `x++`, `x--` or `x = ...`. */ +function updatedLocal(s: IrStmt): string | null { + if (s.kind === "assign") return s.localId; + if (s.kind === "exprStmt" && s.expr.kind === "incDec") return s.expr.localId; + return null; +} + +/** The counters of a loop with the qualifying condition and trailing + * updates, or null. */ +function loopCounters(loop: WhileLoop, locals: ReadonlyMap): string[] | null { + const cond = loop.cond; + if (cond.kind !== "bin") return null; + if (cond.op !== "<" && cond.op !== "<=" && cond.op !== ">" && cond.op !== ">=") return null; + if (cond.left.kind !== "varRef" || cond.right.kind !== "varRef") return null; + const counters = [cond.left.localId, cond.right.localId]; + if (counters[0] === counters[1]) return null; + const n = loop.body.length; + if (n < 2) return null; + const updated = [updatedLocal(loop.body[n - 2]!), updatedLocal(loop.body[n - 1]!)]; + if (!counters.every((id) => updated.includes(id))) return null; + for (const id of counters) { + const local = locals.get(id); + if (local?.type.kind !== "f64" || !local.mutable || local.boxed || local.tdz) return null; + } + return counters; +} + +function isLoop(s: IrStmt): boolean { + return s.kind === "while" || s.kind === "doWhile" || s.kind === "for" || s.kind === "forOf"; +} + +/** True when a `continue` in the body binds to the loop: an unlabeled one + * outside nested loops, or one that names a label of the loop. */ +function continuesLoop(loop: WhileLoop): boolean { + const labels = loop.labels ?? []; + let depth = 0; + let found = false; + const expr = (e: IrExpr): boolean => !found && everyExprChild(e, expr, stmt); + const stmt = (s: IrStmt): boolean => { + if (found) return false; + if (s.kind === "continue") { + if (s.label === undefined ? depth === 0 : labels.includes(s.label)) found = true; + return !found; + } + const nested = isLoop(s); + if (nested) depth++; + const ok = everyStmtChild(s, expr, stmt); + if (nested) depth--; + return ok; + }; + loop.body.every(stmt); + return found; +} + +/** Mentions of each id in `node`, in any field (a string literal equal to + * an id counts too, which only refuses more loops). */ +function countMentions( + node: unknown, + ids: ReadonlySet, + mentions: Map, +): void { + if (typeof node === "string") { + if (ids.has(node)) mentions.set(node, (mentions.get(node) ?? 0) + 1); + } else if (Array.isArray(node)) { + for (const item of node) countMentions(item, ids, mentions); + } else if (node !== null && typeof node === "object") { + const record = node as Record; + for (const key in record) + if (key !== "loc" && key !== "type") countMentions(record[key], ids, mentions); + } +} + +/** A loop with the qualifying shape, the statements before it in its + * statement list, and whether it is inside another loop. */ +interface Candidate { + loop: WhileLoop; + counters: string[]; + before: readonly IrStmt[]; + nested: boolean; +} + +function findCandidates(fn: IrFunction, locals: ReadonlyMap): Candidate[] { + const candidates: Candidate[] = []; + let depth = 0; + const list = (body: readonly IrStmt[]): void => { + body.forEach((s, i) => { + if (s.kind === "while") { + const counters = loopCounters(s, locals); + if (counters !== null && !continuesLoop(s)) + candidates.push({ loop: s, counters, before: body.slice(0, i), nested: depth > 0 }); + } + visit(s); + }); + }; + // Statement lists inside expressions are not searched: their loops stay. + const visit = (s: IrStmt): void => { + const loop = isLoop(s); + if (loop) depth++; + switch (s.kind) { + case "block": + case "while": + case "doWhile": + case "for": + case "forOf": + list(s.body); + break; + case "if": + list(s.then); + if (s.else_ !== null) list(s.else_); + break; + case "switch": + for (const c of s.cases) list(c.body); + break; + case "tryCatch": + list(s.tryBody); + if (s.catchBody !== null) list(s.catchBody); + if (s.finallyBody !== null) list(s.finallyBody); + break; + default: + break; + } + if (loop) depth--; + }; + list(fn.body); + return candidates; +} + +function rewriteFunction(fn: IrFunction): IrFunction { + const locals = new Map(fn.locals.map((l) => [l.id, l])); + const candidates = findCandidates(fn, locals); + if (candidates.length === 0) return fn; + + const ids = new Set(); + for (const c of candidates) for (const id of c.counters) ids.add(id); + const total = new Map(); + countMentions(fn.body, ids, total); + const params = new Set(fn.params.map((p) => p.localId)); + // A counter this function reaches through its environment. + const outer = new Map(); + countMentions([fn.captures ?? [], fn.classCaptures ?? []], ids, outer); + + let ranges: ReadonlyMap | null = null; + const used = new Set(fn.locals.map((l) => l.id)); + const added: IrLocal[] = []; + const rewritten = new Map>(); + for (const { loop, counters, before, nested } of candidates) { + const inside = new Map(); + countMentions(loop, new Set(counters), inside); + // Each counter's start: its declaration's initializer, or null for a + // parameter. + const starts = new Map(); + for (const id of counters) { + if ((outer.get(id) ?? 0) !== 0) break; + const elsewhere = (total.get(id) ?? 0) - (inside.get(id) ?? 0); + if (params.has(id)) { + if (elsewhere === 0 && !nested) starts.set(id, null); + continue; + } + const decl = before.find((s) => s.kind === "varDecl" && s.localId === id); + if (elsewhere === 1 && decl?.kind === "varDecl" && decl.init !== null) + starts.set(id, decl.init); + } + if (starts.size !== counters.length) continue; + const renamed = new Map(); + const fresh: IrLocal[] = []; + for (const id of counters) { + const local = locals.get(id); + if (local === undefined) break; + let k = 0; + while (used.has(`${local.name}.${k}`)) k++; + const next = `${local.name}.${k}`; + used.add(next); + renamed.set(id, next); + fresh.push({ ...local, id: next }); + } + if (renamed.size !== counters.length) { + for (const local of fresh) used.delete(local.id); + continue; + } + const candidate = pairedForLoop(loop, renamed); + const forLocals = new Map(locals); + for (const local of fresh) forLocals.set(local.id, local); + // A start reads its counter on entry, which holds the initializer's + // value, with that value's range. + ranges ??= analyzeIntegerRanges(fn); + const startRanges = new Map(); + if (candidate.init?.kind === "block") + for (const decl of candidate.init.body) { + if (decl.kind !== "varDecl" || decl.init?.kind !== "varRef") continue; + const init = starts.get(decl.init.localId); + if (!init) continue; + const literal = + init.kind === "numLit" && Number.isSafeInteger(init.value) && !Object.is(init.value, -0); + startRanges.set( + decl.init, + literal ? { min: init.value, max: init.value } : (ranges.get(init) ?? null), + ); + } + if (matchIntegerPairedForLoop(candidate, forLocals, startRanges) === null) { + for (const local of fresh) used.delete(local.id); + continue; + } + rewritten.set(loop, renamed); + added.push(...fresh); + } + if (rewritten.size === 0) return fn; + + const expr = (e: IrExpr): IrExpr => mapExprChildren(e, expr, stmt); + const stmt = (s: IrStmt): IrStmt => { + const next = mapStmtChildren(s, expr, stmt); + const renamed = s.kind === "while" ? rewritten.get(s) : undefined; + // The body may hold rewritten loops of its own. + return renamed === undefined ? next : pairedForLoop(next as WhileLoop, renamed); + }; + return { ...fn, body: fn.body.map(stmt), locals: [...fn.locals, ...added] }; +} + +/** The for loop over the renamed counters: their starts read the original + * counters, and the body's two trailing updates become the update. */ +function pairedForLoop( + loop: WhileLoop, + renamed: ReadonlyMap, +): IrStmt & { kind: "for" } { + // Each copy spreads one node kind. + const expr = (e: IrExpr): IrExpr => { + const next = mapExprChildren(e, expr, stmt); + if (next.kind === "varRef") { + const id = renamed.get(next.localId); + if (id !== undefined) return { ...next, localId: id }; + } else if (next.kind === "incDec") { + const id = renamed.get(next.localId); + if (id !== undefined) return { ...next, localId: id }; + } else if (next.kind === "assignExpr") { + const id = renamed.get(next.localId); + if (id !== undefined) return { ...next, localId: id }; + } + return next; + }; + const stmt = (s: IrStmt): IrStmt => { + const next = mapStmtChildren(s, expr, stmt); + if (next.kind === "varDecl") { + const id = renamed.get(next.localId); + if (id !== undefined) return { ...next, localId: id }; + } else if (next.kind === "assign") { + const id = renamed.get(next.localId); + if (id !== undefined) return { ...next, localId: id }; + } else if (next.kind === "forOf") { + const id = renamed.get(next.localId); + if (id !== undefined) return { ...next, localId: id }; + } else if (next.kind === "rethrow") { + const id = renamed.get(next.localId); + if (id !== undefined) return { ...next, localId: id }; + } + return next; + }; + const loc = loop.loc; + const n = loop.body.length; + const declarations: IrStmt[] = []; + for (const [id, next] of renamed) + declarations.push({ + kind: "varDecl", + localId: next, + init: { kind: "varRef", localId: id, type: F64, loc }, + loc, + }); + const result: IrStmt & { kind: "for" } = { + kind: "for", + init: { kind: "block", body: declarations, loc }, + cond: expr(loop.cond), + update: { kind: "block", body: loop.body.slice(n - 2).map(stmt), loc }, + body: loop.body.slice(0, n - 2).map(stmt), + loc, + }; + if (loop.labels !== undefined) result.labels = loop.labels; + return result; +} diff --git a/packages/compiler/src/ir/parameter-versions.ts b/packages/compiler/src/ir/parameter-versions.ts new file mode 100644 index 000000000..88232251c --- /dev/null +++ b/packages/compiler/src/ir/parameter-versions.ts @@ -0,0 +1,307 @@ +import { analyzeInt32Slots } from "./int32-slots.js"; +import { BOOL, F64, type IrExpr, type IrFunction, type IrModule, type IrStmt } from "./ir.js"; +import { reparseStmt } from "./serialize.js"; +import { everyExprChild, everyStmtChild, everyStmtList } from "./traverse.js"; + +const MAX_FUNCTION_NODES = 2048; +const MAX_VERSIONS = 16; + +function size(fn: IrFunction): number { + let nodes = 0; + everyStmtList(fn.body, { + stmt: () => ++nodes <= MAX_FUNCTION_NODES, + expr: () => ++nodes <= MAX_FUNCTION_NODES, + }); + return nodes; +} + +/** The function a direct call or construction runs, if any. */ +function callee(e: IrExpr): string | null { + if (e.kind === "call") return e.callee; + if (e.kind === "new") return `%${e.className}.constructor`; + return null; +} + +/** Direct call and construction sites of each function in the module. */ +function callSites(mod: IrModule): Map { + const sites = new Map(); + for (const fn of mod.functions) + everyStmtList(fn.body, { + stmt: () => true, + expr: (e) => { + const name = callee(e); + if (name !== null) sites.set(name, (sites.get(name) ?? 0) + 1); + return true; + }, + }); + return sites; +} + +/** Expressions and statements of a callee that LLVM inlines at any number + * of call sites: scalar arithmetic and reads, no calls or allocations. */ +const SIMPLE_EXPRESSIONS = new Set([ + "varRef", + "numLit", + "boolLit", + "bin", + "unary", + "logical", + "ternary", + "toBool", + "incDec", + "assignExpr", + "arrayGet", + "fieldGet", + "recordGet", + "unionNarrow", + "unionIsTag", +]); +const SIMPLE_STATEMENTS = new Set([ + "varDecl", + "assign", + "exprStmt", + "if", + "block", + "return", +]); + +function simple(fn: IrFunction): boolean { + return ( + (fn.returnType.kind === "f64" || fn.returnType.kind === "bool") && + everyStmtList(fn.body, { + stmt: (s) => SIMPLE_STATEMENTS.has(s.kind), + expr: (e) => SIMPLE_EXPRESSIONS.has(e.kind), + }) + ); +} + +/** Whether a copy of `fn` would cost a callee its only call site. LLVM + * inlines a function called once wherever its size allows; with a second + * site, both calls can stay out of line, with the records and arguments + * they pass built in memory. A callee that only computes a number or a + * boolean from scalar reads inlines either way. A function calling itself + * would also dispatch on every call. */ +function keepsSingleBody( + fn: IrFunction, + sites: ReadonlyMap, + functions: ReadonlyMap, +): boolean { + return !everyStmtList(fn.body, { + stmt: () => true, + expr: (e) => { + const name = callee(e); + if (name === null) return true; + if (name === fn.name) return false; + if (sites.get(name) !== 1) return true; + const target = functions.get(name); + return target !== undefined && simple(target); + }, + }); +} + +/** Statements and expressions of a loop whose work is numeric: arithmetic, + * element and field reads and writes, `Math` calls and calls of simple + * functions. Loops that allocate, build strings or call other code gain + * little from an integer bound and would only grow the copy. */ +const NUMERIC_STATEMENTS = new Set([ + ...SIMPLE_STATEMENTS, + "for", + "while", + "doWhile", + "break", + "continue", + "arraySet", + "fieldSet", +]); +const NUMERIC_EXPRESSIONS = new Set([...SIMPLE_EXPRESSIONS, "arrayHas", "arrayState"]); + +function numericLoop(loop: IrStmt, functions: ReadonlyMap): boolean { + const expr = (e: IrExpr): boolean => { + let allowed = NUMERIC_EXPRESSIONS.has(e.kind); + if (e.kind === "call") { + const target = functions.get(e.callee); + allowed = target !== undefined && simple(target); + } else if (e.kind === "libCall") allowed = e.fn.startsWith("math."); + else if (e.kind === "arrIntrinsic") allowed = e.method === "length"; + return allowed && everyExprChild(e, expr, stmt); + }; + const stmt = (s: IrStmt): boolean => + NUMERIC_STATEMENTS.has(s.kind) && everyStmtChild(s, expr, stmt); + return stmt(loop); +} + +/** Number parameters of `fn` that bound one of its numeric loops: read in + * the loop's condition or in its `for` initializer. */ +function loopBounds(fn: IrFunction, functions: ReadonlyMap): Set { + const params = new Set( + fn.params.filter((param) => param.type.kind === "f64").map((param) => param.localId), + ); + const result = new Set(); + if (params.size === 0) return result; + const reads = (e: IrExpr): boolean => { + if (e.kind === "varRef" && params.has(e.localId)) result.add(e.localId); + return everyExprChild(e, reads, readsStmt); + }; + const readsStmt = (s: IrStmt): boolean => everyStmtChild(s, reads, readsStmt); + everyStmtList(fn.body, { + stmt: (s) => { + if (s.kind !== "for" && s.kind !== "while" && s.kind !== "doWhile") return true; + if (!numericLoop(s, functions)) return true; + if (s.cond) reads(s.cond); + if (s.kind === "for" && s.init) readsStmt(s.init); + return true; + }, + expr: () => true, + }); + return result; +} + +/** Versions of functions whose number parameters bound their loops. + * + * A parameter that every call site proves an int32 already reaches the + * function's i32 entry (int32-abi.ts). One that some caller passes as an + * unproven number (a value read from a message, a field, a parse) keeps + * its double, so the loops it bounds compare a counter with a double and + * the values derived from it stay doubles. + * + * A parameter qualifies when it bounds a numeric loop (`numericLoop`). + * Such a function gets a copy of its body under a new name, and its own + * body starts by calling the copy when every such parameter holds an + * int32: `(p | 0) === p` (an integer in the int32 range, not NaN) and not + * -0 (`1 / p` is not -Infinity). The copy then receives `p | 0`, which is + * the same double, so it computes what the body would; its only caller + * proves the parameter an int32, so it gets the i32 entry. Other values + * run the body as before. + * + * Only plain synchronous functions qualify: not class members, no + * captures, not the module entry, not dynamic-tier, at most + * MAX_FUNCTION_NODES nodes, not calling themselves, and not holding the + * only call site of another function (`keepsSingleBody`). The caller + * skips modules that observe call stacks, where the copy's frame could be + * seen. */ +export function versionLoopBoundParameters( + mod: IrModule, + symbolName: (name: string) => string = (name) => name, +): IrModule { + const candidates: { fn: IrFunction; params: Set }[] = []; + const sites = callSites(mod); + const functions = new Map(mod.functions.map((fn) => [fn.name, fn])); + const members = new Set((mod.classes ?? []).flatMap((cls) => cls.methods ?? [])); + for (const fn of mod.functions) { + if ( + // Constructors, methods and accessors (`%C.m`) and generated helper + // families (`%arr.map_…`) keep their single body. + fn.name.includes(".") || + members.has(fn.name) || + fn.async || + fn.generator || + fn.captures !== undefined || + fn.classCaptures?.length || + fn.tier === "dynamic" || + fn.name === mod.entry + ) + continue; + const params = loopBounds(fn, functions); + const locals = new Map(fn.locals.map((local) => [local.id, local])); + for (const id of params) { + const local = locals.get(id); + if (!local || local.boxed || local.tdz) params.delete(id); + } + if (params.size === 0 || size(fn) > MAX_FUNCTION_NODES || keepsSingleBody(fn, sites, functions)) + continue; + candidates.push({ fn, params }); + } + if (candidates.length === 0) return mod; + const slots = analyzeInt32Slots(mod); + const names = new Set(mod.functions.map((fn) => symbolName(fn.name))); + const replaced = new Map(); + const added: IrFunction[] = []; + for (const { fn, params } of candidates) { + if (added.length >= MAX_VERSIONS) break; + const unproven = fn.params.filter( + (param) => params.has(param.localId) && !slots.hasParam(fn.name, param.localId), + ); + if (unproven.length === 0) continue; + let name = `${fn.name}.int32Params`; + while (names.has(symbolName(name))) name += "_"; + names.add(symbolName(name)); + const { ownsPrototype: _ownsPrototype, ...implementation } = fn; + added.push({ ...implementation, name, body: fn.body.map(reparseStmt) }); + const loc = fn.body[0]?.loc ?? { file: mod.sourceFile, start: 0, end: 0 }; + const read = (localId: string): IrExpr => ({ kind: "varRef", localId, type: F64, loc }); + const int32 = (localId: string): IrExpr => ({ + kind: "bin", + op: "|", + left: read(localId), + right: { kind: "numLit", value: 0, type: F64, loc }, + type: F64, + loc, + }); + let test: IrExpr | null = null; + for (const param of unproven) { + const exact: IrExpr = { + kind: "bin", + op: "===", + left: int32(param.localId), + right: read(param.localId), + type: BOOL, + loc, + }; + const notNegativeZero: IrExpr = { + kind: "bin", + op: "!==", + left: { + kind: "bin", + op: "/", + left: { kind: "numLit", value: 1, type: F64, loc }, + right: read(param.localId), + type: F64, + loc, + }, + right: { kind: "numLit", value: -Infinity, type: F64, loc }, + type: BOOL, + loc, + }; + const both: IrExpr = { + kind: "logical", + op: "&&", + left: exact, + right: notNegativeZero, + type: BOOL, + loc, + }; + test = + test === null + ? both + : { kind: "logical", op: "&&", left: test, right: both, type: BOOL, loc }; + } + const versioned = new Set(unproven.map((param) => param.localId)); + const call: IrExpr = { + kind: "call", + callee: name, + args: fn.params.map((param) => + versioned.has(param.localId) + ? int32(param.localId) + : { kind: "varRef", localId: param.localId, type: param.type, loc }, + ), + type: fn.returnType, + loc, + }; + const run: IrStmt[] = + fn.returnType.kind === "void" + ? [ + { kind: "exprStmt", expr: call, loc }, + { kind: "return", value: null, loc }, + ] + : [{ kind: "return", value: call, loc }]; + replaced.set(fn, { + ...fn, + body: [{ kind: "if", cond: test!, then: run, else_: null, loc }, ...fn.body], + }); + } + if (added.length === 0) return mod; + return { + ...mod, + functions: [...mod.functions.map((fn) => replaced.get(fn) ?? fn), ...added], + }; +} diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 5b984f318..819cfbee0 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -218,6 +218,17 @@ const STREAM_LIB_FNS: ReadonlySet = new Set([ "child.stdioStream", "stream.chunkDone", "stream.objectMode", + "stream.decodeStrings", + "stream.duplexFromGen", + "stream.duplexFromDyn", + "stream.duplexFromPromise", + "stream.duplexFromAsyncFn", + "stream.duplexFromGenSignal", + "stream.duplexFromAsyncFnSignal", + "fetch.readableFromWeb", + "stream.duplexifyReadable", + "stream.duplexifyWritable", + "stream.duplexifyPair", "console.bind", "crypto.hashStream", "crypto.hashStreamPiped", @@ -230,6 +241,7 @@ const STREAM_LIB_FNS: ReadonlySet = new Set([ "sp.finished", "sp.pipeline", "sp.pipelineStages", + "sp.pipelineStagesOpts", "stream.addAbortSignal", "stream.addAbortSignalFn", "stream.destroy", @@ -257,6 +269,7 @@ const STREAM_LIB_PREFIXES: readonly string[] = [ "sp.pipelineOpts", "stream.finishedOpts", "stream.pipelineStages", + "stream.composeStages", "stream.set", "transform.", "writable.", @@ -270,6 +283,12 @@ const READABLE_DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ "readable.fromDone", "readable.fromDyn", "readable.fromIter", + "stream.duplexFromGen", + "stream.duplexFromDyn", + "stream.duplexFromPromise", + "stream.duplexFromAsyncFn", + "stream.duplexFromGenSignal", + "stream.duplexFromAsyncFnSignal", ]); /** libCall name prefixes of the Readable entry points that await through the dyn-async unit. */ @@ -605,14 +624,23 @@ const STDIO_EXTENDED_NAMES: ReadonlySet = new Set(["setEncoding", "resiz /** The libCall nodes the rules below test. */ type LibCallNode = IrExpr & { kind: "libCall" }; +/** A condition over several nodes of the module (a WebCrypto call and an + * algorithm name): the scan's one walk shows it every expression, and the + * feature is set when `result` answers true after the walk. */ +interface RuleScan { + expr(node: IrExpr): void; + result(): boolean; +} + /** What makes a program need one optional runtime unit. The feature is set * when any condition holds: `initial` from the module's declarations, a * libCall named in `fns` or starting with one of `prefixes`, a libCall - * `libCall` accepts, another expression node `expr` accepts, or a type - * slot of one of the `types` kinds. A libCall or type that needs several - * units appears in each unit's rule. */ + * `libCall` accepts, another expression node `expr` accepts, a type slot of + * one of the `types` kinds, or a `scan` over the whole module. A libCall or + * type that needs several units appears in each unit's rule. */ interface RuntimeFeatureRule { initial?: (mod: IrModule) => boolean; + scan?: () => RuleScan; fns?: readonly IrLibFn[]; prefixes?: readonly string[]; libCall?: (node: LibCallNode) => boolean; @@ -626,6 +654,16 @@ const rule = (r: RuntimeFeatureRule): RuntimeFeatureRule => r; /* The HTTP, HTTPS, HTTP/2 and TLS surfaces, which link the net unit. */ const NET_PREFIXES: readonly string[] = ["net.", "http.", "tls.", "https.", "http2."]; +/** The scr_dh.c entry points (the dh unit). */ +export const DH_LIB_FNS: ReadonlySet = new Set([ + "crypto.dhNew", + "crypto.dhGroupNew", + "crypto.checkPrimeSync", + "crypto.checkPrimeCb", + "crypto.generatePrimeSync", + "crypto.generatePrimeCb", +]); + /** The scr_pki.c entry points (the pki unit). */ export const PKI_LIB_FNS: ReadonlySet = new Set([ "crypto.createPrivateKey", @@ -650,17 +688,32 @@ const WEBCRYPTO_ASYMMETRIC_NAMES: ReadonlySet = new Set([ /** Whether a string literal in the module names Ed25519, without regard * to case ("ed25519" for node:crypto, "Ed25519" for WebCrypto): the * Ed25519 unit links then, alongside the pki unit. */ -export function moduleNamesEd25519(mod: IrModule): boolean { +function ed25519NameScan(): RuleScan { let named = false; - everyModuleNode(mod, { + return { expr: (node) => { - if (node.kind === "strLit" && node.value.toLowerCase() === "ed25519") named = true; - return !named; + if (!named && node.kind === "strLit" && node.value.toLowerCase() === "ed25519") named = true; }, - stmt: () => !named, - type: () => true, - }); - return named; + result: () => named, + }; +} + +/** moduleUsesWebCryptoAsymmetric's condition as a runtime-feature scan. */ +function webCryptoAsymmetricScan(): RuleScan { + let webcrypto = false; + let named = false; + return { + expr: (node) => { + if (node.kind === "libCall" && node.fn.startsWith("webcrypto.")) webcrypto = true; + else if ( + !named && + node.kind === "strLit" && + WEBCRYPTO_ASYMMETRIC_NAMES.has(node.value.toLowerCase()) + ) + named = true; + }, + result: () => webcrypto && named, + }; } /** Whether the module calls WebCrypto and a string literal in it names one @@ -805,6 +858,10 @@ const RUNTIME_FEATURE_RULES = { }), /** Own properties JavaScript adds to children (scr_dyn_handle_props.c). */ handleProps: rule({ fns: ["child.propsInstall"] }), + /** The declaration-typed boundary's adoption (scr_decl_adopt.c): its + * casts, and closures boxed in a boundary program, which adopt their + * arguments. */ + declAdopt: rule({ fns: ["dyn.declAdopt"], initial: (mod) => mod.declBoundary === true }), /** for await over checked-dynamic values (scr_dyn_for_await.c). */ dynForAwait: rule({ fns: ["dyn.asyncIteratorOpen", "dyn.asyncIteratorWasSync"] }), /** Buffer.prototype methods called on checked-dynamic values @@ -820,9 +877,10 @@ const RUNTIME_FEATURE_RULES = { * process-warning unit, and unknown encodings and formats render through * the inspect unit. */ ecdh: rule({ fns: ["crypto.ecdhNew", "crypto.ecdhConvertKey"] }), - /** scr_dh.c over the mbedTLS archive; unknown encodings render through + /** scr_dh.c over the mbedTLS archive, also for checkPrime and + * generatePrime; unknown encodings and argument values render through * the inspect unit. */ - dh: rule({ fns: ["crypto.dhNew", "crypto.dhGroupNew"] }), + dh: rule({ fns: [...DH_LIB_FNS] }), emitter: rule({ initial: (mod) => mod.workers === true || (mod.classes ?? []).some((c) => c.name === RUNTIME_EMITTER_CLASS), @@ -872,8 +930,7 @@ const RUNTIME_FEATURE_RULES = { "webcrypto.unwrapKey", "crypto.ecdhNew", "crypto.ecdhConvertKey", - "crypto.dhNew", - "crypto.dhGroupNew", + ...DH_LIB_FNS, ...BUILTIN_CLASS_LIB_FNS, ...UTIL_CALLABLE_LIB_FNS, ], @@ -906,10 +963,10 @@ const RUNTIME_FEATURE_RULES = { perfLoop: rule({ fns: ["perf.idleTime"], prefixes: ["perf.loop", "perf.eld"] }), /** scr_pki.c over the mbedTLS archive: asymmetric KeyObjects and * signatures. */ - pki: rule({ initial: moduleUsesWebCryptoAsymmetric, fns: [...PKI_LIB_FNS] }), + pki: rule({ scan: webCryptoAsymmetricScan, fns: [...PKI_LIB_FNS] }), /** scr_ed25519.c (the vendored libsodium Ed25519), linked with pki: a * string literal in the module names Ed25519. */ - ed25519: rule({ initial: moduleNamesEd25519 }), + ed25519: rule({ scan: ed25519NameScan }), processEvents: rule({ fns: [...PROCESS_EVENT_LIB_FNS, "util.styleText"], types: ["procStream"] }), /** scr_url_legacy.c parses and stringifies queries. */ qs: rule({ fns: [...QS_LIB_FNS, ...LEGACY_URL_LIB_FNS] }), @@ -1028,6 +1085,7 @@ function noRuntimeFeatures(): RuntimeFeatures { stream: false, streamDyn: false, handleProps: false, + declAdopt: false, dynForAwait: false, dynBuffer: false, builtinValues: false, @@ -1070,9 +1128,11 @@ for (const name of RUNTIME_FEATURE_NAMES) { function scanRuntimeFeatures(mod: IrModule, stopAt?: RuntimeFeatureName): RuntimeFeatures { const features = noRuntimeFeatures(); + const scans: { name: RuntimeFeatureName; scan: RuleScan }[] = []; for (const name of RUNTIME_FEATURE_NAMES) { - const initial = ruleOf(name).initial; + const { initial, scan } = ruleOf(name); if (initial && initial(mod)) features[name] = true; + if (scan) scans.push({ name, scan: scan() }); } const keepGoing = (): boolean => stopAt === undefined || !features[stopAt]; let tables: TypeTables | undefined; @@ -1083,6 +1143,7 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: RuntimeFeatureName): Runtim classes: new Map((mod.classes ?? []).map((c) => [c.name, c])), }); const expr = (node: IrExpr): boolean => { + for (let i = 0; i < scans.length; i++) scans[i]!.scan.expr(node); if (node.kind === "libCall") { const fn = node.fn; for (const name of FEATURES_BY_FN.get(fn) ?? []) features[name] = true; @@ -1101,6 +1162,7 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: RuntimeFeatureName): Runtim return keepGoing(); }; if (keepGoing()) everyModuleNode(mod, { expr, stmt: keepGoing, type }); + for (const { name, scan } of scans) if (scan.result()) features[name] = true; // Inline-cache sites of dynamic-tier functions use the tier's shapes // (scr_js_object.c, with the SCR_JS_TIER build of scr_json.c). if (!features.dynTier && mod.functions.some((fn) => fn.tier === "dynamic")) { diff --git a/packages/compiler/src/ir/serialize.test.ts b/packages/compiler/src/ir/serialize.test.ts index 8345ab10a..c24e1f198 100644 --- a/packages/compiler/src/ir/serialize.test.ts +++ b/packages/compiler/src/ir/serialize.test.ts @@ -198,3 +198,24 @@ test("byte decoding keeps the whole-document errors and version refusal", () => const sentinel = text.replace('"nan"', '"future"'); expect(() => deserializeModuleBytes(Buffer.from(sentinel))).toThrow("Invalid IR number sentinel"); }); + +test("file roots respell the source locations under them and nothing else", () => { + const mod = numbers([1, 2]); + const shim = { file: '/opt/scriptc "a"/shims/pkg/index.ts', start: 3, end: 4 }; + const other = { file: "/opt/elsewhere/shims/pkg/index.ts", start: 5, end: 6 }; + const body = mod.functions[0]!.body as IrStmt[]; + body.push( + { kind: "exprStmt", expr: { kind: "numLit", value: 3, type: F64, loc: shim }, loc: shim }, + { kind: "exprStmt", expr: { kind: "numLit", value: 4, type: F64, loc: other }, loc: other }, + ); + const roots = [{ from: '/opt/scriptc "a"/shims/', to: "scriptc:shims/" }]; + const relocated = deserializeModule(serializeModuleParts(mod, roots).join("")); + const files = (relocated.functions[0]!.body as IrStmt[]).map((stmt) => stmt.loc.file); + expect(files).toEqual([ + "numbers.ts", + "numbers.ts", + "scriptc:shims/pkg/index.ts", + "/opt/elsewhere/shims/pkg/index.ts", + ]); + expect(serializeModuleParts(mod).join("")).toBe(serializeModule(mod, true)); +}); diff --git a/packages/compiler/src/ir/serialize.ts b/packages/compiler/src/ir/serialize.ts index ca5eb955e..314f258d6 100644 --- a/packages/compiler/src/ir/serialize.ts +++ b/packages/compiler/src/ir/serialize.ts @@ -46,11 +46,41 @@ function encodeFunction(fn: IrFunction): string | undefined { return ordinaryNumbers(fn) ? JSON.stringify(fn) : JSON.stringify(fn, encodeNumber); } +/** A directory whose source location files an IR artifact names by a + * prefix of its own: a location file `from + rest` serializes as + * `to + rest`. */ +export interface SerializedFileRoot { + from: string; + to: string; +} + +/** Respells the source location files under `roots` in serialized parts. + * A location's file is the first member of its JSON object, and the + * escaped spelling of a path prefix is the prefix of the escaped path. */ +function relocateFiles(parts: string[], roots: readonly SerializedFileRoot[]): string[] { + const needles = roots.map(({ from, to }) => ({ + from: `"file":${JSON.stringify(from).slice(0, -1)}`, + to: `"file":${JSON.stringify(to).slice(0, -1)}`, + })); + return parts.map((part) => + needles.reduce( + (text, { from, to }) => (text.includes(from) ? text.replaceAll(from, to) : text), + part, + ), + ); +} + /** The compact encoding as ordered parts: the module header up to the * function array, one part per function (with separate comma parts), and the * header's remainder. Concatenating the parts yields exactly - * `serializeModule(mod, true)`; writers stream them to disk in batches. */ -export function serializeModuleParts(mod: IrModule): string[] { + * `serializeModule(mod, true)`; writers stream them to disk in batches. + * `fileRoots` respells the source locations in those directories, so + * artifacts do not depend on where the compiler that wrote them is + * installed. */ +export function serializeModuleParts( + mod: IrModule, + fileRoots: readonly SerializedFileRoot[] = [], +): string[] { // A replacer reads through live native views. Serializing the complete // function array through one view refreshes every function capsule for // each element, making large compiler artifacts quadratic. The sentinel @@ -65,7 +95,7 @@ export function serializeModuleParts(mod: IrModule): string[] { parts.push(encodeFunction(fn) ?? "null"); } parts.push("]" + header.slice(offset + slot.length)); - return parts; + return fileRoots.length === 0 ? parts : relocateFiles(parts, fileRoots); } /** Compiler artifacts use compact JSON; API consumers can retain the diff --git a/packages/compiler/src/ir/traverse.ts b/packages/compiler/src/ir/traverse.ts index 11b20a210..251904351 100644 --- a/packages/compiler/src/ir/traverse.ts +++ b/packages/compiler/src/ir/traverse.ts @@ -109,8 +109,15 @@ export interface IrModuleVisitor extends IrVisitor { * source text, locations, labels, or other metadata. A false predicate stops * the whole walk. Results are never retained across mutable compiler passes. */ export function everyModuleNode(mod: IrModule, visitor: IrModuleVisitor): boolean { - const type = (node: IrType, loc: SrcLoc): boolean => - visitor.type(node, loc) && everyTypeChild(node, (child) => type(child, loc)); + // Every node of one type tree reports the tree's location: one shared + // child callback serves them all. + let typeLoc: SrcLoc; + const typeNode = (node: IrType): boolean => + visitor.type(node, typeLoc) && everyTypeChild(node, typeNode); + const type = (node: IrType, loc: SrcLoc): boolean => { + typeLoc = loc; + return typeNode(node); + }; const expr = (node: IrExpr): boolean => visitor.expr(node) && everyExprChild(node, expr, stmt) && type(node.type, node.loc); const stmt = (node: IrStmt): boolean => visitor.stmt(node) && everyStmtChild(node, expr, stmt); diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index bb36c7007..02825f0de 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -18,6 +18,7 @@ import type { } from "./ir.js"; import { arrayOf, + iterableArmsElem, BYTES_ELEMENT_NAME, BOOL, BYTES_U8, @@ -61,6 +62,7 @@ import { unionWideningTags } from "./analysis.js"; import { alwaysReturns } from "./control-flow.js"; import { dynamicOperationOf, functionTier } from "./tiers.js"; import { LIB_FN_SIGS, pipelinePumpedRuns } from "./builtin-signatures.js"; +import { ALWAYS_THROW_LIB_FNS } from "./builtin-effects.js"; /** The promisified crypto calls' value kinds besides checked-dynamic. */ const CRYPTO_PROMISE_VALUES: Readonly> = { @@ -118,6 +120,8 @@ export function validateModule(mod: IrModule): IrValidationError[] { const moduleLoc: SrcLoc = { file: mod.sourceFile, start: 0, end: 0 }; if (mod.workers !== undefined && typeof mod.workers !== "boolean") errors.push({ message: "worker execution mode must be a boolean", loc: moduleLoc }); + if (mod.declBoundary !== undefined && mod.declBoundary !== true) + errors.push({ message: "declBoundary must be true when present", loc: moduleLoc }); if (mod.lifetimeRegions !== undefined && mod.lifetimeRegions !== true) errors.push({ message: "lifetimeRegions must be true when present", loc: moduleLoc }); if (mod.workers && mod.lib !== undefined) @@ -800,7 +804,7 @@ export function validateModule(mod: IrModule): IrValidationError[] { arm.kind === "union" || arm.kind === "dyn" || arm.kind === "date" || - arm.kind === "generator" + (arm.kind === "generator" && iterableArmsElem(u.arms) === null) ) { errors.push({ message: `union ${u.id}: arm ${i} is ${arm.kind}`, loc: noLoc }); } @@ -4101,8 +4105,10 @@ function validateFunction( e.fn === "stream.pipelineDyn" || e.fn === "stream.compose" || e.fn === "stream.pipelineStages" || + e.fn === "stream.composeStages" || e.fn === "stream.pipelineStagesDyn" || e.fn === "sp.pipelineStages" || + e.fn === "sp.pipelineStagesOpts" || e.fn === "sp.pipeline" || e.fn === "sp.pipelineOpts" || e.fn === "sp.pipelineOptsSignal" || @@ -4140,6 +4146,9 @@ function validateFunction( e.fn === "error.assign" || e.fn === "error.hasKey" || e.fn === "error.hasCode" || + e.fn === "error.ownAccessor" || + e.fn === "error.accessorGet" || + e.fn === "error.accessorSet" || e.fn === "error.defineCause" || e.fn === "error.deleteCause" ) { @@ -4263,7 +4272,11 @@ function validateFunction( if (!ok) err(`libCall dyn.objEntryPairs must return a [string, any] tuple array`, e.loc); return true; } - if (e.fn === "array.namedKeys" || e.fn === "array.hasNamedKey") { + if ( + e.fn === "array.namedKeys" || + e.fn === "array.hasNamedKey" || + e.fn === "array.hasProperties" + ) { if (e.args[0]?.type.kind !== "array") { err(`libCall ${e.fn} arg 0: expected an array, got ${e.args[0]?.type.kind}`, e.loc); } @@ -4286,6 +4299,22 @@ function validateFunction( } return true; } + if (e.fn === "fetch.readableFromWeb") { + // Readable.fromWeb: (stream, objectMode, highWaterMark, encoding) → a Readable. + if ( + e.args.length !== 4 || + e.args[0]?.type.kind !== "dyn" || + e.args[1]?.type.kind !== "bool" || + e.args[2]?.type.kind !== "f64" || + e.args[3]?.type.kind !== "string" || + e.type.kind !== "object" + ) + err( + `libCall ${e.fn} must take (stream, objectMode, highWaterMark, encoding) and return a Readable`, + e.loc, + ); + return true; + } if (e.fn === "fetch.readerRead") { if (e.type.kind !== "promise" || e.type.inner.kind !== "record") { err(`libCall fetch.readerRead must return a promise of a read-result record`, e.loc); @@ -5345,32 +5374,8 @@ function validateFunction( const callback = e.args[e.args.length - 1]?.type; if (callback?.kind !== "func") err(`libCall ${e.fn} callback must be a function`, e.loc); } - if (e.fn === "global.undefRead") { - // Always throws — the result type is whatever the declared type - // of the undefined global mapped to (never materialized). - return true; - } - if ( - e.fn === "error.nodeThrow" || - e.fn === "error.argTypeThrow" || - e.fn === "error.propTypeThrow" || - e.fn === "fs.mkdtempChk" || - e.fn === "fs.readFileChk" || - e.fn === "fs.opendirChk" || - e.fn === "fs.watchFileChk" || - e.fn === "fs.lchmodChk" || - e.fn === "fs.readChk" || - e.fn === "fs.streamOptsChk" || - e.fn === "net.connectOptsChk" || - e.fn === "tls.caCertsChk" - ) { - // Always throws — the result type is the replaced expression's - // own (never materialized; the global.undefRead pattern). The - // fs Chk ladders qualify: every validation failure throws - // Node's typed error, and a full pass throws the trailing - // compiler-rendered fence. - return true; - } + // Always throws: the result type is never materialized. + if (ALWAYS_THROW_LIB_FNS.has(e.fn)) return true; if (e.fn === "error.new" || e.fn === "error.newOptions") { // Which builtin the runtime constructs is named by the result type. if (!isBuiltinErrorObject(e.type)) { @@ -5494,6 +5499,19 @@ function validateFunction( e.fn === "stream.destroy" || e.fn === "stream.destroyErr" || e.fn === "stream.objectMode" || + e.fn === "stream.decodeStrings" || + e.fn === "stream.duplexFromGen" || + e.fn === "stream.duplexFromDyn" || + e.fn === "stream.duplexifyReadable" || + e.fn === "stream.duplexifyWritable" || + e.fn === "stream.duplexifyPair" || + e.fn === "stream.duplexFromPromise" || + e.fn === "stream.duplexFromAsyncFn" || + e.fn === "stream.duplexFromGenSignal" || + e.fn === "stream.duplexFromAsyncFnSignal" || + e.fn === "stream.setConstruct" || + e.fn === "stream.setWritev" || + e.fn === "stream.setWritevDefault" || e.fn === "stream.iteratorClose" || e.fn === "stream.prop" || e.fn === "stream.errored" || @@ -5503,8 +5521,10 @@ function validateFunction( e.fn === "stream.pipelineDyn" || e.fn === "stream.compose" || e.fn === "stream.pipelineStages" || + e.fn === "stream.composeStages" || e.fn === "stream.pipelineStagesDyn" || e.fn === "sp.pipelineStages" || + e.fn === "sp.pipelineStagesOpts" || e.fn === "stream.addAbortSignal" || e.fn === "stream.addAbortSignalFn" || e.fn === "stream.finishedOpts" || @@ -5544,6 +5564,38 @@ function validateFunction( } return true; } + if (e.fn === "stream.duplexFromPromise") { + // Duplex.from(promise): (promise, box) → a Duplex. + if (e.args[0]?.type.kind !== "promise" || e.args[1]?.type.kind !== "func") + err(`libCall ${e.fn} takes a promise and its box helper`, e.loc); + if (!isStreamObject(e.type)) err(`libCall ${e.fn} must return a stream class`, e.loc); + return true; + } + if ( + e.fn === "stream.duplexifyReadable" || + e.fn === "stream.duplexifyWritable" || + e.fn === "stream.duplexifyPair" + ) { + // Duplex.from over streams: (r), (w) or (r, w) → a Duplex. + const want = e.fn === "stream.duplexifyPair" ? 2 : 1; + if (e.args.length !== want || !e.args.every((a) => isStreamObject(a.type))) + err(`libCall ${e.fn} takes ${want} stream argument(s)`, e.loc); + if (!isStreamObject(e.type)) err(`libCall ${e.fn} must return a stream class`, e.loc); + return true; + } + if ( + e.fn === "stream.duplexFromGen" || + e.fn === "stream.duplexFromDyn" || + e.fn === "stream.duplexFromAsyncFn" || + e.fn === "stream.duplexFromGenSignal" || + e.fn === "stream.duplexFromAsyncFnSignal" + ) { + // Duplex.from: (stage, source, helpers...) or (body) → a Duplex. + if (!isStreamObject(e.type)) { + err(`libCall ${e.fn} must return a stream class, got ${e.type.kind}`, e.loc); + } + return true; + } if (e.fn.endsWith(".newDyn")) { // The dyn-options constructor: (optsDyn) → the stream class. if (!isStreamObject(e.type)) { @@ -5614,13 +5666,15 @@ function validateFunction( if ( e.fn === "stream.pipelineStages" || e.fn === "stream.pipelineStagesDyn" || - e.fn === "sp.pipelineStages" + e.fn === "sp.pipelineStages" || + e.fn === "sp.pipelineStagesOpts" || + e.fn === "stream.composeStages" ) { // (n, k, positions, flags, mask, s1..sn, stage1..stagek, - // helpers..., box?, (signal, abort)?[, cb]): positions bit j marks - // stage j a function, flags bits 1-2 the destination, bit 5 the - // signal group; helpers are three per function run a stream - // follows. + // helpers..., box?, (signal, abort)?[, cb | end, signal?]): + // positions bit j marks stage j a function, flags bits 1-2 the + // destination, bit 5 the signal group, bit 6 the options' signal; + // helpers are three per pumped function run. const [n, k, positions, flags] = e.args .slice(0, 4) .map((a) => (a?.kind === "numLit" ? a.value : -1)); @@ -5631,9 +5685,14 @@ function validateFunction( if (Math.floor(positions! / 2 ** j) % 2 === 1) functions++; } const lastIsFunction = Math.floor(positions! / 2 ** (total - 1)) % 2 === 1; - const helperCount = 3 * pipelinePumpedRuns(total, positions!); + const helperCount = 3 * pipelinePumpedRuns(total, positions!, flags!); const groups = helperCount + (destKind === 2 ? 1 : 0) + (flags! & 32 ? 2 : 0); - const cbArgs = e.fn === "sp.pipelineStages" ? 0 : 1; + const cbArgs = + e.fn === "sp.pipelineStagesOpts" + ? 1 + (flags! & 64 ? 1 : 0) + : e.fn === "sp.pipelineStages" || e.fn === "stream.composeStages" + ? 0 + : 1; if ( flags! < 0 || n! < 0 || @@ -5642,7 +5701,8 @@ function validateFunction( total > 52 || positions! >= 2 ** total || functions !== k || - (destKind === 2) !== lastIsFunction || + destKind === 3 || + (destKind !== 0) !== lastIsFunction || total < 2 || e.args.length !== 5 + n! + k! + groups + cbArgs ) { @@ -5670,12 +5730,19 @@ function validateFunction( if (e.args[at2]?.type.kind !== "dyn" || e.args[at2 + 1]?.type.kind !== "func") err(`libCall ${e.fn} signal and abort must be a dyn and a func`, e.loc); } - if (cbArgs) { + if (e.fn === "sp.pipelineStagesOpts") { + const tail = e.args.slice(e.args.length - cbArgs); + if (tail[0]?.type.kind !== "bool" || (cbArgs > 1 && tail[1]?.type.kind !== "dyn")) + err(`libCall ${e.fn} end and signal must be a bool and a dyn`, e.loc); + if (e.type.kind !== "promise") err(`libCall ${e.fn} must return a promise`, e.loc); + } else if (cbArgs) { const cbK = e.args[e.args.length - 1]?.type.kind; if (e.fn === "stream.pipelineStages" ? cbK !== "func" : cbK !== "dyn") { err(`libCall ${e.fn} callback has the wrong kind`, e.loc); } if (!isStreamObject(e.type)) err(`libCall ${e.fn} must return a stream`, e.loc); + } else if (e.fn === "stream.composeStages") { + if (!isStreamObject(e.type)) err(`libCall ${e.fn} must return a stream`, e.loc); } else if (e.type.kind !== "promise") { err(`libCall ${e.fn} must return a promise`, e.loc); } @@ -5710,6 +5777,8 @@ function validateFunction( e.fn === "readable.opMap" || e.fn === "readable.opFilter" || e.fn === "readable.opFlatMap" || + e.fn === "readable.opFlatMapAsyncGen" || + e.fn === "readable.opFlatMapSyncGen" || e.fn === "readable.opDrop" || e.fn === "readable.opTake" ) { @@ -5718,13 +5787,30 @@ function validateFunction( } return true; } - if (e.fn === "readable.opReduce") { + if (e.fn === "readable.opReduce" || e.fn === "readable.opReduceSignal") { const inner = e.type.kind === "promise" ? e.type.inner : null; const kind = e.args[5]?.kind === "numLit" ? e.args[5].value : -1; const want = [DYN, F64, BOOL, STRING][kind]; if (!isStreamObject(e.args[0]?.type) || !inner || !want || !typeEquals(inner, want)) { err(`libCall readable.opReduce must return the promise its kind names`, e.loc); } + if (e.fn === "readable.opReduceSignal" && e.args[6]?.type.kind !== "func") + err(`libCall ${e.fn} takes ac.abort() as a closure`, e.loc); + return true; + } + if (e.fn === "readable.opOnceOptions") { + if (e.args.length !== 0 || e.type.kind !== "dyn") + err(`libCall ${e.fn} takes no arguments`, e.loc); + return true; + } + if ( + e.fn === "readable.opSignalList" || + e.fn === "readable.opSignalOpts" || + e.fn === "readable.opSignalOf" + ) { + // The operator callbacks' { signal }: (dyn) → dyn. + if (e.args.length !== 1 || e.args[0]?.type.kind !== "dyn" || e.type.kind !== "dyn") + err(`libCall ${e.fn} takes and returns a checked-dynamic value`, e.loc); return true; } if (e.fn === "readable.fromDone") { @@ -5785,6 +5871,9 @@ function validateFunction( e.fn === "readable.setEncoding" || e.fn === "readable.pushEncoding" || e.fn === "stream.objectMode" || + e.fn === "stream.decodeStrings" || + e.fn === "writable.writeEncoding" || + e.fn === "writable.setDefaultEncoding" || e.fn === "stream.destroy" || e.fn === "stream.destroyErr" || e.fn === "stream.iteratorClose" diff --git a/packages/compiler/src/library/ir-policy.ts b/packages/compiler/src/library/ir-policy.ts index fb0575121..e8fad6dd1 100644 --- a/packages/compiler/src/library/ir-policy.ts +++ b/packages/compiler/src/library/ir-policy.ts @@ -54,6 +54,8 @@ const LIB_MODE_REFUSED_PREFIXES: readonly [string, string][] = [ ["crypto.ecdh", "crypto.createECDH and ECDH.convertKey"], ["crypto.dhNew", "crypto.createDiffieHellman"], ["crypto.dhGroupNew", "crypto.getDiffieHellman and createDiffieHellmanGroup"], + ["crypto.checkPrime", "crypto.checkPrime and checkPrimeSync"], + ["crypto.generatePrime", "crypto.generatePrime and generatePrimeSync"], ["crypto.createPrivateKey", "asymmetric KeyObjects, generateKeyPair, sign and verify"], ["crypto.createPublicKey", "asymmetric KeyObjects, generateKeyPair, sign and verify"], ["crypto.createSign", "asymmetric KeyObjects, generateKeyPair, sign and verify"], diff --git a/packages/compiler/src/native/cache.test.ts b/packages/compiler/src/native/cache.test.ts index 3ffdf2335..f4910ff23 100644 --- a/packages/compiler/src/native/cache.test.ts +++ b/packages/compiler/src/native/cache.test.ts @@ -11,7 +11,7 @@ import { import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, expect, test, vi } from "vitest"; -import { contentDigest, NativeCache, openNativeCache } from "./cache.js"; +import { contentDigest, fileDigest, NativeCache, openNativeCache } from "./cache.js"; const directories: string[] = []; function directory(): string { @@ -19,11 +19,24 @@ function directory(): string { directories.push(path); return path; } +function* generate(parts: readonly string[]): Generator { + yield* parts; +} afterEach(() => { vi.unstubAllEnvs(); for (const path of directories.splice(0)) rmSync(path, { recursive: true, force: true }); }); +test("a file's digest read in blocks is the digest of its bytes", () => { + const path = join(directory(), "module.ll"); + const bytes = Buffer.alloc(3 * 1024 * 1024 + 17); + for (let index = 0; index < bytes.length; index++) bytes[index] = (index * 31) & 0xff; + writeFileSync(path, bytes); + expect(fileDigest(path)).toBe(contentDigest(bytes)); + writeFileSync(path, ""); + expect(fileDigest(path)).toBe(contentDigest("")); +}); + test("cache hits require an intact payload and digest", () => { const cache = new NativeCache(directory()); const key = contentDigest("program identity"); @@ -42,7 +55,8 @@ test("text parts publish one digest-checked entry and oversized reads are misses const cache = new NativeCache(directory()); const key = contentDigest("parts identity"); const parts = ['{"a":', '"\u00e9é😀"', ",", "", '"b":1}']; - cache.writeParts("frontend", key, parts); + // Parts are generated while they are written, and hashed in the same pass. + cache.writeParts("frontend", key, generate(parts)); const bytes = cache.read("frontend", key); expect(bytes?.toString("utf8")).toBe(parts.join("")); expect(readFileSync(join(cache.root, "frontend", key + ".sha256"), "utf8").trim()).toBe( diff --git a/packages/compiler/src/native/cache.ts b/packages/compiler/src/native/cache.ts index f81311f52..3c9bdf411 100644 --- a/packages/compiler/src/native/cache.ts +++ b/packages/compiler/src/native/cache.ts @@ -1,8 +1,11 @@ import { createHash } from "node:crypto"; import { + closeSync, mkdirSync, mkdtempSync, + openSync, readFileSync, + readSync, readdirSync, renameSync, rmSync, @@ -11,7 +14,7 @@ import { } from "node:fs"; import { join } from "node:path"; import { resolveBuildCacheRoot } from "../backend/cache-root.js"; -import { writeTextPartsSync } from "./text-file.js"; +import { joinedParts, writeTextGeneratedSync } from "./text-file.js"; /** UTF-8 bytes one native compiler string can hold (the runtime's * SCR_STR_MAX_CAP). Exceeding it traps rather than throwing, so payloads @@ -25,6 +28,30 @@ export function contentDigest(value: string | Uint8Array): string { return hash.digest("hex"); } +/** contentDigest of the text the parts concatenate to. */ +export function partsDigest(parts: readonly string[]): string { + const hash = createHash("sha256"); + for (const part of joinedParts(parts)) hash.update(part); + return hash.digest("hex"); +} + +/** contentDigest of a file's bytes, read in 1 MiB blocks so a large file + * (a module's LLVM text) is never held whole. */ +export function fileDigest(path: string): string { + const hash = createHash("sha256"); + const block = Buffer.alloc(1024 * 1024); + const fd = openSync(path, "r"); + try { + for (let read = readSync(fd, block, 0, block.length, null); read > 0;) { + hash.update(block.subarray(0, read)); + read = readSync(fd, block, 0, block.length, null); + } + } finally { + closeSync(fd); + } + return hash.digest("hex"); +} + /** A compiler cache contains private source and executable data. The host * checks ownership and permissions before this class accepts a root. */ export class NativeCache { @@ -86,15 +113,15 @@ export class NativeCache { } /** Publish text parts without joining them into one string. */ - writeParts(family: string, key: string, parts: readonly string[]): void { + writeParts(family: string, key: string, parts: Generator): void { let stage: string | null = null; try { const directory = join(this.root, family); mkdirSync(directory, { recursive: true, mode: 0o700 }); stage = mkdtempSync(join(directory, ".write-")); - writeTextPartsSync(join(stage, "payload"), parts); + // One pass over the parts: they may be generated while they are written. const hash = createHash("sha256"); - for (const part of parts) hash.update(part); + writeTextGeneratedSync(join(stage, "payload"), parts, (slice) => hash.update(slice)); writeFileSync(join(stage, "digest"), hash.digest("hex") + "\n"); const path = join(directory, key); renameSync(join(stage, "payload"), path); diff --git a/packages/compiler/src/native/cli.ts b/packages/compiler/src/native/cli.ts index e2c3a895b..44d5eb9f0 100644 --- a/packages/compiler/src/native/cli.ts +++ b/packages/compiler/src/native/cli.ts @@ -16,16 +16,38 @@ import { createNativeTs7Api } from "../frontend/ts7/native-api.js"; import { resolveProvenanceSourcesWithParser } from "../frontend/provenance-core.js"; import { setCompilerReleaseVersion } from "../library/sidecar.js"; import { NativeCompiler } from "./compiler.js"; -import { openNativeCache } from "./cache.js"; +import { NATIVE_STRING_BYTES, openNativeCache } from "./cache.js"; +import { + checkerExecutableIdentity, + setLibDiagnosticsStore, +} from "../frontend/ts7/lib-diagnostics.js"; import { loadNativeToolchain } from "./toolchain.js"; import { buildSanitizedRuntime } from "./sanitizer.js"; import { runCompilerTask } from "./task.js"; +import { processFork } from "../frontend/process-fork.js"; declare function compilerNativePrivateDirectory(path: string, harden: boolean): boolean; declare function compilerNativeCollectCycles(): void; +declare function compilerNativeFork(): number; +declare function compilerNativeExit(code: number): void; +declare function compilerNativeWait(pid: number): number; +declare function compilerNativeLockOpen(path: string): boolean; +declare function compilerNativeLock(): void; +declare function compilerNativeUnlock(): void; +declare function compilerNativeBoundCycleGrowth(): void; async function main(): Promise { + compilerNativeBoundCycleGrowth(); setFrontendDisposedHook(() => compilerNativeCollectCycles()); + if (process.platform !== "win32") + processFork.primitives = { + fork: () => compilerNativeFork(), + exit: (code) => compilerNativeExit(code), + wait: (pid) => compilerNativeWait(pid), + openLock: (path) => compilerNativeLockOpen(path), + lock: () => compilerNativeLock(), + unlock: () => compilerNativeUnlock(), + }; const manifestPath = process.env["SCRIPTC_TOOLCHAIN"] ?? process.execPath + ".json"; let compiler: NativeCompiler | null = null; let cache: ReturnType = null; @@ -35,6 +57,17 @@ async function main(): Promise { if (toolchain.declarationsRoot !== undefined) setDeclarationRoot(toolchain.declarationsRoot); setCompilerReleaseVersion(toolchain.compilerVersion); cache = openNativeCache((path, harden) => compilerNativePrivateDirectory(path, harden)); + if (cache !== null) { + const store = cache; + setLibDiagnosticsStore( + { + read: (key) => + store.read("lib-diagnostics", key, NATIVE_STRING_BYTES)?.toString("utf8") ?? null, + write: (key, text) => store.write("lib-diagnostics", key, text), + }, + checkerExecutableIdentity(toolchain.ts7Executable), + ); + } compiler = new NativeCompiler(toolchain, cache); return compiler; }; diff --git a/packages/compiler/src/native/compiler.ts b/packages/compiler/src/native/compiler.ts index d4487aba4..a37d76f80 100644 --- a/packages/compiler/src/native/compiler.ts +++ b/packages/compiler/src/native/compiler.ts @@ -56,7 +56,7 @@ import { clearFenceEvalCaches } from "../library/fence-eval.js"; import { archiveNativeLibrary, localizeNativeLibrary, linkNativeWasmLibrary } from "./library.js"; import type { NativeToolchain } from "./toolchain.js"; import { evaluateNativeComptime } from "./comptime.js"; -import { contentDigest, type NativeCache } from "./cache.js"; +import { contentDigest, fileDigest, type NativeCache } from "./cache.js"; import { nativeProfileKey, nativeProfileRefusal, @@ -186,7 +186,7 @@ export class NativeCompiler { key = contentDigest( JSON.stringify({ schema: 1, - llvm: contentDigest(readFileSync(input)), + llvm: fileDigest(input), outputKind, optimization, source, @@ -361,7 +361,8 @@ export class NativeCompiler { const inputDirectory = join(stage, "input"); mkdirSync(inputDirectory); const llvmInput = outputKind === "llvm" ? stagedOutput : join(inputDirectory, "program.ll"); - writeFileSync(llvmInput, llvm); + // In slices: the module text is never encoded whole. + writeTextPartsSync(llvmInput, llvm); timing("llvm-write"); if (outputKind === "llvm") { renameSync(stagedOutput, output); @@ -495,7 +496,7 @@ export class NativeCompiler { renameSync(stagedOutput + ".dSYM", output + ".dSYM"); } mkdirSync(options.outDir, { recursive: true }); - writeFileSync(llvmPath, llvm); + writeTextPartsSync(llvmPath, llvm); let irPath: string | undefined; if (options.emitIr) { irPath = join(options.outDir, `${stem}.ir.json`); @@ -583,7 +584,8 @@ export class NativeCompiler { stage = mkdtempSync(join(dirname(archivePath), ".scriptc-library-")); const llvm = prepared.llvm; const llvmInput = join(stage, "program.ll"); - writeFileSync(llvmInput, llvm); + // In slices: the module text is never encoded whole. + writeTextPartsSync(llvmInput, llvm); timing("llvm-write"); const features = prepared.features; const pack = selectNativeRuntimePack( @@ -646,7 +648,7 @@ export class NativeCompiler { }); } timing("native-archive"); - writeFileSync(llvmPath, llvm); + writeTextPartsSync(llvmPath, llvm); let irPath: string | undefined; if (options.emitIr) { irPath = join(options.outDir, `${stem}.lib.ir.json`); diff --git a/packages/compiler/src/native/executable-cache.ts b/packages/compiler/src/native/executable-cache.ts index a122fb759..c0ac755b9 100644 --- a/packages/compiler/src/native/executable-cache.ts +++ b/packages/compiler/src/native/executable-cache.ts @@ -11,7 +11,7 @@ import { import { runNativeTool } from "../backend/native-tools.js"; import type { NativeRuntimeSelection } from "../backend/runtime-pack-native.js"; import type { NativeToolchain } from "./toolchain.js"; -import { contentDigest, NativeCache } from "./cache.js"; +import { contentDigest, NativeCache, partsDigest } from "./cache.js"; interface InputIdentity { path: string; @@ -254,7 +254,7 @@ export class NativeExecutableCache { export function openNativeExecutableCache( cache: NativeCache | null, toolchain: NativeToolchain, - llvm: string, + llvm: readonly string[], options: CompileRequestOptions, ffi: FfiProfile | null, pack: NativeRuntimeSelection, @@ -299,7 +299,7 @@ export function openNativeExecutableCache( const key = contentDigest( JSON.stringify({ schema: 1, - llvm: contentDigest(llvm), + llvm: partsDigest(llvm), options, toolchain, runtimeIdentity: pack.packageText, diff --git a/packages/compiler/src/native/prepare.test.ts b/packages/compiler/src/native/prepare.test.ts index 7e22450da..112484f4a 100644 --- a/packages/compiler/src/native/prepare.test.ts +++ b/packages/compiler/src/native/prepare.test.ts @@ -11,11 +11,33 @@ import { NativeCache } from "./cache.js"; import { prepareNativeExecutable } from "./prepare.js"; import type { NativeToolchain } from "./toolchain.js"; +/** A prepared input with its LLVM parts joined: a cache hit reads the text + * back as one part. */ +function text(input: ReturnType["prepare"]>) { + return input.ok ? { ...input, llvm: input.llvm.join("") } : input; +} + const scratch: string[] = []; afterEach(() => { for (const path of scratch.splice(0)) rmSync(path, { recursive: true, force: true }); }); +function fixtureToolchain(): NativeToolchain { + const target = nativeCodegenTarget()!; + return { + compilerVersion: "test", + target, + helper: target.helper, + helperExecutable: "unused", + helperPackageRoot: "unused", + runtimePackRoot: "unused", + linker: "unused", + linkerArgs: [], + dsymutil: "unused", + ts7Executable: ts7Executable(), + }; +} + function fixture() { const temporary = mkdtempSync(join(tmpdir(), "scriptc-native-frontend-")); scratch.push(temporary); @@ -32,19 +54,7 @@ function fixture() { }), ); writeFileSync(join(root, "package.json"), '{"type":"module"}'); - const target = nativeCodegenTarget()!; - const toolchain: NativeToolchain = { - compilerVersion: "test", - target, - helper: target.helper, - helperExecutable: "unused", - helperPackageRoot: "unused", - runtimePackRoot: "unused", - linker: "unused", - linkerArgs: [], - dsymutil: "unused", - ts7Executable: ts7Executable(), - }; + const toolchain = fixtureToolchain(); const cache = new NativeCache(join(temporary, "cache")); const options = { outPath: join(root, ".scriptc", "main"), @@ -68,17 +78,77 @@ test("warm frontend reuse survives output creation and invalidates imported sour if (!first.ok) return; mkdirSync(f.options.outDir); writeFileSync(f.options.outPath, "binary placeholder"); - writeFileSync(join(f.options.outDir, "main.ll"), first.llvm); + writeFileSync(join(f.options.outDir, "main.ll"), first.llvm.join("")); const warm = f.prepare(); - expect(warm).toEqual(first); + expect(text(warm)).toEqual(text(first)); expect(f.loads()).toBe(1); writeFileSync(f.dependency, 'export const value = "second";\n'); const edited = f.prepare(); expect(edited.ok).toBe(true); - if (edited.ok) expect(edited.llvm).not.toBe(first.llvm); + if (edited.ok) expect(edited.llvm.join("")).not.toBe(first.llvm.join("")); expect(f.loads()).toBe(2); }); +test("comment and whitespace edits reuse the frontend output a fresh build reproduces", () => { + const f = fixture(); + const first = f.prepare(); + expect(first.ok, JSON.stringify(first)).toBe(true); + if (!first.ok) return; + const commented = '// The exported value.\nexport const value = /* unchanged */ "first";\n'; + writeFileSync(f.dependency, commented); + const reused = f.prepare(); + expect(f.loads()).toBe(1); + expect(reused.ok).toBe(true); + if (!reused.ok) return; + expect(reused.llvm.join("")).toBe(first.llvm.join("")); + expect(new Map(reused.sources).get(f.dependency)).toBe(commented); + // The reused output is the one the edited sources lower to. + const fresh = prepareNativeExecutable( + f.entry, + f.options, + null, + fixtureToolchain(), + (path, npmStatic, externalTypes) => runFrontend(path, loadProgram, npmStatic, externalTypes), + new NativeCache(join(f.root, "fresh-cache")), + ); + expect(fresh.ok && fresh.llvm.join("")).toBe(first.llvm.join("")); + // The refreshed entry still notices a later code edit. + writeFileSync(f.dependency, 'export const value = "second";\n'); + const edited = f.prepare(); + expect(f.loads()).toBe(2); + expect(edited.ok && edited.llvm.join("")).not.toBe(first.llvm.join("")); +}); + +test("a comment edit inside source text the output carries is rebuilt", () => { + const f = fixture(); + // A failing assert(value) reports its call's source text. + const source = (call: string) => + `import assert from "node:assert";\nexport const value = "first";\n${call};\n`; + writeFileSync(f.dependency, source("assert(value.length > 0)")); + const first = f.prepare(); + expect(first.ok, JSON.stringify(first)).toBe(true); + if (!first.ok) return; + writeFileSync(f.dependency, source("assert(value.length > 0 /* checked */)")); + const edited = f.prepare(); + expect(f.loads()).toBe(2); + expect(edited.ok && edited.llvm.join("")).not.toBe(first.llvm.join("")); +}); + +test("a whitespace edit that moves a reported source line is rebuilt", () => { + const f = fixture(); + // node:test reports the file, line and column of each test. + const source = (gap: string) => + `import { test } from "node:test";\nexport const value = "first";\n${gap}test("value", () => {});\n`; + writeFileSync(f.dependency, source("")); + const first = f.prepare(); + expect(first.ok, JSON.stringify(first)).toBe(true); + if (!first.ok) return; + writeFileSync(f.dependency, source("\n")); + const edited = f.prepare(); + expect(f.loads()).toBe(2); + expect(edited.ok && edited.llvm.join("")).not.toBe(first.llvm.join("")); +}); + test("structurally invalid cached features cause a rebuild even with a valid digest", () => { const f = fixture(); expect(f.prepare().ok).toBe(true); @@ -101,13 +171,13 @@ test("serialized IR travels as bounded parts through the frontend cache", () => expect(mod.functions.length).toBeGreaterThan(0); const warm = f.prepare({ emitIr: true }); expect(f.loads()).toBe(1); - expect(warm).toEqual(first); + expect(text(warm)).toEqual(text(first)); const family = join(f.cache.root, "frontend"); const key = readdirSync(family).find((name) => /^[0-9a-f]{64}$/.test(name))!; const payload = JSON.parse(readFileSync(join(family, key), "utf8")); expect(payload.schema).toBe("scriptc.native-frontend.v3"); expect(payload.input.llvm).toBe(""); expect(payload.input.ir).toBeNull(); - expect(payload.llvm).toBe(first.llvm); + expect(payload.llvm).toBe(first.llvm.join("")); expect(payload.ir).toEqual(first.ir); }); diff --git a/packages/compiler/src/native/prepare.ts b/packages/compiler/src/native/prepare.ts index e6e36c08f..f7f399f80 100644 --- a/packages/compiler/src/native/prepare.ts +++ b/packages/compiler/src/native/prepare.ts @@ -7,7 +7,11 @@ import type { CompileLibraryOptions, CompileRequestOptions, } from "../compile-types.js"; -import { emitLlvmModule, lowRegionTarget } from "../backend/llvm/emitter.js"; +import { + emitLlvmModuleParts, + type ForkPrimitives, + lowRegionTarget, +} from "../backend/llvm/emitter.js"; import { inlineRcOption } from "../backend/llvm/shapes.js"; import { executableLinkFeatures } from "../backend/executable-features.js"; import type { NativeLinkFeatures } from "../backend/native-link-info-core.js"; @@ -16,25 +20,32 @@ import type { FfiProfile } from "../ffi/ffi-manifest.js"; import type { FrontendFactory } from "../frontend/pipeline.js"; import { FrontendInputTracker, + frontendInputsSemanticallyMatch, frontendInputsStillMatch, validFrontendInputSnapshot, type FrontendInputSnapshot, type FrontendInputExclusions, trackedReadFile, } from "../frontend/input-tracker.js"; +import { compilerOwnedFileRoots } from "../frontend/dts-paths.js"; import { provenanceSources } from "../frontend/provenance-registry.js"; +import { processFork } from "../frontend/process-fork.js"; +import { tokenEquivalentSource } from "../frontend/token-equivalence.js"; +import { llvmBytes, wtf8Bytes } from "../backend/literals.js"; import { prepareLibrary } from "../library/prepare.js"; import type { LibraryProfile } from "../library/library-profile.js"; import type { IrModule } from "../ir/ir.js"; import { deserializeModuleBytes, serializeModuleParts } from "../ir/serialize.js"; import { contentDigest, NATIVE_STRING_BYTES, type NativeCache } from "./cache.js"; -import { batchTextParts } from "./text-file.js"; +import { batchTextParts, joinedParts, textSlices } from "./text-file.js"; import type { NativeToolchain } from "./toolchain.js"; import { nativeFileIdentity } from "./file-identity.js"; export interface NativeExecutableInput { ok: true; - llvm: string; + /** The module's LLVM text as parts that concatenate to it + * (emitLlvmModuleParts), never joined: a large program's is 150 MB. */ + llvm: readonly string[]; /** Compact serialized IR as bounded parts (`batchTextParts`): compiler-scale * modules exceed one native string. */ ir: string[] | null; @@ -57,51 +68,125 @@ interface FrontendCacheEntry { ir: string[] | null; } -function frontendCacheParts(probes: FrontendInputSnapshot, input: NativeExecutableInput): string[] { +/** The entry's JSON text, generated as it is written. The LLVM text is + * escaped in slices (`textSlices`), the same JSON string as escaping it + * whole, so a large module is never copied whole. */ +function* frontendCacheParts( + probes: FrontendInputSnapshot, + input: NativeExecutableInput, +): Generator { const metadata = JSON.stringify({ schema: "scriptc.native-frontend.v3", probes, input: { ...input, llvm: "", ir: null }, }); - const parts = [metadata.slice(0, metadata.length - 1) + ',"llvm":', JSON.stringify(input.llvm)]; - if (input.ir === null) parts.push(',"ir":null}'); + yield metadata.slice(0, metadata.length - 1) + ',"llvm":"'; + for (const part of joinedParts(input.llvm)) + for (const slice of textSlices(part)) yield JSON.stringify(slice).slice(1, -1); + yield '"'; + if (input.ir === null) yield ',"ir":null}'; else { - parts.push(',"ir":['); + yield ',"ir":['; for (let index = 0; index < input.ir.length; index++) { - if (index > 0) parts.push(","); - parts.push(JSON.stringify(input.ir[index]!)); + if (index > 0) yield ","; + yield JSON.stringify(input.ir[index]!); } - parts.push("]}"); + yield "]}"; + } +} + +/** The parts while their total length stays within `limit`; past it, a + * throw abandons the write. */ +function* boundedParts(parts: Generator, limit: number): Generator { + let length = 0; + for (const part of parts) { + length += part.length; + if (length > limit) throw new Error("frontend cache entry exceeds one native string"); + yield part; } - return parts; } /** Publish an entry only when a native reader can decode it as one string; - * UTF-16 length never exceeds the UTF-8 byte length. */ + * UTF-16 length never exceeds the UTF-8 byte length. A longer entry + * abandons its write, which writeParts discards. */ function publishFrontendCache( cache: NativeCache, key: string, probes: FrontendInputSnapshot, input: NativeExecutableInput, ): void { - const parts = frontendCacheParts(probes, input); - let length = 0; - for (const part of parts) length += part.length; - if (length <= NATIVE_STRING_BYTES) cache.writeParts("frontend", key, parts); + cache.writeParts( + "frontend", + key, + boundedParts(frontendCacheParts(probes, input), NATIVE_STRING_BYTES), + ); } +/** The cached input when every frontend input still matches, else the + * entry the inputs no longer match, for reuse after equivalent edits. */ function readFrontendCache( cache: NativeCache, key: string, exclusions: FrontendInputExclusions, -): NativeExecutableInput | null { +): { input: NativeExecutableInput } | { stale: FrontendCacheEntry } | null { // The entry is decoded as one string; larger entries are misses. const bytes = cache.read("frontend", key, NATIVE_STRING_BYTES); if (bytes === null) return null; const cached: unknown = JSON.parse(bytes.toString("utf8")); - if (!validCachedInput(cached) || !frontendInputsStillMatch(cached.probes, exclusions)) + if (!validCachedInput(cached)) return null; + if (!frontendInputsStillMatch(cached.probes, exclusions)) return { stale: cached }; + return { input: { ...cached.input, llvm: [cached.llvm], ir: cached.ir } }; +} + +/** The stale entry's input when every changed source is token-equivalent to + * the text the entry was built from (edits to whitespace and to comments + * that cannot affect compilation; see token-equivalence.ts). Equivalent + * sources lower to the same IR apart from source locations, which only dev + * builds' debug information and the serialized IR carry. Diagnostics carry + * locations too, so an entry that reported warnings is rebuilt. A changed + * source whose raw text or line positions the frontend read (call-site text + * in messages, comptime callbacks, test locations) is rebuilt as well: + * frontendInputsSemanticallyMatch refuses its `text` probe. */ +function equivalentFrontendInput( + stale: FrontendCacheEntry, + options: CompileRequestOptions, + exclusions: FrontendInputExclusions, +): { input: NativeExecutableInput; probes: FrontendInputSnapshot } | null { + if ( + options.optimization === "dev" || + options.sanitize === true || + options.emitIr === true || + stale.ir !== null || + (stale.input.warnings?.length ?? 0) > 0 + ) return null; - return { ...cached.input, llvm: cached.llvm, ir: cached.ir }; + const match = frontendInputsSemanticallyMatch( + stale.probes, + new Map(stale.input.sources), + tokenEquivalentSource, + exclusions, + ); + if (match === null || match.changed.length === 0) return null; + // Runtime fences and unsupported-construct messages name their source as + // `[ at :]`, and an equivalent edit can move the line. + if ( + match.changed.some(({ path }) => + stale.llvm.includes(` at ${llvmBytes(wtf8Bytes(path), false)}:`), + ) + ) + return null; + return { + input: { + ...stale.input, + llvm: [stale.llvm], + ir: null, + sources: stale.input.sources.map(([path, text]) => [ + path, + match.currentSources.get(path) ?? text, + ]), + }, + probes: match.snapshot, + }; } function implementationIdentity(toolchain: NativeToolchain): string { @@ -263,10 +348,19 @@ export function prepareNativeExecutable( .sort(([a], [b]) => a.localeCompare(b)), }), ); - const input = readFrontendCache(cache, key, exclusions); - if (input !== null) { + const cached = readFrontendCache(cache, key, exclusions); + if (cached !== null && "input" in cached) { timing("frontend-cache-hit"); - return input; + return cached.input; + } + const equivalent = + cached === null || outputKind === "ir" + ? null + : equivalentFrontendInput(cached.stale, options, exclusions); + if (equivalent !== null) { + publishFrontendCache(cache, key, equivalent.probes, equivalent.input); + timing("frontend-cache-equivalent"); + return equivalent.input; } } catch { key = null; @@ -284,19 +378,21 @@ export function prepareNativeExecutable( if (irInput !== "") timing("ir-input"); const ir = outputKind === "ir" || options.emitIr - ? batchTextParts(serializeModuleParts(prepared.mod)) + ? batchTextParts(serializeModuleParts(prepared.mod, compilerOwnedFileRoots())) : null; timing("ir-serialize"); const llvm = outputKind === "ir" - ? "" - : emitLlvmModule(prepared.mod, { + ? [] + : emitLlvmModuleParts(prepared.mod, { targetTriple: toolchain.target.llvmTriple, pointerBits: toolchain.target.pointerBits, wasi: toolchain.target.platform === "wasi", runtimeAbiMarker: outputKind === "obj" || outputKind === "exe", objectAudit: options.sanitize === true, lowRegion: lowRegionTarget(toolchain.target.platform, options.sanitize === true), + functionWorkers: emitWorkers(), + forkPrimitives: emitForkPrimitives(), ...inlineRcOption(options.optimization, options.sanitize === true), ...(options.detachedExit === true && outputKind === "exe" ? { detachedExit: true } : {}), ...(options.optimization === "dev" && !options.strip @@ -376,10 +472,10 @@ export function prepareNativeLibrary( .sort(([a], [b]) => a.localeCompare(b)), }), ); - const input = readFrontendCache(cache, key, exclusions); - if (input !== null) { + const cached = readFrontendCache(cache, key, exclusions); + if (cached !== null && "input" in cached) { timing("frontend-cache-hit"); - return input; + return cached.input; } } catch { key = null; @@ -404,7 +500,7 @@ export function prepareNativeLibrary( ); }); if (!prepared.ok) return prepared; - const llvm = emitLlvmModule(prepared.mod, { + const llvm = emitLlvmModuleParts(prepared.mod, { targetTriple: toolchain.target.llvmTriple, pointerBits: toolchain.target.pointerBits, wasi: toolchain.target.platform === "wasi", @@ -415,7 +511,9 @@ export function prepareNativeLibrary( const input: NativeExecutableInput = { ok: true, llvm, - ir: options.emitIr ? batchTextParts(serializeModuleParts(prepared.mod)) : null, + ir: options.emitIr + ? batchTextParts(serializeModuleParts(prepared.mod, compilerOwnedFileRoots())) + : null, sidecarJson: prepared.sidecarJson, features: executableLinkFeatures(prepared.mod), sources: [...prepared.sourceTexts], @@ -433,3 +531,22 @@ export function prepareNativeLibrary( timing("frontend-cache-publish"); return input; } + +/** Worker processes that emit function bodies at a time + * (SCRIPTC_EMIT_WORKERS; 0 or 1 emits in this process). */ +function emitWorkers(): number { + const value = Number(process.env["SCRIPTC_EMIT_WORKERS"] ?? "4"); + return Number.isFinite(value) && value > 1 ? Math.floor(value) : 1; +} + +/** The process primitives emission workers fork with, unless the host + * cannot fork or this process is a worker itself. */ +function emitForkPrimitives(): ForkPrimitives | undefined { + const primitives = processFork.primitives; + if (primitives === null || processFork.worker) return undefined; + return { + fork: () => primitives.fork(), + exit: (code) => primitives.exit(code), + wait: (pid) => primitives.wait(pid), + }; +} diff --git a/packages/compiler/src/native/sanitizer.test.ts b/packages/compiler/src/native/sanitizer.test.ts index 49867f0ad..4c031a8e7 100644 --- a/packages/compiler/src/native/sanitizer.test.ts +++ b/packages/compiler/src/native/sanitizer.test.ts @@ -79,6 +79,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")( dynTier: false, streamDyn: false, handleProps: false, + declAdopt: false, dynForAwait: false, eventsOnceSignal: false, dynBuffer: false, diff --git a/packages/compiler/src/native/text-file.test.ts b/packages/compiler/src/native/text-file.test.ts new file mode 100644 index 000000000..7534b5058 --- /dev/null +++ b/packages/compiler/src/native/text-file.test.ts @@ -0,0 +1,52 @@ +import { mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, expect, test } from "vitest"; +import { joinedParts, textSlices, writeTextPartsSync } from "./text-file.js"; + +const directories: string[] = []; +afterEach(() => { + for (const path of directories.splice(0)) rmSync(path, { recursive: true, force: true }); +}); + +test("text slices keep surrogate pairs whole and escape as the whole text does", () => { + const text = 'ab😀cé😀😀\ud800x"\\\n' + "z".repeat(5); + for (let length = 2; length <= 8; length++) { + const slices = [...textSlices(text, length)]; + expect(slices.join("")).toBe(text); + for (const slice of slices) { + expect(slice.length).toBeLessThanOrEqual(length); + const last = slice.charCodeAt(slice.length - 1); + // A lone high surrogate of the text may end a slice; a pair's never does. + if (last >= 0xd800 && last <= 0xdbff) expect(slice.endsWith("\ud800")).toBe(true); + } + expect(slices.map((slice) => JSON.stringify(slice).slice(1, -1)).join("")).toBe( + JSON.stringify(text).slice(1, -1), + ); + } + expect([...textSlices("")]).toEqual([]); +}); + +test("a part larger than one slice is written as its UTF-8 bytes", () => { + const dir = mkdtempSync(join(tmpdir(), "scriptc-text-file-")); + directories.push(dir); + // A surrogate pair across the first slice boundary (1 MiB units). + const large = "a".repeat(1024 * 1024 - 1) + "😀" + "b".repeat(1024 * 1024 + 3); + const path = join(dir, "out.txt"); + const seen: string[] = []; + writeTextPartsSync(path, ["head,", large, "", "tail"], (slice) => seen.push(slice)); + const expected = Buffer.from("head," + large + "tail", "utf8"); + expect(readFileSync(path).equals(expected)).toBe(true); + expect(seen.join("")).toBe("head," + large + "tail"); + expect(Math.max(...seen.map((slice) => slice.length))).toBeLessThanOrEqual(1024 * 1024); +}); + +test("small parts join into batches and a long part passes through whole", () => { + const small = Array.from({ length: 3000 }, (_, index) => `line ${index}\n`); + const long = "x".repeat(2 * 1024 * 1024); + const parts = [...small, long, "tail"]; + const joined = [...joinedParts(parts)]; + expect(joined.join("")).toBe(parts.join("")); + expect(joined.length).toBeLessThan(5); + expect(joined).toContain("tail"); +}); diff --git a/packages/compiler/src/native/text-file.ts b/packages/compiler/src/native/text-file.ts index 6f8990339..8b0953120 100644 --- a/packages/compiler/src/native/text-file.ts +++ b/packages/compiler/src/native/text-file.ts @@ -24,17 +24,79 @@ export function batchTextParts(parts: readonly string[]): string[] { return batches; } -/** Write text parts to `path` in order without joining them first. Each part - * is one write; pass batched parts (`batchTextParts`) for many small ones. */ -export function writeTextPartsSync(path: string, parts: readonly string[]): void { +/** The most UTF-16 units of a part that one write encodes. */ +const TEXT_WRITE_LENGTH = 1024 * 1024; + +/** `text` in slices of at most `length` (2 or more) UTF-16 units. A slice + * never ends between the two halves of a surrogate pair, so the slices + * encode, as UTF-8 or as JSON string contents, to the encoding of `text`. */ +export function* textSlices(text: string, length = TEXT_WRITE_LENGTH): Generator { + for (let start = 0; start < text.length;) { + let end = Math.min(text.length, start + length); + if (end < text.length) { + const unit = text.charCodeAt(end - 1); + if (unit >= 0xd800 && unit <= 0xdbff) end--; + } + yield text.slice(start, end); + start = end; + } +} + +/** `parts` with consecutive parts joined into strings of about + * TEXT_WRITE_LENGTH UTF-16 units (a longer part passes through whole), so + * writing or escaping many small parts takes few calls. */ +export function* joinedParts(parts: readonly string[]): Generator { + let batch: string[] = []; + let length = 0; + for (const part of parts) { + batch.push(part); + length += part.length; + if (length >= TEXT_WRITE_LENGTH) { + yield batch.length === 1 ? batch[0]! : batch.join(""); + batch = []; + length = 0; + } + } + if (batch.length > 0) yield batch.join(""); +} + +/** Encodes and writes `part` to `fd` in slices (`textSlices`), so a large + * part is never encoded whole. `written` sees each slice after it is + * written. */ +function writeTextPart(fd: number, part: string, written?: (slice: string) => void): void { + for (const slice of textSlices(part)) { + const bytes = Buffer.from(slice, "utf8"); + for (let offset = 0; offset < bytes.length;) { + offset += writeSync(fd, bytes, offset, bytes.length - offset); + } + written?.(slice); + } +} + +/** Write text parts to `path` in order without joining them whole: small + * parts are written in batches (`joinedParts`). */ +export function writeTextPartsSync( + path: string, + parts: readonly string[], + written?: (slice: string) => void, +): void { const fd = openSync(path, "w"); try { - for (const part of parts) { - const bytes = Buffer.from(part, "utf8"); - for (let offset = 0; offset < bytes.length;) { - offset += writeSync(fd, bytes, offset, bytes.length - offset); - } - } + for (const part of joinedParts(parts)) writeTextPart(fd, part, written); + } finally { + closeSync(fd); + } +} + +/** writeTextPartsSync for parts generated while they are written. */ +export function writeTextGeneratedSync( + path: string, + parts: Generator, + written?: (slice: string) => void, +): void { + const fd = openSync(path, "w"); + try { + for (const part of parts) writeTextPart(fd, part, written); } finally { closeSync(fd); } diff --git a/packages/compiler/src/native/toolchain.test.ts b/packages/compiler/src/native/toolchain.test.ts index 0ee434300..ad4570c0c 100644 --- a/packages/compiler/src/native/toolchain.test.ts +++ b/packages/compiler/src/native/toolchain.test.ts @@ -99,11 +99,11 @@ test("native toolchain assets resolve from the physical manifest through package const direct = loadNativeToolchain(path); const viaLink = loadNativeToolchain(join(linked, "bin", "scriptc.exe.json")); expect(viaLink).toEqual(direct); - expect(viaLink.ts7Executable).toBe(join(assets, "typescript/lib/tsc.exe")); + expect(viaLink.ts7Executable).toBe(join(assets, "tsc-ts/lib/tsc-ts.exe")); expect( - loadNativeToolchain(join(linked, "bin", "scriptc.exe.json"), { SCRIPTC_CHECKER: "tsc-ts" }) + loadNativeToolchain(join(linked, "bin", "scriptc.exe.json"), { SCRIPTC_CHECKER: "tsgo" }) .ts7Executable, - ).toBe(join(assets, "tsc-ts/lib/tsc-ts.exe")); + ).toBe(join(assets, "typescript/lib/tsc.exe")); expect(viaLink.helperExecutable).toBe(join(assets, "llvm/bin/scriptc-llvm-codegen.exe")); expect( loadNativeToolchain(join(linked, "bin", "scriptc.exe.json"), { diff --git a/packages/compiler/src/native/toolchain.ts b/packages/compiler/src/native/toolchain.ts index fbcd94251..5abbb329e 100644 --- a/packages/compiler/src/native/toolchain.ts +++ b/packages/compiler/src/native/toolchain.ts @@ -177,11 +177,11 @@ export function loadNativeToolchain( : [...target.defaultLinkerArgs]; return { compilerVersion: manifest.compiler_version, - // tsc-ts, the default checker except on Windows, comes with the distribution; - // a development build takes it from the tsc-ts package installed with the - // compiler or for the working directory. SCRIPTC_CHECKER selects a checker. + // tsc-ts, the default checker, comes with the distribution; a development + // build takes it from the tsc-ts package installed with the compiler or for + // the working directory. SCRIPTC_CHECKER selects a checker. ts7Executable: - checkerBackend(env, host.platform) === "tsgo" + checkerBackend(env) === "tsgo" ? pathFrom(root, manifest.ts7) : manifest.tsc_ts !== undefined ? pathFrom(root, manifest.tsc_ts) diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index 45b3edbaa..81fb23ae2 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -1,6 +1,6 @@ { "schemaVersion": 2, - "compilerVersion": "0.4.1", + "compilerVersion": "0.4.2", "coverage": [ "Entries are projected mechanically from the compiler's own decision tables: the diagnostics registry, the unsupported-syntax dispatch tables, the stdlib and node-builtin lowering tables, and the supported-builtin-module list. Nothing is hand-maintained; the manifest regenerates byte-identically from the source tree at this version.", "Absence from this manifest means 'not projected', never 'unsupported'. Surfaces lowered through dedicated code paths rather than tables are not yet projected: console, JSON, Promise/async and the timer surface, the net/http/tls/https/dgram/dns/assert/test/stream/readline module member surfaces, template literals, the regex slice, global functions (parseInt, parseFloat, isNaN, isFinite), and the process surface outside its ambient slice.", @@ -480,6 +480,18 @@ "status": "static", "note": "recognized module (bare and node:-prefixed specifiers)" }, + { + "id": "node-builtin.crypto.checkPrime", + "kind": "node-builtin", + "name": "crypto.checkPrime", + "status": "static" + }, + { + "id": "node-builtin.crypto.checkPrimeSync", + "kind": "node-builtin", + "name": "crypto.checkPrimeSync", + "status": "static" + }, { "id": "node-builtin.crypto.createCipheriv", "kind": "node-builtin", @@ -584,13 +596,25 @@ "name": "crypto.generateKeySync", "status": "static" }, + { + "id": "node-builtin.crypto.generatePrime", + "kind": "node-builtin", + "name": "crypto.generatePrime", + "status": "static" + }, + { + "id": "node-builtin.crypto.generatePrimeSync", + "kind": "node-builtin", + "name": "crypto.generatePrimeSync", + "status": "static" + }, { "id": "node-builtin.crypto.getCipherInfo", "kind": "node-builtin", "name": "crypto.getCipherInfo", "status": "unsupported", "code": "SC2020", - "note": "cipher metadata has no lowering — createCipheriv and createDecipheriv support aes-128/192/256 in cbc, ctr, and gcm modes and chacha20-poly1305" + "note": "cipher metadata has no lowering — createCipheriv and createDecipheriv support aes-128/192/256 in cbc, ctr, gcm, ecb, cfb, cfb8, cfb1, and ofb modes, des-ede3 and des-ede, chacha20, and chacha20-poly1305" }, { "id": "node-builtin.crypto.getDiffieHellman", diff --git a/packages/compiler/test/array-index-product-emission.test.ts b/packages/compiler/test/array-index-product-emission.test.ts new file mode 100644 index 000000000..bfd891398 --- /dev/null +++ b/packages/compiler/test/array-index-product-emission.test.ts @@ -0,0 +1,55 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile } from "../src/index.js"; + +async function emitLlvm(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-index-product-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ll"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "llvm" }); + expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true); + return await readFile(outPath, "utf8"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +const source = ` +function at(a: number[], x: number, y: number, k: number): number | undefined { + return a[x * y + k]; +} +function cancel(a: number[], p: number, q: number, r: number, s: number): number | undefined { + return a[p * q - r * s]; +} +const a = [1, 2, 3]; +const v = process.argv.length; +console.log(at(a, v | 0, (v + 1) | 0, -v | 0), cancel(a, v | 0, v | 0, v | 0, v | 0)); +`; + +function body(llvm: string, name: string): string { + return new RegExp(`^define [^@]*@sc_\\w+?_${name}(\\.i32)?\\([^]*?^}`, "m").exec(llvm)?.[0] ?? ""; +} + +test("an int32 product plus an int32 indexes as an exact i64 without a round trip", async () => { + const llvm = await emitLlvm(source); + // The int32 entry: the index is the i64 product and sum, compared with + // the array's counts; the double goes only to the slow path's runtime call. + const at = /^define [^@]*@sc_bf_at\.i32\([^]*?^}/m.exec(llvm)?.[0] ?? ""; + const product = /(%\S+) = mul nsw i64 %\S+, %\S+/.exec(at)?.[1]; + expect(product).toBeDefined(); + expect(at).toContain(`= add nsw i64 ${product}, `); + expect(at).not.toMatch(/fptosi double %\S+ to i64/); +}); + +test("a difference of two int32 products keeps the double's round trip", async () => { + const llvm = await emitLlvm(source); + // Each product may pass 2^53, where its double rounds before the + // difference: the index keeps the exactness test of the double. + const cancel = body(llvm, "cancel"); + expect(cancel).toMatch(/fptosi double %\S+ to i64/); + expect(cancel).toMatch(/fcmp oeq double/); +}); diff --git a/packages/compiler/test/chunked-emission.test.ts b/packages/compiler/test/chunked-emission.test.ts new file mode 100644 index 000000000..b4912c9f6 --- /dev/null +++ b/packages/compiler/test/chunked-emission.test.ts @@ -0,0 +1,122 @@ +import { mkdtemp, readFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule } from "../src/index.js"; +import type { IrModule } from "../src/ir/ir.js"; +import { emitLlvmModuleSource, type LlvmTargetOptions } from "../src/backend/llvm/emitter.js"; +import { inlineRcOption } from "../src/backend/llvm/shapes.js"; + +// Programs whose function bodies request module-level helpers and tables +// that the chunk journal (emit-journal.ts) carries: checked-dynamic +// conversions, inspection, publication walkers, live record views, stream +// adapters, regex instances, colliding literal digests, interned C strings, +// speculative loop sites, typed message listeners and readers. +const PROGRAMS = [ + "1003-json-parse-unions.ts", + "114-live-record-delete.ts", + "1200-regex-test-basics.ts", + "1631-inspect-arrays.ts", + "4598-publish-constructor-stores.ts", + "4900-console-instances.ts", + "5612-readable-from-iterables.cjs", + "5702-publish-candidate-buffers.ts", + "7321-speculative-loop-bail-sites.ts", + "7360-worker-typed-records.ts", + "node24-text-utilities-coercion.js", +]; + +const OPTIONS: Record = { + release: {}, + speed: { + targetTriple: "x86_64-unknown-linux-gnu", + pointerBits: 64, + runtimeAbiMarker: true, + lowRegion: true, + ...inlineRcOption("speed", false), + }, +}; + +const text = (out: string | readonly string[]): string => + typeof out === "string" ? out : out.join(""); + +async function lower(program: string, dir: string): Promise { + const entry = join(import.meta.dirname, "../../../tests/corpus", program); + const outPath = join(dir, `${program}.ir.json`); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + return readFile(outPath, "utf8"); +} + +test("function bodies emitted in chunks merge into the text one emitter gives", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-chunked-emission-")); + try { + for (const program of PROGRAMS) { + const ir = await lower(program, dir); + const fresh = (): IrModule => deserializeModule(ir); + for (const [name, options] of Object.entries(OPTIONS)) { + const serial = text(emitLlvmModuleSource(fresh(), options)); + for (const functionChunks of [3, 7]) { + const chunked = text(emitLlvmModuleSource(fresh(), { ...options, functionChunks })); + expect(chunked === serial, `${program}, ${name}, ${functionChunks} chunks`).toBe(true); + } + } + } + } finally { + await rm(dir, { recursive: true, force: true }); + } +}, 600_000); + +// Debug metadata is numbered per emitter (debug-info.ts) and not journaled: +// a debug build emits its function bodies in one emitter. +test("debug builds emit function bodies serially, with every metadata reference defined", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-chunked-debug-")); + const minFunctions = process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"]; + try { + const corpus = join(import.meta.dirname, "../../../tests/corpus/3066-source-debugging"); + const entry = join(corpus, "main.ts"); + const outPath = join(dir, "main.ir.json"); + const result = await compile(entry, { + outDir: dir, + outPath, + outputKind: "ir", + optimization: "dev", + }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const ir = await readFile(outPath, "utf8"); + const debugSources = new Map(); + for (const name of ["main.ts", "helper.ts"]) + debugSources.set(join(corpus, name), await readFile(join(corpus, name), "utf8")); + const options: LlvmTargetOptions = { debugSources }; + const serial = text(emitLlvmModuleSource(deserializeModule(ir), options)); + expect(serial).toContain("!DILocation"); + + let forks = 0; + process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"] = "1"; + const forkPrimitives = { + fork: (): number => { + forks++; + return -1; + }, + exit: (): void => {}, + wait: (): number => -1, + }; + for (const variant of [ + { functionChunks: 3 }, + { functionWorkers: 4, forkPrimitives }, + ] satisfies LlvmTargetOptions[]) { + const emitted = text(emitLlvmModuleSource(deserializeModule(ir), { ...options, ...variant })); + expect(emitted === serial, JSON.stringify(Object.keys(variant))).toBe(true); + const defined = new Set([...emitted.matchAll(/^(![0-9]+) = /gm)].map((m) => m[1])); + const referenced = new Set([...emitted.matchAll(/(![0-9]+)\b/g)].map((m) => m[1])); + expect([...referenced].filter((id) => !defined.has(id))).toEqual([]); + } + expect(forks).toBe(0); + } finally { + if (minFunctions === undefined) delete process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"]; + else process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"] = minFunctions; + await rm(dir, { recursive: true, force: true }); + } +}, 120_000); diff --git a/packages/compiler/test/forin-keys.test.ts b/packages/compiler/test/forin-keys.test.ts index 91d1848fd..b35ff2154 100644 --- a/packages/compiler/test/forin-keys.test.ts +++ b/packages/compiler/test/forin-keys.test.ts @@ -36,7 +36,7 @@ console.log(count(row), count(JSON.parse('{"b":1}'))); expect(llvm).not.toContain("@scr_dyn_for_in_keys("); // Fully required record shapes are marked fixed for the stable walk. expect(llvm).toMatch( - /_rops = internal global \{ i64, ptr, ptr, ptr, i8, ptr \} \{[^}]*i8 1, ptr null \}/, + /_rops = internal global \{ i64, ptr, ptr, ptr, i8, ptr, ptr \} \{[^}]*i8 1, ptr null, ptr null \}/, ); }); diff --git a/packages/compiler/test/int32-element-array-emission.test.ts b/packages/compiler/test/int32-element-array-emission.test.ts new file mode 100644 index 000000000..0b68a5d3e --- /dev/null +++ b/packages/compiler/test/int32-element-array-emission.test.ts @@ -0,0 +1,198 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule } from "../src/index.js"; +import type { IrModule } from "../src/ir/ir.js"; +import { markInt32ArrayCandidates, selectInt32Arrays } from "../src/ir/int32-arrays.js"; + +async function emitLlvm(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-int32-elements-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ll"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "llvm" }); + expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true); + return await readFile(outPath, "utf8"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +async function lowerIr(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-int32-elements-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ir.json"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true); + return deserializeModule(await readFile(outPath, "utf8")); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +const source = ` +function compose(n: number, rounds: number): number { + const p: number[] = []; + const q: number[] = []; + for (let i = 0; i < n; i++) p.push((i * 7919 + 3) & (n - 1)); + for (let i = 0; i < n; i++) q.push(i); + let check = 0; + for (let r = 0; r < rounds; r++) { + for (let i = 0; i < n; i++) q[i] = p[q[i]!]!; + check = (check + q[r % n]!) | 0; + } + return check; +} +function counts(n: number): number { + const c = new Array(n).fill(0) as number[]; + for (let i = 0; i < n; i++) c[(i * 5) % n] = i; + while (c.length > 1) c.pop(); + return c[0]!; +} +function fraction(n: number): number { + const a = [1, 2, 3]; + a[1] = n / 3; + return a[1]!; +} +function negativeZero(): number { + const a = [1, -0]; + return 1 / a[1]!; +} +function counter(n: number): number { + const a = new Array(n).fill(0) as number[]; + for (let i = 0; i < 1000; i++) a[i % n] = a[i % n]! + 1; + return a[0]!; +} +function shortCounter(): number { + const a = new Array(16).fill(0) as number[]; + for (let i = 0; i < 1000; i++) a[i % 16] = a[i % 16]! + 1; + return a[0]!; +} +function literalCounter(n: number): number { + const a = [0]; + for (let i = 0; i < n; i++) a[0] = a[0]! + 1; + return a[0]!; +} +function joined(n: number): string { + const a = [n | 0, 2]; + return a.join(","); +} +const { n } = JSON.parse('{"n": 8}') as { n: number }; +console.log( + compose(n | 0, 3), + counts(n | 0), + fraction(n), + negativeZero(), + counter(n), + shortCounter(), + literalCounter(n), + joined(n), +); +`; + +/** Every function emitted for the source function \`name\` (its int32 ABI + * body and its wrapper included). */ +function functions(llvm: string, name: string): string { + const pattern = new RegExp(`^define [^@]*@sc_b?f_${name}[.\\w]*\\([^]*?^}`, "gm"); + return [...llvm.matchAll(pattern)].map((m) => m[0]).join("\n"); +} + +test("arrays storing only int32 elements get 4-byte slots", async () => { + const llvm = await emitLlvm(source); + const compose = functions(llvm, "compose"); + expect(compose).not.toBe(""); + // Both arrays are allocated as SCR_ELEM_I32 (7). + expect(compose.match(/@scr_arr_new_small\(i32 7\)/g)?.length).toBe(2); + // Reads load an i32 slot; writes store one. + expect(compose).toMatch(/getelementptr inbounds i32, ptr %\w+, i64 %\w+\n\s+%\w+ = load i32/); + expect(compose).toMatch(/store i32 %\w+, ptr %\w+, !tbaa/); + // The element read through the helper's ternary stays in a stack box. + expect(compose).not.toContain("@scr_union_new_f64"); + expect(compose).not.toMatch(/call [^\n]*@sc_b?f__x25_arr_idxOr/); + // The fill form and a discarded pop. + const counts = functions(llvm, "counts"); + expect(counts).toMatch(/@scr_arr_new\(i32 7, i64 0\)/); + expect(counts).not.toContain("popArray"); +}); + +test("arrays with a store that has no exact integer range, or that escape, keep doubles", async () => { + const llvm = await emitLlvm(source); + for (const name of ["fraction", "negativeZero", "joined"]) { + const fn = functions(llvm, name); + expect(fn, name).not.toBe(""); + expect(fn, name).not.toMatch(/@scr_arr_new(_small)?\(i32 7/); + expect(fn, name).not.toContain("arr.kind."); + } +}); + +test("a counter array keeps int32 slots with checked stores", async () => { + const llvm = await emitLlvm(source); + const counter = functions(llvm, "counter"); + // `a[k] = a[k]! + 1` is an exact integer but no int32 proof: a fill of at + // least 4096 elements creates SCR_ELEM_I32 storage (SCR_ELEM_F64 for a + // shorter one), accesses test the kind, and a store widens the array + // before a value that is not an int32. + expect(counter).toMatch( + /fcmp oge double %\w+, [^\n]+\n\s+%\w+ = select i1 %\w+, i32 7, i32 0\n\s+%\w+ = call ptr @scr_arr_new(_small)?\(i32 %\w+/, + ); + expect(counter).toMatch(/load i8, ptr %\w+[^\n]*\n\s+%\w+ = icmp eq i8 %\w+, 7/); + expect(counter).toContain("@llvm.fptosi.sat.i32.f64"); + expect(counter).toContain("call void @scr_arr_widen_i32("); + // Arrays with proven stores take no kind test, and a fill of a constant + // short length or an array literal keeps doubles. + for (const name of ["compose", "counts", "shortCounter", "literalCounter"]) { + const fn = functions(llvm, name); + expect(fn, name).not.toContain("arr.kind."); + expect(fn, name).not.toContain("kind.fast"); + expect(fn, name).not.toContain("scr_arr_widen_i32"); + } + for (const name of ["shortCounter", "literalCounter"]) + expect(functions(llvm, name), name).not.toMatch(/@scr_arr_new(_small)?\(i32 7/); +}); + +test("a loop over a counter array tests its kind once on entry", async () => { + const llvm = await emitLlvm(source); + const counter = functions(llvm, "counter"); + // The loop's fast copy runs while the array holds int32 slots, with no + // kind test of its own: the sum is an integer, and a sum past the int32 + // range leaves for the third copy, which re-executes the store with kind + // tests and widens the array. + expect(counter).toMatch( + /icmp eq i8 %\w+, 7\n\s+br i1 %\w+, label %kind\.fast\d+, label %kind\.wide/, + ); + const rest = counter.slice(counter.search(/^kind\.fast\d+:/m)); + // The fast copy is the last one, before the loop's exit. + const fast = rest.slice(0, rest.search(/^loop\.e\d+:/m)); + expect(fast).not.toBe(""); + expect(fast).not.toMatch(/icmp eq i8 %\w+, 7/); + expect(fast).toMatch(/add nsw i64 %\w+, 1/); + expect(fast).toMatch( + /icmp sle i64 %\w+, 2147483647\n[^\n]*\n\s+br i1 %\w+, label %arr\.store\.i32\d+, label %kind\.resume\d+/, + ); + expect(fast).toMatch(/store i32 %\w+, ptr %\w+, !tbaa/); + expect(counter).toMatch(/^kind\.resume\d+:/m); +}); + +test("only the candidates the proof keeps are rewritten", async () => { + const mod = await lowerIr(source); + const proposed = markInt32ArrayCandidates(mod).int32ArrayCandidates ?? []; + expect(proposed).toEqual( + expect.arrayContaining(["compose\0p.0", "compose\0q.0", "counts\0c.0", "counter\0a.0"]), + ); + // An array that reaches `join` is never proposed. + expect(proposed.some((key) => key.startsWith("joined\0"))).toBe(false); + // Without a kept candidate, the module is the input itself. + expect(selectInt32Arrays(mod, () => new Set())).toBe(mod); + const kept = new Set(["compose\0p.0", "compose\0q.0"]); + const selected = selectInt32Arrays( + mod, + (marked) => new Set((marked.int32ArrayCandidates ?? []).filter((key) => kept.has(key))), + ); + expect(selected.int32ArrayCandidates).toEqual([...kept]); + for (const fn of selected.functions) + if (fn.name !== "compose") expect(fn).toBe(mod.functions.find((f) => f.name === fn.name)); +}); diff --git a/packages/compiler/test/native-codegen-integration.test.ts b/packages/compiler/test/native-codegen-integration.test.ts index 35a747d60..d398fa1d3 100644 --- a/packages/compiler/test/native-codegen-integration.test.ts +++ b/packages/compiler/test/native-codegen-integration.test.ts @@ -378,7 +378,7 @@ done: expect(await symbols(helperObject, ["-u"])).toEqual(await symbols(clangObject, ["-u"])); expect(await symbols(helperObject, ["-gU"])).toEqual(await symbols(clangObject, ["-gU"])); expect(await symbols(helperObject, ["-gU"])).toEqual(["0000000000000000 T _main"]); - expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v27"); + expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v36"); const clangExe = join(dir, "clang-program"); const linkDriver = join(dir, "clang-driver.c"); @@ -712,7 +712,7 @@ done: .split("\n"); expect(undefinedSymbols).toContain("_sf_scale"); expect(undefinedSymbols).toContain("_sf_callback_mix"); - expect(undefinedSymbols).toContain("_scr_runtime_abi_v27"); + expect(undefinedSymbols).toContain("_scr_runtime_abi_v36"); }); test("helper and clang object paths retain the same outbound FFI C ABI", async () => { diff --git a/packages/compiler/test/observed-method-guard.test.ts b/packages/compiler/test/observed-method-guard.test.ts new file mode 100644 index 000000000..cafe872f9 --- /dev/null +++ b/packages/compiler/test/observed-method-guard.test.ts @@ -0,0 +1,119 @@ +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, deserializeModule, validateModule } from "../src/index.js"; + +const dirs: string[] = []; + +afterEach(async () => { + await Promise.all(dirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); +}); + +type IrRecord = Record; + +function recordsOf(value: unknown, out: IrRecord[] = []): IrRecord[] { + if (value === null || typeof value !== "object") return out; + if (Array.isArray(value)) { + for (const item of value) recordsOf(item, out); + return out; + } + const record = value as IrRecord; + out.push(record); + for (const child of Object.values(record)) recordsOf(child, out); + return out; +} + +// An untyped computed write puts every method name in the census, so typed +// calls read the observable slot. While the slot holds the declared method, +// the call is the native method call, not a dynamic call of the slot. +test("typed calls of an observed method call it natively while its slot is unchanged", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-observed-guard-")); + dirs.push(dir); + const entry = join(dir, "main.ts"); + const outDir = join(dir, ".scriptc"); + const outPath = join(outDir, "main.ir.json"); + await writeFile( + join(dir, "untyped.js"), + "export function put(target, key, value) { target[key] = value; }\n", + ); + await writeFile( + entry, + [ + 'import { put } from "./untyped.js";', + "class Repo {", + " items: number[] = [];", + " total(): number { let s = 0; for (const n of this.items) s += n; return s; }", + "}", + "const repo = new Repo();", + "repo.items.push(1, 2);", + "put({}, String(Math.random()), 1);", + "export function sum(r: Repo): number { return r.total(); }", + "console.log(sum(repo));", + "", + ].join("\n"), + ); + const result = await compile(entry, { outDir, outPath, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const module = deserializeModule(await readFile(outPath, "utf8")); + expect(validateModule(module)).toEqual([]); + const sum = module.functions.find((fn) => fn.name.endsWith("sum")); + expect(sum).toBeDefined(); + const records = recordsOf(sum); + const guards = records.filter( + (record) => + record.kind === "ternary" && + (record.cond as IrRecord | undefined)?.kind === "libCall" && + (record.cond as IrRecord).fn === "dyn.sameValue", + ); + expect(guards).toHaveLength(1); + const then = guards[0]!.then as IrRecord; + expect(then.kind).toBe("call"); + expect(String(then.callee)).toMatch(/Repo\.total$/); + expect(module.globals?.some((global) => global.name === "Repo.prototype.total.original")).toBe( + true, + ); +}); + +// A spread argument keeps the dynamic call, which drains the spread through +// the iterator's current `next` (a replacement included). +test("typed calls of an observed method with a spread argument keep the dynamic call", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-observed-guard-")); + dirs.push(dir); + const entry = join(dir, "main.ts"); + const outDir = join(dir, ".scriptc"); + const outPath = join(outDir, "main.ir.json"); + await writeFile( + join(dir, "untyped.js"), + "export function put(target, key, value) { target[key] = value; }\n", + ); + await writeFile( + entry, + [ + 'import { put } from "./untyped.js";', + "class Bag {", + " sum(...values: number[]): number { let s = 0; for (const n of values) s += n; return s; }", + "}", + "const bag = new Bag();", + "put({}, String(Math.random()), 1);", + "export function spread(b: Bag, values: number[]): number { return b.sum(...values); }", + "console.log(spread(bag, [1, 2]));", + "", + ].join("\n"), + ); + const result = await compile(entry, { outDir, outPath, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const module = deserializeModule(await readFile(outPath, "utf8")); + expect(validateModule(module)).toEqual([]); + const spread = module.functions.find((fn) => fn.name.endsWith("spread")); + expect(spread).toBeDefined(); + const guards = recordsOf(spread).filter( + (record) => + record.kind === "ternary" && + (record.cond as IrRecord | undefined)?.kind === "libCall" && + (record.cond as IrRecord).fn === "dyn.sameValue", + ); + expect(guards).toHaveLength(0); +}); diff --git a/packages/compiler/test/package-shim-guard.test.ts b/packages/compiler/test/package-shim-guard.test.ts index ff01acbb8..a8620c1ce 100644 --- a/packages/compiler/test/package-shim-guard.test.ts +++ b/packages/compiler/test/package-shim-guard.test.ts @@ -49,7 +49,10 @@ describe("package shims compile without refusals", () => { const entries = packageShimEntries(); test("the registry lists its shim modules", () => { expect(entries.map((e) => e.slice(shimRoot.length + 1))).toEqual([ + "chalk/v5.ts", "fs-extra/index.ts", + "hot-shots/v8-unix-dgram.ts", + "hot-shots/v8.ts", "undici/v5.ts", "undici/v7.ts", ]); diff --git a/packages/compiler/test/parameter-version-emission.test.ts b/packages/compiler/test/parameter-version-emission.test.ts new file mode 100644 index 000000000..eb6daff56 --- /dev/null +++ b/packages/compiler/test/parameter-version-emission.test.ts @@ -0,0 +1,71 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile } from "../src/index.js"; + +async function emitLlvm(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-parameter-version-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ll"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "llvm" }); + expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true); + return await readFile(outPath, "utf8"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +const versions = (llvm: string): string[] => [ + ...new Set([...llvm.matchAll(/^define [^@]*@sc_\w+?_(\w+?)_int32Params\b/gm)].map((m) => m[1]!)), +]; + +const loops = ` +function sumTo(n: number): number { + let s = 0; + for (let i = 0; i < n; i++) s += i; + return s; +} +function literal(n: number): number { + let s = 0; + for (let i = 0; i < n; i++) s += i; + return s; +} +function recursive(n: number): number { + let s = 0; + for (let i = 0; i < n; i++) s += i; + return n > 0 ? s + recursive(n - 1) : s; +} +function unbounded(n: number): number { + let s = 0; + for (let i = 0; i < 10; i++) s += n; + return s; +} +class Counter { + total(n: number): number { + let s = 0; + for (let i = 0; i < n; i++) s += i; + return s; + } +} +const n = Number(process.argv.length > 5 ? "7.5" : "6"); +console.log(sumTo(n), literal(4), literal(5), recursive(n), unbounded(n), new Counter().total(n)); +`; + +test("a loop-bounding parameter some caller passes unproven gets an int32 version", async () => { + const llvm = await emitLlvm(loops); + expect(versions(llvm)).toEqual(["sumTo"]); + // The version takes the parameter as an i32, and the body dispatches to it + // after testing for an int32 that is not -0. + expect(llvm).toMatch(/^define [^@]*@sc_\w+_sumTo_int32Params\.i32\(i32 /m); + const body = /^define [^@]*@sc_\w+?_sumTo\([^]*?^}/m.exec(llvm)?.[0] ?? ""; + expect(body).toMatch(/call [^@]*@sc_\w+_sumTo_int32Params/); + expect(body).toMatch(/fdiv double 0x3FF0000000000000, %\S+/); +}); + +test("a module that reads stacks keeps a single body", async () => { + const llvm = await emitLlvm(`${loops}\nconsole.log(typeof new Error("x").stack);\n`); + expect(versions(llvm)).toEqual([]); +}); diff --git a/packages/compiler/test/running-extrema-branches.test.ts b/packages/compiler/test/running-extrema-branches.test.ts new file mode 100644 index 000000000..e1f5a7837 --- /dev/null +++ b/packages/compiler/test/running-extrema-branches.test.ts @@ -0,0 +1,75 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile } from "../src/index.js"; + +async function emitLlvm(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-running-extrema-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ll"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "llvm" }); + if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n")); + return await readFile(outPath, "utf8"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +const RARE = + /br i1 %\S+, label %if\.t\d+, label %if\.j\d+, !prof !\{!"branch_weights", i32 1, i32 1000\}/; + +test("a running maximum's update is weighted as rarely taken", async () => { + const llvm = await emitLlvm(` + function argmax(a: Float64Array): number { + let max = -Infinity; + let at = -1; + for (let i = 0; i < a.length; i++) { + const v = a[i]!; + if (v > max) { max = v; at = i; } + } + return at; + } + console.log(argmax(new Float64Array([1, 3, 2]))); + `); + expect(llvm).toMatch(RARE); +}); + +test("a callback's update of a captured minimum is weighted", async () => { + const llvm = await emitLlvm(` + function least(values: number[]): number { + let min = Infinity; + values.forEach((v) => { if (min > v) min = v; }); + return min; + } + console.log(least([3, 1, 2])); + `); + expect(llvm).toMatch(RARE); +}); + +test("clamps and saturating sums keep their selects", async () => { + const llvm = await emitLlvm(` + function clampSum(a: Float64Array, hi: number): number { + let s = 0; + for (let i = 0; i < a.length; i++) { + let v = a[i]!; + if (hi < v) v = hi; + s += v; + } + return s; + } + function saturate(a: Float64Array, hi: number): number { + let v = 0; + for (let i = 0; i < a.length; i++) { + v += a[i]!; + if (hi < v) v = hi; + } + return v; + } + const a = new Float64Array([0.5, 2, 0.25]); + console.log(clampSum(a, 1), saturate(a, 1)); + `); + expect(llvm).not.toContain("branch_weights"); +}); diff --git a/packages/compiler/test/speculative-loop-emission.test.ts b/packages/compiler/test/speculative-loop-emission.test.ts index a691d856d..a81348102 100644 --- a/packages/compiler/test/speculative-loop-emission.test.ts +++ b/packages/compiler/test/speculative-loop-emission.test.ts @@ -78,15 +78,14 @@ test("a failed check finishes its iteration outside the ordinary copy", async () test("a loop site stops entering its fast copy after repeated bails", async () => { const llvm = await emitLlvm(source); - const site = /^(@scr\.spec\.bails\.\d+) = internal global i8 0$/m.exec(llvm)?.[1]; + const site = /^(@sc_spec_bails_\d+) = internal global i8 0$/m.exec(llvm)?.[1]; expect(site).toBeDefined(); const fn = /^define internal double @sc_bf_scaled\([^]*?^}/m.exec(llvm)?.[0] ?? ""; - const escaped = site!.replace(/\./g, "\\."); // The entry test reads the count, a bail adds one, and a completed fast // copy clears it. expect(fn).toMatch( new RegExp( - `(%\\S+) = load atomic i8, ptr ${escaped} unordered, align 1\\n\\s*%\\S+ = icmp ult i8 \\1, 8`, + `(%\\S+) = load atomic i8, ptr ${site} unordered, align 1\\n\\s*%\\S+ = icmp ult i8 \\1, 8`, ), ); expect(fn).toMatch(new RegExp(`call i8 @llvm\\.uadd\\.sat\\.i8\\(i8 %\\S+, i8 1\\)`)); diff --git a/packages/compiler/test/string-char-loop-emission.test.ts b/packages/compiler/test/string-char-loop-emission.test.ts new file mode 100644 index 000000000..6f5756ce8 --- /dev/null +++ b/packages/compiler/test/string-char-loop-emission.test.ts @@ -0,0 +1,71 @@ +import { execFileSync } from "node:child_process"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule } from "../src/index.js"; +import { emitLlvmModule } from "../src/backend/llvm/emitter.js"; + +test("character reads of ASCII strings in versioned loops read bytes", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-char-loops-")); + try { + const entry = join(dir, "main.ts"); + await writeFile( + entry, + ` +function spaces(s: string): number { + let n = 0; + for (let i = 0; i < s.length; i++) if (s[i] === " ") n++; + return n; +} +function letters(s: string): number { + let n = 0; + for (let i = 0; i < s.length; i++) { + const c = s.charAt(i); + if (c >= "a" && c <= "z") n++; + } + return n; +} +function chars(s: string): string[] { + const out: string[] = []; + for (let i = 0; i < s.length; i++) out.push(s.charAt(i)); + return out; +} +console.log(spaces("a b c"), letters("abC1z"), chars("xyz").join("-"), chars("é!").join("-")); +`, + ); + const outPath = join(dir, "main.ir.json"); + const lowered = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + if (!lowered.ok) + throw new Error(lowered.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const ll = emitLlvmModule(deserializeModule(await readFile(outPath, "utf8"))); + const body = (name: string): string => + ll.match( + new RegExp(`define internal [^\\n]+ @sc_bf_${name}\\([^\\n]*\\) #0 \\{[\\s\\S]*?\\n\\}`), + )![0]; + // The fast copy follows the checked one. + const fast = (fn: string): string => fn.slice(fn.lastIndexOf("bytes.version.fast")); + + // `s[i] === " "` compares the byte; the checked copy keeps the helper. + const spaces = body("spaces"); + expect(fast(spaces)).toMatch(/load i8, ptr %\S+\n\s+%\S+ = uitofp i8/); + expect(fast(spaces)).not.toContain("@sc_str_char_code_at_int"); + expect(spaces).toContain("@sc_str_char_code_at_int"); + // A character local compared with one-unit literals holds its unit. + const letters = body("letters"); + expect(letters).not.toContain("@sc_str_cmp_u16"); + expect(fast(letters)).toMatch(/load i8, ptr %\S+\n\s+%\S+ = uitofp i8/); + // A character used as a string is its immortal table entry. + const chars = body("chars"); + expect(fast(chars)).toContain("@sc_ascii_chars"); + expect(fast(chars)).not.toContain("@sc_str_char_at_int"); + expect(chars).toContain("@sc_str_char_at_int"); + + const exe = join(dir, "main"); + const built = await compile(entry, { outputKind: "exe", outDir: dir, outPath: exe }); + if (!built.ok) throw new Error(JSON.stringify(built.diagnostics)); + expect(execFileSync(exe, { encoding: "utf8" })).toBe("2 3 x-y-z é-!\n"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4638-contract-program-lifetime-arrays.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4638-contract-program-lifetime-arrays.ts.json new file mode 100644 index 000000000..a89ae97b8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4638-contract-program-lifetime-arrays.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/4638-contract-program-lifetime-arrays.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4639-contract-program-lifetime-maps.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4639-contract-program-lifetime-maps.ts.json new file mode 100644 index 000000000..5f7afb8e7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4639-contract-program-lifetime-maps.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/4639-contract-program-lifetime-maps.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4814-integer-or-nan-indexes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4814-integer-or-nan-indexes.ts.json new file mode 100644 index 000000000..0a6abb5a5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4814-integer-or-nan-indexes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/4814-integer-or-nan-indexes.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4815-touint32-wide-magnitudes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4815-touint32-wide-magnitudes.ts.json new file mode 100644 index 000000000..80dcce24d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4815-touint32-wide-magnitudes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/4815-touint32-wide-magnitudes.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6349-instanceof-subclass-capsules/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6349-instanceof-subclass-capsules/main.ts.json new file mode 100644 index 000000000..6b31ee9ee --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6349-instanceof-subclass-capsules/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/6349-instanceof-subclass-capsules/errors.cjs", + "/tests/corpus/6349-instanceof-subclass-capsules/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6913-builtin-constructor-shorthand.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6913-builtin-constructor-shorthand.ts.json new file mode 100644 index 000000000..bd7194022 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6913-builtin-constructor-shorthand.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6913-builtin-constructor-shorthand.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6914-dynamic-builtin-construct.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6914-dynamic-builtin-construct.ts.json new file mode 100644 index 000000000..9bc7d8342 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6914-dynamic-builtin-construct.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6914-dynamic-builtin-construct.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6915-dynamic-builtin-construct.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6915-dynamic-builtin-construct.mjs.json new file mode 100644 index 000000000..a817ad3d2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6915-dynamic-builtin-construct.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6915-dynamic-builtin-construct.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6915-key-order-narrowed-throw.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6915-key-order-narrowed-throw.ts.json new file mode 100644 index 000000000..83f8cc5d5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6915-key-order-narrowed-throw.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6915-key-order-narrowed-throw.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6916-index-record-spread-overrides.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6916-index-record-spread-overrides.ts.json new file mode 100644 index 000000000..db4bc557f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6916-index-record-spread-overrides.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6916-index-record-spread-overrides.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6916-local-class-static-self-reference.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6916-local-class-static-self-reference.mjs.json new file mode 100644 index 000000000..aaefe846a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6916-local-class-static-self-reference.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6916-local-class-static-self-reference.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6917-local-class-expression-self-reference.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6917-local-class-expression-self-reference.ts.json new file mode 100644 index 000000000..41d005bd0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6917-local-class-expression-self-reference.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6917-local-class-expression-self-reference.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6918-local-class-expression-self-reference.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6918-local-class-expression-self-reference.mjs.json new file mode 100644 index 000000000..afed7a70d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6918-local-class-expression-self-reference.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6918-local-class-expression-self-reference.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6919-local-class-inspect-name.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6919-local-class-inspect-name.mjs.json new file mode 100644 index 000000000..229c01bcd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6919-local-class-inspect-name.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6919-local-class-inspect-name.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6921-local-subclass-override-dispatch.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6921-local-subclass-override-dispatch.ts.json new file mode 100644 index 000000000..1878554c1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6921-local-subclass-override-dispatch.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6921-local-subclass-override-dispatch.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6922-local-subclass-override-dispatch.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6922-local-subclass-override-dispatch.mjs.json new file mode 100644 index 000000000..4da840cbc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6922-local-subclass-override-dispatch.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6922-local-subclass-override-dispatch.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6923-instanceof-builtin-constructor-values.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6923-instanceof-builtin-constructor-values.mjs.json new file mode 100644 index 000000000..a2184f9a3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6923-instanceof-builtin-constructor-values.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6923-instanceof-builtin-constructor-values.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6924-instanceof-class-values-native.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6924-instanceof-class-values-native.mjs.json new file mode 100644 index 000000000..54090ccc3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6924-instanceof-class-values-native.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6924-instanceof-class-values-native.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6925-instanceof-dynamic-constructors.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6925-instanceof-dynamic-constructors.ts.json new file mode 100644 index 000000000..20085dc0d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6925-instanceof-dynamic-constructors.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6925-instanceof-dynamic-constructors.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6926-dynamic-collection-seeds.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6926-dynamic-collection-seeds.mjs.json new file mode 100644 index 000000000..32edfc402 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6926-dynamic-collection-seeds.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6926-dynamic-collection-seeds.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6927-dynamic-collection-seeds.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6927-dynamic-collection-seeds.ts.json new file mode 100644 index 000000000..89c591dd7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6927-dynamic-collection-seeds.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6927-dynamic-collection-seeds.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6998-typed-array-while-loop-versions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6998-typed-array-while-loop-versions.ts.json new file mode 100644 index 000000000..f5cfba0cc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6998-typed-array-while-loop-versions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6998-typed-array-while-loop-versions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7002-empty-array-contextual-slots/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7002-empty-array-contextual-slots/main.ts.json new file mode 100644 index 000000000..d22715215 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7002-empty-array-contextual-slots/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7002-empty-array-contextual-slots/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7005-dyn-constructor-instanceof/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7005-dyn-constructor-instanceof/main.ts.json new file mode 100644 index 000000000..313888b49 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7005-dyn-constructor-instanceof/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7005-dyn-constructor-instanceof/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7006-evolving-array-aliases.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7006-evolving-array-aliases.ts.json new file mode 100644 index 000000000..cb3d36498 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7006-evolving-array-aliases.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7006-evolving-array-aliases.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7007-filter-written-type-predicates.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7007-filter-written-type-predicates.ts.json new file mode 100644 index 000000000..d77ee1a57 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7007-filter-written-type-predicates.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7007-filter-written-type-predicates.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7008-js-unit-initialized-properties/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7008-js-unit-initialized-properties/main.ts.json new file mode 100644 index 000000000..85f5a6e12 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7008-js-unit-initialized-properties/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7008-js-unit-initialized-properties/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7009-finalization-registry-wasm-loader/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7009-finalization-registry-wasm-loader/main.ts.json new file mode 100644 index 000000000..89ee292b3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7009-finalization-registry-wasm-loader/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7009-finalization-registry-wasm-loader/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7011-for-in-dictionary-deletes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7011-for-in-dictionary-deletes.ts.json new file mode 100644 index 000000000..3b2b79c3b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7011-for-in-dictionary-deletes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7011-for-in-dictionary-deletes.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7014-js-never-array-literal-fields/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7014-js-never-array-literal-fields/main.ts.json new file mode 100644 index 000000000..a67c77476 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7014-js-never-array-literal-fields/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7014-js-never-array-literal-fields/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7020-conditional-spread-fields.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7020-conditional-spread-fields.ts.json new file mode 100644 index 000000000..f6c5594a1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7020-conditional-spread-fields.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7020-conditional-spread-fields.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7021-promise-all-uniform-tuple.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7021-promise-all-uniform-tuple.ts.json new file mode 100644 index 000000000..c1e5f1ae4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7021-promise-all-uniform-tuple.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7021-promise-all-uniform-tuple.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7022-promise-all-dynamic-arrays.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7022-promise-all-dynamic-arrays.ts.json new file mode 100644 index 000000000..c1856ac5a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7022-promise-all-dynamic-arrays.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7022-promise-all-dynamic-arrays.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7023-union-object-patterns.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7023-union-object-patterns.ts.json new file mode 100644 index 000000000..bae130837 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7023-union-object-patterns.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7023-union-object-patterns.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7024-async-return-fresh-collections.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7024-async-return-fresh-collections.ts.json new file mode 100644 index 000000000..c4b9855ef --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7024-async-return-fresh-collections.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7024-async-return-fresh-collections.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7025-child-listener-chaining.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7025-child-listener-chaining.ts.json new file mode 100644 index 000000000..ed0e7b650 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7025-child-listener-chaining.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7025-child-listener-chaining.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7026-url-object-patterns.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7026-url-object-patterns.ts.json new file mode 100644 index 000000000..f050c4d1a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7026-url-object-patterns.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7026-url-object-patterns.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7027-search-params-conditional-init.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7027-search-params-conditional-init.ts.json new file mode 100644 index 000000000..9b69861b1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7027-search-params-conditional-init.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7027-search-params-conditional-init.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7028-non-empty-rest-tuples.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7028-non-empty-rest-tuples.ts.json new file mode 100644 index 000000000..a40ef1180 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7028-non-empty-rest-tuples.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7028-non-empty-rest-tuples.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7029-flatmap-union-results.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7029-flatmap-union-results.ts.json new file mode 100644 index 000000000..72d564d3b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7029-flatmap-union-results.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7029-flatmap-union-results.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7030-spawn-sync-env.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7030-spawn-sync-env.ts.json new file mode 100644 index 000000000..f8afbe00b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7030-spawn-sync-env.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7030-spawn-sync-env.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7033-any-fields-into-declared-records.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7033-any-fields-into-declared-records.ts.json new file mode 100644 index 000000000..57e76b621 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7033-any-fields-into-declared-records.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7033-any-fields-into-declared-records.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7034-optional-field-union-routes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7034-optional-field-union-routes.ts.json new file mode 100644 index 000000000..a78b23cb1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7034-optional-field-union-routes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7034-optional-field-union-routes.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7036-child-env-path-lookup.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7036-child-env-path-lookup.ts.json new file mode 100644 index 000000000..171ab060a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7036-child-env-path-lookup.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7036-child-env-path-lookup.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7037-js-var-open-writes/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7037-js-var-open-writes/main.ts.json new file mode 100644 index 000000000..a1bdb91b8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7037-js-var-open-writes/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7037-js-var-open-writes/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7038-js-var-later-writes.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7038-js-var-later-writes.mjs.json new file mode 100644 index 000000000..1e51a088f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7038-js-var-later-writes.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7038-js-var-later-writes.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7060-array-numeric-keys.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7060-array-numeric-keys.cjs.json new file mode 100644 index 000000000..5fff9651a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7060-array-numeric-keys.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7060-array-numeric-keys.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7061-array-object-keys.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7061-array-object-keys.ts.json new file mode 100644 index 000000000..b2830e983 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7061-array-object-keys.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7061-array-object-keys.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7062-inspect-array-properties.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7062-inspect-array-properties.ts.json new file mode 100644 index 000000000..891c4ffe0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7062-inspect-array-properties.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7062-inspect-array-properties.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7143-optional-read-integer-index.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7143-optional-read-integer-index.ts.json new file mode 100644 index 000000000..ae0d1f610 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7143-optional-read-integer-index.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7143-optional-read-integer-index.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7144-paired-while-counters.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7144-paired-while-counters.ts.json new file mode 100644 index 000000000..678cbd8b0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7144-paired-while-counters.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7144-paired-while-counters.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7150-cipher-legacy-modes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7150-cipher-legacy-modes.ts.json new file mode 100644 index 000000000..c0e5f9812 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7150-cipher-legacy-modes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7150-cipher-legacy-modes.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7151-cipher-legacy-errors.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7151-cipher-legacy-errors.cjs.json new file mode 100644 index 000000000..1f37c6b64 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7151-cipher-legacy-errors.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7151-cipher-legacy-errors.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7152-crypto-prime.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7152-crypto-prime.ts.json new file mode 100644 index 000000000..722ebaacb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7152-crypto-prime.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7152-crypto-prime.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7153-crypto-prime-errors.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7153-crypto-prime-errors.cjs.json new file mode 100644 index 000000000..9d121895a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7153-crypto-prime-errors.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7153-crypto-prime-errors.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7184-reduce-runtime-accumulator.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7184-reduce-runtime-accumulator.ts.json new file mode 100644 index 000000000..b93eb61b6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7184-reduce-runtime-accumulator.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7184-reduce-runtime-accumulator.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7185-array-callback-arguments-length.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7185-array-callback-arguments-length.ts.json new file mode 100644 index 000000000..2514bcaa8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7185-array-callback-arguments-length.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7185-array-callback-arguments-length.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7246-stream-construct.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7246-stream-construct.ts.json new file mode 100644 index 000000000..f1dcb625f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7246-stream-construct.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7246-stream-construct.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7247-stream-construct.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7247-stream-construct.mjs.json new file mode 100644 index 000000000..00c6c7f0c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7247-stream-construct.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7247-stream-construct.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7248-stream-writev.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7248-stream-writev.ts.json new file mode 100644 index 000000000..0faf76b0f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7248-stream-writev.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7248-stream-writev.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7249-buffer-index-of-candidates.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7249-buffer-index-of-candidates.ts.json new file mode 100644 index 000000000..f5299cb9a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7249-buffer-index-of-candidates.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7249-buffer-index-of-candidates.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7249-stream-writev.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7249-stream-writev.cjs.json new file mode 100644 index 000000000..5374e215e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7249-stream-writev.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7249-stream-writev.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7250-stream-decode-strings.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7250-stream-decode-strings.ts.json new file mode 100644 index 000000000..67d2b759b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7250-stream-decode-strings.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7250-stream-decode-strings.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7251-stream-write-encodings.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7251-stream-write-encodings.mjs.json new file mode 100644 index 000000000..d4c01fab9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7251-stream-write-encodings.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7251-stream-write-encodings.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7252-duplex-from-generator.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7252-duplex-from-generator.ts.json new file mode 100644 index 000000000..d29eed123 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7252-duplex-from-generator.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7252-duplex-from-generator.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7253-duplex-from.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7253-duplex-from.mjs.json new file mode 100644 index 000000000..e12ce5a52 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7253-duplex-from.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7253-duplex-from.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7254-compose-functions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7254-compose-functions.ts.json new file mode 100644 index 000000000..07e468aa2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7254-compose-functions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7254-compose-functions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7255-compose-stages.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7255-compose-stages.mjs.json new file mode 100644 index 000000000..036d9836e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7255-compose-stages.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7255-compose-stages.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7256-readable-compose.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7256-readable-compose.mjs.json new file mode 100644 index 000000000..0b5993d32 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7256-readable-compose.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7256-readable-compose.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7257-pipeline-generator-destination.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7257-pipeline-generator-destination.mjs.json new file mode 100644 index 000000000..79354d9ce --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7257-pipeline-generator-destination.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7257-pipeline-generator-destination.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7258-pipeline-generator-destination.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7258-pipeline-generator-destination.ts.json new file mode 100644 index 000000000..313c9f19e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7258-pipeline-generator-destination.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7258-pipeline-generator-destination.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7259-promises-pipeline-stage-options.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7259-promises-pipeline-stage-options.cjs.json new file mode 100644 index 000000000..6fc082ea1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7259-promises-pipeline-stage-options.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7259-promises-pipeline-stage-options.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7261-acyclic-result-builders.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7261-acyclic-result-builders.ts.json new file mode 100644 index 000000000..4f4afd821 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7261-acyclic-result-builders.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7261-acyclic-result-builders.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7262-fresh-literal-untyped-param/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7262-fresh-literal-untyped-param/main.ts.json new file mode 100644 index 000000000..55a678e1b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7262-fresh-literal-untyped-param/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/7262-fresh-literal-untyped-param/lib.js", + "/tests/corpus/7262-fresh-literal-untyped-param/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7263-error-accessor-object-helper.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7263-error-accessor-object-helper.ts.json new file mode 100644 index 000000000..8bf8f11da --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7263-error-accessor-object-helper.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7263-error-accessor-object-helper.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7301-running-extrema-branches.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7301-running-extrema-branches.ts.json new file mode 100644 index 000000000..6dcb4b57d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7301-running-extrema-branches.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7301-running-extrema-branches.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7302-string-scan-loop-versions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7302-string-scan-loop-versions.ts.json new file mode 100644 index 000000000..feda0ecf7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7302-string-scan-loop-versions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7302-string-scan-loop-versions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7303-string-char-loop-versions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7303-string-char-loop-versions.ts.json new file mode 100644 index 000000000..58242e583 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7303-string-char-loop-versions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7303-string-char-loop-versions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7322-union-scalar-equality.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7322-union-scalar-equality.ts.json new file mode 100644 index 000000000..73257a3f4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7322-union-scalar-equality.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7322-union-scalar-equality.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7323-loop-bound-parameter-versions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7323-loop-bound-parameter-versions.ts.json new file mode 100644 index 000000000..0a79c8cdb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7323-loop-bound-parameter-versions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7323-loop-bound-parameter-versions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7324-loop-bound-parameter-window-sums.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7324-loop-bound-parameter-window-sums.ts.json new file mode 100644 index 000000000..aedd853fb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7324-loop-bound-parameter-window-sums.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7324-loop-bound-parameter-window-sums.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7325-array-index-products-near-2-53.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7325-array-index-products-near-2-53.ts.json new file mode 100644 index 000000000..3239c26bf --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7325-array-index-products-near-2-53.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7325-array-index-products-near-2-53.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7326-int32-element-arrays.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7326-int32-element-arrays.ts.json new file mode 100644 index 000000000..f00823a6e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7326-int32-element-arrays.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7326-int32-element-arrays.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7327-int32-array-widening.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7327-int32-array-widening.ts.json new file mode 100644 index 000000000..050b6a9ac --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7327-int32-array-widening.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7327-int32-array-widening.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7362-globalthis-intersection-singleton.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7362-globalthis-intersection-singleton.ts.json new file mode 100644 index 000000000..74a21dfc5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7362-globalthis-intersection-singleton.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7362-globalthis-intersection-singleton.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7363-json-stringify-live-capsules.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7363-json-stringify-live-capsules.ts.json new file mode 100644 index 000000000..580e84ff0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7363-json-stringify-live-capsules.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7363-json-stringify-live-capsules.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7364-new-target-late-instantiations.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7364-new-target-late-instantiations.ts.json new file mode 100644 index 000000000..20d761c6c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7364-new-target-late-instantiations.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7364-new-target-late-instantiations.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7365-class-methods-untyped-computed-writes/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7365-class-methods-untyped-computed-writes/main.ts.json new file mode 100644 index 000000000..58cdacb62 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7365-class-methods-untyped-computed-writes/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/7365-class-methods-untyped-computed-writes/untyped.js", + "/tests/corpus/7365-class-methods-untyped-computed-writes/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7366-typed-global-view-arrays.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7366-typed-global-view-arrays.ts.json new file mode 100644 index 000000000..49da80b55 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7366-typed-global-view-arrays.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7366-typed-global-view-arrays.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7380-iterable-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7380-iterable-values.ts.json new file mode 100644 index 000000000..cf42c1888 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7380-iterable-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7380-iterable-values.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-iterable-forof-semantics.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-iterable-forof-semantics.ts.json new file mode 100644 index 000000000..824dfe11c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-iterable-forof-semantics.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7381-iterable-forof-semantics.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7382-iterable-generic-repository.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7382-iterable-generic-repository.ts.json new file mode 100644 index 000000000..7ac2b807f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7382-iterable-generic-repository.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7382-iterable-generic-repository.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7383-iterable-union-sources.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7383-iterable-union-sources.ts.json new file mode 100644 index 000000000..ea8476397 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7383-iterable-union-sources.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7383-iterable-union-sources.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7400-record-keyof-union-read.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7400-record-keyof-union-read.ts.json new file mode 100644 index 000000000..4f2a4c712 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7400-record-keyof-union-read.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7400-record-keyof-union-read.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7401-any-spread-into-typed-literal.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7401-any-spread-into-typed-literal.ts.json new file mode 100644 index 000000000..3e3749b3f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7401-any-spread-into-typed-literal.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7401-any-spread-into-typed-literal.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7401-program-lifetime-array-teardown.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7401-program-lifetime-array-teardown.ts.json new file mode 100644 index 000000000..4341cdbdc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7401-program-lifetime-array-teardown.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7401-program-lifetime-array-teardown.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7402-unknown-array-methods.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7402-unknown-array-methods.ts.json new file mode 100644 index 000000000..db33da675 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7402-unknown-array-methods.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7402-unknown-array-methods.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7403-bigint-of-typed-arrays.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7403-bigint-of-typed-arrays.mjs.json new file mode 100644 index 000000000..476611adb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7403-bigint-of-typed-arrays.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7403-bigint-of-typed-arrays.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7403-dyn-string-at.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7403-dyn-string-at.mjs.json new file mode 100644 index 000000000..160a78438 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7403-dyn-string-at.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7403-dyn-string-at.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7420-for-await-close.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7420-for-await-close.ts.json new file mode 100644 index 000000000..6e48bbc1c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7420-for-await-close.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7420-for-await-close.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7421-for-await-close.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7421-for-await-close.mjs.json new file mode 100644 index 000000000..dea911d1b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7421-for-await-close.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7421-for-await-close.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7422-for-await-untyped-streams.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7422-for-await-untyped-streams.mjs.json new file mode 100644 index 000000000..c2677d0b9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7422-for-await-untyped-streams.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7422-for-await-untyped-streams.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7423-stream-untyped-bindings.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7423-stream-untyped-bindings.mjs.json new file mode 100644 index 000000000..180721bd6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7423-stream-untyped-bindings.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7423-stream-untyped-bindings.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7424-duplex-from-streams.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7424-duplex-from-streams.mjs.json new file mode 100644 index 000000000..a3801c775 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7424-duplex-from-streams.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7424-duplex-from-streams.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7425-duplex-from-streams.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7425-duplex-from-streams.ts.json new file mode 100644 index 000000000..069ae4ef4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7425-duplex-from-streams.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7425-duplex-from-streams.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7426-duplex-from-promise.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7426-duplex-from-promise.mjs.json new file mode 100644 index 000000000..e44b3f001 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7426-duplex-from-promise.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7426-duplex-from-promise.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7427-duplex-from-async-function.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7427-duplex-from-async-function.mjs.json new file mode 100644 index 000000000..22d092c60 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7427-duplex-from-async-function.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7427-duplex-from-async-function.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7428-web-stream-sources.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7428-web-stream-sources.mjs.json new file mode 100644 index 000000000..e17735658 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7428-web-stream-sources.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7428-web-stream-sources.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7429-web-stream-sources.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7429-web-stream-sources.ts.json new file mode 100644 index 000000000..8707c2f2b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7429-web-stream-sources.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7429-web-stream-sources.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7430-readable-from-web.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7430-readable-from-web.mjs.json new file mode 100644 index 000000000..3e26c3ba0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7430-readable-from-web.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7430-readable-from-web.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7431-readable-from-web.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7431-readable-from-web.ts.json new file mode 100644 index 000000000..6461c53af --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7431-readable-from-web.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7431-readable-from-web.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7432-readable-push-views.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7432-readable-push-views.mjs.json new file mode 100644 index 000000000..ffde7918d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7432-readable-push-views.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7432-readable-push-views.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7433-readable-operator-signal.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7433-readable-operator-signal.mjs.json new file mode 100644 index 000000000..5de5d9839 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7433-readable-operator-signal.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7433-readable-operator-signal.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7434-readable-operator-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7434-readable-operator-values.ts.json new file mode 100644 index 000000000..c374d9f03 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7434-readable-operator-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7434-readable-operator-values.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7435-readable-flatmap-iterables.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7435-readable-flatmap-iterables.mjs.json new file mode 100644 index 000000000..0c3697807 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7435-readable-flatmap-iterables.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7435-readable-flatmap-iterables.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7436-readable-read-any-value.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7436-readable-read-any-value.mjs.json new file mode 100644 index 000000000..6c2f63e17 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7436-readable-read-any-value.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7436-readable-read-any-value.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7437-duplex-from-signal.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7437-duplex-from-signal.mjs.json new file mode 100644 index 000000000..3b0699407 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7437-duplex-from-signal.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7437-duplex-from-signal.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7438-duplex-from-signal.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7438-duplex-from-signal.ts.json new file mode 100644 index 000000000..0702ed0da --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7438-duplex-from-signal.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7438-duplex-from-signal.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7439-readable-read-string-chunks.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7439-readable-read-string-chunks.ts.json new file mode 100644 index 000000000..e3dab7078 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7439-readable-read-string-chunks.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7439-readable-read-string-chunks.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7440-fromentries-aliased-entries.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7440-fromentries-aliased-entries.ts.json new file mode 100644 index 000000000..108193bac --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7440-fromentries-aliased-entries.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7440-fromentries-aliased-entries.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7441-untyped-receiver-own-string-method-names.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7441-untyped-receiver-own-string-method-names.ts.json new file mode 100644 index 000000000..7477193ef --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7441-untyped-receiver-own-string-method-names.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7441-untyped-receiver-own-string-method-names.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7445-crypto-key-pair-options.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7445-crypto-key-pair-options.mjs.json new file mode 100644 index 000000000..5d57afbef --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7445-crypto-key-pair-options.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7445-crypto-key-pair-options.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7446-crypto-rsa-key-pair-errors.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7446-crypto-rsa-key-pair-errors.ts.json new file mode 100644 index 000000000..f055ddab9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7446-crypto-rsa-key-pair-errors.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7446-crypto-rsa-key-pair-errors.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7480-builtin-constructor-in.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7480-builtin-constructor-in.js.json new file mode 100644 index 000000000..efb9dd725 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7480-builtin-constructor-in.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7480-builtin-constructor-in.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7480-class-call-without-new.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7480-class-call-without-new.ts.json new file mode 100644 index 000000000..8e5ed83bc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7480-class-call-without-new.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7480-class-call-without-new.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7481-class-call-without-new.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7481-class-call-without-new.mjs.json new file mode 100644 index 000000000..7f55a233d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7481-class-call-without-new.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7481-class-call-without-new.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7481-class-runtime-heritage-object-error.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7481-class-runtime-heritage-object-error.js.json new file mode 100644 index 000000000..d0d390662 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7481-class-runtime-heritage-object-error.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7481-class-runtime-heritage-object-error.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7482-define-property-function-class-values.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7482-define-property-function-class-values.js.json new file mode 100644 index 000000000..d1d97ba4f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7482-define-property-function-class-values.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7482-define-property-function-class-values.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7483-constructor-factory-traits.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7483-constructor-factory-traits.js.json new file mode 100644 index 000000000..c3fa9964f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7483-constructor-factory-traits.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7483-constructor-factory-traits.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7484-hoisted-function-captures-later-class.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7484-hoisted-function-captures-later-class.js.json new file mode 100644 index 000000000..515b9bf97 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7484-hoisted-function-captures-later-class.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7484-hoisted-function-captures-later-class.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7485-global-object-own-properties.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7485-global-object-own-properties.js.json new file mode 100644 index 000000000..aa1affb3c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7485-global-object-own-properties.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7485-global-object-own-properties.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7486-prototype-accessor-string-method-names.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7486-prototype-accessor-string-method-names.js.json new file mode 100644 index 000000000..0cf87c443 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7486-prototype-accessor-string-method-names.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7486-prototype-accessor-string-method-names.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7487-namespace-spread-untyped/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7487-namespace-spread-untyped/main.js.json new file mode 100644 index 000000000..b8a66c735 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7487-namespace-spread-untyped/main.js.json @@ -0,0 +1,9 @@ +{ + "order": [ + "/tests/corpus/7487-namespace-spread-untyped/shapes.js", + "/tests/corpus/7487-namespace-spread-untyped/extra.js", + "/tests/corpus/7487-namespace-spread-untyped/all.js", + "/tests/corpus/7487-namespace-spread-untyped/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7488-math-spread-untyped.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7488-math-spread-untyped.js.json new file mode 100644 index 000000000..4d1376a12 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7488-math-spread-untyped.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7488-math-spread-untyped.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7489-math-spread-generator-only.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7489-math-spread-generator-only.js.json new file mode 100644 index 000000000..5277102eb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7489-math-spread-generator-only.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7489-math-spread-generator-only.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7540-undefined-element-index-keys.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7540-undefined-element-index-keys.ts.json new file mode 100644 index 000000000..528758c4e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7540-undefined-element-index-keys.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7540-undefined-element-index-keys.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7541-prototype-identity-stand-ins.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7541-prototype-identity-stand-ins.ts.json new file mode 100644 index 000000000..c5179b475 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7541-prototype-identity-stand-ins.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7541-prototype-identity-stand-ins.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7542-prototype-identity-without-data.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7542-prototype-identity-without-data.ts.json new file mode 100644 index 000000000..09f1a6369 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7542-prototype-identity-without-data.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7542-prototype-identity-without-data.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7544-local-class-prototype-identity.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7544-local-class-prototype-identity.ts.json new file mode 100644 index 000000000..80cebd695 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7544-local-class-prototype-identity.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7544-local-class-prototype-identity.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7560-new-target-constructors.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7560-new-target-constructors.ts.json new file mode 100644 index 000000000..b4426af2c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7560-new-target-constructors.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7560-new-target-constructors.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7561-new-target-generic-registry.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7561-new-target-generic-registry.ts.json new file mode 100644 index 000000000..dc4eab819 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7561-new-target-generic-registry.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7561-new-target-generic-registry.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7565-iterable-replaceable-method-results.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7565-iterable-replaceable-method-results.ts.json new file mode 100644 index 000000000..60631a82f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7565-iterable-replaceable-method-results.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7565-iterable-replaceable-method-results.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7600-navigator-optional-chain.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7600-navigator-optional-chain.js.json new file mode 100644 index 000000000..12465d5e5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7600-navigator-optional-chain.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7600-navigator-optional-chain.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7602-error-stack-limit-value.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7602-error-stack-limit-value.js.json new file mode 100644 index 000000000..99e1b8904 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7602-error-stack-limit-value.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7602-error-stack-limit-value.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7603-error-stack-limit-own.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7603-error-stack-limit-own.js.json new file mode 100644 index 000000000..5ed656fa9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7603-error-stack-limit-own.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7603-error-stack-limit-own.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7604-error-stack-limit-frozen.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7604-error-stack-limit-frozen.js.json new file mode 100644 index 000000000..a437ff57e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7604-error-stack-limit-frozen.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7604-error-stack-limit-frozen.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7610-key-order-json.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7610-key-order-json.ts.json new file mode 100644 index 000000000..5048666d6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7610-key-order-json.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7610-key-order-json.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7611-error-message-accessor.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7611-error-message-accessor.ts.json new file mode 100644 index 000000000..ef262841b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7611-error-message-accessor.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7611-error-message-accessor.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7611-key-order-keys-values-entries.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7611-key-order-keys-values-entries.ts.json new file mode 100644 index 000000000..f800c62c5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7611-key-order-keys-values-entries.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7611-key-order-keys-values-entries.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7612-error-message-accessor-lazy.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7612-error-message-accessor-lazy.js.json new file mode 100644 index 000000000..bc852b7bd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7612-error-message-accessor-lazy.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7612-error-message-accessor-lazy.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7612-key-order-for-in.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7612-key-order-for-in.ts.json new file mode 100644 index 000000000..14745be37 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7612-key-order-for-in.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7612-key-order-for-in.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7613-key-order-inspect.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7613-key-order-inspect.ts.json new file mode 100644 index 000000000..50aa11977 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7613-key-order-inspect.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7613-key-order-inspect.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7613-local-class-prototype-reflection.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7613-local-class-prototype-reflection.js.json new file mode 100644 index 000000000..bbf827605 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7613-local-class-prototype-reflection.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7613-local-class-prototype-reflection.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7614-key-order-copies.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7614-key-order-copies.ts.json new file mode 100644 index 000000000..888715320 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7614-key-order-copies.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7614-key-order-copies.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7614-npm-local-class-per-instance/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7614-npm-local-class-per-instance/main.ts.json new file mode 100644 index 000000000..45f2b63ab --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7614-npm-local-class-per-instance/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7614-npm-local-class-per-instance/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7615-key-order-dynamic.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7615-key-order-dynamic.ts.json new file mode 100644 index 000000000..b2a517432 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7615-key-order-dynamic.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7615-key-order-dynamic.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7615-local-class-prototype-reflection-ts.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7615-local-class-prototype-reflection-ts.ts.json new file mode 100644 index 000000000..229a4e2ff --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7615-local-class-prototype-reflection-ts.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7615-local-class-prototype-reflection-ts.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7616-key-order-late-writes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7616-key-order-late-writes.ts.json new file mode 100644 index 000000000..d4ead81aa --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7616-key-order-late-writes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7616-key-order-late-writes.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7621-js-class-field-methods.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7621-js-class-field-methods.js.json new file mode 100644 index 000000000..a7e5fdd14 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7621-js-class-field-methods.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7621-js-class-field-methods.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7731-npm-any-string-index/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7731-npm-any-string-index/main.ts.json new file mode 100644 index 000000000..78fc40580 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7731-npm-any-string-index/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7731-npm-any-string-index/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7732-untyped-match-pattern.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7732-untyped-match-pattern.js.json new file mode 100644 index 000000000..f6355a2ae --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7732-untyped-match-pattern.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7732-untyped-match-pattern.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7733-property-bag-late-prototype.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7733-property-bag-late-prototype.js.json new file mode 100644 index 000000000..2b9778666 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7733-property-bag-late-prototype.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7733-property-bag-late-prototype.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7734-caught-instanceof-class-binding.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7734-caught-instanceof-class-binding.js.json new file mode 100644 index 000000000..9ce0ed4f0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7734-caught-instanceof-class-binding.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7734-caught-instanceof-class-binding.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7735-untyped-headers-iterators.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7735-untyped-headers-iterators.js.json new file mode 100644 index 000000000..7ca5d34da --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7735-untyped-headers-iterators.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7735-untyped-headers-iterators.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7738-private-logical-assignment-untyped.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7738-private-logical-assignment-untyped.js.json new file mode 100644 index 000000000..4865d3206 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7738-private-logical-assignment-untyped.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7738-private-logical-assignment-untyped.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/arguments-length-self-call.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/arguments-length-self-call.ts.json new file mode 100644 index 000000000..8ed5027e5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/arguments-length-self-call.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/arguments-length-self-call.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/class-call-shadowed-static.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/class-call-shadowed-static.ts.json new file mode 100644 index 000000000..6c87e5f66 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/class-call-shadowed-static.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/class-call-shadowed-static.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/filter-narrow.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/filter-narrow.ts.json deleted file mode 100644 index 40d5e462a..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/filter-narrow.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/diagnostics/filter-narrow.ts" - ], - "diags": [] -} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/fromentries-aliased-entries.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/fromentries-aliased-entries.ts.json deleted file mode 100644 index 7388f2b71..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/fromentries-aliased-entries.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/diagnostics/fromentries-aliased-entries.ts" - ], - "diags": [] -} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/iterable-sources.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/iterable-sources.ts.json new file mode 100644 index 000000000..1b47e8dd2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/iterable-sources.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/iterable-sources.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-late-local-subclass.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-late-local-subclass.ts.json new file mode 100644 index 000000000..7046e28d5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-late-local-subclass.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/new-target-late-local-subclass.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-local-subclass.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-local-subclass.ts.json new file mode 100644 index 000000000..4f597a8a2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-local-subclass.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/new-target-local-subclass.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-plain-function.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-plain-function.ts.json new file mode 100644 index 000000000..9f424457c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-plain-function.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/new-target-plain-function.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-value-generic-subclass.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-value-generic-subclass.ts.json new file mode 100644 index 000000000..1abcce3ad --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-value-generic-subclass.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/new-target-value-generic-subclass.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-value-signatures.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-value-signatures.ts.json new file mode 100644 index 000000000..249f8f4bd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-value-signatures.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/new-target-value-signatures.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/retired-engine-1123-any-conditional-spread.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/retired-engine-1123-any-conditional-spread.ts.json deleted file mode 100644 index 057d24d55..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/retired-engine-1123-any-conditional-spread.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/diagnostics/retired-engine-1123-any-conditional-spread.ts" - ], - "diags": [] -} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/chalk5-builder/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/chalk5-builder/main.ts.json new file mode 100644 index 000000000..8f7268daf --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/chalk5-builder/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/packages/compiler/shims/chalk/v5.ts", + "/tests/fixtures/npm/cases/chalk5-builder/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/chalk5/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/chalk5/main.ts.json index 065c96347..8365d4db9 100644 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/chalk5/main.ts.json +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/chalk5/main.ts.json @@ -1,5 +1,6 @@ { "order": [ + "/packages/compiler/shims/chalk/v5.ts", "/tests/fixtures/npm/cases/chalk5/main.ts" ], "diags": [] diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/decl-pass-through/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/decl-pass-through/main.ts.json new file mode 100644 index 000000000..1fd5083da --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/decl-pass-through/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/decl-pass-through/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-buffered/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-buffered/main.ts.json new file mode 100644 index 000000000..a48e1af78 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-buffered/main.ts.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/packages/compiler/shims/hot-shots/client.ts", + "/packages/compiler/shims/hot-shots/v8.ts", + "/tests/fixtures/npm/cases/hot-shots-buffered/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-send-error/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-send-error/main.ts.json new file mode 100644 index 000000000..716cfe02f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-send-error/main.ts.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/packages/compiler/shims/hot-shots/client.ts", + "/packages/compiler/shims/hot-shots/v8.ts", + "/tests/fixtures/npm/cases/hot-shots-send-error/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-tcp/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-tcp/main.ts.json new file mode 100644 index 000000000..f131361e8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-tcp/main.ts.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/packages/compiler/shims/hot-shots/client.ts", + "/packages/compiler/shims/hot-shots/v8.ts", + "/tests/fixtures/npm/cases/hot-shots-tcp/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-udp/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-udp/main.ts.json index 54baff890..a3014fb9f 100644 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-udp/main.ts.json +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-udp/main.ts.json @@ -1,5 +1,7 @@ { "order": [ + "/packages/compiler/shims/hot-shots/client.ts", + "/packages/compiler/shims/hot-shots/v8.ts", "/tests/fixtures/npm/cases/hot-shots-udp/main.ts" ], "diags": [] diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/js-own-tostring/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/js-own-tostring/main.ts.json new file mode 100644 index 000000000..eacdfc1d4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/js-own-tostring/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/js-own-tostring/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/jsdoc-unresolved-generic-return/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/jsdoc-unresolved-generic-return/main.ts.json new file mode 100644 index 000000000..5395e20be --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/jsdoc-unresolved-generic-return/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/jsdoc-unresolved-generic-return/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/types-boundary/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/types-boundary/main.ts.json new file mode 100644 index 000000000..0ef25fcbd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/types-boundary/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/types-boundary/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod3-adoption/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod3-adoption/main.ts.json new file mode 100644 index 000000000..72fa6d390 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod3-adoption/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/zod3-adoption/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod3-boundary/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod3-boundary/main.ts.json new file mode 100644 index 000000000..4b8187352 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod3-boundary/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/zod3-boundary/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod3-infer/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod3-infer/main.ts.json new file mode 100644 index 000000000..80cbb3523 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod3-infer/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/zod3-infer/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod3/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod3/main.ts.json new file mode 100644 index 000000000..c9ff338fa --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod3/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/zod3/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/union-scalar-equality-emission.test.ts b/packages/compiler/test/union-scalar-equality-emission.test.ts new file mode 100644 index 000000000..62d59e258 --- /dev/null +++ b/packages/compiler/test/union-scalar-equality-emission.test.ts @@ -0,0 +1,45 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile } from "../src/index.js"; + +async function emitLlvm(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-union-scalar-eq-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ll"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "llvm" }); + expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true); + return await readFile(outPath, "utf8"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +const source = ` +function zeros(a: number[], at: number[]): number { + let n = 0; + for (const i of at) if (a[i] === 0) n++; + return n; +} +function flags(a: (boolean | undefined)[]): number { + let n = 0; + for (let i = 0; i < a.length; i++) if (a[i] === true) n++; + return n; +} +console.log(zeros([0, 1, 0], [0, 1, 2, 5]), flags([true, undefined, false])); +`; + +test("comparing a union with a number or boolean reads its payload inline", async () => { + const llvm = await emitLlvm(source); + for (const name of ["zeros", "flags"]) { + const fn = new RegExp(`^define internal double @sc_bf_${name}\\([^]*?^}`, "m").exec(llvm)?.[0]; + expect(fn, name).toBeDefined(); + // The tag test and the payload load stay in the function, so the + // union's stack box has no escaping use. + expect(fn).not.toMatch(/@scr_union_get_(f64|bool)\(/); + expect(fn).toMatch(/getelementptr inbounds %ScrUnion, ptr %\S+, i64 0, i32 5/); + } +}); diff --git a/packages/experimental/package.json b/packages/experimental/package.json index 5b6906de6..cfb4151a4 100644 --- a/packages/experimental/package.json +++ b/packages/experimental/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/experimental", - "version": "0.4.1", + "version": "0.4.2", "description": "Experimental scriptc APIs, with Node.js implementations: lifetime regions for ProgramLifetime objects, intern tables shared between threads, and per-thread slots on shared objects", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/llvm-darwin-arm64/package.json b/packages/llvm-darwin-arm64/package.json index 472056c39..615fe9d81 100644 --- a/packages/llvm-darwin-arm64/package.json +++ b/packages/llvm-darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/llvm-darwin-arm64", - "version": "0.4.1", + "version": "0.4.2", "description": "Pinned LLVM code-generation helper for scriptc on macOS arm64", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", diff --git a/packages/llvm-darwin-x64/package.json b/packages/llvm-darwin-x64/package.json index 7f55cb6b5..fd6caf976 100644 --- a/packages/llvm-darwin-x64/package.json +++ b/packages/llvm-darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/llvm-darwin-x64", - "version": "0.4.1", + "version": "0.4.2", "description": "Pinned LLVM code-generation helper for scriptc on macOS x64", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", diff --git a/packages/llvm-linux-arm64-gnu/package.json b/packages/llvm-linux-arm64-gnu/package.json index 0e1a55b22..7d66257f4 100644 --- a/packages/llvm-linux-arm64-gnu/package.json +++ b/packages/llvm-linux-arm64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/llvm-linux-arm64-gnu", - "version": "0.4.1", + "version": "0.4.2", "description": "Pinned LLVM code-generation helper for scriptc on Linux arm64", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", diff --git a/packages/llvm-linux-arm64-musl/package.json b/packages/llvm-linux-arm64-musl/package.json index efdd97444..b3a01089d 100644 --- a/packages/llvm-linux-arm64-musl/package.json +++ b/packages/llvm-linux-arm64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/llvm-linux-arm64-musl", - "version": "0.4.1", + "version": "0.4.2", "description": "Statically linked LLVM code-generation helper for scriptc on Linux arm64 musl", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", diff --git a/packages/llvm-linux-x64-gnu/package.json b/packages/llvm-linux-x64-gnu/package.json index 39667d535..0b3264ec7 100644 --- a/packages/llvm-linux-x64-gnu/package.json +++ b/packages/llvm-linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/llvm-linux-x64-gnu", - "version": "0.4.1", + "version": "0.4.2", "description": "Pinned LLVM code-generation helper for scriptc on Linux x64", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", diff --git a/packages/llvm-linux-x64-musl/package.json b/packages/llvm-linux-x64-musl/package.json index 9efd41845..6c83d1cb9 100644 --- a/packages/llvm-linux-x64-musl/package.json +++ b/packages/llvm-linux-x64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/llvm-linux-x64-musl", - "version": "0.4.1", + "version": "0.4.2", "description": "Statically linked LLVM code-generation helper for scriptc on Linux x64 musl", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", diff --git a/packages/llvm-win32-x64-msvc/package.json b/packages/llvm-win32-x64-msvc/package.json index 329fa308c..9fb682f0f 100644 --- a/packages/llvm-win32-x64-msvc/package.json +++ b/packages/llvm-win32-x64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/llvm-win32-x64-msvc", - "version": "0.4.1", + "version": "0.4.2", "description": "Pinned LLVM code-generation helper for scriptc on Windows x64", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-android-arm64/package.json b/packages/runtime-android-arm64/package.json index 75c54e9c3..362fbc572 100644 --- a/packages/runtime-android-arm64/package.json +++ b/packages/runtime-android-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-android-arm64", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc library runtime pack for Android arm64", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-darwin-arm64/package.json b/packages/runtime-darwin-arm64/package.json index 7473b05f3..a4176ed60 100644 --- a/packages/runtime-darwin-arm64/package.json +++ b/packages/runtime-darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-darwin-arm64", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc runtime pack for macOS arm64", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index 631750cdf..069cb66e9 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -340,7 +340,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: RUNTIME_PACK_MATRIX.target, - runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" }, + runtime_abi: { version: 36, marker: "scr_runtime_abi_v36" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime-darwin-x64/package.json b/packages/runtime-darwin-x64/package.json index ab8666c88..91bc4182b 100644 --- a/packages/runtime-darwin-x64/package.json +++ b/packages/runtime-darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-darwin-x64", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc runtime pack for macOS x64", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-ios-arm64/package.json b/packages/runtime-ios-arm64/package.json index a3a254685..2fdd68897 100644 --- a/packages/runtime-ios-arm64/package.json +++ b/packages/runtime-ios-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-ios-arm64", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc library runtime pack for iOS arm64", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-ios-simulator-arm64/package.json b/packages/runtime-ios-simulator-arm64/package.json index 7981422b8..cbdf96d1d 100644 --- a/packages/runtime-ios-simulator-arm64/package.json +++ b/packages/runtime-ios-simulator-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-ios-simulator-arm64", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc library runtime pack for iOS simulator arm64", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-linux-arm64-gnu/package.json b/packages/runtime-linux-arm64-gnu/package.json index d633ac303..e0d228925 100644 --- a/packages/runtime-linux-arm64-gnu/package.json +++ b/packages/runtime-linux-arm64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-linux-arm64-gnu", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc runtime pack for Linux arm64 glibc", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-linux-arm64-musl/package.json b/packages/runtime-linux-arm64-musl/package.json index 460ca25fa..70eee223f 100644 --- a/packages/runtime-linux-arm64-musl/package.json +++ b/packages/runtime-linux-arm64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-linux-arm64-musl", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc runtime pack for Linux arm64 musl", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-linux-x64-gnu/package.json b/packages/runtime-linux-x64-gnu/package.json index 9c6336a04..5822fd21d 100644 --- a/packages/runtime-linux-x64-gnu/package.json +++ b/packages/runtime-linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-linux-x64-gnu", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc runtime pack for Linux x64 glibc", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-linux-x64-musl/package.json b/packages/runtime-linux-x64-musl/package.json index e9f885a5c..1db34d4a7 100644 --- a/packages/runtime-linux-x64-musl/package.json +++ b/packages/runtime-linux-x64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-linux-x64-musl", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc runtime pack for Linux x64 musl", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 322832f9f..285e767f4 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -127,8 +127,10 @@ export const OPTIONAL_RUNTIME_SOURCES = [ ["scr_stream_dyn.c", "streamDyn"], // Own properties JavaScript adds to children. ["scr_dyn_handle_props.c", "handleProps"], + ["scr_decl_adopt.c", "declAdopt"], // for await over checked-dynamic values. ["scr_dyn_for_await.c", "dynForAwait"], + ["scr_dyn_for_await_stream.c", all("dynForAwait", "stream", "dynAsync")], // fs.ReadStream / fs.WriteStream reference only the stream unit and base units. ["scr_fs_stream.c", "stream"], // FileHandle streams reference both the stream unit and FileHandle. @@ -160,6 +162,7 @@ export const OPTIONAL_RUNTIME_SOURCES = [ // WebCrypto's AES ciphers run over the cipher feature's mbedTLS archive. ["scr_webcrypto_aes.c", all("cipher", "webcrypto")], ["scr_fetch.c", "nativeFetch"], + ["scr_stream_from_web.c", all("stream", "nativeFetch")], ]; export function variantsFor(source) { diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index 0e81033ad..f15233924 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -437,7 +437,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: matrix.target, - runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" }, + runtime_abi: { version: 36, marker: "scr_runtime_abi_v36" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime-wasm32-wasi/package.json b/packages/runtime-wasm32-wasi/package.json index c9a40c2c0..5bbd23755 100644 --- a/packages/runtime-wasm32-wasi/package.json +++ b/packages/runtime-wasm32-wasi/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-wasm32-wasi", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc runtime pack for WASI Preview 1", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-win32-x64-msvc/package.json b/packages/runtime-win32-x64-msvc/package.json index f7e4c9860..df4a9a109 100644 --- a/packages/runtime-win32-x64-msvc/package.json +++ b/packages/runtime-win32-x64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-win32-x64-msvc", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc runtime pack for Windows x64 MSVC", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 1af249b9d..9232266af 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime", - "version": "0.4.1", + "version": "0.4.2", "description": "scriptc native runtime sources and vendored dependencies", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index c00d1b4f6..91328a1e8 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -48,7 +48,7 @@ static inline const ScrArrExt *scr_arr_ax(const ScrArr *a) { } #define SCR_AX(a) scr_arr_ax(a) -static void scr_arr_move_inline(ScrArr *a, size_t cap, size_t unit); +static void scr_arr_move_inline(ScrArr *a, size_t cap); /* Bytes before the dense slots' allocation start: the ext pointer's word. */ static size_t scr_arr_prefix(const ScrArr *a) { @@ -60,9 +60,10 @@ static void *scr_arr_alloc_start(const ScrArr *a) { return (char *)scr_arr_block(a) - scr_arr_prefix(a); } -/* Bytes per dense slot with its state: an 8-byte value, or a boolean byte. */ +/* Bytes per dense slot: an 8-byte value or a boolean byte, and its state + * byte when the array has states. */ static size_t scr_arr_slot_unit(const ScrArr *a) { - return a->elem == SCR_ELEM_BOOL ? 2 * sizeof(uint8_t) : sizeof(uint64_t) + sizeof(uint8_t); + return scr_arr_slot_width(a) + (scr_arr_has_states(a) ? 1 : 0); } /* Bytes of dense storage (head room included) after the prefix. */ @@ -78,7 +79,7 @@ ScrArrExt *scr_arr_ext(ScrArr *a) { /* The pointer goes in the word before the dense allocation, which an * array with ext always has: inline slots move out, and an array without * storage gets an allocation of the word alone. */ - if (a->spare & SCR_ARR_INLINE_SLOTS) scr_arr_move_inline(a, a->cap, scr_arr_slot_unit(a)); + if (a->spare & SCR_ARR_INLINE_SLOTS) scr_arr_move_inline(a, a->cap); char *start; if (!a->data) { start = scr_mem_alloc(SCR_ARR_EXT_PREFIX); @@ -107,6 +108,7 @@ ScrArrExt *scr_arr_ext(ScrArr *a) { /* Registry of element ops by id (scr_arr_ops); slot 0 is no ops. */ const ScrArrOps *scr_arr_ops_table[SCR_ARR_OPS_OVERFLOW] = {0}; +uint8_t scr_arr_ops_release_nothing[SCR_ARR_OPS_OVERFLOW + 1] = {0}; static uint32_t scr_arr_ops_count = 1; /* Assign `ops` its id on its first array; any thread may get there first, @@ -118,6 +120,9 @@ uint32_t scr_arr_ops_register(ScrArrOps *ops) { if (id) return id; uint32_t fresh = __atomic_fetch_add(&scr_arr_ops_count, 1, __ATOMIC_RELAXED); if (fresh < SCR_ARR_OPS_OVERFLOW) { + /* Before the id is published: an array that carries the id can be + * released on any thread. */ + scr_arr_ops_release_nothing[fresh] = scr_arr_ops_release_nothing_flag(ops); __atomic_store_n(&scr_arr_ops_table[fresh], ops, __ATOMIC_RELEASE); } else { fresh = SCR_ARR_OPS_OVERFLOW; @@ -171,7 +176,22 @@ static size_t scr_arr_prop_index(const ScrArr *a, const char *key, return SIZE_MAX; } +#define SCR_ARR_UNDEFINED_KEY_BITS UINT64_C(0x7ff8000000756e64) + +double scr_arr_undefined_key(void) { + uint64_t bits = SCR_ARR_UNDEFINED_KEY_BITS; + double key; + memcpy(&key, &bits, sizeof key); + return key; +} + static size_t scr_arr_prop_key(double i, char *buf) { + uint64_t bits; + memcpy(&bits, &i, sizeof bits); + if (bits == SCR_ARR_UNDEFINED_KEY_BITS) { + memcpy(buf, "undefined", sizeof "undefined"); + return sizeof "undefined" - 1; + } return scr_f64_to_str(i, buf); } @@ -212,6 +232,11 @@ static bool scr_elem_is_ref(ScrElemKind k) { k == SCR_ELEM_REF; } +/* Whether `a`'s element type comes with element ops (scr_arr_ops). */ +static bool scr_arr_has_ops(const ScrArr *a) { + return a->elem == SCR_ELEM_REF || a->elem == SCR_ELEM_CREF; +} + /* Releases an element slot of kind `elem`, with element ops `ops` for * SCR_ELEM_REF. An array's kind and ops never change, so loops look them up * once (element releases may run teardown, which reloads anything read @@ -266,6 +291,8 @@ typedef struct { static uint64_t scr_arr_storage_get(const ScrArrStorage *s, size_t index) { if (s->elem == SCR_ELEM_BOOL) return ((const uint8_t *)s->data)[index]; + if (s->elem == SCR_ELEM_CREF) return scr_cref_decode(((const uint32_t *)s->data)[index]); + if (s->elem == SCR_ELEM_I32) return scr_arr_f64_slot((double)((const int32_t *)s->data)[index]); return s->data[index]; } @@ -387,18 +414,14 @@ static void scr_arr_sparse_drop_prefix(ScrArr *a, size_t count) { x->sparse_len -= count; } -/* Bytes per dense slot: the value and its state byte. Boolean arrays - * store a one-byte value. */ -#define SCR_ARR_SLOT_BYTES (sizeof(uint64_t) + sizeof(uint8_t)) -#define SCR_ARR_BOOL_SLOT_BYTES (2 * sizeof(uint8_t)) /* Appends to an array smaller than this grow straight to it. */ #define SCR_ARR_MIN_GROWTH ((size_t)4) /* Inline slots: an array built by scr_arr_new_small / scr_arr_new_ops_small * (empty literals the compiler sees appended to), or constructed with a * known element count of up to SCR_ARR_KNOWN_INLINE_MAX, keeps its first - * dense slots and states in its own block, right after the ScrArr, so it - * costs one allocation and its first elements share the header's cache lines. + * dense slots in its own block, right after the ScrArr, so it costs one + * allocation and its first elements share the header's cache lines. * `spare` records it. Growth past the inline capacity moves the slots to an * ordinary allocation; the inline room then stays unused until the array is * freed. */ @@ -449,8 +472,14 @@ static void scr_arr_drop_head(ScrArr *a) { a->spare &= (uint16_t)~SCR_ARR_HEAD_ROOM; if (head == 0) return; size_t cap = a->cap, total = cap + head, live = a->len < cap ? a->len : cap; - uint8_t *end = scr_arr_states_end(a); uint64_t *data = a->data - head; + if (!scr_arr_has_states(a)) { + if (live) memmove(data, a->data, live * scr_arr_slot_width(a)); + a->data = data; + a->cap = total; + return; + } + uint8_t *end = scr_arr_states_end(a); /* The states end moves to the end of the allocation, past the `head` * vacated states: the live states move up with it, and every other state * becomes a hole. */ @@ -469,10 +498,10 @@ static void scr_arr_drop_head(ScrArr *a) { * for storage without head room (booleans, inline slots); the caller then * moves the tail down itself. */ static bool scr_arr_drop_front(ScrArr *a, size_t count) { - if (a->elem == SCR_ELEM_BOOL || scr_arr_inline(a)) return false; + if (scr_arr_slot_width(a) != sizeof(uint64_t) || scr_arr_inline(a)) return false; if (count == 0) return true; size_t head = scr_arr_head(a); - memset(scr_arr_state_run(a, 0, count), SCR_ARR_HOLE, count); + if (scr_arr_has_states(a)) memset(scr_arr_state_run(a, 0, count), SCR_ARR_HOLE, count); memset(a->data, 0, count * sizeof(uint64_t)); if (head) a->data[-1] = 0; a->data += count; @@ -492,9 +521,9 @@ static bool scr_arr_drop_front(ScrArr *a, size_t count) { } /* Slots requested by the small constructions. The small allocator rounds - * them up to fill its block: 3 eight-byte slots after a plain header in a - * 64-byte block (72 after a collector header), or 4 booleans in a 40-byte - * block. Most small arrays hold up to three elements; two slots make + * them up to fill its block: 4 eight-byte slots after a plain header in a + * 64-byte block (3 in 72 bytes after a collector header), or 8 booleans in + * a 40-byte block. Most small arrays hold up to three elements; two slots make * arrays that grow past them move their storage often enough to slow * programs that build many short lists. */ #if SCR_SMALL_ALLOC @@ -504,18 +533,14 @@ static bool scr_arr_drop_front(ScrArr *a, size_t count) { #endif /* Inline capacity of a small construction with `prefix` bytes of block - * before the slots. */ -static size_t scr_arr_small_cap(size_t prefix, bool bools) { + * before slots of `unit` bytes. Inlined, so it folds to a constant for a + * constant unit. */ +static inline size_t scr_arr_small_cap(size_t prefix, size_t unit) { #if SCR_SMALL_ALLOC - /* One return per slot unit, so each folds to a constant at its callers. */ - if (bools) - return (scr_sa_class_size(scr_sa_class(prefix + SCR_ARR_SMALL_SLOTS * SCR_ARR_BOOL_SLOT_BYTES)) - prefix) / - SCR_ARR_BOOL_SLOT_BYTES; - return (scr_sa_class_size(scr_sa_class(prefix + SCR_ARR_SMALL_SLOTS * SCR_ARR_SLOT_BYTES)) - prefix) / - SCR_ARR_SLOT_BYTES; + return (scr_sa_class_size(scr_sa_class(prefix + SCR_ARR_SMALL_SLOTS * unit)) - prefix) / unit; #else (void)prefix; - (void)bools; + (void)unit; return SCR_ARR_SMALL_SLOTS; #endif } @@ -528,44 +553,77 @@ static size_t scr_arr_small_cap(size_t prefix, bool bools) { #define SCR_ARR_KNOWN_INLINE_MAX ((size_t)16) /* Inline capacity of a construction of `need` elements with `prefix` bytes - * of block before the slots, rounded up to fill the block; 0 when its slots - * stay separate. */ -static size_t scr_arr_known_cap(size_t prefix, size_t need, bool bools) { + * of block before slots of `unit` bytes, rounded up to fill the block; 0 + * when its slots stay separate. */ +static size_t scr_arr_known_cap(size_t prefix, size_t need, size_t unit) { if (need == 0 || need > SCR_ARR_KNOWN_INLINE_MAX) return 0; #if SCR_SMALL_ALLOC - size_t unit = bools ? SCR_ARR_BOOL_SLOT_BYTES : SCR_ARR_SLOT_BYTES; size_t bytes = prefix + need * unit; if (bytes > SCR_SA_MAX) return 0; return (scr_sa_class_size(scr_sa_class(bytes)) - prefix) / unit; #else (void)prefix; - (void)bools; + (void)unit; return need; #endif } -static void scr_arr_use_inline(ScrArr *a, size_t cap) { +/* Points `a` at `cap` inline slots of `unit` bytes: the slot width, plus + * the state byte when the array has states (then all holes). */ +static void scr_arr_use_inline(ScrArr *a, size_t cap, size_t unit) { a->data = (uint64_t *)((char *)a + SCR_ARR_INLINE_OFFSET); a->cap = (uint32_t)cap; - memset(scr_arr_state_run(a, 0, cap), SCR_ARR_HOLE, cap); + if (scr_arr_has_states(a)) memset((char *)a->data + cap * (unit - 1), SCR_ARR_HOLE, cap); a->spare |= SCR_ARR_INLINE_SLOTS; } /* Move inline slots to their own allocation of `cap` (>= a->cap) slots; the * array's block keeps the unused room until it is freed. */ -static void scr_arr_move_inline(ScrArr *a, size_t cap, size_t unit) { +static void scr_arr_move_inline(ScrArr *a, size_t cap) { size_t width = scr_arr_slot_width(a); - uint64_t *data = scr_mem_alloc(cap * unit); + uint64_t *data = scr_mem_alloc(cap * scr_arr_slot_unit(a)); if (!data) scr_arr_oom(); - uint8_t *end = (uint8_t *)data + cap * (width + 1); memcpy(data, a->data, a->cap * width); - memcpy(end - a->cap, scr_arr_state_run(a, 0, a->cap), a->cap); - memset(end - cap, SCR_ARR_HOLE, cap - a->cap); + if (scr_arr_has_states(a)) { + uint8_t *end = (uint8_t *)data + cap * (width + 1); + memcpy(end - a->cap, scr_arr_state_run(a, 0, a->cap), a->cap); + memset(end - cap, SCR_ARR_HOLE, cap - a->cap); + } a->data = data; a->cap = cap; a->spare &= (uint16_t)~SCR_ARR_INLINE_SLOTS; } +/* Widen an int32 array (SCR_ELEM_I32) to doubles in place, one way: its + * dense values move to 8-byte slots, with their states when it has them, and + * its kind becomes SCR_ELEM_F64. Compiled code calls it before storing a + * value that is not an int32 into an int32 array with checked stores; `data` + * may move. Sparse entries already hold doubles. */ +void scr_arr_widen_i32(ScrArr *a) { + if (a->elem != SCR_ELEM_I32) return; + size_t cap = a->cap; + if (cap > 0) { + /* Int32 arrays never shift, so they have no head room; inline slots + * have no ext prefix. */ + bool inline_slots = scr_arr_inline(a); + bool states = scr_arr_has_states(a); + size_t unit = sizeof(uint64_t) + (states ? 1 : 0); + size_t prefix = inline_slots ? 0 : scr_arr_prefix(a); + if (cap > (SIZE_MAX - prefix) / unit) scr_arr_oom(); + char *start = scr_mem_alloc(prefix + cap * unit); + if (!start) scr_arr_oom(); + if (prefix) memcpy(start, scr_arr_alloc_start(a), prefix); + uint64_t *data = (uint64_t *)(start + prefix); + const int32_t *old = (const int32_t *)a->data; + for (size_t i = 0; i < cap; i++) data[i] = scr_arr_f64_slot((double)old[i]); + if (states) memcpy((uint8_t *)data + cap * sizeof(uint64_t), scr_arr_states_end(a) - cap, cap); + if (!inline_slots && a->rc != SIZE_MAX) scr_mem_free(scr_arr_alloc_start(a)); + a->data = data; + a->spare &= (uint16_t)~SCR_ARR_INLINE_SLOTS; + } + a->elem = SCR_ELEM_F64; +} + /* How a dense allocation is sized. */ typedef enum { SCR_ARR_GROW_APPEND, /* geometric headroom for further appends */ @@ -582,15 +640,15 @@ typedef enum { * reallocation. Growth keeps its own sequence (4, 8, 16, ...): rounding it * as well moves array storage into other size classes and measured * slower. */ -static size_t scr_arr_block_cap(size_t cap, bool bools) { +static size_t scr_arr_block_cap(size_t cap, size_t unit) { #if SCR_SMALL_ALLOC - size_t bytes = cap * (bools ? SCR_ARR_BOOL_SLOT_BYTES : SCR_ARR_SLOT_BYTES); + size_t bytes = cap * unit; if (bytes <= SCR_SA_MAX) { size_t block = scr_sa_class_size(scr_sa_class((bytes + 15) & ~(size_t)15)); - return bools ? block / SCR_ARR_BOOL_SLOT_BYTES : block / SCR_ARR_SLOT_BYTES; + return block / unit; } #else - (void)bools; + (void)unit; #endif return cap; } @@ -609,27 +667,30 @@ static void scr_arr_grow_dense_slow(ScrArr *a, size_t need, ScrArrGrow mode); * paths pick the same capacity. */ static inline void scr_arr_grow_dense_to(ScrArr *a, size_t need, ScrArrGrow mode) { if (need <= a->cap) return; - if (a->elem == SCR_ELEM_BOOL || (a->spare & (SCR_ARR_INLINE_SLOTS | SCR_ARR_HEAD_ROOM | SCR_ARR_HAS_EXT)) || - need > SCR_ARR_DENSE_LIMIT) { + if (a->elem == SCR_ELEM_BOOL || a->elem == SCR_ELEM_CREF || a->elem == SCR_ELEM_I32 || + (a->spare & (SCR_ARR_INLINE_SLOTS | SCR_ARR_HEAD_ROOM | SCR_ARR_HAS_EXT)) || need > SCR_ARR_DENSE_LIMIT) { scr_arr_grow_dense_slow(a, need, mode); return; } + bool states = scr_arr_has_states(a); + size_t unit = sizeof(uint64_t) + (states ? 1 : 0); size_t cap; if (mode == SCR_ARR_GROW_EXACT) { - cap = scr_arr_block_cap(need, false); + cap = scr_arr_block_cap(need, unit); } else { cap = a->cap < SCR_ARR_MIN_GROWTH ? SCR_ARR_MIN_GROWTH : a->cap; while (cap < need) cap += cap; if (cap > SCR_ARR_DENSE_LIMIT) cap = SCR_ARR_DENSE_LIMIT; } size_t old = a->cap; - uint64_t *data = a->data ? scr_mem_realloc(a->data, cap * SCR_ARR_SLOT_BYTES) - : scr_mem_alloc(cap * SCR_ARR_SLOT_BYTES); + uint64_t *data = a->data ? scr_mem_realloc(a->data, cap * unit) : scr_mem_alloc(cap * unit); if (!data) scr_arr_oom(); - /* The states end the allocation: move them to its new end. */ - uint8_t *end = (uint8_t *)(data + cap) + cap; - if (old) memmove(end - old, (uint8_t *)(data + old), old); - memset(end - cap, SCR_ARR_HOLE, cap - old); + if (states) { + /* The states end the allocation: move them to its new end. */ + uint8_t *end = (uint8_t *)(data + cap) + cap; + if (old) memmove(end - old, (uint8_t *)(data + old), old); + memset(end - cap, SCR_ARR_HOLE, cap - old); + } a->data = data; a->cap = cap; } @@ -660,12 +721,12 @@ static void scr_arr_grow_dense_slow(ScrArr *a, size_t need, ScrArrGrow mode) { mode = SCR_ARR_GROW_APPEND; } } - bool bools = a->elem == SCR_ELEM_BOOL; + bool states = scr_arr_has_states(a); size_t width = scr_arr_slot_width(a); - size_t unit = bools ? SCR_ARR_BOOL_SLOT_BYTES : SCR_ARR_SLOT_BYTES; + size_t unit = scr_arr_slot_unit(a); size_t cap; if (mode == SCR_ARR_GROW_EXACT) { - cap = scr_arr_block_cap(need, bools); + cap = scr_arr_block_cap(need, unit); } else { /* Inline slots overflow into the ordinary sequence (4, 8, 16, ...). */ size_t from = scr_arr_inline(a) ? 0 : a->cap; @@ -680,9 +741,9 @@ static void scr_arr_grow_dense_slow(ScrArr *a, size_t need, ScrArrGrow mode) { * appends benefit from geometric headroom. */ if (mode == SCR_ARR_GROW_SPAN && need > SCR_ARR_DENSE_LIMIT) cap = need; } - if (cap > SIZE_MAX / SCR_ARR_SLOT_BYTES) scr_arr_oom(); /* bounds both slot sizes */ + if (cap > SIZE_MAX / (sizeof(uint64_t) + 1)) scr_arr_oom(); /* bounds every slot unit */ if (scr_arr_inline(a)) { - scr_arr_move_inline(a, cap, unit); + scr_arr_move_inline(a, cap); scr_arr_absorb_sparse(a); return; } @@ -692,7 +753,7 @@ static void scr_arr_grow_dense_slow(ScrArr *a, size_t need, ScrArrGrow mode) { if (!data) scr_arr_oom(); a->data = data; a->cap = (uint32_t)cap; - memset(scr_arr_state_run(a, 0, cap), SCR_ARR_HOLE, cap); + if (states) memset(scr_arr_state_run(a, 0, cap), SCR_ARR_HOLE, cap); return; } /* Values and states share one allocation. Move the old states to its new @@ -702,9 +763,11 @@ static void scr_arr_grow_dense_slow(ScrArr *a, size_t need, ScrArrGrow mode) { char *start = scr_mem_realloc((char *)a->data - prefix, prefix + cap * unit); if (!start) scr_arr_oom(); uint64_t *data = (uint64_t *)(start + prefix); - uint8_t *end = (uint8_t *)data + cap * (width + 1); - if (a->cap) memmove(end - a->cap, (uint8_t *)data + a->cap * width, a->cap); - memset(end - cap, SCR_ARR_HOLE, cap - a->cap); + if (states) { + uint8_t *end = (uint8_t *)data + cap * (width + 1); + if (a->cap) memmove(end - a->cap, (uint8_t *)data + a->cap * width, a->cap); + memset(end - cap, SCR_ARR_HOLE, cap - a->cap); + } a->data = data; a->cap = cap; scr_arr_absorb_sparse(a); @@ -714,6 +777,44 @@ static void scr_arr_grow_dense(ScrArr *a, size_t need) { scr_arr_grow_dense_to(a, need, SCR_ARR_GROW_APPEND); } +/* States follow the value slots, and the capacity stays: inline slots + * move to an allocation of their own, and other storage grows by the state + * bytes (immortal storage, which may be static, is copied instead). */ +void scr_arr_add_states(ScrArr *a) { + if (scr_arr_has_states(a)) return; + size_t width = scr_arr_slot_width(a), len = a->len; + if (a->cap == 0) { + a->spare |= SCR_ARR_STATES; + return; + } + if (scr_arr_inline(a)) { + uint64_t *data = scr_mem_alloc(a->cap * (width + 1)); + if (!data) scr_arr_oom(); + memcpy(data, a->data, len * width); + a->data = data; + a->spare &= (uint16_t)~SCR_ARR_INLINE_SLOTS; + } else if (a->rc == SIZE_MAX) { + size_t prefix = scr_arr_prefix(a); + char *start = scr_mem_alloc(prefix + a->cap * (width + 1)); + if (!start) scr_arr_oom(); + if (prefix) memcpy(start, scr_arr_alloc_start(a), prefix); + memcpy(start + prefix, a->data, len * width); + a->data = (uint64_t *)(start + prefix); + a->spare &= (uint16_t)~SCR_ARR_HEAD_ROOM; + } else { + /* Head room goes back to the capacity first. */ + scr_arr_drop_head(a); + size_t prefix = scr_arr_prefix(a); + char *start = scr_mem_realloc(scr_arr_alloc_start(a), prefix + a->cap * (width + 1)); + if (!start) scr_arr_oom(); + a->data = (uint64_t *)(start + prefix); + } + a->spare |= SCR_ARR_STATES; + uint8_t *end = scr_arr_states_end(a); + memset(end - len, SCR_ARR_VALUE, len); + memset(end - a->cap, SCR_ARR_HOLE, a->cap - len); +} + /* Room for `count` values in front of the first element of a dense array: * moves `data` back into head room, raising `cap`; the caller * stores the new first `count` slots and states. A shifted array reuses the @@ -723,7 +824,7 @@ static void scr_arr_grow_dense(ScrArr *a, size_t need) { * array's allocation. Returns false when the caller must move the elements * itself. */ static bool scr_arr_open_head(ScrArr *a, size_t count) { - if (a->elem == SCR_ELEM_BOOL || scr_arr_inline(a)) return false; + if (scr_arr_slot_width(a) != sizeof(uint64_t) || scr_arr_inline(a)) return false; size_t head = scr_arr_head(a); if (head < count) { size_t len = a->len; @@ -734,21 +835,24 @@ static bool scr_arr_open_head(ScrArr *a, size_t count) { /* The new allocation: head room for the new values plus half the new * length, then the current dense window (its tail capacity included). */ size_t room = count + (len + count) / 2; - if (room > SCR_ARR_MAX_LENGTH || a->cap > SIZE_MAX / SCR_ARR_SLOT_BYTES - room) return false; + size_t unit = scr_arr_slot_unit(a); + if (room > SCR_ARR_MAX_LENGTH || a->cap > SIZE_MAX / unit - room) return false; size_t total = room + a->cap; size_t prefix = scr_arr_prefix(a); - char *start = scr_mem_alloc(prefix + total * SCR_ARR_SLOT_BYTES); + char *start = scr_mem_alloc(prefix + total * unit); if (!start) scr_arr_oom(); /* The ext pointer's word moves with the allocation. */ if (prefix) memcpy(start, scr_arr_alloc_start(a), prefix); uint64_t *block = (uint64_t *)(start + prefix); - /* The window's states end `room` bytes before the allocation does; the - * room's states past it are holes. */ - uint8_t *end = (uint8_t *)(block + total) + a->cap; memcpy(block + room, a->data, len * sizeof(uint64_t)); - memset(end, SCR_ARR_HOLE, room); - memcpy(end - len, scr_arr_state_run(a, 0, len), len); - memset(end - a->cap, SCR_ARR_HOLE, a->cap - len); + if (scr_arr_has_states(a)) { + /* The window's states end `room` bytes before the allocation does; + * the room's states past it are holes. */ + uint8_t *end = (uint8_t *)(block + total) + a->cap; + memset(end, SCR_ARR_HOLE, room); + memcpy(end - len, scr_arr_state_run(a, 0, len), len); + memset(end - a->cap, SCR_ARR_HOLE, a->cap - len); + } scr_mem_free(scr_arr_alloc_start(a)); a->data = block + room; head = room; @@ -764,10 +868,9 @@ static bool scr_arr_open_head(ScrArr *a, size_t count) { * within that gap past capacity. */ static bool scr_arr_dense_tail_populated(const ScrArr *a) { size_t window = a->cap < SCR_ARR_DENSE_GAP ? a->cap : SCR_ARR_DENSE_GAP; - const uint8_t *states = scr_arr_states_end(a); - if (window == 0 || *scr_arr_state_in(states, a->cap - 1) == SCR_ARR_HOLE) return false; + if (window == 0 || scr_arr_dense_state(a, a->cap - 1) == SCR_ARR_HOLE) return false; size_t populated = 0; - for (size_t i = a->cap - window; i < a->cap; i++) populated += *scr_arr_state_in(states, i) != SCR_ARR_HOLE; + for (size_t i = a->cap - window; i < a->cap; i++) populated += scr_arr_dense_state(a, i) != SCR_ARR_HOLE; return populated * 16 >= window * 3; } @@ -841,7 +944,7 @@ static __attribute__((noinline, pure)) ScrArrStateSlot scr_arr_sparse_state_at(c * sparse entries above it. */ static inline uint8_t scr_arr_state_at(const ScrArr *a, size_t index, uint64_t *out) { if (index < a->cap) { - uint8_t state = *scr_arr_state_in(scr_arr_states_end(a), index); + uint8_t state = scr_arr_dense_state(a, index); if (state == SCR_ARR_HOLE) return SCR_ARR_HOLE; if (out) *out = scr_arr_dense_get(a, index); return state; @@ -851,19 +954,38 @@ static inline uint8_t scr_arr_state_at(const ScrArr *a, size_t index, uint64_t * return sparse.state; } +/* scr_arr_state_at for a number[] (SCR_ELEM_F64): 8-byte slots, with no + * element-kind test. */ +static inline uint8_t scr_arr_f64_state_at(const ScrArr *a, size_t index, uint64_t *out) { + if (index < a->cap) { + uint8_t state = !scr_arr_has_states(a) ? (index < a->len ? SCR_ARR_VALUE : SCR_ARR_HOLE) + : *scr_arr_state_in(scr_arr_states_end_w(a, 8), index); + if (state == SCR_ARR_HOLE) return SCR_ARR_HOLE; + *out = a->data[index]; + return state; + } + ScrArrStateSlot sparse = scr_arr_sparse_state_at(a, index); + if (sparse.state != SCR_ARR_HOLE) *out = sparse.slot; + return sparse.state; +} + static inline bool scr_arr_slot_at(const ScrArr *a, size_t index, uint64_t *out) { return scr_arr_state_at(a, index, out) != SCR_ARR_HOLE; } -/* Store an owned slot into an empty index. The caller has already established - * the destination length and does not need a retain. */ +/* Store an owned slot into an empty index. The caller does not need a + * retain. An array with states has its destination length established + * already; an array without states takes only a value at its length, + * whose caller then raises the length, and gets states for any other + * store. */ static void scr_arr_store_state_owned(ScrArr *a, size_t index, uint64_t slot, uint8_t state) { if (state != SCR_ARR_VALUE) scr_arr_cut(a, index); + if (!scr_arr_has_states(a) && (state != SCR_ARR_VALUE || index != a->len)) scr_arr_add_states(a); scr_arr_grow_for_index(a, index); if (index < a->cap) { scr_arr_dense_put(a, index, slot); - *scr_arr_state_ptr(a, index) = state; + if (scr_arr_has_states(a)) *scr_arr_state_ptr(a, index) = state; return; } size_t pos = scr_arr_sparse_lower_bound(SCR_AX(a)->sparse, SCR_AX(a)->sparse_len, index); @@ -879,12 +1001,15 @@ static void scr_arr_store_owned(ScrArr *a, size_t index, uint64_t slot) { scr_arr_store_state_owned(a, index, slot, SCR_ARR_VALUE); } +/* Take the slot at `index`, leaving a hole: an array without states gets + * them. */ static bool scr_arr_take_state(ScrArr *a, size_t index, uint64_t *out, uint8_t *state_out) { if (index < a->cap) { - uint8_t state = *scr_arr_state_ptr(a, index); + uint8_t state = scr_arr_dense_state(a, index); if (state == SCR_ARR_HOLE) return false; scr_arr_cut(a, index); + scr_arr_need_states(a); *out = scr_arr_dense_get(a, index); *scr_arr_state_ptr(a, index) = SCR_ARR_HOLE; scr_arr_dense_put(a, index, 0); @@ -909,11 +1034,19 @@ static void scr_arr_replace_state_owned(ScrArr *a, size_t index, uint64_t slot, uint8_t old_state; uint64_t old = 0; if (state != SCR_ARR_VALUE) scr_arr_cut(a, index); + /* Without states, a value below the length replaces a value. */ + if (!scr_arr_has_states(a) && (state != SCR_ARR_VALUE || index >= a->len)) { + if (state == SCR_ARR_VALUE && index == a->len) { + scr_arr_store_state_owned(a, index, slot, state); + return; + } + scr_arr_add_states(a); + } if (index < a->cap) { - old_state = *scr_arr_state_ptr(a, index); + old_state = scr_arr_dense_state(a, index); if (old_state == SCR_ARR_VALUE) old = scr_arr_dense_get(a, index); scr_arr_dense_put(a, index, slot); - *scr_arr_state_ptr(a, index) = state; + if (scr_arr_has_states(a)) *scr_arr_state_ptr(a, index) = state; } else { size_t pos = scr_arr_sparse_lower_bound(SCR_AX(a)->sparse, SCR_AX(a)->sparse_len, index); if (pos == SCR_AX(a)->sparse_len || SCR_AX(a)->sparse[pos].index != index) { @@ -941,6 +1074,10 @@ static void scr_arr_note_value(ScrArr *a, size_t index) { /* scr_arr_refill bounded to [0, limit): for range operations whose cost is * proportional to the range, not to the whole array. */ static void scr_arr_refill_to(ScrArr *a, size_t limit) { + if (!scr_arr_has_states(a)) { + scr_arr_refill(a); + return; + } size_t end = a->len < a->cap ? a->len : a->cap; if (limit < end) end = limit; size_t f = a->filled; @@ -1022,7 +1159,11 @@ static bool scr_arr_prop_delete(ScrArr *a, double key) { return true; } +/* Detach the storage of `a` for a rebuild through the element stores. Both + * have states: the detached storage for its readers, and the array for + * stores at any index. */ static ScrArrStorage scr_arr_take_storage(ScrArr *a) { + scr_arr_need_states(a); ScrArrExt *x = scr_arr_ext_of(a); ScrArrStorage old = {a->data, scr_arr_states_end(a), a->cap, SCR_AX(a)->sparse, SCR_AX(a)->sparse_len, SCR_AX(a)->sparse_cap, a->elem, @@ -1059,17 +1200,20 @@ static bool scr_arr_is_dense(const ScrArr *a) { return a->len <= a->cap && (!(a->spare & SCR_ARR_HAS_EXT) || scr_arr_ext_of(a)->sparse_len == 0); } +/* Without states, both move the values only: the caller keeps the + * elements contiguous from 0 and sets the length to their count. */ static void scr_arr_move_dense(ScrArr *a, size_t to, size_t from, size_t count) { if (count == 0) return; scr_arr_cut(a, to); memmove(scr_arr_dense_at(a, to), scr_arr_dense_at(a, from), count * scr_arr_slot_width(a)); - memmove(scr_arr_state_run(a, to, count), scr_arr_state_run(a, from, count), count); + if (scr_arr_has_states(a)) + memmove(scr_arr_state_run(a, to, count), scr_arr_state_run(a, from, count), count); } static void scr_arr_clear_dense(ScrArr *a, size_t from, size_t count) { if (count == 0) return; scr_arr_cut(a, from); - memset(scr_arr_state_run(a, from, count), SCR_ARR_HOLE, count); + if (scr_arr_has_states(a)) memset(scr_arr_state_run(a, from, count), SCR_ARR_HOLE, count); memset(scr_arr_dense_at(a, from), 0, count * scr_arr_slot_width(a)); } @@ -1138,39 +1282,75 @@ static inline void scr_arr_copy_slots(uint64_t *dst, const uint64_t *src, size_t } } +/* Whether dense slots [from, from + count) of `a` all hold values. */ +static bool scr_arr_dense_run_values(const ScrArr *a, size_t from, size_t count) { + if (!scr_arr_has_states(a)) return from + count <= a->len; + if (from + count <= a->filled) return true; + return scr_arr_value_run(scr_arr_states_end(a), from, from + count) == from + count; +} + /* The destination owns no values in this range. Only retain adapters run * while copying references: no release can collect an intermediate copy. * Self-appends use disjoint ranges after reserving capacity. Both arrays - * hold the same element kind, so their slots have the same width. */ + * hold the same element kind, so their slots have the same width. A + * destination without states keeps none for a range of values: its caller + * makes the range contiguous with its elements and sets the length; any + * other range gives it states. */ static void scr_arr_copy_dense(ScrArr *dst, size_t to, const ScrArr *src, size_t from, size_t count, bool reverse, bool materialize_holes) { if (count == 0) return; scr_arr_cut(dst, to); /* 8-byte slots: a constant slot size, and constant-width state ends. */ - bool wide = dst->elem != SCR_ELEM_BOOL; - uint8_t *dst_states = wide ? scr_arr_states_end_w(dst, 8) : scr_arr_states_end_w(dst, 1); - const uint8_t *src_states = wide ? scr_arr_states_end_w(src, 8) : scr_arr_states_end_w(src, 1); + size_t width = scr_arr_slot_width(dst); + bool wide = width == sizeof(uint64_t); + /* Callers copy from an array without states only from its elements + * (which a self-copy reads after moving them, past the length). */ + bool values = !scr_arr_has_states(src) || scr_arr_dense_run_values(src, from, count); + if (!values) scr_arr_need_states(dst); + bool refs = scr_elem_is_ref(src->elem); + if (!scr_arr_has_states(dst)) { + if (!reverse && wide) { + scr_arr_copy_slots(dst->data + to, src->data + from, count); + } else if (!reverse) { + memcpy(scr_arr_dense_at(dst, to), scr_arr_dense_at(src, from), count * width); + } else { + for (size_t i = 0; i < count; i++) + scr_arr_dense_put(dst, to + i, scr_arr_dense_get(src, from + count - i - 1)); + } + if (!refs) return; + uint8_t elem = src->elem; + const ScrArrOps *ops = scr_arr_ops(src); + if (wide) + for (size_t i = to; i < to + count; i++) dst->data[i] = scr_elem_retain_with(elem, ops, dst->data[i]); + else + for (size_t i = to; i < to + count; i++) + scr_arr_dense_put(dst, i, scr_elem_retain_with(elem, ops, scr_arr_dense_get(dst, i))); + return; + } + uint8_t *dst_states = wide ? scr_arr_states_end_w(dst, 8) : scr_arr_states_end_w(dst, width); + const uint8_t *src_states = values ? NULL : wide ? scr_arr_states_end_w(src, 8) : scr_arr_states_end_w(src, width); if (!reverse && wide) { scr_arr_copy_slots(dst->data + to, src->data + from, count); - /* A range below the source's hole-free prefix holds only values. */ - if (from + count <= src->filled) + if (values) scr_arr_set_states(dst_states - to - count, SCR_ARR_VALUE, count); else scr_arr_copy_states(dst_states - to - count, src_states - from - count, count); } else if (!reverse) { memcpy(scr_arr_dense_at(dst, to), scr_arr_dense_at(src, from), count * scr_arr_slot_width(dst)); - scr_arr_copy_states(dst_states - to - count, src_states - from - count, count); + if (values) + scr_arr_set_states(dst_states - to - count, SCR_ARR_VALUE, count); + else + scr_arr_copy_states(dst_states - to - count, src_states - from - count, count); } else { for (size_t i = 0; i < count; i++) { size_t source = from + count - i - 1; scr_arr_dense_put(dst, to + i, scr_arr_dense_get(src, source)); - *scr_arr_state_in(dst_states, to + i) = *scr_arr_state_in(src_states, source); + *scr_arr_state_in(dst_states, to + i) = values ? SCR_ARR_VALUE : *scr_arr_state_in(src_states, source); } } - bool refs = scr_elem_is_ref(src->elem); - if (!refs && !materialize_holes) return; + if (!refs && (!materialize_holes || values)) return; uint8_t elem = src->elem; const ScrArrOps *ops = scr_arr_ops(src); for (size_t i = to; i < to + count; i++) { @@ -1185,7 +1365,7 @@ static void scr_arr_copy_dense(ScrArr *dst, size_t to, const ScrArr *src, /* Store `count` 8-byte value slots into dense storage starting at `to`. */ static void scr_arr_put_slots(ScrArr *a, size_t to, size_t count, const uint64_t *slots) { - if (a->elem != SCR_ELEM_BOOL) { + if (scr_arr_slot_width(a) == sizeof(uint64_t)) { scr_arr_copy_slots(a->data + to, slots, count); return; } @@ -1197,9 +1377,9 @@ static void scr_arr_put_slots(ScrArr *a, size_t to, size_t count, const uint64_t static inline __attribute__((always_inline)) ScrArr *scr_arr_make(ScrElemKind elem, size_t initial_cap, bool small) { - size_t unit = elem == SCR_ELEM_BOOL ? SCR_ARR_BOOL_SLOT_BYTES : SCR_ARR_SLOT_BYTES; - size_t inline_cap = small ? scr_arr_small_cap(SCR_ARR_INLINE_OFFSET, elem == SCR_ELEM_BOOL) - : scr_arr_known_cap(SCR_ARR_INLINE_OFFSET, initial_cap, elem == SCR_ELEM_BOOL); + size_t unit = scr_elem_slot_width(elem); + size_t inline_cap = small ? scr_arr_small_cap(SCR_ARR_INLINE_OFFSET, unit) + : scr_arr_known_cap(SCR_ARR_INLINE_OFFSET, initial_cap, unit); ScrArr *a = scr_mem_alloc(inline_cap ? SCR_ARR_INLINE_OFFSET + inline_cap * unit : sizeof(ScrArr)); if (!a) scr_arr_oom(); a->rc = 1; @@ -1210,7 +1390,7 @@ static inline __attribute__((always_inline)) ScrArr *scr_arr_make(ScrElemKind el a->spare = 0; a->filled = 0; a->data = NULL; - if (inline_cap) scr_arr_use_inline(a, inline_cap); + if (inline_cap) scr_arr_use_inline(a, inline_cap, unit); else if (initial_cap > 0) scr_arr_grow_dense_to(a, initial_cap, SCR_ARR_GROW_EXACT); #ifdef SCR_RC_AUDIT scr_live_arrays++; @@ -1225,13 +1405,18 @@ ScrArr *scr_arr_new(ScrElemKind elem, size_t initial_cap) { ScrArr *scr_arr_new_small(ScrElemKind elem) { return scr_arr_make(elem, 0, true); } /* A non-cycle array whose final length the runtime knows when it builds - * it (regex match results): its `cap` slots and states share the header's - * block, so construction and teardown cost one allocation. Growth past + * it (regex match results): its `cap` slots share the header's block, so + * construction and teardown cost one allocation. With `states`, the array + * starts with states, whose bytes share the block as well. Growth past * `cap` moves the slots out like any inline array. */ -ScrArr *scr_arr_new_inline(ScrElemKind elem, size_t cap) { - if (cap == 0) return scr_arr_make(elem, 0, false); +ScrArr *scr_arr_new_inline(ScrElemKind elem, size_t cap, bool states) { + if (cap == 0) { + ScrArr *a = scr_arr_make(elem, 0, false); + if (states) a->spare |= SCR_ARR_STATES; + return a; + } if (cap > SCR_ARR_MAX_LENGTH) scr_arr_oom(); - size_t unit = elem == SCR_ELEM_BOOL ? SCR_ARR_BOOL_SLOT_BYTES : SCR_ARR_SLOT_BYTES; + size_t unit = scr_elem_slot_width(elem) + (states ? 1 : 0); ScrArr *a = scr_mem_alloc(SCR_ARR_INLINE_OFFSET + cap * unit); if (!a) scr_arr_oom(); a->rc = 1; @@ -1239,10 +1424,10 @@ ScrArr *scr_arr_new_inline(ScrElemKind elem, size_t cap) { a->cap = 0; a->elem = (uint8_t)elem; a->cyc = 0; - a->spare = 0; + a->spare = states ? SCR_ARR_STATES : 0; a->filled = 0; a->data = NULL; - scr_arr_use_inline(a, cap); + scr_arr_use_inline(a, cap, unit); #ifdef SCR_RC_AUDIT scr_live_arrays++; #endif @@ -1250,10 +1435,9 @@ ScrArr *scr_arr_new_inline(ScrElemKind elem, size_t cap) { } void scr_arr_outline(ScrArr *a) { - if (scr_arr_inline(a)) - scr_arr_move_inline(a, a->cap, - a->elem == SCR_ELEM_BOOL ? SCR_ARR_BOOL_SLOT_BYTES : SCR_ARR_SLOT_BYTES); - /* The swapped fields do not include `spare`: no head room either. */ + if (scr_arr_inline(a)) scr_arr_move_inline(a, a->cap); + /* Of `spare`, a swap of the storage fields may move only the states + * flag: no head room either. */ if (a->spare & SCR_ARR_HEAD_ROOM) { scr_arr_drop_head(a); scr_arr_absorb_sparse(a); @@ -1287,9 +1471,14 @@ void scr_arr_trace_v(void *a0, ScrTraceVisit visit, void *ctx) { if (SCR_AX(a)->metadata) visit(SCR_AX(a)->metadata, ctx); /* Growth and truncation leave spare capacity empty. Sparse slots and * named properties are separate edges, independent of dense length. */ - const uint8_t *states = scr_arr_states_end(a); - for (size_t i = 0, end = a->len < a->cap ? a->len : a->cap; i < end; i++) { - if (*scr_arr_state_in(states, i) == SCR_ARR_VALUE) visit(scr_slot_to_ptr(a->data[i]), ctx); + size_t end = a->len < a->cap ? a->len : a->cap; + if (!scr_arr_has_states(a)) { + for (size_t i = 0; i < end; i++) visit(scr_slot_to_ptr(a->data[i]), ctx); + } else { + const uint8_t *states = scr_arr_states_end(a); + for (size_t i = 0; i < end; i++) { + if (*scr_arr_state_in(states, i) == SCR_ARR_VALUE) visit(scr_slot_to_ptr(a->data[i]), ctx); + } } for (size_t i = 0; i < SCR_AX(a)->sparse_len; i++) { if (SCR_AX(a)->sparse[i].state == SCR_ARR_VALUE) { @@ -1329,18 +1518,21 @@ static void scr_arr_gc_free(void *a0) { scr_cyc_free_inline(a); } -/* Inlined into each constructor like scr_arr_make: with `small` and the - * count constant, scr_arr_new_ops_small's size class folds to a table - * index. */ -static inline __attribute__((always_inline)) ScrArr *scr_arr_make_ops(const ScrArrOps *ops, - size_t initial_cap, - bool small) { +/* Inlined into each constructor like scr_arr_make: with `small`, the count + * and `elem` constant, scr_arr_new_ops_small's size class folds to a table + * index and the slot sizes to constants. */ +static inline __attribute__((always_inline)) ScrArr *scr_arr_make_ops_kind(const ScrArrOps *ops, + size_t initial_cap, + bool small, + uint8_t elem) { ScrArr *a; bool cyc = ops->trace != NULL; + /* The array starts without states: slots of the value width alone. */ + size_t unit = scr_elem_slot_width(elem); size_t prefix = SCR_ARR_INLINE_OFFSET + (cyc ? sizeof(ScrCycHdr) : 0); size_t inline_cap = - small ? scr_arr_small_cap(prefix, false) : scr_arr_known_cap(prefix, initial_cap, false); - size_t size = inline_cap ? SCR_ARR_INLINE_OFFSET + inline_cap * SCR_ARR_SLOT_BYTES : sizeof(ScrArr); + small ? scr_arr_small_cap(prefix, unit) : scr_arr_known_cap(prefix, initial_cap, unit); + size_t size = inline_cap ? SCR_ARR_INLINE_OFFSET + inline_cap * unit : sizeof(ScrArr); if (cyc) { static ScrCycType cyc_type = { &scr_arr_trace_v, &scr_arr_gc_free, SCR_CYC_KIND_ARRAY }; a = scr_cyc_alloc_inline(size, &cyc_type); @@ -1351,7 +1543,7 @@ static inline __attribute__((always_inline)) ScrArr *scr_arr_make_ops(const ScrA a->rc = 1; a->len = 0; a->cap = 0; - a->elem = SCR_ELEM_REF; + a->elem = elem; a->cyc = cyc; /* The ops id is written with the other header fields: reading `spare` * back after scr_arr_use_inline sets its inline flag stalls the @@ -1360,7 +1552,7 @@ static inline __attribute__((always_inline)) ScrArr *scr_arr_make_ops(const ScrA a->spare = (uint16_t)(id << SCR_ARR_OPS_SHIFT); a->filled = 0; a->data = NULL; - if (inline_cap) scr_arr_use_inline(a, inline_cap); + if (inline_cap) scr_arr_use_inline(a, inline_cap, unit); else if (initial_cap > 0) scr_arr_grow_dense_to(a, initial_cap, SCR_ARR_GROW_EXACT); /* After the storage: ops past the registry live in the ext, whose * pointer precedes the dense allocation, so inline slots move out first @@ -1372,12 +1564,32 @@ static inline __attribute__((always_inline)) ScrArr *scr_arr_make_ops(const ScrA return a; } +/* SCR_ELEM_CREF for compact element types, SCR_ELEM_REF otherwise. Each + * branch inlines scr_arr_make_ops_kind with a constant kind. */ +static inline __attribute__((always_inline)) ScrArr *scr_arr_make_ops(const ScrArrOps *ops, + size_t initial_cap, + bool small) { + if (__builtin_expect(scr_arr_ops_compact(ops), 0)) + return scr_arr_make_ops_kind(ops, initial_cap, small, SCR_ELEM_CREF); + return scr_arr_make_ops_kind(ops, initial_cap, small, SCR_ELEM_REF); +} + ScrArr *scr_arr_new_ops(const ScrArrOps *ops, size_t initial_cap) { return scr_arr_make_ops(ops, initial_cap, false); } ScrArr *scr_arr_new_ops_small(const ScrArrOps *ops) { return scr_arr_make_ops(ops, 0, true); } +/* Frees an acyclic array whose elements are already released. */ +static inline __attribute__((always_inline)) void scr_arr_free(ScrArr *a) { + scr_arr_free_parts(a); +#ifdef SCR_RC_AUDIT + scr_live_arrays--; +#endif + scr_weak_dispose_inline(a); + scr_mem_free(a); +} + static void scr_arr_destroy(void *object) { ScrArr *a = object; if (scr_arr_ext_of(a)) { @@ -1391,18 +1603,28 @@ static void scr_arr_destroy(void *object) { * first (and the storage cannot move under the loop); immortal strings * (interned characters, literals) need no call. */ const uint64_t *data = a->data; - const uint8_t *states = scr_arr_states_end_w(a, 8); - /* Slots below the hole-free prefix bound hold values. */ - size_t filled = a->filled < end ? a->filled : end; + /* Slots below the hole-free prefix bound hold values, as do all + * slots below the length of an array without states. */ + size_t filled = !scr_arr_has_states(a) ? end : a->filled < end ? a->filled : end; for (size_t i = 0; i < filled; i++) { ScrStr *s = (ScrStr *)scr_slot_to_ptr(data[i]); if (s && s->rc != SIZE_MAX) scr_str_release(s); } + const uint8_t *states = scr_arr_states_end_w(a, 8); for (size_t i = filled; i < end; i++) { if (*scr_arr_state_in(states, i) != SCR_ARR_VALUE) continue; ScrStr *s = (ScrStr *)scr_slot_to_ptr(data[i]); if (s && s->rc != SIZE_MAX) scr_str_release(s); } + } else if (!scr_arr_has_states(a)) { + /* Each slot is cleared before its release, as with states. */ + uint8_t elem = a->elem; + const ScrArrOps *ops = scr_arr_ops(a); + for (size_t i = 0; i < end; i++) { + uint64_t old = a->data[i]; + a->data[i] = 0; + scr_elem_release_with(elem, ops, old); + } } else { uint8_t *states = scr_arr_states_end(a); uint8_t elem = a->elem; @@ -1435,21 +1657,29 @@ static void scr_arr_destroy(void *object) { } } } - if (a->cyc) { - scr_arr_gc_free(a); - } else { - scr_arr_free_parts(a); -#ifdef SCR_RC_AUDIT - scr_live_arrays--; -#endif - scr_weak_dispose_inline(a); - scr_mem_free(a); - } + if (a->cyc) scr_arr_gc_free(a); + else scr_arr_free(a); +} + +/* Whether destroying `a`, an acyclic array, releases nothing: it has no ext + * and its elements are numbers, booleans or compact references, or + * references whose release does nothing (SCR_ARR_OPS_RELEASE_NOTHING). */ +static inline bool scr_arr_releases_nothing(const ScrArr *a) { + if (a->spare & SCR_ARR_HAS_EXT) return false; + if (!scr_elem_is_ref(a->elem)) return true; + return a->elem == SCR_ELEM_REF && + scr_arr_ops_release_nothing[(uint32_t)a->spare >> SCR_ARR_OPS_SHIFT]; } void scr_arr_release(ScrArr *a) { if (!a || a->rc == SIZE_MAX) return; /* NULL: an uninitialized `let` local */ if (--a->rc == 0) { + /* Nothing to tear down: free the storage without scr_rc_destroy's + * queue (no release can recurse) or the element loop. */ + if (!a->cyc && scr_arr_releases_nothing(a)) { + scr_arr_free(a); + return; + } if (a->cyc) scr_cyc_dead(a); scr_rc_destroy(a, scr_arr_destroy); } else if (a->cyc) { @@ -1485,6 +1715,7 @@ double scr_arr_next_present(const ScrArr *a, double start) { if (!(start < SCR_ARR_LEN_F64(a->len))) return SCR_ARR_LEN_F64(a->len); size_t i = start > 0 ? (size_t)ceil(start) : 0; size_t end = a->cap < a->len ? a->cap : a->len; + if (!scr_arr_has_states(a)) return (double)i; const uint8_t *states = scr_arr_states_end(a); for (; i < end; i++) { if (*scr_arr_state_in(states, i) != SCR_ARR_HOLE) return (double)i; @@ -1523,6 +1754,22 @@ void scr_arr_set_len(ScrArr *a, double length) { * array must cost only the elements removed, not its historical peak. */ size_t dense_end = a->len < a->cap ? a->len : a->cap; size_t dense_stop = next < dense_end ? next : dense_end; + if (!scr_arr_has_states(a)) { + /* Values only: each slot is cleared before its release. The + * length drops first, so a release that reads the array sees the + * values that remain. */ + a->len = (uint32_t)next; + if (scr_elem_is_ref(a->elem)) { + for (size_t i = dense_stop; i < dense_end; i++) { + uint64_t old = a->data[i]; + a->data[i] = 0; + scr_elem_release(a, old); + } + } else if (dense_stop < dense_end) { + memset(scr_arr_dense_at(a, dense_stop), 0, (dense_end - dense_stop) * scr_arr_slot_width(a)); + } + return; + } if (scr_elem_is_ref(a->elem)) { for (size_t i = dense_stop; i < dense_end; i++) { if (*scr_arr_state_ptr(a, i) != SCR_ARR_HOLE) { @@ -1551,8 +1798,20 @@ void scr_arr_set_len(ScrArr *a, double length) { } if (scr_arr_ext_of(a)) scr_arr_ext_of(a)->sparse_len = scr_arr_sparse_lower_bound(scr_arr_ext_of(a)->sparse, scr_arr_ext_of(a)->sparse_len, next); } - } else if (a->len == 0 && next <= SCR_ARR_PRESIZE_LIMIT && (a->cap == 0 || scr_arr_inline(a))) { - scr_arr_reserve_exact(a, next); + } else if (next > a->len) { + /* The indices past the elements are holes. An empty array gives up + * its inline slots first, so its slots and states take one allocation. */ + if (a->len == 0 && next <= SCR_ARR_PRESIZE_LIMIT && (a->cap == 0 || scr_arr_inline(a))) { + if (scr_arr_inline(a)) { + a->spare &= (uint16_t)~SCR_ARR_INLINE_SLOTS; + a->cap = 0; + a->data = NULL; + } + scr_arr_need_states(a); + scr_arr_reserve_exact(a, next); + } else { + scr_arr_need_states(a); + } } a->len = next; } @@ -1583,11 +1842,24 @@ void scr_arr_copy_range_ex(ScrArr *dst, size_t dst_start, const ScrArr *src, scr_arr_refill_to(dst, dst_start + count); } +/* A destination without states takes a dense copy at its length, whose + * caller then raises the length over the copy; any other copy gives it + * states first. */ static void scr_arr_copy_range_states(ScrArr *dst, size_t dst_start, const ScrArr *src, size_t src_start, size_t count, bool reverse, bool materialize_holes) { - if (src != dst && src_start <= src->cap && count <= src->cap - src_start && - dst_start <= dst->cap && count <= dst->cap - dst_start) { + size_t src_end = scr_arr_has_states(src) ? src->cap : src->len; + bool fits = src != dst && src_start <= src_end && count <= src_end - src_start && + dst_start <= dst->cap && count <= dst->cap - dst_start; + if (!scr_arr_has_states(dst)) { + if (fits && dst_start == dst->len) { + scr_arr_copy_dense(dst, dst_start, src, src_start, count, reverse, materialize_holes); + return; + } + scr_arr_add_states(dst); + fits = fits && dst_start <= dst->cap && count <= dst->cap - dst_start; + } + if (fits) { const uint8_t *dst_states = scr_arr_states_end(dst); size_t empty = 0; while (empty < count && *scr_arr_state_in(dst_states, dst_start + empty) == SCR_ARR_HOLE) empty++; @@ -1673,11 +1945,17 @@ static ScrArr *scr_arr_fill_slot(ScrArr *a, uint64_t slot, uint8_t state, if (from >= until) return scr_arr_retain(a); bool refs = scr_elem_is_ref(a->elem); if (scr_arr_dense_ok(a, until, until - from)) scr_arr_grow_dense(a, until); + if (state != SCR_ARR_VALUE) scr_arr_need_states(a); if (!refs && until <= a->cap) { if (state != SCR_ARR_VALUE) scr_arr_cut(a, from); if (a->elem == SCR_ELEM_BOOL) memset(scr_arr_dense_at(a, from), (int)slot, until - from); + else if (a->elem == SCR_ELEM_CREF) for (size_t i = from; i < until; i++) scr_arr_dense_put(a, i, slot); + else if (a->elem == SCR_ELEM_I32) { + int32_t v = scr_arr_i32_of(slot); + for (size_t i = from; i < until; i++) ((int32_t *)a->data)[i] = v; + } else for (size_t i = from; i < until; i++) a->data[i] = slot; - memset(scr_arr_state_run(a, from, until - from), state, until - from); + if (scr_arr_has_states(a)) memset(scr_arr_state_run(a, from, until - from), state, until - from); } else { for (size_t i = from; i < until; i++) { uint64_t value = refs && state == SCR_ARR_VALUE ? scr_elem_retain_slot(a, slot) : slot; @@ -1712,7 +1990,7 @@ double scr_math_max_arr(ScrArr *a) { double best = -INFINITY; for (size_t i = 0; i < a->len; i++) { uint64_t slot; - if (scr_arr_state_at(a, i, &slot) != SCR_ARR_VALUE) return NAN; + if (scr_arr_f64_state_at(a, i, &slot) != SCR_ARR_VALUE) return NAN; double v = scr_slot_to_f64(slot); if (isnan(v)) return v; if (v > best || (v == 0.0 && best == 0.0 && !signbit(v))) best = v; @@ -1724,7 +2002,7 @@ double scr_math_min_arr(ScrArr *a) { double best = INFINITY; for (size_t i = 0; i < a->len; i++) { uint64_t slot; - if (scr_arr_state_at(a, i, &slot) != SCR_ARR_VALUE) return NAN; + if (scr_arr_f64_state_at(a, i, &slot) != SCR_ARR_VALUE) return NAN; double v = scr_slot_to_f64(slot); if (isnan(v)) return v; if (v < best || (v == 0.0 && best == 0.0 && signbit(v))) best = v; @@ -1739,7 +2017,7 @@ double scr_math_hypot_arr(ScrArr *a) { bool has_nan = false; for (size_t i = 0; i < a->len; i++) { uint64_t slot; - if (scr_arr_state_at(a, i, &slot) != SCR_ARR_VALUE) { + if (scr_arr_f64_state_at(a, i, &slot) != SCR_ARR_VALUE) { has_nan = true; continue; } @@ -1754,7 +2032,7 @@ double scr_math_hypot_arr(ScrArr *a) { double correction = 0.0; for (size_t i = 0; i < a->len; i++) { uint64_t slot; - (void)scr_arr_state_at(a, i, &slot); + (void)scr_arr_f64_state_at(a, i, &slot); double v = scr_slot_to_f64(slot) / scale; double term = v * v - correction; double next = sum + term; @@ -1779,7 +2057,7 @@ static uint64_t scr_arr_require_slot(ScrArr *a, double i) { if (scr_arr_prop_get_state(a, i, &prop, &state)) { if (state == SCR_ARR_UNDEFINED) { char buf[32]; - scr_f64_to_str(i, buf); + scr_arr_prop_key(i, buf); scr_trap_fmt("scriptc: RangeError: numeric property %s is undefined\n", buf); } return prop; @@ -1830,9 +2108,9 @@ void *scr_arr_peek_ref(const ScrArr *a, double i) { if (i >= 0 && i < (double)a->cap) { size_t idx = (size_t)i; if ((double)idx == i) { - if (idx < a->filled) return scr_slot_to_ptr(a->data[idx]); - if (idx >= a->len || *scr_arr_state_ptr(a, idx) != SCR_ARR_VALUE) return NULL; - return scr_slot_to_ptr(a->data[idx]); + if (idx < a->filled) return scr_slot_to_ptr(scr_arr_dense_get(a, idx)); + if (idx >= a->len || scr_arr_dense_state(a, idx) != SCR_ARR_VALUE) return NULL; + return scr_slot_to_ptr(scr_arr_dense_get(a, idx)); } } uint64_t slot; @@ -1861,8 +2139,8 @@ bool scr_arr_index_eq(const ScrArr *a, double i, const ScrArr *b, double j) { if (i >= 0 && i < (double)a->cap && i == trunc(i) && j >= 0 && j < (double)b->cap && j == trunc(j)) { size_t ai = (size_t)i, bi = (size_t)j; - bool ap = ai < a->len && *scr_arr_state_ptr(a, ai) == SCR_ARR_VALUE; - bool bp = bi < b->len && *scr_arr_state_ptr(b, bi) == SCR_ARR_VALUE; + bool ap = ai < a->len && scr_arr_dense_state(a, ai) == SCR_ARR_VALUE; + bool bp = bi < b->len && scr_arr_dense_state(b, bi) == SCR_ARR_VALUE; if (!ap || !bp) return ap == bp; av = scr_arr_dense_get(a, ai); bv = scr_arr_dense_get(b, bi); @@ -1886,7 +2164,7 @@ void *scr_arr_get_ref(ScrArr *a, double i) { if (a->elem == SCR_ELEM_STR) scr_str_retain((ScrStr *)p); else if (a->elem == SCR_ELEM_BYTES) scr_bytes_retain((ScrBytes *)p); else if (a->elem == SCR_ELEM_REF) p = scr_arr_ops(a)->retain(p); - else scr_arr_retain((ScrArr *)p); + else if (a->elem == SCR_ELEM_ARR) scr_arr_retain((ScrArr *)p); return p; } @@ -1902,6 +2180,17 @@ static void scr_arr_set_slot(ScrArr *a, double i, uint64_t slot) { scr_arr_prop_set(a, i, slot); return; } + if (!scr_arr_has_states(a) && idx >= a->len) { + /* An append keeps the array without states; a write past the length + * leaves holes. */ + if (idx == a->len) { + scr_arr_store_owned(a, idx, slot); + a->len = (uint32_t)(idx + 1); + scr_arr_note_value(a, idx); + return; + } + scr_arr_add_states(a); + } if (idx >= a->len) a->len = idx + 1; /* Unlink-then-release: a release can trigger a cycle collection, which * must never see a heap edge whose count was already given up. */ @@ -1927,6 +2216,7 @@ void scr_arr_set_undefined(ScrArr *a, double i) { scr_arr_prop_set_undefined(a, i); return; } + scr_arr_need_states(a); if (idx >= a->len) a->len = idx + 1; scr_arr_replace_state_owned(a, idx, 0, SCR_ARR_UNDEFINED); } @@ -1976,7 +2266,7 @@ double scr_arr_push_many(ScrArr *a, size_t count, const uint64_t *slots) { scr_arr_grow_dense(a, next); if (count) { scr_arr_put_slots(a, from, count, slots); - scr_arr_set_states(scr_arr_state_run(a, from, count), SCR_ARR_VALUE, count); + if (scr_arr_has_states(a)) scr_arr_set_states(scr_arr_state_run(a, from, count), SCR_ARR_VALUE, count); } a->len = (uint32_t)next; /* The appended states are values: a bound at the old length moves to @@ -1985,6 +2275,7 @@ double scr_arr_push_many(ScrArr *a, size_t count, const uint64_t *slots) { else scr_arr_refill(a); return (double)next; } + scr_arr_need_states(a); for (size_t i = 0; i < count; i++) scr_arr_store_owned(a, from + i, slots[i]); a->len = (uint32_t)next; scr_arr_refill(a); @@ -2021,6 +2312,7 @@ double scr_arr_push_spread(ScrArr *a, const ScrArr *src) { scr_arr_refill(a); return SCR_ARR_LEN_F64(a->len); } + scr_arr_need_states(a); a->len = old_len + add; for (size_t i = 0; i < add; i++) { uint64_t slot; @@ -2065,9 +2357,10 @@ double scr_arr_concat_copy(ScrArr *a, const ScrArr *src) { scr_arr_refill(a); return SCR_ARR_LEN_F64(a->len); } + scr_arr_need_states(a); a->len = old_len + add; for (size_t i = 0; i < src->cap; i++) { - if (*scr_arr_state_ptr(src, i) != SCR_ARR_HOLE) { + if (scr_arr_dense_state(src, i) != SCR_ARR_HOLE) { scr_arr_copy_index(a, old_len + i, src, i); } } @@ -2121,7 +2414,7 @@ static double scr_arr_unshift_slot(ScrArr *a, uint64_t slot) { /* The prefix stays put in front of the new first value. */ size_t f = a->filled; a->data[0] = slot; - *scr_arr_state_ptr(a, 0) = SCR_ARR_VALUE; + if (scr_arr_has_states(a)) *scr_arr_state_ptr(a, 0) = SCR_ARR_VALUE; a->len++; a->filled = (uint32_t)(f + 1); return SCR_ARR_LEN_F64(a->len); @@ -2132,7 +2425,7 @@ static double scr_arr_unshift_slot(ScrArr *a, uint64_t slot) { size_t f = a->filled; scr_arr_move_dense(a, 1, 0, old_len); scr_arr_dense_put(a, 0, slot); - *scr_arr_state_ptr(a, 0) = SCR_ARR_VALUE; + if (scr_arr_has_states(a)) *scr_arr_state_ptr(a, 0) = SCR_ARR_VALUE; a->len++; a->filled = (uint32_t)(f + 1); return SCR_ARR_LEN_F64(a->len); @@ -2173,7 +2466,7 @@ double scr_arr_unshift_many(ScrArr *a, size_t count, const uint64_t *slots) { if (scr_arr_is_dense(a) && scr_arr_open_head(a, count)) { size_t f = a->filled; scr_arr_put_slots(a, 0, count, slots); - memset(scr_arr_state_run(a, 0, count), SCR_ARR_VALUE, count); + if (scr_arr_has_states(a)) memset(scr_arr_state_run(a, 0, count), SCR_ARR_VALUE, count); a->len = old_len + count; a->filled = (uint32_t)(f + count); return SCR_ARR_LEN_F64(a->len); @@ -2183,7 +2476,7 @@ double scr_arr_unshift_many(ScrArr *a, size_t count, const uint64_t *slots) { size_t f = a->filled; scr_arr_move_dense(a, count, 0, old_len); scr_arr_put_slots(a, 0, count, slots); - memset(scr_arr_state_run(a, 0, count), SCR_ARR_VALUE, count); + if (scr_arr_has_states(a)) memset(scr_arr_state_run(a, 0, count), SCR_ARR_VALUE, count); a->len = old_len + count; a->filled = (uint32_t)(f + count); return SCR_ARR_LEN_F64(a->len); @@ -2210,6 +2503,10 @@ double scr_arr_unshift_spread(ScrArr *a, const ScrArr *src) { size_t add = src->len; if (add == 0) return (double)old_len; if (add > SCR_ARR_MAX_LENGTH - old_len) scr_arr_oom(); + /* Holes and present undefined in the source become present undefined: + * the receiver needs states, before its elements move. */ + if (!scr_arr_has_states(a) && (!scr_arr_is_dense(src) || !scr_arr_dense_run_values(src, 0, add))) + scr_arr_add_states(a); if (scr_arr_is_dense(a) && scr_arr_is_dense(src) && scr_arr_open_head(a, add)) { /* A self-spread reads the original values behind the opened room. */ scr_arr_copy_dense(a, 0, src, src == a ? add : 0, add, false, true); @@ -2257,6 +2554,15 @@ double scr_arr_unshift_spread(ScrArr *a, const ScrArr *src) { ScrArr *scr_arr_reverse(ScrArr *a) { size_t len = a->len; + if (!scr_arr_has_states(a)) { + for (size_t i = 0; i < len / 2; i++) { + size_t j = len - i - 1; + uint64_t slot = scr_arr_dense_get(a, i); + scr_arr_dense_put(a, i, scr_arr_dense_get(a, j)); + scr_arr_dense_put(a, j, slot); + } + return scr_arr_retain(a); + } if (scr_arr_is_dense(a)) { /* A hole-free array stays hole-free; any other prefix is recomputed. */ if (a->filled != len) scr_arr_cut(a, 0); @@ -2292,6 +2598,13 @@ uint8_t scr_arr_pop_state(ScrArr *a, uint64_t *slot_out) { if (slot_out) *slot_out = 0; if (a->len == 0) return SCR_ARR_HOLE; size_t idx = a->len - 1; + if (!scr_arr_has_states(a)) { + if (slot_out) *slot_out = scr_arr_dense_get(a, idx); + scr_arr_dense_put(a, idx, 0); + scr_arr_cut(a, idx); + a->len = (uint32_t)idx; + return SCR_ARR_VALUE; + } uint64_t slot = 0; uint8_t state = SCR_ARR_HOLE; if (!scr_arr_take_state(a, idx, &slot, &state)) state = SCR_ARR_HOLE; @@ -2327,7 +2640,7 @@ uint8_t scr_arr_shift_state(ScrArr *a, uint64_t *slot_out) { if (a->len == 0) return SCR_ARR_HOLE; size_t old_len = a->len; if (scr_arr_is_dense(a)) { - uint8_t state = *scr_arr_state_ptr(a, 0); + uint8_t state = scr_arr_dense_state(a, 0); if (slot_out && state == SCR_ARR_VALUE) *slot_out = scr_arr_dense_get(a, 0); /* The first slot becomes head room; the tail stays where it is. */ if (scr_arr_drop_front(a, 1)) return state; @@ -2395,16 +2708,18 @@ ScrArr *scr_arr_splice(ScrArr *a, double start, double deleteCount) { * otherwise the range may be mostly holes, so the capacity is a hint. */ size_t hint = scr_arr_is_dense(a) || n <= SCR_ARR_DENSE_LIMIT ? n : SCR_ARR_DENSE_LIMIT; ScrArr *out = - a->elem == SCR_ELEM_REF + scr_arr_has_ops(a) ? scr_arr_new_ops(scr_arr_ops(a), hint ? hint : 1) : scr_arr_new(a->elem, hint ? hint : 1); - out->len = n; size_t old_len = a->len; if (scr_arr_is_dense(a)) { if (n) { + /* The removed elements keep their states unless all are values. */ + if (!scr_arr_dense_run_values(a, from, n)) scr_arr_add_states(out); memcpy(out->data, scr_arr_dense_at(a, from), n * scr_arr_slot_width(a)); - memcpy(scr_arr_state_run(out, 0, n), scr_arr_state_run(a, from, n), n); + if (scr_arr_has_states(out)) memcpy(scr_arr_state_run(out, 0, n), scr_arr_state_run(a, from, n), n); } + out->len = n; scr_arr_refill(out); /* A removed prefix becomes head room. */ if (from == 0 && scr_arr_drop_front(a, n)) return out; @@ -2418,6 +2733,8 @@ ScrArr *scr_arr_splice(ScrArr *a, double start, double deleteCount) { else scr_arr_refill(a); return out; } + scr_arr_add_states(out); + out->len = n; ScrArrStorage old = scr_arr_take_storage(a); a->len = 0; for (size_t i = 0; i < old.cap; i++) { @@ -2475,12 +2792,17 @@ ScrArr *scr_arr_splice_insert(ScrArr *a, double start, double deleteCount, * is needed only by direct self-spread runtime callers. */ ScrArr *snapshot = items == a ? scr_arr_slice(a, 0, INFINITY) : NULL; if (snapshot) items = snapshot; - ScrArr *removed = a->elem == SCR_ELEM_REF + /* Holes and present undefined among the items become present + * undefined: the receiver needs states, before its elements move. */ + if (!scr_arr_dense_run_values(items, 0, add)) scr_arr_need_states(a); + ScrArr *removed = scr_arr_has_ops(a) ? scr_arr_new_ops(scr_arr_ops(a), n) : scr_arr_new(a->elem, n); if (n) { + if (!scr_arr_dense_run_values(a, from, n)) scr_arr_add_states(removed); memcpy(removed->data, scr_arr_dense_at(a, from), n * scr_arr_slot_width(a)); - memcpy(scr_arr_state_run(removed, 0, n), scr_arr_state_run(a, from, n), n); + if (scr_arr_has_states(removed)) + memcpy(scr_arr_state_run(removed, 0, n), scr_arr_state_run(a, from, n), n); } removed->len = n; scr_arr_refill(removed); @@ -2521,7 +2843,7 @@ void scr_arr_splice_drop(ScrArr *a, double start, double deleteCount, size_t cou size_t next_len = a->len - n + count; bool refs = scr_elem_is_ref(a->elem); if (!scr_arr_is_dense(a) || !scr_arr_dense_ok(a, next_len, count)) { - ScrArr *items = a->elem == SCR_ELEM_REF + ScrArr *items = scr_arr_has_ops(a) ? scr_arr_new_ops(scr_arr_ops(a), count) : scr_arr_new(a->elem, count); scr_arr_push_many(items, count, slots); @@ -2538,9 +2860,8 @@ void scr_arr_splice_drop(ScrArr *a, double start, double deleteCount, size_t cou removed = scr_mem_alloc(n * sizeof(uint64_t)); if (!removed) scr_arr_oom(); } - const uint8_t *states = scr_arr_states_end(a); for (size_t i = from; i < from + n; i++) { - if (*scr_arr_state_in(states, i) == SCR_ARR_VALUE) removed[kept++] = a->data[i]; + if (scr_arr_dense_state(a, i) == SCR_ARR_VALUE) removed[kept++] = a->data[i]; } } size_t old_len = a->len; @@ -2555,7 +2876,7 @@ void scr_arr_splice_drop(ScrArr *a, double start, double deleteCount, size_t cou scr_arr_move_dense(a, from + count, from + n, old_len - from - n); if (count) { scr_arr_put_slots(a, from, count, slots); - memset(scr_arr_state_run(a, from, count), SCR_ARR_VALUE, count); + if (scr_arr_has_states(a)) memset(scr_arr_state_run(a, from, count), SCR_ARR_VALUE, count); } if (next_len < old_len) scr_arr_clear_dense(a, next_len, old_len - next_len); a->len = next_len; @@ -2580,13 +2901,15 @@ ScrArr *scr_arr_flat_copy(const ScrArr *a, ScrArr *out, bool flatten) { for (size_t j = 0; j < inner->len; j++) { if (scr_arr_state_at(inner, j, NULL) == SCR_ARR_HOLE) continue; if (out->len == SCR_ARR_MAX_LENGTH) scr_arr_oom(); - size_t at = out->len++; + size_t at = out->len; scr_arr_copy_index(out, at, inner, j); + out->len = (uint32_t)(at + 1); } } else { if (out->len == SCR_ARR_MAX_LENGTH) scr_arr_oom(); - size_t at = out->len++; + size_t at = out->len; scr_arr_copy_index(out, at, a, i); + out->len = (uint32_t)(at + 1); } } scr_arr_refill(out); @@ -2611,10 +2934,12 @@ static bool scr_arr_ref_eq(const ScrArr *a, uint64_t slot, void *v) { return p == v; } +#define SCR_ARR_SCAN_VALUE(states, i) (!(states) || *scr_arr_state_in((states), (i)) == SCR_ARR_VALUE) + double scr_arr_index_of_f64(ScrArr *a, double v) { - const uint8_t *states = scr_arr_states_end(a); + const uint8_t *states = scr_arr_scan_states(a); for (size_t i = 0; i < a->cap && i < a->len; i++) { - if (*scr_arr_state_in(states, i) == SCR_ARR_VALUE && scr_slot_to_f64(a->data[i]) == v) return (double)i; + if (SCR_ARR_SCAN_VALUE(states, i) && scr_slot_to_f64(a->data[i]) == v) return (double)i; } for (size_t i = 0; i < SCR_AX(a)->sparse_len; i++) { if (SCR_AX(a)->sparse[i].state == SCR_ARR_VALUE && scr_slot_to_f64(SCR_AX(a)->sparse[i].slot) == v) return (double)SCR_AX(a)->sparse[i].index; @@ -2623,9 +2948,9 @@ double scr_arr_index_of_f64(ScrArr *a, double v) { } double scr_arr_index_of_bool(ScrArr *a, bool v) { - const uint8_t *states = scr_arr_states_end(a); + const uint8_t *states = scr_arr_scan_states(a); for (size_t i = 0; i < a->cap && i < a->len; i++) { - if (*scr_arr_state_in(states, i) == SCR_ARR_VALUE && ((scr_arr_dense_get(a, i) != 0) == v)) return (double)i; + if (SCR_ARR_SCAN_VALUE(states, i) && ((scr_arr_dense_get(a, i) != 0) == v)) return (double)i; } for (size_t i = 0; i < SCR_AX(a)->sparse_len; i++) { if (SCR_AX(a)->sparse[i].state == SCR_ARR_VALUE && (SCR_AX(a)->sparse[i].slot != 0) == v) return (double)SCR_AX(a)->sparse[i].index; @@ -2634,9 +2959,9 @@ double scr_arr_index_of_bool(ScrArr *a, bool v) { } double scr_arr_index_of_ref(ScrArr *a, void *v) { - const uint8_t *states = scr_arr_states_end(a); + const uint8_t *states = scr_arr_scan_states(a); for (size_t i = 0; i < a->cap && i < a->len; i++) { - if (*scr_arr_state_in(states, i) == SCR_ARR_VALUE && scr_arr_ref_eq(a, a->data[i], v)) return (double)i; + if (SCR_ARR_SCAN_VALUE(states, i) && scr_arr_ref_eq(a, scr_arr_dense_get(a, i), v)) return (double)i; } for (size_t i = 0; i < SCR_AX(a)->sparse_len; i++) { if (SCR_AX(a)->sparse[i].state == SCR_ARR_VALUE && scr_arr_ref_eq(a, SCR_AX(a)->sparse[i].slot, v)) return (double)SCR_AX(a)->sparse[i].index; @@ -2645,9 +2970,9 @@ double scr_arr_index_of_ref(ScrArr *a, void *v) { } bool scr_arr_includes_f64(ScrArr *a, double v) { - const uint8_t *states = scr_arr_states_end(a); + const uint8_t *states = scr_arr_scan_states(a); for (size_t i = 0; i < a->cap && i < a->len; i++) { - if (*scr_arr_state_in(states, i) != SCR_ARR_VALUE) continue; + if (!SCR_ARR_SCAN_VALUE(states, i)) continue; double x = scr_slot_to_f64(a->data[i]); if (x == v || (x != x && v != v)) return true; /* SameValueZero: NaN hits */ } @@ -2667,9 +2992,9 @@ bool scr_arr_includes_ref(ScrArr *a, void *v) { if (a->elem == SCR_ELEM_REF && scr_arr_ops(a)->retain == scr_dyn_retain_v) { const ScrDyn *needle = (const ScrDyn *)v; if (needle->kind == SCR_DYN_NUM && needle->v.num != needle->v.num) { - const uint8_t *states = scr_arr_states_end(a); + const uint8_t *states = scr_arr_scan_states(a); for (size_t i = 0; i < a->cap && i < a->len; i++) { - if (*scr_arr_state_in(states, i) != SCR_ARR_VALUE) continue; + if (!SCR_ARR_SCAN_VALUE(states, i)) continue; const ScrDyn *d = (const ScrDyn *)scr_slot_to_ptr(a->data[i]); if (d->kind == SCR_DYN_NUM && d->v.num != d->v.num) return true; } @@ -2727,25 +3052,30 @@ ScrArr *scr_arr_slice(ScrArr *a, double start, double end) { * sparse side store may be mostly holes, so its capacity is a hint. */ size_t hint = to <= a->cap || n <= SCR_ARR_DENSE_LIMIT ? n : SCR_ARR_DENSE_LIMIT; ScrArr *out = - a->elem == SCR_ELEM_REF + scr_arr_has_ops(a) ? scr_arr_new_ops(scr_arr_ops(a), hint ? hint : 1) : scr_arr_new(a->elem, hint ? hint : 1); - out->len = n; - if (to <= a->cap && to <= a->filled && a->elem != SCR_ELEM_BOOL && !scr_elem_is_ref(a->elem)) { + if (to <= a->cap && to <= a->filled && scr_arr_slot_width(a) == sizeof(uint64_t) && + !scr_elem_is_ref(a->elem)) { /* Numbers below the source's hole-free prefix: values only. */ scr_arr_copy_slots(out->data, a->data + from, n); - scr_arr_set_states(scr_arr_states_end_w(out, 8) - n, SCR_ARR_VALUE, n); + out->len = (uint32_t)n; out->filled = (uint32_t)n; return out; } if (to <= a->cap) { scr_arr_copy_dense(out, 0, a, from, n, false, false); + out->len = (uint32_t)n; /* States copied from below the source's bound are present values. */ if (n > 0 && from < a->filled) out->filled = (uint32_t)((to < a->filled ? to : a->filled) - from); scr_arr_refill(out); return out; } + /* A range reaching the sparse entries: the source has states, and so + * does the copy. */ + scr_arr_add_states(out); + out->len = (uint32_t)n; const uint8_t *states = scr_arr_states_end(a); for (size_t i = 0; i < a->cap && i < to; i++) { if (i < from || *scr_arr_state_in(states, i) == SCR_ARR_HOLE) continue; @@ -2875,8 +3205,9 @@ ScrArr *scr_arr_sort_primitive(ScrArr *a, bool copy) { if (a->len == 0) return copy ? scr_arr_new(a->elem, 0) : scr_arr_retain(a); size_t dense = a->len < a->cap ? a->len : a->cap; size_t count = 0, undefined_count = 0; - const uint8_t *states = scr_arr_states_end(a); - for (size_t i = 0; i < dense; i++) { + const uint8_t *states = scr_arr_scan_states(a); + if (!states) count = dense; + for (size_t i = 0; states && i < dense; i++) { count += *scr_arr_state_in(states, i) == SCR_ARR_VALUE; undefined_count += *scr_arr_state_in(states, i) == SCR_ARR_UNDEFINED; } @@ -2889,7 +3220,7 @@ ScrArr *scr_arr_sort_primitive(ScrArr *a, bool copy) { if (count && !values) scr_arr_oom(); size_t used = 0; for (size_t i = 0; i < dense; i++) - if (*scr_arr_state_in(states, i) == SCR_ARR_VALUE) values[used++] = scr_arr_dense_get(a, i); + if (SCR_ARR_SCAN_VALUE(states, i)) values[used++] = scr_arr_dense_get(a, i); for (size_t i = 0; i < SCR_AX(a)->sparse_len; i++) if (SCR_AX(a)->sparse[i].state == SCR_ARR_VALUE) values[used++] = SCR_AX(a)->sparse[i].slot; ScrNumberSortKey *numbers = NULL; @@ -2911,8 +3242,10 @@ ScrArr *scr_arr_sort_primitive(ScrArr *a, bool copy) { } size_t present = copy ? a->len : count + undefined_count; ScrArr *out = copy ? scr_arr_new(a->elem, present) : a; - if (copy) out->len = a->len; - if (!copy && scr_arr_is_dense(a)) { + if (!copy && !states) { + /* Values only, in place. */ + if (count) scr_arr_put_slots(a, 0, count, values); + } else if (!copy && scr_arr_is_dense(a)) { if (count) scr_arr_put_slots(a, 0, count, values); scr_arr_cut(a, count); memset(scr_arr_state_run(a, 0, count), SCR_ARR_VALUE, count); @@ -2920,14 +3253,20 @@ ScrArr *scr_arr_sort_primitive(ScrArr *a, bool copy) { scr_arr_clear_dense(a, count + undefined_count, a->len - count - undefined_count); if (undefined_count) memset(scr_arr_dense_at(a, count), 0, undefined_count * scr_arr_slot_width(a)); + } else if (copy) { + /* The values, then present undefined for every other index. */ + for (size_t i = 0; i < count; i++) values[i] = scr_elem_retain_slot(a, values[i]); + scr_arr_push_many(out, count, values); + for (size_t i = count; i < present; i++) { + scr_arr_store_state_owned(out, i, 0, SCR_ARR_UNDEFINED); + out->len = (uint32_t)(i + 1); + } } else { - ScrArrStorage old = {0}; - if (!copy) old = scr_arr_take_storage(a); - for (size_t i = 0; i < count; i++) - scr_arr_store_owned(out, i, copy ? scr_elem_retain_slot(a, values[i]) : values[i]); + ScrArrStorage old = scr_arr_take_storage(a); + for (size_t i = 0; i < count; i++) scr_arr_store_owned(out, i, values[i]); for (size_t i = count; i < present; i++) scr_arr_store_state_owned(out, i, 0, SCR_ARR_UNDEFINED); - if (!copy) scr_arr_free_storage(&old); + scr_arr_free_storage(&old); } free(values); scr_arr_refill(out); @@ -3024,14 +3363,14 @@ static ScrSortCompare scr_sort_compare_for(const ScrArr *a, ScrClosure *f, uint3 bool multi = order >= 2; if (order != 0 && (arity != 2 || (order != 1 && order != -1 && !scr_sort_keys_valid(order)) || - (c.numbers ? key != NULL || multi : a->elem != SCR_ELEM_REF || key == NULL))) + (c.numbers ? key != NULL || multi : !scr_arr_has_ops(a) || key == NULL))) scr_trap("scriptc: invalid keyed sort\n"); if (!c.numbers) { c.borrowing = f->fn_borrowed != NULL; if (a->elem == SCR_ELEM_STR) c.retain = scr_sort_retain_str; else if (a->elem == SCR_ELEM_ARR) c.retain = scr_sort_retain_arr; else if (a->elem == SCR_ELEM_BYTES) c.retain = scr_sort_retain_bytes; - else if (a->elem == SCR_ELEM_REF) c.retain = scr_arr_ops(a)->retain; + else if (scr_arr_has_ops(a)) c.retain = scr_arr_ops(a)->retain; else scr_trap("scriptc: invalid comparator sort element kind\n"); } return c; @@ -4606,7 +4945,7 @@ void scr_arr_sort_values(ScrArr *a, double count, ScrClosure *f, uint32_t arity, * holes, no storage-level undefined and no sparse tail. */ static bool scr_arr_dense_values(const ScrArr *a) { if (!scr_arr_is_dense(a)) return false; - if (a->filled == a->len) return true; + if (a->filled == a->len || !scr_arr_has_states(a)) return true; const uint8_t *states = scr_arr_states_end(a); bool all = true; for (size_t i = 0; i < a->len; i++) all &= *scr_arr_state_in(states, i) == SCR_ARR_VALUE; @@ -4619,7 +4958,7 @@ static bool scr_arr_dense_values(const ScrArr *a) { * elements (stored in 1-byte slots). The slots are borrowed and stay valid * while nothing writes the array. */ const uint64_t *scr_arr_dense_slots(const ScrArr *a, size_t *len) { - if (a->len == 0 || a->elem == SCR_ELEM_BOOL || !scr_arr_dense_values(a)) return NULL; + if (a->len == 0 || scr_arr_slot_width(a) != sizeof(uint64_t) || !scr_arr_dense_values(a)) return NULL; *len = a->len; return a->data; } @@ -4646,7 +4985,9 @@ bool scr_arr_sort_dense(ScrArr *a, ScrClosure *f, uint32_t arity, bool owned, in ScrClosure *key) { uint8_t elem = a->elem; const ScrArrOps *ops = scr_arr_ops(a); - if (a->rc == SIZE_MAX || !scr_arr_dense_values(a)) return false; + /* Compact references sort through the snapshot path, which reads and + * writes 8-byte slots. */ + if (a->rc == SIZE_MAX || elem == SCR_ELEM_CREF || !scr_arr_dense_values(a)) return false; size_t n = a->len; if (n < 2) return true; if (n > SIZE_MAX / 2 / sizeof(uint64_t)) scr_arr_oom(); @@ -4683,16 +5024,16 @@ bool scr_arr_sort_dense(ScrArr *a, ScrClosure *f, uint32_t arity, bool owned, in } else if (a->len >= n && a->cap >= n) { if (!retained) { memcpy(a->data, src, n * sizeof(*src)); - memset(scr_arr_state_run(a, 0, n), SCR_ARR_VALUE, n); + if (scr_arr_has_states(a)) memset(scr_arr_state_run(a, 0, n), SCR_ARR_VALUE, n); } else { /* Publish each new edge before releasing the value it replaces: a * release can collect cycles. The buffer still holds the slots not * yet written. */ for (size_t i = 0; i < n; i++) { - bool held = *scr_arr_state_ptr(a, i) == SCR_ARR_VALUE; + bool held = scr_arr_dense_state(a, i) == SCR_ARR_VALUE; uint64_t old = held ? a->data[i] : 0; a->data[i] = src[i]; - *scr_arr_state_ptr(a, i) = SCR_ARR_VALUE; + if (scr_arr_has_states(a)) *scr_arr_state_ptr(a, i) = SCR_ARR_VALUE; if (held) scr_elem_release(a, old); } } @@ -4787,6 +5128,7 @@ ScrStr *scr_arr_join(ScrArr *a, ScrStr *sep) { case SCR_ELEM_ARR: case SCR_ELEM_BYTES: case SCR_ELEM_REF: + case SCR_ELEM_CREF: /* The compiler rejects join on ref-element arrays (SC1090). */ scr_trap("scriptc: internal error: join on a ref-element array\n"); } @@ -4824,7 +5166,15 @@ ScrStr *scr_str_raw(ScrArr *raw, ScrArr *subs) { bool scr_arr_numeric_key(const char *key, size_t len, double *out) { char text[64]; + if (len == sizeof "undefined" - 1 && memcmp(key, "undefined", len) == 0) { + *out = scr_arr_undefined_key(); + return true; + } if (len == 0 || len >= sizeof text) return false; + /* A number's spelling starts with a digit, '-', "NaN" or "Infinity": + * other names answer without parsing. */ + char first = key[0]; + if (!(first >= '0' && first <= '9') && first != '-' && first != 'N' && first != 'I') return false; memcpy(text, key, len); text[len] = '\0'; char *end; @@ -4838,6 +5188,11 @@ bool scr_arr_numeric_key(const char *key, size_t len, double *out) { return true; } +bool scr_arr_has_properties(const ScrArr *a) { + const ScrArrExt *x = scr_arr_ext_of(a); + return x && (x->prop_len || x->metadata); +} + 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 && @@ -4845,21 +5200,7 @@ void scr_arr_copy_metadata(ScrArr *a, ScrDyn *target) { return; metadata = SCR_AX(a)->metadata; if (!metadata) return; - for (size_t i = 0; i < metadata->v.obj.len; i++) { - const ScrDynEntry *entry = &metadata->v.obj.entries[i]; - ScrStr *key = scr_str_new(entry->key, entry->key_len); - if (!entry->accessor && entry->writable && entry->enumerable && entry->configurable) { - scr_dyn_key_set(target, key, entry->value); - } else { - /* Properties defined through a view keep their attributes. */ - ScrDyn *name = scr_dyn_new_str(key); - ScrDyn *descriptor = scr_dyn_own_descriptor(metadata, key); - if (descriptor) scr_dyn_release(scr_dyn_define_property(target, name, descriptor)); - scr_dyn_release(descriptor); - scr_dyn_release(name); - } - scr_str_release(key); - } + for (size_t i = 0; i < metadata->v.obj.len; i++) scr_dyn_copy_entry(target, metadata, &metadata->v.obj.entries[i]); } ScrArr *scr_arr_named_keys(ScrArr *a) { @@ -4917,8 +5258,27 @@ void scr_arr_adopt_view_properties(ScrArr *a, ScrDyn *view, ScrArr *target) { } ScrDyn *named = view && view->kind == SCR_DYN_ARR ? view->v.arr.properties : NULL; if (!named || named->v.obj.len == 0) return; + /* The view's numeric properties are copies of the native ones (every + * write to them goes to the native array), so only the others move. */ + bool numeric = false; + for (size_t i = 0; i < named->v.obj.len && !numeric; i++) { + double number; + numeric = scr_arr_numeric_key(named->v.obj.entries[i].key, named->v.obj.entries[i].key_len, &number); + } + if (numeric) { + ScrDyn *rest = scr_dyn_new_obj(); + for (size_t i = 0; i < named->v.obj.len; i++) { + const ScrDynEntry *entry = &named->v.obj.entries[i]; + double number; + if (!scr_arr_numeric_key(entry->key, entry->key_len, &number)) scr_dyn_copy_entry(rest, named, entry); + } + named = rest; + } else { + scr_dyn_retain(named); + } ScrArrExt *x = scr_arr_ext(a); ScrDyn *old = x->metadata; - x->metadata = scr_dyn_retain(named); + x->metadata = named->v.obj.len ? named : NULL; + if (!x->metadata) scr_dyn_release(named); scr_dyn_release(old); } diff --git a/packages/runtime/src/scr_array.h b/packages/runtime/src/scr_array.h index 6c41b3dbd..4666750ea 100644 --- a/packages/runtime/src/scr_array.h +++ b/packages/runtime/src/scr_array.h @@ -43,6 +43,17 @@ typedef enum { * constant descriptor per element type, stored once per array; every * array is still monomorphic (one element type, one set of entry points). */ SCR_ELEM_REF, + /* An SCR_ELEM_REF type whose elements are compact class instances in the + * program-lifetime region (SCR_ARR_OPS_COMPACT): 4-byte slots holding the + * address over 8, 0 for NULL. Compact instances are never counted or + * traced, so these arrays retain, release and trace nothing. */ + SCR_ELEM_CREF, + /* Numbers that the compiler proves are always int32 (never -0), in + * 4-byte slots: arrays that only compiled element accesses ever reach + * (backend/llvm int32 arrays). Every runtime slot is the value's double + * bits, as for SCR_ELEM_F64: scr_arr_dense_get and scr_arr_dense_put + * convert, so the paths compiled code calls need no case of their own. */ + SCR_ELEM_I32, } ScrElemKind; typedef enum { @@ -63,8 +74,43 @@ typedef struct ScrArrOps { /* Registry id (scr_arr_ops_table), 0 until scr_arr_ops_register assigns * it on the first array of the type. Descriptors are writable for it. */ uint32_t id; + /* SCR_ARR_OPS_*. On 64-bit targets the field takes the struct's former + * tail padding, so descriptors emitted without it read 0 here; on 32-bit + * targets it lies past their end and is never read (scr_arr_ops_compact). */ + uint32_t flags; } ScrArrOps; +/* The elements are compact class instances in the program-lifetime region: + * arrays of the type are SCR_ELEM_CREF (llvm/shapes.ts arrOps). */ +#define SCR_ARR_OPS_COMPACT 1u + +/* Whether `ops` describes compact class elements. Compact instances exist + * only on 64-bit targets. */ +static inline bool scr_arr_ops_compact(const ScrArrOps *ops) { +#if UINTPTR_MAX > 0xffffffffu + return (ops->flags & SCR_ARR_OPS_COMPACT) != 0; +#else + (void)ops; + return false; +#endif +} + +/* `release` does nothing: the element type's instances live until the + * process exits (llvm/shapes.ts arrOps). Destroying such an array frees only + * its storage. */ +#define SCR_ARR_OPS_RELEASE_NOTHING 2u + +/* Whether `ops` has SCR_ARR_OPS_RELEASE_NOTHING. 32-bit descriptors end + * before the flags word, so they never have it. */ +static inline bool scr_arr_ops_release_nothing_flag(const ScrArrOps *ops) { +#if UINTPTR_MAX > 0xffffffffu + return (ops->flags & SCR_ARR_OPS_RELEASE_NOTHING) != 0; +#else + (void)ops; + return false; +#endif +} + typedef struct ScrArr { size_t rc; /* SIZE_MAX = immortal (unused for arrays; kept per convention) */ /* Length and dense capacity are at most SCR_ARR_MAX_LENGTH (2^32 - 1); @@ -82,9 +128,9 @@ typedef struct ScrArr { * header. Kept beside `elem` so the RC fast paths test it with one load. */ uint8_t cyc; /* The low SCR_ARR_OPS_SHIFT bits: storage flags (SCR_ARR_INLINE_SLOTS, - * SCR_ARR_HEAD_ROOM, SCR_ARR_UNSHIFTED, SCR_ARR_HAS_EXT). The rest: the + * SCR_ARR_HEAD_ROOM, SCR_ARR_UNSHIFTED, SCR_ARR_HAS_EXT, SCR_ARR_STATES). The rest: the * registry id of the element type's ops (scr_arr_ops), 0 for every kind - * but SCR_ELEM_REF. */ + * but SCR_ELEM_REF and SCR_ELEM_CREF. */ uint16_t spare; /* A lower bound on the hole-free prefix: filled <= len, filled <= cap, * and every dense state below it is SCR_ARR_VALUE. Element reads below @@ -93,10 +139,14 @@ typedef struct ScrArr { * undefined) or move states below it lowers it first (scr_arr_cut); * 0 is always valid. */ uint32_t filled; - /* Dense values: `cap` 8-byte slots, or `cap` bytes for SCR_ELEM_BOOL - * (scr_arr_slot_width), followed in the same allocation by one state byte - * per slot in reverse slot order (scr_arr_states_end). Sparse entries and properties keep 8-byte - * slots for every kind; a bool slot is 0 or 1 wherever it lives. Rarely + /* Dense values: `cap` 8-byte slots, `cap` bytes for SCR_ELEM_BOOL or + * `cap` 4-byte slots for SCR_ELEM_CREF and SCR_ELEM_I32 + * (scr_arr_slot_width), followed in the same allocation, when the array + * has states (SCR_ARR_STATES), by one state byte per slot in reverse slot + * order (scr_arr_states_end). Sparse entries and properties keep 8-byte + * slots for every kind; a bool slot is 0 or 1 wherever it lives, a CREF + * slot's 8-byte form is the pointer and an I32 slot's the double's bits + * (scr_arr_dense_get). Rarely * used storage (sparse indices, noncanonical numeric properties, * match-result metadata) lives out of line, and an array that has it * (SCR_ARR_HAS_EXT) keeps a pointer to it in the word before the dense @@ -124,14 +174,17 @@ typedef struct 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 * scr_arr_retain and scr_arr_release; its inline element access uses len and - * cap (fields 1 and 2, i32), filled (field 6, i32) and data (field 7), and - * finds the state of slot i at data + cap * (slot width + 1) - 1 - i. */ + * cap (fields 1 and 2, i32), spare (field 5, i16, for SCR_ARR_STATES), + * filled (field 6, i32) and data (field 7), and finds the state of slot i + * of an array with states at data + cap * (slot width + 1) - 1 - i. */ _Static_assert(offsetof(ScrArr, len) == sizeof(size_t) && sizeof(((ScrArr *)0)->len) == 4, "emitted length reads load ScrArr.len as %ScrArr field 1, an i32"); _Static_assert(offsetof(ScrArr, cap) == sizeof(size_t) + 4 && sizeof(((ScrArr *)0)->cap) == 4, "emitted capacity reads load ScrArr.cap as %ScrArr field 2, an i32"); _Static_assert(offsetof(ScrArr, cyc) == sizeof(size_t) + 9, "inline RC fast paths read ScrArr.cyc as %ScrArr field 4"); +_Static_assert(offsetof(ScrArr, spare) == sizeof(size_t) + 10, + "inline element access reads ScrArr.spare as %ScrArr field 5"); _Static_assert(offsetof(ScrArr, filled) == sizeof(size_t) + 12, "inline element access reads ScrArr.filled as %ScrArr field 6"); _Static_assert(offsetof(ScrArr, data) == sizeof(size_t) + 16, @@ -145,11 +198,21 @@ _Static_assert(sizeof(ScrArr) == sizeof(size_t) + 16 + sizeof(void *), * count. Unshifted: an unshift had to move every element. Has ext: the word * before the dense allocation (head room included) points at the array's * ScrArrExt; such an array always has an allocation of its own, without - * inline slots. */ + * inline slots. States: the dense slots carry state bytes (see below). */ #define SCR_ARR_INLINE_SLOTS 1u #define SCR_ARR_HEAD_ROOM 2u #define SCR_ARR_UNSHIFTED 4u #define SCR_ARR_HAS_EXT 8u +#define SCR_ARR_STATES 16u + +/* An array starts without state bytes. Without them, every index below + * `len` is a present value in dense storage: len <= cap, there are no + * sparse entries, and `filled` equals `len` whenever emitted code can read + * the array (its inline reads load a state only at or past `filled`). The + * first hole or present `undefined` below the length, + * a length set past the elements, or a sparse entry gives the array its + * states (scr_arr_add_states), which it keeps. */ +static inline bool scr_arr_has_states(const ScrArr *a) { return (a->spare & SCR_ARR_STATES) != 0; } /* The start of the dense slots' allocation: `data` less any head room. */ static inline uint64_t *scr_arr_block(const ScrArr *a) { @@ -173,9 +236,12 @@ static inline struct ScrArrExt *scr_arr_ext_of(const ScrArr *a) { * Id 0 is no ops; ids are assigned on each descriptor's first array, in * any thread. A program with more element types than the field holds keeps * the ops of the rest in the array's ext (SCR_ARR_OPS_OVERFLOW). */ -#define SCR_ARR_OPS_SHIFT 4 +#define SCR_ARR_OPS_SHIFT 5 #define SCR_ARR_OPS_OVERFLOW (UINT16_MAX >> SCR_ARR_OPS_SHIFT) extern const ScrArrOps *scr_arr_ops_table[SCR_ARR_OPS_OVERFLOW]; +/* By registry id: 1 when the descriptor has SCR_ARR_OPS_RELEASE_NOTHING. + * Written before the id is published; the overflow id stays 0. */ +extern uint8_t scr_arr_ops_release_nothing[SCR_ARR_OPS_OVERFLOW + 1]; /* Assigns the id on a descriptor's first array; out of line, so the id * load in scr_arr_ops_id stays the only cost on later arrays. */ __attribute__((cold, noinline)) uint32_t scr_arr_ops_register(ScrArrOps *ops); @@ -190,24 +256,33 @@ static inline uint32_t scr_arr_ops_id(const ScrArrOps *ops) { /* The element ops of an array whose ops have no registry id. */ const ScrArrOps *scr_arr_ops_overflow(const ScrArr *a); -/* The element ops of `a`: NULL for every kind but SCR_ELEM_REF. */ +/* The element ops of `a`: NULL for every kind but SCR_ELEM_REF and + * SCR_ELEM_CREF. */ static inline const ScrArrOps *scr_arr_ops(const ScrArr *a) { uint32_t id = (uint32_t)a->spare >> SCR_ARR_OPS_SHIFT; if (__builtin_expect(id == SCR_ARR_OPS_OVERFLOW, 0)) return scr_arr_ops_overflow(a); return scr_arr_ops_table[id]; } -/* Bytes per dense value slot. */ -static inline size_t scr_arr_slot_width(const ScrArr *a) { - return a->elem == SCR_ELEM_BOOL ? 1 : sizeof(uint64_t); +/* Bytes per dense value slot of element kind `elem`. */ +static inline size_t scr_elem_slot_width(uint8_t elem) { + if (elem == SCR_ELEM_BOOL) return 1; + if (__builtin_expect(elem == SCR_ELEM_CREF || elem == SCR_ELEM_I32, 0)) return sizeof(uint32_t); + return sizeof(uint64_t); } -/* The dense states, one byte per slot, share the values' allocation (inline - * slots included). They follow the `cap` value slots in reverse slot order: - * the state of slot i is the byte i + 1 before scr_arr_states_end(a). Removing - * slots from the front (head room: `data` advances, `cap` drops) leaves every - * remaining state at its address. The address is computed on integers, so a - * NULL `data` stays NULL. */ +/* Bytes per dense value slot. */ +static inline size_t scr_arr_slot_width(const ScrArr *a) { + return scr_elem_slot_width(a->elem); +} + +/* The dense states of an array with states, one byte per slot, share the + * values' allocation (inline slots included). They follow the `cap` value + * slots in reverse slot order: the state of slot i is the byte i + 1 before + * scr_arr_states_end(a). Removing slots from the front (head room: `data` + * advances, `cap` drops) leaves every remaining state at its address. The + * address is computed on integers, so a NULL `data` stays NULL. Arrays + * without states have no bytes there. */ static inline uint8_t *scr_arr_states_end(const ScrArr *a) { return (uint8_t *)((uintptr_t)a->data + (uintptr_t)a->cap * (scr_arr_slot_width(a) + 1)); } @@ -233,14 +308,71 @@ static inline uint8_t *scr_arr_state_run(const ScrArr *a, size_t from, size_t co return scr_arr_states_end(a) - from - count; } +/* The state of dense slot `index` (< cap), with or without state bytes. */ +static inline uint8_t scr_arr_dense_state(const ScrArr *a, size_t index) { + if (!scr_arr_has_states(a)) return index < a->len ? SCR_ARR_VALUE : SCR_ARR_HOLE; + return *scr_arr_state_ptr(a, index); +} + +/* The dense states of `a` for a scan (scr_arr_state_in), or NULL when it + * has none: every index below its length is then a value. */ +static inline const uint8_t *scr_arr_scan_states(const ScrArr *a) { + return scr_arr_has_states(a) ? scr_arr_states_end(a) : NULL; +} + +/* Give `a` state bytes: present values below the length, holes above it. + * May move the dense slots. */ +void scr_arr_add_states(ScrArr *a); + +static inline void scr_arr_need_states(ScrArr *a) { + if (!scr_arr_has_states(a)) scr_arr_add_states(a); +} +/* A CREF slot's pointer, and the slot of a pointer (NULL or a compact + * instance below 32 GiB). */ +static inline uint64_t scr_cref_decode(uint32_t ref) { return (uint64_t)ref << 3; } +static inline uint32_t scr_cref_encode(uint64_t slot) { return (uint32_t)(slot >> 3); } + +/* The bits of `d` as a slot, and back. */ +static inline uint64_t scr_arr_f64_slot(double d) { + uint64_t slot; + memcpy(&slot, &d, sizeof slot); + return slot; +} + +static inline double scr_arr_slot_f64(uint64_t slot) { + double d; + memcpy(&d, &slot, sizeof d); + return d; +} + +/* An int32 array's slot for a double: its stores are proven int32s that + * are not -0 (and a cleared slot is +0), so anything else is a compiler + * bug. */ +static inline int32_t scr_arr_i32_of(uint64_t slot) { + double d = scr_arr_slot_f64(slot); + if (__builtin_expect(!(d >= -2147483648.0 && d <= 2147483647.0 && d == (double)(int32_t)d) || + slot == UINT64_C(0x8000000000000000), + 0)) + __builtin_trap(); + return (int32_t)d; +} + /* The dense value at `index` (< cap) as an 8-byte slot, and its store. */ static inline uint64_t scr_arr_dense_get(const ScrArr *a, size_t index) { if (a->elem == SCR_ELEM_BOOL) return ((const uint8_t *)a->data)[index]; + if (__builtin_expect(a->elem == SCR_ELEM_CREF, 0)) + return scr_cref_decode(((const uint32_t *)a->data)[index]); + if (__builtin_expect(a->elem == SCR_ELEM_I32, 0)) + return scr_arr_f64_slot((double)((const int32_t *)a->data)[index]); return a->data[index]; } static inline void scr_arr_dense_put(ScrArr *a, size_t index, uint64_t slot) { if (a->elem == SCR_ELEM_BOOL) ((uint8_t *)a->data)[index] = (uint8_t)slot; + else if (__builtin_expect(a->elem == SCR_ELEM_CREF, 0)) + ((uint32_t *)a->data)[index] = scr_cref_encode(slot); + else if (__builtin_expect(a->elem == SCR_ELEM_I32, 0)) + ((int32_t *)a->data)[index] = scr_arr_i32_of(slot); else a->data[index] = slot; } @@ -270,7 +402,8 @@ static inline size_t scr_arr_value_run(const uint8_t *states_end, size_t f, size static inline void scr_arr_refill(ScrArr *a) { size_t end = a->len < a->cap ? a->len : a->cap; size_t f = a->filled; - if (f < end) f = scr_arr_value_run(scr_arr_states_end(a), f, end); + if (!scr_arr_has_states(a)) f = end; + else if (f < end) f = scr_arr_value_run(scr_arr_states_end(a), f, end); a->filled = (uint32_t)f; } @@ -300,10 +433,14 @@ ScrArr *scr_arr_new_ops(const ScrArrOps *ops, size_t initial_cap); * array plus five 8-byte slots). Otherwise exactly scr_arr_new(elem, 0) and * scr_arr_new_ops(ops, 0). Returns +1. */ ScrArr *scr_arr_new_small(ScrElemKind elem); +/* An int32 array's storage as doubles, in place (SCR_ELEM_I32 to + * SCR_ELEM_F64); no-op for any other kind. */ +void scr_arr_widen_i32(ScrArr *a); ScrArr *scr_arr_new_ops_small(const ScrArrOps *ops); /* scr_arr_new(elem, cap) for a known final length: the `cap` slots share - * the array's own block (one allocation). Returns +1. */ -ScrArr *scr_arr_new_inline(ScrElemKind elem, size_t cap); + * the array's own block (one allocation). With `states`, the array starts + * with states (for holes or present undefined). Returns +1. */ +ScrArr *scr_arr_new_inline(ScrElemKind elem, size_t cap, bool states); static inline ScrArr *scr_arr_retain(ScrArr *a) { if (a->rc != SIZE_MAX) { @@ -323,9 +460,15 @@ void scr_arr_copy_metadata(ScrArr *a, struct ScrDyn *target); * properties of `target`, so neither is lost when the storage is swapped. */ void scr_arr_adopt_view_properties(ScrArr *a, struct ScrDyn *view, ScrArr *target); /* True when `key` is the string spelling of a number that is not an array - * index ("-1", "1.5", "NaN", "Infinity"), with the number in *out. Arrays - * keep such properties in their noncanonical property table. */ + * index ("-1", "1.5", "NaN", "Infinity"), or "undefined", with the number in + * *out (scr_arr_undefined_key for "undefined"). Arrays keep such properties + * in their noncanonical property table. */ bool scr_arr_numeric_key(const char *key, size_t len, double *out); +/* The number compiled code passes as the key of an element access whose + * index is undefined at run time: a NaN with its own payload, which the + * property table spells "undefined" (JavaScript names that property, not + * "NaN"). */ +double scr_arr_undefined_key(void); /* Whether the array still has the named or noncanonical numeric property * `key`: `for...in` skips a key deleted before its turn. Borrows both. */ bool scr_arr_has_named_key(ScrArr *a, ScrStr *key); @@ -333,6 +476,10 @@ bool scr_arr_has_named_key(ScrArr *a, ScrStr *key); * noncanonical numeric properties, then the named properties in insertion * order. +1. */ ScrArr *scr_arr_named_keys(ScrArr *a); +/* Whether the array has properties besides its elements (noncanonical + * numeric keys, match metadata, named properties): util.inspect lists + * them after the elements. */ +bool scr_arr_has_properties(const ScrArr *a); /* 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 diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index 9e3365fbe..6afd174b7 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -72,15 +72,26 @@ void __lsan_unregister_root_region(const void *p, size_t size) __attribute__((we #if !defined(_WIN32) && !defined(__wasi__) -/* Keep stacks out of malloc's size classes. Reuse a bounded number during - * a burst of promise/generator work, then unmap the spares before the loop - * sleeps. This avoids both heap fragmentation and per-call mmap overhead. - * The sanitizer lane unmaps every finished stack immediately. */ +/* Keep stacks out of malloc's size classes. Reuse a bounded number of + * finished stacks, which avoids both heap fragmentation and per-call mmap + * overhead. A server's loop sleeps between nearly every pair of requests, + * so unmapping the spares at each sleep would map, fault in and unmap a + * stack for every request that awaits. Spares therefore outlive short + * sleeps: at most once per SCR_FIBER_TRIM_MS, a sleep unmaps the spares + * that no fiber took since the previous trim (the pool's low-water mark), + * and a sleep longer than twice that unmaps every spare, so an idle + * program gives the memory back as before. The sanitizer lane unmaps + * every finished stack immediately. */ #ifndef SCR_ASAN_FIBERS #define SCR_FIBER_SPARES 64 +#define SCR_FIBER_TRIM_MS 1000.0 static SCR_TL void *scr_fiber_spares[SCR_FIBER_SPARES]; static SCR_TL size_t scr_fiber_nspares; static SCR_TL bool scr_fiber_cleanup_registered; +/* The fewest spares the pool held since the last trim: the spares below + * this index went unused for the whole window. */ +static SCR_TL size_t scr_fiber_spares_low; +static SCR_TL double scr_fiber_trimmed_at; #endif static void scr_fiber_stacks_clear(void) { @@ -89,6 +100,29 @@ static void scr_fiber_stacks_clear(void) { (void)munmap(scr_fiber_spares[--scr_fiber_nspares], SCR_FIBER_STACK); scr_fiber_spares[scr_fiber_nspares] = NULL; } + scr_fiber_spares_low = 0; +#endif +} + +/* The loop is about to sleep for `wait` ms (see above). */ +static void scr_fiber_stacks_trim(double now, double wait) { +#ifndef SCR_ASAN_FIBERS + if (wait > 2 * SCR_FIBER_TRIM_MS) { + scr_fiber_stacks_clear(); + scr_fiber_trimmed_at = now; + return; + } + if (now - scr_fiber_trimmed_at < SCR_FIBER_TRIM_MS) return; + size_t unused = scr_fiber_spares_low; + for (size_t i = 0; i < unused; i++) (void)munmap(scr_fiber_spares[i], SCR_FIBER_STACK); + scr_fiber_nspares -= unused; + memmove(scr_fiber_spares, scr_fiber_spares + unused, scr_fiber_nspares * sizeof *scr_fiber_spares); + memset(scr_fiber_spares + scr_fiber_nspares, 0, unused * sizeof *scr_fiber_spares); + scr_fiber_spares_low = scr_fiber_nspares; + scr_fiber_trimmed_at = now; +#else + (void)now; + (void)wait; #endif } @@ -97,6 +131,7 @@ static void *scr_fiber_stack_new(void) { if (scr_fiber_nspares > 0) { void *stack = scr_fiber_spares[--scr_fiber_nspares]; scr_fiber_spares[scr_fiber_nspares] = NULL; + if (scr_fiber_nspares < scr_fiber_spares_low) scr_fiber_spares_low = scr_fiber_nspares; return stack; } if (!scr_fiber_cleanup_registered) { @@ -4599,7 +4634,7 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) { /* An idle moment: a full pass the death rule deferred runs now. */ if (due > now && scr_cyc_collect_idle()) now = scr_now_ms(); #if !defined(_WIN32) && !defined(__wasi__) - if (due > now) scr_fiber_stacks_clear(); + if (due > now) scr_fiber_stacks_trim(now, due - now); #endif #ifdef SCR_WORKERS if (due > now) scr_sa_loop_waiting(true); diff --git a/packages/runtime/src/scr_bytes.c b/packages/runtime/src/scr_bytes.c index 7a6a76cf9..7bcfd0cfb 100644 --- a/packages/runtime/src/scr_bytes.c +++ b/packages/runtime/src/scr_bytes.c @@ -3122,7 +3122,7 @@ static void scr_bytes_fill_from_arr(uint8_t *out, ScrBytesElem elem, const ScrAr /* Sparse tails and explicit undefined retain ordinary iteration * semantics. Numeric array slots own no conversion callbacks. */ if (index < arr->cap) { - if (*scr_arr_state_ptr(arr, index) == SCR_ARR_VALUE) + if (scr_arr_dense_state(arr, index) == SCR_ARR_VALUE) memcpy(&values[i], &arr->data[index], sizeof(double)); else values[i] = NAN; } else values[i] = scr_arr_get_number(arr, (double)index); @@ -3271,16 +3271,24 @@ double scr_bytes_index_of(const ScrBytes *b, const ScrBytes *needle, double off, } if (nlen == 0) return o > (double)len ? (double)len : o; /* '' matches at the clamp */ if (nlen > len) return -1; + /* Candidates are the positions holding the needle's first byte; only + * those compare the rest. */ + const uint8_t first = needle->data[0]; if (fwd) { size_t start = o > (double)len ? len : (size_t)o; - for (size_t i = start; i + nlen <= len; i++) { - if (memcmp(b->data + i, needle->data, nlen) == 0) return (double)i; + const uint8_t *at = b->data + start, *end = b->data + (len - nlen) + 1; + while (at < end) { + at = memchr(at, first, (size_t)(end - at)); + if (!at) return -1; + if (nlen == 1 || memcmp(at + 1, needle->data + 1, nlen - 1) == 0) return (double)(at - b->data); + at++; } return -1; } size_t start = o > (double)(len - nlen) ? len - nlen : (size_t)o; for (size_t i = start;; i--) { - if (memcmp(b->data + i, needle->data, nlen) == 0) return (double)i; + if (b->data[i] == first && (nlen == 1 || memcmp(b->data + i + 1, needle->data + 1, nlen - 1) == 0)) + return (double)i; if (i == 0) break; } return -1; diff --git a/packages/runtime/src/scr_bytes_dyn.c b/packages/runtime/src/scr_bytes_dyn.c index 280aa423c..c492d43b1 100644 --- a/packages/runtime/src/scr_bytes_dyn.c +++ b/packages/runtime/src/scr_bytes_dyn.c @@ -509,8 +509,9 @@ static bool scr_bytes_dyn_buffer(const ScrDyn *recv) { /* One entry per method family, so a program links only the families it * names: each runs its methods on a Buffer receiver and gives any other * receiver the ordinary dispatch. */ -ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, - size_t argc, const char *what) { +ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *method, + int32_t *mid_slot, ScrDyn *const *args, size_t argc, + const char *what) { if (scr_bytes_dyn_buffer(recv)) { bool read = !strncmp(method, "read", 4); const char *rest = method + (read ? 4 : 5); @@ -518,40 +519,45 @@ ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *meth if (!strcmp(rest, scr_bytes_dyn_names[at].name)) return scr_bytes_dyn_num_method(recv->v.bytes, read, at, args, argc); } } - return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); + return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); } -ScrDyn *scr_dyn_invoke_buffer_write(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, - size_t argc, const char *what) { +ScrDyn *scr_dyn_invoke_buffer_write(ScrDyn *recv, ScrDyn *callee, const char *method, + int32_t *mid_slot, ScrDyn *const *args, size_t argc, + const char *what) { if (scr_bytes_dyn_buffer(recv)) return scr_bytes_dyn_write(recv->v.bytes, args, argc); - return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); + return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); } -ScrDyn *scr_dyn_invoke_buffer_fill(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, - size_t argc, const char *what) { +ScrDyn *scr_dyn_invoke_buffer_fill(ScrDyn *recv, ScrDyn *callee, const char *method, + int32_t *mid_slot, ScrDyn *const *args, size_t argc, + const char *what) { if (scr_bytes_dyn_buffer(recv)) return scr_bytes_dyn_fill(recv, args, argc); - return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); + return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); } -ScrDyn *scr_dyn_invoke_buffer_to_string(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, - size_t argc, const char *what) { +ScrDyn *scr_dyn_invoke_buffer_to_string(ScrDyn *recv, ScrDyn *callee, const char *method, + int32_t *mid_slot, ScrDyn *const *args, size_t argc, + const char *what) { if (scr_bytes_dyn_buffer(recv)) return scr_bytes_dyn_to_string(recv->v.bytes, args, argc); - return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); + return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); } /* indexOf, lastIndexOf and includes. */ -ScrDyn *scr_dyn_invoke_buffer_search(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, - size_t argc, const char *what) { +ScrDyn *scr_dyn_invoke_buffer_search(ScrDyn *recv, ScrDyn *callee, const char *method, + int32_t *mid_slot, ScrDyn *const *args, size_t argc, + const char *what) { if (scr_bytes_dyn_buffer(recv)) { bool includes = !strcmp(method, "includes"); return scr_bytes_dyn_index_of(recv->v.bytes, args, argc, strcmp(method, "lastIndexOf") != 0, includes); } - return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); + return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); } /* toJSON, equals, compare, copy, swap16/32/64, and the refused members. */ -ScrDyn *scr_dyn_invoke_buffer(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, - size_t argc, const char *what) { +ScrDyn *scr_dyn_invoke_buffer(ScrDyn *recv, ScrDyn *callee, const char *method, + int32_t *mid_slot, ScrDyn *const *args, size_t argc, + const char *what) { if (scr_bytes_dyn_buffer(recv)) { ScrBytes *b = recv->v.bytes; if (!strcmp(method, "toJSON")) return scr_bytes_dyn_to_json(b); @@ -584,5 +590,5 @@ ScrDyn *scr_dyn_invoke_buffer(ScrDyn *recv, ScrDyn *callee, const char *method, return NULL; } } - return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); + return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); } diff --git a/packages/runtime/src/scr_child.c b/packages/runtime/src/scr_child.c index ec685656d..df7543500 100644 --- a/packages/runtime/src/scr_child.c +++ b/packages/runtime/src/scr_child.c @@ -1105,9 +1105,9 @@ char **scr_child_argv(ScrStr *cmd, ScrArr *args) { static ScrSpawnRes *scr_spawn_sync_core(ScrStr *cmd, ScrArr *args, double timeout_ms, int killsig, int in_mode, int out_mode, - int err_mode) { + int err_mode, ScrArr *env_pairs) { ScrWinSyncRes w; - scr_win_run_sync(cmd, args, NULL, in_mode, out_mode, err_mode, NULL, NULL, + scr_win_run_sync(cmd, args, NULL, in_mode, out_mode, err_mode, NULL, env_pairs, timeout_ms, &w); if (w.spawn_errname != NULL) { free(w.out.data); @@ -1136,19 +1136,26 @@ static ScrSpawnRes *scr_spawn_sync_core(ScrStr *cmd, ScrArr *args, double timeou } ScrSpawnRes *scr_spawn_sync(ScrStr *cmd, ScrArr *args) { - return scr_spawn_sync_core(cmd, args, 0, SIGTERM, 0, 0, 0); + return scr_spawn_sync_core(cmd, args, 0, SIGTERM, 0, 0, 0, NULL); } ScrSpawnRes *scr_spawn_sync_opts(ScrStr *cmd, ScrArr *args, double timeout_ms, ScrStr *killsignal, double in_mode, double out_mode, double err_mode) { + return scr_spawn_sync_env(cmd, args, timeout_ms, killsignal, in_mode, out_mode, err_mode, + NULL); +} + +ScrSpawnRes *scr_spawn_sync_env(ScrStr *cmd, ScrArr *args, double timeout_ms, + ScrStr *killsignal, double in_mode, double out_mode, + double err_mode, ScrArr *env_pairs) { int killsig = SIGTERM; if (killsignal->len > 0) { int resolved = scr_signal_from_name(killsignal); if (resolved > 0) killsig = resolved; } return scr_spawn_sync_core(cmd, args, timeout_ms, killsig, (int)in_mode, - (int)out_mode, (int)err_mode); + (int)out_mode, (int)err_mode, env_pairs); } ScrSpawnRes *scr_spawn_sync_stdio_str(ScrStr *cmd, ScrArr *args, double timeout_ms, @@ -3101,9 +3108,61 @@ static ScrSpawnRes *scr_spawn_res_new(bool has_status, double status, * documented divergence). timeout_ms > 0 sends `killsig` at the deadline * and the result carries error: ETIMEDOUT + the signal — never a throw, * Node's spawnSync shape. */ +static char **scr_exec_envp(ScrArr *pairs); +static void scr_exec_envp_free(char **envp); + +/* posix_spawnp for the child_process spawns. Without `envp` the command + * resolves on this process's PATH over `environ`. With `envp` (a + * replacement environment) it resolves the way Node's libuv runs execvp + * in the child: a name with a '/' runs as given; otherwise each entry of + * the replacement's PATH (an empty entry is the current directory), or of + * the default path when the replacement has no PATH, is tried in order, + * and a candidate whose exec fails with ENOENT, ENOTDIR or EACCES (a + * missing file, or a script whose interpreter is missing) moves on to the + * next entry. The last such failure is EACCES if any candidate was denied, + * else ENOENT. Relative entries are resolved by the child, which starts in + * `cwd`. Returns posix_spawn's error number. */ +static int scr_spawn_command(pid_t *pid, const char *file, const posix_spawn_file_actions_t *fa, + const posix_spawnattr_t *attr, char *const *argv, + char *const *envp, const char *cwd) { + (void)cwd; /* the file actions change into it before the exec */ + if (envp == NULL) return posix_spawnp(pid, file, fa, attr, argv, environ); + if (strchr(file, '/')) return posix_spawn(pid, file, fa, attr, argv, envp); + const char *path = "/usr/bin:/bin"; + for (char *const *e = envp; *e; e++) { + if (strncmp(*e, "PATH=", 5) == 0) { + path = *e + 5; + break; + } + } + size_t file_len = strlen(file); + bool denied = false; + for (const char *dir = path;;) { + const char *end = strchr(dir, ':'); + size_t dir_len = end ? (size_t)(end - dir) : strlen(dir); + const char *prefix = dir_len ? dir : "."; + size_t prefix_len = dir_len ? dir_len : 1; + char *candidate = malloc(prefix_len + 1 + file_len + 1); + if (!candidate) scr_child_oom(); + memcpy(candidate, prefix, prefix_len); + candidate[prefix_len] = '/'; + memcpy(candidate + prefix_len + 1, file, file_len + 1); + int err = posix_spawn(pid, candidate, fa, attr, argv, envp); + free(candidate); + if (err == EACCES) denied = true; + else if (err != ENOENT && err != ENOTDIR) return err; + if (!end) break; + dir = end + 1; + } + return denied ? EACCES : ENOENT; +} + +/* env_pairs: NULL inherits the parent's environment; else [k,v,...] + * replaces it, as Node's `env` option does, and the command is looked up + * on the replacement's PATH (scr_spawn_command). */ static ScrSpawnRes *scr_spawn_sync_core(ScrStr *cmd, ScrArr *args, double timeout_ms, int killsig, int in_mode, int out_mode, - int err_mode) { + int err_mode, ScrArr *env_pairs) { if (out_mode == 2 || err_mode == 2) { /* Inherited output fds: flush the parent's buffers first so earlier * logs precede the child's writes (Node's practical ordering). */ @@ -3138,10 +3197,12 @@ static ScrSpawnRes *scr_spawn_sync_core(ScrStr *cmd, ScrArr *args, double timeou } char **argv = scr_child_argv(cmd, args); + char **envp = env_pairs != NULL ? scr_exec_envp(env_pairs) : NULL; pid_t pid = -1; - int spawn_err = posix_spawnp(&pid, cmd->data, &fa, NULL, argv, environ); + int spawn_err = scr_spawn_command(&pid, cmd->data, &fa, NULL, argv, envp, NULL); posix_spawn_file_actions_destroy(&fa); free(argv); + scr_exec_envp_free(envp); if (cap_out) close(outfd[1]); if (cap_err) close(errfd[1]); @@ -3242,7 +3303,7 @@ static ScrSpawnRes *scr_spawn_sync_core(ScrStr *cmd, ScrArr *args, double timeou } ScrSpawnRes *scr_spawn_sync(ScrStr *cmd, ScrArr *args) { - return scr_spawn_sync_core(cmd, args, 0, SIGTERM, 0, 0, 0); + return scr_spawn_sync_core(cmd, args, 0, SIGTERM, 0, 0, 0, NULL); } /* The options entry (cp.spawnSyncOpts): killSignal as a NAME ("" = the @@ -3252,13 +3313,20 @@ ScrSpawnRes *scr_spawn_sync(ScrStr *cmd, ScrArr *args) { ScrSpawnRes *scr_spawn_sync_opts(ScrStr *cmd, ScrArr *args, double timeout_ms, ScrStr *killsignal, double in_mode, double out_mode, double err_mode) { + return scr_spawn_sync_env(cmd, args, timeout_ms, killsignal, in_mode, out_mode, err_mode, + NULL); +} + +ScrSpawnRes *scr_spawn_sync_env(ScrStr *cmd, ScrArr *args, double timeout_ms, + ScrStr *killsignal, double in_mode, double out_mode, + double err_mode, ScrArr *env_pairs) { int killsig = SIGTERM; if (killsignal->len > 0) { int resolved = scr_signal_from_name(killsignal); if (resolved > 0) killsig = resolved; } return scr_spawn_sync_core(cmd, args, timeout_ms, killsig, (int)in_mode, - (int)out_mode, (int)err_mode); + (int)out_mode, (int)err_mode, env_pairs); } /* The runtime-string stdio entry (cp.spawnSyncStdioStr): the compiler @@ -3478,8 +3546,8 @@ ScrStr *scr_exec_sync_core(ScrStr *cmd, ScrArr *args, const ScrExecOpts *o) { char **argv = scr_child_argv(cmd, args); char **envp = o->env_pairs != NULL ? scr_exec_envp(o->env_pairs) : NULL; pid_t pid = -1; - int spawn_err = posix_spawnp(&pid, cmd->data, &fa, NULL, argv, - envp != NULL ? envp : environ); + int spawn_err = scr_spawn_command(&pid, cmd->data, &fa, NULL, argv, envp, + o->cwd != NULL ? o->cwd->data : NULL); posix_spawn_file_actions_destroy(&fa); free(argv); scr_exec_envp_free(envp); @@ -5037,8 +5105,8 @@ ScrChild *scr_spawn_opts(ScrStr *cmd, ScrArr *args, double in_mode, char **envp = has_env && env_pairs != NULL ? scr_exec_envp(env_pairs) : NULL; pid_t pid = -1; const char *file = shell ? "/bin/sh" : cmd->data; - int spawn_err = posix_spawnp(&pid, file, &fa, &at, argv, - envp != NULL ? envp : environ); + int spawn_err = scr_spawn_command(&pid, file, &fa, &at, argv, envp, + cwd != NULL && cwd->len > 0 ? cwd->data : NULL); posix_spawnattr_destroy(&at); posix_spawn_file_actions_destroy(&fa); if (!shell) free(argv); diff --git a/packages/runtime/src/scr_child.h b/packages/runtime/src/scr_child.h index e899130d8..a5853ad59 100644 --- a/packages/runtime/src/scr_child.h +++ b/packages/runtime/src/scr_child.h @@ -23,6 +23,11 @@ ScrSpawnRes *scr_spawn_sync(ScrStr *cmd, ScrArr *args); /* +1, never throws */ ScrSpawnRes *scr_spawn_sync_opts(ScrStr *cmd, ScrArr *args, double timeout_ms, ScrStr *killsignal, double in_mode, double out_mode, double err_mode); +/* scr_spawn_sync_opts with an `env` option: [k,v,...] pairs replace the + * child's environment (NULL inherits the parent's). */ +ScrSpawnRes *scr_spawn_sync_env(ScrStr *cmd, ScrArr *args, double timeout_ms, + ScrStr *killsignal, double in_mode, double out_mode, + double err_mode, ScrArr *env_pairs); /* The runtime-string stdio entry: the compiler proved the value is * "pipe" | "ignore" | "inherit" by type; the mode mapping happens here. */ ScrSpawnRes *scr_spawn_sync_stdio_str(ScrStr *cmd, ScrArr *args, double timeout_ms, diff --git a/packages/runtime/src/scr_cipher.c b/packages/runtime/src/scr_cipher.c index c8a10a731..00d229bad 100644 --- a/packages/runtime/src/scr_cipher.c +++ b/packages/runtime/src/scr_cipher.c @@ -20,16 +20,30 @@ #include "mbedtls/aes.h" #include "mbedtls/chacha20.h" +#include "mbedtls/des.h" #include "mbedtls/gcm.h" #include "mbedtls/poly1305.h" #include "mbedtls/platform_util.h" -typedef enum { SC_CBC, SC_CTR, SC_GCM, SC_CHACHA } ScCipherMode; +/* SC_CHACHA is chacha20-poly1305; SC_CHACHA20 the bare stream cipher. */ +typedef enum { + SC_CBC, + SC_CTR, + SC_GCM, + SC_CHACHA, + SC_ECB, + SC_CFB, + SC_CFB8, + SC_CFB1, + SC_OFB, + SC_CHACHA20, +} ScCipherMode; typedef struct { const char *name; ScCipherMode mode; unsigned key_len; + bool des; /* triple DES (two- or three-key) rather than AES */ } ScCipherSpec; /* Node's names for the implemented ciphers, including OpenSSL's aliases. @@ -41,6 +55,35 @@ static const ScCipherSpec sc_specs[] = { {"aes-128-gcm", SC_GCM, 16}, {"aes-192-gcm", SC_GCM, 24}, {"aes-256-gcm", SC_GCM, 32}, {"id-aes128-gcm", SC_GCM, 16}, {"id-aes192-gcm", SC_GCM, 24}, {"id-aes256-gcm", SC_GCM, 32}, {"chacha20-poly1305", SC_CHACHA, 32}, + {"aes-128-ecb", SC_ECB, 16}, {"aes-192-ecb", SC_ECB, 24}, {"aes-256-ecb", SC_ECB, 32}, + {"aes-128-cfb", SC_CFB, 16}, {"aes-192-cfb", SC_CFB, 24}, {"aes-256-cfb", SC_CFB, 32}, + {"aes-128-cfb8", SC_CFB8, 16}, {"aes-192-cfb8", SC_CFB8, 24}, {"aes-256-cfb8", SC_CFB8, 32}, + {"aes-128-cfb1", SC_CFB1, 16}, {"aes-192-cfb1", SC_CFB1, 24}, {"aes-256-cfb1", SC_CFB1, 32}, + {"aes-128-ofb", SC_OFB, 16}, {"aes-192-ofb", SC_OFB, 24}, {"aes-256-ofb", SC_OFB, 32}, + {"des-ede3-cbc", SC_CBC, 24, true}, {"des3", SC_CBC, 24, true}, + {"des-ede3", SC_ECB, 24, true}, {"des-ede3-ecb", SC_ECB, 24, true}, + {"des-ede3-cfb", SC_CFB, 24, true}, {"des-ede3-cfb8", SC_CFB8, 24, true}, + {"des-ede3-cfb1", SC_CFB1, 24, true}, {"des-ede3-ofb", SC_OFB, 24, true}, + {"des-ede-cbc", SC_CBC, 16, true}, {"des-ede", SC_ECB, 16, true}, + {"des-ede-ecb", SC_ECB, 16, true}, {"des-ede-cfb", SC_CFB, 16, true}, + {"des-ede-ofb", SC_OFB, 16, true}, + {"chacha20", SC_CHACHA20, 32}, +}; + +/* Ciphers OpenSSL 3 keeps in its legacy provider, which Node 24 does not + * load: crypto.getCipherInfo() knows them, and createCipheriv checks their + * IV length, then fails with OpenSSL's "unsupported" error. */ +static const struct { + const char *name; + unsigned iv_len; +} sc_legacy[] = { + {"des", 8}, {"des-cbc", 8}, {"des-ecb", 0}, {"des-cfb", 8}, {"des-cfb1", 8}, {"des-cfb8", 8}, + {"des-ofb", 8}, {"desx", 8}, {"desx-cbc", 8}, {"bf", 8}, {"bf-cbc", 8}, {"bf-ecb", 0}, + {"bf-cfb", 8}, {"bf-ofb", 8}, {"blowfish", 8}, {"cast", 8}, {"cast-cbc", 8}, {"cast5-cbc", 8}, + {"cast5-ecb", 0}, {"cast5-cfb", 8}, {"cast5-ofb", 8}, {"rc2", 8}, {"rc2-cbc", 8}, {"rc2-ecb", 0}, + {"rc2-cfb", 8}, {"rc2-ofb", 8}, {"rc2-40-cbc", 8}, {"rc2-64-cbc", 8}, {"rc4", 0}, {"rc4-40", 0}, + {"rc4-hmac-md5", 0}, {"seed", 16}, {"seed-cbc", 16}, {"seed-ecb", 0}, {"seed-cfb", 16}, + {"seed-ofb", 16}, {"idea", 8}, {"idea-cbc", 8}, {"idea-ecb", 0}, {"idea-cfb", 8}, {"idea-ofb", 8}, }; /* crypto.getCiphers() on Node 24.22 (OpenSSL 3.5). Names in this list that @@ -88,15 +131,21 @@ typedef struct ScCipher { ScCipherMode mode; bool decrypt; bool active; /* false after final() or a failed backend call */ - bool pad; /* CBC automatic padding */ + bool pad; /* CBC and ECB automatic padding */ + bool des; /* triple DES rather than AES */ + unsigned bs; /* the block size: 16 for AES, 8 for triple DES */ bool data_started; /* a non-empty update() ran, so non-empty AAD is refused */ int tag_len; /* SC_NO_TAG until known */ int tag_state; unsigned char tag[16]; - unsigned char block[16]; /* CBC: chaining value; CTR: counter block */ - unsigned char buf[16]; /* CBC: buffered input; CTR: keystream block */ - size_t bufsz; /* CBC: buffered bytes; CTR: keystream offset */ + unsigned char block[16]; /* CBC: chaining value; CTR: counter block; CFB, OFB: + shift register; chacha20: the IV */ + unsigned char buf[16]; /* CBC, ECB: buffered input; CTR: keystream block */ + size_t bufsz; /* CBC, ECB: buffered bytes; CTR, CFB, OFB: keystream + offset */ + uint64_t chacha_left; /* chacha20: bytes before the low counter word wraps */ mbedtls_aes_context aes; + mbedtls_des3_context des3; mbedtls_gcm_context gcm; mbedtls_chacha20_context chacha; mbedtls_poly1305_context poly; @@ -127,6 +176,7 @@ static void sc_trace(void *h, ScrTraceVisit visit, void *ctx) { static void sc_gcfree(void *h) { ScCipher *c = h; mbedtls_aes_free(&c->aes); + mbedtls_des3_free(&c->des3); mbedtls_gcm_free(&c->gcm); mbedtls_chacha20_free(&c->chacha); mbedtls_poly1305_free(&c->poly); @@ -184,18 +234,21 @@ static void sc_invalid_tag(unsigned long length) { /* An OpenSSL provider error as Node reports it: the formatted message plus * own opensslErrorStack, library, reason and code properties, in Node's * key order. */ -static void sc_throw_openssl(const char *packed, const char *reason, const char *code) { +/* OpenSSL's error with its library, reason and error stack; Node's + * code. */ +static void sc_throw_openssl_in(const char *packed, const char *library, const char *reason, + const char *code) { char message[128]; - snprintf(message, sizeof message, "error:%s:Provider routines::%s", packed, reason); + snprintf(message, sizeof message, "error:%s:%s::%s", packed, library, reason); ScrStr *text = scr_str_new(message, strlen(message)); ScrError *e = scr_error_new(SCR_ERR_ERROR, text); ScrDyn *stack = scr_dyn_new_arr(); scr_dyn_arr_push(stack, scr_dyn_new_str(text)); scr_str_release(text); scr_error_set_prop(e, "opensslErrorStack", 17, stack); - ScrStr *library = scr_str_new("Provider routines", 17); - scr_error_set_prop(e, "library", 7, scr_dyn_new_str(library)); - scr_str_release(library); + ScrStr *library_text = scr_str_new(library, strlen(library)); + scr_error_set_prop(e, "library", 7, scr_dyn_new_str(library_text)); + scr_str_release(library_text); ScrStr *reason_text = scr_str_new(reason, strlen(reason)); scr_error_set_prop(e, "reason", 6, scr_dyn_new_str(reason_text)); scr_str_release(reason_text); @@ -206,6 +259,10 @@ static void sc_throw_openssl(const char *packed, const char *reason, const char scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg()); } +static void sc_throw_openssl(const char *packed, const char *reason, const char *code) { + sc_throw_openssl_in(packed, "Provider routines", reason, code); +} + /* ERR_INVALID_ARG_VALUE's property flavor: "The property 'options.x' is * invalid. Received ". */ static void sc_invalid_property(const char *name, const ScrDyn *value) { @@ -440,6 +497,27 @@ static const ScCipherSpec *sc_lookup(const ScrStr *name) { return NULL; } +/* The legacy-provider IV length of `name`, or -1 for other names. */ +static int sc_legacy_iv(const ScrStr *name) { + for (size_t i = 0; i < sizeof sc_legacy / sizeof sc_legacy[0]; i++) + if (sc_ascii_eq_ci(name->data, name->len, sc_legacy[i].name)) return (int)sc_legacy[i].iv_len; + return -1; +} + +/* The IV length a mode takes; SC_GCM takes any nonzero length. */ +static size_t sc_iv_length(ScCipherMode mode, bool des) { + switch (mode) { + case SC_ECB: + return 0; + case SC_CHACHA: + return 12; + case SC_CHACHA20: + return 16; + default: + return des ? 8 : 16; + } +} + static bool sc_known_name(const ScrStr *name) { for (size_t i = 0; i < sizeof sc_known / sizeof sc_known[0]; i++) if (sc_ascii_eq_ci(name->data, name->len, sc_known[i])) return true; @@ -451,9 +529,10 @@ static void sc_unsupported_cipher(const ScrStr *name) { scr_jb_init(&b); scr_jb_puts(&b, "The cipher '"); scr_jb_put_str(&b, name); - scr_jb_puts(&b, "' has no native implementation; the supported ciphers are aes-128-cbc, aes-192-cbc, " - "aes-256-cbc, aes-128-ctr, aes-192-ctr, aes-256-ctr, aes-128-gcm, aes-192-gcm, " - "aes-256-gcm and chacha20-poly1305"); + scr_jb_puts(&b, "' has no native implementation; the supported ciphers are aes-128, aes-192 and " + "aes-256 in cbc, ctr, gcm, ecb, cfb, cfb8, cfb1 and ofb modes, des-ede3 in cbc, ecb, " + "cfb, cfb8, cfb1 and ofb modes, des-ede in cbc, ecb, cfb and ofb modes, chacha20 and " + "chacha20-poly1305"); ScrStr *message = scr_jb_finish(&b); scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "SC2020"); scr_str_release(message); @@ -522,14 +601,24 @@ static ScrDyn *sc_cipher_new(bool decrypt, ScrDyn *cipher, ScrDyn *key, ScrDyn * goto done; } const ScCipherSpec *spec = sc_lookup(cipher->v.str); + size_t iv_len = iv_bytes ? iv_bytes->len : 0; if (!spec) { - if (sc_known_name(cipher->v.str)) sc_unsupported_cipher(cipher->v.str); - else sc_throw(SCR_ERR_ERROR, "ERR_CRYPTO_UNKNOWN_CIPHER", "Unknown cipher"); + int legacy_iv = sc_legacy_iv(cipher->v.str); + if (legacy_iv >= 0) { + if (iv_len != (size_t)legacy_iv || (legacy_iv > 0 && !iv_bytes)) + sc_throw(SCR_ERR_TYPE, "ERR_CRYPTO_INVALID_IV", "Invalid initialization vector"); + else + sc_throw_openssl_in("0308010C", "digital envelope routines", "unsupported", + "ERR_OSSL_EVP_UNSUPPORTED"); + } else if (sc_known_name(cipher->v.str)) { + sc_unsupported_cipher(cipher->v.str); + } else { + sc_throw(SCR_ERR_ERROR, "ERR_CRYPTO_UNKNOWN_CIPHER", "Unknown cipher"); + } goto done; } - size_t iv_len = iv_bytes ? iv_bytes->len : 0; - bool aead = spec->mode == SC_GCM || spec->mode == SC_CHACHA; - if (iv_len == 0 || (!aead && iv_len != 16) || (spec->mode == SC_CHACHA && iv_len != 12)) { + size_t expected_iv = sc_iv_length(spec->mode, spec->des); + if (spec->mode == SC_GCM ? iv_len == 0 : iv_len != expected_iv || (expected_iv > 0 && !iv_bytes)) { sc_throw(SCR_ERR_TYPE, "ERR_CRYPTO_INVALID_IV", "Invalid initialization vector"); goto done; } @@ -570,8 +659,11 @@ static ScrDyn *sc_cipher_new(bool decrypt, ScrDyn *cipher, ScrDyn *key, ScrDyn * c->decrypt = decrypt; c->active = true; c->pad = true; + c->des = spec->des; + c->bs = spec->des ? 8 : 16; c->tag_len = tag_len; mbedtls_aes_init(&c->aes); + mbedtls_des3_init(&c->des3); mbedtls_gcm_init(&c->gcm); mbedtls_chacha20_init(&c->chacha); mbedtls_poly1305_init(&c->poly); @@ -579,9 +671,39 @@ static ScrDyn *sc_cipher_new(bool decrypt, ScrDyn *cipher, ScrDyn *key, ScrDyn * unsigned bits = spec->key_len * 8; switch (spec->mode) { case SC_CBC: - rc = decrypt ? mbedtls_aes_setkey_dec(&c->aes, key_bytes->data, bits) - : mbedtls_aes_setkey_enc(&c->aes, key_bytes->data, bits); + case SC_ECB: + case SC_CFB: + case SC_CFB8: + case SC_CFB1: + case SC_OFB: { + /* CBC and ECB decrypt with the inverse cipher; the feedback modes run + * the forward cipher both ways. */ + bool inverse = decrypt && (spec->mode == SC_CBC || spec->mode == SC_ECB); + if (spec->des && spec->key_len == 24) + rc = inverse ? mbedtls_des3_set3key_dec(&c->des3, key_bytes->data) + : mbedtls_des3_set3key_enc(&c->des3, key_bytes->data); + else if (spec->des) + rc = inverse ? mbedtls_des3_set2key_dec(&c->des3, key_bytes->data) + : mbedtls_des3_set2key_enc(&c->des3, key_bytes->data); + else + rc = inverse ? mbedtls_aes_setkey_dec(&c->aes, key_bytes->data, bits) + : mbedtls_aes_setkey_enc(&c->aes, key_bytes->data, bits); + if (expected_iv) memcpy(c->block, iv_bytes->data, expected_iv); + break; + } + case SC_CHACHA20: + /* OpenSSL's IV is a 64-bit little-endian block counter and an 8-byte + * nonce; mbedTLS takes a 32-bit counter and a 12-byte nonce, so the + * stream restarts each time the low counter word wraps. */ memcpy(c->block, iv_bytes->data, 16); + rc = mbedtls_chacha20_setkey(&c->chacha, key_bytes->data); + if (rc == 0) { + uint32_t low = (uint32_t)c->block[0] | (uint32_t)c->block[1] << 8 | + (uint32_t)c->block[2] << 16 | (uint32_t)c->block[3] << 24; + c->chacha_left = ((uint64_t)1 << 32) - low; + c->chacha_left *= 64; + rc = mbedtls_chacha20_starts(&c->chacha, c->block + 4, low); + } break; case SC_CTR: rc = mbedtls_aes_setkey_enc(&c->aes, key_bytes->data, bits); @@ -635,39 +757,123 @@ done: /* ── the cipher transforms ────────────────────────────────────────────── */ -/* Raw CBC over whole blocks. */ +/* Raw CBC or ECB over whole blocks. */ static bool sc_cbc_blocks(ScCipher *c, const unsigned char *in, size_t len, unsigned char *out) { + if (c->mode == SC_ECB) { + for (size_t off = 0; off < len; off += c->bs) { + int rc = c->des ? mbedtls_des3_crypt_ecb(&c->des3, in + off, out + off) + : mbedtls_aes_crypt_ecb(&c->aes, c->decrypt ? MBEDTLS_AES_DECRYPT : MBEDTLS_AES_ENCRYPT, + in + off, out + off); + if (rc != 0) return false; + } + return true; + } + if (c->des) + return mbedtls_des3_crypt_cbc(&c->des3, c->decrypt ? MBEDTLS_DES_DECRYPT : MBEDTLS_DES_ENCRYPT, len, c->block, + in, out) == 0; return mbedtls_aes_crypt_cbc(&c->aes, c->decrypt ? MBEDTLS_AES_DECRYPT : MBEDTLS_AES_ENCRYPT, len, c->block, in, out) == 0; } -/* OpenSSL's ossl_cipher_generic_block_update for CBC: complete blocks - * are processed, the remainder is buffered, and a decryption that ends on - * a block boundary holds the last block back for padding removal. Returns - * the output length, or -1 when the backend fails; out has room for - * len + 16 bytes. */ +/* One forward block of the feedback modes' cipher. */ +static bool sc_forward(ScCipher *c, const unsigned char *in, unsigned char *out) { + return (c->des ? mbedtls_des3_crypt_ecb(&c->des3, in, out) + : mbedtls_aes_crypt_ecb(&c->aes, MBEDTLS_AES_ENCRYPT, in, out)) == 0; +} + +/* CFB (full-block feedback), CFB8, CFB1 and OFB, as OpenSSL's + * CRYPTO_cfb128_encrypt, CRYPTO_cfb128_8_encrypt, CRYPTO_cfb128_1_encrypt + * and CRYPTO_ofb128_encrypt: CFB and OFB keep their keystream offset across + * updates; CFB1 feeds back one bit at a time, most significant first. */ +static bool sc_feedback(ScCipher *c, const unsigned char *in, size_t len, unsigned char *out) { + unsigned char stream[16]; + size_t bs = c->bs; + for (size_t i = 0; i < len; i++) { + switch (c->mode) { + case SC_CFB: + case SC_OFB: + if (c->bufsz == 0 && !sc_forward(c, c->block, c->block)) return false; + out[i] = (unsigned char)(in[i] ^ c->block[c->bufsz]); + if (c->mode == SC_CFB) c->block[c->bufsz] = c->decrypt ? in[i] : out[i]; + c->bufsz = (c->bufsz + 1) % bs; + break; + case SC_CFB8: { + if (!sc_forward(c, c->block, stream)) return false; + unsigned char byte = (unsigned char)(in[i] ^ stream[0]); + memmove(c->block, c->block + 1, bs - 1); + c->block[bs - 1] = c->decrypt ? in[i] : byte; + out[i] = byte; + break; + } + default: { + unsigned char result = 0; + for (int bit = 7; bit >= 0; bit--) { + if (!sc_forward(c, c->block, stream)) return false; + unsigned in_bit = (in[i] >> bit) & 1u; + unsigned out_bit = in_bit ^ (stream[0] >> 7); + unsigned feed = c->decrypt ? in_bit : out_bit; + for (size_t k = 0; k + 1 < bs; k++) + c->block[k] = (unsigned char)(c->block[k] << 1 | c->block[k + 1] >> 7); + c->block[bs - 1] = (unsigned char)(c->block[bs - 1] << 1 | feed); + result |= (unsigned char)(out_bit << bit); + } + out[i] = result; + break; + } + } + } + mbedtls_platform_zeroize(stream, sizeof stream); + return true; +} + +/* chacha20 over len bytes: when the 64-bit counter's low word wraps, the + * high word steps (and wraps itself, as OpenSSL's does) and the stream + * restarts from block 0 of the next word. */ +static bool sc_chacha20_update(ScCipher *c, const unsigned char *in, size_t len, unsigned char *out) { + while (len > 0) { + size_t n = len < c->chacha_left ? len : (size_t)c->chacha_left; + if (mbedtls_chacha20_update(&c->chacha, n, in, out) != 0) return false; + in += n; + out += n; + len -= n; + c->chacha_left -= n; + if (c->chacha_left == 0) { + for (int k = 4; k < 8 && ++c->block[k] == 0; k++) { + } + memset(c->block, 0, 4); + c->chacha_left = (uint64_t)1 << 38; + if (mbedtls_chacha20_starts(&c->chacha, c->block + 4, 0) != 0) return false; + } + } + return true; +} + +/* OpenSSL's ossl_cipher_generic_block_update for CBC and ECB: complete + * blocks are processed, the remainder is buffered, and a decryption that + * ends on a block boundary holds the last block back for padding removal. + * Returns the output length, or -1 when the backend fails; out has room + * for len + 16 bytes. */ static long sc_cbc_update(ScCipher *c, const unsigned char *in, size_t inl, unsigned char *out) { + size_t bs = c->bs; size_t outl = 0; size_t nextblocks; if (c->bufsz != 0) { - size_t remain = 16 - c->bufsz; + size_t remain = bs - c->bufsz; if (inl < remain) remain = inl; memcpy(c->buf + c->bufsz, in, remain); in += remain; inl -= remain; c->bufsz += remain; - nextblocks = inl & ~(size_t)15; - } else { - nextblocks = inl & ~(size_t)15; } - if (c->bufsz == 16 && (!c->decrypt || inl > 0 || !c->pad)) { - if (!sc_cbc_blocks(c, c->buf, 16, out)) return -1; + nextblocks = inl - inl % bs; + if (c->bufsz == bs && (!c->decrypt || inl > 0 || !c->pad)) { + if (!sc_cbc_blocks(c, c->buf, bs, out)) return -1; c->bufsz = 0; - outl = 16; - out += 16; + outl = bs; + out += bs; } if (nextblocks > 0) { - if (c->decrypt && c->pad && nextblocks == inl) nextblocks -= 16; + if (c->decrypt && c->pad && nextblocks == inl) nextblocks -= bs; if (nextblocks > 0) { if (!sc_cbc_blocks(c, in, nextblocks, out)) return -1; outl += nextblocks; @@ -682,33 +888,35 @@ static long sc_cbc_update(ScCipher *c, const unsigned char *in, size_t inl, unsi return (long)outl; } -/* ossl_cipher_generic_block_final for CBC. Returns the output length, -1 - * after throwing OpenSSL's error, or -2 when the backend fails. */ +/* ossl_cipher_generic_block_final for CBC and ECB. Returns the output + * length, -1 after throwing OpenSSL's error, or -2 when the backend + * fails. */ static long sc_cbc_final(ScCipher *c, unsigned char out[16]) { + size_t bs = c->bs; if (!c->decrypt) { if (c->pad) { - unsigned char fill = (unsigned char)(16 - c->bufsz); - for (size_t i = c->bufsz; i < 16; i++) c->buf[i] = fill; + unsigned char fill = (unsigned char)(bs - c->bufsz); + for (size_t i = c->bufsz; i < bs; i++) c->buf[i] = fill; } else if (c->bufsz == 0) { return 0; - } else if (c->bufsz != 16) { + } else if (c->bufsz != bs) { sc_throw_openssl("1C80006B", "wrong final block length", "ERR_OSSL_WRONG_FINAL_BLOCK_LENGTH"); return -1; } - if (!sc_cbc_blocks(c, c->buf, 16, out)) return -2; + if (!sc_cbc_blocks(c, c->buf, bs, out)) return -2; c->bufsz = 0; - return 16; + return (long)bs; } - if (c->bufsz != 16) { + if (c->bufsz != bs) { if (c->bufsz == 0 && !c->pad) return 0; sc_throw_openssl("1C80006B", "wrong final block length", "ERR_OSSL_WRONG_FINAL_BLOCK_LENGTH"); return -1; } - if (!sc_cbc_blocks(c, c->buf, 16, c->buf)) return -2; - size_t len = 16; + if (!sc_cbc_blocks(c, c->buf, bs, c->buf)) return -2; + size_t len = bs; if (c->pad) { - unsigned char fill = c->buf[15]; - bool bad = fill == 0 || fill > 16; + unsigned char fill = c->buf[bs - 1]; + bool bad = fill == 0 || fill > bs; for (size_t i = 0; !bad && i < fill; i++) bad = c->buf[--len] != fill; if (bad) { sc_throw_openssl("1C800064", "bad decrypt", "ERR_OSSL_BAD_DECRYPT"); @@ -769,7 +977,8 @@ static ScrBytes *sc_transform(ScCipher *c, const unsigned char *in, size_t len) size_t produced = 0; bool ok = true; switch (c->mode) { - case SC_CBC: { + case SC_CBC: + case SC_ECB: { long n = sc_cbc_update(c, in, len, out->data); ok = n >= 0; produced = ok ? (size_t)n : 0; @@ -786,6 +995,17 @@ static ScrBytes *sc_transform(ScCipher *c, const unsigned char *in, size_t len) ok = len == 0 || sc_chacha_update(c, in, len, out->data); produced = len; break; + case SC_CFB: + case SC_CFB8: + case SC_CFB1: + case SC_OFB: + ok = sc_feedback(c, in, len, out->data); + produced = len; + break; + case SC_CHACHA20: + ok = len == 0 || sc_chacha20_update(c, in, len, out->data); + produced = len; + break; } if (!ok) { mbedtls_platform_zeroize(out->data, room); @@ -926,7 +1146,8 @@ static ScrBytes *sc_final_bytes(ScCipher *c) { out->len = 0; bool ok = true, backend = true; switch (c->mode) { - case SC_CBC: { + case SC_CBC: + case SC_ECB: { long produced = sc_cbc_final(c, out->data); if (produced == -1) { scr_bytes_release(out); @@ -937,6 +1158,11 @@ static ScrBytes *sc_final_bytes(ScCipher *c) { break; } case SC_CTR: + case SC_CFB: + case SC_CFB8: + case SC_CFB1: + case SC_OFB: + case SC_CHACHA20: break; case SC_GCM: { unsigned char full[16]; diff --git a/packages/runtime/src/scr_console.c b/packages/runtime/src/scr_console.c index a3ac43cc0..6def8d866 100644 --- a/packages/runtime/src/scr_console.c +++ b/packages/runtime/src/scr_console.c @@ -148,7 +148,7 @@ static void scr_enable_utf8_console_output(void) { } #endif -void scr_runtime_abi_v27(void) {} +void scr_runtime_abi_v36(void) {} void scr_init(void) { #ifdef SCR_WORKERS diff --git a/packages/runtime/src/scr_copying.c b/packages/runtime/src/scr_copying.c index df4da1738..6cdc0683c 100644 --- a/packages/runtime/src/scr_copying.c +++ b/packages/runtime/src/scr_copying.c @@ -22,13 +22,22 @@ static bool copying_elem_is_ref(ScrElemKind kind) { } static ScrArr *copying_arr_new_like(const ScrArr *a, size_t cap) { - ScrArr *out = a->elem == SCR_ELEM_REF + ScrArr *out = a->elem == SCR_ELEM_REF || a->elem == SCR_ELEM_CREF ? scr_arr_new_ops(scr_arr_ops(a), cap ? cap : 1) : scr_arr_new(a->elem, cap ? cap : 1); - out->len = cap; return out; } +/* Copy [from, from + count) of `src` to the end of `out` and raise its + * length over the copy. */ +static void copying_arr_append_range(ScrArr *out, const ScrArr *src, size_t from, size_t count, + bool reverse) { + size_t at = out->len; + scr_arr_copy_range_ex(out, at, src, from, count, reverse, true); + out->len = (uint32_t)(at + count); + scr_arr_refill(out); +} + static uint64_t copying_arr_retain_slot(const ScrArr *a, uint64_t slot) { if (!copying_elem_is_ref(a->elem)) return slot; void *p = copying_slot_to_ptr(slot); @@ -41,7 +50,7 @@ static uint64_t copying_arr_retain_slot(const ScrArr *a, uint64_t slot) { ScrArr *scr_arr_to_reversed(const ScrArr *a) { ScrArr *out = copying_arr_new_like(a, a->len); - scr_arr_copy_range_ex(out, 0, a, 0, a->len, true, true); + copying_arr_append_range(out, a, 0, a->len, true); return out; } @@ -60,10 +69,9 @@ ScrArr *scr_arr_to_spliced(const ScrArr *a, double start, } size_t out_len = a->len - ndelete + items->len; ScrArr *out = copying_arr_new_like(a, out_len); - scr_arr_copy_range_ex(out, 0, a, 0, from, false, true); - scr_arr_copy_range_ex(out, from, items, 0, items->len, false, true); - scr_arr_copy_range_ex(out, from + items->len, a, from + ndelete, - a->len - from - ndelete, false, true); + copying_arr_append_range(out, a, 0, from, false); + copying_arr_append_range(out, items, 0, items->len, false); + copying_arr_append_range(out, a, from + ndelete, a->len - from - ndelete, false); return out; } @@ -88,7 +96,7 @@ ScrArr *scr_arr_with_f64(ScrArr *a, double index, double value) { size_t i; if (!copying_arr_with_index(a, index, &i)) return NULL; ScrArr *out = copying_arr_new_like(a, a->len); - scr_arr_copy_range_ex(out, 0, a, 0, a->len, false, true); + copying_arr_append_range(out, a, 0, a->len, false); scr_arr_set_f64(out, (double)i, value); return out; } @@ -97,7 +105,7 @@ ScrArr *scr_arr_with_bool(ScrArr *a, double index, bool value) { size_t i; if (!copying_arr_with_index(a, index, &i)) return NULL; ScrArr *out = copying_arr_new_like(a, a->len); - scr_arr_copy_range_ex(out, 0, a, 0, a->len, false, true); + copying_arr_append_range(out, a, 0, a->len, false); scr_arr_set_bool(out, (double)i, value); return out; } @@ -106,7 +114,7 @@ ScrArr *scr_arr_with_ref(ScrArr *a, double index, void *value) { size_t i; if (!copying_arr_with_index(a, index, &i)) return NULL; ScrArr *out = copying_arr_new_like(a, a->len); - scr_arr_copy_range_ex(out, 0, a, 0, a->len, false, true); + copying_arr_append_range(out, a, 0, a->len, false); uint64_t retained = copying_arr_retain_slot(a, copying_slot_from_ptr(value)); scr_arr_set_ref(out, (double)i, copying_slot_to_ptr(retained)); @@ -117,7 +125,7 @@ ScrArr *scr_arr_with_undefined(ScrArr *a, double index) { size_t i; if (!copying_arr_with_index(a, index, &i)) return NULL; ScrArr *out = copying_arr_new_like(a, a->len); - scr_arr_copy_range_ex(out, 0, a, 0, a->len, false, true); + copying_arr_append_range(out, a, 0, a->len, false); scr_arr_set_undefined(out, (double)i); return out; } diff --git a/packages/runtime/src/scr_core.h b/packages/runtime/src/scr_core.h index fea020d16..f9cc8cc5f 100644 --- a/packages/runtime/src/scr_core.h +++ b/packages/runtime/src/scr_core.h @@ -81,7 +81,7 @@ void scr_init(void); /* Program objects emitted by the bundled LLVM helper reference this symbol. * Its versioned spelling makes a mismatched manual runtime link fail before * the program can start. */ -void scr_runtime_abi_v27(void); +void scr_runtime_abi_v36(void); void scr_runtime_workers_v8(void); /* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ────── diff --git a/packages/runtime/src/scr_crypto.h b/packages/runtime/src/scr_crypto.h index 546e3d75b..0e6c6e07f 100644 --- a/packages/runtime/src/scr_crypto.h +++ b/packages/runtime/src/scr_crypto.h @@ -108,6 +108,14 @@ ScrDyn *scr_crypto_ecdh_convert_key(ScrDyn *key, ScrDyn *curve, ScrDyn *in_enc, * Node's error pending. */ ScrDyn *scr_crypto_dh_new(ScrDyn *size_or_key, ScrDyn *key_encoding, ScrDyn *generator, ScrDyn *generator_encoding); ScrDyn *scr_crypto_dh_group_new(ScrDyn *name); +/* crypto.checkPrimeSync, checkPrime, generatePrimeSync and generatePrime + * (scr_dh.c): checked-dynamic arguments, borrowed; the sync forms answer + * +1 or NULL with the error pending. The callbacks receive undefined as + * the error, as Node passes it, or null when `null_error`. */ +ScrDyn *scr_crypto_check_prime_sync(ScrDyn *candidate, ScrDyn *options); +void scr_crypto_check_prime_async(ScrDyn *candidate, ScrDyn *options, ScrDyn *callback, bool null_error); +ScrDyn *scr_crypto_generate_prime_sync(ScrDyn *size, ScrDyn *options); +void scr_crypto_generate_prime_async(ScrDyn *size, ScrDyn *options, ScrDyn *callback, bool null_error); ScrBytes *scr_crypto_pbkdf2(ScrBytes *password, ScrBytes *salt, double iterations, double keylen, ScrStr *digest); ScrBytes *scr_crypto_pbkdf2_job(ScrBytes *password, ScrBytes *salt, double iterations, diff --git a/packages/runtime/src/scr_cycle.c b/packages/runtime/src/scr_cycle.c index 5e5029e14..6dcd546b0 100644 --- a/packages/runtime/src/scr_cycle.c +++ b/packages/runtime/src/scr_cycle.c @@ -529,7 +529,8 @@ static SCR_TL size_t scr_cyc_live_after[SCR_CYC_NGENS]; * large program, say) those walks dominate collection time. The cap trades * that walking for memory only when passes find nothing: while they find * garbage, the yield keeps the fraction narrow. SCR_CYCLE_GROWTH_CAP lowers - * the cap for memory-sensitive programs: a power of two from 1 to 16, with + * the cap for memory-sensitive programs, as scr_cyc_set_growth_cap does for + * a program that sets its own: a power of two from 1 to 16, with * other positive values rounded down to a power of two and larger ones read * as 16. At 1 the heap may at most double past its size after the last pass * at a level before the next. */ @@ -539,20 +540,34 @@ static SCR_TL unsigned char scr_cyc_growth_shift[SCR_CYC_NGENS]; static SCR_TL unsigned char scr_cyc_growth_streak[SCR_CYC_NGENS]; #endif +/* The growth cap a program sets for itself (scr_cyc_set_growth_cap), 0 for + * the default; SCR_CYCLE_GROWTH_CAP takes precedence over it. */ +static long scr_cyc_growth_cap_set = 0; +static SCR_TL unsigned char scr_cyc_growth_max_shift_cached = 0; + /* Shift of the configured growth cap: 2 + log2(cap), since the base * fraction is 1/4. */ static unsigned scr_cyc_growth_max_shift(void) { - static SCR_TL unsigned char cached = 0; - if (cached == 0) { + if (scr_cyc_growth_max_shift_cached == 0) { const char *env = scr_getenv("SCR_CYCLE_GROWTH_CAP"); long v = env ? strtol(env, NULL, 10) : 0; + if (v <= 0) v = scr_cyc_growth_cap_set; unsigned shift = SCR_CYC_GROWTH_MAX_SHIFT; if (v > 0) for (shift = 2; shift < SCR_CYC_GROWTH_MAX_SHIFT && (2L << (shift - 2)) <= v;) shift++; - cached = (unsigned char)shift; + scr_cyc_growth_max_shift_cached = (unsigned char)shift; } - return cached; + return scr_cyc_growth_max_shift_cached; +} + +void scr_cyc_set_growth_cap(long cap) { + scr_cyc_growth_cap_set = cap; + scr_cyc_growth_max_shift_cached = 0; + /* Fractions already widened past the new cap start at it. */ + unsigned max = scr_cyc_growth_max_shift(); + for (unsigned gen = 0; gen < SCR_CYC_NGENS; gen++) + if (scr_cyc_growth_shift[gen] > max) scr_cyc_growth_shift[gen] = (unsigned char)max; } /* Doublings of each generation's backlog threshold earned by consecutive @@ -1244,9 +1259,9 @@ void scr_cyc_on_release(void *obj) { #define SCR_CYC_ARRAY_WALK(name, visit) \ static __attribute__((noinline)) void name(const ScrArr *arr, void *ctx) { \ size_t end = arr->len < arr->cap ? arr->len : arr->cap; \ - size_t filled = arr->filled < end ? arr->filled : end; \ + const uint8_t *states = scr_arr_scan_states(arr); \ + size_t filled = !states || arr->filled > end ? end : arr->filled; \ for (size_t i = 0; i < filled; i++) visit((void *)(uintptr_t)arr->data[i], ctx); \ - const uint8_t *states = scr_arr_states_end(arr); \ for (size_t i = filled; i < end; i++) \ if (*scr_arr_state_in(states, i) == SCR_ARR_VALUE) \ visit((void *)(uintptr_t)arr->data[i], ctx); \ diff --git a/packages/runtime/src/scr_decl_adopt.c b/packages/runtime/src/scr_decl_adopt.c new file mode 100644 index 000000000..7372826ab --- /dev/null +++ b/packages/runtime/src/scr_decl_adopt.c @@ -0,0 +1,174 @@ +/* The declaration-typed boundary's adoption, linked only by programs with + * boundary casts (runtime feature declAdopt). + * + * A boundary cast (decl-projection.ts in the compiler) checks a value + * package code produced against the type the package's declarations give + * it and builds the native value. Copying would be observable whenever + * something else still references a source object or array, so this pass + * walks the source next to a live capsule of the built value and turns + * every source node with another holder (rc above one) into a live + * capsule of the native value built from it: every holder then reads and + * writes the same native value. Unshared nodes are left to be released + * with their parent, which no program can tell apart from a move. + * + * Phase one checks that every converted node is plain data (the native + * value has no prototype, accessors, symbol keys or frozen state) and + * collects the shared ones; it throws the path TypeError before anything + * changes. Phase two converts the shared nodes in place + * (scr_dyn_decl_become, scr_json.c). */ +#include "scr_runtime.h" + +#include +#include + +void scr_dyn_decl_become(ScrDyn *n, const ScrDyn *capsule); + +typedef struct { + ScrDyn *node; + ScrDyn *capsule; /* owned */ +} ScrDeclAdopt; + +typedef struct { + ScrDeclAdopt *items; + size_t len, cap; +} ScrDeclAdoptList; + +static void scr_decl_adopt_push(ScrDeclAdoptList *list, ScrDyn *node, ScrDyn *capsule) { + if (list->len == list->cap) { + list->cap = list->cap ? list->cap * 2 : 8; + list->items = realloc(list->items, list->cap * sizeof *list->items); + if (!list->items) scr_trap("scriptc: out of memory\n"); + } + list->items[list->len].node = node; + list->items[list->len].capsule = capsule; + list->len++; +} + +static bool scr_decl_adopt_plain(const ScrDyn *n, const ScrDynPath *path) { + const char *want = NULL; + if (n->kind == SCR_DYN_OBJ) { + if (n->prototype || n->null_proto || n->v.obj.class_name) + want = "a plain object (this value has a prototype)"; + else if (n->symbol_properties || n->symbol_keys) + want = "a plain object (this value has symbol-keyed properties)"; + else if (n->non_extensible) + want = "a plain object (this value is frozen, sealed or not extensible)"; + else if (n->v.obj.source_access && !n->v.obj.source_identity) + want = "a plain object"; + else { + for (size_t i = 0; i < n->v.obj.len && !want; i++) { + const ScrDynEntry *e = &n->v.obj.entries[i]; + if (e->accessor) want = "a plain object (this value has accessor properties)"; + else if (!e->writable || !e->enumerable || !e->configurable) + want = "a plain object (this value has non-default property attributes)"; + } + } + } else if (n->kind == SCR_DYN_ARR) { + if (n->prototype) + want = "a plain array (this value has a prototype)"; + else if (n->v.arr.properties || n->symbol_properties || n->symbol_keys) + want = "a plain array (this value has extra properties)"; + else if (n->v.arr.sealed || n->v.arr.frozen || n->non_extensible) + want = "a plain array (this value is frozen, sealed or not extensible)"; + } + if (!want) return true; + scr_dyn_check_fail(path, want, n); + return false; +} + +static ScrDyn *scr_decl_capsule_ptr_holder(const ScrDeclAdoptList *list, const ScrDyn *node) { + for (size_t i = 0; i < list->len; i++) + if (list->items[i].node == node) return list->items[i].capsule; + return NULL; +} + +/* `capsule` is owned by the call: pushed or released. */ +static bool scr_decl_adopt_walk(ScrDeclAdoptList *list, ScrDyn *n, ScrDyn *capsule, + const ScrDynPath *path, unsigned depth) { + if (!n || (n->kind != SCR_DYN_OBJ && n->kind != SCR_DYN_ARR) || !capsule || + capsule->kind != SCR_DYN_TYPED_REF || capsule == n || depth > 4096) { + scr_dyn_release(capsule); + return true; + } + if (!scr_decl_adopt_plain(n, path)) { + scr_dyn_release(capsule); + return false; + } + if (n->rc > 1 && n->rc != SIZE_MAX) { + ScrDyn *seen = scr_decl_capsule_ptr_holder(list, n); + if (seen) { + bool same = seen->v.typed_ref.ptr == capsule->v.typed_ref.ptr; + scr_dyn_release(capsule); + if (!same) + scr_dyn_check_fail(path, "a value that is not also stored elsewhere in this value (scriptc builds a separate native copy for each place)", n); + return same; + } + scr_decl_adopt_push(list, n, capsule); + } + void *ptr = capsule->v.typed_ref.ptr; + if (capsule->v.typed_ref.record && capsule->v.typed_ref.array && n->kind == SCR_DYN_OBJ) { + const ScrDynTypedRecordOps *ops = (const ScrDynTypedRecordOps *)(const void *)capsule->v.typed_ref.array; + for (size_t i = 0; i < ops->count; i++) { + if (!ops->has(ptr, i)) continue; + const ScrStr *name = ops->names[i]; + ScrDyn *child = scr_dyn_obj_get(n, name->data, name->len); + if (!child || (child->kind != SCR_DYN_OBJ && child->kind != SCR_DYN_ARR)) continue; + ScrDynPath p = { path, name->data, 0 }; + if (!scr_decl_adopt_walk(list, child, ops->get(ptr, i), &p, depth + 1)) { + if (!(n->rc > 1 && n->rc != SIZE_MAX)) scr_dyn_release(capsule); + return false; + } + } + } else if (!capsule->v.typed_ref.record && !capsule->v.typed_ref.klass && !capsule->v.typed_ref.array && + n->kind == SCR_DYN_ARR && !n->v.arr.presence) { + /* A capsule without element hooks over an array: a tuple, whose index + * field names get no record hooks. Its view holds a live capsule of + * each record or array element, which the source element adopts (the + * walk skips the other elements). */ + ScrDyn *view = capsule->v.typed_ref.materialize(ptr); + if (scr_exc_pending()) { + scr_dyn_release(view); + if (!(n->rc > 1 && n->rc != SIZE_MAX)) scr_dyn_release(capsule); + return false; + } + if (view && view->kind == SCR_DYN_ARR) { + size_t len = n->v.arr.len < view->v.arr.len ? n->v.arr.len : view->v.arr.len; + for (size_t i = 0; i < len; i++) { + ScrDyn *child = n->v.arr.items[i], *element = view->v.arr.items[i]; + if (!child || !element || (child->kind != SCR_DYN_OBJ && child->kind != SCR_DYN_ARR)) continue; + ScrDynPath p = { path, NULL, i }; + if (!scr_decl_adopt_walk(list, child, scr_dyn_retain(element), &p, depth + 1)) { + scr_dyn_release(view); + if (!(n->rc > 1 && n->rc != SIZE_MAX)) scr_dyn_release(capsule); + return false; + } + } + } + scr_dyn_release(view); + } else if (!capsule->v.typed_ref.record && !capsule->v.typed_ref.klass && + capsule->v.typed_ref.array && n->kind == SCR_DYN_ARR && !n->v.arr.presence) { + const ScrDynTypedArrayOps *ops = capsule->v.typed_ref.array; + for (size_t i = 0; i < n->v.arr.len; i++) { + ScrDyn *child = n->v.arr.items[i]; + if (!child || (child->kind != SCR_DYN_OBJ && child->kind != SCR_DYN_ARR)) continue; + ScrDynPath p = { path, NULL, i }; + if (!scr_decl_adopt_walk(list, child, ops->get(ptr, (double)i), &p, depth + 1)) { + if (!(n->rc > 1 && n->rc != SIZE_MAX)) scr_dyn_release(capsule); + return false; + } + } + } + if (!(n->rc > 1 && n->rc != SIZE_MAX)) scr_dyn_release(capsule); + return true; +} + +void scr_dyn_decl_adopt(ScrDyn *source, ScrDyn *capsule) { + ScrDeclAdoptList list = { NULL, 0, 0 }; + bool ok = scr_decl_adopt_walk(&list, source, scr_dyn_retain(capsule), NULL, 0); + for (size_t i = 0; i < list.len; i++) { + ScrDeclAdopt *item = &list.items[i]; + if (ok && item->node->kind != SCR_DYN_TYPED_REF) scr_dyn_decl_become(item->node, item->capsule); + scr_dyn_release(item->capsule); + } + free(list.items); +} diff --git a/packages/runtime/src/scr_dgram.c b/packages/runtime/src/scr_dgram.c index bb14d063e..8d1c7b058 100644 --- a/packages/runtime/src/scr_dgram.c +++ b/packages/runtime/src/scr_dgram.c @@ -2158,3 +2158,73 @@ void scr_dgram_send_chk(ScrDgramSocket *s, const ScrDyn *buffer, const ScrDyn *a * it removes a 'message' listener. Defined last so the functions above * keep their emission order. */ void scr_dgram_rearm(ScrDgramSocket *s) { scr_dgram_update_read(s); } + +/* ── Unix datagram sockets for the hot-shots package shim ────────────── + * The unix-dgram addon's surface, which hot-shots' uds transport uses: a + * non-blocking AF_UNIX datagram socket connected to a path. Each call + * answers its result (fd, 0) or a negative errno, as the addon's binding + * does; send answers 1 when the socket would block (the addon's + * "congestion"). Defined after the functions above so their emission + * order is unchanged. */ +#ifndef _WIN32 +#include +#endif + +double scr_dgram_unix_socket(void) { +#ifdef _WIN32 + return -(double)ENOSYS; +#else + int fd = socket(AF_UNIX, SOCK_DGRAM, 0); + if (fd < 0) return -(double)errno; + int flags = fcntl(fd, F_GETFL, 0); + if (flags >= 0) (void)fcntl(fd, F_SETFL, flags | O_NONBLOCK); + (void)fcntl(fd, F_SETFD, FD_CLOEXEC); + return (double)fd; +#endif +} + +double scr_dgram_unix_connect(double fd, ScrStr *path) { +#ifdef _WIN32 + (void)fd; + (void)path; + return -(double)ENOSYS; +#else + struct sockaddr_un addr; + memset(&addr, 0, sizeof addr); + addr.sun_family = AF_UNIX; + char *owned; + const char *bytes = scr_str_utf8(path, &owned); + size_t n = strlen(bytes); + if (n > sizeof addr.sun_path - 1) n = sizeof addr.sun_path - 1; + memcpy(addr.sun_path, bytes, n); + scr_str_utf8_end(owned); + if (connect((int)fd, (struct sockaddr *)&addr, sizeof addr) != 0) return -(double)errno; + return 0; +#endif +} + +double scr_dgram_unix_send(double fd, ScrBytes *data) { +#ifdef _WIN32 + (void)fd; + (void)data; + return -(double)ENOSYS; +#else + SCR_BYTES_SNAPSHOT(data); + ssize_t sent; + do { + sent = send((int)fd, data->data, data->len, 0); + } while (sent < 0 && errno == EINTR); + if (sent >= 0) return 0; + if (errno == EAGAIN || errno == EWOULDBLOCK) return 1; + return -(double)errno; +#endif +} + +double scr_dgram_unix_close(double fd) { +#ifdef _WIN32 + (void)fd; + return -(double)ENOSYS; +#else + return close((int)fd) != 0 ? -(double)errno : 0; +#endif +} diff --git a/packages/runtime/src/scr_dgram.h b/packages/runtime/src/scr_dgram.h index 8396f0693..08db077df 100644 --- a/packages/runtime/src/scr_dgram.h +++ b/packages/runtime/src/scr_dgram.h @@ -55,6 +55,13 @@ double scr_dgram_addr_port(ScrDgramSocket *s); void scr_dgram_close(ScrDgramSocket *s, ScrClosure *cb /*moves, nullable*/); /* throws on closed */ void scr_dgram_unref(ScrDgramSocket *s); void scr_dgram_ref(ScrDgramSocket *s); +/* Unix datagram sockets for the hot-shots package shim (the unix-dgram + * addon's surface): each answers its result (fd, 0) or a negative errno; + * send answers 1 when the socket would block. */ +double scr_dgram_unix_socket(void); +double scr_dgram_unix_connect(double fd, ScrStr *path /*borrowed*/); +double scr_dgram_unix_send(double fd, ScrBytes *data /*borrowed*/); +double scr_dgram_unix_close(double fd); /* Socket options: Node's checks and errors (ErrnoException " * ", ERR_SOCKET_BAD_BUFFER_SIZE, ERR_SOCKET_BUFFER_SIZE, the * TypeError after close()). The TTL setters and setMulticastLoopback diff --git a/packages/runtime/src/scr_dh.c b/packages/runtime/src/scr_dh.c index cf44338f5..dfa6fd2f3 100644 --- a/packages/runtime/src/scr_dh.c +++ b/packages/runtime/src/scr_dh.c @@ -183,17 +183,17 @@ static void sd_value_too_long(void) { scr_throw_error_msg_code(SCR_ERR_ERROR, message, n < 0 ? 0 : (size_t)n, "SC2020"); } -/* validateInt32(value, name). */ -static bool sd_validate_int32(const char *name, double value) { +/* validateInt32(value, name, min). */ +static bool sd_validate_int32_min(const char *name, double value, int32_t min) { bool integer = isfinite(value) && trunc(value) == value; - if (integer && value >= INT32_MIN && value <= INT32_MAX) return true; + if (integer && value >= min && value <= INT32_MAX) return true; char received[48]; scr_num_received(value, received); char text[192]; int n = integer ? snprintf(text, sizeof text, - "The value of \"%s\" is out of range. It must be >= -2147483648 && <= 2147483647. " + "The value of \"%s\" is out of range. It must be >= %ld && <= 2147483647. " "Received %s", - name, received) + name, (long)min, received) : snprintf(text, sizeof text, "The value of \"%s\" is out of range. It must be an integer. Received %s", name, received); @@ -201,6 +201,11 @@ static bool sd_validate_int32(const char *name, double value) { return false; } +/* validateInt32(value, name). */ +static bool sd_validate_int32(const char *name, double value) { + return sd_validate_int32_min(name, value, INT32_MIN); +} + /* ── arithmetic ───────────────────────────────────────────────────────── */ /* The exponent digit width that keeps every mbedtls_mpi_exp_mod exponent @@ -630,9 +635,9 @@ static int sd_mr_rounds(size_t bits) { return bits > 2048 ? 128 : 64; } -/* BN_check_prime: trial division by the small primes, then Miller-Rabin - * rounds with random bases. */ -static bool sd_is_prime(ScDhSmallPrimes *t, const mbedtls_mpi *x) { +/* BN_is_prime_ex(x, checks): trial division by the small primes, then + * Miller-Rabin rounds with random bases, at least OpenSSL's round count. */ +static bool sd_is_prime_checks(ScDhSmallPrimes *t, const mbedtls_mpi *x, int checks) { if (mbedtls_mpi_cmp_int(x, 2) < 0) return false; if (mbedtls_mpi_get_bit(x, 0) == 0) return mbedtls_mpi_cmp_int(x, 2) == 0; if (!sd_small_residues(t, x)) return false; @@ -643,11 +648,17 @@ static bool sd_is_prime(ScDhSmallPrimes *t, const mbedtls_mpi *x) { ScDhMillerRabin m; bool prime = sd_mr_init(&m, x); int rounds = sd_mr_rounds(mbedtls_mpi_bitlen(x)); + if (checks > rounds) rounds = checks; for (int i = 0; i < rounds && prime; i++) prime = sd_mr_round(&m, x, false) == 1; sd_mr_free(&m); return prime; } +/* BN_check_prime. */ +static bool sd_is_prime(ScDhSmallPrimes *t, const mbedtls_mpi *x) { + return sd_is_prime_checks(t, x, 0); +} + /* DH_check for parameters without q, as ncrypto's CheckDhParams reports * it. */ static int sd_check_params(const mbedtls_mpi *p, const mbedtls_mpi *g) { @@ -793,8 +804,9 @@ static void sd_sieve_window(ScDhSieve *sv, unsigned add) { * when p is a strong probable prime to the base 2 and q = (p - 1) / 2 * passes OpenSSL's Miller-Rabin round count; a prime q then proves p prime * (Pocklington's criterion with 2^(p-1) ≡ 1 and gcd(2^2 - 1, p) = 1). - * Candidates past `bits` bits take a new start. */ -static bool sd_generate_safe_prime(mbedtls_mpi *p, size_t bits, unsigned add, unsigned rem) { + * Candidates past `bits` bits take a new start. With `top_two`, starts + * also set the second-highest bit, as BN_RAND_TOP_TWO does. */ +static bool sd_generate_safe_prime(mbedtls_mpi *p, size_t bits, unsigned add, unsigned rem, bool top_two) { ScDhSieve *sv = sd_sieve_new(bits, add); int rounds = sd_mr_rounds(bits); mbedtls_mpi start, q; @@ -803,7 +815,8 @@ static bool sd_generate_safe_prime(mbedtls_mpi *p, size_t bits, unsigned add, un int ret = 0; while (ret == 0) { mbedtls_mpi_uint r; - if (!sd_random_bits(&start, bits, true) || mbedtls_mpi_mod_int(&r, &start, add) != 0 || + if (!sd_random_bits(&start, bits, true) || (top_two && mbedtls_mpi_set_bit(&start, bits - 2, 1) != 0) || + mbedtls_mpi_mod_int(&r, &start, add) != 0 || mbedtls_mpi_sub_int(&start, &start, (mbedtls_mpi_sint)r) != 0 || mbedtls_mpi_add_int(&start, &start, rem) != 0 || (mbedtls_mpi_bitlen(&start) < bits && mbedtls_mpi_add_int(&start, &start, add) != 0)) { @@ -900,7 +913,7 @@ static ScrDyn *sd_generate(double bits, const ScrBytes *generator_bytes, double if (generator == 2) add = 24; else if (generator == 5) add = 60; ScDh *d = sd_alloc(); - if (!sd_generate_safe_prime(&d->p, (size_t)bits, add, add - 1) || mbedtls_mpi_lset(&d->g, (mbedtls_mpi_sint)generator) != 0) { + if (!sd_generate_safe_prime(&d->p, (size_t)bits, add, add - 1, false) || mbedtls_mpi_lset(&d->g, (mbedtls_mpi_sint)generator) != 0) { sd_release(d); sd_throw(SCR_ERR_TYPE, "ERR_INVALID_ARG_VALUE", "Invalid DH parameters"); return NULL; @@ -1034,6 +1047,505 @@ ScrDyn *scr_crypto_dh_group_new(ScrDyn *name) { return sd_box(d); } +/* ── checkPrime and generatePrime ───────────────────────────────────────── + * crypto.checkPrime(Sync) and generatePrime(Sync) over the primality test + * and prime search above, with random.js's argument checks and ncrypto's + * RandomPrimeConfig and BN_generate_prime_ex2 errors. The callback forms + * compute at once and call back in a later macrotask. */ + +/* The longest candidate the Miller-Rabin rounds take: sd_mont_init's R^2 + * has twice the candidate's limbs, within MBEDTLS_MPI_MAX_LIMBS, and + * mbedtls_mpi_exp_mod stops at MBEDTLS_MPI_MAX_BITS. */ +#if SD_MONT +#define SD_MAX_PRIME_BITS 262144 +#else +#define SD_MAX_PRIME_BITS MBEDTLS_MPI_MAX_BITS +#endif + +static void sd_prime_too_long(const char *what) { + char message[128]; + int n = snprintf(message, sizeof message, "%s longer than %d bits have no native implementation", what, + SD_MAX_PRIME_BITS); + scr_throw_error_msg_code(SCR_ERR_ERROR, message, n < 0 ? 0 : (size_t)n, "SC2020"); +} + +/* An OpenSSL bignum error as a crypto job reports it: an Error with the + * formatted message and no other properties. */ +static void sd_throw_bignum(const char *packed, const char *reason) { + char message[96]; + int n = snprintf(message, sizeof message, "error:%s:bignum routines::%s", packed, reason); + ScrStr *text = scr_str_new(message, n < 0 ? 0 : (size_t)n); + ScrError *e = scr_error_new(SCR_ERR_ERROR, text); + scr_str_release(text); + scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg()); +} + +/* validateObject(value, name): an object that is neither null nor an + * array. */ +static bool sd_validate_object(const char *name, const ScrDyn *value) { + ScrStr *type = scr_dyn_typeof(value); + bool object = type->len == 6 && memcmp(type->data, "object", 6) == 0; + scr_str_release(type); + if (object && value->kind != SCR_DYN_NULL && value->kind != SCR_DYN_ARR) return true; + scr_dyn_arg_type_fail(name, "of type object", value); + return false; +} + +static bool sd_validate_function(const char *name, const ScrDyn *value) { + if (scr_dyn_is_callable(value)) return true; + scr_dyn_arg_type_fail(name, "of type function", value); + return false; +} + +/* options[key] (borrowed), or undefined. */ +static const ScrDyn *sd_option(const ScrDyn *options, const char *key) { + const ScrDyn *value = options->kind == SCR_DYN_OBJ ? scr_dyn_obj_get(options, key, strlen(key)) : NULL; + return value ? value : scr_dyn_undefined(); +} + +static const char sd_large_number_types[] = + "of type bigint or an instance of ArrayBuffer, TypedArray, Buffer, or DataView"; + +/* A LargeNumberLike argument as a bignum: a bigint (unsignedBigIntToBuffer, + * which rejects negative values), or the bytes of an ArrayBuffer or view. + * False with the error pending. */ +static bool sd_large_number(mbedtls_mpi *x, const char *name, bool property, const ScrDyn *value) { + if (value->kind == SCR_DYN_BIGINT) { + ScrStr *text = scr_bigint_to_string(value->v.bigint, 16); + if (!text) return false; + bool negative = text->len > 0 && text->data[0] == '-'; + if (negative) { + scr_str_release(text); + ScrStr *decimal = scr_bigint_to_string(value->v.bigint, 10); + if (!decimal) return false; + /* ERR_OUT_OF_RANGE's received value groups the digits of a bigint + * past 2^32 in threes. */ + ScrBigInt *limit = scr_bigint_from_i64(-((int64_t)1 << 32)); + bool group = scr_bigint_cmp_f64(value->v.bigint, limit) < 0; + scr_bigint_release(limit); + char digits[160]; + size_t len = decimal->len < 120 ? decimal->len : 120, out = 0; + for (size_t i = 0; i < len; i++) { + size_t rest = len - i; + if (group && i > 1 && rest % 3 == 0) digits[out++] = '_'; + digits[out++] = decimal->data[i]; + } + digits[out] = '\0'; + scr_str_release(decimal); + char message[256]; + int n = snprintf(message, sizeof message, "The value of \"%s\" is out of range. It must be >= 0. Received %sn", + name, digits); + scr_throw_error_msg_code(SCR_ERR_RANGE, message, n < 0 ? 0 : (size_t)n, "ERR_OUT_OF_RANGE"); + return false; + } + char *hex = malloc(text->len + 1); + if (!hex) scr_trap("scriptc: out of memory\n"); + memcpy(hex, text->data, text->len); + hex[text->len] = '\0'; + scr_str_release(text); + bool ok = strlen(hex) <= SD_MAX_VALUE_BYTES * 2; + if (ok && mbedtls_mpi_read_string(x, 16, hex) != 0) scr_trap("scriptc: out of memory\n"); + free(hex); + if (!ok) sd_value_too_long(); + return ok; + } + if (!sd_is_view(value)) { + if (property) scr_dyn_prop_type_fail(name, sd_large_number_types, value); + else scr_dyn_arg_type_fail(name, sd_large_number_types, value); + return false; + } + ScrBytes *bytes = scr_bytes_buffer_source(value); + if (!bytes) return false; + bool held = sd_read(x, bytes); + scr_bytes_release(bytes); + if (!held) sd_value_too_long(); + return held; +} + +/* calc_trial_divisions: how many of the first primes (2 included) the + * search divides a candidate of `bits` bits by. */ +static size_t sd_trial_divisions(size_t bits) { + return bits <= 512 ? 64 : bits <= 1024 ? 128 : bits <= 2048 ? 384 : bits <= 4096 ? 1024 : 2048; +} + +static uint64_t sd_word(const mbedtls_mpi *x) { + if (mbedtls_mpi_bitlen(x) > 64) return UINT64_MAX; + unsigned char buf[8]; + if (mbedtls_mpi_write_binary(x, buf, 8) != 0) return UINT64_MAX; + uint64_t w = 0; + for (int i = 0; i < 8; i++) w = w << 8 | buf[i]; + return w; +} + +static bool sd_add_word(mbedtls_mpi *x, uint64_t w) { + unsigned char buf[8]; + for (int i = 7; i >= 0; i--, w >>= 8) buf[i] = (unsigned char)w; + mbedtls_mpi t; + mbedtls_mpi_init(&t); + bool ok = mbedtls_mpi_read_binary(&t, buf, 8) == 0 && mbedtls_mpi_add_mpi(x, x, &t) == 0; + mbedtls_mpi_free(&t); + return ok; +} + +/* probable_prime and bn_probable_prime_dh: a random odd candidate of + * `bits` bits (the top two bits set, or with `add` the top bit, moved to + * rem modulo add), then the first one stepping by 2, by 4 for a safe + * prime, or by add, that no trial-division prime divides, nor for a safe + * prime divides (p - 1) / 2. */ +static bool sd_probable_prime(ScDhSmallPrimes *t, mbedtls_mpi *rnd, size_t bits, bool safe, const mbedtls_mpi *add, + const mbedtls_mpi *rem) { + size_t trials = sd_trial_divisions(bits) - 1; /* odd primes */ + uint64_t step = safe ? 4 : 2; + uint64_t maxdelta = UINT64_MAX - t->primes[trials - 1]; + if (add) { + step = sd_word(add); + if (maxdelta > UINT64_MAX - step) maxdelta = UINT64_MAX - step; + } + mbedtls_mpi r; + mbedtls_mpi_init(&r); + bool ok = true; + for (;;) { + if (!sd_random_bits(rnd, bits, true)) ok = false; + if (ok && !add) { + if ((bits >= 2 && mbedtls_mpi_set_bit(rnd, bits - 2, 1) != 0) || (safe && mbedtls_mpi_set_bit(rnd, 1, 1) != 0)) + ok = false; + } else if (ok) { + ok = mbedtls_mpi_mod_mpi(&r, rnd, add) == 0 && mbedtls_mpi_sub_mpi(rnd, rnd, &r) == 0 && + (rem ? mbedtls_mpi_add_mpi(rnd, rnd, rem) == 0 : mbedtls_mpi_add_int(rnd, rnd, safe ? 3 : 1) == 0); + if (ok && (mbedtls_mpi_bitlen(rnd) < bits || sd_word(rnd) < (safe ? 5u : 3u))) + ok = mbedtls_mpi_add_mpi(rnd, rnd, add) == 0; + } + if (!ok || !sd_small_residues(t, rnd)) { + ok = false; + break; + } + uint64_t delta = 0, word = sd_word(rnd); + bool again = false; + for (size_t i = 0; i < trials && !again;) { + uint64_t prime = t->primes[i]; + /* A one-word candidate needs only the primes up to its square root. */ + if (bits <= 31 && delta <= 0x7fffffff && prime * prime > word + delta) break; + uint64_t m = (t->mods[i] + delta) % prime; + if (safe ? m <= 1 : m == 0) { + delta += step; + if (delta > maxdelta) again = true; + i = 0; + continue; + } + i++; + } + if (again) continue; + if (!sd_add_word(rnd, delta)) { + ok = false; + break; + } + if (add || mbedtls_mpi_bitlen(rnd) == bits) break; + } + mbedtls_mpi_free(&r); + return ok; +} + +/* BN_generate_prime_ex2(p, bits, safe, add, rem): false with OpenSSL's + * error pending. Long safe primes without add take the sieve over + * p ≡ 11 (mod 12), which every safe prime above 7 satisfies. */ +static bool sd_generate_prime(mbedtls_mpi *p, size_t bits, bool safe, const mbedtls_mpi *add, const mbedtls_mpi *rem) { + if (bits < 2 || (!add && safe && bits < 6 && bits != 3)) { + sd_throw_bignum("01800076", "bits too small"); + return false; + } + if (add && mbedtls_mpi_cmp_int(add, 0) == 0) { + sd_throw_bignum("01800067", "div by zero"); + return false; + } + if (!add && safe && bits >= 64) { + if (!sd_generate_safe_prime(p, bits, 12, 11, true)) scr_trap("scriptc: out of memory\n"); + return true; + } + ScDhSmallPrimes *t = sd_small_primes(); + mbedtls_mpi q; + mbedtls_mpi_init(&q); + bool found = false; + while (!found) { + if (!sd_probable_prime(t, p, bits, safe, add, rem)) scr_trap("scriptc: out of memory\n"); + if (!safe) { + found = sd_is_prime(t, p); + } else { + if (mbedtls_mpi_copy(&q, p) != 0 || mbedtls_mpi_shift_r(&q, 1) != 0) scr_trap("scriptc: out of memory\n"); + found = sd_is_prime(t, p) && sd_is_prime(t, &q); + } + } + mbedtls_mpi_free(&q); + free(t); + return true; +} + +/* checkPrime's arguments after the candidate: Node's options and checks + * rules. -1 with the error pending. */ +static int sd_prime_checks(const ScrDyn *options) { + if (!sd_validate_object("options", options)) return -1; + const ScrDyn *checks = sd_option(options, "checks"); + if (checks->kind == SCR_DYN_UNDEF) return 0; + if (checks->kind != SCR_DYN_NUM) { + scr_dyn_prop_type_fail("options.checks", "of type number", checks); + return -1; + } + if (!sd_validate_int32_min("options.checks", checks->v.num, 0)) return -1; + return (int)checks->v.num; +} + +/* The candidate's verdict as a boolean dyn (+1), or NULL with the error + * pending. */ +static ScrDyn *sd_check_prime(const mbedtls_mpi *candidate, int checks) { + if (mbedtls_mpi_bitlen(candidate) > SD_MAX_PRIME_BITS) { + sd_prime_too_long("checkPrime candidates"); + return NULL; + } + ScDhSmallPrimes *t = sd_small_primes(); + bool prime = sd_is_prime_checks(t, candidate, checks); + free(t); + return scr_dyn_new_bool(prime); +} + +/* A borrowed, unboxed view of each argument (+1 each); false after a + * throw. */ +static bool sd_unbox_all(ScrDyn **out, ScrDyn *const *args, size_t count) { + for (size_t i = 0; i < count; i++) out[i] = NULL; + for (size_t i = 0; i < count; i++) { + ScrDyn *value = args[i]; + out[i] = value->kind == SCR_DYN_TYPED_REF ? scr_dyn_typed_ref_materialize(value) : scr_dyn_retain(value); + if (!out[i]) return false; + } + return true; +} + +static void sd_release_all(ScrDyn **values, size_t count) { + for (size_t i = 0; i < count; i++) + if (values[i]) scr_dyn_release(values[i]); +} + +/* checkPrimeSync(candidate[, options]). */ +ScrDyn *scr_crypto_check_prime_sync(ScrDyn *candidate, ScrDyn *options) { + ScrDyn *args[2] = {candidate, options}; + ScrDyn *v[2]; + ScrDyn *result = NULL; + mbedtls_mpi x; + mbedtls_mpi_init(&x); + if (sd_unbox_all(v, args, 2) && sd_large_number(&x, "candidate", false, v[0])) { + ScrDyn *empty = v[1]->kind == SCR_DYN_UNDEF ? scr_dyn_new_obj() : NULL; + int checks = sd_prime_checks(empty ? empty : v[1]); + if (empty) scr_dyn_release(empty); + if (checks >= 0) result = sd_check_prime(&x, checks); + } + mbedtls_mpi_free(&x); + sd_release_all(v, 2); + return result; +} + +/* A deferred callback(...args): the callback and its one or two + * arguments, each in a box. */ +static void sd_deliver(ScrClosure *self) { + ScrDyn *callback = (ScrDyn *)scr_box_get_ref(self->caps[0]); + size_t argc = self->ncaps - 1; + ScrDyn *args[2] = {NULL, NULL}; + for (size_t i = 0; i < argc; i++) args[i] = (ScrDyn *)scr_box_get_ref(self->caps[i + 1]); + ScrDyn *result = scr_dyn_call(callback, args, argc, "callback"); + if (result) scr_dyn_release(result); + for (size_t i = 0; i < argc; i++) scr_dyn_release(args[i]); + scr_dyn_release(callback); +} + +/* Calls callback(first[, second]) in a later macrotask (moves first and + * second). */ +static void sd_defer(const ScrDyn *callback, ScrDyn *first, ScrDyn *second) { + ScrDyn *values[3] = {scr_dyn_retain((ScrDyn *)callback), first, second}; + size_t count = second ? 3 : 2; + ScrClosure *c = scr_closure_new((void *)sd_deliver, count); + for (size_t i = 0; i < count; i++) { + c->caps[i] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, NULL); + scr_box_set_ref(c->caps[i], values[i]); + } + scr_loop_defer(c); +} + +/* The callback's error argument on success (+1): Node's undefined, or + * null for a callback typed to take Error | null. */ +static ScrDyn *sd_no_error(bool null_error) { + return null_error ? scr_dyn_new_null() : scr_dyn_retain((ScrDyn *)scr_dyn_undefined()); +} + +/* The pending error, taken as a dyn value (+1). */ +static ScrDyn *sd_take_error(void) { + ScrCaught *caught = scr_exc_take(); + ScrDyn *error = scr_caught_to_dyn(caught); + scr_caught_release(caught); + return error; +} + +/* checkPrime(candidate[, options], callback): argument errors throw; the + * callback receives (undefined, verdict) in a later macrotask (null for + * undefined when `null_error`). */ +void scr_crypto_check_prime_async(ScrDyn *candidate, ScrDyn *options, ScrDyn *callback, bool null_error) { + ScrDyn *args[3] = {candidate, options, callback}; + ScrDyn *v[3]; + mbedtls_mpi x; + mbedtls_mpi_init(&x); + if (sd_unbox_all(v, args, 3) && sd_large_number(&x, "candidate", false, v[0])) { + ScrDyn *bag = v[1], *fn = v[2]; + ScrDyn *empty = NULL; + if (scr_dyn_is_callable(bag)) { + fn = bag; + bag = empty = scr_dyn_new_obj(); + } else if (bag->kind == SCR_DYN_UNDEF) { + bag = empty = scr_dyn_new_obj(); + } + int checks = sd_validate_function("callback", fn) ? sd_prime_checks(bag) : -1; + ScrDyn *verdict = checks >= 0 ? sd_check_prime(&x, checks) : NULL; + if (verdict) sd_defer(fn, sd_no_error(null_error), verdict); + if (empty) scr_dyn_release(empty); + } + mbedtls_mpi_free(&x); + sd_release_all(v, 3); +} + +/* RandomPrimeConfig: the size and options checks, then the add and rem + * bounds. False with the error pending. */ +typedef struct { + size_t bits; + bool safe, bigint, has_add, has_rem; + mbedtls_mpi add, rem; +} ScDhPrimeConfig; + +static void sd_prime_config_free(ScDhPrimeConfig *c) { + mbedtls_mpi_free(&c->add); + mbedtls_mpi_free(&c->rem); +} + +static bool sd_prime_size(const ScrDyn *size) { + if (size->kind != SCR_DYN_NUM) { + scr_dyn_arg_type_fail("size", "of type number", size); + return false; + } + return sd_validate_int32_min("size", size->v.num, 1); +} + +static bool sd_prime_config(ScDhPrimeConfig *c, double size, const ScrDyn *options) { + if (!sd_validate_object("options", options)) return false; + const ScrDyn *safe = sd_option(options, "safe"); + const ScrDyn *bigint = sd_option(options, "bigint"); + const ScrDyn *add = sd_option(options, "add"); + const ScrDyn *rem = sd_option(options, "rem"); + if (safe->kind != SCR_DYN_UNDEF && safe->kind != SCR_DYN_BOOL) { + scr_dyn_prop_type_fail("options.safe", "of type boolean", safe); + return false; + } + if (bigint->kind != SCR_DYN_UNDEF && bigint->kind != SCR_DYN_BOOL) { + scr_dyn_prop_type_fail("options.bigint", "of type boolean", bigint); + return false; + } + c->safe = safe->kind == SCR_DYN_BOOL && safe->v.b; + c->bigint = bigint->kind == SCR_DYN_BOOL && bigint->v.b; + c->has_add = add->kind != SCR_DYN_UNDEF; + c->has_rem = rem->kind != SCR_DYN_UNDEF; + if (c->has_add && !sd_large_number(&c->add, "options.add", true, add)) return false; + if (c->has_rem && !sd_large_number(&c->rem, "options.rem", true, rem)) return false; + c->bits = (size_t)size; + if (c->has_add) { + if (mbedtls_mpi_bitlen(&c->add) > c->bits) { + sd_throw(SCR_ERR_RANGE, "ERR_OUT_OF_RANGE", "invalid options.add"); + return false; + } + if (c->has_rem && mbedtls_mpi_cmp_mpi(&c->add, &c->rem) <= 0) { + sd_throw(SCR_ERR_RANGE, "ERR_OUT_OF_RANGE", "invalid options.rem"); + return false; + } + } + if (c->bits > SD_MAX_PRIME_BITS) { + sd_prime_too_long("generatePrime sizes"); + return false; + } + return true; +} + +/* The generated prime as an ArrayBuffer of its bytes, or a bigint (+1), + * or NULL with OpenSSL's error pending. */ +static ScrDyn *sd_prime_result(ScDhPrimeConfig *c) { + mbedtls_mpi p; + mbedtls_mpi_init(&p); + ScrDyn *result = NULL; + if (sd_generate_prime(&p, c->bits, c->safe, c->has_add ? &c->add : NULL, c->has_add && c->has_rem ? &c->rem : NULL)) { + if (c->bigint) { + size_t len = 0; + mbedtls_mpi_write_string(&p, 16, NULL, 0, &len); + char *hex = malloc(len + 2); + if (!hex) scr_trap("scriptc: out of memory\n"); + hex[0] = '0'; + hex[1] = 'x'; + if (mbedtls_mpi_write_string(&p, 16, hex + 2, len, &len) != 0) scr_trap("scriptc: out of memory\n"); + ScrStr *text = scr_str_new(hex, strlen(hex)); + free(hex); + ScrBigInt *value = scr_bigint_parse(text); + scr_str_release(text); + if (value) { + result = scr_dyn_new_bigint(value); + scr_bigint_release(value); + } + } else { + size_t len = mbedtls_mpi_size(&p); + ScrBytes *bytes = scr_bytes_new(SCR_BYTES_U8, (double)len); + if (mbedtls_mpi_write_binary(&p, bytes->data, len) != 0) scr_trap("scriptc: out of memory\n"); + result = scr_array_buffer_from_bytes(bytes); + scr_bytes_release(bytes); + } + } + mbedtls_mpi_free(&p); + return result; +} + +/* generatePrimeSync(size[, options]). */ +ScrDyn *scr_crypto_generate_prime_sync(ScrDyn *size, ScrDyn *options) { + ScrDyn *args[2] = {size, options}; + ScrDyn *v[2]; + ScrDyn *result = NULL; + ScDhPrimeConfig c = {0}; + mbedtls_mpi_init(&c.add); + mbedtls_mpi_init(&c.rem); + if (sd_unbox_all(v, args, 2) && sd_prime_size(v[0])) { + ScrDyn *empty = v[1]->kind == SCR_DYN_UNDEF ? scr_dyn_new_obj() : NULL; + if (sd_prime_config(&c, v[0]->v.num, empty ? empty : v[1])) result = sd_prime_result(&c); + if (empty) scr_dyn_release(empty); + } + sd_prime_config_free(&c); + sd_release_all(v, 2); + return result; +} + +/* generatePrime(size[, options], callback): argument errors throw; the + * callback receives (undefined, prime), or OpenSSL's error alone, in a + * later macrotask (null for undefined when `null_error`). */ +void scr_crypto_generate_prime_async(ScrDyn *size, ScrDyn *options, ScrDyn *callback, bool null_error) { + ScrDyn *args[3] = {size, options, callback}; + ScrDyn *v[3]; + ScDhPrimeConfig c = {0}; + mbedtls_mpi_init(&c.add); + mbedtls_mpi_init(&c.rem); + if (sd_unbox_all(v, args, 3) && sd_prime_size(v[0])) { + ScrDyn *bag = v[1], *fn = v[2]; + ScrDyn *empty = NULL; + if (scr_dyn_is_callable(bag)) { + fn = bag; + bag = empty = scr_dyn_new_obj(); + } + if (sd_validate_function("callback", fn) && sd_prime_config(&c, v[0]->v.num, bag)) { + ScrDyn *prime = sd_prime_result(&c); + if (prime) sd_defer(fn, sd_no_error(null_error), prime); + else sd_defer(fn, sd_take_error(), NULL); + } + if (empty) scr_dyn_release(empty); + } + sd_prime_config_free(&c); + sd_release_all(v, 3); +} + /* ── methods ──────────────────────────────────────────────────────────── */ static ScrDyn *sd_arg(ScrDyn *const *args, size_t argc, size_t index) { diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index bb3b06847..c75c9791c 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -169,6 +169,7 @@ typedef enum { X(FETCH_RESPONSE, 12) /* native static-fetch Response */ \ X(FILE, 42) /* ScrBlob with a name — new File() */ \ X(FILE_HANDLE, 17) /* fs/promises.FileHandle */ \ + X(FINALIZATION_REGISTRY, 72) /* FinalizationRegistry (bookkeeping only) */ \ X(GLOBAL, 22) /* native global object identity */ \ X(H2_SESSION, 4) /* ScrH2Session — Http2Session (client & server) */ \ X(H2_STREAM, 5) /* ScrH2Stream — Http2Stream (client & server) */ \ @@ -363,10 +364,22 @@ typedef struct ScrDynTypedRecordOps { /* The field names as an immortal string[], built on first use by * for-in over a record with every field present (atomic slot). */ ScrArr *keys; + /* A shape whose values enumerate their keys in more than one order + * (IrRecordShape.orderVariants) binds a table with only this hook: it + * answers the table of the record's current order, which a write of an + * optional key can change after the record is boxed. NULL in every other + * table. */ + const struct ScrDynTypedRecordOps *(*select)(void *rec); } ScrDynTypedRecordOps; /* A record capsule's ops while it answers from the native record (no view * created yet), or NULL. */ const ScrDynTypedRecordOps *scr_dyn_typed_ref_live_record(const ScrDyn *d); +/* The declaration-typed boundary's adoption (scr_decl_adopt.c, linked by + * programs with boundary casts): the nodes of `source` that something else + * still references become live capsules of the native value `capsule` + * holds. Both borrowed; a node that is not plain data throws the path + * TypeError before anything changes. */ +void scr_dyn_decl_adopt(ScrDyn *source, ScrDyn *capsule); /* Prototype members of a compiled class seen as `unknown`, emitted per * class by the compiler: the string names of methods, getters and setters @@ -382,6 +395,25 @@ typedef struct ScrDynTypedClassOps { ScrStr *class_name; /* immortal constructor name; NULL for an anonymous class */ } ScrDynTypedClassOps; +/* A compiled JavaScript class whose instances record their own properties + * in a property bag and whose views inherit from the class's prototype + * data, emitted per class by the compiler. The view of such an instance is + * a fresh object holding exactly the bag's string-keyed properties, where + * each declared field among them takes its value from the native slot, and + * whose prototype is the class's. A method call can therefore resolve + * without building the view (scr_dyn_typed_ref_bag_method). + * bag: the instance's bag, borrowed; NULL or undefined when it has none; + * prototype: +1 prototype a fresh view of the instance takes; + * names: the declared fields whose values come from native slots; + * field: +1 view value of declared field `index`. */ +typedef struct ScrDynTypedBagClassOps { + ScrDyn *(*bag)(void *instance); + ScrDyn *(*prototype)(void *instance); + size_t count; + ScrStr *const *names; /* immortal field-name strings */ + ScrDyn *(*field)(void *instance, size_t index); +} ScrDynTypedBagClassOps; + struct ScrDyn { size_t rc; /* SIZE_MAX = immortal (unused for dyn; kept per convention) */ ScrDynKind kind; @@ -425,11 +457,13 @@ struct ScrDyn { void *source_identity; ScrDyn *(*source_access)(void *, bool materialize); struct ScrDynObjectIndex *index; /* optional wide-object lookup; owns no values */ - /* On a runtime-built class prototype (e.g. url.Url.prototype): the - * constructor name (static text) util.inspect prints for objects - * inheriting from it (scr_dyn_class_name). NULL otherwise. Only - * prototypes that live until their thread exits set it, and they - * clear it before release, so freelist reuse never sees it. */ + /* On a runtime-built class prototype (e.g. url.Url.prototype) and + * on a compiled class's prototype object: the constructor name + * (static or immortal text) util.inspect prints for objects + * inheriting from it (scr_dyn_class_name). NULL otherwise. Names + * that are copies live on prototypes that live until their thread + * exits, and those clear it before release; reuse of a freed object + * resets it (scr_dyn_alloc). */ const char *class_name; struct ScrDynRegion *region; /* see ScrDynRegion */ /* The dynamic tier's hidden class (scr_js_object.c): the transition @@ -464,7 +498,9 @@ struct ScrDyn { * keep their ScrDynTypedRecordOps here instead, flagged by `record` * (scr_dyn_typed_ref_bind_record), and class instances their * ScrDynTypedClassOps, flagged by `klass` - * (scr_dyn_typed_ref_bind_class). NULL for other referents. */ + * (scr_dyn_typed_ref_bind_class), or their ScrDynTypedBagClassOps, + * flagged by `bag_class` (scr_dyn_typed_ref_bind_bag_class). NULL for + * other referents. */ const ScrDynTypedArrayOps *array; /* Both caches belong to this capsule. ReadableStream canonicalizes * capsules for repeated source references, so the refreshed dyn view @@ -473,11 +509,12 @@ struct ScrDyn { ScrDyn *materialized; ScrDynTypedCast *casts; /* One word keeps this, the largest payload, at its previous size. */ - uint32_t type_key_len : 28; + uint32_t type_key_len : 27; uint32_t traced : 1; /* native referent has a cycle header */ uint32_t observed : 1; /* native release reports this identity's disposal */ uint32_t record : 1; /* `array` holds ScrDynTypedRecordOps */ uint32_t klass : 1; /* `array` holds ScrDynTypedClassOps */ + uint32_t bag_class : 1; /* `array` holds ScrDynTypedBagClassOps */ } typed_ref; /* SCR_DYN_PROMISE: the retained promise. The boundary contract: it * settles with a dyn payload (SCR_EXC_REF ScrDyn fulfillment or a @@ -614,13 +651,18 @@ static inline const char *scr_dyn_class_name(const ScrDyn *d) { /* The element hooks of a live array capsule, or NULL (including for record * and class capsules, which keep their hooks in the same slot). */ static inline const ScrDynTypedArrayOps *scr_dyn_typed_ref_array_ops(const ScrDyn *d) { - return d->v.typed_ref.record || d->v.typed_ref.klass ? NULL : d->v.typed_ref.array; + return d->v.typed_ref.record || d->v.typed_ref.klass || d->v.typed_ref.bag_class + ? NULL + : d->v.typed_ref.array; } /* BORROWED member lookup on a SCR_DYN_OBJ; NULL when the key is absent. */ ScrDyn *scr_dyn_obj_get(const ScrDyn *d, const char *key, size_t key_len); /* Own-enumerable check on a SCR_DYN_OBJ. */ bool scr_dyn_obj_enumerable(const ScrDyn *d, const char *key, size_t key_len); +/* Stores own property `entry` of `source` on `target` with its attributes: + * plain data properties by an ordinary write, others by their descriptor. */ +void scr_dyn_copy_entry(ScrDyn *target, const ScrDyn *source, const ScrDynEntry *entry); /* Literal discriminator tests used before selecting a typed record layout. * Missing fields and different primitive kinds never match. Borrowed args. */ bool scr_dyn_field_eq_str(const ScrDyn *d, const ScrStr *key, const ScrStr *value); @@ -714,6 +756,9 @@ ScrDyn *scr_dyn_native_set_new(const ScrDyn *seed); ScrDyn *scr_dyn_native_set(ScrMap *value); /* borrowed backing, +1 box */ bool scr_dyn_native_set_is(const ScrDyn *value); ScrDyn *scr_dyn_native_map(ScrMap *value); /* borrowed backing, +1 box */ +/* `new Map(entries)` over a checked-dynamic seed: +1, or NULL with the + * exception pending. */ +ScrDyn *scr_dyn_native_map_new(const ScrDyn *seed); bool scr_dyn_native_map_is(const ScrDyn *value); void scr_regex_reset_last_index(ScrRegex *re, double index); ScrRegex *scr_regex_new_checked(const ScrDyn *pattern, const ScrDyn *flags); @@ -839,6 +884,7 @@ ScrDyn *scr_dyn_iterator(const ScrDyn *value, const ScrStr *spell); /* for await over a checked-dynamic value (scr_dyn_for_await.c). */ ScrDyn *scr_dyn_async_iterator_open(ScrDyn *value, ScrStr *spell); bool scr_dyn_async_iterator_was_sync(void); +void scr_dyn_async_iterator_set_sync(bool sync); ScrDyn *scr_dyn_iterator_optional(const ScrDyn *value); /* from.js's probe: 2 when value[Symbol.asyncIterator] is set, 1 when * value[Symbol.iterator] is, 0 for neither, -1 with an error pending. */ @@ -857,6 +903,7 @@ ScrDyn *scr_dyn_open_iterator(const ScrDyn *value, int *kind, bool generators); * for-of protocol reads it. +1, or NULL with the error pending. */ ScrDyn *scr_dyn_iterator_result_get(const ScrDyn *result, const char *key, size_t len); void scr_dyn_install_iterator_symbol(ScrDyn *key); +void scr_dyn_install_has_instance_symbol(ScrDyn *key); void scr_dyn_install_async_iterator_symbol(ScrDyn *key); void scr_dyn_install_async_dispose_symbol(ScrDyn *key); /* True when key is Symbol.asyncDispose (registered by the symbol unit when @@ -911,6 +958,9 @@ void *scr_dyn_obj_source_cast(const ScrDyn *d, ScrDyn *scr_dyn_new_obj_null_proto(void); ScrDyn *scr_dyn_obj_create(ScrDyn *prototype); ScrDyn *scr_dyn_obj_create_with_properties(ScrDyn *prototype, ScrDyn *descriptors); +ScrDyn *scr_dyn_get_prototype_for_identity(ScrDyn *object); +bool scr_dyn_bag_create_field(ScrDyn *bag, const ScrStr *key); +bool scr_dyn_is_class_capsule(const ScrDyn *d); ScrDyn *scr_dyn_get_prototype(ScrDyn *object); ScrDyn *scr_dyn_get_own_property_descriptors(ScrDyn *object); ScrDyn *scr_dyn_set_prototype(ScrDyn *object, ScrDyn *prototype); @@ -931,9 +981,16 @@ void scr_dyn_proxy_unsupported(const char *operation); ScrDyn *scr_dyn_new_bytes(const ScrBytes *b); ScrDyn *scr_bytes_constructor(const ScrStr *name); bool scr_bytes_instanceof(const ScrDyn *value, const ScrDyn *callee); +bool scr_bytes_instanceof_at(const ScrDyn *value, const ScrDyn *callee, const ScrStr *refusal); bool scr_dyn_data_view_is(const ScrDyn *value); ScrDyn *scr_dyn_construct(const ScrDyn *callee, const ScrDyn *args, const ScrStr *what); ScrDyn *scr_bytes_construct(const ScrDyn *callee, const ScrDyn *args, const ScrStr *what); +/* Installed by scr_dyn_invoke.c when it interns a builtin constructor value + * (scr_dyn_global_constructor): false when `callee` is not one of them; + * otherwise true with `new callee(...args)` in *result (+1, or NULL with + * the exception pending). */ +extern SCR_TL bool (*scr_dyn_global_construct_fn)(const ScrDyn *callee, const ScrDyn *args, + ScrDyn **result); ScrDyn *scr_dyn_bytes_key_get(const ScrDyn *value, const ScrStr *key); /* The Buffer-flavored twin (stream chunks): string coercion/toString * decode utf8 instead of joining elements. */ @@ -999,6 +1056,17 @@ ScrDyn *scr_record_source_apply(ScrDyn *snapshot, const void *rec); /* Attach the prototype member names of a class instance capsule * (idempotent), so `in` sees its methods and accessors. */ void scr_dyn_typed_ref_bind_class(ScrDyn *d, const ScrDynTypedClassOps *ops); +/* Attach the bag and prototype hooks of a capsule of a bag-tracked class + * instance (idempotent). */ +void scr_dyn_typed_ref_bind_bag_class(ScrDyn *d, const ScrDynTypedBagClassOps *ops); +/* The method `key` that a call on a bag-tracked class instance capsule + * reads from its view, +1, when an ordinary data property answers it: an + * own one (a declared field's value from its native slot, or another bag + * property's value), or else one on the view's prototype chain. NULL when + * only the view can answer (the caller builds it), and also NULL, with the + * exception pending, when the prototype hook threw. Never runs user + * code. */ +ScrDyn *scr_dyn_typed_ref_bag_method(const ScrDyn *d, const char *key, size_t key_len); /* The constructor name of a class instance capsule's own class, for * util.inspect; NULL when the capsule is not a bound class instance, its * class is anonymous, or its prototype was replaced. */ @@ -1066,6 +1134,8 @@ ScrDyn *scr_dyn_map_seed_entry(const ScrDyn *entry); bool scr_weak_map_is(const ScrDyn *value); bool scr_weak_set_is(const ScrDyn *value); ScrDyn *scr_weak_map_new(ScrDyn *entries); +ScrDyn *scr_finalization_registry_new(ScrDyn *cleanup); +bool scr_finalization_registry_is(const ScrDyn *value); ScrDyn *scr_weak_set_new(ScrDyn *values); ScrDyn *scr_dyn_from_entries(ScrDyn *entries); ScrDyn *scr_dyn_mark_snapshot(ScrDyn *value); /* consumes and returns +1 */ @@ -1257,6 +1327,10 @@ void scr_error_set_prop(ScrError *e, const char *key, size_t key_len, ScrDyn *va ScrDyn *scr_errdyn_view_of(const ScrError *e); /* borrowed or NULL */ /* ToBoolean over the dyn kind (JS-exact); borrowed, never throws. */ bool scr_dyn_truthy(const ScrDyn *d); +/* Math.max/Math.min(...src) over an untyped iterable; `what` names the + * source in V8's null/undefined spread TypeError. NaN with it pending. */ +double scr_dyn_math_fold(const ScrDyn *src, bool max, const ScrStr *what); +int scr_dyn_has_instance_hook(const ScrDyn *value, const ScrDyn *callee); /* JS String() over a dyn value (arrays join, [object Object], the error * encoding renders "name: message"); borrowed, result +1, never throws. */ ScrStr *scr_dyn_display(const ScrDyn *d); @@ -1272,15 +1346,19 @@ bool scr_dyn_strict_eq(const ScrDyn *a, const ScrDyn *b); * result owned (+1). MAY THROW (NULL with the exception pending). */ ScrDyn *scr_dyn_prepare_method(ScrDyn *recv, const char *method); ScrDyn *scr_dyn_invoke_prepared(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); -/* scr_dyn_invoke_prepared for Buffer.prototype method names (scr_bytes_dyn.c), - * one entry per method family: a Buffer receiver runs the method, any other - * takes the ordinary dispatch. */ -ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); -ScrDyn *scr_dyn_invoke_buffer_write(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); -ScrDyn *scr_dyn_invoke_buffer_fill(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); -ScrDyn *scr_dyn_invoke_buffer_to_string(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); -ScrDyn *scr_dyn_invoke_buffer_search(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); -ScrDyn *scr_dyn_invoke_buffer(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); +/* scr_dyn_invoke_prepared for a call site whose method name is a constant: + * *mid_slot, the site's -1-initialized slot, caches the name's method id + * after the first call, so later calls skip the name lookup. */ +ScrDyn *scr_dyn_invoke_prepared_id(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); +/* scr_dyn_invoke_prepared_id for Buffer.prototype method names + * (scr_bytes_dyn.c), one entry per method family: a Buffer receiver runs + * the method, any other takes the ordinary dispatch. */ +ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); +ScrDyn *scr_dyn_invoke_buffer_write(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); +ScrDyn *scr_dyn_invoke_buffer_fill(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); +ScrDyn *scr_dyn_invoke_buffer_to_string(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); +ScrDyn *scr_dyn_invoke_buffer_search(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); +ScrDyn *scr_dyn_invoke_buffer(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); ScrDyn *scr_dyn_invoke(ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc, const char *what); /* Keyed read on a FUNC node: the own-property table first (defineProperties * writes land there), then "name" (the box's best-effort static name; "" diff --git a/packages/runtime/src/scr_dyn_builtin_class.c b/packages/runtime/src/scr_dyn_builtin_class.c index 2f947048a..0647a65c0 100644 --- a/packages/runtime/src/scr_dyn_builtin_class.c +++ b/packages/runtime/src/scr_dyn_builtin_class.c @@ -1581,6 +1581,8 @@ bool scr_dyn_function_instanceof(ScrDyn *value, ScrDyn *callee) { scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); return false; } + int hooked = scr_dyn_has_instance_hook(value, callee); + if (hooked >= 0) return hooked == 1; ScrDyn *prototype = bc_get(callee, "prototype"); if (!prototype) return false; bool found = false; diff --git a/packages/runtime/src/scr_dyn_builtin_stream.c b/packages/runtime/src/scr_dyn_builtin_stream.c index cdd815642..926f85eea 100644 --- a/packages/runtime/src/scr_dyn_builtin_stream.c +++ b/packages/runtime/src/scr_dyn_builtin_stream.c @@ -108,10 +108,6 @@ static ScrDyn *bcs_undefined(void) { return scr_dyn_retain(scr_dyn_undefined()); } -static ScrDyn *bcs_str(const char *text) { - return bcs_key(text); -} - static void bcs_unsupported(const char *message) { scr_throw_error_msg_code(SCR_ERR_ERROR, message, strlen(message), "SC2020"); } @@ -231,7 +227,7 @@ static ScrStream *bcs_need(ScrDyn *self, const char *reading) { /* ── completion callbacks ───────────────────────────────────────────── */ -enum { BCS_DONE_WRITE, BCS_DONE_FINAL, BCS_DONE_DESTROY, BCS_DONE_TRANSFORM, BCS_DONE_FLUSH }; +enum { BCS_DONE_WRITE, BCS_DONE_FINAL, BCS_DONE_DESTROY, BCS_DONE_TRANSFORM, BCS_DONE_FLUSH, BCS_DONE_CONSTRUCT }; static void bcs_run_destroy_cbs(ScrStream *s, ScrError *err) { BcsRec *rec = bcs_rec(s); @@ -266,6 +262,9 @@ static ScrDyn *bcs_done_call(ScrClosure *closure, ScrDyn *const *args, size_t ar case BCS_DONE_FINAL: scr_stream_final_done(s, err); break; + case BCS_DONE_CONSTRUCT: + scr_stream_construct_done(s, err); + break; case BCS_DONE_DESTROY: { ScrError *kept = err ? scr_error_retain(err) : NULL; scr_stream_destroy_done(s, err); @@ -401,6 +400,30 @@ static void bcs_plain_call(ScrClosure *cb, ScrStream *s, const char *method, int static bool bcs_is_function(ScrDyn *object, const char *name, bool *failed); +/* constructNT: `try { stream._construct(cb) } catch (err) { onConstruct(err) }`. */ +static void bcs_construct_inv(ScrClosure *cb, ScrStream *s) { + ScrDyn *object = scr_box_get_ref(cb->caps[0]); + ScrDyn *callback = bcs_done(s, BCS_DONE_CONSTRUCT); + ScrDyn *result = bcs_invoke(object, "_construct", &callback, 1); + scr_dyn_release(callback); + if (result) scr_dyn_release(result); + else if (scr_exc_pending()) scr_stream_construct_done(s, bcs_take_error()); + scr_dyn_release(object); +} + +/* clearBuffer's batch: this._writev(chunks, cb); a throw reaches the + * write() caller, as in Node. */ +static void bcs_writev_inv(ScrClosure *cb, ScrStream *s, ScrDyn *chunks) { + ScrDyn *object = scr_box_get_ref(cb->caps[0]); + ScrDyn *args[2]; + args[0] = scr_dyn_retain(chunks); + args[1] = bcs_done(s, BCS_DONE_WRITE); + ScrDyn *result = bcs_invoke(object, "_writev", args, 2); + for (int i = 0; i < 2; i++) scr_dyn_release(args[i]); + if (result) scr_dyn_release(result); + scr_dyn_release(object); +} + /* prefinish reads `stream._final` when the writable side finishes, so a * method assigned after the constructor runs; without one the finish goes * on as Node's does when `typeof stream._final !== 'function'`. */ @@ -875,16 +898,22 @@ static ScrDyn *bcs_construct(int kind, ScrDyn *self, ScrDyn *const *args, size_t } bool failed = false; bool writes = kind != BCS_READABLE; - if (writes && bcs_is_function(self, "_writev", &failed)) { - bcs_unsupported("_writev on a constructor-function stream is not supported yet"); - return NULL; - } - if (!failed && bcs_is_function(self, "_construct", &failed)) { - bcs_unsupported("_construct on a constructor-function stream is not supported yet"); - return NULL; - } - if (failed) return NULL; + bool transformish = kind == BCS_TRANSFORM || kind == BCS_PASSTHROUGH; ScrDyn *opts = bcs_arg(args, argc, 0); + /* Node's constructors assign options.writev and options.construct to + * the instance (this._writev = options.writev), then read _writev and + * _construct, so an option shadows the prototype's method. */ + if (opts->kind == SCR_DYN_OBJ) { + if (writes && !transformish && !bcs_copy_option(self, opts, "writev", "_writev")) return NULL; + if (!bcs_copy_option(self, opts, "construct", "_construct")) return NULL; + } + bool has_writev = writes && bcs_is_function(self, "_writev", &failed); + if (has_writev && transformish) { + bcs_unsupported("_writev on a constructor-function Transform is not supported yet"); + return NULL; + } + bool has_construct = !failed && bcs_is_function(self, "_construct", &failed); + if (failed) return NULL; ScrStream *s = kind == BCS_READABLE ? scr_stream_new_readable_dyn(opts) : kind == BCS_WRITABLE ? scr_stream_new_writable_dyn(opts) : kind == BCS_DUPLEX ? scr_stream_new_duplex_dyn(opts) @@ -936,6 +965,9 @@ static ScrDyn *bcs_construct(int kind, ScrDyn *self, ScrDyn *const *args, size_t } } scr_stream_set_destroy(s, bcs_hook(self, (void *)bcs_destroy_inv), bcs_destroy_inv); + if (has_writev) scr_stream_set_writev(s, bcs_hook(self, (void *)bcs_writev_inv), bcs_writev_inv); + if (has_construct) + scr_stream_set_construct(s, bcs_hook(self, (void *)bcs_construct_inv), bcs_construct_inv); bcs_hide(self, s); if (!failed) failed = !bcs_adopt_listeners(s, self); if (!failed && kind != BCS_WRITABLE) failed = !bcs_state_view(self, s, true); @@ -1020,17 +1052,6 @@ static ScrClosure *bcs_callback(ScrDyn *fn, ScrDyn *self, ScrStream *s) { return closure; } -/* Strings in another encoding than utf8 have no write path here. */ -static bool bcs_utf8_only(const ScrDyn *encoding) { - if (encoding->kind != SCR_DYN_STR) return true; - ScrStr *canonical = bcs_encoding(encoding); - if (!canonical) return false; - bool utf8 = bcs_is(canonical, "utf8"); - scr_str_release(canonical); - if (!utf8) bcs_unsupported("writing strings in an encoding other than utf8 to a constructor-function stream is not supported yet"); - return utf8; -} - static ScrDyn *bcs_write(ScrStream *s, ScrDyn *self, ScrDyn *const *args, size_t argc) { ScrDyn *chunk = bcs_arg(args, argc, 0); ScrDyn *encoding = bcs_arg(args, argc, 1); @@ -1039,14 +1060,9 @@ static ScrDyn *bcs_write(ScrStream *s, ScrDyn *self, ScrDyn *const *args, size_t cb = encoding; encoding = scr_stream_writes_values(s) ? scr_dyn_new_null() : scr_dyn_undefined(); } - bool below; - if (scr_stream_writes_values(s)) { - /* an object-mode side hands the encoding argument to _write */ - below = scr_stream_write_enc(s, chunk, encoding, bcs_callback(cb, self, s)); - } else { - if (chunk->kind == SCR_DYN_STR && !bcs_utf8_only(encoding)) return NULL; - below = scr_stream_write_dyn(s, chunk, bcs_callback(cb, self, s)); - } + /* an object-mode side hands the encoding argument to _write; a byte-mode + * side encodes a string with it (or the default encoding) */ + bool below = scr_stream_write_enc(s, chunk, encoding, bcs_callback(cb, self, s)); if (scr_exc_pending()) return NULL; return scr_dyn_new_bool(below); } @@ -1064,24 +1080,17 @@ static ScrDyn *bcs_end(ScrStream *s, ScrDyn *self, ScrDyn *const *args, size_t a encoding = scr_dyn_new_null(); } ScrStream *chained = NULL; - if (!bcs_nullish(chunk) && scr_stream_writes_values(s)) { - /* end(chunk) writes the value with the encoding argument */ + if (!bcs_nullish(chunk)) { + /* end(chunk) writes the chunk with the encoding argument first */ + if (scr_stream_prop(s, "writableObjectMode") == 0 && chunk->kind != SCR_DYN_STR && + chunk->kind != SCR_DYN_BYTES) { + scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer, TypedArray, or DataView", chunk); + return NULL; + } scr_stream_write_enc(s, chunk, encoding, NULL); if (scr_exc_pending()) return NULL; - chained = scr_stream_end(s, NULL, NULL, bcs_callback(cb, self, s)); - } else if (chunk->kind == SCR_DYN_STR && !bcs_utf8_only(encoding)) { - return NULL; - } else if (bcs_nullish(chunk)) { - chained = scr_stream_end(s, NULL, NULL, bcs_callback(cb, self, s)); - } else if (chunk->kind == SCR_DYN_STR) { - chained = scr_stream_end(s, NULL, chunk->v.str, bcs_callback(cb, self, s)); - } else if (chunk->kind == SCR_DYN_BYTES) { - ScrBytes *bytes = scr_bytes_raw_view(chunk->v.bytes); - chained = scr_stream_end(s, bytes, NULL, bcs_callback(cb, self, s)); - scr_bytes_release(bytes); - } else { - scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer, TypedArray, or DataView", chunk); } + chained = scr_stream_end(s, NULL, NULL, bcs_callback(cb, self, s)); if (chained) scr_stream_release(chained); if (scr_exc_pending()) return NULL; return scr_dyn_retain(self); @@ -1164,8 +1173,26 @@ static ScrDyn *bcs_method_call(ScrClosure *closure, ScrDyn *const *args, size_t switch (method) { case BCS_M_READ_DEFAULT: return bcs_not_implemented("_read()"); - case BCS_M_WRITE_DEFAULT: + case BCS_M_WRITE_DEFAULT: { + /* Writable.prototype._write: this._writev([{ chunk, encoding }], cb) */ + ScrDyn *self = scr_dyn_this_get(); + bool failed = false; + if (bcs_is_function(self, "_writev", &failed)) { + ScrDyn *chunks = scr_dyn_new_arr(); + ScrDyn *entry = scr_dyn_new_obj(); + scr_dyn_obj_set(entry, "chunk", 5, scr_dyn_retain(bcs_arg(args, argc, 0))); + scr_dyn_obj_set(entry, "encoding", 8, scr_dyn_retain(bcs_arg(args, argc, 1))); + scr_dyn_arr_push(chunks, entry); + ScrDyn *pass[2] = {chunks, bcs_arg(args, argc, 2)}; + ScrDyn *result = bcs_invoke(self, "_writev", pass, 2); + scr_dyn_release(chunks); + scr_dyn_release(self); + return result; + } + scr_dyn_release(self); + if (failed) return NULL; return bcs_not_implemented("_write()"); + } case BCS_M_TRANSFORM_DEFAULT: return bcs_not_implemented("_transform()"); case BCS_M_DESTROY_DEFAULT: @@ -1317,9 +1344,12 @@ static ScrDyn *bcs_method_call(ScrClosure *closure, ScrDyn *const *args, size_t scr_stream_uncork(s); result = bcs_undefined(); break; - case BCS_M_SET_DEFAULT_ENCODING: - if (bcs_utf8_only(first)) result = scr_dyn_retain(self); + case BCS_M_SET_DEFAULT_ENCODING: { + ScrStream *chained = scr_stream_set_default_encoding(s, first); + if (chained) scr_stream_release(chained); + if (!scr_exc_pending()) result = scr_dyn_retain(self); break; + } default: bcs_unsupported("Readable.prototype.wrap on a constructor-function stream is not supported yet"); break; diff --git a/packages/runtime/src/scr_dyn_for_await.c b/packages/runtime/src/scr_dyn_for_await.c index 25183c4ee..3a1a6458d 100644 --- a/packages/runtime/src/scr_dyn_for_await.c +++ b/packages/runtime/src/scr_dyn_for_await.c @@ -38,6 +38,11 @@ ScrDyn *scr_dyn_async_iterator_open(ScrDyn *value, ScrStr *spell) { return iterator; } +/* For an iterator another open path made (scr_dyn_for_await_stream.c). */ +void scr_dyn_async_iterator_set_sync(bool sync) { + scr_for_await_sync = sync; +} + /* Whether the iterator the last scr_dyn_async_iterator_open returned is a * synchronous one. Read immediately after the open. */ bool scr_dyn_async_iterator_was_sync(void) { diff --git a/packages/runtime/src/scr_dyn_for_await_stream.c b/packages/runtime/src/scr_dyn_for_await_stream.c new file mode 100644 index 000000000..6facf916c --- /dev/null +++ b/packages/runtime/src/scr_dyn_for_await_stream.c @@ -0,0 +1,61 @@ +/* `for await` over a checked-dynamic value in a program that links the + * stream unit: a native readable stream behind the value (a boxed stream, + * or a Cipheriv/Decipheriv handle's Transform) iterates through an + * iterator object whose next() answers the readable iterator's step as + * { value, done } and whose return() closes it as a typed loop does + * (destroy with an AbortError). Any other value takes + * scr_dyn_async_iterator_open. Linked only by programs with such a loop + * and streams. */ +#include "scr_runtime.h" +#include "scr_stream.h" + +/* caps[0]: the stream */ +static ScrStream *iter_stream(ScrClosure *clo) { + return scr_box_get_ref(clo->caps[0]); /* +1 */ +} + +static ScrDyn *iter_next_thunk(ScrClosure *clo, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + ScrStream *s = iter_stream(clo); + ScrPromise *p = scr_stream_next_result_dyn(s); + scr_stream_release(s); + ScrDyn *d = scr_dyn_new_promise(p); + scr_promise_release(p); + return d; +} + +static ScrDyn *iter_return_thunk(ScrClosure *clo, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + ScrStream *s = iter_stream(clo); + scr_stream_release(scr_stream_iterator_close(s)); + scr_stream_release(s); + ScrDyn *result = scr_dyn_new_obj(); + scr_dyn_obj_set(result, "value", 5, scr_dyn_retain(scr_dyn_undefined())); + scr_dyn_obj_set(result, "done", 4, scr_dyn_new_bool(true)); + ScrPromise *p = scr_promise_new(); + scr_promise_fulfill_ref(p, result, scr_dyn_retain_v, scr_dyn_release_v, NULL); + ScrDyn *d = scr_dyn_new_promise(p); + scr_promise_release(p); + return d; +} + +static ScrDyn *iter_method(ScrStream *s, ScrDynThunk thunk, const char *name) { + ScrClosure *clo = scr_closure_new((void *)thunk, 1); + clo->caps[0] = scr_box_new_obj(&scr_stream_retain_v, &scr_stream_release_v, &scr_stream_trace); + scr_box_set_ref(clo->caps[0], scr_stream_retain(s)); + return scr_dyn_new_func(clo, thunk, 0, "()", name); +} + +ScrDyn *scr_stream_dyn_async_iterator_open(ScrDyn *value, ScrStr *spell) { + ScrStream *s = scr_stream_native_readable_of_dyn(value); + if (!s) return scr_dyn_async_iterator_open(value, spell); + /* the loop awaits its results with the async protocol */ + scr_dyn_async_iterator_set_sync(false); + ScrDyn *it = scr_dyn_new_obj(); + scr_dyn_obj_set(it, "next", 4, iter_method(s, &iter_next_thunk, "next")); + scr_dyn_obj_set(it, "return", 6, iter_method(s, &iter_return_thunk, "return")); + scr_stream_release(s); + return it; +} diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index 80e01b08c..364017e05 100644 --- a/packages/runtime/src/scr_dyn_invoke.c +++ b/packages/runtime/src/scr_dyn_invoke.c @@ -8,6 +8,7 @@ * scr_json.c; this unit uses only its public surface plus ScrJsonBuf. */ #include "scr_runtime.h" +#include "scr_dyn_methods.h" #include #include @@ -549,7 +550,11 @@ ScrDyn *scr_dyn_array_proto_call(ScrDyn *recv, ScrStr *method, ScrDyn *args) { return scr_dyn_retain(scr_dyn_undefined()); } -static bool dyn_name_is(const char *m, const char *n) { return strcmp(m, n) == 0; } +/* The dispatch below maps a call's method name to an id once + * (dyn_method_of, scr_dyn_methods.h) and its branches compare ids. Names + * compare by content, and any name the branches do not test is DYN_M_NONE. + * scripts/gen-dyn-methods.mjs writes the header from the DYN_M_ ids used + * here. */ /* Handle receivers: hand the whole call to the tag's ops (installed by * the owning unit at main() — a missing install is an internal error the @@ -692,29 +697,34 @@ static bool dyn_arr_reversed_copy(ScrDyn *out, const ScrDyn *source) { /* Names each prototype declares BEYOND what's implemented here — these * throw the "not supported yet" Error instead of mis-answering "is not a * function". */ -static bool dyn_arr_proto_unimpl(const char *m) { - static const char *names[] = { "toString", "toLocaleString", NULL }; - for (size_t i = 0; names[i]; i++) if (dyn_name_is(m, names[i])) return true; - return false; +static bool dyn_arr_proto_unimpl(int m) { + return m == DYN_M_toString || m == DYN_M_toLocaleString; } -static bool dyn_arr_proto_mutates(const char *m) { - static const char *names[] = { - "push", "pop", "shift", "unshift", "reverse", "sort", "fill", "copyWithin", "splice", NULL - }; - for (size_t i = 0; names[i]; i++) if (dyn_name_is(m, names[i])) return true; - return false; +static bool dyn_arr_proto_mutates(int m) { + switch (m) { + case DYN_M_push: case DYN_M_pop: case DYN_M_shift: case DYN_M_unshift: case DYN_M_reverse: + case DYN_M_sort: case DYN_M_fill: case DYN_M_copyWithin: case DYN_M_splice: + return true; + default: + return false; + } } -static bool dyn_bytes_proto_real(const char *m) { - static const char *names[] = { "slice", "at", "indexOf", "lastIndexOf", "includes", "join", - "forEach", "map", "filter", "some", "every", "find", "findIndex", "reverse", "fill", "set", - "subarray", "sort", "keys", "values", "entries", "reduce", "reduceRight", "copyWithin", - "toString", "toLocaleString", NULL }; - for (size_t i = 0; names[i]; i++) if (dyn_name_is(m, names[i])) return true; - return false; +static bool dyn_bytes_proto_real(int m) { + switch (m) { + case DYN_M_slice: case DYN_M_at: case DYN_M_indexOf: case DYN_M_lastIndexOf: case DYN_M_includes: + case DYN_M_join: case DYN_M_forEach: case DYN_M_map: case DYN_M_filter: case DYN_M_some: + case DYN_M_every: case DYN_M_find: case DYN_M_findIndex: case DYN_M_reverse: case DYN_M_fill: + case DYN_M_set: case DYN_M_subarray: case DYN_M_sort: case DYN_M_keys: case DYN_M_values: + case DYN_M_entries: case DYN_M_reduce: case DYN_M_reduceRight: case DYN_M_copyWithin: + case DYN_M_toString: case DYN_M_toLocaleString: + return true; + default: + return false; + } } static ScrDyn *scr_dyn_invoke_impl( - ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc, + ScrDyn *recv, const char *method, int mid, ScrDyn *const *args, size_t argc, const char *what, ScrDyn *callback_recv); /* Live native arrays: the constant-time methods that loops call once per @@ -724,18 +734,18 @@ static ScrDyn *scr_dyn_invoke_impl( * array. Anything that could observe more — a hole, argument coercion, a * multi-value push, a value the element type rejects — sets *handled false * and takes the snapshot path. */ -static ScrDyn *dyn_live_array_method(ScrDyn *recv, const char *method, ScrDyn *const *args, +static ScrDyn *dyn_live_array_method(ScrDyn *recv, int mid, ScrDyn *const *args, size_t argc, const char *what, bool *handled) { const ScrDynTypedArrayOps *ops = scr_dyn_typed_ref_array_ops(recv); if (!ops) return NULL; ScrArr *arr = recv->v.typed_ref.ptr; size_t len = arr->len; - if (dyn_name_is(method, "push") && argc == 1 && ops->set) { + if ((mid == DYN_M_push) && argc == 1 && ops->set) { if (!ops->set(arr, (double)len, args[0])) return NULL; *handled = true; return scr_exc_pending() ? NULL : scr_dyn_new_num((double)arr->len); } - if (dyn_name_is(method, "pop")) { + if ((mid == DYN_M_pop)) { if (len == 0) { *handled = true; return scr_dyn_retain(scr_dyn_undefined()); @@ -746,7 +756,7 @@ static ScrDyn *dyn_live_array_method(ScrDyn *recv, const char *method, ScrDyn *c *handled = true; return last; } - if (dyn_name_is(method, "at") && (argc == 0 || args[0]->kind == SCR_DYN_NUM || args[0]->kind == SCR_DYN_UNDEF)) { + if ((mid == DYN_M_at) && (argc == 0 || args[0]->kind == SCR_DYN_NUM || args[0]->kind == SCR_DYN_UNDEF)) { double index = dyn_index_arg(args, argc, 0, 0, what); if (index < 0) index += (double)len; if (index < 0 || index >= (double)len) { @@ -786,11 +796,25 @@ ScrDyn *scr_dyn_invoke_prepared(ScrDyn *recv, ScrDyn *callee, const char *method ScrDyn *scr_dyn_invoke(ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc, const char *what) { - return scr_dyn_invoke_impl(recv, method, args, argc, what, NULL); + return scr_dyn_invoke_impl(recv, method, dyn_method_of(method), args, argc, what, NULL); +} + +ScrDyn *scr_dyn_invoke_prepared_id(ScrDyn *recv, ScrDyn *callee, const char *method, + int32_t *mid_slot, ScrDyn *const *args, size_t argc, + const char *what) { + if (callee) return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); + /* The slot is the call site's, shared by every thread: each writes the + * same id. */ + int32_t mid = __atomic_load_n(mid_slot, __ATOMIC_RELAXED); + if (mid < 0) { + mid = dyn_method_of(method); + __atomic_store_n(mid_slot, mid, __ATOMIC_RELAXED); + } + return scr_dyn_invoke_impl(recv, method, mid, args, argc, what, NULL); } static ScrDyn *scr_dyn_invoke_impl( - ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc, + ScrDyn *recv, const char *method, int mid, ScrDyn *const *args, size_t argc, const char *what, ScrDyn *callback_recv) { if (recv->kind == SCR_DYN_UNDEF || recv->kind == SCR_DYN_NULL) { ScrJsonBuf b; @@ -811,7 +835,7 @@ static ScrDyn *scr_dyn_invoke_impl( if (recv->kind == SCR_DYN_HANDLE) { return scr_dynh_dispatch(recv, method, args, argc, what); } - if (scr_dyn_is_array_prototype(recv) && dyn_arr_proto_mutates(method)) { + if (scr_dyn_is_array_prototype(recv) && dyn_arr_proto_mutates(mid)) { scr_dyn_array_prototype_element_fence(recv); return NULL; } @@ -836,7 +860,7 @@ static ScrDyn *scr_dyn_invoke_impl( * snapshot identity back to the externally visible capsule. */ if (recv->kind == SCR_DYN_TYPED_REF) { bool handled = false; - ScrDyn *fast = dyn_live_array_method(recv, method, args, argc, what, &handled); + ScrDyn *fast = dyn_live_array_method(recv, mid, args, argc, what, &handled); if (handled) return fast; /* Iterator helpers on a compiled generator. */ if (scr_dyn_generator_helper_hook && @@ -855,15 +879,29 @@ static ScrDyn *scr_dyn_invoke_impl( scr_dyn_release(date_method); return result; } + /* A method of a bag-tracked class instance that its view holds as an + * ordinary data property, own or inherited: the call the view's + * dispatch below makes, without building the view. */ + size_t method_len = strlen(method); + ScrDyn *inherited = scr_dyn_typed_ref_bag_method(recv, method, method_len); + if (scr_exc_pending()) return NULL; + if (inherited && scr_dyn_is_callable(inherited)) { + scr_dyn_this_push_dyn(recv); + ScrDyn *result = scr_dyn_call(inherited, args, argc, what); + scr_dyn_this_pop(); + scr_dyn_release(inherited); + return result; + } + scr_dyn_release(inherited); ScrDyn *materialized = scr_dyn_typed_ref_materialize(recv); if (scr_exc_pending()) { scr_dyn_release(materialized); return NULL; } bool mutates = materialized->kind == SCR_DYN_ARR && - dyn_arr_proto_mutates(method); + dyn_arr_proto_mutates(mid); ScrDyn *result = scr_dyn_invoke_impl( - materialized, method, args, argc, what, recv); + materialized, method, mid, args, argc, what, recv); if (mutates && !scr_exc_pending()) scr_dyn_typed_ref_commit(recv); if (result == materialized) { scr_dyn_release(result); @@ -892,18 +930,18 @@ static ScrDyn *scr_dyn_invoke_impl( } if (recv->kind == SCR_DYN_FUNC) { - if (dyn_name_is(method, "apply")) { + if ((mid == DYN_M_apply)) { return scr_dyn_apply_array_like(recv, argc ? args[0] : scr_dyn_undefined(), argc > 1 ? args[1] : scr_dyn_undefined(), what); } - if (dyn_name_is(method, "call")) { + if ((mid == DYN_M_call)) { scr_dyn_this_push_dyn(argc >= 1 ? args[0] : scr_dyn_undefined()); ScrDyn *r = scr_dyn_call(recv, argc > 1 ? args + 1 : NULL, argc > 1 ? argc - 1 : 0, what); scr_dyn_this_pop(); return r; } - if (dyn_name_is(method, "bind")) return scr_dyn_bind(recv, args, argc); - if (dyn_name_is(method, "toString")) { + if ((mid == DYN_M_bind)) return scr_dyn_bind(recv, args, argc); + if ((mid == DYN_M_toString)) { ScrDyn *member = scr_dyn_fn_get(recv, method, strlen(method)); if (member && member->kind != SCR_DYN_UNDEF) { scr_dyn_this_push_dyn(recv); @@ -938,9 +976,9 @@ static ScrDyn *scr_dyn_invoke_impl( } if ((recv->kind == SCR_DYN_SYMBOL || recv->kind == SCR_DYN_BIGINT || recv->kind == SCR_DYN_NUM || recv->kind == SCR_DYN_BOOL || recv->kind == SCR_DYN_STR) && - dyn_name_is(method, "valueOf")) return scr_dyn_retain(recv); + (mid == DYN_M_valueOf)) return scr_dyn_retain(recv); - if (recv->kind == SCR_DYN_NUM && dyn_name_is(method, "toFixed")) { + if (recv->kind == SCR_DYN_NUM && (mid == DYN_M_toFixed)) { double digits = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; ScrStr *text = scr_num_to_fixed(recv->v.num, digits); @@ -949,7 +987,7 @@ static ScrDyn *scr_dyn_invoke_impl( scr_str_release(text); return result; } - if (recv->kind == SCR_DYN_NUM && dyn_name_is(method, "toExponential")) { + if (recv->kind == SCR_DYN_NUM && (mid == DYN_M_toExponential)) { if (argc && args[0]->kind != SCR_DYN_UNDEF) { dyn_throw_unsupported("Number", method); return NULL; @@ -962,7 +1000,7 @@ static ScrDyn *scr_dyn_invoke_impl( if (recv->kind == SCR_DYN_STR) { ScrStr *s = recv->v.str; - if (dyn_name_is(method, "slice")) { + if ((mid == DYN_M_slice)) { double start = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; double end = dyn_index_arg(args, argc, 1, scr_str_utf16_len(s), what); @@ -972,16 +1010,29 @@ static ScrDyn *scr_dyn_invoke_impl( scr_str_release(piece); return r; } - if (dyn_name_is(method, "at") || dyn_name_is(method, "concat") || - dyn_name_is(method, "indexOf") || dyn_name_is(method, "lastIndexOf") || - dyn_name_is(method, "includes")) { - if ((dyn_name_is(method, "indexOf") || dyn_name_is(method, "lastIndexOf") || - dyn_name_is(method, "includes")) && + if (mid == DYN_M_at) { + /* String.prototype.at: one UTF-16 code unit, counted from the end + * for a negative index; undefined outside the string. */ + double index = dyn_index_arg(args, argc, 0, 0, what); + if (scr_exc_pending()) return NULL; + double len = (double)scr_str_utf16_len(s); + double k = index < 0 ? len + index : index; + if (k < 0 || k >= len) return scr_dyn_retain(scr_dyn_undefined()); + ScrStr *unit = scr_str_slice(s, k, k + 1); + ScrDyn *r = scr_dyn_new_str(unit); /* retains */ + scr_str_release(unit); + return r; + } + if ((mid == DYN_M_concat) || + (mid == DYN_M_indexOf) || (mid == DYN_M_lastIndexOf) || + (mid == DYN_M_includes)) { + if (((mid == DYN_M_indexOf) || (mid == DYN_M_lastIndexOf) || + (mid == DYN_M_includes)) && argc >= 1 && args[0]->kind == SCR_DYN_STR) { - double position = dyn_index_arg(args, argc, 1, dyn_name_is(method, "lastIndexOf") ? INFINITY : 0, what); + double position = dyn_index_arg(args, argc, 1, (mid == DYN_M_lastIndexOf) ? INFINITY : 0, what); if (scr_exc_pending()) return NULL; - if (dyn_name_is(method, "includes")) return scr_dyn_new_bool(scr_str_index_of(s, args[0]->v.str, position) >= 0); - if (dyn_name_is(method, "indexOf")) return scr_dyn_new_num(scr_str_index_of(s, args[0]->v.str, position)); + if ((mid == DYN_M_includes)) return scr_dyn_new_bool(scr_str_index_of(s, args[0]->v.str, position) >= 0); + if ((mid == DYN_M_indexOf)) return scr_dyn_new_num(scr_str_index_of(s, args[0]->v.str, position)); return scr_dyn_new_num(scr_str_last_index_of_from(s, args[0]->v.str, position)); } dyn_throw_unsupported("String", method); @@ -992,19 +1043,19 @@ static ScrDyn *scr_dyn_invoke_impl( } if (recv->kind == SCR_DYN_ARR) { - if (dyn_name_is(method, "keys") || dyn_name_is(method, "values") || dyn_name_is(method, "entries")) + if ((mid == DYN_M_keys) || (mid == DYN_M_values) || (mid == DYN_M_entries)) return scr_dyn_native_handle_iterator(callback_recv ? callback_recv : recv, - dyn_name_is(method, "keys") ? 1 : dyn_name_is(method, "entries") ? 2 : 0); + (mid == DYN_M_keys) ? 1 : (mid == DYN_M_entries) ? 2 : 0); size_t len = recv->v.arr.len; if (recv->v.arr.presence) { bool sparse = false; for (size_t i = 0; i < len; i++) if (!recv->v.arr.presence[i]) { sparse = true; break; } if (!sparse) { free(recv->v.arr.presence); recv->v.arr.presence = NULL; } - else if (dyn_name_is(method, "forEach") || dyn_name_is(method, "map") || dyn_name_is(method, "filter") || - dyn_name_is(method, "some") || dyn_name_is(method, "every") || dyn_name_is(method, "find") || - dyn_name_is(method, "findIndex") || dyn_name_is(method, "findLast") || dyn_name_is(method, "findLastIndex") || - dyn_name_is(method, "reduce") || dyn_name_is(method, "reduceRight") || dyn_name_is(method, "slice") || - dyn_name_is(method, "indexOf") || dyn_name_is(method, "lastIndexOf") || dyn_name_is(method, "includes")) { + else if ((mid == DYN_M_forEach) || (mid == DYN_M_map) || (mid == DYN_M_filter) || + (mid == DYN_M_some) || (mid == DYN_M_every) || (mid == DYN_M_find) || + (mid == DYN_M_findIndex) || (mid == DYN_M_findLast) || (mid == DYN_M_findLastIndex) || + (mid == DYN_M_reduce) || (mid == DYN_M_reduceRight) || (mid == DYN_M_slice) || + (mid == DYN_M_indexOf) || (mid == DYN_M_lastIndexOf) || (mid == DYN_M_includes)) { ScrDyn *pack = scr_dyn_new_arr(); for (size_t i = 0; i < argc; i++) scr_dyn_arr_push(pack, scr_dyn_retain(args[i])); ScrStr *name = scr_str_new(method, strlen(method)); @@ -1012,7 +1063,7 @@ static ScrDyn *scr_dyn_invoke_impl( scr_str_release(name); scr_dyn_release(pack); return result; - } else if (dyn_arr_proto_mutates(method) && !dyn_name_is(method, "fill") && !dyn_name_is(method, "push") && !dyn_name_is(method, "pop") && !dyn_name_is(method, "shift")) { + } else if (dyn_arr_proto_mutates(mid) && !(mid == DYN_M_fill) && !(mid == DYN_M_push) && !(mid == DYN_M_pop) && !(mid == DYN_M_shift)) { static const char message[] = "This checked array mutation over holes is not supported yet"; scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); return NULL; @@ -1020,11 +1071,11 @@ static ScrDyn *scr_dyn_invoke_impl( } size_t splice_start = 0; size_t splice_removed = 0; - if (dyn_arr_proto_mutates(method)) { - bool grows = dyn_name_is(method, "push") && argc > 0; - grows = grows || (dyn_name_is(method, "unshift") && argc > 0); - bool shrinks = (dyn_name_is(method, "pop") || dyn_name_is(method, "shift")) && len > 0; - if (dyn_name_is(method, "splice")) { + if (dyn_arr_proto_mutates(mid)) { + bool grows = (mid == DYN_M_push) && argc > 0; + grows = grows || ((mid == DYN_M_unshift) && argc > 0); + bool shrinks = ((mid == DYN_M_pop) || (mid == DYN_M_shift)) && len > 0; + if ((mid == DYN_M_splice)) { double startD = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; splice_start = dyn_rel_index(startD, len); @@ -1061,33 +1112,33 @@ static ScrDyn *scr_dyn_invoke_impl( return NULL; } } - if (recv->v.arr.frozen && (dyn_name_is(method, "push") || - dyn_name_is(method, "pop") || dyn_name_is(method, "shift") || - dyn_name_is(method, "unshift") || dyn_name_is(method, "splice"))) { + if (recv->v.arr.frozen && ((mid == DYN_M_push) || + (mid == DYN_M_pop) || (mid == DYN_M_shift) || + (mid == DYN_M_unshift) || (mid == DYN_M_splice))) { static const char message[] = "Cannot assign to read only property 'length'"; scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); return NULL; } - if ((grows && recv->non_extensible) || (shrinks && recv->v.arr.sealed && !dyn_name_is(method, "shift")) || - (recv->v.arr.frozen && ((dyn_name_is(method, "sort") && len > 1) || - (dyn_name_is(method, "reverse") && len > 1)))) { + if ((grows && recv->non_extensible) || (shrinks && recv->v.arr.sealed && !(mid == DYN_M_shift)) || + (recv->v.arr.frozen && (((mid == DYN_M_sort) && len > 1) || + ((mid == DYN_M_reverse) && len > 1)))) { static const char message[] = "Cannot modify a sealed or frozen array"; scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); return NULL; } } - if (dyn_name_is(method, "push")) { + if ((mid == DYN_M_push)) { for (size_t i = 0; i < argc; i++) scr_dyn_arr_push(recv, scr_dyn_retain(args[i])); return scr_dyn_new_num((double)recv->v.arr.len); } - if (dyn_name_is(method, "pop")) { + if ((mid == DYN_M_pop)) { if (len == 0) return scr_dyn_retain(scr_dyn_undefined()); ScrDyn *result = scr_dyn_arr_at(recv, (double)(len - 1)); if (!result) return NULL; scr_dyn_release(recv->v.arr.items[--recv->v.arr.len]); return result; } - if (dyn_name_is(method, "shift")) { + if ((mid == DYN_M_shift)) { if (len == 0) return scr_dyn_retain(scr_dyn_undefined()); if (recv->v.arr.sealed) { if (recv->v.arr.frozen) { @@ -1109,7 +1160,7 @@ static ScrDyn *scr_dyn_invoke_impl( recv->v.arr.len = len - 1; return first; /* ownership moves out */ } - if (dyn_name_is(method, "unshift")) { + if ((mid == DYN_M_unshift)) { /* Grow the backing store before shifting the old elements. The * appended placeholders are immortal, so overwriting them does not * abandon retained argument references. */ @@ -1121,7 +1172,7 @@ static ScrDyn *scr_dyn_invoke_impl( } return scr_dyn_new_num((double)recv->v.arr.len); } - if (dyn_name_is(method, "splice")) { + if ((mid == DYN_M_splice)) { size_t start = splice_start; size_t removed = splice_removed; size_t inserted = argc > 2 ? argc - 2 : 0; @@ -1151,7 +1202,7 @@ static ScrDyn *scr_dyn_invoke_impl( recv->v.arr.len = next_len; return out; } - if (dyn_name_is(method, "slice")) { + if ((mid == DYN_M_slice)) { double startD = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; double endD = dyn_index_arg(args, argc, 1, (double)len, what); @@ -1163,7 +1214,7 @@ static ScrDyn *scr_dyn_invoke_impl( for (size_t i = start; i < end; i++) scr_dyn_arr_push(out, scr_dyn_retain(recv->v.arr.items[i])); return out; } - if (dyn_name_is(method, "at")) { + if ((mid == DYN_M_at)) { double iD = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; if (!dyn_arr_length_unchanged(recv, len)) return NULL; @@ -1171,17 +1222,17 @@ static ScrDyn *scr_dyn_invoke_impl( if (idx < 0 || idx >= (double)len) return scr_dyn_retain(scr_dyn_undefined()); return scr_dyn_arr_at(recv, idx); } - if (dyn_name_is(method, "indexOf") || dyn_name_is(method, "lastIndexOf") || - dyn_name_is(method, "includes")) { - if (len == 0) return dyn_name_is(method, "includes") ? scr_dyn_new_bool(false) : scr_dyn_new_num(-1); + if ((mid == DYN_M_indexOf) || (mid == DYN_M_lastIndexOf) || + (mid == DYN_M_includes)) { + if (len == 0) return (mid == DYN_M_includes) ? scr_dyn_new_bool(false) : scr_dyn_new_num(-1); ScrDyn *needle = argc > 0 ? args[0] : scr_dyn_undefined(); - double from = dyn_name_is(method, "lastIndexOf") ? (double)len - 1 : 0; + double from = (mid == DYN_M_lastIndexOf) ? (double)len - 1 : 0; if (argc > 1) { from = dyn_index_arg(args, argc, 1, 0, what); if (scr_exc_pending()) return NULL; } if (!dyn_arr_length_unchanged(recv, len)) return NULL; - if (dyn_name_is(method, "lastIndexOf")) { + if ((mid == DYN_M_lastIndexOf)) { double first = from < 0 ? (double)len + from : from; if (first >= (double)len) first = (double)len - 1; for (size_t i = first < 0 ? 0 : (size_t)first + 1; i > 0; i--) { @@ -1193,16 +1244,16 @@ static ScrDyn *scr_dyn_invoke_impl( if (first < 0) first = 0; for (size_t i = first >= (double)len ? len : (size_t)first; i < len; i++) { const ScrDyn *item = recv->v.arr.items[i]; - bool nan_match = dyn_name_is(method, "includes") && + bool nan_match = (mid == DYN_M_includes) && item->kind == SCR_DYN_NUM && needle->kind == SCR_DYN_NUM && item->v.num != item->v.num && needle->v.num != needle->v.num; if (nan_match || scr_dyn_strict_eq(item, needle)) { - return dyn_name_is(method, "includes") ? scr_dyn_new_bool(true) : scr_dyn_new_num((double)i); + return (mid == DYN_M_includes) ? scr_dyn_new_bool(true) : scr_dyn_new_num((double)i); } } - return dyn_name_is(method, "includes") ? scr_dyn_new_bool(false) : scr_dyn_new_num(-1); + return (mid == DYN_M_includes) ? scr_dyn_new_bool(false) : scr_dyn_new_num(-1); } - if (dyn_name_is(method, "findLast") || dyn_name_is(method, "findLastIndex")) { + if ((mid == DYN_M_findLast) || (mid == DYN_M_findLastIndex)) { if (!dyn_cb_check(args, argc)) return NULL; ScrDyn *visible_recv = callback_recv ? callback_recv : recv; for (size_t i = len; i > 0; i--) { @@ -1215,16 +1266,16 @@ static ScrDyn *scr_dyn_invoke_impl( bool matched = scr_dyn_truthy(result); scr_dyn_release(result); if (matched) { - if (dyn_name_is(method, "findLast")) return item; + if ((mid == DYN_M_findLast)) return item; scr_dyn_release(item); return scr_dyn_new_num((double)index); } scr_dyn_release(item); } - return dyn_name_is(method, "findLast") + return (mid == DYN_M_findLast) ? scr_dyn_retain(scr_dyn_undefined()) : scr_dyn_new_num(-1); } - if (dyn_name_is(method, "join")) { + if ((mid == DYN_M_join)) { ScrJsonBuf b; scr_jb_init(&b); ScrStr *separator = argc > 0 && args[0]->kind != SCR_DYN_UNDEF @@ -1250,7 +1301,7 @@ static ScrDyn *scr_dyn_invoke_impl( scr_str_release(joined); return r; } - if (dyn_name_is(method, "concat")) { + if ((mid == DYN_M_concat)) { ScrDyn *out = scr_dyn_new_arr(); for (size_t i = 0; i < len; i++) { if (!dyn_arr_copy_item(out, recv, i, true)) { scr_dyn_release(out); return NULL; } @@ -1271,7 +1322,7 @@ static ScrDyn *scr_dyn_invoke_impl( } return out; } - if (dyn_name_is(method, "flat")) { + if ((mid == DYN_M_flat)) { double depth = dyn_index_arg(args, argc, 0, 1, what); if (scr_exc_pending()) return NULL; if (!dyn_arr_length_unchanged(recv, len)) return NULL; @@ -1283,7 +1334,7 @@ static ScrDyn *scr_dyn_invoke_impl( } return out; } - if (dyn_name_is(method, "reverse")) { + if ((mid == DYN_M_reverse)) { for (size_t i = 0; i < len / 2; i++) { ScrDyn *tmp = recv->v.arr.items[i]; recv->v.arr.items[i] = recv->v.arr.items[len - 1 - i]; @@ -1291,12 +1342,12 @@ static ScrDyn *scr_dyn_invoke_impl( } return scr_dyn_retain(recv); } - if (dyn_name_is(method, "toReversed")) { + if ((mid == DYN_M_toReversed)) { ScrDyn *out = scr_dyn_new_arr(); if (!dyn_arr_reversed_copy(out, recv)) { scr_dyn_release(out); return NULL; } return out; } - if (dyn_name_is(method, "with")) { + if ((mid == DYN_M_with)) { double indexD = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; if (!dyn_arr_length_unchanged(recv, len)) return NULL; @@ -1314,7 +1365,7 @@ static ScrDyn *scr_dyn_invoke_impl( } return out; } - if (dyn_name_is(method, "toSpliced")) { + if ((mid == DYN_M_toSpliced)) { double startD = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; size_t start = dyn_rel_index(startD, len); @@ -1331,7 +1382,7 @@ static ScrDyn *scr_dyn_invoke_impl( for (size_t i = start + delete_count; i < len; i++) if (!dyn_arr_copy_item(out, recv, i, false)) { scr_dyn_release(out); return NULL; } return out; } - if (dyn_name_is(method, "fill")) { + if ((mid == DYN_M_fill)) { double startD = dyn_index_arg(args, argc, 1, 0, what); if (scr_exc_pending()) return NULL; double endD = dyn_index_arg(args, argc, 2, (double)len, what); @@ -1359,7 +1410,7 @@ static ScrDyn *scr_dyn_invoke_impl( } return scr_dyn_retain(recv); } - if (dyn_name_is(method, "copyWithin")) { + if ((mid == DYN_M_copyWithin)) { double targetD = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; double startD = dyn_index_arg(args, argc, 1, 0, what); @@ -1392,13 +1443,13 @@ static ScrDyn *scr_dyn_invoke_impl( } return scr_dyn_retain(recv); } - if (dyn_name_is(method, "forEach") || dyn_name_is(method, "map") || - dyn_name_is(method, "filter") || dyn_name_is(method, "some") || - dyn_name_is(method, "every") || dyn_name_is(method, "find") || - dyn_name_is(method, "findIndex")) { + if ((mid == DYN_M_forEach) || (mid == DYN_M_map) || + (mid == DYN_M_filter) || (mid == DYN_M_some) || + (mid == DYN_M_every) || (mid == DYN_M_find) || + (mid == DYN_M_findIndex)) { if (!dyn_cb_check(args, argc)) return NULL; ScrDyn *cb = args[0]; - ScrDyn *out = (dyn_name_is(method, "map") || dyn_name_is(method, "filter")) ? scr_dyn_new_arr() : NULL; + ScrDyn *out = ((mid == DYN_M_map) || (mid == DYN_M_filter)) ? scr_dyn_new_arr() : NULL; /* Array iteration methods snapshot length before the first callback. * A shrinking callback can remove a later dense entry; an expanding * callback must not make the new tail observable to this pass. Live @@ -1413,30 +1464,30 @@ static ScrDyn *scr_dyn_invoke_impl( ScrDyn *item = scr_dyn_retain(recv->v.arr.items[i]); ScrDyn *r = dyn_call_cb(cb, item, i, visible_recv); if (!r) { scr_dyn_release(item); scr_dyn_release(out); return NULL; } - if (dyn_name_is(method, "map")) { + if ((mid == DYN_M_map)) { scr_dyn_arr_push(out, r); /* ownership moves in */ r = NULL; } else { bool truthy = scr_dyn_truthy(r); scr_dyn_release(r); - if (dyn_name_is(method, "filter") && truthy) scr_dyn_arr_push(out, scr_dyn_retain(item)); - if (dyn_name_is(method, "some") && truthy) { scr_dyn_release(item); return scr_dyn_new_bool(true); } - if (dyn_name_is(method, "every") && !truthy) { scr_dyn_release(item); return scr_dyn_new_bool(false); } - if (dyn_name_is(method, "find") && truthy) return item; /* +1 moves out */ - if (dyn_name_is(method, "findIndex") && truthy) { scr_dyn_release(item); return scr_dyn_new_num((double)i); } + if ((mid == DYN_M_filter) && truthy) scr_dyn_arr_push(out, scr_dyn_retain(item)); + if ((mid == DYN_M_some) && truthy) { scr_dyn_release(item); return scr_dyn_new_bool(true); } + if ((mid == DYN_M_every) && !truthy) { scr_dyn_release(item); return scr_dyn_new_bool(false); } + if ((mid == DYN_M_find) && truthy) return item; /* +1 moves out */ + if ((mid == DYN_M_findIndex) && truthy) { scr_dyn_release(item); return scr_dyn_new_num((double)i); } } scr_dyn_release(item); } if (out) return out; - if (dyn_name_is(method, "some")) return scr_dyn_new_bool(false); - if (dyn_name_is(method, "every")) return scr_dyn_new_bool(true); - if (dyn_name_is(method, "find")) return scr_dyn_retain(scr_dyn_undefined()); - if (dyn_name_is(method, "findIndex")) return scr_dyn_new_num(-1); + if ((mid == DYN_M_some)) return scr_dyn_new_bool(false); + if ((mid == DYN_M_every)) return scr_dyn_new_bool(true); + if ((mid == DYN_M_find)) return scr_dyn_retain(scr_dyn_undefined()); + if ((mid == DYN_M_findIndex)) return scr_dyn_new_num(-1); return scr_dyn_retain(scr_dyn_undefined()); /* forEach */ } - if (dyn_name_is(method, "reduce") || dyn_name_is(method, "reduceRight")) { + if ((mid == DYN_M_reduce) || (mid == DYN_M_reduceRight)) { if (!dyn_cb_check(args, argc)) return NULL; - bool backward = dyn_name_is(method, "reduceRight"); + bool backward = (mid == DYN_M_reduceRight); ScrDyn *acc = argc > 1 ? scr_dyn_retain(args[1]) : NULL; ScrDyn *visible_recv = callback_recv ? callback_recv : recv; for (size_t step = 0; step < len; step++) { @@ -1458,7 +1509,7 @@ static ScrDyn *scr_dyn_invoke_impl( scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); return NULL; } - if (dyn_name_is(method, "flatMap")) { + if ((mid == DYN_M_flatMap)) { /* JS Array.prototype.flatMap over a dyn array: map + a depth-1 * flatten. Native dyn-array results flatten element-by-element; a * WRAPPED engine array flattens through the routed keyed reads @@ -1496,7 +1547,7 @@ static ScrDyn *scr_dyn_invoke_impl( } return out; } - if (dyn_name_is(method, "sort")) { + if ((mid == DYN_M_sort)) { ScrDyn *cmp = argc > 0 ? args[0] : scr_dyn_undefined(); if (cmp->kind != SCR_DYN_UNDEF && cmp->kind != SCR_DYN_FUNC) { /* V8 appends the received value's string image. */ @@ -1510,7 +1561,7 @@ static ScrDyn *scr_dyn_invoke_impl( if (len > 1 && !dyn_arr_sort(recv, cmp->kind == SCR_DYN_FUNC ? cmp : NULL)) return NULL; return scr_dyn_retain(recv); } - if (dyn_name_is(method, "toSorted")) { + if ((mid == DYN_M_toSorted)) { ScrDyn *cmp = argc > 0 ? args[0] : scr_dyn_undefined(); if (cmp->kind != SCR_DYN_UNDEF && cmp->kind != SCR_DYN_FUNC) { static const char message[] = "The comparison function must be either a function or undefined"; @@ -1525,7 +1576,7 @@ static ScrDyn *scr_dyn_invoke_impl( } return out; } - if (dyn_arr_proto_unimpl(method)) { + if (dyn_arr_proto_unimpl(mid)) { dyn_throw_unsupported("Array", method); return NULL; } @@ -1541,14 +1592,14 @@ static ScrDyn *scr_dyn_invoke_impl( #ifdef SCR_LIB scr_trap("scriptc: promise dispatch is unavailable in library mode\n"); #else - if (dyn_name_is(method, "then")) { + if ((mid == DYN_M_then)) { return scr_dyn_promise_then(recv->v.promise, argc >= 1 ? args[0] : NULL, argc >= 2 ? args[1] : NULL, NULL); } - if (dyn_name_is(method, "catch")) { + if ((mid == DYN_M_catch)) { return scr_dyn_promise_then(recv->v.promise, NULL, argc >= 1 ? args[0] : NULL, NULL); } - if (dyn_name_is(method, "finally")) { + if ((mid == DYN_M_finally)) { return scr_dyn_promise_then(recv->v.promise, NULL, NULL, argc >= 1 ? args[0] : NULL); } dyn_throw_not_fn(what); @@ -1600,44 +1651,44 @@ static ScrDyn *scr_dyn_invoke_impl( } /* The typed array methods below are not DataView's; it inherits * Object.prototype's. */ - if (dyn_name_is(method, "toString") || dyn_name_is(method, "toLocaleString")) { + if ((mid == DYN_M_toString) || (mid == DYN_M_toLocaleString)) { ScrStr *tag = scr_str_new("[object DataView]", 17); ScrDyn *out = scr_dyn_new_str(tag); scr_str_release(tag); return out; } - if (dyn_name_is(method, "valueOf")) return scr_dyn_retain(recv); + if ((mid == DYN_M_valueOf)) return scr_dyn_retain(recv); dyn_throw_not_fn(what); return NULL; } size_t blen = bytes->len; - if (dyn_name_is(method, "at")) { + if ((mid == DYN_M_at)) { double iD = dyn_bytes_index_arg(args, argc, 0, 0); if (scr_exc_pending()) return NULL; double idx = iD < 0 ? (double)blen + iD : iD; if (idx < 0 || idx >= (double)blen) return scr_dyn_retain(scr_dyn_undefined()); return scr_dyn_new_num(scr_bytes_get(bytes, idx)); } - if (dyn_name_is(method, "slice") || dyn_name_is(method, "subarray")) { + if ((mid == DYN_M_slice) || (mid == DYN_M_subarray)) { /* subarray and Buffer.slice alias their source. TypedArray.slice * owns an independent copy; all keep the receiver's Buffer flavor. */ double startD = dyn_bytes_index_arg(args, argc, 0, 0); if (scr_exc_pending()) return NULL; double endD = dyn_bytes_index_arg(args, argc, 1, (double)blen); if (scr_exc_pending()) return NULL; - ScrBytes *out = recv->buffer || dyn_name_is(method, "subarray") + ScrBytes *out = recv->buffer || (mid == DYN_M_subarray) ? scr_bytes_subarray(bytes, startD, endD) : scr_bytes_slice(bytes, startD, endD); ScrDyn *d = recv->buffer ? scr_dyn_new_buffer(out) : scr_dyn_new_bytes(out); scr_bytes_release(out); return d; } - if (dyn_name_is(method, "set")) { + if ((mid == DYN_M_set)) { double offset = dyn_bytes_index_arg(args, argc, 1, 0); if (scr_exc_pending()) return NULL; scr_bytes_set_from_dyn(bytes, argc ? args[0] : scr_dyn_undefined(), offset); return scr_exc_pending() ? NULL : scr_dyn_retain(scr_dyn_undefined()); } - if (dyn_name_is(method, "copyWithin")) { + if ((mid == DYN_M_copyWithin)) { double target = dyn_bytes_index_arg(args, argc, 0, 0); if (scr_exc_pending()) return NULL; double start = dyn_bytes_index_arg(args, argc, 1, 0); @@ -1647,7 +1698,7 @@ static ScrDyn *scr_dyn_invoke_impl( scr_bytes_release(scr_bytes_copy_within(bytes, target, start, end)); return scr_dyn_retain(recv); } - if (dyn_name_is(method, "fill") && !recv->buffer) { + if ((mid == DYN_M_fill) && !recv->buffer) { double value; if (!scr_dyn_number_coerce_js(argc ? args[0] : scr_dyn_undefined(), &value)) return NULL; double start = dyn_bytes_index_arg(args, argc, 1, 0); @@ -1657,8 +1708,8 @@ static ScrDyn *scr_dyn_invoke_impl( scr_bytes_release(scr_bytes_fill_elem(bytes, value, start, end)); return scr_dyn_retain(recv); } - if (dyn_name_is(method, "join") || (dyn_name_is(method, "toString") && !recv->buffer)) { - ScrStr *separator = dyn_name_is(method, "join") && argc && args[0]->kind != SCR_DYN_UNDEF + if ((mid == DYN_M_join) || ((mid == DYN_M_toString) && !recv->buffer)) { + ScrStr *separator = (mid == DYN_M_join) && argc && args[0]->kind != SCR_DYN_UNDEF ? scr_dyn_string_coerce_js(args[0]) : scr_str_new(",", 1); if (scr_exc_pending()) { scr_str_release(separator); return NULL; } ScrStr *joined = scr_bytes_join(bytes, separator); @@ -1667,7 +1718,7 @@ static ScrDyn *scr_dyn_invoke_impl( scr_str_release(joined); return out; } - if (dyn_bytes_proto_real(method)) { + if (dyn_bytes_proto_real(mid)) { dyn_throw_unsupported(scr_bytes_elem_name(bytes->elem), method); return NULL; } @@ -1854,9 +1905,12 @@ static const char dyn_s29[] SCR_DYN_COMPAT_STR = "Date"; static const char dyn_s30[] SCR_DYN_COMPAT_STR = "Map"; static const char dyn_s31[] SCR_DYN_COMPAT_STR = "Set"; static const char dyn_s32[] SCR_DYN_COMPAT_STR = ""; +static const char dyn_s33[] SCR_DYN_COMPAT_STR = "constructing %s%s through a stored value is not supported yet"; +static const char dyn_s34[] SCR_DYN_COMPAT_STR = " from a primitive"; +static const char dyn_s35[] SCR_DYN_COMPAT_STR = "FinalizationRegistry"; static const char *const dyn_global_ctor_names[] = { - dyn_s26, dyn_s29, dyn_s25, dyn_s30, dyn_s31, dyn_s21, dyn_s22, dyn_s24, + dyn_s26, dyn_s29, dyn_s25, dyn_s30, dyn_s31, dyn_s21, dyn_s22, dyn_s24, dyn_s35, }; #define DYN_GLOBAL_CTORS (sizeof dyn_global_ctor_names / sizeof dyn_global_ctor_names[0]) static SCR_TL ScrDyn *dyn_global_ctors[DYN_GLOBAL_CTORS]; @@ -1887,17 +1941,47 @@ static ScrDyn *dyn_global_ctor_call(ScrClosure *closure, ScrDyn *const *args, si return NULL; } +/* `new C(...args)` through a stored Object, Date, Map, Set, WeakMap or + * WeakSet builds what the `new` expression builds. RegExp, Promise, and + * Object over a primitive (a wrapper object) are fenced. */ +static bool dyn_global_construct(const ScrDyn *callee, const ScrDyn *args, ScrDyn **result) { + if (callee->kind != SCR_DYN_FUNC || callee->v.fn.thunk != dyn_global_ctor_call) return false; + const char *name = callee->v.fn.clo->fn; + ScrDyn *const *items = args->v.arr.items; + size_t argc = args->v.arr.len; + const ScrDyn *first = argc ? items[0] : scr_dyn_undefined(); + bool primitive = first->kind == SCR_DYN_NUM || first->kind == SCR_DYN_STR || first->kind == SCR_DYN_BOOL || + first->kind == SCR_DYN_BIGINT || first->kind == SCR_DYN_SYMBOL; + if (strcmp(name, dyn_s26) == 0 && !primitive) *result = dyn_global_ctor_call(callee->v.fn.clo, items, argc); + else if (strcmp(name, dyn_s29) == 0) + *result = argc == 0 ? scr_dyn_native_date_new(args) + : argc == 1 ? scr_dyn_native_date_new_value(args) + : scr_dyn_native_date_new_fields(args); + else if (strcmp(name, dyn_s30) == 0) *result = scr_dyn_native_map_new(first); + else if (strcmp(name, dyn_s31) == 0) *result = scr_dyn_native_set_new(first); + else if (strcmp(name, dyn_s22) == 0) *result = scr_weak_map_new((ScrDyn *)first); + else if (strcmp(name, dyn_s24) == 0) *result = scr_weak_set_new((ScrDyn *)first); + else { + char message[128]; + int length = snprintf(message, sizeof message, dyn_s33, name, strcmp(name, dyn_s26) == 0 ? dyn_s34 : dyn_s32); + scr_throw_error_msg_code(SCR_ERR_ERROR, message, (size_t)length, dyn_s27); + *result = NULL; + } + return true; +} + static ScrDyn *dyn_global_ctor(const char *name) { for (size_t i = 0; i < DYN_GLOBAL_CTORS; i++) { if (strcmp(dyn_global_ctor_names[i], name) != 0) continue; if (!dyn_global_ctors_registered) { dyn_global_ctors_registered = true; scr_atexit(dyn_global_ctors_cleanup); + scr_dyn_global_construct_fn = dyn_global_construct; } if (!dyn_global_ctors[i]) { /* Each constructor's own length: Object 1, Date 7, RegExp 2, Promise 1, - * the collections 0. */ - uint32_t length = i == 0 || i == 5 ? 1 : i == 1 ? 7 : i == 2 ? 2 : 0; + * the collections 0, FinalizationRegistry 1. */ + uint32_t length = i == 0 || i == 5 || i == 8 ? 1 : i == 1 ? 7 : i == 2 ? 2 : 0; dyn_global_ctors[i] = scr_dyn_new_func(scr_closure_new((void *)dyn_global_ctor_names[i], 0), dyn_global_ctor_call, length, dyn_s6, dyn_global_ctor_names[i]); diff --git a/packages/runtime/src/scr_dyn_methods.h b/packages/runtime/src/scr_dyn_methods.h new file mode 100644 index 000000000..49e5c7254 --- /dev/null +++ b/packages/runtime/src/scr_dyn_methods.h @@ -0,0 +1,160 @@ +/* Generated by scripts/gen-dyn-methods.mjs from the DYN_M_ ids scr_dyn_invoke.c + * uses. Do not edit by hand. */ +#ifndef SCR_DYN_METHODS_H +#define SCR_DYN_METHODS_H + +#include +#include + +enum { + DYN_M_NONE, + DYN_M_apply, + DYN_M_at, + DYN_M_bind, + DYN_M_call, + DYN_M_catch, + DYN_M_concat, + DYN_M_copyWithin, + DYN_M_entries, + DYN_M_every, + DYN_M_fill, + DYN_M_filter, + DYN_M_finally, + DYN_M_find, + DYN_M_findIndex, + DYN_M_findLast, + DYN_M_findLastIndex, + DYN_M_flat, + DYN_M_flatMap, + DYN_M_forEach, + DYN_M_includes, + DYN_M_indexOf, + DYN_M_join, + DYN_M_keys, + DYN_M_lastIndexOf, + DYN_M_map, + DYN_M_pop, + DYN_M_push, + DYN_M_reduce, + DYN_M_reduceRight, + DYN_M_reverse, + DYN_M_set, + DYN_M_shift, + DYN_M_slice, + DYN_M_some, + DYN_M_sort, + DYN_M_splice, + DYN_M_subarray, + DYN_M_then, + DYN_M_toExponential, + DYN_M_toFixed, + DYN_M_toLocaleString, + DYN_M_toReversed, + DYN_M_toSorted, + DYN_M_toSpliced, + DYN_M_toString, + DYN_M_unshift, + DYN_M_valueOf, + DYN_M_values, + DYN_M_with, + DYN_M_COUNT +}; + +static const char *const dyn_method_names[DYN_M_COUNT] = { + NULL, + "apply", + "at", + "bind", + "call", + "catch", + "concat", + "copyWithin", + "entries", + "every", + "fill", + "filter", + "finally", + "find", + "findIndex", + "findLast", + "findLastIndex", + "flat", + "flatMap", + "forEach", + "includes", + "indexOf", + "join", + "keys", + "lastIndexOf", + "map", + "pop", + "push", + "reduce", + "reduceRight", + "reverse", + "set", + "shift", + "slice", + "some", + "sort", + "splice", + "subarray", + "then", + "toExponential", + "toFixed", + "toLocaleString", + "toReversed", + "toSorted", + "toSpliced", + "toString", + "unshift", + "valueOf", + "values", + "with", +}; + +static const unsigned char dyn_method_lengths[DYN_M_COUNT] = { + 0, 5, 2, 4, 4, 5, 6, 10, 7, 5, 4, 6, 7, 4, 9, 8, + 13, 4, 7, 7, 8, 7, 4, 4, 11, 3, 3, 4, 6, 11, 7, 3, + 5, 5, 4, 4, 6, 8, 4, 13, 7, 14, 10, 8, 9, 8, 7, 7, + 6, 4, +}; + +/* Each name's id at its slot (or the next free one); 0 ends a probe. */ +static const unsigned char dyn_method_slots[256] = { + 0, 0, 0, 0, 12, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 14, 0, 0, 27, 26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 28, 3, 0, + 22, 0, 20, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 0, + 0, 0, 0, 0, 0, 33, 31, 0, 0, 0, 0, 0, 36, 35, 0, 0, + 0, 0, 0, 0, 23, 4, 32, 0, 5, 0, 0, 0, 0, 0, 0, 29, + 0, 0, 0, 0, 0, 0, 38, 0, 0, 0, 0, 6, 0, 0, 37, 0, + 0, 40, 0, 0, 0, 0, 0, 0, 43, 0, 0, 45, 0, 0, 0, 44, + 0, 7, 0, 0, 0, 0, 42, 0, 25, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, + 46, 0, 0, 39, 0, 41, 0, 9, 0, 0, 13, 0, 0, 0, 0, 8, + 0, 47, 10, 0, 0, 0, 0, 48, 0, 0, 17, 0, 0, 49, 0, 0, + 0, 0, 0, 19, 0, 0, 11, 0, 0, 0, 0, 18, 0, 0, 0, 0, +}; + +#define DYN_METHOD_MAX_LENGTH 14 + +/* The id of a method name, compared by content; DYN_M_NONE for any other + * name. */ +static inline int dyn_method_of(const char *m) { + size_t length = strlen(m); + if (length == 0 || length > DYN_METHOD_MAX_LENGTH) return DYN_M_NONE; + unsigned slot = (unsigned)(length * 7 + (unsigned char)m[0] * 31 + + (unsigned char)m[length - 1]) & 255; + for (;;) { + unsigned id = dyn_method_slots[slot]; + if (id == 0) return DYN_M_NONE; + if (dyn_method_lengths[id] == length && memcmp(m, dyn_method_names[id], length) == 0) return (int)id; + slot = (slot + 1) & 255; + } +} + +#endif diff --git a/packages/runtime/src/scr_dyn_methods.test.ts b/packages/runtime/src/scr_dyn_methods.test.ts new file mode 100644 index 000000000..77911aa2d --- /dev/null +++ b/packages/runtime/src/scr_dyn_methods.test.ts @@ -0,0 +1,10 @@ +import { execFileSync } from "node:child_process"; +import { join } from "node:path"; +import { expect, test } from "vitest"; + +test("scr_dyn_methods.h lists every method id the checked-dynamic dispatch compares", () => { + const script = join(import.meta.dirname, "../../../scripts/gen-dyn-methods.mjs"); + expect(() => + execFileSync(process.execPath, [script, "--check"], { stdio: "pipe" }), + ).not.toThrow(); +}); diff --git a/packages/runtime/src/scr_error.h b/packages/runtime/src/scr_error.h index a7c921f23..fbb64aad9 100644 --- a/packages/runtime/src/scr_error.h +++ b/packages/runtime/src/scr_error.h @@ -296,6 +296,9 @@ void scr_error_assign_dyn(ScrError *e, const struct ScrDyn *sources); /* `key in err` for a key no compiled class declares: a runtime-set own * property or an Object.prototype member. Borrows both (scr_json.c). */ bool scr_error_has_key(ScrError *e, const ScrStr *key); +bool scr_error_own_accessor(ScrError *e, const ScrStr *key); +struct ScrDyn *scr_error_accessor_get(ScrError *e, const ScrStr *key); +void scr_error_accessor_set(ScrError *e, const ScrStr *key, struct ScrDyn *value); bool scr_error_has_code(ScrError *e); const char *scr_error_message_code(const ScrStr *msg); ScrStr *scr_error_code(ScrError *e); diff --git a/packages/runtime/src/scr_fetch.c b/packages/runtime/src/scr_fetch.c index 735183b89..7cc2beef4 100644 --- a/packages/runtime/src/scr_fetch.c +++ b/packages/runtime/src/scr_fetch.c @@ -44,7 +44,9 @@ typedef struct SfChunk { } SfChunk; struct SfReadRequest { - ScrPromise *promise; + ScrPromise *promise; /* or NULL for a sink request */ + const ScrFetchReadSink *sink; /* readableStreamDefaultReaderRead's read request */ + void *sink_ctx; SfReadRequest *next; }; @@ -776,6 +778,11 @@ static SfReadRequest *sf_reader_take_request(SfReader *r) { static void sf_reader_reject_message_all(SfReader *r, const char *message) { SfReadRequest *request; while ((request = sf_reader_take_request(r)) != NULL) { + if (request->sink) { + request->sink->error(request->sink_ctx, NULL); + free(request); + continue; + } sf_type_error(message); scr_promise_reject_pending(request->promise); scr_promise_release(request->promise); @@ -958,6 +965,12 @@ static void sf_reader_fulfill_one(SfReader *r, bool done, ScrDyn *value) { if (!r) return; SfReadRequest *request = sf_reader_take_request(r); if (!request) return; + if (request->sink) { + if (done) request->sink->close(request->sink_ctx); + else request->sink->chunk(request->sink_ctx, value); + free(request); + return; + } scr_promise_fulfill_ref(request->promise, sf_read_result(done, value), &scr_dyn_retain_v, &scr_dyn_release_v, NULL); scr_promise_release(request->promise); @@ -968,6 +981,11 @@ static void sf_reader_reject_all(SfReader *r, ScrDyn *reason) { if (!r) return; SfReadRequest *request; while ((request = sf_reader_take_request(r)) != NULL) { + if (request->sink) { + request->sink->error(request->sink_ctx, reason); + free(request); + continue; + } sf_reject_promise_reason(request->promise, reason); scr_promise_release(request->promise); free(request); @@ -1654,18 +1672,21 @@ static SfReader *sf_stream_get_reader(SfStream *s) { return r; } -static ScrPromise *sf_reader_read(SfReader *r) { - ScrPromise *p = scr_promise_new(); +/* readableStreamDefaultReaderRead: queue the request, then serve it from + * the queue or a pull. false (TypeError pending) for a released reader. */ +static bool sf_reader_read_request(SfReader *r, ScrPromise *p, const ScrFetchReadSink *sink, + void *sink_ctx) { SfStream *s = r->stream; if (!s || s->reader != r) { sf_type_error("This reader has been released"); - scr_promise_reject_pending(p); - return p; + return false; } s->disturbed = true; SfReadRequest *request = calloc(1, sizeof *request); if (!request) sf_oom(); - request->promise = scr_promise_retain(p); + request->promise = p ? scr_promise_retain(p) : NULL; + request->sink = sink; + request->sink_ctx = sink_ctx; if (r->pending_tail) r->pending_tail->next = request; else r->pending_head = request; r->pending_tail = request; @@ -1674,6 +1695,12 @@ static ScrPromise *sf_reader_read(SfReader *r) { sf_stream_pull(s); sf_stream_drain(s); } + return true; +} + +static ScrPromise *sf_reader_read(SfReader *r) { + ScrPromise *p = scr_promise_new(); + if (!sf_reader_read_request(r, p, NULL, NULL)) scr_promise_reject_pending(p); return p; } @@ -1960,6 +1987,15 @@ ScrDyn *scr_fetch_stream_from_string(ScrStr *iterable) { return out; } +bool scr_fetch_reader_read_into(ScrDyn *reader, const ScrFetchReadSink *sink, void *ctx) { + if (!reader || reader->kind != SCR_DYN_HANDLE || + reader->v.handle.tag != SCR_DYNH_WEB_READER) { + sf_type_error("Expected a ReadableStreamDefaultReader"); + return false; + } + return sf_reader_read_request((SfReader *)reader->v.handle.ptr, NULL, sink, ctx); +} + ScrPromise *scr_fetch_reader_read(ScrDyn *reader) { if (!reader || reader->kind != SCR_DYN_HANDLE || reader->v.handle.tag != SCR_DYNH_WEB_READER) { @@ -2455,11 +2491,44 @@ static bool sf_headers_sorted_entry(SfHeaders *h, size_t index, return found; } +/* One step of a live Headers iterator (entries, keys or values): the + * `*index`th entry of the current sorted list, as Node's iterator re-reads + * the list on every step. The result is { value, done }. */ +static ScrDyn *sf_headers_step(void *ptr, double *index, unsigned selection) { + ScrStr *name = NULL; + ScrStr *value = NULL; + bool found = sf_headers_sorted_entry(ptr, (size_t)*index, &name, &value); + ScrDyn *item; + if (found) { + (*index)++; + if (selection == 2) { + item = scr_dyn_new_arr(); + scr_dyn_arr_push(item, scr_dyn_new_str(name)); + scr_dyn_arr_push(item, scr_dyn_new_str(value)); + } else { + item = scr_dyn_new_str(selection == 1 ? name : value); + } + scr_str_release(name); + scr_str_release(value); + } else { + item = scr_dyn_retain(scr_dyn_undefined()); + } + ScrDyn *result = scr_dyn_new_obj(); + scr_dyn_obj_set(result, "value", 5, item); + scr_dyn_obj_set(result, "done", 4, scr_dyn_new_bool(!found)); + return result; +} + static ScrDyn *sf_headers_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what) { SfHeaders *h = ptr; + if (strcmp(method, "entries") == 0 || strcmp(method, "keys") == 0 || + strcmp(method, "values") == 0) { + return scr_dyn_native_handle_iterator( + self, strcmp(method, "entries") == 0 ? 2 : strcmp(method, "keys") == 0 ? 1 : 0); + } if (strcmp(method, "get") == 0 || strcmp(method, "has") == 0) { if (argc < 1) { sf_type_error("Headers.get/has requires a header name"); @@ -5451,7 +5520,8 @@ static const ScrDynHandleOps sf_response_ops = { &sf_response_invoke, &sf_response_get, &sf_no_set, NULL}; static const ScrDynHandleOps sf_headers_ops = { "Headers", &sf_headers_retain_v, &sf_headers_release_v, - &sf_headers_invoke, &sf_headers_get, &sf_no_set, NULL}; + &sf_headers_invoke, &sf_headers_get, &sf_no_set, NULL, NULL, + &sf_headers_step}; static void sf_teardown(void) { while (sf_live) { diff --git a/packages/runtime/src/scr_generator.h b/packages/runtime/src/scr_generator.h index c4397e021..c3dac861d 100644 --- a/packages/runtime/src/scr_generator.h +++ b/packages/runtime/src/scr_generator.h @@ -200,5 +200,17 @@ ScrDyn *scr_fetch_stream_from_bytes( ScrDyn *scr_fetch_stream_from_string( ScrStr *iterable); /* borrowed string; +1 handle */ ScrPromise *scr_fetch_reader_read(ScrDyn *reader); /* borrowed handle; +1 */ +/* A read request served by callbacks instead of a promise (the streams + * spec's readableStreamDefaultReaderRead with a read request; Node's + * Readable.fromWeb adapter uses one). chunk and error borrow their value; + * error's reason is NULL when the reader was released. */ +typedef struct ScrFetchReadSink { + void (*chunk)(void *ctx, ScrDyn *value); + void (*close)(void *ctx); + void (*error)(void *ctx, ScrDyn *reason); +} ScrFetchReadSink; +/* Queue a sink read request on a reader handle (borrowed). false with a + * TypeError pending for a value that is not a reader or a released one. */ +bool scr_fetch_reader_read_into(ScrDyn *reader, const ScrFetchReadSink *sink, void *ctx); #endif /* SCR_GENERATOR_H */ diff --git a/packages/runtime/src/scr_http.c b/packages/runtime/src/scr_http.c index a5847e59b..cb88297bf 100644 --- a/packages/runtime/src/scr_http.c +++ b/packages/runtime/src/scr_http.c @@ -4323,14 +4323,32 @@ static void scr_http_client_end_raw(ScrHttpClientReq *c, const char *data, size_ } if (!c->head_sent) { /* whole body known NOW: Content-Length framing (or none — the head - * serializer's empty-body method split) */ + * serializer's empty-body method split). Head and body go out as one + * write, as Node sends them: on a connected (reused) socket two writes + * would be two segments, and the server could answer before the body + * arrives. */ + c->hold_head = true; scr_http_client_send_head(c, (long long)len); + c->hold_head = false; if (scr_exc_pending()) return; if (c->chunked) { scr_http_client_write_raw(c, data, len); if (c->sock) scr_http_send_chunk_end(c->sock, &c->trailers); - } else if (c->sock && len > 0) { - scr_net_sock_write_native(c->sock, data, len); + } else if (c->held_head != NULL) { + char *head = c->held_head; + size_t head_len = c->held_head_len; + c->held_head = NULL; + c->held_head_len = c->held_head_cap = 0; + if (c->sock) { + if (len > 0) { + char *joined = realloc(head, head_len + len); + if (!joined) scr_http_oom(); + head = joined; + memcpy(head + head_len, data, len); + } + scr_net_sock_write_native(c->sock, head, head_len + len); + } + free(head); } } else { scr_http_client_write_raw(c, data, len); diff --git a/packages/runtime/src/scr_inspect.c b/packages/runtime/src/scr_inspect.c index a4d124ac3..1dce8d3c2 100644 --- a/packages/runtime/src/scr_inspect.c +++ b/packages/runtime/src/scr_inspect.c @@ -992,6 +992,15 @@ ScrStr *scr_insp_error(ScrError *e, double recurse, double depth) { /* Property-name rendering: bare when the key matches Node's keyStrRegExp * (/^[a-zA-Z_][a-zA-Z_0-9]*$/, '__proto__' excepted), quoted otherwise. */ +/* util.inspect's text for an accessor property: [Getter], [Setter] or + * [Getter/Setter], by which of the two are functions. +1. */ +static ScrStr *insp_accessor_tag(const ScrDynEntry *ent) { + bool get = ent->getter && ent->getter->kind == SCR_DYN_FUNC; + bool set = ent->setter && ent->setter->kind == SCR_DYN_FUNC; + const char *text = get && set ? "[Getter/Setter]" : get ? "[Getter]" : set ? "[Setter]" : "undefined"; + return scr_str_new(text, strlen(text)); +} + static void insp_key_into(InspBuf *b, const char *key, size_t len) { if (len == 9 && memcmp(key, "__proto__", 9) == 0) { ib_cstr(b, "['__proto__']"); @@ -1499,14 +1508,20 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { return scr_insp_str(d->v.str); case SCR_DYN_ARR: { size_t len = d->v.arr.len; - if (len == 0) return scr_str_new("[]", 2); + /* Named and noncanonical numeric properties follow the elements as + * `key: value` entries (Node's array extras). */ + const ScrDyn *props = d->v.arr.properties; + size_t nprops = 0; + for (size_t i = 0; props && i < props->v.obj.len; i++) nprops += props->v.obj.entries[i].enumerable; + if (len == 0 && nprops == 0) return scr_str_new("[]", 2); if (recurse > depth) return scr_str_new("[Array]", 7); scr_insp_begin(recurse + 1); /* Node's maxArrayLength (100) counts output entries; a run of holes * is one `` entry (formatSpecialArray). */ const unsigned char *presence = d->v.arr.presence; size_t i = 0; - for (size_t shown = 0; i < len && shown < 100; shown++) { + size_t shown = 0; + for (; i < len && shown < 100; shown++) { if (presence && !presence[i]) { size_t next = i + 1; while (next < len && !presence[next]) next++; @@ -1524,15 +1539,38 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { scr_str_release(s); } bool more = i < len; + size_t entries = shown + (more ? 1 : 0); if (more) { ScrStr *m = scr_insp_more_items((double)(len - i)); scr_insp_entry(m, d->v.arr.items[i]->kind == SCR_DYN_NUM); scr_str_release(m); } + for (size_t k = 0; props && k < props->v.obj.len; k++) { + const ScrDynEntry *ent = &props->v.obj.entries[k]; + if (!ent->enumerable) continue; + ScrStr *val = ent->accessor ? insp_accessor_tag(ent) : scr_insp_dyn(ent->value, recurse + 1, depth); + InspBuf eb = {0}; + insp_key_into(&eb, ent->key, ent->key_len); + ib_cstr(&eb, ": "); + ib_bytes(&eb, val->data, val->len); + scr_str_release(val); + ScrStr *entry = ib_take(&eb); + /* Node's grouping pads by the array element at the entry's output + * position (value[i] in groupArrayElements), not by the entry. */ + bool number = entries < len && (!presence || presence[entries]) && + d->v.arr.items[entries]->kind == SCR_DYN_NUM; + scr_insp_entry(entry, number); + scr_str_release(entry); + entries++; + } + /* Node's grouping leaves out the last entry when there are more + * entries than maxArrayLength (the `... more items` entry when the + * array has no properties). */ + bool last_out = entries > 100; ScrStr *base = scr_str_new("", 0); ScrStr *b0 = scr_str_new("[", 1); ScrStr *b1 = scr_str_new("]", 1); - ScrStr *out = scr_insp_end(base, b0, b1, recurse + 1, true, more); + ScrStr *out = scr_insp_end(base, b0, b1, recurse + 1, true, last_out); scr_str_release(base); scr_str_release(b0); scr_str_release(b1); diff --git a/packages/runtime/src/scr_itable.c b/packages/runtime/src/scr_itable.c index a333aab95..25a074f49 100644 --- a/packages/runtime/src/scr_itable.c +++ b/packages/runtime/src/scr_itable.c @@ -472,7 +472,7 @@ static ScrArrState scr_itable_element(ScrITableElements *w, size_t i, uint64_t * } if (i < a->cap) { *slot = a->data[i]; - return (ScrArrState)*scr_arr_state_ptr(a, i); + return (ScrArrState)scr_arr_dense_state(a, i); } if (scr_arr_ext_of(a)) { while (w->sparse < scr_arr_ext_of(a)->sparse_len && scr_arr_ext_of(a)->sparse[w->sparse].index < i) w->sparse++; diff --git a/packages/runtime/src/scr_itable.test.c b/packages/runtime/src/scr_itable.test.c index f6d98654a..c85370add 100644 --- a/packages/runtime/src/scr_itable.test.c +++ b/packages/runtime/src/scr_itable.test.c @@ -55,6 +55,7 @@ static ScrArr *make_f64s(const double *values, size_t n) { a->filled = (uint32_t)n; a->elem = SCR_ELEM_F64; /* Values, then one state byte per slot (scr_arr_states_end). */ + a->spare = SCR_ARR_STATES; a->data = calloc(n ? n : 1, sizeof(uint64_t) + 1); for (size_t i = 0; i < n; i++) memcpy(&a->data[i], &values[i], sizeof(double)); memset(scr_arr_state_run(a, 0, n), SCR_ARR_VALUE, n); @@ -170,6 +171,7 @@ int main(void) { assert(scr_itable_size(lists) == 4); ScrArr *big = make_f64s(NULL, 0); big->len = big->cap = big->filled = 100; + big->spare = 0; /* values without states */ free(big->data); big->data = calloc(100, sizeof(uint64_t)); for (unsigned i = 0; i < 100; i++) memcpy(&big->data[i], &(double){i}, sizeof(double)); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 1c85a928a..faed47129 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -189,6 +189,19 @@ void scr_dyn_install_async_iterator_symbol(ScrDyn *key) { scr_atexit(scr_async_iterator_symbol_cleanup); } +/* Symbol.hasInstance, installed when the program first evaluates it: a + * function can only define a hook after reading the symbol. */ +static SCR_TL ScrDyn *scr_has_instance_symbol; +static void scr_has_instance_symbol_cleanup(void) { + scr_dyn_release(scr_has_instance_symbol); + scr_has_instance_symbol = NULL; +} +void scr_dyn_install_has_instance_symbol(ScrDyn *key) { + if (scr_has_instance_symbol) return; + scr_has_instance_symbol = scr_dyn_retain(key); + scr_atexit(scr_has_instance_symbol_cleanup); +} + void scr_dyn_install_iterator_symbol(ScrDyn *key) { if (scr_iterator_symbol) return; scr_iterator_symbol = scr_dyn_retain(key); @@ -1021,6 +1034,7 @@ static ScrDyn *scr_dyn_alloc(ScrDynKind kind) { } else if (kind == SCR_DYN_OBJ) { d->v.obj.len = 0; /* cap/entries preserved */ d->v.obj.region = NULL; + d->v.obj.class_name = NULL; /* a released class prototype's name */ SCR_JS_SHAPE_RESET(d); if (d->v.obj.cap <= SCR_DYN_KEY_CACHE_SLOTS) { for (size_t i = 0; i < d->v.obj.cap; i++) { @@ -1556,6 +1570,50 @@ static ScrDyn *scr_dyn_not_iterable(const ScrDyn *src, const ScrStr *msg) { return NULL; } +static ScrDyn *scr_native_iterator_drain(const ScrDyn *src); + +/* Math.max(...src) / Math.min(...src) over an untyped iterable: every + * value's ToNumber runs in order, then NaN poisons and ±0 orders as in + * Node. A non-iterable source throws V8's call-spread TypeError: `what` + * names the source for null and undefined, every other kind gets the + * Symbol.iterator wording. NaN with the exception pending when packing or + * a coercion throws. */ +double scr_dyn_math_fold(const ScrDyn *src, bool max, const ScrStr *what) { + if (src->kind == SCR_DYN_UNDEF || src->kind == SCR_DYN_NULL) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_write(&b, what->data, what->len); + scr_jb_puts(&b, src->kind == SCR_DYN_NULL ? " is not iterable (cannot read property null)" + : " is not iterable (cannot read property undefined)"); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b)); + return NAN; + } + static const char spread_msg[] = "Spread syntax requires ...iterable[Symbol.iterator] to be a function"; + ScrStr *msg = scr_str_new(spread_msg, sizeof spread_msg - 1); + /* The spread takes every value before the call: arrays, strings, bytes + * and collections pack, a native iterator (a Map's keys()) drains, and + * other iterables (a generator, an object with a Symbol.iterator method) + * step their iterator to the end. */ + ScrDyn *pack = src->kind == SCR_DYN_HANDLE && (src->v.handle.tag == SCR_DYNH_ITERATOR || + scr_dyn_handle_ops_of(src)->iter_step) + ? scr_native_iterator_drain(src) + : scr_dyn_destructure_rest(src, msg); + scr_str_release(msg); + if (!pack) return NAN; + double best = max ? -INFINITY : INFINITY; + bool nan = false; + for (size_t i = 0; i < pack->v.arr.len; i++) { + double v = scr_dyn_number_coerce(pack->v.arr.items[i]); + if (scr_exc_pending()) break; + if (isnan(v)) nan = true; + else if (max ? (v > best || (v == 0.0 && best == 0.0 && !signbit(v))) + : (v < best || (v == 0.0 && best == 0.0 && signbit(v)))) + best = v; + } + scr_dyn_release(pack); + return nan ? NAN : best; +} + ScrDyn *scr_dyn_iter_pack(const ScrDyn *src, const ScrStr *msg) { if (src->kind == SCR_DYN_HANDLE && scr_dyn_handle_ops_of(src)->iter_pack) return scr_dyn_handle_ops_of(src)->iter_pack(src->v.handle.ptr); @@ -1577,6 +1635,20 @@ ScrDyn *scr_dyn_iter_pack(const ScrDyn *src, const ScrStr *msg) { static ScrDyn *scr_native_iterator_drain(const ScrDyn *src); +/* The values a collection constructor's iterable seed gives, in order: +1 + * array, or NULL with the exception pending. A native iterator (a Map's + * keys(), a Set's values()) drains, which runs no program code; arrays, + * strings, typed arrays and collections pack; other iterables (a + * generator, an object with a Symbol.iterator method) step their + * iterator to the end, so their values are all taken before the + * constructor adds the first one. */ +static ScrDyn *scr_dyn_seed_values(const ScrDyn *src) { + if (src->kind == SCR_DYN_HANDLE && + (src->v.handle.tag == SCR_DYNH_ITERATOR || scr_dyn_handle_ops_of(src)->iter_step)) + return scr_native_iterator_drain(src); + return scr_dyn_destructure_rest(src, NULL); +} + /* The for-of-over-dyn pack accessors: the emitted index loop drives them * over a scr_dyn_iter_pack result (ARR by construction — the defensive * arms cover nothing reachable from that lowering). Never throw. */ @@ -1592,18 +1664,10 @@ ScrDyn *scr_dyn_map_seed_entries(const ScrDyn *src) { if (scr_exc_pending()) return NULL; } /* A native iterator (sp.entries(), a Map's entries()) or a handle that - * steps through live entries (URLSearchParams): producing its entries - * runs no program code, so draining them first is what the consumer - * observes. */ - if (src->kind == SCR_DYN_HANDLE && - (src->v.handle.tag == SCR_DYNH_ITERATOR || scr_dyn_handle_ops_of(src)->iter_step)) - return scr_native_iterator_drain(src); - if (src->kind == SCR_DYN_HANDLE || src->kind == SCR_DYN_TYPED_REF) { - static const char message[] = "new Map(entries) over native non-array iterables has no lowering"; - scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); - return NULL; - } - return scr_dyn_iter_pack(src, NULL); + * steps through live entries (URLSearchParams) drains; a Set, a + * generator or another iterable gives its values in order + * (scr_dyn_seed_values). */ + return scr_dyn_seed_values(src); } ScrDyn *scr_dyn_map_seed_entry(const ScrDyn *entry) { @@ -1863,6 +1927,25 @@ ScrDyn *scr_dyn_error_constructor(const char *name) { return NULL; } +/* `Error.stackTraceLimit` read or written through the Error constructor + * held as a value (zod's `const E = Error; E.stackTraceLimit = 0`): the + * same limit the static member reads and stack capture honors. */ +static bool scr_dyn_is_stack_limit(const ScrDyn *d, const char *key, size_t key_len) { + return key_len == 15 && memcmp(key, "stackTraceLimit", 15) == 0 && d->kind == SCR_DYN_FUNC && + scr_error_ctor_values[0] && d->v.fn.clo == scr_error_ctor_values[0]->v.fn.clo; +} + +/* An own `stackTraceLimit` on the Error value, assigned a non-number or + * defined with Object.defineProperty, replaces the limit for reads and + * writes through the value; writes to it keep its descriptor. */ +static bool scr_dyn_stack_limit_shadowed(const ScrDyn *d) { + ScrClosure *owner = scr_closure_identity(d->v.fn.clo); + ScrDyn *table = owner->props ? (ScrDyn *)scr_box_get_ref(owner->props) : NULL; + bool own = table && scr_dyn_find_entry(table, "stackTraceLimit", 15); + scr_dyn_release(table); + return own; +} + ScrDyn *scr_dyn_object_prototype(void) { if (!scr_builtin_object_prototype) { scr_builtin_object_prototype = scr_dyn_alloc(SCR_DYN_OBJ); @@ -2140,6 +2223,26 @@ ScrDyn *scr_dyn_get_prototype(ScrDyn *object) { return NULL; } +/* scr_dyn_get_prototype for a walk that compares each prototype with a + * program class's prototype (instanceof against a class value): a native + * Date, RegExp, Map, Set, WeakMap or WeakSet, or a promise, has only builtin + * prototypes on its chain, and no program class extends these (a class + * extending Date makes instances of its own), so the walk ends there with + * null. */ +ScrDyn *scr_dyn_get_prototype_for_identity(ScrDyn *object) { + if (object->kind == SCR_DYN_PROMISE) return scr_dyn_new_null(); + if (object->kind == SCR_DYN_HANDLE) { + switch (object->v.handle.tag) { + case SCR_DYNH_DATE: case SCR_DYNH_REGEXP: case SCR_DYNH_MAP: case SCR_DYNH_SET: + case SCR_DYNH_WEAK_MAP: case SCR_DYNH_WEAK_SET: + return scr_dyn_new_null(); + default: + break; + } + } + return scr_dyn_get_prototype(object); +} + ScrDyn *scr_dyn_set_prototype(ScrDyn *object, ScrDyn *prototype) { /* A primitive receiver is returned unchanged when the prototype is null or * any object or function; nothing is mutated, so the prototype's kind is the @@ -3331,7 +3434,7 @@ static ScrDyn *scr_dyn_typed_ref_new( return scr_dyn_retain(value); } } - if (type_key_len >= (size_t)1 << 28) scr_trap("scriptc: typed reference key too long\n"); + if (type_key_len >= (size_t)1 << 27) scr_trap("scriptc: typed reference key too long\n"); ScrDyn *d = scr_dyn_alloc(SCR_DYN_TYPED_REF); d->v.typed_ref.ptr = retain(ptr); d->v.typed_ref.retain = retain; @@ -3354,6 +3457,7 @@ static ScrDyn *scr_dyn_typed_ref_new( d->v.typed_ref.observed = observed; d->v.typed_ref.record = false; d->v.typed_ref.klass = false; + d->v.typed_ref.bag_class = false; scr_typed_ref_slots[slot] = d; /* the empty slot that ended the probe */ scr_typed_ref_count++; return d; @@ -3447,6 +3551,43 @@ ScrDyn *scr_dyn_class_view_unavailable(void *ptr) { return scr_dyn_retain(scr_dyn_undefined()); } +void scr_dyn_copy_entry(ScrDyn *target, const ScrDyn *source, const ScrDynEntry *entry) { + ScrStr *key = scr_str_new(entry->key, entry->key_len); + if (!entry->accessor && entry->writable && entry->enumerable && entry->configurable) { + scr_dyn_key_set(target, key, entry->value); + } else { + /* Properties defined through a view keep their attributes. */ + ScrDyn *name = scr_dyn_new_str(key); + ScrDyn *descriptor = scr_dyn_own_descriptor(source, key); + if (descriptor) scr_dyn_release(scr_dyn_define_property(target, name, descriptor)); + scr_dyn_release(descriptor); + scr_dyn_release(name); + } + scr_str_release(key); +} + +static ScrDyn *scr_typed_ref_numeric_property(const ScrDyn *d, double key); + +/* A live array's view lists the native numeric properties first, then the + * named properties the snapshot copied from the array's metadata. */ +static void scr_typed_ref_view_numeric_properties(const ScrDyn *capsule, ScrDyn *view) { + const ScrDynTypedArrayOps *ops = scr_dyn_typed_ref_array_ops(capsule); + ScrArr *arr = capsule->v.typed_ref.ptr; + const ScrArrExt *x = ops ? scr_arr_ext_of(arr) : NULL; + if (!x || x->prop_len == 0 || view->kind != SCR_DYN_ARR) return; + ScrDyn *named = view->v.arr.properties; + view->v.arr.properties = NULL; + for (size_t i = 0; i < x->prop_len; i++) { + double number; + if (!scr_arr_numeric_key(x->props[i].key, x->props[i].key_len, &number)) continue; + ScrDyn *value = scr_typed_ref_numeric_property(capsule, number); + if (value) scr_dyn_arr_named_put(view, x->props[i].key, x->props[i].key_len, value); + } + if (!named) return; + for (size_t i = 0; i < named->v.obj.len; i++) scr_dyn_copy_entry(view, named, &named->v.obj.entries[i]); + scr_dyn_release(named); +} + ScrDyn *scr_dyn_typed_ref_materialize(const ScrDyn *d) { ScrDyn *capsule = (ScrDyn *)d; ScrDyn *fresh = capsule->v.typed_ref.materialize(capsule->v.typed_ref.ptr); @@ -3454,6 +3595,7 @@ ScrDyn *scr_dyn_typed_ref_materialize(const ScrDyn *d) { scr_dyn_release(fresh); return scr_dyn_retain(scr_dyn_undefined()); } + scr_typed_ref_view_numeric_properties(capsule, fresh); if (!capsule->v.typed_ref.materialized) { capsule->v.typed_ref.materialized = fresh; } else { @@ -3531,6 +3673,7 @@ void scr_dyn_typed_ref_bind_array(ScrDyn *d, const ScrDynTypedArrayOps *ops) { d->v.typed_ref.array = ops; d->v.typed_ref.record = false; d->v.typed_ref.klass = false; + d->v.typed_ref.bag_class = false; } } @@ -3549,6 +3692,7 @@ void scr_dyn_typed_ref_bind_record(ScrDyn *d, const ScrDynTypedRecordOps *ops) { d->v.typed_ref.array = (const ScrDynTypedArrayOps *)(const void *)ops; d->v.typed_ref.record = true; d->v.typed_ref.klass = false; + d->v.typed_ref.bag_class = false; } } @@ -3557,9 +3701,75 @@ void scr_dyn_typed_ref_bind_class(ScrDyn *d, const ScrDynTypedClassOps *ops) { d->v.typed_ref.array = (const ScrDynTypedArrayOps *)(const void *)ops; d->v.typed_ref.record = false; d->v.typed_ref.klass = true; + d->v.typed_ref.bag_class = false; } } +void scr_dyn_typed_ref_bind_bag_class(ScrDyn *d, const ScrDynTypedBagClassOps *ops) { + if (d && d->kind == SCR_DYN_TYPED_REF) { + d->v.typed_ref.array = (const ScrDynTypedArrayOps *)(const void *)ops; + d->v.typed_ref.record = false; + d->v.typed_ref.klass = false; + d->v.typed_ref.bag_class = true; + } +} + +/* The view's own string keys are the bag's (ScrDynTypedBagClassOps). An + * own data property answers as the view would hold it: a declared field + * from its native slot (the materializer's ordinary write of that value, + * which a read-only or accessor entry would refuse, so those take the + * view), another property with the bag's value. A key the bag lacks is + * looked up on the prototype the view has: the cached view's, which keeps + * prototype changes made through the view, or for a capsule without one, + * the prototype a fresh view takes. The walk is scr_dyn_obj_read_receiver's + * from the view's prototype on, answering only when it meets an ordinary + * data property before anything that needs more (an accessor, a function, + * array or capsule link, Object.prototype). */ +ScrDyn *scr_dyn_typed_ref_bag_method(const ScrDyn *d, const char *key, size_t key_len) { + if (d->kind != SCR_DYN_TYPED_REF || !d->v.typed_ref.bag_class || d->prototype) return NULL; + const ScrDynTypedBagClassOps *ops = (const ScrDynTypedBagClassOps *)(const void *)d->v.typed_ref.array; + void *instance = d->v.typed_ref.ptr; + const ScrDyn *bag = ops->bag(instance); + if (bag && bag->kind != SCR_DYN_UNDEF) { + if (bag->kind != SCR_DYN_OBJ) return NULL; + const ScrDynEntry *own = scr_dyn_find_entry(bag, key, key_len); + if (own) { + for (size_t i = 0; i < ops->count; i++) { + const ScrStr *name = ops->names[i]; + if (name->len != key_len || memcmp(name->data, key, key_len) != 0) continue; + return own->accessor || !own->writable ? NULL : ops->field(instance, i); + } + return own->accessor ? NULL : scr_dyn_retain(own->value); + } + } + ScrDyn *prototype; + const ScrDyn *cached = d->v.typed_ref.materialized; + if (cached) { + if (cached->kind != SCR_DYN_OBJ || !cached->prototype) return NULL; + prototype = scr_dyn_retain(cached->prototype); + } else { + prototype = ops->prototype(instance); + if (scr_exc_pending()) { + scr_dyn_release(prototype); + return NULL; + } + if (!prototype) return NULL; + } + ScrDyn *found = NULL; + for (const ScrDyn *current = prototype; current; current = current->prototype) { + if (current == scr_builtin_object_prototype || current->kind == SCR_DYN_FUNC || + current->kind == SCR_DYN_ARR || current->kind == SCR_DYN_TYPED_REF) + break; + if (current->kind != SCR_DYN_OBJ) continue; + ScrDynEntry *entry = scr_dyn_find_entry(current, key, key_len); + if (!entry) continue; + if (!entry->accessor) found = scr_dyn_retain(entry->value); + break; + } + scr_dyn_release(prototype); + return found; +} + static const ScrDynTypedClassOps *scr_typed_ref_class_ops(const ScrDyn *d) { if (!d->v.typed_ref.klass) return NULL; const ScrDynTypedClassOps *ops = (const ScrDynTypedClassOps *)(const void *)d->v.typed_ref.array; @@ -3588,7 +3798,8 @@ bool scr_dyn_typed_ref_class_has(const ScrDyn *d, const ScrStr *key) { static const ScrDynTypedRecordOps *scr_typed_ref_record_ops(const ScrDyn *d) { if (!d->v.typed_ref.record || d->v.typed_ref.materialized) return NULL; - return (const ScrDynTypedRecordOps *)(const void *)d->v.typed_ref.array; + const ScrDynTypedRecordOps *ops = (const ScrDynTypedRecordOps *)(const void *)d->v.typed_ref.array; + return ops && ops->select ? ops->select(d->v.typed_ref.ptr) : ops; } const ScrDynTypedRecordOps *scr_dyn_typed_ref_live_record(const ScrDyn *d) { @@ -3690,6 +3901,16 @@ ScrDyn *scr_dyn_typed_ref_string_view(const ScrDyn *d) { return out; } +/* A numeric property of a live array ("-1", "1.5"): +1, or NULL when the + * native array has none. */ +static ScrDyn *scr_typed_ref_numeric_property(const ScrDyn *d, double key) { + ScrArr *arr = d->v.typed_ref.ptr; + double state = scr_arr_state(arr, key); + if (state == (double)SCR_ARR_VALUE) return scr_dyn_typed_ref_array_ops(d)->get(arr, key); + if (state == (double)SCR_ARR_UNDEFINED) return scr_dyn_retain(scr_dyn_undefined()); + return NULL; +} + /* An inherited Date method of an instance of a class declared `extends * Date`, when the instance's own members do not define `key`: +1, or * NULL. */ @@ -3714,6 +3935,8 @@ ScrDyn *scr_dyn_typed_ref_key_get(const ScrDyn *d, const ScrStr *key) { if (date_method) return date_method; if (!scr_dyn_typed_ref_array_ops(d)) return NULL; if (scr_typed_ref_is_length(key)) return scr_dyn_new_num((double)((ScrArr *)d->v.typed_ref.ptr)->len); + double number; + if (scr_arr_numeric_key(key->data, key->len, &number)) return scr_typed_ref_numeric_property(d, number); size_t index; if (!scr_typed_ref_canonical_index(key->data, key->len, &index)) return NULL; return scr_dyn_typed_ref_element(d, index); @@ -3723,6 +3946,9 @@ ScrDyn *scr_dyn_typed_ref_key_get(const ScrDyn *d, const ScrStr *key) { static bool scr_typed_ref_array_has_own(const ScrDyn *d, const ScrStr *key) { if (!scr_dyn_typed_ref_array_ops(d)) return false; if (scr_typed_ref_is_length(key)) return true; + double number; + if (scr_arr_numeric_key(key->data, key->len, &number)) + return scr_arr_state(d->v.typed_ref.ptr, number) != (double)SCR_ARR_HOLE; size_t index; if (!scr_typed_ref_canonical_index(key->data, key->len, &index)) return false; ScrArr *arr = d->v.typed_ref.ptr; @@ -3735,6 +3961,22 @@ static bool scr_typed_ref_array_has_own(const ScrDyn *d, const ScrStr *key) { static bool scr_typed_ref_array_set(ScrDyn *d, const ScrStr *key, const ScrDyn *value) { const ScrDynTypedArrayOps *ops = scr_dyn_typed_ref_array_ops(d); if (!ops || !ops->set) return false; + /* Numeric properties live in the native array's property table, which + * holds values of the element type. */ + double number; + if (scr_arr_numeric_key(key->data, key->len, &number)) { + if (ops->set(d->v.typed_ref.ptr, number, value)) return true; + ScrStr *kind = scr_dyn_typeof(value); + ScrJsonBuf message; + scr_jb_init(&message); + scr_jb_puts(&message, "expected the array's element type at $["); + scr_jb_write(&message, key->data, key->len); + scr_jb_puts(&message, "], got "); + scr_jb_write(&message, kind->data, kind->len); + scr_str_release(kind); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&message)); + return true; + } size_t index; if (!scr_typed_ref_canonical_index(key->data, key->len, &index)) return false; ScrArr *arr = d->v.typed_ref.ptr; @@ -3949,36 +4191,82 @@ ScrDyn *scr_array_buffer_constructor(void) { /* `value instanceof C` for C a global constructor held as a value * (`{ instanceOf: Date }`, scr_dyn_global_constructor): 1 or 0 by the - * value's native kind, or -1 when callee is not one of them, or for Object - * and for class instances, which these kinds cannot answer (except Date, - * whose subclasses' instances are known). A typed record or array is none - * of these kinds. */ + * value's native kind, or -1 when callee is not one of them, or for Object. + * A typed record or array is none of these kinds, and neither is an + * instance of a program class: a class can extend Date (answered through + * scr_dyn_date_instance_is), and extending RegExp, Map, Set, WeakMap, + * WeakSet or Promise refuses where the class is defined. The error + * constructors (scr_dyn_error_constructor) answer -1. */ static int scr_dyn_global_constructor_instanceof(const ScrDyn *value, const ScrDyn *callee) { if (!callee->v.fn.sig || !callee->v.fn.name || strcmp(callee->v.fn.sig, "native:constructor") != 0 || strcmp(callee->v.fn.name, "Object") == 0) return -1; /* A native Date, or an instance of a class declared `extends Date`. */ if (strcmp(callee->v.fn.name, "Date") == 0) return scr_dyn_date_instance_is(value); - if (value->kind == SCR_DYN_TYPED_REF) - return strncmp(value->v.typed_ref.type_key, "record:", 7) == 0 || scr_dyn_ref_is_array(value) ? 0 : -1; static const struct { const char *name; int tag; } kinds[] = { - {"Date", SCR_DYNH_DATE}, {"RegExp", SCR_DYNH_REGEXP}, {"Map", SCR_DYNH_MAP}, {"Set", SCR_DYNH_SET}, + {"RegExp", SCR_DYNH_REGEXP}, {"Map", SCR_DYNH_MAP}, {"Set", SCR_DYNH_SET}, {"WeakMap", SCR_DYNH_WEAK_MAP}, {"WeakSet", SCR_DYNH_WEAK_SET}, + {"FinalizationRegistry", SCR_DYNH_FINALIZATION_REGISTRY}, }; - if (strcmp(callee->v.fn.name, "Promise") == 0) return value->kind == SCR_DYN_PROMISE; - for (size_t i = 0; i < sizeof kinds / sizeof kinds[0]; i++) { - if (strcmp(callee->v.fn.name, kinds[i].name) == 0) - return value->kind == SCR_DYN_HANDLE && (int)value->v.handle.tag == kinds[i].tag; + bool promise = strcmp(callee->v.fn.name, "Promise") == 0; + int tag = -1; + for (size_t i = 0; i < sizeof kinds / sizeof kinds[0]; i++) + if (strcmp(callee->v.fn.name, kinds[i].name) == 0) tag = kinds[i].tag; + if (value->kind == SCR_DYN_TYPED_REF) { + if (strncmp(value->v.typed_ref.type_key, "record:", 7) == 0 || scr_dyn_ref_is_array(value)) return 0; + return (promise || tag >= 0) && strncmp(value->v.typed_ref.type_key, "object:", 7) == 0 ? 0 : -1; } + if (promise) return value->kind == SCR_DYN_PROMISE; + if (tag >= 0) return value->kind == SCR_DYN_HANDLE && (int)value->v.handle.tag == tag; return -1; } -bool scr_bytes_instanceof(const ScrDyn *value, const ScrDyn *callee) { +/* `value instanceof callee` for an untyped constructor value. `refusal`, when + * given, is the site's SC1090 message for a runtime constructor with no + * instance test. */ +/* A function whose own-property table defines Symbol.hasInstance + * (`Object.defineProperty(F, Symbol.hasInstance, { value })`): 1 or 0 by the + * truthiness of the method's answer with `this` bound to F, -1 when F + * defines none, or 0 with the exception pending. Functions without a table + * use the ordinary prototype test. */ +int scr_dyn_has_instance_hook(const ScrDyn *value, const ScrDyn *callee) { + if (!scr_has_instance_symbol || callee->kind != SCR_DYN_FUNC || !scr_closure_identity(callee->v.fn.clo)->props) + return -1; + ScrDyn *hook = scr_dyn_symbol_key_get(callee, scr_has_instance_symbol, true); + if (scr_exc_pending()) { + scr_dyn_release(hook); + return 0; + } + if (!hook || hook->kind != SCR_DYN_FUNC) { + scr_dyn_release(hook); + return -1; + } + scr_dyn_this_push_dyn((ScrDyn *)callee); + ScrDyn *argument = (ScrDyn *)value; + ScrDyn *result = scr_dyn_call(hook, &argument, 1, "Symbol.hasInstance"); + scr_dyn_this_pop(); + scr_dyn_release(hook); + if (!result) return 0; + bool truthy = scr_dyn_truthy(result); + scr_dyn_release(result); + return truthy ? 1 : 0; +} + +bool scr_bytes_instanceof_at(const ScrDyn *value, const ScrDyn *callee, const ScrStr *refusal) { if (!callee || callee->kind != SCR_DYN_FUNC) { static const char message[] = "Right-hand side of 'instanceof' is not callable"; scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); return false; } + int hooked = scr_dyn_has_instance_hook(value, callee); + if (hooked >= 0) return hooked == 1; + /* An error constructor held as a value (`inst instanceof params.Parent` + * with Parent === Error): the error kind's interval, and for Error itself + * objects inheriting Error.prototype. */ + for (size_t i = 0; i < SCR_ERROR_CTORS; i++) { + if (!scr_error_ctor_values[i] || callee->v.fn.clo != scr_error_ctor_values[i]->v.fn.clo) continue; + return scr_dyn_err_instanceof(value, (double)i) || (i == 0 && scr_dyn_inherits_error_prototype(value)); + } if (scr_array_buffer_ctor && callee->v.fn.clo == scr_array_buffer_ctor->v.fn.clo) return scr_array_buffer_is(value); if (scr_array_ctor && callee->v.fn.clo == scr_array_ctor->v.fn.clo) return value->kind == SCR_DYN_ARR || scr_dyn_ref_is_array(value); for (size_t i = 0; i < SCR_BYTES_ELEM_COUNT; i++) { @@ -4015,12 +4303,35 @@ bool scr_bytes_instanceof(const ScrDyn *value, const ScrDyn *callee) { scr_dyn_release(prototype); return found; } + bool undefined_prototype = prototype->kind == SCR_DYN_UNDEF; scr_dyn_release(prototype); + /* A function the program compiled with no prototype (an arrow function + * or a method): false for a primitive, and Node's TypeError for every + * object, typed arrays, native handles, promises and typed references + * included. The runtime's own constructors carry "native:" signatures. */ + if (undefined_prototype && (!callee->v.fn.sig || strncmp(callee->v.fn.sig, "native:", 7) != 0)) { + if (value->kind == SCR_DYN_UNDEF || value->kind == SCR_DYN_NULL || value->kind == SCR_DYN_BOOL || + value->kind == SCR_DYN_NUM || value->kind == SCR_DYN_STR || value->kind == SCR_DYN_BIGINT || + value->kind == SCR_DYN_SYMBOL) + return false; + static const char message[] = "Function has non-object prototype 'undefined' in instanceof check"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + return false; + } + } + if (refusal) { + scr_throw_error_msg_code(SCR_ERR_ERROR, refusal->data, refusal->len, "SC1090"); + return false; } static const char message[] = "instanceof against this native constructor is not supported yet"; scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC1090"); return false; } +bool scr_bytes_instanceof(const ScrDyn *value, const ScrDyn *callee) { + return scr_bytes_instanceof_at(value, callee, NULL); +} +SCR_TL bool (*scr_dyn_global_construct_fn)(const ScrDyn *callee, const ScrDyn *args, ScrDyn **result) = NULL; + ScrDyn *scr_bytes_construct(const ScrDyn *callee, const ScrDyn *args, const ScrStr *what) { if (callee->kind == SCR_DYN_FUNC && scr_array_ctor && callee->v.fn.clo == scr_array_ctor->v.fn.clo) return scr_array_ctor_call(callee->v.fn.clo, args->v.arr.items, args->v.arr.len); @@ -4037,6 +4348,9 @@ ScrDyn *scr_bytes_construct(const ScrDyn *callee, const ScrDyn *args, const ScrS scr_bytes_release(bytes); return result; } + ScrDyn *constructed = NULL; + if (scr_dyn_global_construct_fn && scr_dyn_global_construct_fn(callee, args, &constructed)) + return constructed; ScrJsonBuf message; scr_jb_init(&message); scr_jb_write(&message, what->data, what->len); @@ -4191,6 +4505,16 @@ ScrDyn *scr_dyn_class_prototype(ScrDyn *value, ScrDyn *base) { if (!cls->prototype_data) { ScrDyn *data = base->kind == SCR_DYN_UNDEF ? scr_dyn_new_obj() : scr_dyn_obj_create(base); if (!data) return NULL; + /* util.inspect prints an instance with its class's name: the class + * object's interned name, which is immortal. The prototype is owned by + * the class object and no registry keeps it alive, so evaluating a + * function-local class again releases the previous one's prototype. */ + if (cls->name && cls->name->len) data->v.obj.class_name = cls->name->data; + /* Like every class prototype, it starts with its own non-enumerable + * `constructor`: the class value. The cycle it closes through the class + * object is one the collector traces. */ + scr_dyn_obj_set(data, "constructor", 11, scr_dyn_retain(value)); + data->v.obj.entries[data->v.obj.len - 1].enumerable = false; cls->prototype_data = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v); scr_box_set_ref(cls->prototype_data, data); } @@ -4824,6 +5148,8 @@ ScrDyn *scr_dyn_this_get(void) { scr_dyn_typed_ref_bind_record(e->dv, (const ScrDynTypedRecordOps *)r->ops); else if (r->view == 3) scr_dyn_typed_ref_bind_class(e->dv, (const ScrDynTypedClassOps *)r->ops); + else if (r->view == 4) + scr_dyn_typed_ref_bind_bag_class(e->dv, (const ScrDynTypedBagClassOps *)r->ops); } if (e->dv) return scr_dyn_retain(e->dv); /* An uninstalled tag never binds: a unit that fires without its dyn @@ -4980,7 +5306,43 @@ ScrDyn *scr_dyn_class_inherit(ScrDyn *constructor, ScrDyn *base) { return scr_exc_pending() ? NULL : scr_dyn_retain(constructor); } +/* Defines `key` on target as a writable, configurable, non-enumerable data + * property holding value (borrowed), as Error's constructor installs its + * message and cause. */ +static void scr_dyn_define_hidden(ScrDyn *target, const ScrStr *key, ScrDyn *value) { + ScrDyn *descriptor = scr_dyn_new_obj(); + scr_dyn_obj_set(descriptor, "value", 5, scr_dyn_retain(value)); + scr_dyn_obj_set(descriptor, "writable", 8, scr_dyn_new_bool(true)); + scr_dyn_obj_set(descriptor, "enumerable", 10, scr_dyn_new_bool(false)); + scr_dyn_obj_set(descriptor, "configurable", 12, scr_dyn_new_bool(true)); + ScrDyn *name = scr_dyn_new_str((ScrStr *)key); + ScrDyn *defined = scr_dyn_define_property(target, name, descriptor); + scr_dyn_release(name); + scr_dyn_release(descriptor); + scr_dyn_release(defined); +} + +/* A builtin constructor a class expression's runtime heritage can name: + * 1 for Object, 2 for Error, 0 otherwise. */ +static int scr_dyn_builtin_base_kind(const ScrDyn *constructor) { + if (constructor->kind != SCR_DYN_FUNC || constructor->v.fn.class_obj || !constructor->v.fn.sig || + !constructor->v.fn.name || strcmp(constructor->v.fn.sig, "native:constructor") != 0) + return 0; + if (strcmp(constructor->v.fn.name, "Object") == 0) return 1; + if (strcmp(constructor->v.fn.name, "Error") == 0 && constructor->v.fn.clo->fn == (void *)scr_error_ctor_names[0]) + return 2; + return 0; +} + ScrDyn *scr_dyn_class_base_prototype(ScrDyn *constructor) { + /* `class extends (Base ?? Object)`: Object's instances are ordinary + * objects, laid out like a class without heritage (undefined); Error's + * inherit Error.prototype. */ + switch (scr_dyn_builtin_base_kind(constructor)) { + case 1: return scr_dyn_retain(scr_dyn_undefined()); + case 2: return scr_dyn_error_prototype(); + default: break; + } if (constructor->kind != SCR_DYN_FUNC || constructor->v.fn.class_obj || scr_closure_identity(constructor->v.fn.clo)->function_kind != 4) { static const char message[] = "Class extends value is not a native ordinary constructor"; @@ -4997,6 +5359,55 @@ ScrDyn *scr_dyn_class_base_prototype(ScrDyn *constructor) { } void scr_dyn_class_super(ScrDyn *constructor, ScrDyn *receiver, ScrDyn *arguments) { + switch (scr_dyn_builtin_base_kind(constructor)) { + case 1: + /* super() through Object keeps the receiver the derived constructor + * created (OrdinaryCreateFromConstructor). */ + return; + case 2: { + /* super(message, options) through Error: the message and cause become + * non-enumerable own properties, then the stack (its text alone, as for + * scriptc's other errors). */ + size_t argc = arguments->v.arr.len; + ScrDyn *const *argv = arguments->v.arr.items; + if (argc > 0 && argv[0]->kind != SCR_DYN_UNDEF) { + ScrStr *text = scr_dyn_string_coerce_js(argv[0]); + if (!text) return; + ScrDyn *value = scr_dyn_new_str(text); + scr_str_release(text); + ScrStr *key = scr_str_new("message", 7); + scr_dyn_define_hidden(receiver, key, value); + scr_str_release(key); + scr_dyn_release(value); + if (scr_exc_pending()) return; + } + /* InstallErrorCause: an options object with a `cause` property, own or + * inherited, defines the value its getter or data property reads. */ + ScrDyn *options = argc > 1 ? argv[1] : NULL; + if (options && options->kind != SCR_DYN_NULL && + (options->kind == SCR_DYN_FUNC || scr_dyn_is_object(options))) { + ScrStr *key = scr_str_new("cause", 5); + if (scr_dyn_in(options, key) && !scr_exc_pending()) { + ScrDyn *cause = scr_dyn_obj_read(options, "cause", 5); + if (cause && !scr_exc_pending()) scr_dyn_define_hidden(receiver, key, cause); + scr_dyn_release(cause); + } + scr_str_release(key); + if (scr_exc_pending()) return; + } + ScrStr *text = scr_dyn_error_string(receiver); + if (!text) return; + ScrDyn *stack = scr_dyn_new_str(text); + scr_str_release(text); + ScrStr *key = scr_str_new("stack", 5); + scr_dyn_define_hidden(receiver, key, stack); + scr_str_release(key); + scr_dyn_release(stack); + return; + } + default: + break; + } scr_dyn_this_push_dyn(receiver); ScrDyn *result = scr_dyn_call(constructor, arguments->v.arr.items, arguments->v.arr.len, "super"); scr_dyn_this_pop(); @@ -6648,6 +7059,21 @@ static bool scr_dyn_fn_property_fence(const ScrStr *key) { ScrDyn *scr_dyn_define_property(ScrDyn *target, ScrDyn *key, ScrDyn *descriptor) { if (scr_dyn_error_prototype_fence(target)) return NULL; + if (target->kind == SCR_DYN_TYPED_REF && scr_dyn_typed_ref_array_ops(target) && + (key->kind == SCR_DYN_STR || key->kind == SCR_DYN_NUM)) { + /* A native array's numeric properties ("-1", "1.5") live in its + * property table, which holds values without attributes. A number key + * names one unless it is an array index. */ + double number = key->kind == SCR_DYN_NUM ? key->v.num : 0; + bool numeric = key->kind == SCR_DYN_NUM + ? !(number >= 0 && number < 4294967295.0 && number == trunc(number)) + : scr_arr_numeric_key(key->v.str->data, key->v.str->len, &number); + if (numeric) { + static const char message[] = "Object.defineProperty on a numeric property of a native array is not supported yet"; + scr_throw_error_msg(SCR_ERR_ERROR, message, sizeof message - 1); + return NULL; + } + } if (target->kind == SCR_DYN_TYPED_REF) { ScrDyn *view = scr_dyn_typed_ref_materialize(target); ScrDyn *defined = scr_exc_pending() ? NULL : scr_dyn_define_property(view, key, descriptor); @@ -7030,6 +7456,7 @@ ScrStr *scr_dyn_object_tag(const ScrDyn *d) { d->v.handle.tag <= SCR_DYNH_ABORT_CONTROLLER) || d->v.handle.tag == SCR_DYNH_EVENT_TARGET || d->v.handle.tag == SCR_DYNH_CUSTOM_EVENT || d->v.handle.tag == SCR_DYNH_ARRAY_BUFFER || d->v.handle.tag == SCR_DYNH_SHARED_ARRAY_BUFFER || d->v.handle.tag == SCR_DYNH_WEAK_MAP || d->v.handle.tag == SCR_DYNH_WEAK_SET || + d->v.handle.tag == SCR_DYNH_FINALIZATION_REGISTRY || d->v.handle.tag == SCR_DYNH_SET || d->v.handle.tag == SCR_DYNH_MAP || d->v.handle.tag == SCR_DYNH_REGEXP || d->v.handle.tag == SCR_DYNH_DATE || d->v.handle.tag == SCR_DYNH_BLOB || d->v.handle.tag == SCR_DYNH_FILE || d->v.handle.tag == SCR_DYNH_DISPOSABLE_STACK) { @@ -7685,35 +8112,36 @@ ScrDyn *scr_errdyn_view_of(const ScrError *e) { static void scr_error_cause_drop_impl(void *obj); void scr_error_commit_dyn(ScrError *e, const ScrDyn *view) { - ScrDynEntry *name_entry = NULL; - for (size_t i = 0; i < view->v.obj.len; i++) { - ScrDynEntry *entry = &view->v.obj.entries[i]; - if (entry->key_len == 4 && memcmp(entry->key, "name", 4) == 0) { name_entry = entry; break; } - } - ScrDyn *name = name_entry ? name_entry->value : NULL; + ScrDynEntry *name_entry = scr_dyn_find_entry(view, "name", 4); + ScrDynEntry *message_entry = scr_dyn_find_entry(view, "message", 7); + /* An own accessor over `name` or `message` (zod's lazy error message) + * has no value to snapshot: the slot keeps its last value, and the + * property reads as absent so typed reads take the accessor path + * (scr_error_own_accessor) and toString asks the installed reader. */ + ScrDyn *name = name_entry && !name_entry->accessor ? name_entry->value : NULL; if (name && name->kind != SCR_DYN_STR) { scr_dyn_check_fail(NULL, "string", name); return; } - ScrStr *next_name = name ? scr_str_retain(name->v.str) : scr_error_default_name(e); - scr_str_release(e->name); - e->name = next_name; - e->name_present = name != NULL; - e->name_enumerable = name_entry && name_entry->enumerable; - ScrDyn *message = scr_dyn_obj_get(view, "message", 7); + ScrDyn *message = message_entry && !message_entry->accessor ? message_entry->value : NULL; if (message && message->kind != SCR_DYN_STR) { scr_dyn_check_fail(NULL, "string", message); return; } - ScrStr *next_message = message ? scr_str_retain(message->v.str) : scr_str_new("", 0); - scr_str_release(e->message); - e->message = next_message; - e->message_present = message != NULL; - e->message_enumerable = false; - for (size_t i = 0; message && i < view->v.obj.len; i++) { - const ScrDynEntry *entry = &view->v.obj.entries[i]; - if (entry->key_len == 7 && memcmp(entry->key, "message", 7) == 0) e->message_enumerable = entry->enumerable; + if (!name_entry || !name_entry->accessor) { + ScrStr *next_name = name ? scr_str_retain(name->v.str) : scr_error_default_name(e); + scr_str_release(e->name); + e->name = next_name; } + e->name_present = name != NULL; + e->name_enumerable = name_entry && name_entry->enumerable; + if (!message_entry || !message_entry->accessor) { + ScrStr *next_message = message ? scr_str_retain(message->v.str) : scr_str_new("", 0); + scr_str_release(e->message); + e->message = next_message; + } + e->message_present = message != NULL; + e->message_enumerable = message_entry && message_entry->enumerable; ScrDyn *stack = scr_dyn_obj_get(view, "stack", 5); if (stack && stack->kind == SCR_DYN_STR) { ScrStr *replacement = scr_str_retain(stack->v.str); @@ -7809,8 +8237,20 @@ static void scr_error_sync_slots(const ScrDyn *view, ScrDyn *obj, const ScrStr * return; } if (name || message) { - /* The native error's toString and inspect read these fields. */ - if (!entry || entry->accessor || entry->value->kind != SCR_DYN_STR) return; + /* The native error's toString and inspect read these fields. An own + * accessor leaves the slot, which then reads as absent: typed reads + * call it (scr_error_own_accessor). */ + if (entry && entry->accessor) { + if (name) { + e->name_present = false; + e->name_enumerable = entry->enumerable; + } else { + e->message_present = false; + e->message_enumerable = entry->enumerable; + } + return; + } + if (!entry || entry->value->kind != SCR_DYN_STR) return; ScrStr **field = name ? &e->name : &e->message; scr_str_release(*field); *field = scr_str_retain(entry->value->v.str); @@ -8949,8 +9389,19 @@ void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) { } if (recv->kind == SCR_DYN_FUNC) { if (scr_dyn_fn_property_fence(key)) return; + bool limit = scr_dyn_is_stack_limit(recv, key->data, key->len); + /* A frozen Error keeps the limit read-only. */ + if (limit && value->kind == SCR_DYN_NUM && !scr_dyn_stack_limit_shadowed(recv) && !scr_dyn_is_frozen(recv)) { + scr_stack_limit_set(value->v.num); + return; + } ScrDyn *table = scr_dyn_fn_properties(recv); scr_dyn_object_key_set(table, key, value, recv); + /* A number the own data property takes also sets the limit. */ + ScrDynEntry *own = limit && value->kind == SCR_DYN_NUM && !scr_exc_pending() + ? scr_dyn_find_entry(table, key->data, key->len) + : NULL; + if (own && own->value == value) scr_stack_limit_set(value->v.num); scr_dyn_release(table); return; } @@ -10703,8 +11154,9 @@ static bool scr_json_callback_depth(size_t depth) { static ScrStr *scr_json_index_key(size_t index) { char bytes[32]; - int length = snprintf(bytes, sizeof bytes, "%zu", index); - return scr_str_new(bytes, (size_t)length); + /* array indexes are below 2^32: exact doubles, printed as integers */ + size_t length = scr_f64_to_str((double)index, bytes); + return scr_str_new(bytes, length); } /* Keys passed here come from our own index formatter or own-key walk. */ @@ -10980,12 +11432,22 @@ static bool scr_json_live_container(const ScrDyn *value, bool inspect) { * { type, data } view, and an object with a callable toJSON through that * method, called with the key. Returns the value to serialize (+1), or NULL * with an exception pending. */ -static ScrDyn *scr_json_to_json(ScrDyn *value, const ScrStr *key) { +/* The toJSON key argument: `key`, or the decimal spelling of array index + * `index` when `key` is NULL (formed only for a call that receives it). */ +static ScrDyn *scr_json_key_arg(const ScrStr *key, size_t index) { + if (key) return scr_dyn_new_str((ScrStr *)key); + ScrStr *text = scr_json_index_key(index); + ScrDyn *name = scr_dyn_new_str(text); + scr_str_release(text); + return name; +} + +static ScrDyn *scr_json_to_json_at(ScrDyn *value, const ScrStr *key, size_t index) { /* An instance of a class declared `extends Date` converts through its * toJSON, declared or inherited. */ double time; if (value->kind == SCR_DYN_TYPED_REF && scr_dyn_date_class_time(value, &time)) { - ScrDyn *name = scr_dyn_new_str((ScrStr *)key); + ScrDyn *name = scr_json_key_arg(key, index); ScrDyn *converted = scr_dyn_date_class_to_json(value, name); scr_dyn_release(name); scr_dyn_release(value); @@ -11013,7 +11475,7 @@ static ScrDyn *scr_json_to_json(ScrDyn *value, const ScrStr *key) { ScrDyn *method = scr_dyn_obj_read(inspected, "toJSON", 6); if (!method) { scr_dyn_release(view); scr_dyn_release(value); return NULL; } if (method->kind == SCR_DYN_FUNC) { - ScrDyn *name = scr_dyn_new_str((ScrStr *)key); + ScrDyn *name = scr_json_key_arg(key, index); ScrDyn *args[] = { name }; scr_dyn_this_push_dyn(value); ScrDyn *converted = scr_dyn_call(method, args, 1, "toJSON"); @@ -11028,6 +11490,10 @@ static ScrDyn *scr_json_to_json(ScrDyn *value, const ScrStr *key) { return value; } +static ScrDyn *scr_json_to_json(ScrDyn *value, const ScrStr *key) { + return scr_json_to_json_at(value, key, 0); +} + static ScrDyn *scr_json_replace(const ScrDyn *holder, const ScrStr *key, const ScrDyn *replacer) { ScrDyn *value = scr_json_member(holder, key); if (!value) return NULL; @@ -11162,6 +11628,81 @@ static ScrDyn *scr_json_property_list(const ScrDyn *replacer) { return list; } +/* The protocol walk over a live record or array capsule without a replacer + * (the caller entered it on the circular-structure stack). Members are read + * from the native value exactly as scr_json_member reads them, but the walk + * needs no key list: a record's present fields are snapshotted by index (its + * keys are the immortal field names) and an array's indexes are counted, so + * an index key string is formed only for a toJSON call that receives it. */ +static bool scr_json_live_write(ScrJsonBuf *buffer, const ScrDyn *value, bool array, + const ScrStr *gap, size_t depth) { + const ScrDynTypedRecordOps *record = array ? NULL : scr_typed_ref_record_ops(value); + /* EnumerableOwnProperties runs once, before any member's toJSON (the + * caller sends records of more than 64 fields down the key-list walk). */ + uint64_t present = 0; + size_t length; + if (array) { + length = ((ScrArr *)value->v.typed_ref.ptr)->len; + } else { + length = record->count; + for (size_t i = 0; i < length; i++) + if (record->fixed || record->has(value->v.typed_ref.ptr, i)) present |= (uint64_t)1 << i; + } + scr_jb_putc(buffer, array ? '[' : '{'); + bool first = true; + for (size_t i = 0; i < length; i++) { + if (!array && !(present & ((uint64_t)1 << i))) continue; + const ScrStr *key = array ? NULL : record->names[i]; + ScrDyn *child; + if (array) { + scr_jb_edge_idx(buffer, i); + /* scr_json_member's index read, minus the key: a present element + * answers directly; holes, a length a callback shrank, a capsule a + * callback materialized and inherited event members take the keyed + * read */ + child = !scr_event_ref_hooks && scr_dyn_typed_ref_array_ops(value) ? scr_dyn_typed_ref_element(value, i) + : NULL; + if (!child) { + ScrStr *index = scr_json_index_key(i); + child = scr_json_member(value, index); + scr_str_release(index); + } + } else { + scr_jb_edge_key(buffer, key); + child = scr_json_member(value, key); + } + if (child) child = scr_json_to_json_at(child, key, i); + if (!child) return false; + if (!array && scr_json_omitted(child)) { + scr_dyn_release(child); + continue; + } + if (!first) scr_jb_putc(buffer, ','); + first = false; + scr_json_gap(buffer, gap, depth + 1); + if (!array) { + scr_jb_put_json_str(buffer, key); + scr_jb_putc(buffer, ':'); + if (gap->len) scr_jb_putc(buffer, ' '); + } + bool ok = true; + if (scr_json_omitted(child)) scr_jb_puts(buffer, "null"); + else if (scr_json_plain_value(child)) { + /* the protocol walk's depth check, then the primitive itself */ + ok = scr_json_callback_depth(depth + 1); + if (ok) { + scr_dyn_json_write(buffer, child); + ok = !scr_exc_pending(); + } + } else ok = scr_json_replaced_write(buffer, child, scr_dyn_undefined(), NULL, gap, depth + 1); + scr_dyn_release(child); + if (!ok) return false; + } + if (!first) scr_json_gap(buffer, gap, depth); + scr_jb_putc(buffer, array ? ']' : '}'); + return true; +} + /* `properties`, when present, is an array replacer's PropertyList: objects * serialize exactly those keys, own or inherited, in that order; arrays * keep all their indexes. */ @@ -11191,6 +11732,12 @@ static bool scr_json_replaced_write(ScrJsonBuf *buffer, const ScrDyn *value, * after the current frame leaves the traversal stack. */ const void *identity = view || live ? value->v.typed_ref.ptr : (const void *)value; if (!scr_jb_enter(buffer, identity, array)) { scr_dyn_release(view); return false; } + if (live && replacer->kind == SCR_DYN_UNDEF && !properties && + (array || scr_typed_ref_record_ops(value)->count <= 64)) { + bool ok = scr_json_live_write(buffer, value, array, gap, depth); + scr_jb_leave(buffer); + return ok; + } scr_jb_putc(buffer, array ? '[' : '{'); ScrDyn *keys = array ? NULL : properties ? scr_dyn_retain((ScrDyn *)properties) : scr_dyn_obj_keys(observed); if (scr_exc_pending()) { scr_dyn_release(keys); scr_dyn_release(view); scr_jb_leave(buffer); return false; } @@ -11634,6 +12181,8 @@ static ScrDyn *scr_function_constructor(uint32_t kind) { /* Keyed read on a FUNC node (see scr_runtime.h): own props first, then * the function-instance built-ins name/length. +1 or NULL. */ ScrDyn *scr_dyn_fn_get(const ScrDyn *d, const char *key, size_t key_len) { + if (scr_dyn_is_stack_limit(d, key, key_len) && !scr_dyn_stack_limit_shadowed(d)) + return scr_dyn_new_num(scr_stack_limit_get()); ScrClosure *owner = scr_closure_identity(d->v.fn.clo); if (d->v.fn.class_obj && d->v.fn.class_obj->static_data && !owner->props) owner->props = scr_box_retain(d->v.fn.class_obj->static_data); @@ -14417,7 +14966,8 @@ static int scr_weak_key(const ScrDyn *key, void **ptr, unsigned *kind) { *kind = key->v.fn.class_obj ? 3 : 2; return 1; case SCR_DYN_HANDLE: if (key->v.handle.tag != SCR_DYNH_ARRAY_BUFFER && key->v.handle.tag != SCR_DYNH_SHARED_ARRAY_BUFFER && key->v.handle.tag != SCR_DYNH_WEAK_MAP && - key->v.handle.tag != SCR_DYNH_WEAK_SET && key->v.handle.tag != SCR_DYNH_STDIO && + key->v.handle.tag != SCR_DYNH_WEAK_SET && key->v.handle.tag != SCR_DYNH_FINALIZATION_REGISTRY && + key->v.handle.tag != SCR_DYNH_STDIO && key->v.handle.tag != SCR_DYNH_FETCH_REQUEST && key->v.handle.tag != SCR_DYNH_FETCH_RESPONSE && key->v.handle.tag != SCR_DYNH_MAP && key->v.handle.tag != SCR_DYNH_SET && key->v.handle.tag != SCR_DYNH_NET_SOCKET && key->v.handle.tag != SCR_DYNH_NET_SERVER) return -1; @@ -14596,6 +15146,159 @@ bool scr_weak_set_is(const ScrDyn *value) { ScrDyn *scr_weak_map_new(ScrDyn *seed) { return scr_weak_collection_new(seed, false); } ScrDyn *scr_weak_set_new(ScrDyn *seed) { return scr_weak_collection_new(seed, true); } +/* FinalizationRegistry. register() and unregister() validate and keep their + * bookkeeping as Node does; the cleanup callback never runs, which + * ECMAScript allows (an implementation need not collect a target or call + * its cleanup), so the registry keeps neither the callback nor the held + * values. The unregister tokens are keys of an internal WeakMap counting + * each token's registrations, so the registry keeps no token alive. */ +typedef struct { + size_t rc; + ScrDyn *tokens; /* WeakMap: token -> registration count */ +} ScrFinalizationRegistry; + +static void *scr_finalization_registry_retain(void *ptr) { + ((ScrFinalizationRegistry *)ptr)->rc++; + return ptr; +} + +static void scr_finalization_registry_release(void *ptr) { + ScrFinalizationRegistry *registry = ptr; + if (--registry->rc) return; + /* Weak collections keyed by the registry drop their entries. */ + scr_weak_dispose(registry); + scr_dyn_release(registry->tokens); + free(registry); +} + +static void scr_finalization_type_error(const char *message) { + scr_throw_error_msg(SCR_ERR_TYPE, message, strlen(message)); +} + +/* The weak-key identity of a registration target or token, or false with + * the TypeError (or the SC2020 refusal) pending. */ +static bool scr_finalization_key(const ScrDyn *value, void **ptr, unsigned *kind, + const char *invalid) { + if (value->kind == SCR_DYN_SYMBOL) { + static const char message[] = "FinalizationRegistry with symbol targets or tokens is not supported yet"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + return false; + } + int supported = scr_weak_key(value, ptr, kind); + if (supported < 0) { + static const char message[] = "FinalizationRegistry targets of this native reference type have no weak lifetime lowering"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + return false; + } + if (!supported) { + if (invalid) { + scr_finalization_type_error(invalid); + } else { + /* Node's unregister message quotes the token as String() renders it. */ + ScrStr *text = scr_dyn_string_coerce(value); + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, "Invalid unregisterToken ('"); + if (text) scr_jb_write(&b, text->data, text->len); + scr_jb_puts(&b, "')"); + scr_str_release(text); + ScrStr *message = scr_jb_finish(&b); + scr_throw_error_msg(SCR_ERR_TYPE, message->data, message->len); + scr_str_release(message); + } + return false; + } + return true; +} + +static ScrDyn *scr_finalization_registry_invoke(void *ptr, ScrDyn *self, const char *method, + ScrDyn *const *args, size_t argc, const char *what) { + (void)self; + ScrFinalizationRegistry *registry = ptr; + ScrWeakMap *tokens = registry->tokens->v.handle.ptr; + ScrDyn *first = argc > 0 ? args[0] : scr_dyn_undefined(); + void *identity = NULL; + unsigned kind = 0; + if (strcmp(method, "register") == 0) { + ScrDyn *held = argc > 1 ? args[1] : scr_dyn_undefined(); + ScrDyn *token = argc > 2 ? args[2] : scr_dyn_undefined(); + if (!scr_finalization_key(first, &identity, &kind, "FinalizationRegistry.prototype.register: invalid target")) + return NULL; + if (scr_dyn_strict_eq(first, held)) { + scr_finalization_type_error("FinalizationRegistry.prototype.register: target and holdings must not be same"); + return NULL; + } + if (token->kind != SCR_DYN_UNDEF) { + if (!scr_finalization_key(token, &identity, &kind, NULL)) return NULL; + ScrWeakEntry *entry = scr_weak_find(tokens, identity, kind); + double count = entry && entry->value->kind == SCR_DYN_NUM ? entry->value->v.num : 0; + ScrDyn *next = scr_dyn_new_num(count + 1); + bool ok = scr_weak_set(tokens, token, next); + scr_dyn_release(next); + if (!ok) return NULL; + } + return scr_dyn_retain(scr_dyn_undefined()); + } + if (strcmp(method, "unregister") == 0) { + if (!scr_finalization_key(first, &identity, &kind, NULL)) return NULL; + ScrWeakEntry *entry = scr_weak_find(tokens, identity, kind); + if (entry) scr_weak_remove(entry); + return scr_dyn_new_bool(entry != NULL); + } + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, what ? what : method); + scr_jb_puts(&b, " is not a function"); + ScrStr *message = scr_jb_finish(&b); + scr_throw_error_msg(SCR_ERR_TYPE, message->data, message->len); + scr_str_release(message); + return NULL; +} + +/* A property read answers undefined for names the registry does not have. + * Its methods read as values (`const register = registry.register`) are + * refused, as a weak collection's are. */ +static ScrDyn *scr_finalization_registry_get(void *ptr, const char *key, size_t len) { + (void)ptr; + if ((len == 8 && memcmp(key, "register", 8) == 0) || (len == 10 && memcmp(key, "unregister", 10) == 0)) { + static const char message[] = "FinalizationRegistry method values have no native lowering"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + } + return NULL; +} + +static ScrDyn *scr_finalization_registry_view(void *ptr) { + (void)ptr; + return scr_dyn_new_obj(); +} + +ScrDyn *scr_finalization_registry_new(ScrDyn *cleanup) { + if (!scr_dyn_is_callable(cleanup)) { + scr_finalization_type_error("FinalizationRegistry: cleanup must be callable"); + return NULL; + } + static const ScrDynHandleOps ops = { + .cls = "FinalizationRegistry", + .retain = &scr_finalization_registry_retain, + .release = &scr_finalization_registry_release, + .invoke = &scr_finalization_registry_invoke, + .get = &scr_finalization_registry_get, + .inspect_view = &scr_finalization_registry_view, + .inspect_view_empty = true, + }; + scr_dyn_handle_install(SCR_DYNH_FINALIZATION_REGISTRY, &ops); + ScrFinalizationRegistry *registry = calloc(1, sizeof *registry); + if (!registry) scr_json_oom(); + registry->tokens = scr_weak_collection_new(scr_dyn_undefined(), false); + if (!registry->tokens) { free(registry); return NULL; } + ScrDyn *result = scr_dyn_new_handle(registry, SCR_DYNH_FINALIZATION_REGISTRY); + return result; +} + +bool scr_finalization_registry_is(const ScrDyn *value) { + return value->kind == SCR_DYN_HANDLE && value->v.handle.tag == SCR_DYNH_FINALIZATION_REGISTRY; +} + ScrDyn *scr_dyn_from_entries(ScrDyn *seed) { if (seed->kind == SCR_DYN_UNDEF || seed->kind == SCR_DYN_NULL) { static const char message[] = "undefined is not iterable"; @@ -14836,7 +15539,7 @@ ScrDyn *scr_dyn_native_set(ScrMap *value) { ScrDyn *scr_dyn_native_set_new(const ScrDyn *seed) { ScrDyn *items = NULL; if (seed->kind != SCR_DYN_NULL && seed->kind != SCR_DYN_UNDEF) { - items = scr_dyn_iter_pack(seed, NULL); + items = scr_dyn_seed_values(seed); if (!items) return NULL; } ScrMap *set = scr_map_new(SCR_MAP_KEY_DYN, SCR_MAP_VAL_F64, &scr_map_dyn_set_ops); @@ -14950,6 +15653,27 @@ ScrDyn *scr_dyn_native_map(ScrMap *value) { return scr_dyn_new_handle(value, SCR_DYNH_MAP); } +ScrDyn *scr_dyn_native_map_new(const ScrDyn *seed) { + ScrDyn *entries = scr_dyn_map_seed_entries(seed); + if (!entries) return NULL; + ScrMap *map = scr_map_new(SCR_MAP_KEY_DYN, SCR_MAP_VAL_REF, &scr_map_dyn_map_ops); + for (size_t i = 0; i < entries->v.arr.len; i++) { + ScrDyn *entry = scr_dyn_map_seed_entry(entries->v.arr.items[i]); + if (!entry) break; + ScrDyn *key = scr_dyn_entry_read(entry, 0); + ScrDyn *value = scr_exc_pending() ? NULL : scr_dyn_entry_read(entry, 1); + scr_dyn_release(entry); + if (value) scr_map_dyn_set(map, key, value, false); + scr_dyn_release(key); + scr_dyn_release(value); + if (scr_exc_pending()) break; + } + scr_dyn_release(entries); + ScrDyn *result = scr_exc_pending() ? NULL : scr_dyn_native_map(map); + scr_map_release(map); + return result; +} + bool scr_dyn_native_map_is(const ScrDyn *value) { return value && value->kind == SCR_DYN_HANDLE && value->v.handle.tag == SCR_DYNH_MAP; } @@ -15435,9 +16159,145 @@ bool scr_error_has_code(ScrError *e) { * property set at runtime, which lives on the error's checked-dynamic node * (that node exists once anything reflected or wrote through it), or a * member of Object.prototype. Borrows both; never throws. */ +/* Whether the error's own `name` or `message` (the key) is an accessor a + * reflective definition installed on its checked-dynamic node (the slot + * then reads as absent): typed reads call its getter and typed writes its + * setter through that node. A class view keeps the accessor in its last + * materialized object. Borrows both; never throws. */ +static ScrDynEntry *scr_error_accessor_entry(ScrError *e, const ScrStr *key) { + const ScrDyn *view = scr_errdyn_view_of(e); + if (view != NULL && view->kind == SCR_DYN_TYPED_REF) view = view->v.typed_ref.materialized; + if (view == NULL || view->kind != SCR_DYN_OBJ) return NULL; + ScrDynEntry *entry = scr_dyn_find_entry(view, key->data, key->len); + return entry != NULL && entry->accessor ? entry : NULL; +} + +bool scr_error_own_accessor(ScrError *e, const ScrStr *key) { return scr_error_accessor_entry(e, key) != NULL; } + +/* A typed read of that accessor: its getter called on the error's node + * (the undefined value without one). Owned result, or NULL with the + * getter's exception pending. */ +ScrDyn *scr_error_accessor_get(ScrError *e, const ScrStr *key) { + ScrDynEntry *entry = scr_error_accessor_entry(e, key); + if (entry == NULL || entry->getter == NULL) return scr_dyn_retain(scr_dyn_undefined()); + ScrDyn *getter = scr_dyn_retain(entry->getter); + ScrDyn *receiver = scr_dyn_from_error(e); + scr_dyn_this_push_dyn(receiver); + ScrDyn *result = scr_dyn_call(getter, NULL, 0, "getter"); + scr_dyn_this_pop(); + scr_dyn_release(receiver); + scr_dyn_release(getter); + return result; +} + +/* A typed write of that accessor: its setter called on the error's node + * with the value, or V8's strict-mode TypeError without one. Borrows all; + * the setter's exception stays pending. */ +void scr_error_accessor_set(ScrError *e, const ScrStr *key, ScrDyn *value) { + ScrDynEntry *entry = scr_error_accessor_entry(e, key); + if (entry == NULL) return; + if (entry->setter == NULL) { + char message[256]; + int n = snprintf(message, sizeof message, "Cannot set property %.*s of %.*s which has only a getter", + (int)key->len, key->data, (int)e->name->len, e->name->data); + scr_throw_error_msg(SCR_ERR_TYPE, message, (size_t)n < sizeof message ? (size_t)n : sizeof message - 1); + return; + } + ScrDyn *setter = scr_dyn_retain(entry->setter); + ScrDyn *receiver = scr_dyn_from_error(e); + ScrDyn *args[1] = {value}; + scr_dyn_this_push_dyn(receiver); + ScrDyn *result = scr_dyn_call(setter, args, 1, "setter"); + scr_dyn_this_pop(); + scr_dyn_release(result); + scr_dyn_release(receiver); + scr_dyn_release(setter); +} + bool scr_error_has_key(ScrError *e, const ScrStr *key) { const ScrDyn *view = scr_errdyn_view_of(e); if (view != NULL && view->kind == SCR_DYN_OBJ && scr_dyn_find_entry(view, key->data, key->len) != NULL) return true; return scr_dyn_object_proto_name(key); } + +/* ── declaration-typed boundary: in-place capsules ───────────────────── + * scr_decl_adopt.c (linked only into programs with boundary casts) turns a + * shared source node into a live capsule of the native value built from + * it. The node's old payload moves to a scratch node released here; the + * node's weak-collection entries move to the capsule's native identity. + * Nothing else in the runtime calls this. */ +static bool scr_dyn_decl_rekey(ScrDyn *node, void *ptr) { + bool moved = false; + if (!scr_weak_observing) return false; + for (;;) { + ScrWeakEntry **slot = &scr_weak_entries[scr_weak_bucket(node)]; + while (*slot && !((*slot)->key == (void *)node && (*slot)->kind == 0)) slot = &(*slot)->all_next; + ScrWeakEntry *entry = *slot; + if (!entry) return moved; + *slot = entry->all_next; + entry->key = ptr; + entry->kind = 32; + ScrWeakEntry **head = &scr_weak_entries[scr_weak_bucket(ptr)]; + entry->all_next = *head; + *head = entry; + moved = true; + } +} + +void scr_dyn_decl_become(ScrDyn *n, const ScrDyn *capsule); +void scr_dyn_decl_become(ScrDyn *n, const ScrDyn *capsule) { + ScrDynRegion *region = scr_dyn_region_of(n); + if (region) region->intact = false; + ScrDyn *old = scr_dyn_alloc(SCR_DYN_NULL); + old->kind = n->kind; + old->buffer = n->buffer; + old->null_proto = n->null_proto; + old->copied_from_native = n->copied_from_native; + old->non_extensible = n->non_extensible; + old->v = n->v; + old->prototype = n->prototype; + old->symbol_properties = n->symbol_properties; + old->symbol_keys = n->symbol_keys; + n->kind = SCR_DYN_TYPED_REF; + n->buffer = false; + n->null_proto = false; + n->copied_from_native = false; + n->non_extensible = false; + n->prototype = NULL; + n->symbol_properties = NULL; + n->symbol_keys = NULL; + memset(&n->v, 0, sizeof n->v); + n->v.typed_ref = capsule->v.typed_ref; + n->v.typed_ref.ptr = capsule->v.typed_ref.retain(capsule->v.typed_ref.ptr); + n->v.typed_ref.materialized = NULL; + n->v.typed_ref.casts = NULL; + if (scr_dyn_decl_rekey(n, n->v.typed_ref.ptr)) n->v.typed_ref.observed = true; + if (n->v.typed_ref.type_key_len >= 7 && memcmp(n->v.typed_ref.type_key, "record:", 7) == 0) { + ScrDyn *view = scr_record_source_view(n->v.typed_ref.ptr); + if (view) n->v.typed_ref.materialized = scr_dyn_retain(view); + } + scr_dyn_release(old); +} + +/* A compiled class's field creation in its instance's property bag (the + * compiler's trackClassFieldCreation): an ordinary extensible bag gains + * `key` as a writable, enumerable, configurable data property when it + * does not have it yet. Answers false, changing nothing, for any other bag, + * whose creation takes the general defineProperty path. Calls only + * exported functions. */ +bool scr_dyn_bag_create_field(ScrDyn *bag, const ScrStr *key) { + if (!bag || bag->kind != SCR_DYN_OBJ || bag->non_extensible || bag->v.obj.source_access) + return false; + if (scr_dyn_obj_get(bag, key->data, key->len)) return true; + scr_dyn_obj_set(bag, key->data, key->len, scr_dyn_retain(scr_dyn_undefined())); + return true; +} + +/* Whether a value is a capsule of a compiled class instance (its type key + * names an object type): the class tables' fast exit for every other value, + * including record and array capsules. Calls only libc. */ +bool scr_dyn_is_class_capsule(const ScrDyn *d) { + return d && d->kind == SCR_DYN_TYPED_REF && d->v.typed_ref.type_key_len > 7 && + memcmp(d->v.typed_ref.type_key, "object:", 7) == 0; +} diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index 4be596ab5..c17f44f9d 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -12961,50 +12961,51 @@ static bool scr_set_to_arr_sparse(const ScrMap *s, ScrArr *out) { ScrArr *scr_set_to_arr_f64(const ScrMap *s) { ScrArr *out = scr_arr_new(SCR_ELEM_F64, s->nlive); if (scr_set_to_arr_sparse(s, out)) return out; - uint8_t *states = scr_arr_states_end(out); + /* A fresh array without states: the values in order. */ for (size_t i = 0; i < s->nentries; i++) { if (!scr_map_entry_live(s, i)) continue; - out->data[out->len] = scr_map_entry_at(s, i)->key; - *scr_arr_state_in(states, out->len++) = SCR_ARR_VALUE; + out->data[out->len++] = scr_map_entry_at(s, i)->key; } + scr_arr_refill(out); return out; } ScrArr *scr_set_to_arr_bool(const ScrMap *s) { ScrArr *out = scr_arr_new(SCR_ELEM_BOOL, s->nlive); if (scr_set_to_arr_sparse(s, out)) return out; - uint8_t *states = scr_arr_states_end(out); + /* A fresh array without states: the values in order. */ for (size_t i = 0; i < s->nentries; i++) { if (!scr_map_entry_live(s, i)) continue; - scr_arr_dense_put(out, out->len, scr_map_entry_at(s, i)->key != 0); - *scr_arr_state_in(states, out->len++) = SCR_ARR_VALUE; + scr_arr_dense_put(out, out->len++, scr_map_entry_at(s, i)->key != 0); } + scr_arr_refill(out); return out; } ScrArr *scr_set_to_arr_str(const ScrMap *s) { ScrArr *out = scr_arr_new(SCR_ELEM_STR, s->nlive); if (scr_set_to_arr_sparse(s, out)) return out; - uint8_t *states = scr_arr_states_end(out); + /* A fresh array without states: the values in order. */ for (size_t i = 0; i < s->nentries; i++) { if (!scr_map_entry_live(s, i)) continue; scr_str_retain((ScrStr *)(uintptr_t)scr_map_entry_at(s, i)->key); - out->data[out->len] = scr_map_entry_at(s, i)->key; - *scr_arr_state_in(states, out->len++) = SCR_ARR_VALUE; + out->data[out->len++] = scr_map_entry_at(s, i)->key; } + scr_arr_refill(out); return out; } ScrArr *scr_set_to_arr_ref(const ScrMap *s, const ScrArrOps *ops) { ScrArr *out = scr_arr_new_ops(ops, s->nlive); if (scr_set_to_arr_sparse(s, out)) return out; - uint8_t *states = scr_arr_states_end(out); + /* A fresh array without states: the values in order. */ for (size_t i = 0; i < s->nentries; i++) { if (!scr_map_entry_live(s, i)) continue; ops->retain((void *)(uintptr_t)scr_map_entry_at(s, i)->key); - out->data[out->len] = scr_map_entry_at(s, i)->key; - *scr_arr_state_in(states, out->len++) = SCR_ARR_VALUE; + scr_arr_dense_put(out, out->len, scr_map_entry_at(s, i)->key); + out->len++; } + scr_arr_refill(out); return out; } diff --git a/packages/runtime/src/scr_loop_epoll.c b/packages/runtime/src/scr_loop_epoll.c index 484ef36b6..8040c76fe 100644 --- a/packages/runtime/src/scr_loop_epoll.c +++ b/packages/runtime/src/scr_loop_epoll.c @@ -1,6 +1,6 @@ /* The Linux backend of the platform readiness contract (scr_platform.h): - * epoll for fd readiness, one timerfd per armed one-shot timer. This is - * the LIVE Linux poller behind scr_net.c and scr_dgram.c (native-toolchain.ts links it + * epoll for fd readiness, one timerfd for every armed one-shot timer. This + * is the LIVE Linux poller behind scr_net.c and scr_dgram.c (native-toolchain.ts links it * alongside scr_loop_kqueue.c whenever those units compile; each TU is * empty off its platform). Child-exit wakeups are NOT here: scr_child.c * keeps its narrow seam inline (kqueue EVFILT_PROC on BSD, pidfd_open + @@ -9,18 +9,22 @@ * * Design notes: * - Interest is keyed by fd with ONE mask under epoll, so the poller keeps - * a linear fd -> (mask, udata) table and merges read/write interest into - * EPOLL_CTL_MOD. Linear scan is fine at the units' scale (dozens of fds, - * drained in 64-event batches today); swap for a hash if profiles say so. + * an fd -> (mask, udata) table (indexed by fd number) and merges + * read/write interest into EPOLL_CTL_MOD. * - EPOLLHUP/EPOLLERR/EPOLLRDHUP report as READABLE (plus WRITABLE when * write interest is armed): the units learn EOF/errors from read()==0 or * errno exactly as they do from kqueue's EV_EOF today. Note an fd is in * the epoll set ONLY while some interest is armed, so a consumer-less * socket can never busy-report HUP. - * - Timers: EVFILT_TIMER has no epoll analogue, so each armed key owns a - * TFD_NONBLOCK timerfd registered EPOLLIN in the same epoll set, tagged - * through the same table (mask=TIMER). Fired timers are disarmed and - * their timerfd closed at delivery (kqueue's EV_ONESHOT semantics). + * - Timers: EVFILT_TIMER has no epoll analogue. Armed keys live in a + * deadline min-heap (a pointer hash finds a key's slot), and ONE timerfd + * in the epoll set wakes the loop at the earliest deadline. Re-arming, + * arming a later deadline and cancelling make no system call: the + * timerfd is reprogrammed only when a deadline earlier than its own + * arrives, or when it fires early (the heap then holds later deadlines). + * A server's per-request idle-timer churn therefore costs nothing in the + * kernel, like libuv's timer heap. Expired timers are delivered (and + * disarmed: kqueue's EV_ONESHOT semantics) by the next drain. * - Units must scrp_forget a watched fd before close(2) (the contract's * epoll obligation). As a second line of defense against a missed * forget followed by fd-number reuse, watch self-heals: a MOD that @@ -32,47 +36,75 @@ #include "scr_platform.h" #include +#include #include #include #include #include #include +#include #include /* ── fd/timer poller ─────────────────────────────────────────────────── */ typedef struct { - int fd; /* watched fd, or the timer's timerfd */ - unsigned mask; /* SCRP_READABLE|SCRP_WRITABLE, or SCRP_TIMER */ - void *udata; /* delivery tag; for timers: the arm-time udata */ - void *timer_key; /* timers only: the caller's key */ + int fd; /* watched fd */ + unsigned mask; /* SCRP_READABLE|SCRP_WRITABLE */ + void *udata; /* delivery tag */ } ScrpEntry; +typedef struct { + double deadline; /* CLOCK_MONOTONIC milliseconds */ + uint64_t seq; /* arm order: equal deadlines fire first-armed first */ + void *key; + void *udata; +} ScrpTimer; + +typedef struct { + void *key; + size_t idx; /* the key's heap index */ +} ScrpKeySlot; + struct ScrPoller { int ep; ScrpEntry *entries; size_t n, cap; + size_t *slot_of_fd; /* fd -> entry index + 1 (0: unwatched) */ + size_t fd_cap; + int tfd; /* the shared timerfd (-1 until a timer arms) */ + double tfd_deadline; /* what tfd is programmed for; INFINITY: disarmed */ + ScrpTimer *heap; + size_t nt, heap_cap; + ScrpKeySlot *key_slots; /* open addressing over keys (key NULL: empty) */ + size_t key_cap; /* power of two */ + uint64_t seq; }; static ScrpEntry *scrp_find_fd(ScrPoller *p, int fd) { - for (size_t i = 0; i < p->n; i++) - if (p->entries[i].fd == fd) return &p->entries[i]; - return NULL; -} - -static ScrpEntry *scrp_find_key(ScrPoller *p, void *key) { - for (size_t i = 0; i < p->n; i++) - if ((p->entries[i].mask & SCRP_TIMER) != 0 && p->entries[i].timer_key == key) return &p->entries[i]; - return NULL; + if (fd < 0 || (size_t)fd >= p->fd_cap || p->slot_of_fd[fd] == 0) return NULL; + return &p->entries[p->slot_of_fd[fd] - 1]; } static void scrp_remove(ScrPoller *p, ScrpEntry *e) { size_t i = (size_t)(e - p->entries); - p->entries[i] = p->entries[p->n - 1]; + p->slot_of_fd[e->fd] = 0; p->n--; + if (i != p->n) { + p->entries[i] = p->entries[p->n]; + p->slot_of_fd[p->entries[i].fd] = i + 1; + } } static bool scrp_push(ScrPoller *p, ScrpEntry e) { + if ((size_t)e.fd >= p->fd_cap) { + size_t cap = p->fd_cap == 0 ? 64 : p->fd_cap; + while (cap <= (size_t)e.fd) cap *= 2; + size_t *grown = realloc(p->slot_of_fd, cap * sizeof *grown); + if (grown == NULL) return false; + memset(grown + p->fd_cap, 0, (cap - p->fd_cap) * sizeof *grown); + p->slot_of_fd = grown; + p->fd_cap = cap; + } if (p->n == p->cap) { size_t cap = p->cap == 0 ? 16 : p->cap * 2; ScrpEntry *grown = realloc(p->entries, cap * sizeof *grown); @@ -81,6 +113,117 @@ static bool scrp_push(ScrPoller *p, ScrpEntry e) { p->cap = cap; } p->entries[p->n++] = e; + p->slot_of_fd[e.fd] = p->n; + return true; +} + +/* ── the timer heap ── */ + +static double scrp_now_ms(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1e6; +} + +static size_t scrp_key_hash(const ScrPoller *p, const void *key) { + uintptr_t h = (uintptr_t)key; + h ^= h >> 17; + h *= (uintptr_t)0x9E3779B97F4A7C15ull; + h ^= h >> 29; + return (size_t)h & (p->key_cap - 1); +} + +/* The hash slot holding `key`, or the empty slot where it would go. */ +static size_t scrp_key_probe(const ScrPoller *p, const void *key) { + size_t i = scrp_key_hash(p, key); + while (p->key_slots[i].key != NULL && p->key_slots[i].key != key) + i = (i + 1) & (p->key_cap - 1); + return i; +} + +static ScrpKeySlot *scrp_key_find(const ScrPoller *p, const void *key) { + if (p->key_cap == 0) return NULL; + size_t i = scrp_key_probe(p, key); + return p->key_slots[i].key != NULL ? &p->key_slots[i] : NULL; +} + +/* Backward-shift deletion keeps linear probing tombstone-free. */ +static void scrp_key_erase(ScrPoller *p, size_t i) { + size_t mask = p->key_cap - 1; + p->key_slots[i].key = NULL; + for (size_t j = (i + 1) & mask; p->key_slots[j].key != NULL; j = (j + 1) & mask) { + size_t home = scrp_key_hash(p, p->key_slots[j].key); + /* move j into the hole when its home is not cyclically within (i, j] */ + if (((j - home) & mask) >= ((j - i) & mask)) { + p->key_slots[i] = p->key_slots[j]; + p->key_slots[j].key = NULL; + i = j; + } + } +} + +static bool scrp_key_grow(ScrPoller *p) { + size_t cap = p->key_cap == 0 ? 64 : p->key_cap * 2; + ScrpKeySlot *slots = calloc(cap, sizeof *slots); + if (slots == NULL) return false; + free(p->key_slots); + p->key_slots = slots; + p->key_cap = cap; + for (size_t h = 0; h < p->nt; h++) + p->key_slots[scrp_key_probe(p, p->heap[h].key)] = (ScrpKeySlot){p->heap[h].key, h}; + return true; +} + +static bool scrp_timer_less(const ScrpTimer *a, const ScrpTimer *b) { + return a->deadline < b->deadline || (a->deadline == b->deadline && a->seq < b->seq); +} + +/* Place `t` at heap index `i` and record it in the key table. */ +static void scrp_heap_put(ScrPoller *p, size_t i, ScrpTimer t) { + p->heap[i] = t; + scrp_key_find(p, t.key)->idx = i; +} + +static void scrp_heap_fix(ScrPoller *p, size_t i) { + ScrpTimer t = p->heap[i]; + while (i > 0) { + size_t parent = (i - 1) / 2; + if (!scrp_timer_less(&t, &p->heap[parent])) break; + scrp_heap_put(p, i, p->heap[parent]); + i = parent; + } + for (;;) { + size_t child = 2 * i + 1; + if (child >= p->nt) break; + if (child + 1 < p->nt && scrp_timer_less(&p->heap[child + 1], &p->heap[child])) child++; + if (!scrp_timer_less(&p->heap[child], &t)) break; + scrp_heap_put(p, i, p->heap[child]); + i = child; + } + scrp_heap_put(p, i, t); +} + +static void scrp_heap_remove(ScrPoller *p, size_t i) { + scrp_key_erase(p, scrp_key_probe(p, p->heap[i].key)); + p->nt--; + if (i == p->nt) return; + p->heap[i] = p->heap[p->nt]; + scrp_heap_fix(p, i); +} + +/* Program the shared timerfd for `deadline` (INFINITY disarms). */ +static bool scrp_tfd_program(ScrPoller *p, double deadline) { + struct itimerspec its; + memset(&its, 0, sizeof its); + if (deadline != INFINITY) { + if (deadline < 0) deadline = 0; + its.it_value.tv_sec = (time_t)(deadline / 1000.0); + its.it_value.tv_nsec = (long)((deadline - (double)its.it_value.tv_sec * 1000.0) * 1e6); + if (its.it_value.tv_nsec >= 1000000000L) its.it_value.tv_nsec = 999999999L; + if (its.it_value.tv_sec == 0 && its.it_value.tv_nsec == 0) its.it_value.tv_nsec = 1; /* 0 disarms */ + } + if (timerfd_settime(p->tfd, TFD_TIMER_ABSTIME, &its, NULL) != 0) return false; + p->tfd_deadline = deadline; return true; } @@ -92,18 +235,21 @@ ScrPoller *scrp_poller_new(void) { free(p); return NULL; } + p->tfd = -1; + p->tfd_deadline = INFINITY; return p; } void scrp_poller_free(ScrPoller *p) { if (p == NULL) return; - for (size_t i = 0; i < p->n; i++) - if ((p->entries[i].mask & SCRP_TIMER) != 0) close(p->entries[i].fd); + if (p->tfd >= 0) close(p->tfd); close(p->ep); free(p->entries); + free(p->slot_of_fd); + free(p->heap); + free(p->key_slots); free(p); } - int scrp_poller_fd(const ScrPoller *p) { return p->ep; } static uint32_t scrp_epoll_mask(unsigned mask) { @@ -124,7 +270,7 @@ static bool scrp_watch(ScrPoller *p, int fd, void *udata, bool on, unsigned bit) struct epoll_event ev = {.events = scrp_epoll_mask(next), .data = {.fd = fd}}; if (e == NULL) { if (next == 0) return true; /* unwatch of an unwatched fd: no-op */ - if (!scrp_push(p, (ScrpEntry){fd, next, udata, NULL})) return false; + if (!scrp_push(p, (ScrpEntry){fd, next, udata})) return false; int rc = epoll_ctl(p->ep, EPOLL_CTL_ADD, fd, &ev); if (rc != 0 && errno == EEXIST) rc = epoll_ctl(p->ep, EPOLL_CTL_MOD, fd, &ev); if (rc != 0) { @@ -163,41 +309,51 @@ void scrp_forget(ScrPoller *p, int fd) { bool scrp_timer_arm(ScrPoller *p, void *key, double ms, void *udata) { if (!(ms >= 0)) ms = 0; - ScrpEntry *e = scrp_find_key(p, key); - int tfd = e != NULL ? e->fd : timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC); - if (tfd < 0) return false; - struct itimerspec its; - memset(&its, 0, sizeof its); - its.it_value.tv_sec = (time_t)(ms / 1000.0); - its.it_value.tv_nsec = (long)((ms - (double)its.it_value.tv_sec * 1000.0) * 1e6); - if (its.it_value.tv_sec == 0 && its.it_value.tv_nsec == 0) its.it_value.tv_nsec = 1; /* 0 disarms timerfd */ - if (timerfd_settime(tfd, 0, &its, NULL) != 0) { - if (e == NULL) close(tfd); - return false; + if (p->tfd < 0) { + int tfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC); + if (tfd < 0) return false; + struct epoll_event ev = {.events = EPOLLIN, .data = {.fd = tfd}}; + if (epoll_ctl(p->ep, EPOLL_CTL_ADD, tfd, &ev) != 0) { + close(tfd); + return false; + } + p->tfd = tfd; } - if (e != NULL) { - e->udata = udata; - return true; /* re-arm replaces the deadline */ + ScrpTimer t = {scrp_now_ms() + ms, p->seq++, key, udata}; + ScrpKeySlot *slot = scrp_key_find(p, key); + if (slot != NULL) { + /* re-arm replaces the deadline */ + size_t i = slot->idx; + p->heap[i] = t; + scrp_heap_fix(p, i); + } else { + if (p->nt == p->heap_cap) { + size_t cap = p->heap_cap == 0 ? 16 : p->heap_cap * 2; + ScrpTimer *grown = realloc(p->heap, cap * sizeof *grown); + if (grown == NULL) return false; + p->heap = grown; + p->heap_cap = cap; + } + if ((p->nt + 1) * 2 > p->key_cap && !scrp_key_grow(p)) return false; + p->heap[p->nt] = t; + p->key_slots[scrp_key_probe(p, key)] = (ScrpKeySlot){key, p->nt}; + p->nt++; + scrp_heap_fix(p, p->nt - 1); } - if (!scrp_push(p, (ScrpEntry){tfd, SCRP_TIMER, udata, key})) { - close(tfd); - return false; - } - struct epoll_event ev = {.events = EPOLLIN, .data = {.fd = tfd}}; - if (epoll_ctl(p->ep, EPOLL_CTL_ADD, tfd, &ev) != 0) { - scrp_remove(p, &p->entries[p->n - 1]); - close(tfd); + /* Only an earlier deadline needs the kernel: a later one is found when + * the timerfd fires early. */ + if (p->heap[0].deadline < p->tfd_deadline && !scrp_tfd_program(p, p->heap[0].deadline)) { + scrp_heap_remove(p, scrp_key_find(p, key)->idx); return false; } return true; } void scrp_timer_cancel(ScrPoller *p, void *key) { - ScrpEntry *e = scrp_find_key(p, key); - if (e == NULL) return; /* unarmed or already fired: no-op */ - (void)epoll_ctl(p->ep, EPOLL_CTL_DEL, e->fd, NULL); - close(e->fd); - scrp_remove(p, e); + ScrpKeySlot *slot = scrp_key_find(p, key); + if (slot == NULL) return; /* unarmed or already fired: no-op */ + /* the timerfd keeps its deadline; an early wake finds nothing due */ + scrp_heap_remove(p, slot->idx); } int scrp_drain(ScrPoller *p, ScrPollerEvent *out, int max) { @@ -205,20 +361,17 @@ int scrp_drain(ScrPoller *p, ScrPollerEvent *out, int max) { struct epoll_event evs[64]; int want = max < 64 ? max : 64; int n = epoll_wait(p->ep, evs, want, 0); - if (n <= 0) return 0; /* EINTR/none: a spurious pass */ int filled = 0; + bool timer_woke = false; for (int i = 0; i < n; i++) { - ScrpEntry *e = scrp_find_fd(p, evs[i].data.fd); - if (e == NULL) continue; /* raced with forget */ - if ((e->mask & SCRP_TIMER) != 0) { - out[filled].udata = e->udata; - out[filled++].events = SCRP_TIMER; - /* one-shot: disarm at delivery, EV_ONESHOT semantics */ - (void)epoll_ctl(p->ep, EPOLL_CTL_DEL, e->fd, NULL); - close(e->fd); - scrp_remove(p, e); + if (evs[i].data.fd == p->tfd && p->tfd >= 0) { + uint64_t ticks; + (void)!read(p->tfd, &ticks, sizeof ticks); + timer_woke = true; continue; } + ScrpEntry *e = scrp_find_fd(p, evs[i].data.fd); + if (e == NULL) continue; /* raced with forget */ unsigned got = 0; uint32_t ep = evs[i].events; if ((ep & (EPOLLIN | EPOLLHUP | EPOLLERR | EPOLLRDHUP)) != 0) got |= SCRP_READABLE; @@ -229,6 +382,21 @@ int scrp_drain(ScrPoller *p, ScrPollerEvent *out, int max) { out[filled].udata = e->udata; out[filled++].events = got; } + /* The timerfd always holds a deadline no later than the heap's earliest + * (arming an earlier one reprograms it), so a timer can only be due + * after it fires. */ + if (timer_woke) { + p->tfd_deadline = INFINITY; /* one-shot: it fired */ + /* deliver every expired timer the batch has room for, one-shot */ + double now = scrp_now_ms(); + while (p->nt > 0 && filled < max && p->heap[0].deadline <= now) { + out[filled].udata = p->heap[0].udata; + out[filled++].events = SCRP_TIMER; + scrp_heap_remove(p, 0); + } + /* the next deadline (one already past fires at once) */ + if (p->nt > 0) (void)scrp_tfd_program(p, p->heap[0].deadline); + } return filled; } diff --git a/packages/runtime/src/scr_loop_epoll.test.c b/packages/runtime/src/scr_loop_epoll.test.c new file mode 100644 index 000000000..6ab1621c5 --- /dev/null +++ b/packages/runtime/src/scr_loop_epoll.test.c @@ -0,0 +1,115 @@ +#undef NDEBUG +#define _GNU_SOURCE 1 +#include "scr_platform.h" +#include +#include +#include +#include +#include +#include +#include + +/* The epoll poller alone: timers live in a deadline heap behind one + * timerfd, so arming, re-arming and cancelling many keys must still + * deliver each armed key exactly once, in deadline order, after its + * deadline, and never a cancelled key. */ + +enum { KEYS = 2000 }; + +static double now_ms(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1e6; +} + +static int keys[KEYS]; +static double deadline[KEYS]; +static int state[KEYS]; /* 0 unarmed/cancelled, 1 armed, 2 fired */ + +/* Wait for the poller fd, then drain; returns the events drained. */ +static int pump(ScrPoller *p, ScrPollerEvent *evs, int max, int wait_ms) { + struct pollfd pfd = {.fd = scrp_poller_fd(p), .events = POLLIN}; + (void)poll(&pfd, 1, wait_ms); + return scrp_drain(p, evs, max); +} + +int main(void) { + ScrPoller *p = scrp_poller_new(); + assert(p != NULL); + srand(7); + double start = now_ms(); + /* Each deadline counts from its own arm call: arming takes time, here + * stretched by a pause halfway, and the poller reads the clock at each + * call. */ + for (int i = 0; i < KEYS; i++) { + if (i == KEYS / 2) + for (double until = now_ms() + 3; now_ms() < until;) { + } + double ms = 5 + rand() % 120; + double t = now_ms(); + assert(scrp_timer_arm(p, &keys[i], ms, &keys[i])); + deadline[i] = t + ms; + state[i] = 1; + } + /* re-arm a third later and earlier, cancel a third (the heap and the key + * table move entries on every one of these) */ + for (int i = 0; i < KEYS; i++) { + if (i % 3 == 0) { + double ms = 1 + rand() % 150; + double t = now_ms(); + assert(scrp_timer_arm(p, &keys[i], ms, &keys[i])); + deadline[i] = t + ms; + } else if (i % 3 == 1) { + scrp_timer_cancel(p, &keys[i]); + state[i] = 0; + } + } + scrp_timer_cancel(p, &keys[1]); /* cancel of a cancelled key: no-op */ + /* an fd delivers alongside the timers */ + int fds[2]; + assert(pipe(fds) == 0); + static int pipe_tag; + assert(scrp_watch_read(p, fds[0], &pipe_tag, true)); + assert(write(fds[1], "x", 1) == 1); + int armed = 0; + for (int i = 0; i < KEYS; i++) armed += state[i] == 1; + int fired = 0, piped = 0; + double last = 0; + ScrPollerEvent evs[16]; /* small batches leave due timers for the next drain */ + while (fired < armed) { + int n = pump(p, evs, 16, 1000); + for (int e = 0; e < n; e++) { + if (evs[e].udata == &pipe_tag) { + assert(evs[e].events == SCRP_READABLE); + char c; + assert(read(fds[0], &c, 1) == 1); + piped++; + continue; + } + assert(evs[e].events == SCRP_TIMER); + int i = (int)((int *)evs[e].udata - keys); + assert(i >= 0 && i < KEYS); + assert(state[i] == 1); + double t = now_ms(); + assert(t + 1.0 >= deadline[i]); /* never early (timerfd rounding aside) */ + assert(deadline[i] + 1.0 >= last); /* deadline order */ + if (deadline[i] > last) last = deadline[i]; + state[i] = 2; + fired++; + } + assert(now_ms() - start < 10000); + } + assert(piped == 1); + /* nothing else fires: the cancelled keys stay quiet */ + assert(pump(p, evs, 16, 200) == 0); + /* a key fires again after re-arming once it fired; 0 ms fires at once */ + assert(scrp_timer_arm(p, &keys[0], 0, &keys[0])); + int n = pump(p, evs, 16, 1000); + assert(n == 1 && evs[0].udata == &keys[0] && evs[0].events == SCRP_TIMER); + scrp_forget(p, fds[0]); + close(fds[0]); + close(fds[1]); + scrp_poller_free(p); + puts("epoll timers fire once each, in deadline order, never when cancelled"); + return 0; +} diff --git a/packages/runtime/src/scr_loop_epoll.test.ts b/packages/runtime/src/scr_loop_epoll.test.ts new file mode 100644 index 000000000..89b2e9c3d --- /dev/null +++ b/packages/runtime/src/scr_loop_epoll.test.ts @@ -0,0 +1,41 @@ +import { execFile } from "node:child_process"; +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { expect, test } from "vitest"; + +const exec = promisify(execFile); + +/* The Linux poller links alone (it needs only libc), under AddressSanitizer + * and UndefinedBehaviorSanitizer. */ +test.skipIf(process.platform !== "linux")( + "the epoll poller's timer heap fires each armed key once, in deadline order", + async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-epoll-")); + try { + const bin = join(dir, "epoll"); + await exec("clang", [ + "-std=c11", + "-O1", + "-g", + "-fsanitize=address,undefined", + join(import.meta.dirname, "scr_loop_epoll.test.c"), + join(import.meta.dirname, "scr_loop_epoll.c"), + "-lm", + "-o", + bin, + ]); + const result = await exec(bin, [], { + timeout: 60000, + env: { ...process.env, UBSAN_OPTIONS: "halt_on_error=1" }, + }); + expect(result.stdout).toBe( + "epoll timers fire once each, in deadline order, never when cancelled\n", + ); + expect(result.stderr).toBe(""); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }, +); diff --git a/packages/runtime/src/scr_map.c b/packages/runtime/src/scr_map.c index 99caa31a8..1a6a3fc00 100644 --- a/packages/runtime/src/scr_map.c +++ b/packages/runtime/src/scr_map.c @@ -220,47 +220,106 @@ static bool scr_map_key_eq(const ScrMap *m, uint64_t stored, uint64_t probe) { * the value: its hash is the key word's scr_key_mix, which is cheap to * recompute, and its stored keys are normalized (one NaN pattern, +0), so * a dead entry marks its key with a NaN pattern that no stored key has. + * A SCR_MAP_VAL_CREF map (string keys, compact class values) keeps 16-byte + * entries: the key, its 32-bit hash and the value as an address over 8. * Every other map keeps 24-byte entries with the cached hash, zero for a - * dead entry. Both layouts put the key at byte 0 and the value at byte 8. */ + * dead entry. Every layout puts the key at byte 0; the 8-byte layouts put + * the value at byte 8. */ static inline bool scr_map_num(const ScrMap *m) { return m->key_kind == SCR_MAP_KEY_F64; } +static inline bool scr_map_cref(const ScrMap *m) { + return __builtin_expect(m->val_kind == SCR_MAP_VAL_CREF, 0); +} + static inline size_t scr_map_entry_size(const ScrMap *m) { - return scr_map_num(m) ? sizeof(ScrMapNumEntry) : sizeof(ScrMapEntry); + if (scr_map_num(m)) return sizeof(ScrMapNumEntry); + if (scr_map_cref(m)) return sizeof(ScrMapCEntry); + return sizeof(ScrMapEntry); } /* The entry at index e. A number-key entry has no hash field: read its key - * and value only (scr_map_hash_at gives the hash). */ + * and value only (scr_map_hash_at gives the hash); a SCR_MAP_VAL_CREF + * entry's value is read with scr_map_val_at. */ static inline ScrMapEntry *scr_map_at(const ScrMap *m, size_t e) { return (ScrMapEntry *)((char *)m->entries + e * scr_map_entry_size(m)); } +static inline ScrMapCEntry *scr_map_c_at(const ScrMap *m, size_t e) { + return (ScrMapCEntry *)m->entries + e; +} + static inline bool scr_map_live_at(const ScrMap *m, size_t e) { - const ScrMapEntry *x = scr_map_at(m, e); - return scr_map_num(m) ? x->key != SCR_MAP_NUM_DEAD : x->hash != 0; + if (scr_map_num(m)) return ((const ScrMapNumEntry *)m->entries)[e].key != SCR_MAP_NUM_DEAD; + if (scr_map_cref(m)) return scr_map_c_at(m, e)->hash != 0; + return m->entries[e].hash != 0; } /* The entry's hash, nonzero when it is live and zero for a dead entry. */ static inline uint64_t scr_map_hash_at(const ScrMap *m, size_t e) { - const ScrMapEntry *x = scr_map_at(m, e); - if (!scr_map_num(m)) return x->hash; - if (x->key == SCR_MAP_NUM_DEAD) return 0; - uint64_t hash = scr_map_hash_word(x->key); - return hash ? hash : 1; + if (scr_map_num(m)) { + uint64_t key = ((const ScrMapNumEntry *)m->entries)[e].key; + if (key == SCR_MAP_NUM_DEAD) return 0; + uint64_t hash = scr_map_hash_word(key); + return hash ? hash : 1; + } + if (scr_map_cref(m)) { + uint32_t h = scr_map_c_at(m, e)->hash; + return h ? scr_map_hash_widen(h) : 0; + } + return m->entries[e].hash; } static inline void scr_map_kill_at(ScrMap *m, size_t e) { - ScrMapEntry *x = scr_map_at(m, e); - if (scr_map_num(m)) x->key = SCR_MAP_NUM_DEAD; - else x->hash = 0; + if (scr_map_num(m)) ((ScrMapNumEntry *)m->entries)[e].key = SCR_MAP_NUM_DEAD; + else if (scr_map_cref(m)) scr_map_c_at(m, e)->hash = 0; + else m->entries[e].hash = 0; } +/* A compact value's 4-byte form and back (scr_cref_encode in scr_array.h). */ +static inline uint32_t scr_map_cref_encode(uint64_t val) { return (uint32_t)(val >> 3); } +static inline uint64_t scr_map_cref_decode(uint32_t val) { return (uint64_t)val << 3; } + +/* The value of entry e as an 8-byte slot, and its store. */ +static inline uint64_t scr_map_val_at(const ScrMap *m, size_t e) { + if (scr_map_num(m)) return ((const ScrMapNumEntry *)m->entries)[e].val; + if (scr_map_cref(m)) return scr_map_cref_decode(scr_map_c_at(m, e)->val); + return m->entries[e].val; +} + +static inline void scr_map_val_put(ScrMap *m, size_t e, uint64_t val) { + if (scr_map_num(m)) ((ScrMapNumEntry *)m->entries)[e].val = val; + else if (scr_map_cref(m)) scr_map_c_at(m, e)->val = scr_map_cref_encode(val); + else m->entries[e].val = val; +} + +/* A reference value as a +1 result: compact values are never counted. */ +static inline void *scr_map_val_retain(const ScrMap *m, uint64_t val) { + if (scr_map_cref(m)) return scr_map_slot_to_ptr(val); + return m->ops->val_retain(scr_map_slot_to_ptr(val)); +} + +/* A SCR_MAP_VAL_CREF map's keys are strings, whose hashes are widened + * 32-bit values: the entry keeps the low half. */ static inline void scr_map_put_at(ScrMap *m, size_t e, uint64_t key, uint64_t val, uint64_t hash) { - ScrMapEntry *x = scr_map_at(m, e); + if (scr_map_num(m)) { + ScrMapNumEntry *x = (ScrMapNumEntry *)m->entries + e; + x->key = key; + x->val = val; + return; + } + if (scr_map_cref(m)) { + ScrMapCEntry *x = scr_map_c_at(m, e); + x->key = key; + x->hash = (uint32_t)hash; + x->val = scr_map_cref_encode(val); + return; + } + ScrMapEntry *x = m->entries + e; x->key = key; x->val = val; - if (!scr_map_num(m)) x->hash = hash; + x->hash = hash; } /* ── absent-key filter ───────────────────────────────────────────────── @@ -282,17 +341,25 @@ static inline uint64_t scr_map_filter_bits(uint64_t hash) { * before equality and survive table growth without hashing the keys again. * Remap zero to one so every live entry has a nonzero hash. */ /* SCR_MAP_LOOKUP_NUM is a word compare on a number-key map, so the probe - * reads 16-byte entries; every other lookup is on a map with 24-byte - * entries. Knowing the layout from the lookup keeps the entry stride a - * constant in the inlined probes. */ + * reads 16-byte entries; SCR_MAP_LOOKUP_STR_C and SPAN_C are the string and + * span probes of a SCR_MAP_VAL_CREF map, whose 16-byte entries keep a + * 32-bit hash; every other lookup is on a map with 24-byte entries. Knowing + * the layout from the lookup keeps the entry stride a constant in the + * inlined probes. */ typedef enum { SCR_MAP_LOOKUP_STR, SCR_MAP_LOOKUP_SPAN, SCR_MAP_LOOKUP_WORD, SCR_MAP_LOOKUP_NUM, - SCR_MAP_LOOKUP_GENERIC + SCR_MAP_LOOKUP_GENERIC, + SCR_MAP_LOOKUP_STR_C, + SCR_MAP_LOOKUP_SPAN_C } ScrMapLookup; +static inline bool scr_map_lookup_c(ScrMapLookup lookup) { + return lookup == SCR_MAP_LOOKUP_STR_C || lookup == SCR_MAP_LOOKUP_SPAN_C; +} + typedef struct { const char *bytes; size_t length; } ScrMapSpan; /* Choose equality once per operation. Inlining the traversal with a constant @@ -301,12 +368,12 @@ typedef struct { const char *bytes; size_t length; } ScrMapSpan; static inline bool scr_map_lookup_eq(const ScrMap *m, uint64_t stored, uint64_t key, ScrMapLookup lookup) { if (lookup == SCR_MAP_LOOKUP_WORD || lookup == SCR_MAP_LOOKUP_NUM) return stored == key; - if (lookup == SCR_MAP_LOOKUP_SPAN) { + if (lookup == SCR_MAP_LOOKUP_SPAN || lookup == SCR_MAP_LOOKUP_SPAN_C) { const ScrStr *a = scr_map_slot_to_ptr(stored); const ScrMapSpan *b = scr_map_slot_to_ptr(key); return a->len == b->length && scr_key_equal(a->data, b->bytes, a->len); } - if (lookup == SCR_MAP_LOOKUP_STR) { + if (lookup == SCR_MAP_LOOKUP_STR || lookup == SCR_MAP_LOOKUP_STR_C) { const ScrStr *a = scr_map_slot_to_ptr(stored); const ScrStr *b = scr_map_slot_to_ptr(key); return a == b || (a->len == b->len && scr_key_equal(a->data, b->data, a->len)); @@ -318,6 +385,7 @@ static inline bool scr_map_lookup_eq(const ScrMap *m, uint64_t stored, uint64_t static inline __attribute__((always_inline)) ScrMapEntry *scr_map_at_with(const ScrMap *m, size_t e, ScrMapLookup lookup) { if (lookup == SCR_MAP_LOOKUP_NUM) return (ScrMapEntry *)((ScrMapNumEntry *)m->entries + e); + if (scr_map_lookup_c(lookup)) return (ScrMapEntry *)((ScrMapCEntry *)m->entries + e); return &m->entries[e]; } @@ -328,6 +396,8 @@ static inline __attribute__((always_inline)) bool scr_map_match_at( const ScrMap *m, size_t e, uint64_t hash, uint64_t key, ScrMapLookup lookup) { const ScrMapEntry *x = scr_map_at_with(m, e, lookup); if (lookup == SCR_MAP_LOOKUP_NUM) return x->key == key; + if (scr_map_lookup_c(lookup)) + return ((const ScrMapCEntry *)x)->hash == (uint32_t)hash && scr_map_lookup_eq(m, x->key, key, lookup); return x->hash == hash && scr_map_lookup_eq(m, x->key, key, lookup); } @@ -437,7 +507,7 @@ static inline __attribute__((always_inline)) size_t scr_map_find_with( static ScrMapLookup scr_map_lookup_kind(const ScrMap *m) { switch (m->key_kind) { - case SCR_MAP_KEY_STR: return SCR_MAP_LOOKUP_STR; + case SCR_MAP_KEY_STR: return scr_map_cref(m) ? SCR_MAP_LOOKUP_STR_C : SCR_MAP_LOOKUP_STR; case SCR_MAP_KEY_F64: return SCR_MAP_LOOKUP_NUM; case SCR_MAP_KEY_BOOL: case SCR_MAP_KEY_REF: @@ -484,6 +554,7 @@ static size_t scr_map_find(const ScrMap *m, uint64_t hash, uint64_t key) { if (m->ndense) return scr_map_find_dense(m, scr_map_slot_to_f64(key)); switch (scr_map_lookup_kind(m)) { case SCR_MAP_LOOKUP_STR: return scr_map_find_with(m, hash, key, SCR_MAP_LOOKUP_STR); + case SCR_MAP_LOOKUP_STR_C: return scr_map_find_with(m, hash, key, SCR_MAP_LOOKUP_STR_C); case SCR_MAP_LOOKUP_WORD: return scr_map_find_with(m, hash, key, SCR_MAP_LOOKUP_WORD); case SCR_MAP_LOOKUP_NUM: return scr_map_find_with(m, hash, key, SCR_MAP_LOOKUP_NUM); default: return scr_map_find_with(m, hash, key, SCR_MAP_LOOKUP_GENERIC); @@ -504,9 +575,24 @@ const ScrMapEntry *scr_map_entry_f64(const ScrMap *m, double key) { /* The key's hash is usually cached, so even the small linear scan rejects * entries by hash before touching their bytes. */ +/* The probes of a SCR_MAP_VAL_CREF map, out of line so that the inlined + * probes of other maps keep their size. */ +static __attribute__((noinline)) size_t scr_map_find_str_c(const ScrMap *m, const ScrStr *key) { + return scr_map_find_with(m, scr_map_hash_str(key), scr_map_slot_from_ptr((void *)key), + SCR_MAP_LOOKUP_STR_C); +} + +static __attribute__((noinline)) size_t scr_map_find_span_c(const ScrMap *m, const char *bytes, + size_t length) { + ScrMapSpan key = { bytes, length }; + return scr_map_find_with(m, scr_map_hash_bytes(bytes, length), scr_map_slot_from_ptr(&key), + SCR_MAP_LOOKUP_SPAN_C); +} + static inline __attribute__((always_inline)) size_t scr_map_find_str(const ScrMap *m, const ScrStr *key) { if (m->nlive == 0) return SCR_MAP_EMPTY; + if (scr_map_cref(m)) return scr_map_find_str_c(m, key); return scr_map_find_with(m, scr_map_hash_str(key), scr_map_slot_from_ptr((void *)key), SCR_MAP_LOOKUP_STR); } @@ -515,6 +601,7 @@ static inline __attribute__((always_inline)) size_t scr_map_find_str(const ScrMa * rules as a string key without allocating or copying the probe. */ static size_t scr_map_find_span(const ScrMap *m, const char *bytes, size_t length) { if (m->nlive == 0) return SCR_MAP_EMPTY; + if (scr_map_cref(m)) return scr_map_find_span_c(m, bytes, length); ScrMapSpan key = { bytes, length }; return scr_map_find_with(m, scr_map_hash_bytes(bytes, length), scr_map_slot_from_ptr(&key), SCR_MAP_LOOKUP_SPAN); @@ -523,20 +610,20 @@ static size_t scr_map_find_span(const ScrMap *m, const char *bytes, size_t lengt bool scr_map_get_span_f64(const ScrMap *m, const char *bytes, size_t length, double *out) { size_t e = scr_map_find_span(m, bytes, length); if (e == SCR_MAP_EMPTY) return false; - *out = scr_map_slot_to_f64(scr_map_at(m, e)->val); + *out = scr_map_slot_to_f64(scr_map_val_at(m, e)); return true; } bool scr_map_get_span_bool(const ScrMap *m, const char *bytes, size_t length, bool *out) { size_t e = scr_map_find_span(m, bytes, length); if (e == SCR_MAP_EMPTY) return false; - *out = scr_map_at(m, e)->val != 0; + *out = scr_map_val_at(m, e) != 0; return true; } void *scr_map_get_span_ref(const ScrMap *m, const char *bytes, size_t length) { size_t e = scr_map_find_span(m, bytes, length); - return e == SCR_MAP_EMPTY ? NULL : m->ops->val_retain(scr_map_slot_to_ptr(scr_map_at(m, e)->val)); + return e == SCR_MAP_EMPTY ? NULL : scr_map_val_retain(m, scr_map_val_at(m, e)); } /* Rebuild the bucket table (size must be a power of two >= 2 * nentries): @@ -666,7 +753,12 @@ static void scr_map_compact(ScrMap *m, bool reindex) { if (k > top) top = k; } filter |= scr_map_filter_bits(scr_map_hash_at(m, r)); - if (w != r) memcpy(scr_map_at(m, w), scr_map_at(m, r), scr_map_entry_size(m)); + if (w != r) { + /* A constant size per layout keeps the copy inline. */ + if (scr_map_num(m)) memcpy(scr_map_at(m, w), scr_map_at(m, r), sizeof(ScrMapNumEntry)); + else if (scr_map_cref(m)) memcpy(scr_map_at(m, w), scr_map_at(m, r), sizeof(ScrMapCEntry)); + else memcpy(scr_map_at(m, w), scr_map_at(m, r), sizeof(ScrMapEntry)); + } w++; } m->nentries = (uint32_t)w; @@ -778,7 +870,7 @@ static void scr_map_trace(void *o, ScrTraceVisit visit, void *ctx) { for (size_t e = 0; e < m->nentries; e++) { if (!scr_map_live_at(m, e)) continue; if (keys) visit(scr_map_slot_to_ptr(scr_map_at(m, e)->key), ctx); - if (vals) visit(scr_map_slot_to_ptr(scr_map_at(m, e)->val), ctx); + if (vals) visit(scr_map_slot_to_ptr(scr_map_val_at(m, e)), ctx); } } @@ -791,7 +883,7 @@ static void scr_map_gcfree(void *o) { for (size_t e = 0; e < m->nentries; e++) { if (!scr_map_live_at(m, e)) continue; if (!keys) scr_map_release_key(m, scr_map_at(m, e)->key); - if (!vals) scr_map_release_val(m, scr_map_at(m, e)->val); + if (!vals) scr_map_release_val(m, scr_map_val_at(m, e)); } scr_mem_free(m->entries); scr_mem_free(m->buckets); @@ -843,10 +935,15 @@ ScrMap *scr_map_retain(ScrMap *m) { static void scr_map_destroy(void *object) { ScrMap *m = object; scr_weak_dispose_inline(m); - for (size_t e = 0; e < m->nentries; e++) { - if (!scr_map_live_at(m, e)) continue; - scr_map_release_key(m, scr_map_at(m, e)->key); - scr_map_release_val(m, scr_map_at(m, e)->val); + /* Number and boolean keys, and values other than references, own + * nothing: a map of only those releases no entries. */ + bool keys = m->key_kind == SCR_MAP_KEY_STR || scr_map_ref_key(m->key_kind); + if (keys || m->val_kind == SCR_MAP_VAL_REF) { + for (size_t e = 0; e < m->nentries; e++) { + if (!scr_map_live_at(m, e)) continue; + scr_map_release_key(m, scr_map_at(m, e)->key); + scr_map_release_val(m, scr_map_val_at(m, e)); + } } scr_mem_free(m->entries); scr_mem_free(m->buckets); @@ -897,7 +994,7 @@ void scr_map_clear(ScrMap *m) { uint32_t *dense = m->ndense ? m->dense : NULL; for (size_t e = 0; e < walked; e++) { if (!scr_map_live_at(m, e)) continue; - uint64_t key = scr_map_at(m, e)->key, val = scr_map_at(m, e)->val; + uint64_t key = scr_map_at(m, e)->key, val = scr_map_val_at(m, e); scr_map_kill_at(m, e); if (dense) dense[(size_t)scr_map_slot_to_f64(key)] = 0; scr_map_release_key(m, key); @@ -955,7 +1052,7 @@ bool scr_map_has_str(const ScrMap *m, const ScrStr *key) { static bool scr_map_delete_found(ScrMap *m, size_t e) { if (e == SCR_MAP_EMPTY) return false; - uint64_t key = scr_map_at(m, e)->key, val = scr_map_at(m, e)->val; + uint64_t key = scr_map_at(m, e)->key, val = scr_map_val_at(m, e); scr_map_kill_at(m, e); /* bucket slot stays: probe chains intact */ m->nlive--; if (m->ndense) m->dense[(size_t)scr_map_slot_to_f64(key)] = 0; @@ -1060,6 +1157,11 @@ static inline __attribute__((always_inline)) void scr_map_set_with( e = scr_map_bucket(m, slot); } if (e != SCR_MAP_EMPTY) { + if (scr_map_lookup_c(lookup)) { + /* Compact values are never counted: nothing to release. */ + ((ScrMapCEntry *)m->entries)[e].val = scr_map_cref_encode(val); + return; + } ScrMapEntry *x = scr_map_at_with(m, e, lookup); uint64_t old = x->val; x->val = val; /* unlink before releasing (cycle collector) */ @@ -1069,7 +1171,18 @@ static inline __attribute__((always_inline)) void scr_map_set_with( scr_map_append(m, hash, key, val, slot); } +/* The insert of a SCR_MAP_VAL_CREF map, out of line so that the string-key + * insert of other maps keeps its size. */ +static __attribute__((noinline)) void scr_map_set_str_c(ScrMap *m, uint64_t hash, uint64_t key, + uint64_t val) { + scr_map_set_with(m, hash, key, val, SCR_MAP_LOOKUP_STR_C); +} + static void scr_map_set_str_slot(ScrMap *m, uint64_t hash, uint64_t key, uint64_t val) { + if (scr_map_cref(m)) { + scr_map_set_str_c(m, hash, key, val); + return; + } scr_map_set_with(m, hash, key, val, SCR_MAP_LOOKUP_STR); } @@ -1157,6 +1270,7 @@ static void scr_map_set(ScrMap *m, uint64_t hash, uint64_t key, uint64_t val) { } switch (scr_map_lookup_kind(m)) { case SCR_MAP_LOOKUP_STR: scr_map_set_str_slot(m, hash, key, val); return; + case SCR_MAP_LOOKUP_STR_C: scr_map_set_str_c(m, hash, key, val); return; case SCR_MAP_LOOKUP_WORD: scr_map_set_word(m, hash, key, val); return; case SCR_MAP_LOOKUP_NUM: scr_map_set_num(m, hash, key, val); return; default: scr_map_set_with(m, hash, key, val, SCR_MAP_LOOKUP_GENERIC); return; @@ -1246,10 +1360,14 @@ bool scr_map_get_str_bool(const ScrMap *m, const ScrStr *key, bool *out) { return true; } +/* scr_map_entry_str serves maps with 24-byte entries (generated code reads + * a SCR_MAP_VAL_CREF map through scr_map_entry_str_c). */ static __attribute__((noinline)) const ScrMapEntry *scr_map_entry_str_slow(const ScrMap *m, const ScrStr *key) { - size_t e = scr_map_find_str(m, key); - return e == SCR_MAP_EMPTY ? NULL : scr_map_at(m, e); + if (m->nlive == 0) return NULL; + size_t e = scr_map_find_with(m, scr_map_hash_str(key), scr_map_slot_from_ptr((void *)key), + SCR_MAP_LOOKUP_STR); + return e == SCR_MAP_EMPTY ? NULL : &m->entries[e]; } /* A stored key with the probe's hash but another identity: compare bytes. @@ -1302,10 +1420,60 @@ const ScrMapEntry *scr_map_entry_str(const ScrMap *m, const ScrStr *key) { return scr_map_entry_str_slow(m, key); } +static __attribute__((noinline)) const ScrMapCEntry *scr_map_entry_str_c_slow(const ScrMap *m, + const ScrStr *key) { + size_t e = scr_map_find_str(m, key); + return e == SCR_MAP_EMPTY ? NULL : scr_map_c_at(m, e); +} + +static __attribute__((noinline)) const ScrMapCEntry *scr_map_entry_str_c_bytes(const ScrMap *m, + const ScrStr *key, + const ScrMapCEntry *e) { + const ScrStr *stored = scr_map_slot_to_ptr(e->key); + if (stored->len == key->len && scr_key_equal(stored->data, key->data, key->len)) return e; + return scr_map_entry_str_c_slow(m, key); +} + +/* scr_map_entry_str over a SCR_MAP_VAL_CREF map's 16-byte entries, whose + * hash is the low half of the widened one. */ +const ScrMapCEntry *scr_map_entry_str_c(const ScrMap *m, const ScrStr *key) { +#if SCR_STR_HASH_CACHE + uint32_t h = key->hash; + size_t nbuckets = m->nbuckets; + if (__builtin_expect(h != 0 && scr_map_compact_buckets(nbuckets), 1)) { + uint64_t hash = scr_map_hash_widen(h); + uint64_t bits = scr_map_filter_bits(hash); + if ((m->filter & bits) != bits) return NULL; + const ScrMapCEntry *entries = (const ScrMapCEntry *)m->entries; + if (nbuckets == 0) { + for (size_t e = 0, n = m->nentries; e < n; e++) { + if (entries[e].hash != h) continue; + if (entries[e].key == scr_map_slot_from_ptr((void *)key)) return &entries[e]; + return scr_map_entry_str_c_bytes(m, key, &entries[e]); + } + return NULL; + } + const uint32_t *buckets = m->buckets; + uint32_t tag = (uint32_t)(hash >> 32 & UINT32_C(0xff000000)); + size_t mask = nbuckets - 1; + for (size_t i = hash & mask;; i = (i + 1) & mask) { + uint32_t bucket = buckets[i]; + if (!bucket) return NULL; + if ((bucket & ~SCR_MAP_INDEX_MASK) != tag) continue; + const ScrMapCEntry *e = &entries[(bucket & SCR_MAP_INDEX_MASK) - 1]; + if (e->hash != h) continue; + if (e->key == scr_map_slot_from_ptr((void *)key)) return e; + return scr_map_entry_str_c_bytes(m, key, e); + } + } +#endif + return scr_map_entry_str_c_slow(m, key); +} + void *scr_map_get_str_ref(const ScrMap *m, const ScrStr *key) { size_t e = scr_map_find_str(m, key); if (e == SCR_MAP_EMPTY) return NULL; - return m->ops->val_retain(scr_map_slot_to_ptr(scr_map_at(m, e)->val)); /* +1 */ + return scr_map_val_retain(m, scr_map_val_at(m, e)); /* +1 */ } bool scr_map_get_ref_f64(const ScrMap *m, const void *key, double *out) { @@ -1408,6 +1576,12 @@ static const ScrMapEntry *scr_map_iter_at(const ScrMap *m, double i) { return scr_map_at(m, (size_t)i); } +/* The value of the live entry at iteration index i as an 8-byte slot. */ +static uint64_t scr_map_iter_val_slot(const ScrMap *m, double i) { + (void)scr_map_iter_at(m, i); + return scr_map_val_at(m, (size_t)i); +} + double scr_map_iter_key_f64(const ScrMap *m, double i) { return scr_map_slot_to_f64(scr_map_iter_at(m, i)->key); } @@ -1442,7 +1616,7 @@ ScrDyn *scr_map_dyn_key(const ScrMap *map, double index) { } ScrDyn *scr_map_dyn_value(const ScrMap *map, double index) { - uint64_t slot = scr_map_iter_at(map, index)->val; + uint64_t slot = scr_map_iter_val_slot(map, index); return map->dyn_ops ? map->dyn_ops->val_box(slot) : scr_dyn_retain(scr_map_slot_to_ptr(slot)); } @@ -1491,6 +1665,9 @@ ScrMap *scr_map_clone(const ScrMap *source, bool keys_only, const ScrMapOps *ops if (!out->entries) scr_map_oom(); out->ecap = (uint32_t)count; size_t top = 0; + /* A key-only copy of a SCR_MAP_VAL_CREF map has the 24-byte layout: its + * entries are copied field by field. */ + bool same_layout = scr_map_cref(source) == scr_map_cref(out); for (size_t i = 0; i < source->nentries; i++) { if (!scr_map_live_at(source, i)) continue; const ScrMapEntry *entry = scr_map_at(source, i); @@ -1498,11 +1675,13 @@ ScrMap *scr_map_clone(const ScrMap *source, bool keys_only, const ScrMapOps *ops size_t k = (size_t)scr_map_slot_to_f64(entry->key); if (k > top) top = k; } - ScrMapEntry *copy = scr_map_at(out, out->nentries++); - memcpy(copy, entry, entry_size); + size_t e = out->nentries++; + ScrMapEntry *copy = scr_map_at(out, e); + if (__builtin_expect(same_layout, 1)) memcpy(copy, entry, entry_size); + else scr_map_put_at(out, e, entry->key, keys_only ? 0 : scr_map_val_at(source, i), scr_map_hash_at(source, i)); if (out->key_kind == SCR_MAP_KEY_STR) scr_str_retain(scr_map_slot_to_ptr(copy->key)); else if (scr_map_ref_key(out->key_kind)) out->ops->key_retain(scr_map_slot_to_ptr(copy->key)); - if (keys_only) copy->val = 0; + if (keys_only) scr_map_val_put(out, e, 0); else if (out->val_kind == SCR_MAP_VAL_REF) out->ops->val_retain(scr_map_slot_to_ptr(copy->val)); } out->nlive = (uint32_t)count; @@ -1527,7 +1706,7 @@ ScrMap *scr_map_clone(const ScrMap *source, bool keys_only, const ScrMapOps *ops const ScrMapOps scr_map_dyn_set_ops = { scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v, NULL, NULL, NULL, NULL }; -static const ScrMapOps scr_map_dyn_map_ops = { +const ScrMapOps scr_map_dyn_map_ops = { scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v, scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v, NULL }; @@ -1550,7 +1729,7 @@ ScrMap *scr_map_clone_dyn(const ScrMap *source, bool keys_only) { void scr_map_values_into(ScrMap *set, const ScrMap *source, bool checked) { for (size_t i = 0; i < source->nentries; i++) { if (!scr_map_live_at(source, i)) continue; - uint64_t value = checked ? scr_map_slot_from_ptr(scr_map_dyn_value(source, (double)i)) : scr_map_at(source, i)->val; + uint64_t value = checked ? scr_map_slot_from_ptr(scr_map_dyn_value(source, (double)i)) : scr_map_val_at(source, i); if (set->key_kind == SCR_MAP_KEY_F64) value = scr_map_f64_bits(scr_map_slot_to_f64(value)); scr_map_set(set, scr_map_hash_key(set, value), value, 0); if (checked) scr_dyn_release(scr_map_slot_to_ptr(value)); @@ -1567,7 +1746,7 @@ ScrArr *scr_set_to_arr_dyn(const ScrMap *map) { } void *scr_map_iter_val_ref(const ScrMap *m, double i) { - return m->ops->val_retain(scr_map_slot_to_ptr(scr_map_iter_at(m, i)->val)); /* +1 */ + return scr_map_val_retain(m, scr_map_iter_val_slot(m, i)); /* +1 */ } /* A published (immortal) map never changes, so iterating it needs no @@ -1600,7 +1779,7 @@ void scr_set_add_all(ScrMap *set, ScrArr *values) { /* Packed values stay owned by the seed array throughout construction. * Avoid taking a temporary owner only to release it after insertion. * Sparse/missing positions retain the existing accessor semantics. */ - if (i < values->cap && *scr_arr_state_ptr(values, i) == SCR_ARR_VALUE) { + if (i < values->cap && scr_arr_dense_state(values, i) == SCR_ARR_VALUE) { uint64_t key = scr_arr_dense_get(values, i); if (set->key_kind == SCR_MAP_KEY_F64) key = scr_map_f64_bits(scr_map_slot_to_f64(key)); scr_map_set(set, scr_map_hash_key(set, key), key, 0); diff --git a/packages/runtime/src/scr_map.h b/packages/runtime/src/scr_map.h index c61df9a90..d1ca387bf 100644 --- a/packages/runtime/src/scr_map.h +++ b/packages/runtime/src/scr_map.h @@ -50,7 +50,11 @@ typedef enum { SCR_MAP_KEY_BIGINT, SCR_MAP_KEY_BOOL, SCR_MAP_KEY_UNION_VALUE, SCR_MAP_KEY_NULL, SCR_MAP_KEY_UNDEFINED /* tag-only union arms */ } ScrMapKeyKind; -typedef enum { SCR_MAP_VAL_F64, SCR_MAP_VAL_BOOL, SCR_MAP_VAL_REF } ScrMapValKind; +/* SCR_MAP_VAL_CREF: a string-key map whose values are compact class + * instances in the program-lifetime region (llvm/classes.ts + * LlClassMeta.compact). Its entries are ScrMapCEntry; the values are never + * counted or traced. */ +typedef enum { SCR_MAP_VAL_F64, SCR_MAP_VAL_BOOL, SCR_MAP_VAL_REF, SCR_MAP_VAL_CREF } ScrMapValKind; typedef struct { uint64_t key; /* double bits (normalized), ScrStr* (owned), or ref ptr (owned) */ @@ -65,6 +69,15 @@ typedef struct { uint64_t val; } ScrMapNumEntry; #define SCR_MAP_NUM_DEAD UINT64_C(0x7ff0000000000001) +/* A SCR_MAP_VAL_CREF map's entry: the string key, the key's 32-bit hash + * (its cached 64-bit hash is the same value in both halves, + * scr_map.c scr_map_hash_widen; zero = tombstone), and the value as an + * address over 8 (0 for NULL). */ +typedef struct { + uint64_t key; + uint32_t hash; + uint32_t val; +} ScrMapCEntry; /* Checked views use the original storage. Slot converters borrow on reads * and return owned references on writes, just like typed Map operations. */ @@ -140,16 +153,25 @@ typedef struct ScrMap { * the whole deleted prefix again. */ uint32_t head; } ScrMap; -/* The entry at index i of either layout, and whether it is live (code that - * walks a map's entries outside scr_map.c). Key and value are at bytes 0 - * and 8 in both layouts; a number-key entry has no hash field. */ +/* The entry at index i of any layout, whether it is live, and its value as + * an 8-byte slot (code that walks a map's entries outside scr_map.c). The + * key is at byte 0 in every layout; a number-key entry has no hash field, + * and a SCR_MAP_VAL_CREF entry keeps its value in 4 bytes. */ static inline const ScrMapEntry *scr_map_entry_at(const ScrMap *m, size_t i) { - size_t size = m->key_kind == SCR_MAP_KEY_F64 ? sizeof(ScrMapNumEntry) : sizeof(ScrMapEntry); + size_t size = m->key_kind == SCR_MAP_KEY_F64 ? sizeof(ScrMapNumEntry) + : m->val_kind == SCR_MAP_VAL_CREF ? sizeof(ScrMapCEntry) + : sizeof(ScrMapEntry); return (const ScrMapEntry *)((const char *)m->entries + i * size); } static inline bool scr_map_entry_live(const ScrMap *m, size_t i) { const ScrMapEntry *e = scr_map_entry_at(m, i); - return m->key_kind == SCR_MAP_KEY_F64 ? e->key != SCR_MAP_NUM_DEAD : e->hash != 0; + if (m->key_kind == SCR_MAP_KEY_F64) return e->key != SCR_MAP_NUM_DEAD; + if (m->val_kind == SCR_MAP_VAL_CREF) return ((const ScrMapCEntry *)e)->hash != 0; + return e->hash != 0; +} +static inline uint64_t scr_map_entry_val(const ScrMap *m, const ScrMapEntry *e) { + if (m->val_kind == SCR_MAP_VAL_CREF) return (uint64_t)((const ScrMapCEntry *)e)->val << 3; + return e->val; } /* The LLVM backend reads the header through two types (emitter.ts). Its * inline retain/release test the traced byte as %ScrMapRc field 3, as @@ -162,7 +184,8 @@ static inline bool scr_map_entry_live(const ScrMap *m, size_t i) { * third field (sc_map_iter_entry), or for a number-key map its key against * SCR_MAP_NUM_DEAD (sc_map_iter_entry_num). Iteration over a checked * (unknown-key) Map or Set picks one of the two steps from the key kind, - * %ScrMapIx field 1, compared with SCR_MAP_KEY_F64 (0). */ + * %ScrMapIx field 1, compared with SCR_MAP_KEY_F64 (0). Map.size + * (sc_map_size) reads the live count, field 9, as scr_map_size does. */ _Static_assert(offsetof(ScrMap, key_kind) == sizeof(size_t) && SCR_MAP_KEY_F64 == 0, "inline iteration over checked collections reads ScrMap.key_kind as %ScrMapIx field 1"); _Static_assert(offsetof(ScrMap, traced) == sizeof(size_t) + 2, @@ -193,6 +216,9 @@ _Static_assert(sizeof(ScrMapEntry) == 24 && offsetof(ScrMapEntry, val) == 8, "inline lookups read ScrMapEntry.val at byte 8"); _Static_assert(sizeof(ScrMapNumEntry) == 16 && offsetof(ScrMapNumEntry, val) == 8, "inline number-key lookups and iteration read %ScrMapNumEntry { key, val }"); +_Static_assert(sizeof(ScrMapCEntry) == 16 && offsetof(ScrMapCEntry, hash) == 8 && + offsetof(ScrMapCEntry, val) == 12, + "inline compact-value lookups and iteration read %ScrMapCEntry { key, hash, val }"); _Static_assert(sizeof(ScrMap) <= 11 * sizeof(uint64_t), "the ScrMap header stays within 88 bytes"); /* Entry for a number key, or NULL when absent. The pointer is valid until * the next mutation; generated code reads the value slot through it. */ @@ -201,6 +227,9 @@ const ScrMapEntry *scr_map_entry_f64(const ScrMap *m, double key); * reference reads retain the value with its static type's retain instead * of the collection's adapter. */ const ScrMapEntry *scr_map_entry_str(const ScrMap *m, const ScrStr *key); +/* scr_map_entry_str for a SCR_MAP_VAL_CREF map: its entry is a + * ScrMapCEntry. */ +const ScrMapCEntry *scr_map_entry_str_c(const ScrMap *m, const ScrStr *key); /* String-key lookups borrow an exact byte span. Reference results return * +1; scalar outputs are written only on a hit, like the string getters. */ @@ -214,8 +243,10 @@ void *scr_map_get_span_ref(const ScrMap *m, const char *bytes, size_t length); * adapters); NULL keeps the source's descriptor. */ ScrMap *scr_map_clone(const ScrMap *source, bool keys_only, const ScrMapOps *ops); ScrMap *scr_map_clone_dyn(const ScrMap *source, bool keys_only); -/* The descriptor of a Set of checked values (SCR_MAP_KEY_DYN keys). */ +/* The descriptors of a Set and a Map of checked values (SCR_MAP_KEY_DYN + * keys; the Map's values are checked values too). */ extern const ScrMapOps scr_map_dyn_set_ops; +extern const ScrMapOps scr_map_dyn_map_ops; void scr_map_values_into(ScrMap *set, const ScrMap *source, bool checked); void scr_map_dyn_attach(ScrMap *map, const ScrMapDynOps *ops); diff --git a/packages/runtime/src/scr_node_builtin.c b/packages/runtime/src/scr_node_builtin.c index 233617092..c0031e67b 100644 --- a/packages/runtime/src/scr_node_builtin.c +++ b/packages/runtime/src/scr_node_builtin.c @@ -90,6 +90,10 @@ static SCR_TL ScrDyn *scr_global_own; * 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; +/* The compiler's immortal name table (one per program) whose names + * scr_global_known holds; cleared with the tables, so a library re-init + * registers the names again. */ +static SCR_TL const ScrArr *scr_global_registered; 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); @@ -190,6 +194,7 @@ static void scr_global_cleanup(void) { scr_dyn_release(scr_global_own); scr_global_known = NULL; scr_global_own = NULL; + scr_global_registered = NULL; } static void *scr_global_retain(void *handle) { return handle; } @@ -246,8 +251,12 @@ ScrDyn *scr_global_object(void) { } ScrDyn *scr_global_native_init(ScrArr *known, ScrDyn *(*console_get)(void)) { + /* The compiler passes one immortal name table per program: once it is + * registered, later reads of the global object only take a reference. */ + if (known == scr_global_registered && scr_global_box) return scr_dyn_retain(scr_global_box); ScrDyn *global = scr_global_object(); scr_global_console_get = console_get; + if (known->rc == SIZE_MAX) scr_global_registered = known; 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)) diff --git a/packages/runtime/src/scr_pki.c b/packages/runtime/src/scr_pki.c index 54aadc06b..a1a5e97c2 100644 --- a/packages/runtime/src/scr_pki.c +++ b/packages/runtime/src/scr_pki.c @@ -68,14 +68,25 @@ static ScrStr *sp_received(const ScrDyn *got, bool range) { } /* ERR_INVALID_ARG_VALUE for an option ("The property 'options.type' is - * invalid. Received 'spki'"). */ -static void sp_property_invalid(const char *name, const ScrDyn *got) { + * invalid. Received 'spki'"), with another reason than "is invalid". */ +static void sp_property_invalid_because(const char *name, const ScrDyn *got, const char *reason) { ScrStr *received = sp_received(got, false); - scr_pki_throw_fmt(SCR_ERR_TYPE, "ERR_INVALID_ARG_VALUE", "The property '%s' is invalid. Received %.*s", name, + scr_pki_throw_fmt(SCR_ERR_TYPE, "ERR_INVALID_ARG_VALUE", "The property '%s' %s. Received %.*s", name, reason, (int)received->len, received->data); scr_str_release(received); } +static void sp_property_invalid(const char *name, const ScrDyn *got) { + sp_property_invalid_because(name, got, "is invalid"); +} + +/* ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS ("The selected key encoding pkcs1 + * can only be used for RSA keys."). */ +static void sp_incompatible(const char *encoding, const char *reason) { + scr_pki_throw_fmt(SCR_ERR_ERROR, "ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS", "The selected key encoding %s %s.", encoding, + reason); +} + static void sp_unsupported_input(void) { sp_throw(SCR_ERR_ERROR, "ERR_OSSL_UNSUPPORTED", "error:1E08010C:DECODER routines::unsupported"); } @@ -989,13 +1000,11 @@ static ScrDyn *sp_export_plain(SpKey *k, const ScrDyn *options) { return NULL; } if (pkcs1 && !sp_is_rsa(k)) { - sp_throw(SCR_ERR_ERROR, "ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS", - "The selected key encoding pkcs1 can only be used for RSA keys."); + sp_incompatible("pkcs1", "can only be used for RSA keys"); return NULL; } if (sec1 && !sp_is_ec(k)) { - sp_throw(SCR_ERR_ERROR, "ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS", - "The selected key encoding sec1 can only be used for EC keys."); + sp_incompatible("sec1", "can only be used for EC keys"); return NULL; } unsigned char *der = malloc(SP_DER_MAX); @@ -1175,132 +1184,292 @@ static void sp_key_install(void) { /* ── generateKeyPairSync ──────────────────────────────────────────────── */ /* A key in the encoding an options.publicKeyEncoding/privateKeyEncoding - * object asks for, or the KeyObject when it is undefined. Consumes `k`. */ + * object asks for, or the KeyObject when it is undefined or null. + * Consumes `k`. */ static ScrDyn *sp_encoded(SpKey *k, const ScrDyn *encoding) { - if (sp_absent(encoding)) return sp_key_box(k); + if (sp_absent(encoding) || encoding->kind == SCR_DYN_NULL) return sp_key_box(k); ScrDyn *result = sp_export(k, encoding); sp_key_release(k); return result; } -static bool sp_check_encoding_plain(const ScrDyn *encoding, const char *name, bool private_key); - -static bool sp_check_encoding(const ScrDyn *options, const char *name, bool private_key) { - ScrDyn *encoding = sp_plain(sp_opt(options, name)); - if (!encoding) return false; - bool ok = sp_check_encoding_plain(encoding, name, private_key); - scr_dyn_release(encoding); - return ok; +/* validateObject: an object other than null or an array (typeof + * "object"); false with ERR_INVALID_ARG_TYPE pending. */ +static bool sp_check_object(const ScrDyn *v, const char *name) { + if (v->kind != SCR_DYN_NULL && v->kind != SCR_DYN_ARR && scr_dyn_is_object(v)) return true; + scr_dyn_arg_type_fail(name, "of type object", v); + return false; } -static bool sp_check_encoding_plain(const ScrDyn *encoding, const char *name, bool private_key) { - if (sp_absent(encoding)) return true; - char full[64]; - snprintf(full, sizeof full, "options.%s", name); - if (!sp_is_object(encoding)) { - sp_property_invalid(full, encoding); +/* validateUint32 for an option: a number, then an integer, then the + * range; -0 reads as 0. False with the error pending. */ +static bool sp_check_uint32(const ScrDyn *v, const char *name, double *out) { + if (v->kind != SCR_DYN_NUM) { + scr_dyn_prop_type_fail(name, "of type number", v); return false; } - const ScrDyn *format = sp_opt(encoding, "format"); - if (!sp_is_str(format, "pem") && !sp_is_str(format, "der") && !sp_is_str(format, "jwk")) { - snprintf(full, sizeof full, "options.%s.format", name); - sp_property_invalid(full, format); - return false; - } - if (sp_is_str(format, "jwk")) return true; - const ScrDyn *type = sp_opt(encoding, "type"); - bool valid = private_key ? sp_is_str(type, "pkcs1") || sp_is_str(type, "pkcs8") || sp_is_str(type, "sec1") - : sp_is_str(type, "pkcs1") || sp_is_str(type, "spki"); - if (!valid) { - snprintf(full, sizeof full, "options.%s.type", name); - sp_property_invalid(full, type); + double x = v->v.num; + bool integer = isfinite(x) && trunc(x) == x; + if (!integer || x < 0 || x > 4294967295.0) { + ScrStr *received = sp_received(v, true); + scr_pki_throw_fmt(SCR_ERR_RANGE, "ERR_OUT_OF_RANGE", + "The value of \"%s\" is out of range. It must be %s. Received %.*s", name, + integer ? ">= 0 && <= 4294967295" : "an integer", (int)received->len, received->data); + scr_str_release(received); return false; } + *out = x + 0.0; return true; } -/* Generates the private key for (type, options), validated in Node's - * order; NULL with the error pending. */ -static SpKey *sp_generate(const ScrDyn *type, const ScrDyn *options) { +static bool sp_check_encoding_plain(const ScrDyn *enc, const char *name, const ScrDyn *type, bool private_key, + const char **refusal); + +/* Node's parseKeyEncoding for options.publicKeyEncoding or + * options.privateKeyEncoding of a `type` key: true when the encoding is + * undefined, null or valid, false with the error pending. A valid raw or + * encrypted encoding, which this runtime does not write, sets *refusal + * when it is unset. */ +static bool sp_check_encoding(const ScrDyn *options, const char *key, const ScrDyn *type, bool private_key, + const char **refusal) { + ScrDyn *enc = sp_plain(sp_opt(options, key)); + if (!enc) return false; + char name[64]; + snprintf(name, sizeof name, "options.%s", key); + bool ok = sp_check_encoding_plain(enc, name, type, private_key, refusal); + scr_dyn_release(enc); + return ok; +} + +static bool sp_check_encoding_plain(const ScrDyn *enc, const char *name, const ScrDyn *type, bool private_key, + const char **refusal) { + if (sp_absent(enc) || enc->kind == SCR_DYN_NULL) return true; + if (!scr_dyn_is_object(enc)) { + sp_property_invalid(name, enc); + return false; + } + if (!sp_check_object(enc, "options")) return false; + /* Node's isPublic: true for a public key encoding of a non-empty type, + * false for a private key encoding, and unset otherwise. */ + bool is_public = !private_key && type->v.str->len > 0; + char full[96]; + const ScrDyn *format = sp_opt(enc, "format"); + const ScrDyn *kind = sp_opt(enc, "type"); + bool raw_public = sp_is_str(format, "raw-public"); + bool raw_private = sp_is_str(format, "raw-private") || sp_is_str(format, "raw-seed"); + bool jwk = sp_is_str(format, "jwk"); + bool der = sp_is_str(format, "der"); + if ((!raw_public && !raw_private && !jwk && !der && !sp_is_str(format, "pem")) || (raw_public && private_key) || + (raw_private && is_public)) { + snprintf(full, sizeof full, "%s.format", name); + sp_property_invalid(full, format); + return false; + } + bool pkcs1 = false, sec1 = false; + if (raw_public || raw_private) { + if (!sp_absent(kind) && + !(raw_public && (sp_is_str(kind, "uncompressed") || sp_is_str(kind, "compressed")))) { + snprintf(full, sizeof full, "%s.type", name); + sp_property_invalid(full, kind); + return false; + } + } else { + pkcs1 = sp_is_str(kind, "pkcs1"); + sec1 = !is_public && sp_is_str(kind, "sec1"); + bool valid = (jwk && sp_absent(kind)) || pkcs1 || sec1 || (!private_key && sp_is_str(kind, "spki")) || + (!is_public && sp_is_str(kind, "pkcs8")); + if (!valid) { + snprintf(full, sizeof full, "%s.type", name); + sp_property_invalid(full, kind); + return false; + } + if (pkcs1 && !sp_is_str(type, "rsa")) { + sp_incompatible("pkcs1", "can only be used for RSA keys"); + return false; + } + if (sec1 && !sp_is_str(type, "ec")) { + sp_incompatible("sec1", "can only be used for EC keys"); + return false; + } + } + bool encrypted = false; + if (!is_public) { + const ScrDyn *cipher = sp_opt(enc, "cipher"); + const ScrDyn *passphrase = sp_opt(enc, "passphrase"); + encrypted = !sp_absent(cipher) && cipher->kind != SCR_DYN_NULL; + if (raw_private) { + if (encrypted || !sp_absent(passphrase)) { + sp_incompatible("raw format", "does not support encryption"); + return false; + } + } else if (encrypted) { + if (cipher->kind != SCR_DYN_STR) { + snprintf(full, sizeof full, "%s.cipher", name); + sp_property_invalid(full, cipher); + return false; + } + if (der && (pkcs1 || sec1)) { + sp_incompatible(pkcs1 ? "pkcs1" : "sec1", "does not support encryption"); + return false; + } + if (passphrase->kind != SCR_DYN_STR && passphrase->kind != SCR_DYN_BYTES && !scr_buffer_storage_is(passphrase)) { + snprintf(full, sizeof full, "%s.passphrase", name); + sp_property_invalid(full, passphrase); + return false; + } + } else if (!sp_absent(passphrase)) { + snprintf(full, sizeof full, "%s.cipher", name); + sp_property_invalid_because(full, cipher, "is required when a passphrase is specified"); + return false; + } + } + if (!*refusal && (raw_public || raw_private)) *refusal = "generateKeyPair with a raw key encoding"; + if (!*refusal && encrypted) *refusal = "encrypted private key export"; + return true; +} + +/* The key generateKeyPair makes, as sp_keygen_check reads it. */ +typedef struct { + enum { SP_GEN_RSA, SP_GEN_EC, SP_GEN_ED25519 } kind; + double bits, exponent; /* RSA */ + const SpCurve *curve; /* EC */ +} SpKeyGen; + +/* The key types Node generates that this runtime does not, apart from + * rsa-pss, dsa and dh: Node's named key pairs. */ +static const char *const sp_other_named_types[] = { + "ed448", + "x25519", + "x448", + "ml-dsa-44", + "ml-dsa-65", + "ml-dsa-87", + "ml-kem-512", + "ml-kem-768", + "ml-kem-1024", + "slh-dsa-sha2-128f", + "slh-dsa-sha2-128s", + "slh-dsa-sha2-192f", + "slh-dsa-sha2-192s", + "slh-dsa-sha2-256f", + "slh-dsa-sha2-256s", + "slh-dsa-shake-128f", + "slh-dsa-shake-128s", + "slh-dsa-shake-192f", + "slh-dsa-shake-192s", + "slh-dsa-shake-256f", + "slh-dsa-shake-256s", +}; + +/* createJob's checks for generateKeyPair(Sync)(type, options) in Node's + * order: the type, the options object, the public and private key + * encodings, then the options of the type. False with the error pending. + * The forms this runtime does not generate throw a coded SC2020 error + * after all of these checks. */ +static bool sp_keygen_check(const ScrDyn *type, const ScrDyn *options, SpKeyGen *gen) { if (type->kind != SCR_DYN_STR) { scr_dyn_arg_type_fail("type", "of type string", type); - return NULL; + return false; } - bool rsa = sp_is_str(type, "rsa"); - bool ec = sp_is_str(type, "ec"); - bool ed = sp_is_str(type, "ed25519"); - static const char *const known[] = {"rsa-pss", "dsa", "ed448", "x25519", "x448", "dh"}; - bool other = false; - for (size_t i = 0; i < sizeof known / sizeof known[0]; i++) other = other || sp_is_str(type, known[i]); - if (!rsa && !ec && !ed && !other) { - ScrStr *received = sp_received(type, false); - scr_pki_throw_fmt(SCR_ERR_TYPE, "ERR_INVALID_ARG_VALUE", - "The argument 'type' must be a supported key type. Received %.*s", (int)received->len, - received->data); - scr_str_release(received); - return NULL; + if (!sp_absent(options) && !sp_check_object(options, "options")) return false; + const char *refusal = NULL; + if (!sp_check_encoding(options, "publicKeyEncoding", type, false, &refusal) || + !sp_check_encoding(options, "privateKeyEncoding", type, true, &refusal)) + return false; + bool pss = sp_is_str(type, "rsa-pss"); + if (sp_is_str(type, "rsa") || pss) { + if (!sp_check_object(options, "options")) return false; + gen->kind = SP_GEN_RSA; + if (!sp_check_uint32(sp_opt(options, "modulusLength"), "options.modulusLength", &gen->bits)) return false; + const ScrDyn *exponent = sp_opt(options, "publicExponent"); + gen->exponent = 65537; + if (!sp_absent(exponent) && exponent->kind != SCR_DYN_NULL && + !sp_check_uint32(exponent, "options.publicExponent", &gen->exponent)) + return false; + if (pss && !refusal) refusal = "generateKeyPairSync for this key type"; + } else if (sp_is_str(type, "dsa") || sp_is_str(type, "dh")) { + if (!sp_check_object(options, "options")) return false; + if (!refusal) refusal = "generateKeyPairSync for this key type"; + } else if (sp_is_str(type, "ec")) { + if (!sp_check_object(options, "options")) return false; + gen->kind = SP_GEN_EC; + const ScrDyn *curve_name = sp_opt(options, "namedCurve"); + if (curve_name->kind != SCR_DYN_STR) { + scr_dyn_prop_type_fail("options.namedCurve", "of type string", curve_name); + return false; + } + const ScrDyn *param = sp_opt(options, "paramEncoding"); + if (sp_is_str(param, "explicit")) { + if (!refusal) refusal = "generateKeyPair with paramEncoding 'explicit'"; + } else if (!sp_absent(param) && param->kind != SCR_DYN_NULL && !sp_is_str(param, "named")) { + sp_property_invalid("options.paramEncoding", param); + return false; + } + gen->curve = NULL; + for (size_t i = 0; i < sizeof sp_curves / sizeof sp_curves[0]; i++) + if (sp_is_str(curve_name, sp_curves[i].name) || + (sp_curves[i].nist && sp_is_str(curve_name, sp_curves[i].nist))) + gen->curve = &sp_curves[i]; + if (!gen->curve) { + sp_throw(SCR_ERR_TYPE, "ERR_CRYPTO_INVALID_CURVE", "Invalid EC curve name"); + return false; + } + } else if (sp_is_str(type, "ed25519")) { + gen->kind = SP_GEN_ED25519; + } else { + bool named = false; + for (size_t i = 0; i < sizeof sp_other_named_types / sizeof sp_other_named_types[0]; i++) + named = named || sp_is_str(type, sp_other_named_types[i]); + if (!named) { + ScrStr *received = sp_received(type, false); + scr_pki_throw_fmt(SCR_ERR_TYPE, "ERR_INVALID_ARG_VALUE", + "The argument 'type' must be a supported key type. Received %.*s", (int)received->len, + received->data); + scr_str_release(received); + return false; + } + if (!refusal) refusal = "generateKeyPairSync for this key type"; } - /* The ed25519 options are optional. */ - if (!sp_is_object(options) && !(ed && sp_absent(options))) { - scr_dyn_arg_type_fail("options", "of type object", options); - return NULL; - } - if (other) { - sp_not_supported("generateKeyPairSync for this key type"); - return NULL; + if (refusal) { + sp_not_supported(refusal); + return false; } + return gen->kind != SP_GEN_ED25519 || sp_ed() != NULL; +} + +/* OpenSSL's error for an RSA modulus length or public exponent it does not + * generate, as Node reports it, or NULL. */ +static const char *sp_rsa_keygen_failure(double bits, double exponent) { + if (bits < 512) return "error:1C8000AB:Provider routines::key size too small"; + if (exponent == 0) return "Key generation job failed"; + if (bits > 2147483647.0) return "error:02000078:rsa routines::key size too small"; + if (exponent == 1 || fmod(exponent, 2) == 0) return "error:020000B2:rsa routines::pub exponent out of range"; + return NULL; +} + +/* Generates the key `gen` describes: NULL with the error pending, and + * *job_error set for the errors generateKeyPair passes to its callback. */ +static SpKey *sp_keygen_run(const SpKeyGen *gen, bool *job_error) { + *job_error = false; SpKey *k = sp_key_alloc(true); - if (ed) { - if (!sp_check_encoding(options, "publicKeyEncoding", false) || - !sp_check_encoding(options, "privateKeyEncoding", true)) { - sp_key_release(k); - return NULL; - } - const ScrPkiEd25519 *ed = sp_ed(); - if (!ed) { - sp_key_release(k); - return NULL; - } + if (gen->kind == SP_GEN_ED25519) { k->ed25519 = true; sp_random(NULL, k->ed_seed, 32); - ed->public_key(k->ed_pub, k->ed_seed); + scr_pki_ed25519->public_key(k->ed_pub, k->ed_seed); return k; } - if (rsa) { - const ScrDyn *bits = sp_opt(options, "modulusLength"); - if (bits->kind != SCR_DYN_NUM) { + if (gen->kind == SP_GEN_RSA) { + const char *failure = sp_rsa_keygen_failure(gen->bits, gen->exponent); + if (failure) { sp_key_release(k); - scr_dyn_prop_type_fail("options.modulusLength", "of type number", bits); + *job_error = true; + scr_pki_throw_fmt(SCR_ERR_ERROR, NULL, "%s", failure); return NULL; } - double b = bits->v.num; - /* validateUint32: an integer first, then the range. */ - bool integer = isfinite(b) && trunc(b) == b; - if (!integer || b < 0 || b > 4294967295.0) { - sp_key_release(k); - ScrStr *received = sp_received(bits, true); - scr_pki_throw_fmt(SCR_ERR_RANGE, "ERR_OUT_OF_RANGE", - "The value of \"options.modulusLength\" is out of range. It must be %s. Received %.*s", - integer ? ">= 0 && <= 4294967295" : "an integer", (int)received->len, received->data); - scr_str_release(received); - return NULL; - } - const ScrDyn *exp = sp_opt(options, "publicExponent"); - double e = 65537; - if (!sp_absent(exp)) { - if (exp->kind != SCR_DYN_NUM) { - sp_key_release(k); - scr_dyn_prop_type_fail("options.publicExponent", "of type number", exp); - return NULL; - } - e = exp->v.num; - } - if (!sp_check_encoding(options, "publicKeyEncoding", false) || - !sp_check_encoding(options, "privateKeyEncoding", true)) { - sp_key_release(k); - return NULL; - } - if (b < 512 || b > 16384 || e < 3 || e > 2147483647.0 || e != (double)(int64_t)e || ((int64_t)e & 1) == 0 || + /* mbedTLS generates even modulus lengths from 1024 to 8192 bits and, + * past the checks above, odd exponents up to INT_MAX. */ + double b = gen->bits, e = gen->exponent; + if (b < MBEDTLS_RSA_GEN_KEY_MIN_BITS || b > MBEDTLS_MPI_MAX_BITS || fmod(b, 2) != 0 || e > 2147483647.0 || mbedtls_pk_setup(&k->pk, mbedtls_pk_info_from_type(MBEDTLS_PK_RSA)) != 0 || mbedtls_rsa_gen_key(mbedtls_pk_rsa(k->pk), sp_random, NULL, (unsigned)b, (int)e) != 0) { sp_key_release(k); @@ -1309,29 +1478,8 @@ static SpKey *sp_generate(const ScrDyn *type, const ScrDyn *options) { } return k; } - const ScrDyn *curve_name = sp_opt(options, "namedCurve"); - if (curve_name->kind != SCR_DYN_STR) { - sp_key_release(k); - scr_dyn_prop_type_fail("options.namedCurve", "of type string", curve_name); - return NULL; - } - if (!sp_check_encoding(options, "publicKeyEncoding", false) || - !sp_check_encoding(options, "privateKeyEncoding", true)) { - sp_key_release(k); - return NULL; - } - const SpCurve *curve = NULL; - for (size_t i = 0; i < sizeof sp_curves / sizeof sp_curves[0]; i++) - if (sp_is_str(curve_name, sp_curves[i].name) || - (sp_curves[i].nist && sp_is_str(curve_name, sp_curves[i].nist))) - curve = &sp_curves[i]; - if (!curve) { - sp_key_release(k); - sp_throw(SCR_ERR_TYPE, "ERR_CRYPTO_INVALID_CURVE", "Invalid EC curve name"); - return NULL; - } if (mbedtls_pk_setup(&k->pk, mbedtls_pk_info_from_type(MBEDTLS_PK_ECKEY)) != 0 || - mbedtls_ecp_gen_key(curve->id, mbedtls_pk_ec(k->pk), sp_random, NULL) != 0) { + mbedtls_ecp_gen_key(gen->curve->id, mbedtls_pk_ec(k->pk), sp_random, NULL) != 0) { sp_key_release(k); sp_unsupported_input(); return NULL; @@ -1339,17 +1487,14 @@ static SpKey *sp_generate(const ScrDyn *type, const ScrDyn *options) { return k; } -/* generateKeyPairSync(type, options): { publicKey, privateKey }, each a - * KeyObject or the encoding the options ask for. */ -ScrDyn *scr_crypto_generate_key_pair_sync(ScrDyn *type, ScrDyn *options) { - if (options->kind == SCR_DYN_TYPED_REF) { - ScrDyn *value = scr_dyn_typed_ref_materialize(options); - if (!value) return NULL; - ScrDyn *result = scr_crypto_generate_key_pair_sync(type, value); - scr_dyn_release(value); - return result; - } - SpKey *k = sp_generate(type, options); +/* The { publicKey, privateKey } pair for (type, options), each a + * KeyObject or the encoding the options ask for; NULL with the error + * pending, and *job_error as sp_keygen_run sets it. */ +static ScrDyn *sp_key_pair(const ScrDyn *type, const ScrDyn *options, bool *job_error) { + *job_error = false; + SpKeyGen gen; + if (!sp_keygen_check(type, options, &gen)) return NULL; + SpKey *k = sp_keygen_run(&gen, job_error); if (!k) return NULL; SpKey *pub = sp_public_of(k); if (!pub) { @@ -1373,6 +1518,16 @@ ScrDyn *scr_crypto_generate_key_pair_sync(ScrDyn *type, ScrDyn *options) { return pair; } +/* generateKeyPairSync(type, options). */ +ScrDyn *scr_crypto_generate_key_pair_sync(ScrDyn *type, ScrDyn *options) { + ScrDyn *plain = sp_plain(options); + if (!plain) return NULL; + bool job_error; + ScrDyn *pair = sp_key_pair(type, plain, &job_error); + scr_dyn_release(plain); + return pair; +} + /* ── sign and verify ──────────────────────────────────────────────────── */ typedef struct { @@ -1931,12 +2086,44 @@ static void sp_key_pair_deliver(ScrClosure *self) { scr_dyn_release(callback); } -/* generateKeyPair(type, options, callback): argument errors throw, and the - * callback receives (null, publicKey, privateKey) in a later macrotask. */ +/* Calls the callback with the error alone, as Node does for a key + * generation that fails. */ +static void sp_key_pair_fail(ScrClosure *self) { + ScrDyn *callback = sp_cap(self, 0); + ScrDyn *error = sp_cap(self, 1); + ScrDyn *result = scr_dyn_call(callback, &error, 1, "callback"); + if (result) scr_dyn_release(result); + scr_dyn_release(error); + scr_dyn_release(callback); +} + +/* generateKeyPair(type, options, callback): a function in the options + * position is the callback. Argument errors throw; the callback receives + * (null, publicKey, privateKey), or the error of a key generation that + * fails, in a later macrotask. */ void scr_crypto_generate_key_pair_async(ScrDyn *type, ScrDyn *options, ScrDyn *callback) { - ScrDyn *pair = scr_crypto_generate_key_pair_sync(type, options); - if (!pair) return; - ScrClosure *c = scr_closure_new((void *)sp_key_pair_deliver, 2); + if (scr_dyn_is_callable(options)) { + callback = options; + options = scr_dyn_undefined(); + } + if (!scr_dyn_is_callable(callback)) { + scr_dyn_arg_type_fail("callback", "of type function", callback); + return; + } + ScrDyn *plain = sp_plain(options); + if (!plain) return; + bool job_error; + ScrDyn *pair = sp_key_pair(type, plain, &job_error); + scr_dyn_release(plain); + if (!pair && !job_error) return; + void *deliver = (void *)sp_key_pair_deliver; + if (!pair) { + ScrCaught *caught = scr_exc_take(); + pair = scr_caught_to_dyn(caught); + scr_caught_release(caught); + deliver = (void *)sp_key_pair_fail; + } + ScrClosure *c = scr_closure_new(deliver, 2); 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(callback)); c->caps[1] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, NULL); diff --git a/packages/runtime/src/scr_publish.c b/packages/runtime/src/scr_publish.c index 675a13ca9..4767122dd 100644 --- a/packages/runtime/src/scr_publish.c +++ b/packages/runtime/src/scr_publish.c @@ -199,6 +199,19 @@ static void scr_pub_slot(ScrPub *ctx, uint64_t slot, ScrPubFn fn) { if (fn) scr_pub_push(ctx, p, fn); } +/* The elements of a compact reference array (SCR_ELEM_CREF): 4-byte dense + * slots, 8-byte pointers in the sparse entries. */ +static __attribute__((noinline)) void scr_pub_cref_elems(ScrArr *a, ScrPub *ctx, ScrPubFn fn) { + size_t dense = a->len < a->cap ? a->len : a->cap; + const uint32_t *refs = (const uint32_t *)a->data; + const uint8_t *states = scr_arr_has_states(a) ? scr_arr_states_end_w(a, sizeof(uint32_t)) : NULL; + for (size_t i = 0; i < dense; i++) + if (!states || *scr_arr_state_in(states, i)) scr_pub_slot(ctx, scr_cref_decode(refs[i]), fn); + if (scr_arr_ext_of(a)) + for (size_t i = 0; i < scr_arr_ext_of(a)->sparse_len; i++) + if (scr_arr_ext_of(a)->sparse[i].state) scr_pub_slot(ctx, scr_arr_ext_of(a)->sparse[i].slot, fn); +} + void scr_pub_arr(void *v, ScrPub *ctx, ScrPubFn elem) { ScrArr *a = v; if (scr_arr_ext_of(a) && (scr_arr_ext_of(a)->props || scr_arr_ext_of(a)->metadata)) { @@ -206,10 +219,15 @@ void scr_pub_arr(void *v, ScrPub *ctx, ScrPubFn elem) { return; } if (!scr_pub_mark(ctx, a, a->cyc != 0, SCR_PUB_KIND_ARRAY)) return; + if (__builtin_expect(a->elem == SCR_ELEM_CREF, 0)) { + scr_pub_cref_elems(a, ctx, elem); + return; + } ScrPubFn fn = a->elem == SCR_ELEM_STR ? scr_pub_str_fn : a->elem == SCR_ELEM_REF ? elem : NULL; if (!fn) return; + /* String and reference elements have 8-byte slots. */ size_t dense = a->len < a->cap ? a->len : a->cap; - const uint8_t *states = scr_arr_states_end(a); + const uint8_t *states = scr_arr_scan_states(a); for (size_t i = 0; i < dense; i++) if (!states || *scr_arr_state_in(states, i)) scr_pub_slot(ctx, a->data[i], fn); if (scr_arr_ext_of(a)) @@ -231,12 +249,12 @@ void scr_pub_map(void *v, ScrPub *ctx, ScrPubFn key, ScrPubFn val) { m->key_kind == SCR_MAP_KEY_BIGINT || m->key_kind == SCR_MAP_KEY_UNION_VALUE ? key : NULL; - ScrPubFn vfn = m->val_kind == SCR_MAP_VAL_REF ? val : NULL; + ScrPubFn vfn = m->val_kind == SCR_MAP_VAL_REF || m->val_kind == SCR_MAP_VAL_CREF ? val : NULL; for (size_t i = 0; i < m->nentries; i++) { if (!scr_map_entry_live(m, i)) continue; /* tombstone */ const ScrMapEntry *e = scr_map_entry_at(m, i); if (kfn) scr_pub_slot(ctx, e->key, kfn); - if (vfn) scr_pub_slot(ctx, e->val, vfn); + if (vfn) scr_pub_slot(ctx, scr_map_entry_val(m, e), vfn); } } diff --git a/packages/runtime/src/scr_rc.h b/packages/runtime/src/scr_rc.h index 7a921b451..18df3a237 100644 --- a/packages/runtime/src/scr_rc.h +++ b/packages/runtime/src/scr_rc.h @@ -670,6 +670,10 @@ static inline void scr_cyc_dead(void *obj) { * live heap — do not put it on a per-turn path. */ void scr_collect_cycles(void); void scr_collect_cycles_phase(void); +/* Sets the growth cap SCR_CYCLE_GROWTH_CAP sets (scr_cycle.c), for a program + * that knows its own memory profile; the environment variable takes + * precedence. Call it before the program allocates much. */ +void scr_cyc_set_growth_cap(long cap); void scr_cyc_context_cleanup(void); /* One pass on the normal generational schedule, for callers that reach a diff --git a/packages/runtime/src/scr_regex.c b/packages/runtime/src/scr_regex.c index b62cba896..158697a14 100644 --- a/packages/runtime/src/scr_regex.c +++ b/packages/runtime/src/scr_regex.c @@ -692,7 +692,7 @@ static ScrDyn *scr_regex_groups_of(const ScrArr *a, uint8_t *bc) { 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 && - *scr_arr_state_ptr(a, (size_t)k) == SCR_ARR_VALUE; + scr_arr_dense_state(a, (size_t)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))) @@ -845,7 +845,7 @@ static ScrArr *scr_regex_exec_core(ScrStr *s, ScrRegex *re, uint64_t mask, bool if (rc == 1) { 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); + out = scr_arr_new_inline(SCR_ELEM_STR, (size_t)count + spans, true); /* 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 @@ -905,14 +905,15 @@ static bool scr_regex_group_bytes(const ScrArr *m, double k, const char **p, siz /* Array lengths stay below 2^53, so signed conversions are exact. */ if (!(k >= 0 && k < (double)(int64_t)m->len)) return false; size_t i = (size_t)(int64_t)k; - if (*scr_arr_state_ptr(m, i) == SCR_ARR_VALUE) { + uint8_t state = scr_arr_dense_state(m, i); + if (state == SCR_ARR_VALUE) { const ScrStr *str = (const ScrStr *)(uintptr_t)m->data[i]; *p = str->data; *n = str->len; return true; } uint64_t span = m->data[i]; - if (*scr_arr_state_ptr(m, i) != SCR_ARR_UNDEFINED || span == SCR_RE_NO_SPAN) return false; + if (state != SCR_ARR_UNDEFINED || span == SCR_RE_NO_SPAN) return false; const ScrStr *subject = (const ScrStr *)(uintptr_t)m->data[m->len - 1]; *p = subject->data + (span >> 32); *n = (size_t)((span & UINT32_MAX) - (span >> 32)); @@ -1043,10 +1044,13 @@ static ScrArr *scr_regex_match_all_core(ScrStr *s, ScrRegex *re, ScrArr *indices int start = scr_capture_index(&input, capture[0]); int end = scr_capture_index(&input, capture[1]); /* 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); + * directly, then the hole-free prefix is the run of captures from 0. + * A row needs states only for a capture that did not participate. */ + bool all = true; + for (int k = 0; k < capture_count; k++) all &= capture[2 * k] != NULL && capture[2 * k + 1] != NULL; + ScrArr *row = scr_arr_new_inline(SCR_ELEM_STR, (size_t)capture_count, !all); uint64_t *data = row->data; - uint8_t *states = scr_arr_states_end_w(row, 8); + uint8_t *states = all ? NULL : scr_arr_states_end_w(row, 8); 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) { @@ -1056,11 +1060,11 @@ static ScrArr *scr_regex_match_all_core(ScrStr *s, ScrRegex *re, ScrArr *indices ScrStr *text = scr_regex_span(&input, scr_capture_index(&input, cs), scr_capture_index(&input, ce)); data[k] = (uint64_t)(uintptr_t)text; - *scr_arr_state_in(states, (size_t)k) = SCR_ARR_VALUE; + if (states) *scr_arr_state_in(states, (size_t)k) = SCR_ARR_VALUE; } } size_t filled = 0; - while (filled < (size_t)capture_count && *scr_arr_state_in(states, filled) == SCR_ARR_VALUE) + while (filled < (size_t)capture_count && (!states || *scr_arr_state_in(states, filled) == SCR_ARR_VALUE)) filled++; row->filled = (uint32_t)filled; row->len = (size_t)capture_count; @@ -2795,7 +2799,7 @@ static ScrDyn *scr_native_regex_invoke(void *ptr, ScrDyn *self, const char *meth if (!row) return scr_exc_pending() ? NULL : scr_dyn_new_null(); ScrDyn *match = scr_dyn_new_arr(); for (size_t k = 0; k < row->len; k++) - scr_dyn_arr_push(match, *scr_arr_state_ptr(row, k) == SCR_ARR_VALUE + scr_dyn_arr_push(match, scr_arr_dense_state(row, k) == SCR_ARR_VALUE ? scr_dyn_new_str((ScrStr *)(uintptr_t)row->data[k]) : scr_dyn_retain(scr_dyn_undefined())); scr_arr_copy_metadata(row, match); diff --git a/packages/runtime/src/scr_region.c b/packages/runtime/src/scr_region.c index dbd74f3ac..25bbcf7ae 100644 --- a/packages/runtime/src/scr_region.c +++ b/packages/runtime/src/scr_region.c @@ -805,7 +805,7 @@ static void scr_rgn_walk_arr(ScrRgnWalk *w, ScrArr *a) { ScrRgnSlotKind k = a->elem == SCR_ELEM_ARR ? (ScrRgnSlotKind){SCR_RGN_W_ARR, NULL} : scr_rgn_slot_kind(scr_arr_ops(a) ? scr_arr_ops(a)->release : NULL); size_t dense = a->len < a->cap ? a->len : a->cap; - const uint8_t *states = scr_arr_states_end(a); + const uint8_t *states = scr_arr_scan_states(a); for (size_t i = 0; i < dense; i++) if (!states || *scr_arr_state_in(states, i) == SCR_ARR_VALUE) scr_rgn_slot_as(w, scr_rgn_slot_ptr(a->data[i]), k); if (!scr_arr_ext_of(a)) return; diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index be5ccb910..275c03039 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -145,9 +145,12 @@ struct ScrStreamState { bool object_mode_opt; /* The parked for-await next() promise (at most one — the loop awaits * each chunk before asking again). next_dyn picks the resolution - * shape: a dyn boxed by tag (the JS lane), or bytes. */ + * shape: a dyn boxed by tag (the JS lane), or bytes; next_result + * wraps the dyn in an iterator result ({ value, done }) for the + * checked-dynamic for-await protocol. */ ScrPromise *next_waiter; bool next_dyn; + bool next_result; /* The iterator's own read() is calling _read: synchronous pushes * buffer, and the waiter takes the whole buffer afterwards (Node's * read() returns everything _read pushed). */ @@ -186,6 +189,19 @@ struct ScrStreamState { * and transform steps receive, are ScrDyn* values (in the ScrBytes* * slots), each counting 1 toward length. */ bool object_mode; + /* The queue holds ScrDyn* values: on an object-mode side, and with + * decodeStrings: false, where strings stay strings (their UTF-16 + * length counts) and Buffers are buffer values. Each entry's encoding + * argument rides in ScrWEntry.enc. */ + bool values, decode_strings_off; + /* The defaultEncoding option or setDefaultEncoding(), as given; NULL + * is utf8. What write(string) encodes with, and the encoding _write + * receives for a string when decodeStrings is false. */ + ScrStr *default_enc; + /* _writev: more than one buffered write reaches it as one batch, and a + * stream without _write sends each write through it. */ + ScrClosure *writev_cb; + ScrStreamWritevInv writev_inv; bool ending, finished, prefinished, final_called, finish_scheduled; bool final_pending; /* _final called, its callback not yet run */ ScrClosure *inflight_cb; /* owned or NULL */ @@ -252,6 +268,7 @@ struct ScrStreamState { * path and _destroy wait. A destroy() in that window marks the stream * destroyed and runs its _destroy once construction completes. */ bool constructing, destroy_deferred; + bool construct_finished; /* onConstruct ran: a second callback is ERR_MULTIPLE_CALLBACK */ ScrError *destroy_err; /* owned: the deferred destroy's error */ ScrClosure *construct_cb; ScrStreamPlainInv construct_inv; @@ -340,18 +357,25 @@ static ScrDyn *scr_stream_entry_box(void *entry, ScrEntryKind kind) { return d; } -/* Writable-queue chunks: Buffers, or dyn values on an object-mode side. */ +/* Writable-queue chunks: Buffers, or dyn values (w.values). A value + * counts 1 on an object-mode side, else its length (a string's UTF-16 + * length, as Node's chunk.length). */ static size_t scr_stream_wchunk_len(const ScrStreamState *st, ScrBytes *chunk) { - return st->w.object_mode ? 1 : chunk->len; + if (st->w.object_mode) return 1; + if (!st->w.values) return chunk->len; + ScrDyn *v = (ScrDyn *)chunk; + if (v->kind == SCR_DYN_STR) return (size_t)scr_str_utf16_len(v->v.str); + if (v->kind == SCR_DYN_BYTES) return v->v.bytes->len; + return 1; } static ScrBytes *scr_stream_wchunk_retain(const ScrStreamState *st, ScrBytes *chunk) { - if (st->w.object_mode) return (ScrBytes *)scr_dyn_retain((ScrDyn *)chunk); + if (st->w.values) return (ScrBytes *)scr_dyn_retain((ScrDyn *)chunk); return scr_bytes_retain(chunk); } static void scr_stream_wchunk_release(const ScrStreamState *st, ScrBytes *chunk) { - if (st->w.object_mode) scr_dyn_release((ScrDyn *)chunk); + if (st->w.values) scr_dyn_release((ScrDyn *)chunk); else scr_bytes_release(chunk); } @@ -377,6 +401,7 @@ static void scr_stream_state_drop(ScrStreamState *st, bool gc) { if (st->destroy_err) scr_error_release(st->destroy_err); if (!gc) { if (st->construct_cb) scr_closure_release(st->construct_cb); + if (st->w.writev_cb) scr_closure_release(st->w.writev_cb); if (st->r.read_cb) scr_closure_release(st->r.read_cb); if (st->w.write_cb) scr_closure_release(st->w.write_cb); if (st->w.final_cb) scr_closure_release(st->w.final_cb); @@ -390,13 +415,14 @@ static void scr_stream_state_drop(ScrStreamState *st, bool gc) { for (size_t i = 0; i < st->drain_srcs.n; i++) scr_stream_release(st->drain_srcs.src[i]); } for (size_t i = st->w.head; i < st->w.n; i++) { - bool traced = st->w.object_mode && scr_stream_dyn_entry_traced((ScrDyn *)st->w.q[i].chunk); + bool traced = st->w.values && scr_stream_dyn_entry_traced((ScrDyn *)st->w.q[i].chunk); if (!gc || !traced) scr_stream_wchunk_release(st, st->w.q[i].chunk); if (!gc && st->w.q[i].cb) scr_closure_release(st->w.q[i].cb); ScrDyn *enc = st->w.q[i].enc; if (enc && (!gc || !scr_stream_dyn_entry_traced(enc))) scr_dyn_release(enc); } free(st->w.q); + if (st->w.default_enc) scr_str_release(st->w.default_enc); free(st->w.end_cbs); free(st->fin.cb); free(st->fin.inv); @@ -426,6 +452,7 @@ void scr_stream_st_trace(ScrStreamState *st, ScrTraceVisit visit, void *ctx) { if (st->construct_cb) visit(st->construct_cb, ctx); if (st->w.write_cb) visit(st->w.write_cb, ctx); if (st->w.final_cb) visit(st->w.final_cb, ctx); + if (st->w.writev_cb) visit(st->w.writev_cb, ctx); if (st->w.inflight_cb) visit(st->w.inflight_cb, ctx); if (st->transform_cb) visit(st->transform_cb, ctx); if (st->flush_cb) visit(st->flush_cb, ctx); @@ -434,7 +461,7 @@ void scr_stream_st_trace(ScrStreamState *st, ScrTraceVisit visit, void *ctx) { for (size_t i = 0; i < st->fin.n; i++) visit(st->fin.cb[i], ctx); for (size_t i = st->w.head; i < st->w.n; i++) { if (st->w.q[i].cb) visit(st->w.q[i].cb, ctx); - if (st->w.object_mode && scr_stream_dyn_entry_traced((ScrDyn *)st->w.q[i].chunk)) + if (st->w.values && scr_stream_dyn_entry_traced((ScrDyn *)st->w.q[i].chunk)) visit(st->w.q[i].chunk, ctx); if (st->w.q[i].enc && scr_stream_dyn_entry_traced(st->w.q[i].enc)) visit(st->w.q[i].enc, ctx); } @@ -1032,23 +1059,10 @@ static void scr_stream_resume_kick(ScrStream *s) { static ScrError *scr_stream_dyn_err(ScrDyn *const *args, size_t argc) { if (argc < 1) return NULL; const ScrDyn *a = args[0]; - if (a->kind == SCR_DYN_UNDEF || a->kind == SCR_DYN_NULL) return NULL; - if (a->kind == SCR_DYN_STR) { - ScrError *e = scr_error_new(SCR_ERR_ERROR, a->v.str); - return e; - } - if (a->kind == SCR_DYN_OBJ) { - ScrDyn *m = scr_dyn_obj_get((ScrDyn *)a, "message", 7); - ScrStr *msg = m && m->kind == SCR_DYN_STR ? scr_str_retain(m->v.str) : scr_str_new("", 0); - ScrError *e = scr_error_new(SCR_ERR_ERROR, msg); - scr_str_release(msg); - return e; - } - /* any other truthy value: Node would wrap it — approximate with "" */ - ScrStr *empty = scr_str_new("", 0); - ScrError *e = scr_error_new(SCR_ERR_ERROR, empty); - scr_str_release(empty); - return e; + if (!scr_dyn_truthy(a)) return NULL; + if (a->kind == SCR_DYN_STR) return scr_error_new(SCR_ERR_ERROR, a->v.str); + /* the error value itself (the same object reaches 'error') */ + return scr_error_from_dyn(a); } static ScrStream *scr_stream_dyn_recv(ScrClosure *clo) { @@ -1071,6 +1085,13 @@ ScrDyn *scr_stream_done_dyn_f(ScrClosure *clo, ScrDyn *const *args, size_t argc) return scr_stream_done_dyn_ret(); } +ScrDyn *scr_stream_done_dyn_c(ScrClosure *clo, ScrDyn *const *args, size_t argc) { + ScrStream *s = scr_stream_dyn_recv(clo); + scr_stream_construct_done(s, scr_stream_dyn_err(args, argc)); + scr_stream_release(s); + return scr_stream_done_dyn_ret(); +} + ScrDyn *scr_stream_done_dyn_d(ScrClosure *clo, ScrDyn *const *args, size_t argc) { ScrStream *s = scr_stream_dyn_recv(clo); scr_stream_destroy_done(s, scr_stream_dyn_err(args, argc)); @@ -1108,7 +1129,7 @@ bool scr_stream_push_dyn(ScrStream *s, const ScrDyn *d) { switch (d->kind) { case SCR_DYN_BYTES: { ScrBytes *b = scr_bytes_raw_view(d->v.bytes); - bool r = scr_stream_push(s, b); + bool r = scr_stream_push_view(s, b); scr_bytes_release(b); return r; } @@ -1208,7 +1229,15 @@ static void scr_stream_next_fulfill(ScrStreamState *st, ScrPromise *w /*moves*/, scr_promise_release(w); return; } - if (dyn) { + if (dyn && st->r.next_result) { + /* { value, done }: the value, or undefined at the end */ + ScrDyn *result = scr_dyn_new_obj(); + scr_dyn_obj_set(result, "value", 5, + chunk == NULL ? scr_dyn_retain(scr_dyn_undefined()) + : scr_stream_entry_box(chunk, kind)); + scr_dyn_obj_set(result, "done", 4, scr_dyn_new_bool(chunk == NULL)); + payload = result; + } else if (dyn) { payload = chunk == NULL ? scr_stream_from_done() : scr_stream_entry_box(chunk, kind); } else { payload = chunk ? (ScrBytes *)chunk : scr_bytes_new(SCR_BYTES_U8, 0); @@ -1334,11 +1363,12 @@ static void scr_stream_settle_next(ScrStream *s) { } } -static ScrPromise *scr_stream_next_chunk_impl(ScrStream *s, bool dyn) { +static ScrPromise *scr_stream_next_chunk_impl(ScrStream *s, bool dyn, bool result) { ScrStreamState *st = s->st; ScrPromise *p = scr_promise_new(); if (!st->has_r) { st->r.next_dyn = dyn; + st->r.next_result = result; scr_stream_next_fulfill(st, scr_promise_retain(p), NULL, SCR_ENTRY_BYTES); return p; } @@ -1350,6 +1380,7 @@ static ScrPromise *scr_stream_next_chunk_impl(ScrStream *s, bool dyn) { return p; } st->r.next_dyn = dyn; + st->r.next_result = result; st->r.next_waiter = scr_promise_retain(p); st->r.next_parked = false; if (!st->r.next_started) { @@ -1373,11 +1404,15 @@ ScrPromise *scr_stream_next_chunk(ScrStream *s) { scr_promise_reject_pending(p); return p; } - return scr_stream_next_chunk_impl(s, false); + return scr_stream_next_chunk_impl(s, false, false); } ScrPromise *scr_stream_next_chunk_dyn(ScrStream *s) { - return scr_stream_next_chunk_impl(s, true); + return scr_stream_next_chunk_impl(s, true, false); +} + +ScrPromise *scr_stream_next_result_dyn(ScrStream *s) { + return scr_stream_next_chunk_impl(s, true, true); } /* ── Readable.from (array, string and Buffer sources) ─────────────────── */ @@ -1516,8 +1551,21 @@ static bool scr_from_check_hwm(double hwm) { * highWaterMark 1 for iterables, the mode's default for the string / * Buffer case or a nullish option, or the option's value. +1, or NULL with the error * pending. Moves f. */ +/* from.js's `new Readable(...)`: a Readable, or for Duplex.from (duplexify.js's + * from(Duplexify, body, { objectMode: true, writable: false })) a Duplexify + * whose writable side is off. */ +static ScrStream *scr_from_new(bool duplex, double hwm, ScrClosure *read, ScrStreamReadInv read_inv, + ScrClosure *destroy, ScrStreamErrInv destroy_inv) { + if (!duplex) return scr_stream_new_readable(hwm, true, true, read, read_inv, destroy, destroy_inv); + ScrStream *d = scr_stream_new_duplex(hwm, hwm, true, true, true, true, false, read, read_inv, NULL, + NULL, NULL, NULL, destroy, destroy_inv); + scr_stream_set_object_mode(d, false, true); + d->cls = "Duplexify"; + return d; +} + static ScrStream *scr_from_stream(ScrFromSrc *f, ScrStreamReadInv read_inv, bool object_mode, - int hwm_kind, double hwm, bool strings) { + int hwm_kind, double hwm, bool strings, bool duplex) { if (hwm_kind == SCR_FROM_HWM_SET && !scr_from_check_hwm(hwm)) { scr_from_src_release(f); return NULL; @@ -1529,8 +1577,8 @@ static ScrStream *scr_from_stream(ScrFromSrc *f, ScrStreamReadInv read_inv, bool scr_box_set_ref(read->caps[0], f); /* moves */ ScrClosure *destroy = NULL; if (!f->single) destroy = scr_closure_new((void *)&scr_from_destroy_inv, 0); - ScrStream *s = scr_stream_new_readable(hwm, true, true, read, read_inv, destroy, - destroy ? &scr_from_destroy_inv : NULL); + ScrStream *s = scr_from_new(duplex, hwm, read, read_inv, destroy, + destroy ? &scr_from_destroy_inv : NULL); ScrStreamState *st = s->st; if (object_mode) { st->r.object_entries = true; @@ -1557,7 +1605,7 @@ ScrStream *scr_stream_new_from_readable(ScrClosure *read, ScrStreamReadInv read_ bool object_mode = (bits & SCR_FROM_BYTE_MODE) == 0; if (bits & SCR_FROM_DEFAULT_HWM) hwm = object_mode ? 16 : -1; else if (!(bits & SCR_FROM_HAS_HWM)) hwm = 1; - ScrStream *s = scr_stream_new_readable(hwm, true, true, read, read_inv, destroy, destroy_inv); + ScrStream *s = scr_from_new((bits & SCR_FROM_DUPLEX) != 0, hwm, read, read_inv, destroy, destroy_inv); if (object_mode) s->st->r.object_entries = true; return s; } @@ -1572,7 +1620,7 @@ ScrStream *scr_stream_from_arr(ScrArr *arr, double flags, double hwm) { f->single = (bits & SCR_FROM_SINGLE) != 0; return scr_from_stream(f, &scr_from_read_inv, (bits & SCR_FROM_BYTE_MODE) == 0, (bits & SCR_FROM_HAS_HWM) ? SCR_FROM_HWM_SET : SCR_FROM_HWM_FROM, hwm, - f->strings); + f->strings, (bits & SCR_FROM_DUPLEX) != 0); } /* The JS lane: one checked-dynamic value into the stream. null is @@ -1636,7 +1684,7 @@ static void scr_from_read_dyn_inv(ScrClosure *cb, ScrStream *s, double size) { scr_from_src_release(f); } -ScrStream *scr_stream_from_dyn(ScrDyn *src, ScrDyn *opts) { +static ScrStream *scr_from_dyn_ex(ScrDyn *src, ScrDyn *opts, bool duplex) { bool single = src->kind == SCR_DYN_STR || src->kind == SCR_DYN_BYTES; ScrDyn *iter = NULL; /* from.js's iterator, for sources other than arrays */ int kind = 0; @@ -1648,7 +1696,14 @@ ScrStream *scr_stream_from_dyn(ScrDyn *src, ScrDyn *opts) { if (kind < 0) return NULL; } if (!single && kind == 0) { - scr_dyn_arg_type_fail("iterable", "an instance of Iterable", src); + if (duplex) { + scr_dyn_arg_type_fail("body", + "of type function or an instance of Blob, ReadableStream, WritableStream, " + "Stream, Iterable, AsyncIterable, or Promise or { readable, writable } pair", + src); + } else { + scr_dyn_arg_type_fail("iterable", "an instance of Iterable", src); + } return NULL; } bool object_mode = true; @@ -1695,7 +1750,7 @@ ScrStream *scr_stream_from_dyn(ScrDyn *src, ScrDyn *opts) { } } if (iter) { - double flags = (object_mode ? 0 : SCR_FROM_BYTE_MODE) | + double flags = (object_mode ? 0 : SCR_FROM_BYTE_MODE) | (duplex ? SCR_FROM_DUPLEX : 0) | (hwm_kind == SCR_FROM_HWM_SET ? SCR_FROM_HAS_HWM : hwm_kind == SCR_FROM_HWM_DEFAULT ? SCR_FROM_DEFAULT_HWM : 0); @@ -1706,7 +1761,47 @@ ScrStream *scr_stream_from_dyn(ScrDyn *src, ScrDyn *opts) { f->rc = 1; f->dyn = scr_dyn_retain(src); f->single = single; - return scr_from_stream(f, &scr_from_read_dyn_inv, object_mode, hwm_kind, hwm, false); + return scr_from_stream(f, &scr_from_read_dyn_inv, object_mode, hwm_kind, hwm, false, duplex); +} + +ScrStream *scr_stream_from_dyn(ScrDyn *src, ScrDyn *opts) { + return scr_from_dyn_ex(src, opts, false); +} + +/* Duplex.from(body) over a checked-dynamic body (duplexify.js): a native + * Duplex answers itself, a lone native Readable or Writable is wrapped, a + * promise reads as its value, an iterable reads as Duplex.from does, and + * the other bodies Node accepts (a function, a thenable, a { readable, + * writable } pair) are not supported yet. */ +static ScrStream *scr_stream_boxed(const ScrDyn *value); +static ScrStream *scr_dupx_from_boxed(ScrDyn *boxed); + +ScrStream *scr_stream_duplex_from_dyn(ScrDyn *body) { + ScrStream *native = scr_stream_of_handle(body); /* +1 */ + if (!native) native = scr_stream_boxed(body); /* +1 */ + if (native) { + if (native->st->has_r && native->st->has_w) return native; + /* _duplexify({ readable }) / _duplexify({ writable }) */ + ScrStream *d = native->st->has_r ? scr_stream_duplexify(native, NULL) + : scr_stream_duplexify(NULL, native); + scr_stream_release(native); + return d; + } + if (body->kind == SCR_DYN_PROMISE) return scr_dupx_from_boxed(scr_dyn_retain(body)); + bool unsupported = body->kind == SCR_DYN_FUNC; + if (!unsupported && body->kind == SCR_DYN_OBJ) { + static const char *const keys[] = {"then", "readable", "writable"}; + for (size_t i = 0; i < 3 && !unsupported; i++) { + ScrDyn *v = scr_dyn_obj_get(body, keys[i], strlen(keys[i])); + unsupported = v != NULL && v->kind != SCR_DYN_UNDEF; + } + } + if (unsupported) { + static const char msg[] = "Duplex.from over this body is not supported yet"; + scr_throw_error_msg_code(SCR_ERR_ERROR, msg, sizeof msg - 1, "SC2020"); + return NULL; + } + return scr_from_dyn_ex(body, NULL, true); } /* ── object-mode streams for the stream operators (scr_stream_ops.c) ──── */ @@ -1865,6 +1960,8 @@ static void scr_stream_wq_push(ScrStreamState *st, ScrBytes *chunk /*moves*/, Sc static void scr_stream_transform_callback(ScrStream *s); static void scr_stream_do_write_inner(ScrStream *s, ScrBytes *chunk /*moves*/, ScrClosure *cb /*moves*/); +static ScrDyn *scr_stream_wentry_value(const ScrStreamState *st, ScrBytes *chunk); +static ScrDyn *scr_stream_wentry_encoding(const ScrStreamState *st, ScrDyn *enc); /* One write reaching _write/_transform; the callback reads the write's * encoding argument through w.cur_enc while it runs. */ @@ -1891,7 +1988,7 @@ static void scr_stream_do_write_inner(ScrStream *s, ScrBytes *chunk /*moves*/, S if (st->is_transform || st->passthrough) st->xf_length = st->r.length; if (st->passthrough && st->transform_cb == NULL) { /* identity: push through, complete synchronously */ - if (st->w.object_mode) scr_stream_push_dyn(s, (ScrDyn *)chunk); + if (st->w.values) scr_stream_push_dyn(s, (ScrDyn *)chunk); else scr_stream_add_chunk(s, chunk, false); scr_stream_wchunk_release(st, chunk); if (!scr_exc_pending()) scr_stream_transform_callback(s); @@ -1912,6 +2009,19 @@ static void scr_stream_do_write_inner(ScrStream *s, ScrBytes *chunk /*moves*/, S st->w.wsync = false; return; } + if (st->w.write_cb == NULL && st->w.writev_cb) { + /* Writable.prototype._write: this._writev([{ chunk, encoding }], cb) */ + ScrDyn *chunks = scr_dyn_new_arr(); + ScrDyn *entry = scr_dyn_new_obj(); + scr_dyn_obj_set(entry, "chunk", 5, scr_stream_wentry_value(st, chunk)); + scr_dyn_obj_set(entry, "encoding", 8, scr_stream_wentry_encoding(st, st->w.cur_enc)); + scr_dyn_arr_push(chunks, entry); + scr_stream_wchunk_release(st, chunk); + st->w.writev_inv(st->w.writev_cb, s, chunks); + scr_dyn_release(chunks); + st->w.wsync = false; + return; + } if (st->w.write_cb == NULL) { scr_stream_wchunk_release(st, chunk); ScrError *e = scr_stream_mkerr("ERR_METHOD_NOT_IMPLEMENTED", "The _write() method is not implemented"); @@ -2031,11 +2141,107 @@ static SCR_COLD void scr_stream_writev_buffer(ScrStream *s) { scr_stream_do_write(s, all, cbs, NULL); } +/* A write's chunk and encoding as _writev receives them (+1 each). */ +static ScrDyn *scr_stream_wentry_value(const ScrStreamState *st, ScrBytes *chunk) { + if (st->w.values) return scr_dyn_retain((ScrDyn *)chunk); + return chunk->is_buffer ? scr_dyn_new_buffer(chunk) : scr_dyn_new_bytes(chunk); +} + +static ScrDyn *scr_stream_wentry_encoding(const ScrStreamState *st, ScrDyn *enc) { + if (!st->w.values) { + ScrStr *buffer = scr_str_new("buffer", 6); + ScrDyn *d = scr_dyn_new_str(buffer); + scr_str_release(buffer); + return d; + } + return scr_dyn_retain(enc ? enc : scr_dyn_undefined()); +} + +/* A batch entry's callback: the write's callback, or Node's nop. */ +static ScrDyn *scr_stream_writev_entry_cb_call(ScrClosure *clo, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + if (clo->ncaps > 0) { + ScrClosure *cb = scr_box_get_ref(clo->caps[0]); + ((void (*)(ScrClosure *))cb->fn)(cb); + scr_closure_release(cb); + } + return scr_dyn_retain(scr_dyn_undefined()); +} + +static ScrDyn *scr_stream_writev_entry_cb(ScrClosure *cb /*borrowed or NULL*/) { + ScrClosure *c = scr_closure_new((void *)&scr_stream_writev_entry_cb_call, cb ? 1 : 0); + if (cb) { + c->caps[0] = scr_box_new_obj(&scr_closure_retain_v, &scr_closure_release_v, &scr_closure_trace_v); + scr_box_set_ref(c->caps[0], scr_closure_retain(cb)); + } + return scr_dyn_new_func(c, &scr_stream_writev_entry_cb_call, 1, "(error)", cb ? "" : "nop"); +} + +/* Calls _writev with chunks as the in-flight write of len, whose + * completion runs cb (moves; NULL when every write had none). */ +static void scr_stream_writev_call(ScrStream *s, ScrDyn *chunks, size_t len, ScrClosure *cb) { + ScrStreamState *st = s->st; + st->w.writing = true; + st->w.wsync = true; + st->w.inflight_len = len; + st->w.inflight_cb = cb; + st->w.writev_inv(st->w.writev_cb, s, chunks); + st->w.wsync = false; +} + +/* Node's clearBuffer with _writev and more than one buffered write: the + * writes go out as one [{ chunk, encoding, callback }] batch (its + * allBuffers says whether every encoding is 'buffer'), and the batch's + * completion runs the writes' callbacks in order. */ +static void scr_stream_writev_batch(ScrStream *s) { + ScrStreamState *st = s->st; + size_t total = 0, ncbs = 0; + for (size_t i = st->w.head; i < st->w.n; i++) + if (st->w.q[i].cb) ncbs++; + ScrClosure *cbs = ncbs > 0 ? scr_closure_new((void *)&scr_stream_writev_cbs, ncbs) : NULL; + ScrDyn *chunks = scr_dyn_new_arr(); + bool all_buffers = true; + size_t k = 0; + for (size_t i = st->w.head; i < st->w.n; i++) { + ScrWEntry e = st->w.q[i]; + total += scr_stream_wchunk_len(st, e.chunk); + ScrDyn *entry = scr_dyn_new_obj(); + ScrDyn *encoding = scr_stream_wentry_encoding(st, e.enc); + if (!(encoding->kind == SCR_DYN_STR && encoding->v.str->len == 6 && + memcmp(encoding->v.str->data, "buffer", 6) == 0)) + all_buffers = false; + scr_dyn_obj_set(entry, "chunk", 5, scr_stream_wentry_value(st, e.chunk)); + scr_dyn_obj_set(entry, "encoding", 8, encoding); + scr_dyn_obj_set(entry, "callback", 8, scr_stream_writev_entry_cb(e.cb)); + scr_dyn_arr_push(chunks, entry); + scr_stream_wchunk_release(st, e.chunk); + if (e.enc) scr_dyn_release(e.enc); + if (e.cb) { + cbs->caps[k] = scr_box_new_obj(&scr_closure_retain_v, &scr_closure_release_v, + &scr_closure_trace_v); + scr_box_set_ref(cbs->caps[k], e.cb); /* moves */ + k++; + } + } + scr_dyn_arr_named_put(chunks, "allBuffers", 10, scr_dyn_new_bool(all_buffers)); + st->w.head = 0; + st->w.n = 0; + scr_stream_writev_call(s, chunks, total, cbs); + scr_dyn_release(chunks); +} + /* The buffered writes, one at a time (Node's clearBuffer without _writev). */ static void scr_stream_clear_buffer_each(ScrStream *s) { ScrStreamState *st = s->st; if (st->w.buffer_processing) return; st->w.buffer_processing = true; + if (st->w.writev_cb && !st->w.writing && st->w.n - st->w.head > 1 && st->w.corked == 0 && + !st->destroyed && !st->constructing && !scr_exc_pending()) { + scr_stream_writev_batch(s); + st->w.buffer_processing = false; + return; + } while (!st->w.writing && st->w.head < st->w.n && st->w.corked == 0 && !st->destroyed && !scr_exc_pending()) { ScrWEntry e = st->w.q[st->w.head++]; @@ -2528,6 +2734,27 @@ static void scr_stream_dynopt_transform_inv(ScrClosure *cb, ScrStream *s, ScrByt if (r != NULL) scr_dyn_release(r); } +static void scr_stream_dynopt_construct_inv(ScrClosure *cb, ScrStream *s) { + ScrDyn *fn = scr_stream_dynopt_target(cb); + ScrDyn *a = scr_stream_dynopt_done(s, &scr_stream_done_dyn_c, 1, "(error)"); + ScrDyn *r = scr_dyn_call(fn, &a, 1, "construct"); + scr_dyn_release(a); + scr_dyn_release(fn); + if (r != NULL) scr_dyn_release(r); +} + +static void scr_stream_dynopt_writev_inv(ScrClosure *cb, ScrStream *s, ScrDyn *chunks) { + ScrDyn *fn = scr_stream_dynopt_target(cb); + ScrDyn *args[2]; + args[0] = scr_dyn_retain(chunks); + args[1] = scr_stream_dynopt_done(s, &scr_stream_done_dyn_w, 1, "(error)"); + ScrDyn *r = scr_dyn_call(fn, args, 2, "writev"); + scr_dyn_release(args[0]); + scr_dyn_release(args[1]); + scr_dyn_release(fn); + if (r != NULL) scr_dyn_release(r); +} + static void scr_stream_dynopt_final_inv(ScrClosure *cb, ScrStream *s) { ScrDyn *fn = scr_stream_dynopt_target(cb); ScrDyn *a = scr_stream_dynopt_done(s, &scr_stream_done_dyn_f, 1, "(error)"); @@ -2570,9 +2797,13 @@ typedef struct { bool auto_destroy, emit_close, allow_half_open; bool readable_side, writable_side; bool readable_object, writable_object; /* objectMode and the sided keys */ + bool decode_strings_off; /* decodeStrings: false */ ScrStr *enc; /* owned canonical name, or NULL */ + /* defaultEncoding other than utf8: as given (borrowed) and canonical + * (owned), or NULL */ + ScrStr *default_enc, *default_canonical; /* BORROWED callable fields (owned by the options record). */ - ScrDyn *read, *write, *final_fn, *destroy, *transform, *flush; + ScrDyn *read, *write, *final_fn, *destroy, *transform, *flush, *construct, *writev; } ScrStreamDynOpts; /* Node's `=== false` comparisons (autoDestroy/emitClose/allowHalfOpen and @@ -2647,7 +2878,10 @@ static bool scr_stream_parse_dyn_opts(const ScrDyn *opts, char base, ScrStreamDy out->writable_side = true; out->enc = NULL; out->readable_object = out->writable_object = false; + out->decode_strings_off = false; + out->default_enc = out->default_canonical = NULL; out->read = out->write = out->final_fn = out->destroy = out->transform = out->flush = NULL; + out->construct = out->writev = NULL; if (opts == NULL || opts->kind != SCR_DYN_OBJ) return true; /* Node's `if (options)` */ const bool duplexish = base == 'd' || base == 't' || base == 'p'; const bool transformish = base == 't' || base == 'p'; @@ -2658,10 +2892,9 @@ static bool scr_stream_parse_dyn_opts(const ScrDyn *opts, char base, ScrStreamDy out->readable_object = object || (duplexish && scr_stream_dynopt_truthy(opts, "readableObjectMode", 18)); out->writable_object = object || (duplexish && scr_stream_dynopt_truthy(opts, "writableObjectMode", 18)); } - if (scr_stream_dynopt_false(scr_dyn_obj_get((ScrDyn *)opts, "decodeStrings", 13))) { - static const char m[] = "decodeStrings: false is not supported yet (chunks always decode to Buffers)"; - return scr_stream_dynopt_unsupported(m, sizeof m - 1); - } + /* WritableState: decodeStrings === false keeps string writes */ + out->decode_strings_off = + base != 'r' && scr_stream_dynopt_false(scr_dyn_obj_get((ScrDyn *)opts, "decodeStrings", 13)); { ScrDyn *v = scr_dyn_obj_get((ScrDyn *)opts, "signal", 6); if (v != NULL && v->kind != SCR_DYN_NULL && v->kind != SCR_DYN_UNDEF) { @@ -2669,21 +2902,22 @@ static bool scr_stream_parse_dyn_opts(const ScrDyn *opts, char base, ScrStreamDy return scr_stream_dynopt_unsupported(m, sizeof m - 1); } } - if (scr_stream_dynopt_fn(opts, "construct", 9) != NULL) { - static const char m[] = "the stream option 'construct' is not supported yet (deferred construction has no lowering)"; - return scr_stream_dynopt_unsupported(m, sizeof m - 1); - } - if (scr_stream_dynopt_fn(opts, "writev", 6) != NULL) { - static const char m[] = "the stream option 'writev' is not supported yet (writes deliver one chunk at a time)"; + out->construct = scr_stream_dynopt_fn(opts, "construct", 9); + if (base != 'r') out->writev = scr_stream_dynopt_fn(opts, "writev", 6); + if (transformish && out->writev != NULL) { + static const char m[] = "the stream option 'writev' on a Transform is not supported yet (batches would bypass the transform)"; return scr_stream_dynopt_unsupported(m, sizeof m - 1); } { + /* a known encoding or ERR_UNKNOWN_ENCODING (the states' constructors) */ ScrDyn *v = scr_dyn_obj_get((ScrDyn *)opts, "defaultEncoding", 15); if (v != NULL && v->kind == SCR_DYN_STR && !(v->v.str->len == 4 && memcmp(v->v.str->data, "utf8", 4) == 0) && !(v->v.str->len == 5 && memcmp(v->v.str->data, "utf-8", 5) == 0)) { - static const char m[] = "the stream option 'defaultEncoding' is not supported yet (utf8, the default, is the write-side string encoding)"; - return scr_stream_dynopt_unsupported(m, sizeof m - 1); + ScrStr *canonical = scr_bytes_normalize_encoding(v->v.str); + if (canonical == NULL) return false; + out->default_enc = v->v.str; + out->default_canonical = canonical; } } if (transformish) { @@ -2752,8 +2986,33 @@ static ScrClosure *scr_stream_dynopt_pick(ScrDyn *opt, void *dyn_inv, ScrClosure return fb; } -static void scr_stream_dynopt_apply_object(ScrStream *s, const ScrStreamDynOpts *o) { - if (s != NULL && s->st != NULL) scr_stream_set_object_mode(s, o->readable_object, o->writable_object); +static void scr_stream_dynopt_construct_inv(ScrClosure *cb, ScrStream *s); +static void scr_stream_dynopt_writev_inv(ScrClosure *cb, ScrStream *s, ScrDyn *chunks); + +/* The options a dyn constructor applies over the fresh stream: the object + * modes, decodeStrings, defaultEncoding (the push decoding of a readable + * side and the writes of a writable side), writev, and construct, which + * Node's constructor schedules last. Releases the owned canonical name. */ +static void scr_stream_dynopt_apply_object(ScrStream *s, ScrStreamDynOpts *o) { + if (s != NULL && s->st != NULL) { + scr_stream_set_object_mode(s, o->readable_object, o->writable_object); + if (o->decode_strings_off) scr_stream_set_decode_strings(s, false); + if (o->default_enc) { + scr_stream_set_write_encoding(s, o->default_enc); + if (s->st->has_r && !s->st->r.object_entries) + scr_stream_release(scr_stream_set_push_encoding(s, o->default_canonical)); + } + if (o->writev && s->st->has_w) + scr_stream_set_writev(s, scr_stream_dynopt_clo(o->writev, (void *)&scr_stream_dynopt_writev_inv), + &scr_stream_dynopt_writev_inv); + if (o->construct) + scr_stream_set_construct(s, scr_stream_dynopt_clo(o->construct, (void *)&scr_stream_dynopt_construct_inv), + &scr_stream_dynopt_construct_inv); + } + if (o->default_canonical) { + scr_str_release(o->default_canonical); + o->default_canonical = NULL; + } } static void scr_stream_dynopt_apply_enc(ScrStream *s, ScrStr *enc) { @@ -3513,7 +3772,9 @@ typedef ScrPromise *(*ScrPipeNextFn)(ScrClosure *, ScrGen *); typedef ScrDyn *(*ScrPipeReadFn)(ScrClosure *, void *); typedef ScrDyn *(*ScrPipeBoxFn)(ScrClosure *, ScrPromise *); -enum { SCR_PIPE_DEST_STREAM, SCR_PIPE_DEST_PROMISE = 2 }; +enum { SCR_PIPE_DEST_STREAM, SCR_PIPE_DEST_GEN, SCR_PIPE_DEST_PROMISE }; +/* flags bit 7: the callback is typed code's (stream.pipelineStagesDyn) */ +enum { SCR_PIPE_TYPED_CB = 128 }; typedef struct { size_t count; /* the function stages */ @@ -3525,8 +3786,9 @@ typedef struct { int dest; /* the last stage: SCR_PIPE_DEST_* */ unsigned opts_mask; /* bit i: stages[i] takes the { signal } argument */ ScrDyn *signal; /* the pipeline's AbortSignal when opts_mask != 0 */ - /* (next, read, return) for each generator pumped into a stream, in - * pipeline order, or NULL */ + /* (next, read, return) for each generator pumped into a stream (an + * async generator destination's PassThrough included), in pipeline + * order, or NULL */ ScrClosure **helpers; ScrClosure *box; /* SCR_PIPE_DEST_PROMISE: boxes its promise as a dyn promise */ } ScrPipeStages; @@ -3861,7 +4123,7 @@ static void scr_pipe_dest_entry(ScrFiber *fiber, void *arg) { ctx->value = v; if (ctx->value_p) scr_promise_release(ctx->value_p); ctx->value_p = scr_promise_retain(d.typed); - if (pt && v && (v->kind == SCR_DYN_STR || v->kind == SCR_DYN_BYTES)) { + if (pt && v && v->kind != SCR_DYN_NULL && v->kind != SCR_DYN_UNDEF) { scr_stream_write_dyn(pt, v, NULL); if (scr_exc_pending()) scr_exc_clear(); } @@ -3965,17 +4227,22 @@ static ScrStream *scr_stream_pipeline_ex(size_t n, ScrStream **streams, ScrClosu ret_gen = out; continue; } - /* The destination function: pipeline() answers a PassThrough proxy - * that the promise's value feeds. Node's is in object mode; the - * frontend refuses programs that read it, so a byte-mode - * PassThrough plays its part in the pipeline's bookkeeping. */ + /* The destination function: pipeline() answers an object-mode + * PassThrough proxy that the promise's value or the generator's + * values feed. */ ScrDyn *pt_opts = scr_dyn_new_obj(); + scr_dyn_obj_set(pt_opts, "objectMode", 10, scr_dyn_new_bool(true)); ScrStream *pt = scr_stream_new_passthrough_dyn(pt_opts); scr_dyn_release(pt_opts); size_t pi = ctx->n++; ctx->streams[pi] = pt; ctx->finish_count++; - scr_pipe_dest_promise(ctx, out, fs->box, pi, end); + if (fs->dest == SCR_PIPE_DEST_GEN) { + /* pumpToNode(ret, pt, finish, { end }) */ + scr_pump_start(ctx, out, fs->helpers + 3 * h++, pt, end); + } else { + scr_pipe_dest_promise(ctx, out, fs->box, pi, end); + } /* destroyer(pt, false, true), with its cleanup as the last stream's */ ctx->destroyer[pi] = true; scr_pipe_on(pt, "close", scr_pipe_closure((void *)&scr_pipe_destroyer_close_l, ctx, pi), @@ -4063,12 +4330,14 @@ static ScrPipeStages scr_pipe_stages_of(double count, ScrClosure **stages, doubl } /* callback(error, value) through a checked-dynamic callback: Node passes - * both arguments. */ -static void scr_pipe_dyn_value_inv(ScrClosure *cb, ScrError *err, ScrDyn *value, ScrPromise *value_p) { - (void)value_p; + * both arguments. A typed callback (SCR_PIPE_TYPED_CB) takes null for no + * error, as the typed callback forms do. */ +static void scr_pipe_dyn_value_call(ScrClosure *cb, ScrError *err, ScrDyn *value, bool typed) { ScrDyn *fn = scr_stream_dynopt_target(cb); - ScrDyn *args[2] = {err ? scr_dyn_from_error(err) : scr_dyn_retain(scr_dyn_undefined()), + ScrDyn *none = typed ? scr_dyn_new_null() : scr_dyn_retain(scr_dyn_undefined()); + ScrDyn *args[2] = {err ? scr_dyn_from_error(err) : none, value && !err ? scr_dyn_retain(value) : scr_dyn_retain(scr_dyn_undefined())}; + if (err) scr_dyn_release(none); ScrDyn *r = scr_dyn_call(fn, args, 2, "callback"); scr_dyn_release(args[0]); scr_dyn_release(args[1]); @@ -4076,6 +4345,17 @@ static void scr_pipe_dyn_value_inv(ScrClosure *cb, ScrError *err, ScrDyn *value, if (r != NULL) scr_dyn_release(r); } +static void scr_pipe_dyn_value_inv(ScrClosure *cb, ScrError *err, ScrDyn *value, ScrPromise *value_p) { + (void)value_p; + scr_pipe_dyn_value_call(cb, err, value, false); +} + +static void scr_pipe_dyn_value_typed_inv(ScrClosure *cb, ScrError *err, ScrDyn *value, + ScrPromise *value_p) { + (void)value_p; + scr_pipe_dyn_value_call(cb, err, value, true); +} + /* pipeline(...) with function stages (see ScrPipeStages): positions * marks the function stages among the n + count stages, flags bits 1-2 * give the destination kind, and mask names the stages taking @@ -4097,7 +4377,9 @@ ScrStream *scr_stream_pipeline_stages_dyn(double n, ScrStream **streams, double ScrPipeStages fs = scr_pipe_stages_of(count, stages, positions, flags, mask, helpers, box, signal); ScrClosure *c = scr_stream_dynopt_clo(cb, (void *)&scr_stream_finished_dyn_inv); /* a function destination's value reaches the callback */ - ScrPipeValInv vi = fs.dest != SCR_PIPE_DEST_STREAM ? &scr_pipe_dyn_value_inv : NULL; + ScrPipeValInv vi = fs.dest == SCR_PIPE_DEST_STREAM ? NULL + : ((int)flags & SCR_PIPE_TYPED_CB) ? &scr_pipe_dyn_value_typed_inv + : &scr_pipe_dyn_value_inv; return scr_stream_pipeline_ex((size_t)n, streams, c, &scr_stream_finished_dyn_inv, true, NULL, NULL, &fs, abort, vi); } @@ -4151,7 +4433,7 @@ static void *scr_compose_retain_v(void *p) { static void scr_compose_release_v(void *p) { ScrCompose *c = p; if (--c->rc > 0) return; - scr_stream_release(c->head); + if (c->head) scr_stream_release(c->head); scr_stream_release(c->tail); free(c); } @@ -4211,7 +4493,9 @@ static void scr_compose_destroyed(ScrCompose *c, ScrError *err) { /* d._write: head.write(chunk) ? callback() : (ondrain = callback) */ static void scr_compose_write_inv(ScrClosure *cb, ScrStream *s, ScrBytes *chunk) { ScrCompose *c = scr_box_get_ref(cb->caps[0]); - if (scr_stream_write(c->head, chunk, NULL)) { + bool ok = scr_stream_writes_values(s) ? scr_stream_write_dyn(c->head, (ScrDyn *)chunk, NULL) + : scr_stream_write(c->head, chunk, NULL); + if (ok) { if (!scr_exc_pending()) scr_stream_write_done(s, NULL); } else { c->ondrain = true; @@ -4342,43 +4626,54 @@ static void scr_compose_arg_value(size_t i, const char *what, ScrStream *s) { scr_throw_error_msg_code(SCR_ERR_TYPE, m, (size_t)n, "ERR_INVALID_ARG_VALUE"); } -ScrStream *scr_stream_compose(double n_d, ScrStream **streams) { - size_t n = (size_t)n_d; - if (n == 0) { +/* compose(...streams) over n streams and fs's function stages: the stream + * at pipeline position j must be readable unless it is last and writable + * unless it is first. A first stage that is not a stream (an iterable or + * source function) leaves the Duplex without a writable side. */ +static ScrStream *scr_stream_compose_impl(size_t n, ScrStream **streams, const ScrPipeStages *fs, + ScrClosure *abort) { + size_t total = n + (fs ? fs->count : 0); + if (total == 0) { static const char msg[] = "The \"streams\" argument must be specified"; scr_throw_error_msg_code(SCR_ERR_TYPE, msg, sizeof msg - 1, "ERR_MISSING_ARGS"); return NULL; } - if (n == 1) return scr_stream_retain(streams[0]); /* Duplex.from(duplex) */ - for (size_t i = 0; i < n; i++) { - if (i + 1 < n && !scr_stream_prop(streams[i], "readable")) { - scr_compose_arg_value(i, "readable", streams[i]); + if (total == 1 && n == 1) return scr_stream_retain(streams[0]); /* Duplex.from(duplex) */ + for (size_t j = 0, i = 0; j < total; j++) { + if (fs && j < 64 && ((fs->positions >> j) & 1u)) continue; + if (j + 1 < total && !scr_stream_prop(streams[i], "readable")) { + scr_compose_arg_value(j, "readable", streams[i]); return NULL; } - if (i > 0 && !scr_stream_prop(streams[i], "writable")) { - scr_compose_arg_value(i, "writable", streams[i]); + if (j > 0 && !scr_stream_prop(streams[i], "writable")) { + scr_compose_arg_value(j, "writable", streams[i]); return NULL; } + i++; } + bool stage_head = fs && (fs->positions & 1u); ScrCompose *c = calloc(1, sizeof *c); if (!c) scr_stream_oom(); c->rc = 1; - c->head = scr_stream_retain(streams[0]); + c->head = stage_head ? NULL : scr_stream_retain(streams[0]); c->tail = scr_stream_retain(streams[n - 1]); c->onclose = SCR_COMPOSE_RUNNING; ScrStream *tail = scr_stream_pipeline_ex(n, streams, scr_compose_closure((void *)&scr_compose_finished, c), - &scr_compose_finished, true, NULL, NULL, NULL, NULL, + &scr_compose_finished, true, NULL, NULL, fs, abort, NULL); if (!tail) { scr_compose_release_v(c); return NULL; } scr_stream_release(tail); - c->writable = scr_stream_prop(c->head, "writable") != 0; + c->writable = c->head && scr_stream_prop(c->head, "writable") != 0; c->readable = scr_stream_prop(c->tail, "readable") != 0; + /* readableObjectMode: !!tail?.readableObjectMode, writableObjectMode: + * !!head?.writableObjectMode */ bool object_mode = c->tail->st->has_r && c->tail->st->r.object_entries; + bool w_object = c->head && c->head->st->has_w && c->head->st->w.object_mode; ScrStream *d = scr_stream_new_duplex( - object_mode ? 16 : -1, -1, true, true, true, c->readable, c->writable, + object_mode ? 16 : -1, w_object ? 16 : -1, true, true, true, c->readable, c->writable, c->readable ? scr_compose_closure((void *)&scr_compose_read_inv, c) : NULL, c->readable ? &scr_compose_read_inv : NULL, c->writable ? scr_compose_closure((void *)&scr_compose_write_inv, c) : NULL, @@ -4387,6 +4682,7 @@ ScrStream *scr_stream_compose(double n_d, ScrStream **streams) { c->writable ? &scr_compose_final_inv : NULL, scr_compose_closure((void *)&scr_compose_destroy_inv, c), &scr_compose_destroy_inv); if (object_mode) d->st->r.object_entries = true; + if (w_object) scr_stream_set_object_mode(d, false, true); c->d = scr_stream_retain(d); if (c->writable) { scr_compose_on(c->head, "drain", &c->drain_l, (void *)&scr_compose_drain_l, c, scr_ee_inv_fixed0); @@ -4408,6 +4704,374 @@ ScrStream *scr_stream_compose(double n_d, ScrStream **streams) { return d; } +ScrStream *scr_stream_compose(double n_d, ScrStream **streams) { + return scr_stream_compose_impl((size_t)n_d, streams, NULL, NULL); +} + +/* compose(...stages) with function stages among the streams (the + * pipeline stage layout: scr_stream_pipeline_stages). */ +ScrStream *scr_stream_compose_stages(double n, ScrStream **streams, double count, ScrClosure **stages, + double positions, double flags, double mask, ScrClosure **helpers, + ScrClosure *box, ScrDyn *signal, ScrClosure *abort) { + ScrPipeStages fs = scr_pipe_stages_of(count, stages, positions, flags, mask, helpers, box, signal); + return scr_stream_compose_impl((size_t)n, streams, &fs, abort); +} + +/* ── Duplex.from over a stream or a { readable, writable } pair ───────── + * duplexify.js's _duplexify({ readable, writable }): a Duplexify that + * writes to w (write() answering false waits for 'drain'; final ends w and + * waits for its 'finish') and reads r through 'readable' and read(), with + * each side's end of stream (eos) finishing the Duplex. The readable and + * writable object modes follow r's and w's. */ + +enum { SCR_DUPX_UNSET, SCR_DUPX_NULL, SCR_DUPX_WAIT }; + +typedef struct ScrDuplexify { + size_t rc; + ScrStream *r, *w; /* +1 or NULL */ + ScrStream *d; /* +1 until teardown */ + ScrClosure *drain_l, *finish_l, *readable_l, *end_l; /* +1, the listeners, or NULL */ + ScrClosure *r_eos, *w_eos; /* +1: the eos listeners' cleanups, or NULL */ + bool readable, writable; + bool ondrain, onfinish, onreadable; + int onclose; /* _duplexify's onclose: undefined, null, or d's destroy callback */ + struct ScrDuplexify *live_prev, *live_next; +} ScrDuplexify; + +static SCR_TL ScrDuplexify *scr_dupx_live = NULL; + +static void *scr_dupx_retain_v(void *p) { + ((ScrDuplexify *)p)->rc++; + return p; +} + +static void scr_dupx_release_v(void *p) { + ScrDuplexify *x = p; + if (--x->rc > 0) return; + if (x->r_eos) scr_closure_release(x->r_eos); + if (x->w_eos) scr_closure_release(x->w_eos); + if (x->r) scr_stream_release(x->r); + if (x->w) scr_stream_release(x->w); + free(x); +} + +static ScrClosure *scr_dupx_closure(void *fn, ScrDuplexify *x) { + ScrClosure *cl = scr_closure_new(fn, 1); + cl->caps[0] = scr_box_new_obj(&scr_dupx_retain_v, &scr_dupx_release_v, NULL); + scr_box_set_ref(cl->caps[0], scr_dupx_retain_v(x)); + return cl; +} + +static void scr_dupx_on(ScrStream *s, const char *name, ScrClosure **slot, void *fn, ScrDuplexify *x) { + ScrStr *n = scr_str_new(name, strlen(name)); + *slot = scr_dupx_closure(fn, x); + scr_emitter_release( + scr_emitter_on((ScrEmitter *)s, n, scr_closure_retain(*slot), scr_ee_inv_fixed0, false, false)); + scr_str_release(n); +} + +/* Drop the Duplex and the listeners on r and w (the eos listeners stay + * registered; their cleanups, which hold the context, are dropped). */ +static void scr_dupx_teardown(ScrDuplexify *x) { + if (!x->d) return; + if (x->r_eos) scr_closure_release(x->r_eos); + if (x->w_eos) scr_closure_release(x->w_eos); + x->r_eos = x->w_eos = NULL; + if (x->w) { + scr_compose_off(x->w, "drain", &x->drain_l); + scr_compose_off(x->w, "finish", &x->finish_l); + } + if (x->r) { + scr_compose_off(x->r, "readable", &x->readable_l); + scr_compose_off(x->r, "end", &x->end_l); + } + if (x->live_prev) x->live_prev->live_next = x->live_next; + else scr_dupx_live = x->live_next; + if (x->live_next) x->live_next->live_prev = x->live_prev; + x->live_prev = x->live_next = NULL; + ScrStream *d = x->d; + x->d = NULL; + scr_stream_release(d); + scr_dupx_release_v(x); /* the live list's reference */ +} + +/* At exit, a Duplexify still open also drops its eos listeners on r and + * w: they hold the context, which holds r and w. */ +static void scr_dupx_atexit(void) { + while (scr_dupx_live) { + ScrDuplexify *x = scr_dupx_live; + ScrClosure *eos[2] = {x->r_eos, x->w_eos}; + x->r_eos = x->w_eos = NULL; + for (int i = 0; i < 2; i++) { + if (!eos[i]) continue; + ((void (*)(ScrClosure *))eos[i]->fn)(eos[i]); + scr_closure_release(eos[i]); + } + scr_dupx_teardown(x); + } +} + +/* d's destroy callback: callback(err), then the context lets go. */ +static void scr_dupx_destroyed(ScrDuplexify *x, ScrError *err) { + ScrStream *d = scr_stream_retain(x->d); + scr_dupx_teardown(x); + scr_stream_destroy_done(d, err ? scr_error_retain(err) : NULL); + scr_stream_release(d); +} + +/* onfinished(err): the pending destroy callback, else d.destroy(err) */ +static void scr_dupx_onfinished(ScrDuplexify *x, ScrError *err) { + int onclose = x->onclose; + x->onclose = SCR_DUPX_NULL; + if (!x->d) return; + if (onclose == SCR_DUPX_WAIT) { + scr_dupx_destroyed(x, err); + } else if (err) { + ScrStream *d = scr_stream_retain(x->d); + scr_stream_release(scr_stream_destroy(d, err)); + scr_stream_release(d); + } +} + +/* eos(w): writable = false; an error destroys r */ +static void scr_dupx_w_eos(ScrClosure *cb, ScrStream *s, ScrError *err) { + (void)s; + ScrDuplexify *x = scr_box_get_ref(cb->caps[0]); + x->writable = false; + if (err && x->r) scr_stream_release(scr_stream_destroy(x->r, err)); + scr_dupx_onfinished(x, err); + scr_dupx_release_v(x); +} + +/* eos(r): readable = false; an error destroys w */ +static void scr_dupx_r_eos(ScrClosure *cb, ScrStream *s, ScrError *err) { + (void)s; + ScrDuplexify *x = scr_box_get_ref(cb->caps[0]); + x->readable = false; + if (err && x->w) scr_stream_release(scr_stream_destroy(x->w, err)); + scr_dupx_onfinished(x, err); + scr_dupx_release_v(x); +} + +/* d._write: w.write(chunk) ? callback() : (ondrain = callback) */ +static void scr_dupx_write_inv(ScrClosure *cb, ScrStream *s, ScrBytes *chunk) { + ScrDuplexify *x = scr_box_get_ref(cb->caps[0]); + bool ok = scr_stream_writes_values(s) ? scr_stream_write_dyn(x->w, (ScrDyn *)chunk, NULL) + : scr_stream_write(x->w, chunk, NULL); + if (ok) { + if (!scr_exc_pending()) scr_stream_write_done(s, NULL); + } else { + x->ondrain = true; + } + scr_dupx_release_v(x); +} + +/* d._final: w.end(); onfinish = callback */ +static void scr_dupx_final_inv(ScrClosure *cb, ScrStream *s) { + (void)s; + ScrDuplexify *x = scr_box_get_ref(cb->caps[0]); + ScrStream *r = scr_stream_end(x->w, NULL, NULL, NULL); + if (r) scr_stream_release(r); + x->onfinish = true; + scr_dupx_release_v(x); +} + +static void scr_dupx_drain_l(ScrClosure *cb) { + ScrDuplexify *x = scr_box_get_ref(cb->caps[0]); + if (x->ondrain && x->d) { + x->ondrain = false; + scr_stream_write_done(x->d, NULL); + } + scr_dupx_release_v(x); +} + +static void scr_dupx_finish_l(ScrClosure *cb) { + ScrDuplexify *x = scr_box_get_ref(cb->caps[0]); + if (x->onfinish && x->d) { + x->onfinish = false; + scr_stream_final_done(x->d, NULL); + } + scr_dupx_release_v(x); +} + +/* d._read: push r.read() until it answers null (then wait for 'readable') + * or d is full */ +static void scr_dupx_pull(ScrDuplexify *x) { + ScrStream *d = x->d; + while (d && x->r) { + ScrEntryKind kind = SCR_ENTRY_BYTES; + void *chunk = scr_stream_read_n_kind(x->r, -1, &kind); + if (scr_exc_pending()) return; + if (!chunk) { + x->onreadable = true; + return; + } + bool more; + if (d->st->r.object_entries) { + more = scr_stream_add_entry(d, chunk, kind) > 0; + } else if (kind == SCR_ENTRY_STR) { + more = scr_stream_push_str(d, chunk); + scr_str_release(chunk); + } else if (kind == SCR_ENTRY_DYN) { + more = scr_stream_push_dyn(d, chunk); + scr_dyn_release(chunk); + } else { + more = scr_stream_push(d, chunk); + scr_bytes_release(chunk); + } + if (!more || scr_exc_pending()) return; + } +} + +static void scr_dupx_read_inv(ScrClosure *cb, ScrStream *s, double size) { + (void)s; + (void)size; + ScrDuplexify *x = scr_box_get_ref(cb->caps[0]); + x->onreadable = false; + scr_dupx_pull(x); + scr_dupx_release_v(x); +} + +static void scr_dupx_readable_l(ScrClosure *cb) { + ScrDuplexify *x = scr_box_get_ref(cb->caps[0]); + if (x->onreadable && x->d) { + x->onreadable = false; + scr_dupx_pull(x); + } + scr_dupx_release_v(x); +} + +static void scr_dupx_end_l(ScrClosure *cb) { + ScrDuplexify *x = scr_box_get_ref(cb->caps[0]); + if (x->d) scr_stream_push_null(x->d); + scr_dupx_release_v(x); +} + +/* d._destroy */ +static void scr_dupx_destroy_inv(ScrClosure *cb, ScrStream *s, ScrError *err) { + (void)s; + ScrDuplexify *x = scr_box_get_ref(cb->caps[0]); + ScrError *e = err ? scr_error_retain(err) : NULL; + if (!e && x->onclose != SCR_DUPX_NULL) e = scr_compose_abort_error(); + x->onreadable = x->ondrain = x->onfinish = false; + if (x->onclose == SCR_DUPX_NULL) { + if (x->d) scr_dupx_destroyed(x, e); + } else { + x->onclose = SCR_DUPX_WAIT; + if (x->w && !x->w->st->destroyed) scr_stream_release(scr_stream_destroy(x->w, e)); + if (x->r && !x->r->st->destroyed) scr_stream_release(scr_stream_destroy(x->r, e)); + } + if (e) scr_error_release(e); + scr_dupx_release_v(x); +} + +/* _duplexify({ readable: r, writable: w }): r and w borrowed, either NULL. + * +1. */ +ScrStream *scr_stream_duplexify(ScrStream *r, ScrStream *w) { + ScrDuplexify *x = calloc(1, sizeof *x); + if (!x) scr_stream_oom(); + x->rc = 1; + x->r = r ? scr_stream_retain(r) : NULL; + x->w = w ? scr_stream_retain(w) : NULL; + x->onclose = SCR_DUPX_UNSET; + x->readable = r && scr_stream_prop(r, "readable") != 0; + x->writable = w && scr_stream_prop(w, "writable") != 0; + /* new Duplexify({ readableObjectMode, writableObjectMode, readable, writable }) */ + bool r_object = r && r->st->has_r && r->st->r.object_entries; + bool w_object = w && w->st->has_w && w->st->w.object_mode; + ScrStream *d = scr_stream_new_duplex( + r_object ? 16 : -1, w_object ? 16 : -1, true, true, true, x->readable, x->writable, + x->readable ? scr_dupx_closure((void *)&scr_dupx_read_inv, x) : NULL, + x->readable ? &scr_dupx_read_inv : NULL, + x->writable ? scr_dupx_closure((void *)&scr_dupx_write_inv, x) : NULL, + x->writable ? &scr_dupx_write_inv : NULL, + x->writable ? scr_dupx_closure((void *)&scr_dupx_final_inv, x) : NULL, + x->writable ? &scr_dupx_final_inv : NULL, + scr_dupx_closure((void *)&scr_dupx_destroy_inv, x), &scr_dupx_destroy_inv); + if (r_object) d->st->r.object_entries = true; + if (w_object) scr_stream_set_object_mode(d, false, true); + d->cls = "Duplexify"; + x->d = scr_stream_retain(d); + if (x->writable) { + x->w_eos = scr_stream_finished(w, scr_dupx_closure((void *)&scr_dupx_w_eos, x), &scr_dupx_w_eos); + scr_dupx_on(w, "drain", &x->drain_l, (void *)&scr_dupx_drain_l, x); + scr_dupx_on(w, "finish", &x->finish_l, (void *)&scr_dupx_finish_l, x); + } + if (x->readable) { + x->r_eos = scr_stream_finished(r, scr_dupx_closure((void *)&scr_dupx_r_eos, x), &scr_dupx_r_eos); + scr_dupx_on(r, "readable", &x->readable_l, (void *)&scr_dupx_readable_l, x); + scr_dupx_on(r, "end", &x->end_l, (void *)&scr_dupx_end_l, x); + } + static SCR_TL bool registered = false; + if (!registered) { + registered = true; + scr_atexit(scr_dupx_atexit); + } + x->live_next = scr_dupx_live; + if (scr_dupx_live) scr_dupx_live->live_prev = x; + scr_dupx_live = x; /* the reference x was created with */ + return d; +} + +/* Duplex.from(promise) (duplexify.js): a Duplexify({ objectMode: true, + * writable: false, read() {} }) that pushes the promise's value unless it + * is nullish, then ends; a rejection destroys it with the reason. */ +typedef struct { + ScrStream *d; /* +1 */ + ScrDyn *boxed; /* +1, the promise boxed */ +} ScrDupxPromise; + +static void scr_dupx_noop_read(ScrClosure *cb, ScrStream *s, double size) { + (void)cb; + (void)s; + (void)size; +} + +static void scr_dupx_promise_entry(ScrFiber *fiber, void *arg) { + (void)fiber; + ScrDupxPromise p = *(ScrDupxPromise *)arg; + free(arg); + /* the boxed promise's payload is a dyn value (NULL: undefined) */ + ScrDyn *v = scr_await_ref(p.boxed->v.promise); + if (!scr_exc_pending()) { + if (v && v->kind != SCR_DYN_NULL && v->kind != SCR_DYN_UNDEF) scr_stream_push_object(p.d, v); + if (!scr_exc_pending()) scr_stream_push_null(p.d); + if (scr_exc_pending()) scr_exc_clear(); + if (v) scr_dyn_release(v); + } else { + ScrError *err = scr_pump_caught(); + scr_stream_release(scr_stream_destroy(p.d, err)); + if (scr_exc_pending()) scr_exc_clear(); + scr_error_release(err); + } + scr_stream_release(p.d); + scr_dyn_release(p.boxed); +} + +/* The readable Duplexify over a boxed promise (moves boxed). +1. */ +static ScrStream *scr_dupx_from_boxed(ScrDyn *boxed) { + ScrClosure *read = scr_closure_new((void *)&scr_dupx_noop_read, 0); + ScrStream *d = scr_stream_new_duplex(16, 16, true, true, true, true, false, read, + &scr_dupx_noop_read, NULL, NULL, NULL, NULL, NULL, NULL); + scr_stream_set_object_mode(d, true, true); + d->cls = "Duplexify"; + ScrDupxPromise *ctx = malloc(sizeof *ctx); + if (!ctx) scr_stream_oom(); + ctx->d = scr_stream_retain(d); + ctx->boxed = boxed; + scr_promise_release(scr_async_spawn_after(ctx->boxed->v.promise, &scr_dupx_promise_entry, ctx)); + return d; +} + +/* p and box borrowed (box: the compiler's promise → dyn promise helper). +1. */ +ScrStream *scr_stream_duplex_from_promise(ScrPromise *p, ScrClosure *box) { + return scr_dupx_from_boxed(((ScrPipeBoxFn)box->fn)(box, scr_promise_retain(p))); +} + +ScrStream *scr_stream_duplexify_readable(ScrStream *r) { return scr_stream_duplexify(r, NULL); } +ScrStream *scr_stream_duplexify_writable(ScrStream *w) { return scr_stream_duplexify(NULL, w); } +ScrStream *scr_stream_duplexify_pair(ScrStream *r, ScrStream *w) { return scr_stream_duplexify(r, w); } + /* ── node:stream/promises ───────────────────────────────────────────── * The promise forms ride the callback machinery above with a settling * watcher: caps[0] boxes the pending promise; the terminal status @@ -4484,6 +5148,24 @@ ScrPromise *scr_sp_pipeline_stages(double n, ScrStream **streams, double count, return p; } +/* The options' signal: NULL for a nullish one; false (p rejected with + * ERR_INVALID_ARG_TYPE) for a value that is not an AbortSignal. */ +static bool scr_sp_opts_signal(ScrPromise *p, ScrDyn **signal) { + if (*signal && scr_dyn_is_nullish(*signal)) *signal = NULL; + if (!*signal) return true; + ScrDyn *aborted = NULL; + if ((*signal)->kind == SCR_DYN_OBJ || (*signal)->kind == SCR_DYN_HANDLE) { + aborted = scr_stream_signal_prop(*signal, "aborted"); + } + if (!aborted) { + scr_dyn_prop_type_fail("options.signal", "an instance of AbortSignal", *signal); + scr_promise_reject_pending(p); + return false; + } + scr_dyn_release(aborted); + return true; +} + /* pipeline(...streams, { signal, end }): end false leaves the destination * open (the promise settles once the stages before it finish); signal is * NULL or borrowed — a value that is not an AbortSignal rejects with @@ -4491,19 +5173,7 @@ ScrPromise *scr_sp_pipeline_stages(double n, ScrStream **streams, double count, ScrPromise *scr_sp_pipeline_opts(double n, ScrStream **streams, bool end, ScrDyn *signal, ScrAbortAddFn add) { ScrPromise *p = scr_promise_new(); - if (signal && scr_dyn_is_nullish(signal)) signal = NULL; - if (signal) { - ScrDyn *aborted = NULL; - if (signal->kind == SCR_DYN_OBJ || signal->kind == SCR_DYN_HANDLE) { - aborted = scr_stream_signal_prop(signal, "aborted"); - } - if (!aborted) { - scr_dyn_prop_type_fail("options.signal", "an instance of AbortSignal", signal); - scr_promise_reject_pending(p); - return p; - } - scr_dyn_release(aborted); - } + if (!scr_sp_opts_signal(p, &signal)) return p; ScrStream *dst = scr_stream_pipeline_ex((size_t)n, streams, scr_sp_watcher(p), &scr_sp_settle_inv, end, signal, add, NULL, NULL, NULL); if (dst) scr_stream_release(dst); @@ -4511,6 +5181,25 @@ ScrPromise *scr_sp_pipeline_opts(double n, ScrStream **streams, bool end, ScrDyn return p; } +/* scr_sp_pipeline_stages with the options argument: end and the + * options' signal (outer, NULL or borrowed) as scr_sp_pipeline_opts + * takes them. The stages' signal is the pipeline's own. */ +ScrPromise *scr_sp_pipeline_stages_opts(double n, ScrStream **streams, double count, + ScrClosure **stages, double positions, double flags, + double mask, ScrClosure **helpers, ScrClosure *box, + ScrDyn *signal, ScrClosure *abort, bool end, + ScrDyn *outer, ScrAbortAddFn add) { + ScrPromise *p = scr_promise_new(); + if (!scr_sp_opts_signal(p, &outer)) return p; + ScrPipeStages fs = scr_pipe_stages_of(count, stages, positions, flags, mask, helpers, box, signal); + ScrPipeValInv vi = fs.dest != SCR_PIPE_DEST_STREAM ? &scr_sp_value_inv : NULL; + ScrStream *dst = scr_stream_pipeline_ex((size_t)n, streams, scr_sp_watcher(p), &scr_sp_settle_inv, + end, outer, add, &fs, abort, vi); + if (dst) scr_stream_release(dst); + else scr_promise_reject_pending(p); + return p; +} + /* ── node:stream/consumers ──────────────────────────────────────────── * text/json/buffer/arrayBuffer over the readable machinery: a native 'data' listener * accumulates every chunk (Buffer chunks as-is; string chunks — an @@ -5542,7 +6231,69 @@ void scr_stream_set_object_mode(ScrStream *s, bool readable, bool writable) { st->r.object_entries = true; st->r.object_mode_opt = true; } - if (writable && st->has_w) st->w.object_mode = true; + if (writable && st->has_w) st->w.object_mode = st->w.values = true; +} + +/* decodeStrings: false — string writes stay strings (scr_stream_write_string). */ +void scr_stream_set_decode_strings(ScrStream *s, bool decode) { + ScrStreamState *st = s->st; + if (decode || !st->has_w) return; + st->w.decode_strings_off = true; + st->w.values = true; +} + +ScrStream *scr_stream_decode_strings(ScrStream *s, bool decode) { + scr_stream_set_decode_strings(s, decode); + return scr_stream_retain(s); +} + +static void scr_stream_store_default_enc(ScrStreamState *st, ScrStr *enc /*moves*/) { + if (st->w.default_enc) scr_str_release(st->w.default_enc); + st->w.default_enc = NULL; + if ((enc->len == 4 && memcmp(enc->data, "utf8", 4) == 0) || + (enc->len == 5 && memcmp(enc->data, "utf-8", 5) == 0)) { + scr_str_release(enc); /* Node's kDefaultUTF8Encoding */ + return; + } + st->w.default_enc = enc; +} + +/* The defaultEncoding option on the writable side, a known encoding as + * given (borrowed; validated by the caller). */ +void scr_stream_set_write_encoding(ScrStream *s, ScrStr *enc) { + if (s->st->has_w) scr_stream_store_default_enc(s->st, scr_str_retain(enc)); +} + +ScrStream *scr_stream_write_encoding(ScrStream *s, ScrStr *enc) { + scr_stream_set_write_encoding(s, enc); + return scr_stream_retain(s); +} + +/* setDefaultEncoding(encoding): the name lowercased, a known encoding or + * ERR_UNKNOWN_ENCODING (thrown); answers the stream (+1). */ +ScrStream *scr_stream_set_default_encoding(ScrStream *s, ScrDyn *enc) { + ScrStr *raw = enc->kind == SCR_DYN_STR ? scr_str_retain(enc->v.str) : scr_dyn_string_coerce_js(enc); + ScrStr *lower = raw; + if (enc->kind == SCR_DYN_STR) { + char *buf = malloc(raw->len ? raw->len : 1); + if (!buf) scr_stream_oom(); + for (size_t i = 0; i < raw->len; i++) { + char c = raw->data[i]; + buf[i] = c >= 'A' && c <= 'Z' ? (char)(c - 'A' + 'a') : c; + } + lower = scr_str_new(buf, raw->len); + free(buf); + scr_str_release(raw); + } + ScrStr *canonical = scr_bytes_normalize_encoding(lower); + if (canonical == NULL) { + scr_str_release(lower); + return scr_stream_retain(s); + } + scr_str_release(canonical); + if (s->st->has_w) scr_stream_store_default_enc(s->st, lower); + else scr_str_release(lower); + return scr_stream_retain(s); } /* stream.objectMode: scr_stream_set_object_mode, answering the receiver @@ -5556,7 +6307,7 @@ ScrStream *scr_stream_object_mode(ScrStream *s, bool readable, bool writable) { * object-mode writable side): the compiler's chunk thunks read the chunk * by this. */ bool scr_stream_writes_values(ScrStream *s) { - return s->st->has_w && s->st->w.object_mode; + return s->st->has_w && s->st->w.values; } /* emit('data', value) with a checked-dynamic chunk: the 'data' slots by @@ -5614,6 +6365,23 @@ bool scr_stream_push(ScrStream *s, ScrBytes *chunk) { return scr_stream_add_chunk(s, chunk, false); } +/* readable.push(chunk) from a program: in byte mode another + * ArrayBufferView becomes a Buffer over the same bytes + * (Stream._uint8ArrayToBuffer); the runtime's own producers push Buffers + * through scr_stream_push. */ +bool scr_stream_push_view(ScrStream *s, ScrBytes *chunk) { + if (chunk && !chunk->is_buffer && !(s->st->has_r && s->st->r.object_entries)) { + ScrBytes *b = scr_bytes_buffer_view(chunk, SCR_BYTES_U8, scr_bytes_byte_offset(chunk), true, + scr_bytes_byte_len(chunk)); + if (!b) return false; + b->is_buffer = true; + bool r = scr_stream_add_chunk(s, b, false); + scr_bytes_release(b); + return r; + } + return scr_stream_add_chunk(s, chunk, false); +} + bool scr_stream_push_str(ScrStream *s, ScrStr *str) { /* object mode: the string is one whole entry, not converted */ if (s->st->has_r && s->st->r.object_entries) @@ -6050,6 +6818,60 @@ static bool scr_stream_write_value(ScrStream *s, ScrDyn *v, ScrClosure *cb) { return scr_stream_write_chunk_enc(s, (ScrBytes *)v, cb, scr_stream_cb_enc(cb)); } +/* A Buffer write on a byte-mode side: with decodeStrings: false the + * queue holds values, so the Buffer is boxed with its 'buffer' encoding. + * Borrows chunk. */ +static bool scr_stream_write_buffer(ScrStream *s, ScrBytes *chunk, ScrClosure *cb) { + ScrStreamState *st = s->st; + if (!st->has_w || !st->w.values) return scr_stream_write_chunk(s, chunk, cb); + ScrDyn *v = chunk->is_buffer ? scr_dyn_new_buffer(chunk) : scr_dyn_new_bytes(chunk); + ScrStr *buffer = scr_str_new("buffer", 6); + ScrDyn *enc = scr_dyn_new_str(buffer); + scr_str_release(buffer); + bool ret = scr_stream_write_chunk_enc(s, (ScrBytes *)v, cb, enc); + scr_dyn_release(enc); + scr_dyn_release(v); + return ret; +} + +/* A string write on a byte-mode side, with the encoding argument as + * given (borrowed; NULL takes the default encoding): decodeStrings + * encodes it (Buffer.from(chunk, encoding)), and with decodeStrings: + * false it stays a string that _write receives with that encoding. */ +static bool scr_stream_write_string(ScrStream *s, ScrStr *str, ScrStr *name, ScrClosure *cb) { + ScrStreamState *st = s->st; + ScrStr *given = name ? scr_str_retain(name) : st->has_w && st->w.default_enc ? scr_str_retain(st->w.default_enc) : NULL; + if (st->has_w && st->w.decode_strings_off) { + ScrStr *e = given ? given : scr_str_new("utf8", 4); + ScrDyn *v = scr_dyn_new_str(str); + ScrDyn *enc = scr_dyn_new_str(e); + scr_str_release(e); + bool ret = scr_stream_write_chunk_enc(s, (ScrBytes *)v, cb, enc); + scr_dyn_release(enc); + scr_dyn_release(v); + return ret; + } + ScrBytes *b; + if (given) { + ScrStr *canonical = scr_bytes_normalize_encoding(given); + scr_str_release(given); + if (canonical == NULL) { + if (cb) scr_closure_release(cb); + return false; + } + b = scr_bytes_from_str(str, canonical); + scr_str_release(canonical); + } else { + b = scr_bytes_new(SCR_BYTES_U8, (double)str->len); + memcpy(b->data, str->data, str->len); + if (!str->ascii) scr_wtf8_to_utf8((char *)b->data, str->len); + } + scr_stream_buffer(b); + bool ret = scr_stream_write_buffer(s, b, cb); + scr_bytes_release(b); + return ret; +} + bool scr_stream_write(ScrStream *s, ScrBytes *chunk, ScrClosure *cb) { if (s->st->has_w && s->st->w.object_mode) { ScrDyn *v = chunk->is_buffer ? scr_dyn_new_buffer(chunk) : scr_dyn_new_bytes(chunk); @@ -6057,7 +6879,22 @@ bool scr_stream_write(ScrStream *s, ScrBytes *chunk, ScrClosure *cb) { scr_dyn_release(v); return ret; } - return scr_stream_write_chunk(s, chunk, cb); + if (!chunk->is_buffer) { + /* Writable's _write: a typed array or DataView chunk becomes a Buffer + * over the same bytes (Stream._uint8ArrayToBuffer), which _write, + * _writev and a decodeStrings: false queue receive. */ + ScrBytes *b = scr_bytes_buffer_view(chunk, SCR_BYTES_U8, scr_bytes_byte_offset(chunk), true, + scr_bytes_byte_len(chunk)); + if (!b) { + if (cb) scr_closure_release(cb); + return false; + } + b->is_buffer = true; + bool ret = scr_stream_write_buffer(s, b, cb); + scr_bytes_release(b); + return ret; + } + return scr_stream_write_buffer(s, chunk, cb); } /* write(null) reached at runtime (a nullable-union chunk's null arm): @@ -6077,13 +6914,7 @@ bool scr_stream_write_str(ScrStream *s, ScrStr *str, ScrClosure *cb) { scr_dyn_release(v); return ret; } - ScrBytes *b = scr_bytes_new(SCR_BYTES_U8, (double)str->len); - memcpy(b->data, str->data, str->len); - if (!str->ascii) scr_wtf8_to_utf8((char *)b->data, str->len); - scr_stream_buffer(b); - bool ret = scr_stream_write_chunk(s, b, cb); - scr_bytes_release(b); - return ret; + return scr_stream_write_string(s, str, NULL, cb); } /* write(chunk, encoding, cb) with the encoding argument as passed (NULL @@ -6103,20 +6934,15 @@ bool scr_stream_write_enc(ScrStream *s, const ScrDyn *chunk, ScrDyn *enc, ScrClo name = scr_dyn_string_coerce_js(enc); if (name && !(name->len == 6 && memcmp(name->data, "buffer", 6) == 0)) { ScrStr *canonical = scr_bytes_normalize_encoding(name); - scr_str_release(name); if (canonical == NULL) { + scr_str_release(name); if (cb) scr_closure_release(cb); return false; } - if (chunk->kind != SCR_DYN_STR) { - scr_str_release(canonical); - return scr_stream_write_dyn(s, chunk, cb); - } - ScrBytes *b = scr_bytes_from_str(chunk->v.str, canonical); scr_str_release(canonical); - scr_stream_buffer(b); - bool ret = scr_stream_write_chunk(s, b, cb); - scr_bytes_release(b); + bool ret = chunk->kind == SCR_DYN_STR ? scr_stream_write_string(s, chunk->v.str, name, cb) + : scr_stream_write_dyn(s, chunk, cb); + scr_str_release(name); return ret; } if (name && chunk->kind == SCR_DYN_STR) { @@ -6535,8 +7361,41 @@ static void scr_stream_emit_readable_now(ScrStream *s) { /* ── tick dispatch ────────────────────────────────────────────────────── */ /* onConstruct + the readable/writable onConstructed hooks. err moves. */ +/* aggregateTwoErrors(inner, outer) for streams without native ops: both + * present and distinct make an AggregateError([outer, inner]) with + * outer's message and code. Borrows both; +1 or NULL. */ +static ScrError *scr_stream_aggregate_errors(ScrError *inner, ScrError *outer) { + if (inner && outer && inner != outer) { + ScrDyn *message = scr_dyn_new_str(outer->message); + ScrError *agg = scr_aggregate_new(message, scr_dyn_undefined()); + scr_dyn_release(message); + if (agg == NULL) return scr_error_retain(outer); + ScrDyn *outer_value = scr_dyn_from_error(outer); + ScrDyn *list = scr_dyn_new_arr(); + scr_dyn_arr_push(list, scr_dyn_retain(outer_value)); + scr_dyn_arr_push(list, scr_dyn_from_error(inner)); + scr_aggregate_set_errors(agg, list); + scr_dyn_release(list); + ScrDyn *code = scr_dyn_obj_read(outer_value, "code", 4); + if (code && code->kind == SCR_DYN_STR) scr_error_set_code(agg, code->v.str->data); + scr_dyn_release(code); + scr_dyn_release(outer_value); + return agg; + } + ScrError *one = outer ? outer : inner; + return one ? scr_error_retain(one) : NULL; +} + static void scr_stream_constructed(ScrStream *s, ScrError *err) { ScrStreamState *st = s->st; + if (st->construct_finished) { + /* onConstruct called again: errorOrDestroy(err ?? ERR_MULTIPLE_CALLBACK) */ + if (!err) err = scr_stream_mkerr("ERR_MULTIPLE_CALLBACK", "Callback called multiple times"); + scr_stream_error_or_destroy(s, err); + scr_error_release(err); + return; + } + st->construct_finished = true; st->constructing = false; if (st->construct_cb) { scr_closure_release(st->construct_cb); @@ -6549,8 +7408,9 @@ static void scr_stream_constructed(ScrStream *s, ScrError *err) { /* aggregateTwoErrors(constructError, destroyError): the owner builds * the AggregateError when both are present and distinct */ ScrError *use; - if (err && derr && err != derr && st->native_ops && st->native_ops->aggregate) { - use = st->native_ops->aggregate(err, derr); + if (err && derr && err != derr) { + use = st->native_ops && st->native_ops->aggregate ? st->native_ops->aggregate(err, derr) + : scr_stream_aggregate_errors(err, derr); } else { use = derr ? derr : err; if (use) scr_error_retain(use); @@ -6589,6 +7449,39 @@ void scr_stream_construct_done(ScrStream *s, ScrError *err /*moves*/) { void scr_stream_set_writev_concat(ScrStream *s) { s->st->writev_concat = true; } +/* A user construct option or _construct method: Node schedules it from the + * constructor (construct(stream, cb)), and the hook's callback reports + * through scr_stream_construct_done. */ +void scr_stream_set_construct(ScrStream *s, ScrClosure *cb /*moves*/, ScrStreamPlainInv inv) { + if (s->st->construct_cb || s->st->construct_finished) { + scr_closure_release(cb); + return; + } + scr_stream_construct(s, cb, inv); +} + +/* A class's _writev under options that may carry their own writev (the + * option, an instance property in Node, wins). */ +void scr_stream_set_writev_default(ScrStream *s, ScrClosure *cb /*moves*/, ScrStreamWritevInv inv) { + if (s->st->has_w && s->st->w.writev_cb == NULL) { + scr_stream_set_writev(s, cb, inv); + return; + } + scr_closure_release(cb); +} + +/* A user writev option or _writev method. */ +void scr_stream_set_writev(ScrStream *s, ScrClosure *cb /*moves*/, ScrStreamWritevInv inv) { + ScrStreamState *st = s->st; + if (!st->has_w) { + scr_closure_release(cb); + return; + } + if (st->w.writev_cb) scr_closure_release(st->w.writev_cb); + st->w.writev_cb = cb; + st->w.writev_inv = inv; +} + void scr_stream_fail(ScrStream *s, ScrError *err) { scr_stream_error_or_destroy(s, err); } static void scr_stream_plain_inv(ScrClosure *cb, va_list ap) { @@ -6780,6 +7673,7 @@ static void scr_stream_run_tick(ScrStreamTick *t) { if (st->r.read_cb) { scr_closure_release(st->r.read_cb); st->r.read_cb = NULL; } if (st->w.write_cb) { scr_closure_release(st->w.write_cb); st->w.write_cb = NULL; } if (st->w.final_cb) { scr_closure_release(st->w.final_cb); st->w.final_cb = NULL; } + if (st->w.writev_cb) { scr_closure_release(st->w.writev_cb); st->w.writev_cb = NULL; } if (st->transform_cb) { scr_closure_release(st->transform_cb); st->transform_cb = NULL; } if (st->flush_cb) { scr_closure_release(st->flush_cb); st->flush_cb = NULL; } if (st->destroy_cb) { scr_closure_release(st->destroy_cb); st->destroy_cb = NULL; } @@ -6834,7 +7728,11 @@ static void scr_stream_run_tick(ScrStreamTick *t) { scr_stream_error_buffer(s); break; case SCR_ST_CONSTRUCT: { - if (st->construct_cb) st->construct_inv(st->construct_cb, s); + if (st->construct_cb) { + st->construct_inv(st->construct_cb, s); + /* a throw inside _construct reaches onConstruct on a tick */ + if (scr_exc_pending()) scr_stream_construct_done(s, scr_stream_take_error()); + } break; } case SCR_ST_CONSTRUCTED: { @@ -7038,6 +7936,18 @@ static ScrStream *scr_stream_boxed(const ScrDyn *value) { return NULL; } +/* The native readable stream behind a checked-dynamic value (+1): a + * boxed stream or a Cipheriv/Decipheriv handle's Transform, or NULL. */ +ScrStream *scr_stream_native_readable_of_dyn(const ScrDyn *value) { + ScrStream *s = scr_stream_of_handle(value); + if (!s) s = scr_stream_boxed(value); + if (s && !s->st->has_r) { + scr_stream_release(s); + return NULL; + } + return s; +} + /* stream.handleStream: the stream of a value typed as a stream handle * class (a Cipheriv or Decipheriv), +1. A value of that type can also be * the stream itself, boxed (the stream addAbortSignal answers). */ diff --git a/packages/runtime/src/scr_stream.h b/packages/runtime/src/scr_stream.h index 8b8a72406..d26077b5c 100644 --- a/packages/runtime/src/scr_stream.h +++ b/packages/runtime/src/scr_stream.h @@ -65,6 +65,8 @@ typedef void (*ScrStreamReadInv)(ScrClosure *cb, ScrStream *s, double size); typedef void (*ScrStreamChunkInv)(ScrClosure *cb, ScrStream *s, ScrBytes *chunk); typedef void (*ScrStreamPlainInv)(ScrClosure *cb, ScrStream *s); typedef void (*ScrStreamErrInv)(ScrClosure *cb, ScrStream *s, ScrError *err); +/* writev(chunks, callback): chunks is the batch's dyn array (borrowed). */ +typedef void (*ScrStreamWritevInv)(ScrClosure *cb, ScrStream *s, struct ScrDyn *chunks); /* Constructors (+1 result; option callbacks MOVE; NULL = absent). hwm < 0 * means the byte default (65536 — Node 24's getDefaultHighWaterMark). */ @@ -223,6 +225,9 @@ ScrStream *scr_stream_pipeline_stages(double n, ScrStream **streams, double coun double mask, ScrClosure **helpers, ScrClosure *box, struct ScrDyn *signal, ScrClosure *abort, ScrClosure *cb, ScrStreamErrInv inv); +ScrStream *scr_stream_compose_stages(double n, ScrStream **streams, double count, ScrClosure **stages, + double positions, double flags, double mask, ScrClosure **helpers, + ScrClosure *box, ScrDyn *signal, ScrClosure *abort); ScrStream *scr_stream_pipeline_stages_dyn(double n, ScrStream **streams, double count, ScrClosure **stages, double positions, double flags, double mask, ScrClosure **helpers, ScrClosure *box, @@ -261,6 +266,9 @@ ScrDyn *scr_stream_done_dyn_l(ScrClosure *clo, ScrDyn *const *args, size_t argc) /* The readable surface. push/unshift borrow their chunk (strings convert * utf8, Node's decodeStrings default); push answers the below-hwm bool. */ bool scr_stream_push(ScrStream *s, ScrBytes *chunk); +/* readable.push(chunk) from a program: a non-Buffer view becomes a Buffer + * over the same bytes in byte mode. */ +bool scr_stream_push_view(ScrStream *s, ScrBytes *chunk); bool scr_stream_push_str(ScrStream *s, ScrStr *str); /* push(chunk, enc): the per-call literal encoding (canonical); overrides * the stream's defaultEncoding. Borrows both. */ @@ -296,6 +304,14 @@ bool scr_stream_write_dyn(ScrStream *s, const ScrDyn *d, ScrClosure *cb); /* cb bool scr_stream_write_enc(ScrStream *s, const ScrDyn *chunk, ScrDyn *enc, ScrClosure *cb); ScrPromise *scr_stream_next_chunk(ScrStream *s); ScrPromise *scr_stream_next_chunk_dyn(ScrStream *s); +/* The same step resolving with an iterator result ({ value, done }). */ +ScrPromise *scr_stream_next_result_dyn(ScrStream *s); +/* The native readable stream behind a checked-dynamic value (+1), or NULL. */ +ScrStream *scr_stream_native_readable_of_dyn(const struct ScrDyn *value); +/* for await over a checked-dynamic value in a program with streams + * (scr_stream_ops.c): a native readable stream iterates as a typed loop + * does; any other value takes scr_dyn_async_iterator_open. */ +struct ScrDyn *scr_stream_dyn_async_iterator_open(struct ScrDyn *value, ScrStr *spell); /* node:stream/promises — the promise forms over the finished/pipeline * machinery above: a pending void promise the terminal watcher settles * (fulfilled on a clean finish, rejected with the finish status @@ -329,6 +345,7 @@ enum { SCR_FROM_BYTE_MODE = 4, SCR_FROM_HAS_HWM = 8, SCR_FROM_DEFAULT_HWM = 16, /* a nullish highWaterMark: the mode's default */ + SCR_FROM_DUPLEX = 32, /* Duplex.from: a Duplexify with its writable side off */ }; ScrStream *scr_stream_from_arr(ScrArr *arr, double flags, double hwm); /* Readable.from(iterable, options) over a checked-dynamic source: a @@ -414,18 +431,41 @@ int scr_stream_push_object(ScrStream *s, const ScrDyn *v); * dyn array, find and reduce a dyn value — reduce's `kind` picks the * payload instead: 0 dyn, 1 number, 2 boolean, 3 string — some and every * a boolean, forEach nothing. */ -ScrStream *scr_stream_op_map(ScrStream *s, ScrDyn *fn, ScrDyn *options); -ScrStream *scr_stream_op_filter(ScrStream *s, ScrDyn *fn, ScrDyn *options); -ScrStream *scr_stream_op_flat_map(ScrStream *s, ScrDyn *fn, ScrDyn *options); +ScrStream *scr_stream_op_map(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts); +ScrStream *scr_stream_op_filter(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts); +ScrStream *scr_stream_op_flat_map(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts); +/* flatMap whose callback returns a compiled generator: the helpers over its + * type (async: next, read, return, throw; sync: next, return, throw; + * borrowed). */ +ScrStream *scr_stream_op_flat_map_async_gen(ScrStream *s, ScrDyn *fn, ScrDyn *options, + ScrDyn *cb_opts, ScrClosure *next, ScrClosure *read, + ScrClosure *ret, ScrClosure *thr); +ScrStream *scr_stream_op_flat_map_sync_gen(ScrStream *s, ScrDyn *fn, ScrDyn *options, + ScrDyn *cb_opts, ScrClosure *next, ScrClosure *ret, + ScrClosure *thr); ScrStream *scr_stream_op_drop(ScrStream *s, ScrDyn *number, ScrDyn *options); ScrStream *scr_stream_op_take(ScrStream *s, ScrDyn *number, ScrDyn *options); ScrPromise *scr_stream_op_to_array(ScrStream *s, ScrDyn *options); -ScrPromise *scr_stream_op_for_each(ScrStream *s, ScrDyn *fn, ScrDyn *options); -ScrPromise *scr_stream_op_some(ScrStream *s, ScrDyn *fn, ScrDyn *options); -ScrPromise *scr_stream_op_every(ScrStream *s, ScrDyn *fn, ScrDyn *options); -ScrPromise *scr_stream_op_find(ScrStream *s, ScrDyn *fn, ScrDyn *options); +ScrPromise *scr_stream_op_for_each(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts); +ScrPromise *scr_stream_op_some(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts); +ScrPromise *scr_stream_op_every(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts); +ScrPromise *scr_stream_op_find(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts); ScrPromise *scr_stream_op_reduce(ScrStream *s, ScrDyn *fn, ScrDyn *init, bool has_init, ScrDyn *options, double kind); +/* reduce whose reducer reads its { signal } (cb_opts): abort is ac.abort() + * (borrowed), run when the reduction ends. */ +ScrPromise *scr_stream_op_reduce_signal(ScrStream *s, ScrDyn *fn, ScrDyn *init, bool has_init, + ScrDyn *options, double kind, ScrClosure *abort, + ScrDyn *cb_opts); +/* `options?.signal` (+1, undefined when absent) and `{ once: true }` (+1), + * for the listener that makes options.signal abort reduce's controller. */ +ScrDyn *scr_stream_op_signal_of(ScrDyn *options); +ScrDyn *scr_stream_op_once_options(void); +/* The operator callbacks' { signal } (the options argument): the signals + * AbortSignal.any combines (`[options?.signal].filter(Boolean)`) and the + * object around the combined signal. +1 each. */ +ScrDyn *scr_stream_op_signal_list(ScrDyn *options); +ScrDyn *scr_stream_op_signal_opts(ScrDyn *signal); /* finished(s, options[, cb]) — Node's eos with its listeners and state * predicates: readable/writable are -1 (the default), 0 or 1; on_error is * options.error !== false; signal is NULL or borrowed (ERR_INVALID_ARG_TYPE @@ -445,6 +485,12 @@ ScrPromise *scr_sp_finished_opts(ScrStream *s, double readable, double writable, * ERR_INVALID_ARG_TYPE. Streams borrowed. */ ScrPromise *scr_sp_pipeline_opts(double n, ScrStream **streams, bool end, ScrDyn *signal, ScrAbortAddFn add); +/* scr_sp_pipeline_stages with the options' end and signal (outer). */ +ScrPromise *scr_sp_pipeline_stages_opts(double n, ScrStream **streams, double count, + ScrClosure **stages, double positions, double flags, + double mask, ScrClosure **helpers, ScrClosure *box, + ScrDyn *signal, ScrClosure *abort, bool end, + ScrDyn *outer, ScrAbortAddFn add); double scr_stream_prop(ScrStream *s, const char *name); ScrError *scr_stream_errored(ScrStream *s); /* +1 or NULL */ @@ -512,6 +558,57 @@ void scr_stream_flush_done_value(ScrStream *s, ScrError *err, struct ScrDyn *v); void scr_stream_construct(ScrStream *s, ScrClosure *cb, ScrStreamPlainInv inv); void scr_stream_construct_done(ScrStream *s, ScrError *err); void scr_stream_set_writev_concat(ScrStream *s); +/* The user construct option / _construct method (scheduled now; cb + * moves), and the writev option / _writev method (cb moves). */ +void scr_stream_set_construct(ScrStream *s, ScrClosure *cb, ScrStreamPlainInv inv); +void scr_stream_set_writev(ScrStream *s, ScrClosure *cb, ScrStreamWritevInv inv); +void scr_stream_set_writev_default(ScrStream *s, ScrClosure *cb, ScrStreamWritevInv inv); +/* Duplex.from (duplexify.js): over an async generator function (the + * compiled stage takes the source generator that the compiled source + * makes from next() and ack(); all borrowed, helpers too), and over a + * checked-dynamic body (borrowed). +1 each. The typed iterable bodies take + * the Readable.from builders with SCR_FROM_DUPLEX. */ +ScrStream *scr_stream_duplex_from_gen(ScrClosure *stage, ScrClosure *source, ScrClosure *next, + ScrClosure *read, ScrClosure *ret, ScrClosure *thr); +ScrStream *scr_stream_duplex_from_dyn(struct ScrDyn *body); +/* Readable.fromWeb(readableStream, { objectMode, highWaterMark, encoding }) + * (scr_stream_from_web.c): hwm < 0 is the mode's default, encoding NULL or + * a known name; web and encoding borrowed. +1, or NULL with the error. */ +ScrStream *scr_stream_readable_from_web(struct ScrDyn *web, bool object_mode, double hwm, ScrStr *encoding); +/* Duplex.from over an async function (the compiled stage takes the source + * generator and answers its promise; source and the promise's box helper + * borrowed): a writable Duplexify whose final waits for the promise. +1. */ +ScrStream *scr_stream_duplex_from_async_fn(ScrClosure *stage, ScrClosure *source, ScrClosure *box); +/* The same for a body that declares the options argument: it takes + * { signal } (signal borrowed), and destroy calls abort(err) (borrowed), + * the AbortController's abort, before it settles (fromAsyncGen's + * ac.abort(err)). */ +ScrStream *scr_stream_duplex_from_gen_signal(ScrClosure *stage, ScrClosure *source, ScrClosure *next, + ScrClosure *read, ScrClosure *ret, ScrClosure *thr, + ScrClosure *abort, struct ScrDyn *signal); +ScrStream *scr_stream_duplex_from_async_fn_signal(ScrClosure *stage, ScrClosure *source, + ScrClosure *box, ScrClosure *abort, + struct ScrDyn *signal); +/* Duplex.from over a Readable, a Writable, or a { readable, writable } + * pair (duplexify.js's _duplexify): a Duplexify that reads r and writes w + * (either NULL in scr_stream_duplexify; streams borrowed). +1. */ +ScrStream *scr_stream_duplexify(ScrStream *r, ScrStream *w); +ScrStream *scr_stream_duplexify_readable(ScrStream *r); +ScrStream *scr_stream_duplexify_writable(ScrStream *w); +ScrStream *scr_stream_duplexify_pair(ScrStream *r, ScrStream *w); +/* Duplex.from(promise): a readable Duplexify of the promise's value. p and + * box (the compiler's promise → dyn promise helper) borrowed. +1. */ +ScrStream *scr_stream_duplex_from_promise(struct ScrPromise *p, ScrClosure *box); +/* decodeStrings (false keeps string writes as strings), the writable + * defaultEncoding option (a known encoding, borrowed), and + * setDefaultEncoding (validated; answers the stream +1). The chaining + * forms answer the stream (+1). */ +void scr_stream_set_decode_strings(ScrStream *s, bool decode); +ScrStream *scr_stream_decode_strings(ScrStream *s, bool decode); +void scr_stream_set_write_encoding(ScrStream *s, ScrStr *enc); +ScrStream *scr_stream_write_encoding(ScrStream *s, ScrStr *enc); +ScrStream *scr_stream_set_default_encoding(ScrStream *s, struct ScrDyn *enc); +ScrDyn *scr_stream_done_dyn_c(ScrClosure *clo, ScrDyn *const *args, size_t argc); /* The native state's extra operations: view adds its members to the * stream's untyped view and close is the view's close() (both NULL when * absent); aggregate builds aggregateTwoErrors' +1 AggregateError from the diff --git a/packages/runtime/src/scr_stream_from_web.c b/packages/runtime/src/scr_stream_from_web.c new file mode 100644 index 000000000..f88826335 --- /dev/null +++ b/packages/runtime/src/scr_stream_from_web.c @@ -0,0 +1,247 @@ +/* Readable.fromWeb (Node's newStreamReadableFromReadableStream in + * internal/webstreams/adapters.js): a Readable over a web ReadableStream's + * reader. read() queues one read request (scr_fetch_reader_read_into); a + * chunk is pushed in a microtask, the end pushes null and an error + * destroys the Readable. destroy() cancels the reader unless it has + * closed, then completes. reader.closed's settlement marks it closed (a + * rejection also destroys the Readable). Linked only by programs that + * use both streams and the native web streams. */ +#include "scr_runtime.h" +#include "scr_stream.h" + +#include +#include + +typedef struct SfwCtx SfwCtx; +struct SfwCtx { + size_t rc; + ScrStream *readable; /* +1 until teardown */ + ScrDyn *reader; /* +1 */ + ScrDyn *pending; /* +1: pendingChunk, or NULL */ + bool closed; + SfwCtx *live_prev, *live_next; +}; + +static SCR_TL SfwCtx *sfw_live = NULL; + +static SfwCtx *sfw_ref(SfwCtx *c) { + c->rc++; + return c; +} + +static void sfw_unref(SfwCtx *c) { + if (--c->rc > 0) return; + if (c->readable) scr_stream_release(c->readable); + if (c->reader) scr_dyn_release(c->reader); + if (c->pending) scr_dyn_release(c->pending); + free(c); +} + +static void *sfw_retain_v(void *p) { return sfw_ref(p); } +static void sfw_release_v(void *p) { sfw_unref(p); } + +/* Drop the Readable (the cycle through its callbacks). */ +static void sfw_teardown(SfwCtx *c) { + if (c->live_prev) c->live_prev->live_next = c->live_next; + else if (sfw_live == c) sfw_live = c->live_next; + else return; /* already torn down */ + if (c->live_next) c->live_next->live_prev = c->live_prev; + c->live_prev = c->live_next = NULL; + ScrStream *r = c->readable; + c->readable = NULL; + if (r) scr_stream_release(r); + sfw_unref(c); /* the live list's reference */ +} + +static void sfw_atexit(void) { + while (sfw_live) sfw_teardown(sfw_live); +} + +static ScrClosure *sfw_closure(void *fn, SfwCtx *c) { + ScrClosure *cl = scr_closure_new(fn, 1); + cl->caps[0] = scr_box_new_obj(&sfw_retain_v, &sfw_release_v, NULL); + scr_box_set_ref(cl->caps[0], sfw_ref(c)); + return cl; +} + +static SfwCtx *sfw_of(ScrClosure *cl) { return scr_box_get_ref(cl->caps[0]); /* +1 */ } + +/* destroyer(readable, error) */ +static void sfw_destroyer(SfwCtx *c, ScrDyn *reason) { + ScrStream *r = c->readable; + if (!r || scr_stream_prop(r, "destroyed")) return; + ScrError *err = reason ? scr_error_from_dyn(reason) : NULL; + if (scr_exc_pending()) scr_exc_clear(); + scr_stream_release(scr_stream_destroy(r, err)); + if (err) scr_error_release(err); +} + +/* forwardChunk: readable.push(pendingChunk) */ +static void sfw_forward(ScrClosure *job) { + SfwCtx *c = sfw_of(job); + ScrDyn *chunk = c->pending; + c->pending = NULL; + if (chunk && c->readable) { + scr_stream_push_dyn(c->readable, chunk); + if (scr_exc_pending()) scr_exc_clear(); + } + if (chunk) scr_dyn_release(chunk); + sfw_unref(c); +} + +static void sfw_sink_chunk(void *ctx, ScrDyn *value) { + SfwCtx *c = ctx; + if (c->pending) scr_dyn_release(c->pending); + c->pending = scr_dyn_retain(value); + scr_queue_microtask(sfw_closure((void *)&sfw_forward, c)); + sfw_unref(c); /* the request's reference */ +} + +static void sfw_sink_close(void *ctx) { + SfwCtx *c = ctx; + if (c->readable) scr_stream_push_null(c->readable); + if (scr_exc_pending()) scr_exc_clear(); + sfw_unref(c); +} + +static void sfw_sink_error(void *ctx, ScrDyn *reason) { + SfwCtx *c = ctx; + sfw_destroyer(c, reason); + sfw_unref(c); +} + +static const ScrFetchReadSink sfw_sink = {&sfw_sink_chunk, &sfw_sink_close, &sfw_sink_error}; + +/* read(): readableStreamDefaultReaderRead(reader, readRequest) */ +static void sfw_read_inv(ScrClosure *cb, ScrStream *s, double size) { + (void)s; + (void)size; + SfwCtx *c = sfw_of(cb); + if (c->reader) { + if (!scr_fetch_reader_read_into(c->reader, &sfw_sink, sfw_ref(c))) { + sfw_unref(c); + if (scr_exc_pending()) scr_exc_clear(); + } + } + sfw_unref(c); +} + +/* destroy's done(): callback(error) */ +typedef struct { + SfwCtx *c; /* +1 */ + ScrStream *s; /* +1 */ + ScrError *err; /* +1 or NULL */ + ScrDyn *cancel; /* +1: reader.cancel(error)'s promise */ +} SfwDestroy; + +static void sfw_destroy_done(SfwCtx *c, ScrStream *s, ScrError *err) { + scr_stream_destroy_done(s, err ? scr_error_retain(err) : NULL); + sfw_teardown(c); +} + +static void sfw_cancel_entry(ScrFiber *fiber, void *arg) { + (void)fiber; + SfwDestroy d = *(SfwDestroy *)arg; + free(arg); + ScrDyn *v = scr_await_ref(d.cancel->v.promise); /* then(done, done) */ + if (scr_exc_pending()) scr_exc_clear(); + if (v) scr_dyn_release(v); + sfw_destroy_done(d.c, d.s, d.err); + if (d.err) scr_error_release(d.err); + scr_stream_release(d.s); + scr_dyn_release(d.cancel); + sfw_unref(d.c); +} + +static void sfw_destroy_inv(ScrClosure *cb, ScrStream *s, ScrError *err) { + SfwCtx *c = sfw_of(cb); + if (!c->closed && c->reader) { + /* _destroy(err, cb) gets null for a destroy without an error */ + ScrDyn *reason = err ? scr_dyn_from_error(err) : scr_dyn_new_null(); + ScrDyn *cancel = scr_dyn_invoke(c->reader, "cancel", &reason, 1, "reader.cancel"); + scr_dyn_release(reason); + if (cancel && cancel->kind == SCR_DYN_PROMISE) { + SfwDestroy *d = malloc(sizeof *d); + if (!d) abort(); + d->c = sfw_ref(c); + d->s = scr_stream_retain(s); + d->err = err ? scr_error_retain(err) : NULL; + d->cancel = cancel; + scr_promise_release(scr_async_spawn_after(cancel->v.promise, &sfw_cancel_entry, d)); + sfw_unref(c); + return; + } + if (cancel) scr_dyn_release(cancel); + if (scr_exc_pending()) scr_exc_clear(); + } + sfw_destroy_done(c, s, err); + sfw_unref(c); +} + +/* reader.closed.then(() => closed = true, (e) => { closed = true; destroy(readable, e) }) */ +typedef struct { + SfwCtx *c; /* +1 */ + ScrDyn *closed; /* +1, the promise */ +} SfwClosed; + +static void sfw_closed_entry(ScrFiber *fiber, void *arg) { + (void)fiber; + SfwClosed w = *(SfwClosed *)arg; + free(arg); + ScrDyn *v = scr_await_ref(w.closed->v.promise); + w.c->closed = true; + if (scr_exc_pending()) { + ScrCaught *caught = scr_exc_take(); + ScrDyn *reason = scr_caught_to_dyn(caught); + scr_caught_release(caught); + sfw_destroyer(w.c, reason); + if (reason) scr_dyn_release(reason); + } + if (v) scr_dyn_release(v); + scr_dyn_release(w.closed); + sfw_unref(w.c); +} + +/* web: the ReadableStream (borrowed); hwm < 0 = the mode's default; + * encoding NULL or a known name (borrowed). +1, or NULL with the error + * pending. */ +ScrStream *scr_stream_readable_from_web(ScrDyn *web, bool object_mode, double hwm, ScrStr *encoding) { + if (encoding && encoding->len == 0) encoding = NULL; + if (!web || web->kind != SCR_DYN_HANDLE || web->v.handle.tag != SCR_DYNH_WEB_STREAM) { + scr_dyn_arg_type_fail("readableStream", "an instance of ReadableStream", web); + return NULL; + } + ScrDyn *reader = scr_dyn_invoke(web, "getReader", NULL, 0, "readableStream.getReader"); + if (!reader) return NULL; + SfwCtx *c = calloc(1, sizeof *c); + if (!c) abort(); + c->rc = 1; + c->reader = reader; + ScrStream *r = scr_stream_new_readable(hwm < 0 ? (object_mode ? 16 : -1) : hwm, true, true, + sfw_closure((void *)&sfw_read_inv, c), &sfw_read_inv, + sfw_closure((void *)&sfw_destroy_inv, c), &sfw_destroy_inv); + if (object_mode) scr_stream_set_object_mode(r, true, false); + if (encoding) scr_stream_release(scr_stream_set_encoding(r, encoding)); + c->readable = scr_stream_retain(r); + static SCR_TL bool registered = false; + if (!registered) { + registered = true; + scr_atexit(sfw_atexit); + } + c->live_next = sfw_live; + if (sfw_live) sfw_live->live_prev = c; + sfw_live = c; /* the reference c was created with */ + ScrStr *key = scr_str_new("closed", 6); + ScrDyn *closed = scr_dyn_handle_key_get(reader, key); + scr_str_release(key); + if (closed && closed->kind == SCR_DYN_PROMISE) { + SfwClosed *w = malloc(sizeof *w); + if (!w) abort(); + w->c = sfw_ref(c); + w->closed = closed; + scr_promise_release(scr_async_spawn_after(closed->v.promise, &sfw_closed_entry, w)); + } else if (closed) { + scr_dyn_release(closed); + } + return r; +} diff --git a/packages/runtime/src/scr_stream_ops.c b/packages/runtime/src/scr_stream_ops.c index 8fd220168..a92b4a0e5 100644 --- a/packages/runtime/src/scr_stream_ops.c +++ b/packages/runtime/src/scr_stream_ops.c @@ -267,20 +267,15 @@ static bool op_aborted(const ScrDyn *signal) { return r; } -/* The callback argument: a function that does not read the options - * argument. `max` is how many parameters Node's call passes before the - * options. False with the error pending. */ +/* The callback argument: validateFunction. False with the error pending. + * (The options argument the callback may read is the operator's + * cb_opts.) */ static bool op_check_fn(const ScrDyn *fn, const char *name, uint32_t max) { + (void)max; if (!fn || !scr_dyn_is_callable(fn)) { scr_dyn_arg_type_fail(name, "of type function", fn ? fn : scr_dyn_undefined()); return false; } - if (fn->kind == SCR_DYN_FUNC && fn->v.fn.arity > max) { - static const char m[] = - "stream operator callbacks that read the options argument ({ signal }) are not supported yet"; - scr_throw_error_msg(SCR_ERR_ERROR, m, sizeof m - 1); - return false; - } return true; } @@ -610,8 +605,9 @@ enum { MODE_MAP, MODE_FILTER, MODE_FOREACH, MODE_EVERY }; typedef struct { OpGen g; OpSrc src; - ScrDyn *fn; /* +1 */ - ScrDyn *signal; /* +1 or NULL */ + ScrDyn *fn; /* +1 */ + ScrDyn *signal; /* +1 or NULL */ + ScrDyn *cb_opts; /* +1: the { signal } fn takes as its second argument, or NULL */ int mode; double concurrency, hwm; ScrDyn **queue; /* +1 entries: dyn promises or kEof */ @@ -628,6 +624,7 @@ static void op_map_body_drop(OpGen *g) { src_release(&m->src); scr_dyn_release(m->fn); if (m->signal) scr_dyn_release(m->signal); + if (m->cb_opts) scr_dyn_release(m->cb_opts); for (size_t i = m->qh; i < m->qn; i++) scr_dyn_release(m->queue[i]); free(m->queue); if (m->next_p) scr_promise_release(m->next_p); @@ -734,9 +731,11 @@ static void op_wrap_k(void *self, ScrDyn *v) { } /* Calls fn(chunk): +1 result, or NULL with the exception pending. */ -static ScrDyn *op_call(ScrDyn *fn, ScrDyn *chunk) { - ScrDyn *args[1] = {chunk}; - return scr_dyn_call(fn, args, 1, "fn"); +/* Calls fn(chunk, signalOpt) — the second argument when the callback + * declares it. +1 result, or NULL with the exception pending. */ +static ScrDyn *op_call(OpMap *m, ScrDyn *chunk) { + ScrDyn *args[2] = {chunk, m->cb_opts}; + return scr_dyn_call(m->fn, args, m->cb_opts ? 2 : 1, "fn"); } /* An async wrapper body: `await fn(chunk)` then its transform. The @@ -744,7 +743,7 @@ static ScrDyn *op_call(ScrDyn *fn, ScrDyn *chunk) { static ScrPromise *op_wrap_call(OpMap *m, ScrDyn *chunk, int mode, int stage) { OpWrap *w = op_wrap_new(mode, chunk, stage); ScrPromise *result = scr_promise_retain(w->p); - ScrDyn *r = op_call(m->fn, chunk); + ScrDyn *r = op_call(m, chunk); if (!r) { scr_promise_reject_pending(w->p); } else { @@ -772,7 +771,7 @@ static ScrPromise *op_every_call(OpMap *m, ScrDyn *chunk) { static ScrDyn *map_item(OpMap *m, ScrDyn *chunk) { ScrPromise *p; if (m->mode == MODE_MAP) { - ScrDyn *r = op_call(m->fn, chunk); + ScrDyn *r = op_call(m, chunk); if (!r) { ScrCaught *c = scr_exc_take(); ScrDyn *d = op_rejected_dyn(c); @@ -981,7 +980,7 @@ static void map_thr(OpGen *g, ScrCaught *c) { } static OpMap *map_new(ScrStream *s, ScrDyn *fn, int mode, double concurrency, double hwm, - ScrDyn *signal) { + ScrDyn *signal, ScrDyn *cb_opts) { OpMap *m = op_alloc(sizeof *m); gen_init(&m->g, &op_map_body_drop); m->g.resume = &map_resume; @@ -990,6 +989,7 @@ static OpMap *map_new(ScrStream *s, ScrDyn *fn, int mode, double concurrency, do m->src.s = scr_stream_retain(s); m->fn = scr_dyn_retain(fn); m->signal = signal ? scr_dyn_retain(signal) : NULL; + m->cb_opts = cb_opts && cb_opts->kind != SCR_DYN_UNDEF ? scr_dyn_retain(cb_opts) : NULL; m->mode = mode; m->concurrency = concurrency; m->hwm = hwm; @@ -1137,27 +1137,137 @@ static void slice_thr(OpGen *g, ScrCaught *c) { * goes through the async-from-sync iterator: each step resolves after the * element's PromiseResolve job, and yield* awaits that result before * yielding without a further await. */ +/* yield* over the delegate: an array or string (its elements, through + * the async-from-sync iterator), another iterable's iterator (FLAT_SYNC + * through the async-from-sync iterator, FLAT_ASYNC awaiting next()), or a + * native Readable (FLAT_STREAM, its async iterator). */ +enum { FLAT_NONE, FLAT_SYNC, FLAT_ASYNC, FLAT_STREAM, FLAT_GEN_SYNC, FLAT_GEN_ASYNC }; + +typedef ScrDyn *(*FromSyncFn)(ScrClosure *, ScrGen *); +typedef ScrDyn *(*FromSyncThrowFn)(ScrClosure *, ScrGen *, ScrDyn *); +typedef ScrPromise *(*FromAsyncFn)(ScrClosure *, ScrGen *); +typedef ScrPromise *(*FromAsyncThrowFn)(ScrClosure *, ScrGen *, ScrDyn *); +typedef ScrDyn *(*FromReadFn)(ScrClosure *, void *); +static void op_await_gen(ScrPromise *p, ScrClosure *read, OpCont k, void *self); + typedef struct { OpGen g; OpSrc values; /* the map generator */ ScrDyn *inner; /* +1: the array or string being delegated to, or NULL */ size_t idx; /* element index, or the string's byte offset */ ScrPromise *step; /* the async-from-sync iterator result being awaited */ + ScrDyn *iter; /* +1: the delegate's iterator (FLAT_SYNC / FLAT_ASYNC), or NULL */ + ScrStream *stream; /* +1: the delegate stream (FLAT_STREAM), or NULL */ + ScrGen *gen; /* +1: a compiled generator delegate (FLAT_GEN_*), or NULL */ + /* The compiler's helpers over the callback's generator type (+1 each, + * or NULL): next/return/throw, and read for an async generator. */ + ScrClosure *gnext, *gread, *gret, *gthr; + bool gen_async; + int iter_kind; bool started; } OpFlat; +/* A delegate's member (+1), read as the IR's keyed read does: a compiled + * value (a generator, its result record) through its typed ref, anything + * else through the iterator-result read. NULL with the error pending. */ +static ScrDyn *flat_get(const ScrDyn *v, const char *key) { + if (v->kind == SCR_DYN_TYPED_REF) { + ScrStr *k = scr_str_new(key, strlen(key)); + ScrDyn *r = scr_dyn_typed_ref_key_get(v, k); + scr_str_release(k); + return r; + } + return scr_dyn_iterator_result_get(v, key, strlen(key)); +} + +/* iterator[name](...args) with the iterator as this: +1, or NULL with the + * error pending. */ +static ScrDyn *flat_call(const ScrDyn *iter, const char *name, ScrDyn *const *args, size_t argc) { + ScrDyn *m = flat_get(iter, name); + if (!m) return NULL; + scr_dyn_this_push_dyn(iter); + ScrDyn *r = scr_dyn_call(m, args, argc, name); + scr_dyn_this_pop(); + scr_dyn_release(m); + return r; +} + +/* `const { value, done } = result`: +1 value with *done set, or NULL with + * the error pending. */ +static ScrDyn *flat_result(const ScrDyn *r, bool *done) { + if (r->kind == SCR_DYN_UNDEF || r->kind == SCR_DYN_NULL || r->kind == SCR_DYN_NUM || + r->kind == SCR_DYN_STR || r->kind == SCR_DYN_BOOL) { + ScrDyn *checked = scr_dyn_iterator_result((ScrDyn *)r); /* throws V8's TypeError */ + if (checked) scr_dyn_release(checked); + return NULL; + } + ScrDyn *d = flat_get(r, "done"); + if (!d) return NULL; + *done = scr_dyn_truthy(d); + scr_dyn_release(d); + return flat_get(r, "value"); +} + +static void flat_drop_delegate(OpFlat *f) { + if (f->inner) scr_dyn_release(f->inner); + if (f->iter) scr_dyn_release(f->iter); + if (f->stream) scr_stream_release(f->stream); + if (f->gen) scr_gen_release(f->gen); + f->inner = f->iter = NULL; + f->stream = NULL; + f->gen = NULL; + f->iter_kind = FLAT_NONE; +} + static void op_flat_body_drop(OpGen *g) { OpFlat *f = (OpFlat *)g; src_release(&f->values); - if (f->inner) scr_dyn_release(f->inner); + flat_drop_delegate(f); if (f->step) scr_promise_release(f->step); + if (f->gnext) scr_closure_release(f->gnext); + if (f->gread) scr_closure_release(f->gread); + if (f->gret) scr_closure_release(f->gret); + if (f->gthr) scr_closure_release(f->gthr); free(f); } static void flat_next_value(OpFlat *f); -/* The next element (+1), or NULL when the delegate is exhausted. */ -static ScrDyn *flat_take_element(OpFlat *f) { +/* The next element (+1), or NULL when the delegate is exhausted; for a + * sync iterator NULL with *failed set when next() threw (pending). */ +static ScrDyn *flat_take_element(OpFlat *f, bool *failed) { + *failed = false; + if (f->iter_kind == FLAT_GEN_SYNC) { + ScrDyn *v = ((FromSyncFn)f->gnext->fn)(f->gnext, scr_gen_retain(f->gen)); + if (!v) { + *failed = true; + return NULL; + } + if (v == op_k_done) { + scr_dyn_release(v); + return NULL; + } + return v; + } + if (f->iter_kind == FLAT_SYNC) { + ScrDyn *r = flat_call(f->iter, "next", NULL, 0); + if (!r) { + *failed = true; + return NULL; + } + bool done = false; + ScrDyn *v = flat_result(r, &done); + scr_dyn_release(r); + if (!v) { + *failed = true; + return NULL; + } + if (done) { + scr_dyn_release(v); + return NULL; + } + return v; + } ScrDyn *in = f->inner; if (in->kind == SCR_DYN_ARR) { if (f->idx >= in->v.arr.len) return NULL; @@ -1185,10 +1295,15 @@ static void flat_step_k(void *self, ScrDyn *v) { scr_promise_release(f->step); f->step = NULL; } - if (v == op_k_done || (v && v == op_k_eof)) { + if (!v) { + /* the delegate's next() threw: it leaves the for await */ + flat_drop_delegate(f); + gen_close_source(&f->g, &f->values, scr_exc_take()); + return; + } + if (v == op_k_done || v == op_k_eof) { scr_dyn_release(v); - scr_dyn_release(f->inner); - f->inner = NULL; + flat_drop_delegate(f); flat_next_value(f); return; } @@ -1196,6 +1311,33 @@ static void flat_step_k(void *self, ScrDyn *v) { scr_dyn_release(v); } +/* Await(innerResult) for an async delegate: done ends it, else its value + * is yielded as it is. */ +static void flat_iter_k(void *self, ScrDyn *v) { + OpFlat *f = self; + if (!v) { + flat_drop_delegate(f); + gen_close_source(&f->g, &f->values, scr_exc_take()); + return; + } + bool done = false; + ScrDyn *value = flat_result(v, &done); + scr_dyn_release(v); + if (!value) { + flat_drop_delegate(f); + gen_close_source(&f->g, &f->values, scr_exc_take()); + return; + } + if (done) { + scr_dyn_release(value); + flat_drop_delegate(f); + flat_next_value(f); + return; + } + gen_yield_now(&f->g, value); + scr_dyn_release(value); +} + /* The async-from-sync continuation: PromiseResolve(value) then the result * promise settles; yield* awaits that promise. */ typedef struct { @@ -1220,8 +1362,56 @@ static void op_flat_wrap_k(void *self, ScrDyn *v) { scr_dyn_release(v); } +/* The awaited resume of an async compiled generator delegate, read to its + * value or the done marker: done ends it, else the value is yielded. */ +static void flat_gen_k(void *self, ScrDyn *v) { + OpFlat *f = self; + if (!v) { + flat_drop_delegate(f); + gen_close_source(&f->g, &f->values, scr_exc_take()); + return; + } + if (v == op_k_done) { + scr_dyn_release(v); + flat_drop_delegate(f); + flat_next_value(f); + return; + } + gen_yield_now(&f->g, v); + scr_dyn_release(v); +} + static void flat_delegate_step(OpFlat *f) { - ScrDyn *e = flat_take_element(f); + if (f->iter_kind == FLAT_GEN_ASYNC) { + ScrPromise *p = ((FromAsyncFn)f->gnext->fn)(f->gnext, scr_gen_retain(f->gen)); + op_await_gen(p, f->gread, &flat_gen_k, f); + scr_promise_release(p); + return; + } + if (f->iter_kind == FLAT_ASYNC) { + ScrDyn *r = flat_call(f->iter, "next", NULL, 0); + if (!r) { + flat_drop_delegate(f); + gen_close_source(&f->g, &f->values, scr_exc_take()); + return; + } + op_await_value(r, &flat_iter_k, f); + scr_dyn_release(r); + return; + } + if (f->iter_kind == FLAT_STREAM) { + ScrPromise *p = scr_stream_next_result_dyn(f->stream); + op_await(p, &flat_iter_k, f); + scr_promise_release(p); + return; + } + bool failed = false; + ScrDyn *e = flat_take_element(f, &failed); + if (failed) { + flat_drop_delegate(f); + gen_close_source(&f->g, &f->values, scr_exc_take()); + return; + } OpFlatWrap *w = op_alloc(sizeof *w); w->base.rc = 1; w->base.drop = &op_flat_wrap_drop; @@ -1247,15 +1437,63 @@ static void flat_val_k(void *self, ScrDyn *v) { gen_complete(&f->g, NULL); return; } - if (v->kind != SCR_DYN_ARR && v->kind != SCR_DYN_STR) { - scr_dyn_release(v); - ScrCaught *c = op_take_error( - op_unsupported("flatMap callback results other than arrays or strings")); - gen_close_source(&f->g, &f->values, c); + if (v->kind == SCR_DYN_ARR || v->kind == SCR_DYN_STR) { + f->inner = v; + f->idx = 0; + flat_delegate_step(f); return; } - f->inner = v; - f->idx = 0; + ScrStream *stream = scr_stream_native_readable_of_dyn(v); + if (stream) { + scr_dyn_release(v); + f->stream = stream; + f->iter_kind = FLAT_STREAM; + flat_delegate_step(f); + return; + } + /* a compiled generator of the callback's declared type: stepped through + * the compiler's helpers */ + if (f->gnext && v->kind == SCR_DYN_TYPED_REF && v->v.typed_ref.type_key && + !strncmp(v->v.typed_ref.type_key, f->gen_async ? "async-generator<" : "generator<", + f->gen_async ? 16 : 10)) { + f->gen = scr_dyn_typed_ref_unbox(v); + scr_dyn_release(v); + f->iter_kind = f->gen_async ? FLAT_GEN_ASYNC : FLAT_GEN_SYNC; + flat_delegate_step(f); + return; + } + /* yield* val: GetIterator(val, async); a compiled generator is its own + * iterator */ + int kind = 0; + ScrDyn *iter = NULL; + if (v->kind == SCR_DYN_TYPED_REF && v->v.typed_ref.type_key && + (!strncmp(v->v.typed_ref.type_key, "async-generator<", 16) || + !strncmp(v->v.typed_ref.type_key, "generator<", 10))) { + kind = v->v.typed_ref.type_key[0] == 'a' ? 2 : 1; + iter = scr_dyn_retain(v); + } else { + iter = scr_dyn_open_iterator(v, &kind, true); + } + if (!iter && kind == 0 && !scr_exc_pending()) { + /* V8's GetIterator over a value without either method */ + if (v->kind == SCR_DYN_NULL || v->kind == SCR_DYN_UNDEF) { + char m[96]; + int n = snprintf(m, sizeof m, + "Cannot read properties of %s (reading 'Symbol(Symbol.asyncIterator)')", + v->kind == SCR_DYN_NULL ? "null" : "undefined"); + scr_throw_error_msg(SCR_ERR_TYPE, m, (size_t)n); + } else { + static const char m[] = "undefined is not a function"; + scr_throw_error_msg(SCR_ERR_TYPE, m, sizeof m - 1); + } + } + scr_dyn_release(v); + if (!iter) { + gen_close_source(&f->g, &f->values, scr_exc_take()); + return; + } + f->iter = iter; + f->iter_kind = kind == 2 ? FLAT_ASYNC : FLAT_SYNC; flat_delegate_step(f); } @@ -1300,19 +1538,85 @@ static void op_flat_ret_k(void *self, ScrDyn *v) { op_await_value(scr_dyn_undefined(), &op_flat_ret_k, r); return; } - if (r->f->inner) { - scr_dyn_release(r->f->inner); - r->f->inner = NULL; - } + flat_drop_delegate(r->f); gen_close_source(&r->f->g, &r->f->values, NULL); } +/* Whether the delegate's iterator has a callable `name` method. */ +static bool flat_iter_has(OpFlat *f, const char *name) { + if (!f->iter) return false; + ScrDyn *m = flat_get(f->iter, name); + if (!m) { + scr_exc_clear(); + return false; + } + bool callable = scr_dyn_is_callable(m); + scr_dyn_release(m); + return callable; +} + static void flat_ret(OpGen *g, ScrDyn *v) { (void)v; + OpFlat *f = (OpFlat *)g; OpFlatRet *r = op_alloc(sizeof *r); r->base.rc = 1; r->base.drop = &op_flat_ret_drop; - r->f = op_ref((OpFlat *)g); + r->f = op_ref(f); + if (f->iter_kind == FLAT_GEN_ASYNC) { + /* the generator's return(): awaited, then the value */ + ScrPromise *p = ((FromAsyncFn)f->gret->fn)(f->gret, scr_gen_retain(f->gen)); + op_await_gen(p, f->gread, &op_flat_ret_k, r); + scr_promise_release(p); + op_unref(r); + return; + } + if (f->iter_kind == FLAT_GEN_SYNC) { + ScrDyn *res = ((FromSyncFn)f->gret->fn)(f->gret, scr_gen_retain(f->gen)); + if (!res) { + flat_drop_delegate(f); + gen_close_source(g, &f->values, scr_exc_take()); + op_unref(r); + return; + } + scr_dyn_release(res); + op_await_value(scr_dyn_undefined(), &op_flat_ret_k, r); + op_unref(r); + return; + } + if (f->iter_kind == FLAT_ASYNC) { + /* yield*'s return: Await(iterator.return(value)), then the value */ + if (flat_iter_has(f, "return")) { + ScrDyn *arg = scr_dyn_undefined(); + ScrDyn *res = flat_call(f->iter, "return", &arg, 1); + if (!res) { + flat_drop_delegate(f); + gen_close_source(g, &f->values, scr_exc_take()); + op_unref(r); + return; + } + op_await_value(res, &op_flat_ret_k, r); + scr_dyn_release(res); + } else { + r->stage = 1; + op_await_value(scr_dyn_undefined(), &op_flat_ret_k, r); + } + op_unref(r); + return; + } + if (f->iter_kind == FLAT_SYNC && flat_iter_has(f, "return")) { + /* the async-from-sync iterator's return() calls the sync one */ + ScrDyn *arg = scr_dyn_undefined(); + ScrDyn *res = flat_call(f->iter, "return", &arg, 1); + if (!res) { + flat_drop_delegate(f); + gen_close_source(g, &f->values, scr_exc_take()); + op_unref(r); + return; + } + scr_dyn_release(res); + } else if (f->iter_kind == FLAT_STREAM) { + scr_stream_release(scr_stream_iterator_close(f->stream)); + } op_await_value(scr_dyn_undefined(), &op_flat_ret_k, r); op_unref(r); } @@ -1321,12 +1625,72 @@ static void flat_ret(OpGen *g, ScrDyn *v) { * yield* rejects with Node's TypeError, which leaves the for await and * closes map's generator. */ static void flat_thr(OpGen *g, ScrCaught *c) { - (void)c; OpFlat *f = (OpFlat *)g; - if (f->inner) { - scr_dyn_release(f->inner); - f->inner = NULL; + if (f->iter_kind == FLAT_GEN_ASYNC || f->iter_kind == FLAT_GEN_SYNC) { + /* the generator's throw(error) answers the next step */ + ScrDyn *err = scr_caught_to_dyn(c); + if (f->iter_kind == FLAT_GEN_ASYNC) { + ScrPromise *p = ((FromAsyncThrowFn)f->gthr->fn)(f->gthr, scr_gen_retain(f->gen), err); + op_await_gen(p, f->gread, &flat_gen_k, f); + scr_promise_release(p); + return; + } + ScrDyn *v = ((FromSyncThrowFn)f->gthr->fn)(f->gthr, scr_gen_retain(f->gen), err); + if (!v) { + flat_drop_delegate(f); + gen_close_source(g, &f->values, scr_exc_take()); + return; + } + if (v == op_k_done) { + scr_dyn_release(v); + flat_drop_delegate(f); + flat_next_value(f); + return; + } + op_await_value(v, &flat_step_k, f); + scr_dyn_release(v); + return; } + if ((f->iter_kind == FLAT_ASYNC || f->iter_kind == FLAT_SYNC) && flat_iter_has(f, "throw")) { + /* yield*'s throw: the delegate's throw(error) answers the next step */ + ScrDyn *err = scr_caught_to_dyn(c); + ScrDyn *res = flat_call(f->iter, "throw", &err, 1); + scr_dyn_release(err); + if (!res) { + flat_drop_delegate(f); + gen_close_source(g, &f->values, scr_exc_take()); + return; + } + if (f->iter_kind == FLAT_ASYNC) { + op_await_value(res, &flat_iter_k, f); + scr_dyn_release(res); + return; + } + ScrPromise *p = op_resolved(res); + scr_dyn_release(res); + op_await(p, &flat_iter_k, f); + scr_promise_release(p); + return; + } + if (f->iter_kind == FLAT_STREAM) { + /* the stream's iterator rethrows after destroying the stream */ + ScrDyn *err = scr_caught_to_dyn(c); + ScrError *e = err ? scr_error_from_dyn(err) : NULL; + if (scr_exc_pending()) scr_exc_clear(); + if (err) scr_dyn_release(err); + scr_stream_release(scr_stream_destroy(f->stream, e)); + if (e) scr_error_release(e); + flat_drop_delegate(f); + gen_close_source(g, &f->values, scr_caught_retain(c)); + return; + } + if (f->iter_kind != FLAT_NONE && flat_iter_has(f, "return")) { + /* no throw method: the iterator closes first */ + ScrDyn *res = flat_call(f->iter, "return", NULL, 0); + if (res) scr_dyn_release(res); + else scr_exc_clear(); + } + flat_drop_delegate(f); ScrError *e = op_coded_error(SCR_ERR_TYPE, NULL, "The iterator does not provide a 'throw' method."); gen_close_source(g, &f->values, op_take_error(e)); } @@ -1370,12 +1734,6 @@ static void op_from_drop(OpObj *o) { /* The helper ABIs: the closure first, then the declared parameters, which * the callee owns. */ -typedef ScrDyn *(*FromSyncFn)(ScrClosure *, ScrGen *); -typedef ScrDyn *(*FromSyncThrowFn)(ScrClosure *, ScrGen *, ScrDyn *); -typedef ScrPromise *(*FromAsyncFn)(ScrClosure *, ScrGen *); -typedef ScrPromise *(*FromAsyncThrowFn)(ScrClosure *, ScrGen *, ScrDyn *); -typedef ScrDyn *(*FromReadFn)(ScrClosure *, void *); - /* `await p` over an async generator's resume promise: the IteratorResult * record goes through `read` (the value, or the done marker) before k. * Awaiting the typed promise itself keeps from.js's single await. */ @@ -2029,6 +2387,466 @@ ScrStream *scr_stream_from_async_iter(ScrDyn *iter, double flags, double hwm) { return scr_stream_from_dyn_iter(scr_dyn_retain(iter), true, flags, hwm); } +/* ── Duplex.from (duplexify.js) ───────────────────────────────────── + * Duplex.from(asyncGeneratorFunction): fromAsyncGen hands the function a + * source generator that the writes and the final feed through a deferred + * promise, and from(Duplexify, value, { objectMode: true, write, final, + * destroy }) reads what the function yields. The source generator is + * compiled (the frontend's Duplex.from source helper): + * + * while (true) { const v = await next(); ack(); if (v is done) return; yield v; } + * + * next() answers the current deferred, a write or the final settles it + * (the chunk, or the done marker), and ack() is fromAsyncGen's + * `process.nextTick(cb)` followed by a fresh deferred. */ + +enum { CHAN_NONE, CHAN_WRITE, CHAN_FINAL, CHAN_ABORT, CHAN_FINAL_ASYNC }; + +typedef struct OpChan OpChan; +struct OpChan { + OpObj base; + ScrStream *d; /* +1, dropped at destroy (or exit) */ + ScrPromise *promise; /* +1: the deferred next() answers, NULL once taken */ + ScrPromise *resolve; /* +1: the deferred a write or the final settles, NULL once settled */ + int settled; /* CHAN_*: what settled the deferred, for ack() */ + bool aborted; /* destroy ran (fromAsyncGen's ac.abort()) */ + ScrClosure *abort; /* +1: the body's AbortController's abort(), or NULL */ + /* An async function body: its promise's then() (duplexify.js) settles + * `promise` there; the final waits for it. */ + bool finalized; /* final() has started */ + bool body_settled; /* the then() callbacks ran */ + bool final_waiting; /* the final's callback awaits the settlement */ + ScrError *body_err; /* +1: what awaiting the then() result throws, or NULL */ + OpChan *live_prev, *live_next; +}; + +static SCR_TL OpChan *op_chan_live = NULL; + +static void op_chan_drop(OpObj *o) { + OpChan *c = (OpChan *)o; + if (c->body_err) scr_error_release(c->body_err); + if (c->abort) scr_closure_release(c->abort); + if (c->d) scr_stream_release(c->d); + if (c->promise) scr_promise_release(c->promise); + if (c->resolve) scr_promise_release(c->resolve); + free(c); +} + +/* createDeferredPromise() */ +static void op_chan_defer(OpChan *c) { + ScrPromise *p = scr_promise_new(); + if (c->promise) scr_promise_release(c->promise); + if (c->resolve) scr_promise_release(c->resolve); + c->promise = p; + c->resolve = scr_promise_retain(p); +} + +/* Unlinks the channel from the live list and drops its stream and its + * deferred (a source still awaiting it stays pending, as in Node). */ +static void op_chan_retire(OpChan *c) { + if (c->live_prev) c->live_prev->live_next = c->live_next; + else if (op_chan_live == c) op_chan_live = c->live_next; + else return; /* already retired */ + if (c->live_next) c->live_next->live_prev = c->live_prev; + c->live_prev = c->live_next = NULL; + ScrStream *d = c->d; + c->d = NULL; + if (c->abort) scr_closure_release(c->abort); + c->abort = NULL; + if (c->promise) scr_promise_release(c->promise); + if (c->resolve) scr_promise_release(c->resolve); + c->promise = c->resolve = NULL; + if (d) scr_stream_release(d); + op_unref(c); /* the live list's reference */ +} + +static void op_chan_atexit(void) { + while (op_chan_live) op_chan_retire(op_chan_live); +} + +static ScrClosure *op_chan_closure(void *fn, OpChan *c) { + ScrClosure *cl = scr_closure_new(fn, 1); + cl->caps[0] = scr_box_new_obj(&op_ref, &op_unref, NULL); + scr_box_set_ref(cl->caps[0], op_ref(c)); + return cl; +} + +static OpChan *op_chan_of(ScrClosure *cb) { return scr_box_get_ref(cb->caps[0]); /* +1 */ } + +/* process.nextTick(cb): the write's or the final's completion. */ +static void op_chan_done_tick(ScrClosure *job) { + ScrStream *d = scr_box_get_ref(job->caps[0]); + bool final = scr_box_get_bool(job->caps[1]); + if (final) scr_stream_final_done(d, NULL); + else scr_stream_write_done(d, NULL); + scr_stream_release(d); +} + +/* The source's `await _promise`: the current deferred (+1). */ +static ScrPromise *op_chan_next_inv(ScrClosure *cb) { + OpChan *c = op_chan_of(cb); + ScrPromise *p = c->promise; + c->promise = NULL; + op_unref(c); + return p ? p : scr_promise_new(); +} + +static ScrError *op_chan_abort_error(void) { + ScrStr *message = scr_str_new("The operation was aborted", 25); + ScrError *e = scr_error_new(SCR_ERR_ERROR, message); + scr_str_release(message); + scr_str_release(e->name); + e->name = scr_str_new("AbortError", 10); + scr_error_set_code(e, "ABORT_ERR"); + e->name_present = true; + e->name_enumerable = true; + return e; +} + +/* After the source's await: process.nextTick(cb), then (unless done) the + * aborted check and a fresh deferred. */ +static void op_chan_final_async_wait(OpChan *c); + +/* The final's completion after `await promise`: process.nextTick(cb, err) */ +static void op_chan_final_async_done(ScrClosure *job) { + OpChan *c = op_chan_of(job); + if (c->d) { + ScrError *err = c->body_err ? scr_error_retain(c->body_err) : NULL; + ScrStream *d = scr_stream_retain(c->d); + scr_stream_final_done(d, err); + scr_stream_release(d); + } + op_unref(c); +} + +/* `await promise` resumed: the completion follows on the next tick */ +static void op_chan_final_async_resume(ScrClosure *job) { + OpChan *c = op_chan_of(job); + scr_next_tick(op_chan_closure((void *)&op_chan_final_async_done, c)); + op_unref(c); +} + +/* The final's async callback (run by the source's nextTick): it awaits + * the then() result, which may still be pending. */ +static void op_chan_final_async_tick(ScrClosure *job) { + OpChan *c = op_chan_of(job); + op_chan_final_async_wait(c); + op_unref(c); +} + +static void op_chan_final_async_wait(OpChan *c) { + if (c->body_settled) scr_queue_microtask(op_chan_closure((void *)&op_chan_final_async_resume, c)); + else c->final_waiting = true; +} + +static void op_chan_ack_inv(ScrClosure *cb) { + OpChan *c = op_chan_of(cb); + int settled = c->settled; + c->settled = CHAN_NONE; + if (settled == CHAN_FINAL_ASYNC) { + scr_next_tick(op_chan_closure((void *)&op_chan_final_async_tick, c)); + op_unref(c); + return; + } + if (c->d && (settled == CHAN_WRITE || settled == CHAN_FINAL)) { + ScrClosure *tick = scr_closure_new((void *)&op_chan_done_tick, 2); + tick->caps[0] = scr_box_new_obj(&scr_stream_retain_v, &scr_stream_release_v, &scr_stream_trace); + scr_box_set_ref(tick->caps[0], scr_stream_retain(c->d)); + tick->caps[1] = scr_box_new(SCR_BOX_BOOL); + scr_box_set_bool(tick->caps[1], settled == CHAN_FINAL); + scr_next_tick(tick); + } + if (settled != CHAN_FINAL && settled != CHAN_ABORT) { + if (c->aborted) { + ScrError *e = op_chan_abort_error(); + scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg()); + } else if (c->d) { + op_chan_defer(c); + } + } + op_unref(c); +} + +/* `_resolve(value)`: the write's chunk or the final's done marker. */ +static void op_chan_settle(OpChan *c, ScrDyn *v /*moves*/, int kind) { + ScrPromise *p = c->resolve; + c->resolve = NULL; + if (!p) { + scr_dyn_release(v); + return; + } + c->settled = kind; + scr_promise_fulfill_ref(p, v, scr_dyn_retain_v, scr_dyn_release_v, NULL); + scr_promise_release(p); +} + +/* Duplexify's write(chunk, encoding, cb) */ +static void op_chan_write_inv(ScrClosure *cb, ScrStream *s, ScrBytes *chunk) { + OpChan *c = op_chan_of(cb); + op_chan_settle(c, scr_stream_chunk_value(s, chunk), CHAN_WRITE); + op_unref(c); +} + +/* Duplexify's final(cb) */ +static void op_chan_final_inv(ScrClosure *cb, ScrStream *s) { + (void)s; + OpChan *c = op_chan_of(cb); + op_chan_settle(c, scr_stream_from_done(), CHAN_FINAL); + op_unref(c); +} + +/* fromAsyncGen's ac.abort(err): the body's signal fires its abort + * listeners once (a body that declares the options argument); without an + * error the reason is null (destroy passes `err || null`). */ +static void op_chan_abort(OpChan *c, ScrError *err) { + c->aborted = true; + ScrClosure *abort = c->abort; + if (!abort) return; + c->abort = NULL; + ScrDyn *reason = err ? scr_dyn_from_error(err) : NULL; + if (!reason) reason = scr_dyn_new_null(); + ((void (*)(ScrClosure *, ScrDyn *))abort->fn)(abort, reason); /* reason moves in */ + if (scr_exc_pending()) scr_exc_clear(); + scr_closure_release(abort); +} + +/* destroy: fromAsyncGen's (ac.abort(); a source waiting for a write or + * the final is unblocked with the done marker and a no-op callback; + * cb(err)), then from()'s close of the function's iterator. caps: the + * channel and the from() destroy closure. */ +static void op_dup_destroy_inv(ScrClosure *cb, ScrStream *s, ScrError *err) { + OpChan *c = scr_box_get_ref(cb->caps[0]); + ScrClosure *from = scr_box_get_ref(cb->caps[1]); + op_chan_abort(c, err); + if (c->resolve) op_chan_settle(c, scr_stream_from_done(), CHAN_ABORT); + op_chan_retire(c); + op_unref(c); + op_from_destroy_inv(from, s, err); + scr_closure_release(from); +} + +typedef ScrGen *(*OpSourceFn)(ScrClosure *, ScrClosure *, ScrClosure *); +typedef ScrGen *(*OpStageFn)(ScrClosure *, ScrGen *); +typedef ScrGen *(*OpStageOptsFn)(ScrClosure *, ScrGen *, ScrDyn *); + +/* A new channel; with a signal (borrowed), the body's AbortController's + * abort() (borrowed) is kept for destroy and *opts is { signal } (+1). */ +static OpChan *op_chan_new(ScrClosure *abort, ScrDyn *signal, ScrDyn **opts) { + op_sentinels_init(); + static SCR_TL bool registered = false; + if (!registered) { + registered = true; + scr_atexit(op_chan_atexit); + } + OpChan *c = op_alloc(sizeof *c); + c->base.rc = 1; + c->base.drop = &op_chan_drop; + *opts = NULL; + if (signal) { + c->abort = scr_closure_retain(abort); + *opts = scr_dyn_new_obj(); + scr_dyn_obj_set(*opts, "signal", 6, scr_dyn_retain(signal)); + } + op_chan_defer(c); + return c; +} + +/* Duplex.from(fn) for an async generator function: the compiled stage + * (borrowed) takes the source generator and answers its own; source + * (borrowed) makes the source generator from next() and ack(); the + * compiler's helpers over the stage's generator are borrowed. With a + * signal, the stage also takes { signal }. +1, or NULL with the + * function's throw pending. */ +static ScrStream *op_duplex_from_gen(ScrClosure *stage, ScrClosure *source, ScrClosure *next, + ScrClosure *read, ScrClosure *ret, ScrClosure *thr, + ScrClosure *abort, ScrDyn *signal) { + ScrDyn *opts; + OpChan *c = op_chan_new(abort, signal, &opts); + ScrGen *src = ((OpSourceFn)source->fn)(source, op_chan_closure((void *)&op_chan_next_inv, c), + op_chan_closure((void *)&op_chan_ack_inv, c)); + ScrGen *g = NULL; /* src and opts move in */ + if (scr_exc_pending()) { + if (opts) scr_dyn_release(opts); + } else { + g = opts ? ((OpStageOptsFn)stage->fn)(stage, src, opts) : ((OpStageFn)stage->fn)(stage, src); + } + if (scr_exc_pending()) { + if (g) scr_gen_release(g); + op_unref(c); + return NULL; + } + OpFrom *f = op_alloc(sizeof *f); + f->base.rc = 1; + f->base.drop = &op_from_drop; + f->kind = FROM_ASYNC; + f->gen = g; + f->next = scr_closure_retain(next); + f->read = scr_closure_retain(read); + f->ret = scr_closure_retain(ret); + f->thr = scr_closure_retain(thr); + ScrClosure *destroy = scr_closure_new((void *)&op_dup_destroy_inv, 2); + destroy->caps[0] = scr_box_new_obj(&op_ref, &op_unref, NULL); + scr_box_set_ref(destroy->caps[0], op_ref(c)); + destroy->caps[1] = scr_box_new_obj(&scr_closure_retain_v, &scr_closure_release_v, &scr_closure_trace_v); + scr_box_set_ref(destroy->caps[1], op_from_closure((void *)&op_from_destroy_inv, f)); + /* Duplexify({ objectMode: true, highWaterMark: 1, write, final, destroy }) */ + ScrStream *d = scr_stream_new_duplex(1, 1, true, true, true, true, true, + op_from_closure((void *)&op_from_read_inv, f), &op_from_read_inv, + op_chan_closure((void *)&op_chan_write_inv, c), &op_chan_write_inv, + op_chan_closure((void *)&op_chan_final_inv, c), &op_chan_final_inv, + destroy, &op_dup_destroy_inv); + scr_stream_set_object_mode(d, true, true); + d->cls = "Duplexify"; + f->out = d; + op_unref(f); + c->d = scr_stream_retain(d); + c->live_next = op_chan_live; + if (op_chan_live) op_chan_live->live_prev = c; + op_chan_live = c; /* the reference c was created with */ + return d; +} + +ScrStream *scr_stream_duplex_from_gen(ScrClosure *stage, ScrClosure *source, ScrClosure *next, + ScrClosure *read, ScrClosure *ret, ScrClosure *thr) { + return op_duplex_from_gen(stage, source, next, read, ret, thr, NULL, NULL); +} + +ScrStream *scr_stream_duplex_from_gen_signal(ScrClosure *stage, ScrClosure *source, ScrClosure *next, + ScrClosure *read, ScrClosure *ret, ScrClosure *thr, + ScrClosure *abort, ScrDyn *signal) { + return op_duplex_from_gen(stage, source, next, read, ret, thr, abort, signal); +} + +/* ── Duplex.from(asyncFunction) ── duplexify.js: the function's promise + * must settle to a nullish value (else ERR_INVALID_RETURN_VALUE when the + * final awaits it), a return before the input was consumed destroys the + * Duplex, a rejection destroys it with the reason, and the Duplexify is + * writable only, its final waiting for the promise. */ + +typedef ScrPromise *(*OpAsyncStageFn)(ScrClosure *, ScrGen *); +typedef ScrPromise *(*OpAsyncStageOptsFn)(ScrClosure *, ScrGen *, ScrDyn *); +typedef ScrDyn *(*OpBoxFn)(ScrClosure *, ScrPromise *); + +typedef struct { + OpChan *c; /* +1 */ + ScrDyn *boxed; /* +1, the function's promise boxed */ +} OpAsyncBody; + +static void op_async_body_entry(ScrFiber *fiber, void *arg) { + (void)fiber; + OpAsyncBody b = *(OpAsyncBody *)arg; + free(arg); + OpChan *c = b.c; + ScrDyn *v = scr_await_ref(b.boxed->v.promise); /* NULL: undefined */ + ScrStream *d = c->d ? scr_stream_retain(c->d) : NULL; + if (!scr_exc_pending()) { + if (v && v->kind != SCR_DYN_NULL && v->kind != SCR_DYN_UNDEF) { + /* the then() callback throws: awaiting its result rejects */ + char detail[160]; + const char *got = scr_dyn_specific_type(v, detail, sizeof detail); + char msg[320]; + int n = snprintf(msg, sizeof msg, + "Expected nully to be returned from the \"body\" function but got %s.", got); + c->body_err = op_coded_error(SCR_ERR_TYPE, "ERR_INVALID_RETURN_VALUE", msg); + (void)n; + } else if (!c->finalized && d) { + /* returned without consuming the input: destroyer(d) */ + ScrError *e = op_chan_abort_error(); + scr_stream_release(scr_stream_destroy(d, e)); + scr_error_release(e); + } + if (v) scr_dyn_release(v); + } else { + ScrCaught *caught = scr_exc_take(); + ScrError *err = op_caught_error(caught); + scr_caught_release(caught); + if (d) scr_stream_release(scr_stream_destroy(d, err)); + scr_error_release(err); + } + if (scr_exc_pending()) scr_exc_clear(); + if (d) scr_stream_release(d); + c->body_settled = true; + if (c->final_waiting) { + c->final_waiting = false; + op_chan_final_async_wait(c); + } + op_unref(c); + scr_dyn_release(b.boxed); +} + +/* Duplexify's final(cb) for an async function body: finalized, then + * fromAsyncGen's final with the awaiting callback */ +static void op_chan_final_async_inv(ScrClosure *cb, ScrStream *s) { + (void)s; + OpChan *c = op_chan_of(cb); + c->finalized = true; + op_chan_settle(c, scr_stream_from_done(), CHAN_FINAL_ASYNC); + op_unref(c); +} + +/* fromAsyncGen's destroy alone (no from() iterator to close) */ +static void op_chan_destroy_inv(ScrClosure *cb, ScrStream *s, ScrError *err) { + OpChan *c = op_chan_of(cb); + op_chan_abort(c, err); + if (c->resolve) op_chan_settle(c, scr_stream_from_done(), CHAN_ABORT); + op_chan_retire(c); + op_unref(c); + scr_stream_destroy_done(s, err ? scr_error_retain(err) : NULL); +} + +/* Duplex.from(fn) for an async function: the compiled stage (borrowed) + * takes the source generator (and, with a signal, { signal }) and answers + * its promise; source and box (promise → dyn promise) borrowed. +1, or + * NULL with the function's throw pending. */ +static ScrStream *op_duplex_from_async_fn(ScrClosure *stage, ScrClosure *source, ScrClosure *box, + ScrClosure *abort, ScrDyn *signal) { + ScrDyn *opts; + OpChan *c = op_chan_new(abort, signal, &opts); + ScrGen *src = ((OpSourceFn)source->fn)(source, op_chan_closure((void *)&op_chan_next_inv, c), + op_chan_closure((void *)&op_chan_ack_inv, c)); + ScrPromise *p = NULL; /* src and opts move in */ + if (scr_exc_pending()) { + if (opts) scr_dyn_release(opts); + } else { + p = opts ? ((OpAsyncStageOptsFn)stage->fn)(stage, src, opts) + : ((OpAsyncStageFn)stage->fn)(stage, src); + } + if (scr_exc_pending()) { + if (p) scr_promise_release(p); + op_unref(c); + return NULL; + } + /* Duplexify({ objectMode: true, readable: false, write, final, destroy }) */ + ScrStream *d = scr_stream_new_duplex(16, 16, true, true, true, false, true, NULL, NULL, + op_chan_closure((void *)&op_chan_write_inv, c), &op_chan_write_inv, + op_chan_closure((void *)&op_chan_final_async_inv, c), + &op_chan_final_async_inv, + op_chan_closure((void *)&op_chan_destroy_inv, c), &op_chan_destroy_inv); + scr_stream_set_object_mode(d, true, true); + d->cls = "Duplexify"; + c->d = scr_stream_retain(d); + c->live_next = op_chan_live; + if (op_chan_live) op_chan_live->live_prev = c; + op_chan_live = c; /* the reference c was created with */ + /* value.then(onFulfilled, onRejected) */ + OpAsyncBody *b = malloc(sizeof *b); + if (!b) op_oom(); + b->c = op_ref(c); + b->boxed = ((OpBoxFn)box->fn)(box, p); /* p moves */ + scr_promise_release(scr_async_spawn_after(b->boxed->v.promise, &op_async_body_entry, b)); + return d; +} + +ScrStream *scr_stream_duplex_from_async_fn(ScrClosure *stage, ScrClosure *source, ScrClosure *box) { + return op_duplex_from_async_fn(stage, source, box, NULL, NULL); +} + +ScrStream *scr_stream_duplex_from_async_fn_signal(ScrClosure *stage, ScrClosure *source, + ScrClosure *box, ScrClosure *abort, + ScrDyn *signal) { + return op_duplex_from_async_fn(stage, source, box, abort, signal); +} + /* ── the promise operators ─────────────────────────────────────────── */ enum { PK_DYN, PK_F64, PK_BOOL, PK_STR, PK_VOID }; @@ -2041,6 +2859,8 @@ typedef struct { ScrDyn *fn; /* +1 or NULL */ ScrDyn *signal; /* +1 or NULL */ ScrDyn *acc; /* toArray's array / reduce's accumulator (+1) */ + ScrClosure *ctl; /* +1: reduce's ac.abort(), run when it ends, or NULL */ + ScrDyn *cb_opts; /* +1: the { signal } the reducer takes as its third argument, or NULL */ int op; int kind; /* the promise payload (PK_*) */ bool has_init, got_any, negate; @@ -2055,9 +2875,21 @@ static void op_fn_drop(OpObj *o) { if (a->fn) scr_dyn_release(a->fn); if (a->signal) scr_dyn_release(a->signal); if (a->acc) scr_dyn_release(a->acc); + if (a->ctl) scr_closure_release(a->ctl); + if (a->cb_opts) scr_dyn_release(a->cb_opts); free(a); } +/* reduce's `finally { ac.abort(); }`, before its promise settles. */ +static void op_fn_abort_ctl(OpFn *a) { + if (!a->ctl) return; + ScrClosure *ctl = a->ctl; + a->ctl = NULL; + ((void (*)(ScrClosure *))ctl->fn)(ctl); + if (scr_exc_pending()) scr_exc_clear(); + scr_closure_release(ctl); +} + static OpFn *op_fn_new(int op, int kind) { OpFn *a = op_alloc(sizeof *a); a->base.rc = 1; @@ -2071,6 +2903,7 @@ static OpFn *op_fn_new(int op, int kind) { /* Settles the operator's promise with v converted to its payload kind (a * value of the wrong kind rejects with the checked conversion's error). */ static void op_fn_resolve(OpFn *a, ScrDyn *v /*borrowed*/) { + op_fn_abort_ctl(a); switch (a->kind) { case PK_VOID: scr_promise_fulfill_void(a->p); @@ -2095,7 +2928,10 @@ static void op_fn_resolve(OpFn *a, ScrDyn *v /*borrowed*/) { scr_promise_reject_pending(a->p); } -static void op_fn_reject(OpFn *a, const ScrCaught *c) { op_reject(a->p, c); } +static void op_fn_reject(OpFn *a, const ScrCaught *c) { + op_fn_abort_ctl(a); + op_reject(a->p, c); +} static void op_fn_next(OpFn *a); @@ -2236,8 +3072,8 @@ static void op_fn_val_k(void *self, ScrDyn *v) { op_fn_next(a); return; } - ScrDyn *args[2] = {a->acc, v}; - ScrDyn *r = scr_dyn_call(a->fn, args, 2, "reducer"); + ScrDyn *args[3] = {a->acc, v, a->cb_opts}; + ScrDyn *r = scr_dyn_call(a->fn, args, a->cb_opts ? 3 : 2, "reducer"); scr_dyn_release(v); if (!r) { op_fn_close(a, scr_exc_take(), NULL); @@ -2319,7 +3155,7 @@ static bool op_map_options(const ScrDyn *options, double *concurrency, double *h /* map(fn, options) with fn already checked: +1 generator, or NULL with * the error pending. */ -static OpMap *op_map_make(ScrStream *s, ScrDyn *fn, ScrDyn *options, int mode) { +static OpMap *op_map_make(ScrStream *s, ScrDyn *fn, ScrDyn *options, int mode, ScrDyn *cb_opts) { op_sentinels_init(); if (!op_check_options(options)) return NULL; bool ok; @@ -2330,26 +3166,28 @@ static OpMap *op_map_make(ScrStream *s, ScrDyn *fn, ScrDyn *options, int mode) { if (signal) scr_dyn_release(signal); return NULL; } - OpMap *m = map_new(s, fn, mode, concurrency, hwm, signal); + OpMap *m = map_new(s, fn, mode, concurrency, hwm, signal, cb_opts); if (signal) scr_dyn_release(signal); return m; } -ScrStream *scr_stream_op_map(ScrStream *s, ScrDyn *fn, ScrDyn *options) { +ScrStream *scr_stream_op_map(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts) { if (!op_check_fn(fn, "fn", 1)) return NULL; - OpMap *m = op_map_make(s, fn, options, MODE_MAP); + OpMap *m = op_map_make(s, fn, options, MODE_MAP, cb_opts); return m ? op_from_gen(&m->g) : NULL; } -ScrStream *scr_stream_op_filter(ScrStream *s, ScrDyn *fn, ScrDyn *options) { +ScrStream *scr_stream_op_filter(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts) { if (!op_check_fn(fn, "fn", 1)) return NULL; - OpMap *m = op_map_make(s, fn, options, MODE_FILTER); + OpMap *m = op_map_make(s, fn, options, MODE_FILTER, cb_opts); return m ? op_from_gen(&m->g) : NULL; } -ScrStream *scr_stream_op_flat_map(ScrStream *s, ScrDyn *fn, ScrDyn *options) { +static ScrStream *op_flat_map(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts, + ScrClosure *next, ScrClosure *read, ScrClosure *ret, + ScrClosure *thr, bool async_gen) { if (!op_check_fn(fn, "fn", 1)) return NULL; - OpMap *m = op_map_make(s, fn, options, MODE_MAP); + OpMap *m = op_map_make(s, fn, options, MODE_MAP, cb_opts); if (!m) return NULL; OpFlat *f = op_alloc(sizeof *f); gen_init(&f->g, &op_flat_body_drop); @@ -2357,9 +3195,34 @@ ScrStream *scr_stream_op_flat_map(ScrStream *s, ScrDyn *fn, ScrDyn *options) { f->g.ret = &flat_ret; f->g.thr = &flat_thr; f->values.g = &m->g; /* moves */ + if (next) { + f->gnext = scr_closure_retain(next); + f->gread = read ? scr_closure_retain(read) : NULL; + f->gret = scr_closure_retain(ret); + f->gthr = scr_closure_retain(thr); + f->gen_async = async_gen; + } return op_from_gen(&f->g); } +ScrStream *scr_stream_op_flat_map(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts) { + return op_flat_map(s, fn, options, cb_opts, NULL, NULL, NULL, NULL, false); +} + +/* flatMap whose callback returns a compiled generator: the helpers over + * its type (borrowed) step the generators it yields from. */ +ScrStream *scr_stream_op_flat_map_async_gen(ScrStream *s, ScrDyn *fn, ScrDyn *options, + ScrDyn *cb_opts, ScrClosure *next, ScrClosure *read, + ScrClosure *ret, ScrClosure *thr) { + return op_flat_map(s, fn, options, cb_opts, next, read, ret, thr, true); +} + +ScrStream *scr_stream_op_flat_map_sync_gen(ScrStream *s, ScrDyn *fn, ScrDyn *options, + ScrDyn *cb_opts, ScrClosure *next, ScrClosure *ret, + ScrClosure *thr) { + return op_flat_map(s, fn, options, cb_opts, next, NULL, ret, thr, false); +} + /* drop(number, options) / take(number, options) */ static ScrStream *op_slice(ScrStream *s, ScrDyn *number, ScrDyn *options, bool take) { op_sentinels_init(); @@ -2444,8 +3307,8 @@ static void op_noop_listener(ScrClosure *cb, void *err) { (void)err; } -ScrPromise *scr_stream_op_reduce(ScrStream *s, ScrDyn *fn, ScrDyn *init, bool has_init, - ScrDyn *options, double kind) { +static ScrPromise *op_reduce(ScrStream *s, ScrDyn *fn, ScrDyn *init, bool has_init, + ScrDyn *options, double kind, ScrClosure *ctl, ScrDyn *cb_opts) { op_sentinels_init(); OpFn *a = op_fn_new(FN_REDUCE, (int)kind); if (!op_check_fn(fn, "reducer", 2)) return op_fn_reject_now(a); @@ -2461,6 +3324,7 @@ ScrPromise *scr_stream_op_reduce(ScrStream *s, ScrDyn *fn, ScrDyn *init, bool ha ScrDyn *reason = op_get(a->signal, "reason"); ScrError *e = op_abort_error(reason ? reason : scr_dyn_undefined()); if (reason) scr_dyn_release(reason); + /* (thrown before the controller exists) */ /* this.once('error', () => {}) */ ScrStr *name = scr_str_new("error", 5); scr_emitter_release(scr_emitter_on((ScrEmitter *)s, name, @@ -2476,40 +3340,97 @@ ScrPromise *scr_stream_op_reduce(ScrStream *s, ScrDyn *fn, ScrDyn *init, bool ha op_unref(a); return p; } + /* const ac = new AbortController() (options.signal's listener that + * aborts it too is registered by the caller) */ + if (ctl) { + a->ctl = scr_closure_retain(ctl); + a->cb_opts = cb_opts && cb_opts->kind != SCR_DYN_UNDEF ? scr_dyn_retain(cb_opts) : NULL; + } return op_fn_start(a); } +ScrPromise *scr_stream_op_reduce(ScrStream *s, ScrDyn *fn, ScrDyn *init, bool has_init, + ScrDyn *options, double kind) { + return op_reduce(s, fn, init, has_init, options, kind, NULL, NULL); +} + +/* reduce with a reducer that reads its { signal }: abort is ac.abort() + * (borrowed), cb_opts `{ signal: ac.signal }`. */ +ScrPromise *scr_stream_op_reduce_signal(ScrStream *s, ScrDyn *fn, ScrDyn *init, bool has_init, + ScrDyn *options, double kind, ScrClosure *abort, + ScrDyn *cb_opts) { + return op_reduce(s, fn, init, has_init, options, kind, abort, cb_opts); +} + +/* `options?.signal`: +1, undefined when absent. */ +ScrDyn *scr_stream_op_signal_of(ScrDyn *options) { + if (!options || options->kind == SCR_DYN_UNDEF || options->kind == SCR_DYN_NULL) + return scr_dyn_retain(scr_dyn_undefined()); + ScrDyn *sig = op_get(options, "signal"); + if (scr_exc_pending()) scr_exc_clear(); + return sig ? sig : scr_dyn_retain(scr_dyn_undefined()); +} + +/* `{ once: true }`: +1. */ +ScrDyn *scr_stream_op_once_options(void) { + ScrDyn *o = scr_dyn_new_obj(); + scr_dyn_obj_set(o, "once", 4, scr_dyn_new_bool(true)); + return o; +} + +/* `[options?.signal].filter(Boolean)` for AbortSignal.any: +1 array. */ +ScrDyn *scr_stream_op_signal_list(ScrDyn *options) { + ScrDyn *list = scr_dyn_new_arr(); + if (options && options->kind != SCR_DYN_UNDEF && options->kind != SCR_DYN_NULL) { + /* A signal that fails validateAbortSignal is left out: the operator + * itself throws Node's ERR_INVALID_ARG_TYPE for it. */ + bool ok; + ScrDyn *sig = op_signal(options, &ok); + if (sig) scr_dyn_arr_push(list, sig); /* moves */ + if (scr_exc_pending()) scr_exc_clear(); + } + return list; +} + +/* `{ signal }`, the callbacks' second argument: +1 object (borrows signal). */ +ScrDyn *scr_stream_op_signal_opts(ScrDyn *signal) { + ScrDyn *o = scr_dyn_new_obj(); + scr_dyn_obj_set(o, "signal", 6, scr_dyn_retain(signal)); + return o; +} + /* some/find/every/forEach: the loop over map's generator (filter's for * some, find and every). */ -static ScrPromise *op_gen_fn(ScrStream *s, ScrDyn *fn, ScrDyn *options, int op, int mode, int kind) { +static ScrPromise *op_gen_fn(ScrStream *s, ScrDyn *fn, ScrDyn *options, int op, int mode, int kind, + ScrDyn *cb_opts) { op_sentinels_init(); OpFn *a = op_fn_new(op, kind); if (!op_check_fn(fn, "fn", 1)) return op_fn_reject_now(a); - OpMap *m = op_map_make(s, fn, options, mode); + OpMap *m = op_map_make(s, fn, options, mode, cb_opts); if (!m) return op_fn_reject_now(a); a->src.g = &m->g; /* moves */ return op_fn_start(a); } -ScrPromise *scr_stream_op_some(ScrStream *s, ScrDyn *fn, ScrDyn *options) { - return op_gen_fn(s, fn, options, FN_SOME, MODE_FILTER, PK_BOOL); +ScrPromise *scr_stream_op_some(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts) { + return op_gen_fn(s, fn, options, FN_SOME, MODE_FILTER, PK_BOOL, cb_opts); } -ScrPromise *scr_stream_op_find(ScrStream *s, ScrDyn *fn, ScrDyn *options) { - return op_gen_fn(s, fn, options, FN_FIND, MODE_FILTER, PK_DYN); +ScrPromise *scr_stream_op_find(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts) { + return op_gen_fn(s, fn, options, FN_FIND, MODE_FILTER, PK_DYN, cb_opts); } -ScrPromise *scr_stream_op_for_each(ScrStream *s, ScrDyn *fn, ScrDyn *options) { - return op_gen_fn(s, fn, options, FN_FOR_EACH, MODE_FOREACH, PK_VOID); +ScrPromise *scr_stream_op_for_each(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts) { + return op_gen_fn(s, fn, options, FN_FOR_EACH, MODE_FOREACH, PK_VOID, cb_opts); } /* every(fn, options) = !(await some.call(this, async (...args) => * !(await fn(...args)), options)) */ -ScrPromise *scr_stream_op_every(ScrStream *s, ScrDyn *fn, ScrDyn *options) { +ScrPromise *scr_stream_op_every(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts) { op_sentinels_init(); OpFn *outer = op_fn_new(FN_EVERY, PK_BOOL); if (!op_check_fn(fn, "fn", 1)) return op_fn_reject_now(outer); - ScrPromise *some = op_gen_fn(s, fn, options, FN_EVERY, MODE_EVERY, PK_DYN); + ScrPromise *some = op_gen_fn(s, fn, options, FN_EVERY, MODE_EVERY, PK_DYN, cb_opts); ScrPromise *p = scr_promise_retain(outer->p); op_await(some, &op_every_k, outer); scr_promise_release(some); diff --git a/packages/runtime/src/scr_string.c b/packages/runtime/src/scr_string.c index 58272cf67..f94554642 100644 --- a/packages/runtime/src/scr_string.c +++ b/packages/runtime/src/scr_string.c @@ -2571,15 +2571,11 @@ ScrArr *scr_str_split_limit(ScrStr *s, ScrStr *sep, double limit_num) { while (count < 64 && (bytes = scr_str_split_span(s, sep, &cursor, false, &len))) slots[count++] = (uint64_t)(uintptr_t)(s->ascii ? scr_str_from_span_ascii(bytes, len) : scr_str_from_span(bytes, len)); - ScrArr *out = scr_arr_new_inline(SCR_ELEM_STR, count); + ScrArr *out = scr_arr_new_inline(SCR_ELEM_STR, count, false); if (count) { - /* A fresh inline array of exactly `count` slots: store the pieces and - * their states directly. */ - uint8_t *states = scr_arr_states_end(out); - for (size_t k = 0; k < count; k++) { - out->data[k] = slots[k]; - *scr_arr_state_in(states, k) = SCR_ARR_VALUE; - } + /* A fresh inline array of exactly `count` slots: store the pieces + * directly. */ + for (size_t k = 0; k < count; k++) out->data[k] = slots[k]; out->len = (uint32_t)count; out->filled = (uint32_t)count; } diff --git a/packages/runtime/src/scr_symbol.c b/packages/runtime/src/scr_symbol.c index 304f2544f..647c31dc3 100644 --- a/packages/runtime/src/scr_symbol.c +++ b/packages/runtime/src/scr_symbol.c @@ -85,6 +85,11 @@ ScrSym *scr_sym_well_known(ScrStr *name) { scr_dyn_install_iterator_symbol(key); scr_dyn_release(key); } + if (name->len == 11 && !memcmp(name->data, "hasInstance", 11)) { + ScrDyn *key = scr_dyn_new_symbol(symbol); + scr_dyn_install_has_instance_symbol(key); + scr_dyn_release(key); + } if (name->len == 13 && !memcmp(name->data, "asyncIterator", 13)) { ScrDyn *key = scr_dyn_new_symbol(symbol); scr_dyn_install_async_iterator_symbol(key); diff --git a/packages/runtime/src/scr_worker_events.c b/packages/runtime/src/scr_worker_events.c index 3d10183f4..a23395c91 100644 --- a/packages/runtime/src/scr_worker_events.c +++ b/packages/runtime/src/scr_worker_events.c @@ -354,17 +354,23 @@ static bool scr_worker_transfer_has(const ScrDyn *transfer, const ScrWorkerView return false; } +/* Whether a method name is `name`: first characters compare inline, so + * the dispatch ladders below call strcmp only for names that can match. */ +static inline bool method_is(const char *method, const char *name) { + return method[0] == name[0] && strcmp(method, name) == 0; +} + static ScrDyn *scr_worker_view_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what) { (void)what; ScrWorkerView *view = ptr; - if (!strcmp(method, "ref") || !strcmp(method, "unref")) { - view->referenced = !strcmp(method, "ref"); + if (method_is(method, "ref") || method_is(method, "unref")) { + view->referenced = method_is(method, "ref"); return scr_dyn_retain(scr_dyn_undefined()); } - if (view->port && !strcmp(method, "hasRef")) + if (view->port && method_is(method, "hasRef")) return scr_dyn_new_bool(view->referenced && (!view->closed || (view->channel && view->closing))); - if (!view->port && !strcmp(method, "terminate")) { + if (!view->port && method_is(method, "terminate")) { if (!view->termination) { view->termination = scr_promise_new(); if (view->closed) scr_promise_fulfill_ref(view->termination, @@ -373,7 +379,7 @@ static ScrDyn *scr_worker_view_invoke(void *ptr, ScrDyn *self, const char *metho } return scr_dyn_new_promise(view->termination); } - if (!strcmp(method, "postMessage")) { + if (method_is(method, "postMessage")) { /* Worker drops messages after exit before validating or cloning. A * closed MessagePort still validates its payload, as Node does. */ if (!view->port && view->closed) return scr_dyn_retain(scr_dyn_undefined()); @@ -405,9 +411,9 @@ static ScrDyn *scr_worker_view_invoke(void *ptr, ScrDyn *self, const char *metho else scr_worker_thread_post(view->thread, message); return scr_dyn_retain(scr_dyn_undefined()); } - bool once = !strcmp(method, "once") || !strcmp(method, "prependOnceListener"); - bool prepend = !strcmp(method, "prependListener") || !strcmp(method, "prependOnceListener"); - if (once || prepend || !strcmp(method, "on") || !strcmp(method, "addListener")) { + bool once = method_is(method, "once") || method_is(method, "prependOnceListener"); + bool prepend = method_is(method, "prependListener") || method_is(method, "prependOnceListener"); + if (once || prepend || method_is(method, "on") || method_is(method, "addListener")) { ScrDyn *listener = argc > 1 ? args[1] : scr_dyn_undefined(); scr_dyn_check_listener(listener, "listener"); if (scr_exc_pending()) return NULL; @@ -416,7 +422,7 @@ static ScrDyn *scr_worker_view_invoke(void *ptr, ScrDyn *self, const char *metho scr_emitter_release(scr_emitter_on_flex(view->events, args[0]->v.str, listener, once, prepend)); return scr_exc_pending() ? NULL : scr_dyn_retain(self); } - if (!strcmp(method, "off") || !strcmp(method, "removeListener")) { + if (method_is(method, "off") || method_is(method, "removeListener")) { ScrDyn *listener = argc > 1 ? args[1] : scr_dyn_undefined(); scr_dyn_check_listener(listener, "listener"); if (scr_exc_pending()) return NULL; @@ -425,21 +431,21 @@ static ScrDyn *scr_worker_view_invoke(void *ptr, ScrDyn *self, const char *metho scr_emitter_release(scr_emitter_off_dyn(view->events, args[0]->v.str, listener)); return scr_exc_pending() ? NULL : scr_dyn_retain(self); } - if (!strcmp(method, "removeAllListeners")) { + if (method_is(method, "removeAllListeners")) { bool all = !argc || args[0]->kind == SCR_DYN_UNDEF; if (!all && args[0]->kind != SCR_DYN_STR) return scr_worker_refusal("Worker event names must be strings"); scr_emitter_release(scr_emitter_remove_all(view->events, all ? NULL : args[0]->v.str, all)); return scr_exc_pending() ? NULL : scr_dyn_retain(self); } - if (!strcmp(method, "listenerCount")) { + if (method_is(method, "listenerCount")) { if (!argc || args[0]->kind != SCR_DYN_STR) return scr_worker_refusal("Worker event names must be strings"); return scr_dyn_new_num(argc > 1 && args[1]->kind != SCR_DYN_UNDEF ? scr_emitter_listener_count_dyn(view->events, args[0]->v.str, args[1]) : scr_emitter_listener_count(view->events, args[0]->v.str)); } - if (view->port && !strcmp(method, "close")) { + if (view->port && method_is(method, "close")) { if (argc && args[0]->kind != SCR_DYN_UNDEF) { scr_dyn_check_listener(args[0], "callback"); if (scr_exc_pending()) return NULL; @@ -460,7 +466,7 @@ static ScrDyn *scr_worker_view_invoke(void *ptr, ScrDyn *self, const char *metho } return scr_dyn_retain(scr_dyn_undefined()); } - if (view->port && !strcmp(method, "start")) { + if (view->port && method_is(method, "start")) { view->started = true; return scr_dyn_retain(scr_dyn_undefined()); } @@ -1021,7 +1027,7 @@ static ScrDyn *scr_broadcast_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what) { (void)what; ScrWorkerView *view = ptr; - if (!strcmp(method, "postMessage")) { + if (method_is(method, "postMessage")) { if (view->closed) { scr_throw_domex("InvalidStateError", "BroadcastChannel is closed."); return NULL; @@ -1045,7 +1051,7 @@ static ScrDyn *scr_broadcast_invoke(void *ptr, ScrDyn *self, const char *method, scr_message_free(first); return scr_exc_pending() ? NULL : scr_dyn_retain(scr_dyn_undefined()); } - if (!strcmp(method, "close")) { + if (method_is(method, "close")) { if (!view->closed) { view->closed = true; scr_broadcast_leave(view->channel); @@ -1053,8 +1059,8 @@ static ScrDyn *scr_broadcast_invoke(void *ptr, ScrDyn *self, const char *method, } return scr_dyn_retain(scr_dyn_undefined()); } - if (!strcmp(method, "ref") || !strcmp(method, "unref")) { - view->referenced = !strcmp(method, "ref"); + if (method_is(method, "ref") || method_is(method, "unref")) { + view->referenced = method_is(method, "ref"); return scr_dyn_retain(self); } ScrDyn *out = NULL; diff --git a/packages/runtime/src/scr_zlib_stream.c b/packages/runtime/src/scr_zlib_stream.c index 54ffacf02..40daa6c76 100644 --- a/packages/runtime/src/scr_zlib_stream.c +++ b/packages/runtime/src/scr_zlib_stream.c @@ -549,7 +549,12 @@ void scr_zlib_stream_flush(ScrStream *s, ScrDyn *kind, ScrClosure *cb) { return; } int ki = (int)k; - if (zs->flush_bufs[ki] == NULL) zs->flush_bufs[ki] = scr_bytes_new(SCR_BYTES_U8, 0); + if (zs->flush_bufs[ki] == NULL) { + /* a Buffer, as Node's kFlushBuffers are: write() passes a Buffer + * through unchanged, so _transform recognizes the sentinel */ + zs->flush_bufs[ki] = scr_bytes_new(SCR_BYTES_U8, 0); + zs->flush_bufs[ki]->is_buffer = true; + } scr_stream_write(s, zs->flush_bufs[ki], cb); } diff --git a/packages/runtime/test/test_array.c b/packages/runtime/test/test_array.c index d943acfb2..7ca0ebd1a 100644 --- a/packages/runtime/test/test_array.c +++ b/packages/runtime/test/test_array.c @@ -446,6 +446,44 @@ static void test_ref_elements(void) { #endif } +/* Elements whose release does nothing (SCR_ARR_OPS_RELEASE_NOTHING): the + * last release of an array frees its storage without calling the element + * release. An array with out-of-line storage keeps the full teardown. */ +static int nothing_releases = 0; +static void *nothing_retain(void *p) { return p; } +static void nothing_release(void *p) { + (void)p; + nothing_releases++; +} +static ScrArrOps nothing_ops = { ¬hing_retain, ¬hing_release, NULL, 0, + SCR_ARR_OPS_RELEASE_NOTHING }; + +static void test_release_nothing_ops(void) { +#ifdef SCR_RC_AUDIT + long arrays0 = scr_arr_live_count(); +#endif + static MockRec kept[3]; + ScrArr *a = scr_arr_new_ops(¬hing_ops, 0); + for (int i = 0; i < 3; i++) scr_arr_push_ref(a, &kept[i]); + ScrArr *shared = scr_arr_retain(a); + scr_arr_release(shared); + check_f64(scr_arr_len(a), 3, "release-nothing: a shared release keeps the array"); + scr_arr_release(a); + check(nothing_releases == 0, "release-nothing: teardown calls no element release"); + ScrArr *sparse = scr_arr_new_ops(¬hing_ops, 0); + scr_arr_push_ref(sparse, &kept[0]); + scr_arr_set_ref(sparse, 4000000000.0, &kept[1]); /* out-of-line sparse slot */ + check(scr_arr_ext_of(sparse) != NULL, "release-nothing: a far index makes an ext"); + scr_arr_release(sparse); + check(nothing_releases == 2, "release-nothing: an array with an ext keeps the full teardown"); + ScrArr *nums = scr_arr_new(SCR_ELEM_F64, 0); + scr_arr_push_f64(nums, 1); + scr_arr_release(nums); +#ifdef SCR_RC_AUDIT + check(scr_arr_live_count() == arrays0, "release-nothing: no arrays leaked"); +#endif +} + /* Element ops past the registry's capacity: every descriptor still finds its * own retain and release, and the first ones keep their registry ids. */ static void test_ops_overflow(void) { @@ -1365,11 +1403,13 @@ static void test_exact_construction(void) { } /* The hole-free prefix bound never covers a non-value state, and the - * operations that keep an array hole-free keep the bound at its length. */ + * operations that keep an array hole-free keep the bound at its length. An + * array without states keeps its elements in dense storage. */ static bool filled_ok(const ScrArr *a) { if (a->filled > a->len || a->filled > a->cap) return false; + if (!scr_arr_has_states(a) && (a->len > a->cap || a->filled != a->len)) return false; for (size_t i = 0; i < a->filled; i++) - if (*scr_arr_state_ptr(a, i) != SCR_ARR_VALUE) return false; + if (scr_arr_dense_state(a, i) != SCR_ARR_VALUE) return false; return true; } @@ -1444,7 +1484,7 @@ static void test_filled_prefix(void) { scr_arr_release(scr_arr_sort_primitive(b, false)); check(filled_ok(b), "sorting moves holes past the bound"); for (size_t i = 0; i < b->len; i++) { - if (i < b->cap && *scr_arr_state_ptr(b, i) != SCR_ARR_VALUE) break; + if (i < b->cap && scr_arr_dense_state(b, i) != SCR_ARR_VALUE) break; check(i < b->filled, "sorted values form the bound"); } while (scr_arr_len(b) > 0) { @@ -1464,11 +1504,13 @@ static void test_filled_prefix(void) { } /* Boolean arrays store one byte per dense element, followed by the states in - * the same allocation. Growth moves the states to the end of the larger area, - * densifying absorbs sparse entries into bytes, and every moving and copying - * operation keeps values, holes and present undefined in place. */ + * the same allocation once they have states. Growth moves the states to the + * end of the larger area, densifying absorbs sparse entries into bytes, and + * every moving and copying operation keeps values, holes and present + * undefined in place. */ static bool bool_layout_ok(const ScrArr *a) { - return a->cap == 0 || scr_arr_states_end(a) == (const uint8_t *)a->data + 2 * (size_t)a->cap; + return a->cap == 0 || !scr_arr_has_states(a) || + scr_arr_states_end(a) == (const uint8_t *)a->data + 2 * (size_t)a->cap; } static bool bool_states(const ScrArr *a, const char *want) { @@ -1572,6 +1614,102 @@ static bool inline_slots(const ScrArr *a) { return a->data == (const uint64_t *)((const char *)a + ((sizeof(ScrArr) + 7) & ~(size_t)7)); } +/* Int32 slots: 4-byte values, then the state bytes when the array has them. */ +static bool i32_layout_ok(const ScrArr *a) { + return a->elem == SCR_ELEM_I32 && scr_arr_slot_width(a) == sizeof(int32_t); +} + +/* SCR_ELEM_I32: 4-byte int32 slots behind the double-valued entry points. */ +static void test_i32_slots(void) { + ScrArr *a = scr_arr_new_small(SCR_ELEM_I32); + for (int i = 0; i < 1000; i++) scr_arr_push_f64(a, (double)(i * 7 - 3000)); + check(i32_layout_ok(a) && !scr_arr_has_states(a) && a->filled == 1000, + "int32 pushes grow four-byte storage without states"); + bool values_ok = true; + for (int i = 0; i < 1000; i++) { + values_ok &= ((const int32_t *)a->data)[i] == i * 7 - 3000; + values_ok &= scr_arr_get_f64(a, i) == (double)(i * 7 - 3000); + values_ok &= scr_arr_get_number(a, i) == (double)(i * 7 - 3000); + } + check(values_ok, "int32 values are stored as 4-byte integers and read as doubles"); + scr_arr_set_f64(a, 10, 2147483647); + scr_arr_set_f64(a, 11, -2147483648.0); + check(scr_arr_get_f64(a, 10) == 2147483647 && scr_arr_get_f64(a, 11) == -2147483648.0, + "int32 extremes round-trip"); + check(scr_arr_pop_f64(a) == 999 * 7 - 3000 && scr_arr_len(a) == 999, "pop reads the last int32"); + scr_arr_set_len(a, 5); + scr_arr_set_len(a, 8); + check(i32_layout_ok(a) && scr_arr_has_states(a) && scr_arr_state(a, 6) == SCR_ARR_HOLE && + isnan(scr_arr_get_number(a, 6)) && scr_arr_get_f64(a, 4) == 4 * 7 - 3000, + "int32 truncation clears slots and extension adds holes"); + scr_arr_set_f64(a, 20, 5); + check(scr_arr_len(a) == 21 && scr_arr_state(a, 15) == SCR_ARR_HOLE && scr_arr_get_f64(a, 20) == 5, + "an int32 store past the length leaves holes"); + scr_arr_release(scr_arr_fill_f64(a, -9, 0, 21)); + bool filled_ok = a->filled == 21; + for (int i = 0; i < 21; i++) filled_ok &= scr_arr_get_f64(a, i) == -9; + check(filled_ok, "int32 fill writes every slot"); + scr_arr_release(a); + + ScrArr *s = scr_arr_new(SCR_ELEM_I32, 0); + scr_arr_set_f64(s, 0, 1); + scr_arr_set_f64(s, 3000000, 3); + scr_arr_set_f64(s, 2000000, -2); + check(arr_sparse_len(s) == 2 && scr_arr_get_f64(s, 2000000) == -2 && scr_arr_get_f64(s, 3000000) == 3, + "int32 sparse entries keep double slots"); + scr_arr_set_len(s, 2000001); + scr_arr_release(scr_arr_fill_f64(s, 4, 1, 2000000)); + check(arr_sparse_len(s) == 0 && i32_layout_ok(s) && scr_arr_get_f64(s, 2000000) == -2 && + scr_arr_get_f64(s, 1999999) == 4 && scr_arr_get_f64(s, 0) == 1, + "densifying absorbs sparse entries into int32 slots"); + scr_arr_release(s); + + ScrArr *k = scr_arr_new(SCR_ELEM_I32, 6); + for (int i = 0; i < 6; i++) scr_arr_push_f64(k, i); + scr_arr_push_f64(k, 6); + check(i32_layout_ok(k) && scr_arr_get_f64(k, 6) == 6 && scr_arr_get_f64(k, 0) == 0, + "a known-count int32 array grows out of its block"); + scr_arr_release(k); + + /* Widening: in place, to 8-byte double slots, keeping states. */ + ScrArr *w = scr_arr_new(SCR_ELEM_I32, 3); + for (int i = 0; i < 3; i++) scr_arr_push_f64(w, i - 1); + check(inline_slots(w), "a three-element int32 construction keeps inline slots"); + scr_arr_widen_i32(w); + check(w->elem == SCR_ELEM_F64 && !inline_slots(w) && scr_arr_len(w) == 3 && w->filled == 3 && + scr_arr_get_f64(w, 0) == -1 && scr_arr_get_f64(w, 2) == 1, + "widening moves inline int32 slots out as doubles"); + scr_arr_set_f64(w, 1, 2.5); + check(scr_arr_get_f64(w, 1) == 2.5, "a widened array stores fractions"); + scr_arr_widen_i32(w); + check(w->elem == SCR_ELEM_F64 && scr_arr_get_f64(w, 1) == 2.5, "widening twice is a no-op"); + scr_arr_release(w); + + ScrArr *h = scr_arr_new_small(SCR_ELEM_I32); + for (int i = 0; i < 100; i++) scr_arr_push_f64(h, i * 3); + scr_arr_set_len(h, 120); + scr_arr_set_undefined(h, 5); + scr_arr_set_f64(h, 4000000, -7); + size_t cap = h->cap; + scr_arr_widen_i32(h); + bool widened = h->elem == SCR_ELEM_F64 && h->cap == cap && scr_arr_len(h) == 4000001 && + scr_arr_states_end(h) == (const uint8_t *)h->data + 9 * (size_t)h->cap; + for (int i = 0; i < 100; i++) + if (i != 5) widened &= scr_arr_get_f64(h, i) == i * 3; + check(widened && scr_arr_state(h, 5) == SCR_ARR_UNDEFINED && scr_arr_state(h, 110) == SCR_ARR_HOLE && + scr_arr_get_f64(h, 4000000) == -7 && arr_sparse_len(h) == 1, + "widening keeps values, present undefined, holes and sparse entries"); + scr_arr_push_f64(h, 0.5); + check(scr_arr_get_f64(h, 4000001) == 0.5, "a widened array keeps growing as doubles"); + scr_arr_release(h); + + ScrArr *e = scr_arr_new_small(SCR_ELEM_I32); + scr_arr_widen_i32(e); + scr_arr_push_f64(e, -0.0); + check(e->elem == SCR_ELEM_F64 && signbit(scr_arr_get_f64(e, 0)), "an empty int32 array widens too"); + scr_arr_release(e); +} + static void test_inline_slots(void) { #ifdef SCR_RC_AUDIT long strings0 = scr_str_live_count(); @@ -1584,7 +1722,7 @@ static void test_inline_slots(void) { * block; growth past them moves them out like any inline array. */ ScrArr *known = scr_arr_new(SCR_ELEM_F64, 4); check(inline_slots(known) && known->cap >= 4 && known->len == 0 && !scr_arr_has(known, 0) && - scr_arr_states_end(known) == (uint8_t *)(known->data + known->cap) + known->cap, + !scr_arr_has_states(known), "a known-count construction places its slots in the array block"); size_t known_cap = known->cap; for (size_t i = 0; i <= known_cap; i++) scr_arr_push_f64(known, (double)i); @@ -1605,26 +1743,25 @@ static void test_inline_slots(void) { "known-count boolean constructions are inline too"); scr_arr_release(known_flags); ScrArr *a = scr_arr_new_small(SCR_ELEM_F64); - check(inline_slots(a) && a->cap >= 4 && a->len == 0 && - scr_arr_states_end(a) == (uint8_t *)(a->data + a->cap) + a->cap, - "small construction places slots and states in the array block"); + check(inline_slots(a) && a->cap >= 4 && a->len == 0 && !scr_arr_has_states(a), + "small construction places its slots in the array block, without states"); size_t inline_cap = a->cap; for (size_t i = 0; i < inline_cap; i++) scr_arr_push_f64(a, (double)i); check(inline_slots(a) && a->filled == inline_cap, "appends within the inline capacity stay inline"); scr_arr_push_f64(a, 100); check(!inline_slots(a) && a->cap > inline_cap && scr_arr_get_f64(a, 0) == 0 && scr_arr_get_f64(a, (double)inline_cap - 1) == (double)inline_cap - 1 && - scr_arr_get_f64(a, (double)inline_cap) == 100 && - scr_arr_states_end(a) == (uint8_t *)(a->data + a->cap) + a->cap, + scr_arr_get_f64(a, (double)inline_cap) == 100 && !scr_arr_has_states(a), "growth moves inline slots to their own allocation"); scr_arr_release(a); ScrArr *flags = scr_arr_new_small(SCR_ELEM_BOOL); - check(inline_slots(flags) && scr_arr_states_end(flags) == (uint8_t *)flags->data + 2 * (size_t)flags->cap, + check(inline_slots(flags) && flags->cap >= 4 && !scr_arr_has_states(flags), "boolean inline slots are one byte each"); scr_arr_push_bool(flags, true); scr_arr_set_bool(flags, 3, true); - check(scr_arr_get_bool(flags, 0) && !scr_arr_has(flags, 1) && scr_arr_get_bool(flags, 3), + check(scr_arr_get_bool(flags, 0) && !scr_arr_has(flags, 1) && scr_arr_get_bool(flags, 3) && + scr_arr_has_states(flags) && bool_layout_ok(flags), "boolean inline slots keep holes"); scr_arr_release(flags); @@ -2107,8 +2244,8 @@ static void test_head_room(void) { check_f64(scr_arr_shift_f64(q), 0, "shift answers the first element"); check(q->data == block + 1 && q->cap == cap - 1 && q->len == 63, "shift advances the storage instead of moving the tail"); - check(scr_arr_states_end(q) == (uint8_t *)(block + cap) + cap - 1 && head_room(q) && q->data[-1] == 1, - "the remaining states stay at the end of the allocation"); + check(!scr_arr_has_states(q) && head_room(q) && q->data[-1] == 1, + "shift records the vacated slot in front of the data"); check(q->filled == 63, "shift keeps the hole-free prefix"); for (int i = 1; i < 40; i++) scr_arr_shift_f64(q); check(q->data == block + 40 && q->len == 24 && scr_arr_get_f64(q, 0) == 40 && @@ -2223,6 +2360,159 @@ static void test_head_room(void) { #endif } +/* SCR_ARR_STATES (scr_array.h): an array starts without state bytes and + * keeps none while every index below its length is a present value. A + * hole, a present undefined or a length past the elements gives it states, + * with the elements in place. */ +static ScrStr *str_of(const char *text) { return scr_str_new(text, strlen(text)); } + +static bool strs_are(ScrArr *a, const char *want) { + /* want: one character per element, '_' a hole, 'u' undefined. */ + if (scr_arr_len(a) != (double)strlen(want)) return false; + for (size_t i = 0; want[i]; i++) { + double state = scr_arr_state(a, (double)i); + if (want[i] == '_') { + if (state != SCR_ARR_HOLE) return false; + } else if (want[i] == 'u') { + if (state != SCR_ARR_UNDEFINED) return false; + } else { + if (state != SCR_ARR_VALUE) return false; + ScrStr *got = scr_arr_get_ref(a, (double)i); + bool same = got->len == 1 && got->data[0] == want[i]; + scr_str_release(got); + if (!same) return false; + } + } + return filled_ok(a); +} + +static ScrArr *strs_of(const char *text, bool small) { + ScrArr *a = small ? scr_arr_new_small(SCR_ELEM_STR) : scr_arr_new(SCR_ELEM_STR, 0); + for (size_t i = 0; text[i]; i++) scr_arr_push_ref(a, scr_str_new(text + i, 1)); + return a; +} + +static void test_states_on_demand(void) { +#ifdef SCR_RC_AUDIT + long strings0 = scr_str_live_count(); + long arrays0 = scr_arr_live_count(); +#endif + ScrArr *a = strs_of("abcdef", false); + scr_arr_set_ref(a, 1, str_of("B")); + scr_arr_set_ref(a, 6, str_of("g")); + scr_str_release(scr_arr_pop_ref(a)); + scr_arr_set_len(a, 5); + check(!scr_arr_has_states(a) && a->filled == 5 && strs_are(a, "aBcde"), + "pushes, writes below the length, appends, pops and truncation keep no states"); + scr_arr_set_undefined(a, 2); + check(scr_arr_has_states(a) && strs_are(a, "aBude"), "a present undefined gives the array states"); + scr_arr_release(a); + + ScrArr *gap = strs_of("ab", true); + scr_arr_set_ref(gap, 3, str_of("d")); + check(scr_arr_has_states(gap) && strs_are(gap, "ab_d"), "a write past the length leaves holes"); + scr_arr_release(gap); + + ScrArr *longer = strs_of("abc", true); + scr_arr_set_len(longer, 5); + check(scr_arr_has_states(longer) && strs_are(longer, "abc__"), "a longer length adds holes"); + scr_arr_release(longer); + + /* `new Array(n)` and a length set on an empty small array: the inline + * slots give way to one allocation holding the slots and their states. */ + ScrArr *sized = scr_arr_new_small(SCR_ELEM_STR); + scr_arr_set_len(sized, 6); + scr_arr_set_ref(sized, 4, str_of("e")); + check(scr_arr_has_states(sized) && !(sized->spare & SCR_ARR_INLINE_SLOTS) && sized->cap == 6 && + strs_are(sized, "____e_") && filled_ok(sized), + "a length set on an empty inline array reserves that many slots with states"); + scr_arr_release(sized); + + ScrArr *deleted = strs_of("abcd", false); + scr_arr_delete(deleted, 1); + check(scr_arr_has_states(deleted) && strs_are(deleted, "a_cd"), "delete leaves a hole"); + scr_arr_release(deleted); + + /* Conversion keeps out-of-line storage and head room's elements. */ + ScrArr *props = strs_of("abc", true); + scr_arr_set_ref(props, -1, str_of("p")); + scr_arr_set_undefined(props, 3); + ScrStr *prop = scr_arr_get_ref(props, -1); + check(scr_arr_has_states(props) && strs_are(props, "abcu") && prop->data[0] == 'p', + "states keep a noncanonical property"); + scr_str_release(prop); + scr_arr_release(props); + ScrArr *q = strs_of("abcdefghijklmnopqrst", false); + for (int i = 0; i < 3; i++) scr_str_release(scr_arr_shift_ref(q)); + check(!scr_arr_has_states(q) && head_room(q), "shift keeps no states"); + scr_arr_set_undefined(q, 1); + check(scr_arr_has_states(q) && !head_room(q) && strs_are(q, "dufghijklmnopqrst"), + "states move the elements of head room to the front"); + scr_arr_release(q); + + /* Copies keep no states for values, and states for holes. */ + ScrArr *src = strs_of("abcde", false); + ScrArr *part = scr_arr_slice(src, 1, 4); + ScrArr *joined = scr_arr_slice(src, 0, INFINITY); + scr_arr_concat_copy(joined, src); + ScrArr *spliced = scr_arr_splice(joined, 2, 3); + ScrArr *reversed = scr_arr_to_reversed(src); + scr_arr_release(scr_arr_reverse(src)); + scr_arr_release(scr_arr_sort_primitive(src, false)); + check(!scr_arr_has_states(part) && strs_are(part, "bcd"), "a slice of values keeps no states"); + check(!scr_arr_has_states(joined) && strs_are(joined, "ababcde"), + "concat and splice of values keep no states"); + check(!scr_arr_has_states(spliced) && strs_are(spliced, "cde"), "removed values keep no states"); + check(!scr_arr_has_states(reversed) && strs_are(reversed, "edcba"), "toReversed of values keeps no states"); + check(!scr_arr_has_states(src) && strs_are(src, "abcde"), "reverse and sort of values keep no states"); + scr_arr_release(src); + scr_arr_release(part); + scr_arr_release(joined); + scr_arr_release(spliced); + scr_arr_release(reversed); + ScrArr *holey = strs_of("ab", false); + scr_arr_set_ref(holey, 3, str_of("d")); + ScrArr *spread = strs_of("x", true); + scr_arr_push_spread(spread, holey); + ScrArr *concat = strs_of("x", true); + scr_arr_concat_copy(concat, holey); + ScrArr *values = scr_arr_slice(holey, 0, 2); + ScrArr *front = strs_of("yz", false); + scr_arr_unshift_spread(front, holey); + check(scr_arr_has_states(spread) && strs_are(spread, "xabud") && scr_arr_has_states(concat) && + strs_are(concat, "xab_d") && !scr_arr_has_states(values) && strs_are(values, "ab") && + scr_arr_has_states(front) && strs_are(front, "abudyz"), + "copies of holes keep holes or present undefined"); + scr_arr_release(holey); + scr_arr_release(spread); + scr_arr_release(concat); + scr_arr_release(values); + scr_arr_release(front); + + /* Numbers and booleans take the same paths without references. */ + ScrArr *nums = scr_arr_new(SCR_ELEM_F64, 3); + for (int i = 0; i < 40; i++) scr_arr_push_f64(nums, i); + scr_arr_release(scr_arr_fill_f64(nums, 7, 30, INFINITY)); + check(!scr_arr_has_states(nums) && scr_arr_get_number(nums, 39) == 7 && scr_arr_get_number(nums, 40) != 0, + "number fills keep no states"); + scr_arr_release(scr_arr_fill_undefined(nums, 0, 1)); + check(scr_arr_has_states(nums) && scr_arr_state(nums, 0) == SCR_ARR_UNDEFINED && + scr_arr_get_number(nums, 1) == 1 && scr_arr_get_number(nums, 39) == 7 && filled_ok(nums), + "an undefined fill gives a number array states"); + scr_arr_release(nums); + ScrArr *bits = scr_arr_new_small(SCR_ELEM_BOOL); + for (int i = 0; i < 6; i++) scr_arr_push_bool(bits, i % 2 == 0); + scr_arr_set_undefined(bits, 6); + check(scr_arr_has_states(bits) && bool_layout_ok(bits) && scr_arr_get_bool(bits, 4) && + !scr_arr_get_bool(bits, 5) && scr_arr_state(bits, 6) == SCR_ARR_UNDEFINED, + "boolean arrays get one state byte per element"); + scr_arr_release(bits); +#ifdef SCR_RC_AUDIT + check(scr_str_live_count() == strings0, "states on demand: no strings leaked"); + check(scr_arr_live_count() == arrays0, "states on demand: no arrays leaked"); +#endif +} + int main(int argc, char **argv) { if (argc > 1) { ScrArr *a = scr_arr_new(SCR_ELEM_F64, 0); @@ -2263,6 +2553,7 @@ int main(int argc, char **argv) { test_nested_rc(); test_index_of_includes(); test_ref_elements(); + test_release_nothing_ops(); test_ref_known_count_cycle(&mock_cyc_ops); test_ops_overflow(); test_ref_known_count_cycle(&mock_cyc_ops_late); @@ -2277,8 +2568,10 @@ int main(int argc, char **argv) { test_splice_drop(); test_filled_prefix(); test_bool_bytes(); + test_i32_slots(); test_inline_slots(); test_head_room(); + test_states_on_demand(); fprintf(stderr, "%ld/%ld cases passed\n", total - failed, total); return failed == 0 ? 0 : 1; diff --git a/packages/runtime/test/test_cycle.c b/packages/runtime/test/test_cycle.c index 4e00b852e..160083d6b 100644 --- a/packages/runtime/test/test_cycle.c +++ b/packages/runtime/test/test_cycle.c @@ -536,6 +536,27 @@ static void check_mixed_dead_structure_narrows(void) { check(live <= narrowed, "a pass that freed a dead mature and old structure did not narrow the growth fraction"); } +/* scr_cyc_set_growth_cap sets the cap SCR_CYCLE_GROWTH_CAP sets, and a + * fraction already widened past it starts at it: after passes that found + * nothing widened the old level's fraction to the configured cap, a cap of + * 1 has the next dead cycle go once the heap has doubled past the floor. + * Restores the configured cap. */ +static void check_program_growth_cap(void) { + enum { FLOOR = 4096 }; + if (getenv("SCR_CYCLE_GROWTH_CAP") != NULL) return; /* it takes precedence */ + size_t slack = 2 * configured_nursery_threshold() + 2; + size_t live = 0; + for (int i = 0; i < 4; i++) live = grow_dead_cycle_until_full_pass(0, false); + check(live > FLOOR + FLOOR * configured_growth_cap(), + "passes that found nothing did not widen the growth fraction to the cap"); + scr_cyc_set_growth_cap(1); + for (int i = 0; i < 4; i++) { + live = grow_dead_cycle_until_full_pass(0, false); + check(live <= FLOOR + FLOOR + slack, "a program's growth cap of 1 did not bound the heap"); + } + scr_cyc_set_growth_cap(0); +} + /* The phase sweep (scr_collect_cycles_phase) is for a caller that knows a * phase's data has just died: it sweeps, and the growth schedule starts * over. After passes that find nothing have widened the old level's @@ -1500,6 +1521,7 @@ int main(void) { check_large_old_buffer_with_nursery_candidates(); check_unjudged_backlog_follows_growth(); check_acyclic_objects(); + check_program_growth_cap(); printf("cycle collection checks passed: threshold=%zu growth_cap=%zu\n", configured_nursery_threshold(), configured_growth_cap()); return 0; diff --git a/packages/runtime/test/test_dh.c b/packages/runtime/test/test_dh.c index aa8f175a3..f459f1047 100644 --- a/packages/runtime/test/test_dh.c +++ b/packages/runtime/test/test_dh.c @@ -199,7 +199,7 @@ static void test_generate(void) { mbedtls_mpi_lset(&g, 2); for (size_t i = 0; i < sizeof adds / sizeof adds[0]; i++) { for (int trial = 0; trial < 2; trial++) { - bool ok = sd_generate_safe_prime(&p, 512, adds[i], adds[i] - 1); + bool ok = sd_generate_safe_prime(&p, 512, adds[i], adds[i] - 1, false); mbedtls_mpi_uint r = 0; ok = ok && mbedtls_mpi_bitlen(&p) == 512 && mbedtls_mpi_mod_int(&r, &p, adds[i]) == 0 && r == adds[i] - 1; mbedtls_mpi_copy(&q, &p); @@ -217,12 +217,64 @@ static void test_generate(void) { mbedtls_mpi_free(&g); } +/* generatePrime's search: the sizes and add forms with one possible + * result, top-two-bit safe-prime starts, and checks above OpenSSL's + * round count. */ +static void test_generate_prime(void) { + static const struct { + size_t bits; + bool safe; + int add, rem; + int want; + } cases[] = { + {2, false, 0, 0, 3}, {3, false, 0, 0, 7}, {4, false, 0, 0, 13}, {3, true, 0, 0, 7}, + {6, true, 0, 0, 59}, {7, true, 0, 0, 107}, {8, false, 200, 1, 401}, {8, false, 128, 0, 257}, + {5, false, 16, 3, 19}, + }; + mbedtls_mpi p, add, rem, q; + mbedtls_mpi_init(&p); + mbedtls_mpi_init(&add); + mbedtls_mpi_init(&rem); + mbedtls_mpi_init(&q); + for (size_t i = 0; i < sizeof cases / sizeof cases[0]; i++) { + mbedtls_mpi_lset(&add, cases[i].add); + mbedtls_mpi_lset(&rem, cases[i].rem); + bool ok = sd_generate_prime(&p, cases[i].bits, cases[i].safe, cases[i].add ? &add : NULL, + cases[i].rem ? &rem : NULL) && + mbedtls_mpi_cmp_int(&p, cases[i].want) == 0; + char what[96]; + snprintf(what, sizeof what, "generatePrime(%zu, safe %d, add %d, rem %d) is %d", cases[i].bits, cases[i].safe, + cases[i].add, cases[i].rem, cases[i].want); + check(ok, what); + } + for (int trial = 0; trial < 2; trial++) { + bool ok = sd_generate_safe_prime(&p, 128, 12, 11, true) && mbedtls_mpi_bitlen(&p) == 128 && + mbedtls_mpi_get_bit(&p, 126) == 1; + mbedtls_mpi_copy(&q, &p); + mbedtls_mpi_shift_r(&q, 1); + ok = ok && mbedtls_mpi_is_prime_ext(&p, 40, sd_random, NULL) == 0 && + mbedtls_mpi_is_prime_ext(&q, 40, sd_random, NULL) == 0; + check(ok, "a 128-bit safe prime with its top two bits set"); + } + ScDhSmallPrimes *small = sd_small_primes(); + mbedtls_mpi_read_string(&q, 10, "3215031751"); + check(!sd_is_prime_checks(small, &q, 200), "200 rounds reject a strong pseudoprime to 2, 3, 5 and 7"); + mbedtls_mpi_read_string(&q, 10, "2305843009213693951"); + check(sd_is_prime_checks(small, &q, 200), "200 rounds accept 2^61 - 1"); + free(small); + mbedtls_mpi_free(&p); + mbedtls_mpi_free(&add); + mbedtls_mpi_free(&rem); + mbedtls_mpi_free(&q); +} + int main(void) { test_mont_pow(); test_is_prime(); test_check_params(); test_long_exponent(); test_generate(); + test_generate_prime(); fprintf(stderr, "%ld/%ld cases passed\n", total - failed, total); return failed ? 1 : 0; } diff --git a/packages/runtime/test/test_map.c b/packages/runtime/test/test_map.c index 32bdb7be1..46805da1e 100644 --- a/packages/runtime/test/test_map.c +++ b/packages/runtime/test/test_map.c @@ -826,11 +826,96 @@ static void test_absent_key_filter(void) { for (int i = 0; i < UNIVERSE; i++) scr_str_release(names[i]); } +/* SCR_MAP_VAL_CREF: string keys with compact class values in 16-byte + * entries (a 32-bit hash, the value as an address over 8). The values here + * are stand-in addresses, never dereferenced. */ +static uint64_t cref_val(size_t i) { return (uint64_t)(i + 1) * 8; } + +static void test_compact_values(void) { + ScrMap *m = scr_map_new(SCR_MAP_KEY_STR, SCR_MAP_VAL_CREF, NULL); + enum { N = 2000 }; + char buf[32]; + ScrStr *keys[N]; + for (size_t i = 0; i < N; i++) { + snprintf(buf, sizeof buf, "k%zu", i); + keys[i] = S(buf); + scr_map_set_str_ref(m, keys[i], (void *)(uintptr_t)cref_val(i)); + } + check(scr_map_size(m) == N, "cref: size after inserts"); + bool all = true; + for (size_t i = 0; i < N; i++) { + all &= scr_map_get_str_ref(m, keys[i]) == (void *)(uintptr_t)cref_val(i); + const ScrMapCEntry *e = scr_map_entry_str_c(m, keys[i]); + all &= e && ((uint64_t)e->val << 3) == cref_val(i) && e->key == (uint64_t)(uintptr_t)keys[i]; + } + check(all, "cref: get and entry_str_c find every value"); + ScrStr *missing = S("absent"); + check(!scr_map_get_str_ref(m, missing) && !scr_map_entry_str_c(m, missing), "cref: absent key"); + check(scr_map_get_span_ref(m, "k7", 2) == (void *)(uintptr_t)cref_val(7), "cref: span lookup"); + /* A key object other than the stored one, equal bytes. */ + ScrStr *again = S("k42"); + check(scr_map_entry_str_c(m, again) && scr_map_get_str_ref(m, again) == (void *)(uintptr_t)cref_val(42), + "cref: equal key bytes, other object"); + scr_str_release(again); + for (size_t i = 0; i < N; i += 2) scr_map_set_str_ref(m, keys[i], (void *)(uintptr_t)cref_val(i + N)); + all = true; + for (size_t i = 0; i < N; i++) + all &= scr_map_get_str_ref(m, keys[i]) == (void *)(uintptr_t)cref_val(i % 2 ? i : i + N); + check(all && scr_map_size(m) == N, "cref: overwrite keeps the entry"); + for (size_t i = 0; i < N; i++) + if (i % 4) scr_map_delete_str(m, keys[i]); + check(scr_map_size(m) == N / 4, "cref: deletes"); + all = true; + for (size_t i = 0; i < N; i++) { + bool want = i % 4 == 0; + all &= scr_map_has_str(m, keys[i]) == want; + if (want) all &= scr_map_get_str_ref(m, keys[i]) == (void *)(uintptr_t)cref_val(i + N); + } + check(all, "cref: lookups after compaction"); + /* Iteration order and values. */ + size_t seen = 0; + all = true; + scr_map_iter_enter(m); + for (double i = scr_map_iter_start(m); i < scr_map_iter_count(m); i = scr_map_iter_skip(m, i + 1)) { + ScrStr *k = scr_map_iter_key_str(m, i); + void *v = scr_map_iter_val_ref(m, i); + size_t want = seen * 4; + all &= k->len == keys[want]->len && memcmp(k->data, keys[want]->data, k->len) == 0; + all &= v == (void *)(uintptr_t)cref_val(want + N); + scr_str_release(k); + seen++; + } + scr_map_iter_exit(m); + check(all && seen == N / 4, "cref: iteration order and values"); + /* Copies keep the layout; a key-only copy takes the 24-byte one. */ + ScrMap *copy = scr_map_clone(m, false, NULL); + ScrMap *set = scr_map_clone(m, true, NULL); + all = scr_map_size(copy) == N / 4 && scr_map_size(set) == N / 4; + for (size_t i = 0; i < N; i += 4) { + all &= scr_map_get_str_ref(copy, keys[i]) == (void *)(uintptr_t)cref_val(i + N); + all &= scr_map_has_str(set, keys[i]); + } + all &= !scr_map_has_str(set, keys[1]); + check(all, "cref: clone and key-only clone"); + scr_map_set_str_ref(copy, keys[1], (void *)(uintptr_t)cref_val(1)); + check(!scr_map_has_str(m, keys[1]) && scr_map_size(copy) == N / 4 + 1, "cref: copies are independent"); + scr_map_clear(m); + check(scr_map_size(m) == 0 && !scr_map_has_str(m, keys[0]), "cref: clear"); + scr_map_set_str_ref(m, keys[3], (void *)(uintptr_t)cref_val(3)); + check(scr_map_get_str_ref(m, keys[3]) == (void *)(uintptr_t)cref_val(3), "cref: reuse after clear"); + scr_map_release(copy); + scr_map_release(set); + scr_map_release(m); + scr_str_release(missing); + for (size_t i = 0; i < N; i++) scr_str_release(keys[i]); +} + int main(void) { test_key_bytes(); test_string_bucket_collisions(); test_string_keys(); test_string_entries(); + test_compact_values(); test_same_value_zero(); test_span_keys(); test_scratch_keys(); diff --git a/packages/threads/package.json b/packages/threads/package.json index 2aeeb7cbb..11b8ef624 100644 --- a/packages/threads/package.json +++ b/packages/threads/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/threads", - "version": "0.4.1", + "version": "0.4.2", "description": "Zero-copy publication of immutable object graphs to worker threads (scriptc), with a deep-freezing Node.js implementation", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/types/package.json b/packages/types/package.json index ffbbdf4a4..689c7b72f 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/types", - "version": "0.4.1", + "version": "0.4.2", "description": "Contract types for scriptc: type-only annotations (Int32, NoThrow, Immutable) that scriptc checks and optimizes, with no runtime semantics", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 589de2a51..525994ba1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -110,8 +110,8 @@ importers: specifier: workspace:* version: link:../runtime tsc-ts: - specifier: 0.0.7 - version: 0.0.7 + specifier: 0.0.8 + version: 0.0.8 typescript: specifier: 7.0.2 version: 7.0.2 @@ -1360,8 +1360,8 @@ packages: cpu: [x64] os: [win32] - '@scriptc/threads@0.4.1': - resolution: {integrity: sha512-wKgEBiVc07ZIGNFGwJpV5JbTXJN1N42qG6/g7ipnwt3klsU0nuWH2ToKV9vYH+RQstF7+twq6zMCf2qhUloTHQ==} + '@scriptc/threads@0.4.2': + resolution: {integrity: sha512-nR9FUxpOpgtTmJu6dejttElBL+JJTAZzJ2DZM+eNoujmLx9VpdlAVdgj3cu/MDsTUg/1sIUBq1ME1csC/Aa6yg==} engines: {node: '>=24.0.0'} '@sinclair/typebox@0.25.24': @@ -1377,42 +1377,42 @@ packages: '@ts-morph/common@0.11.1': resolution: {integrity: sha512-7hWZS0NRpEsNV8vWJzg7FEz6V8MaLNeJOmwmghqUXTpzk16V1LLZhdo+4QvE/+zv4cVci0OviuJFnqhEfoV3+g==} - '@tsc-ts/darwin-arm64@0.0.7': - resolution: {integrity: sha512-nIaTJKBpG6n60xxRYfBlh02G6aSO2punIvj4qPy8/CXZu0Kw4ra/J3PYJtrtYMIh/eIGo4+f1Q01v5dFPPwZbw==} + '@tsc-ts/darwin-arm64@0.0.8': + resolution: {integrity: sha512-+hB9yWk58lwZHGrVEzqEC4D7MQW7Vd2z7blV3TULIKZPTMwdf8aT7XFAmfP48PoGff0H9yuQKyRXJl9I+sQv9w==} cpu: [arm64] os: [darwin] - '@tsc-ts/darwin-x64@0.0.7': - resolution: {integrity: sha512-8ITDyF3FG4SYeS1cV4+oRpbbk0jVsfd23Ww138wbQymbOLYjWPafzj4bymxnIE5kEWjZPVqTFDbaYqgWwNStag==} + '@tsc-ts/darwin-x64@0.0.8': + resolution: {integrity: sha512-yz4UddicqMXe+T7nV7ftpf155lkiK6gIPNwSB8rH9dlwiV6a17CO/vpSk627QLMqnfGNicB6YwLMk/SJ9moPdw==} cpu: [x64] os: [darwin] - '@tsc-ts/linux-arm64-gnu@0.0.7': - resolution: {integrity: sha512-nXGjtbM3YrARWOb+nkDaFdhKGSzPf3ofl/gV4x8JnVQ8SgK4IWJ5J0u93sk3mTYgdLj/Gcobp/D3lZ6WIzjKvA==} + '@tsc-ts/linux-arm64-gnu@0.0.8': + resolution: {integrity: sha512-LHqfT9mQ4tusPdWmnanCvC8/YBOF5bHM5h5863a3t2HTgFVdps1Qwb0yzrW5DDbojIhQ0Z+GupxtXVUQcV0Q/Q==} cpu: [arm64] os: [linux] libc: [glibc] - '@tsc-ts/linux-arm64-musl@0.0.7': - resolution: {integrity: sha512-hSjpQsOhrLaDsbvEO5EH9nC3Vy0WAt3aB+GejytEh9fl4F3kT0BasbBP4wJqu6S5SG9upLW7ZClXiNVnGYDGlQ==} + '@tsc-ts/linux-arm64-musl@0.0.8': + resolution: {integrity: sha512-I0CC3xZMwoQM6OiVsubRHb0VI/RrJpneb33kDQhadYHq3taPos71KwP+CbCXNu88rNsDsyn7D1oCVfJb73g4Mw==} cpu: [arm64] os: [linux] libc: [musl] - '@tsc-ts/linux-x64-gnu@0.0.7': - resolution: {integrity: sha512-MCU2MFefQGG/++nepg09BtYnsN2mVzeYfIS6GW30ChxUt2nJUMkEvzbDim9czDevW4LRqHgfnwEt+hs2ud1SDQ==} + '@tsc-ts/linux-x64-gnu@0.0.8': + resolution: {integrity: sha512-PLERuwUuxT92x0BZAMQubiQP5jhlqoLmswaZWqcvfKAZyBp0TbWiVOmuNnDbJozvBaNUrfdGPb9suiQ+Me7M6A==} cpu: [x64] os: [linux] libc: [glibc] - '@tsc-ts/linux-x64-musl@0.0.7': - resolution: {integrity: sha512-abTZZkn74772MPABStuXWJ1GHI07nbEf4a3Nwl7NYI5mLFYxeLSbq6+cGh0BUJeBeMhFNsjtQ5/j5sGmsrJ2tg==} + '@tsc-ts/linux-x64-musl@0.0.8': + resolution: {integrity: sha512-zDl36l1uyWc0mQniOAMZmkVD9TrlFExHlDT0fL8JGD4XLgx+bRe+To38yP0KSBehXBzjDNn8xrEoJ+3RP6mYwQ==} cpu: [x64] os: [linux] libc: [musl] - '@tsc-ts/win32-x64-msvc@0.0.7': - resolution: {integrity: sha512-EOhLTkuWYytJYklR5BI1jRtTpEIPh1FQJKD8R37qqlcnPrELlRO6Nc2ncDpdCoYbpN+KrewIVPB3xD4o3VGUaw==} + '@tsc-ts/win32-x64-msvc@0.0.8': + resolution: {integrity: sha512-9gO+pFoLiIxITlAE2P6JL8ihZwns7z/2Raw5+38RqLzHmnSsRN47+H4p/4EqQsLW5/TTsAo9uSeSkf3EuRg+XA==} cpu: [x64] os: [win32] @@ -2577,8 +2577,8 @@ packages: ts-toolbelt@6.15.5: resolution: {integrity: sha512-FZIXf1ksVyLcfr7M317jbB67XFJhOO1YqdTcuGaq9q5jLUoTikukZ+98TPjKiP2jC5CgmYdWWYs0s2nLSU0/1A==} - tsc-ts@0.0.7: - resolution: {integrity: sha512-KRWTXRUvwMXi4At6xOWPDa/0BreY3MR2tPHooEMNWLZUA2eLphNe7PJiHclPLLn9RCnHY4ffDTTeF5hqgyBvtA==} + tsc-ts@0.0.8: + resolution: {integrity: sha512-EiyDziYOryBg/YEvev/mKCn+hD4M6VFjH7VP9nv2WadDKYaPrdb+a3hP+wYr/UsDWNE7AdL7YGnjZppeXlXy6g==} engines: {node: '>=20'} hasBin: true @@ -3439,7 +3439,7 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.62.2': optional: true - '@scriptc/threads@0.4.1': {} + '@scriptc/threads@0.4.2': {} '@sinclair/typebox@0.25.24': {} @@ -3454,25 +3454,25 @@ snapshots: mkdirp: 1.0.4 path-browserify: 1.0.1 - '@tsc-ts/darwin-arm64@0.0.7': + '@tsc-ts/darwin-arm64@0.0.8': optional: true - '@tsc-ts/darwin-x64@0.0.7': + '@tsc-ts/darwin-x64@0.0.8': optional: true - '@tsc-ts/linux-arm64-gnu@0.0.7': + '@tsc-ts/linux-arm64-gnu@0.0.8': optional: true - '@tsc-ts/linux-arm64-musl@0.0.7': + '@tsc-ts/linux-arm64-musl@0.0.8': optional: true - '@tsc-ts/linux-x64-gnu@0.0.7': + '@tsc-ts/linux-x64-gnu@0.0.8': optional: true - '@tsc-ts/linux-x64-musl@0.0.7': + '@tsc-ts/linux-x64-musl@0.0.8': optional: true - '@tsc-ts/win32-x64-msvc@0.0.7': + '@tsc-ts/win32-x64-msvc@0.0.8': optional: true '@tybys/wasm-util@0.10.3': @@ -4813,17 +4813,17 @@ snapshots: ts-toolbelt@6.15.5: {} - tsc-ts@0.0.7: + tsc-ts@0.0.8: dependencies: - '@scriptc/threads': 0.4.1 + '@scriptc/threads': 0.4.2 optionalDependencies: - '@tsc-ts/darwin-arm64': 0.0.7 - '@tsc-ts/darwin-x64': 0.0.7 - '@tsc-ts/linux-arm64-gnu': 0.0.7 - '@tsc-ts/linux-arm64-musl': 0.0.7 - '@tsc-ts/linux-x64-gnu': 0.0.7 - '@tsc-ts/linux-x64-musl': 0.0.7 - '@tsc-ts/win32-x64-msvc': 0.0.7 + '@tsc-ts/darwin-arm64': 0.0.8 + '@tsc-ts/darwin-x64': 0.0.8 + '@tsc-ts/linux-arm64-gnu': 0.0.8 + '@tsc-ts/linux-arm64-musl': 0.0.8 + '@tsc-ts/linux-x64-gnu': 0.0.8 + '@tsc-ts/linux-x64-musl': 0.0.8 + '@tsc-ts/win32-x64-msvc': 0.0.8 tslib@2.8.1: optional: true diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ceb1506e9..e74d40442 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -7,15 +7,15 @@ minimumReleaseAgeExclude: - "effect@4.0.0" - "@effect/platform-node@4.0.0" - "@effect/platform-node-shared@4.0.0" - - "@tsc-ts/darwin-arm64@0.0.7" - - "@tsc-ts/darwin-x64@0.0.7" - - "@tsc-ts/linux-arm64-gnu@0.0.7" - - "@tsc-ts/linux-arm64-musl@0.0.7" - - "@tsc-ts/linux-x64-gnu@0.0.7" - - "@tsc-ts/linux-x64-musl@0.0.7" - - "@tsc-ts/win32-x64-msvc@0.0.7" - - "tsc-ts@0.0.7" - - "@scriptc/threads@0.4.1" + - "@tsc-ts/darwin-arm64@0.0.8" + - "@tsc-ts/darwin-x64@0.0.8" + - "@tsc-ts/linux-arm64-gnu@0.0.8" + - "@tsc-ts/linux-arm64-musl@0.0.8" + - "@tsc-ts/linux-x64-gnu@0.0.8" + - "@tsc-ts/linux-x64-musl@0.0.8" + - "@tsc-ts/win32-x64-msvc@0.0.8" + - "tsc-ts@0.0.8" + - "@scriptc/threads@0.4.2" allowBuilds: esbuild: true overrides: diff --git a/scripts/gen-dyn-methods.mjs b/scripts/gen-dyn-methods.mjs new file mode 100644 index 000000000..fe4f81a90 --- /dev/null +++ b/scripts/gen-dyn-methods.mjs @@ -0,0 +1,101 @@ +#!/usr/bin/env node +/* Writes packages/runtime/src/scr_dyn_methods.h: the method-name ids the + * checked-dynamic dispatch in scr_dyn_invoke.c compares (DYN_M_), the + * names, and an open-addressed hash table mapping a name to its id. The ids + * are the DYN_M_ names scr_dyn_invoke.c uses, so the header follows the + * dispatch code. + * + * node scripts/gen-dyn-methods.mjs write the header + * node scripts/gen-dyn-methods.mjs --check fail when it is out of date */ +import { readFileSync, writeFileSync } from "node:fs"; + +const source = new URL("../packages/runtime/src/scr_dyn_invoke.c", import.meta.url); +const target = new URL("../packages/runtime/src/scr_dyn_methods.h", import.meta.url); +const SLOTS = 256; + +const used = new Set(); +for (const match of readFileSync(source, "utf8").matchAll(/\bDYN_M_([A-Za-z][A-Za-z0-9]*)\b/g)) + if (match[1] !== "NONE" && match[1] !== "COUNT") used.add(match[1]); +const names = [...used].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)); +if (names.length >= SLOTS / 2) throw new Error("too many method names for the hash table"); + +// The slot dyn_method_of in the header computes. +const slotOf = (name) => + (name.length * 7 + name.charCodeAt(0) * 31 + name.charCodeAt(name.length - 1)) & (SLOTS - 1); +const slots = new Array(SLOTS).fill(0); +names.forEach((name, i) => { + let slot = slotOf(name); + while (slots[slot] !== 0) slot = (slot + 1) & (SLOTS - 1); + slots[slot] = i + 1; +}); + +const rows = (values, width) => { + const out = []; + for (let i = 0; i < values.length; i += width) + out.push(` ${values.slice(i, i + width).join(", ")},`); + return out; +}; +const text = [ + "/* Generated by scripts/gen-dyn-methods.mjs from the DYN_M_ ids scr_dyn_invoke.c", + " * uses. Do not edit by hand. */", + "#ifndef SCR_DYN_METHODS_H", + "#define SCR_DYN_METHODS_H", + "", + "#include ", + "#include ", + "", + "enum {", + " DYN_M_NONE,", + ...names.map((name) => ` DYN_M_${name},`), + " DYN_M_COUNT", + "};", + "", + "static const char *const dyn_method_names[DYN_M_COUNT] = {", + " NULL,", + ...names.map((name) => ` ${JSON.stringify(name)},`), + "};", + "", + "static const unsigned char dyn_method_lengths[DYN_M_COUNT] = {", + ...rows([0, ...names.map((name) => name.length)], 16), + "};", + "", + `/* Each name's id at its slot (or the next free one); 0 ends a probe. */`, + `static const unsigned char dyn_method_slots[${SLOTS}] = {`, + ...rows(slots, 16), + "};", + "", + `#define DYN_METHOD_MAX_LENGTH ${Math.max(...names.map((name) => name.length))}`, + "", + "/* The id of a method name, compared by content; DYN_M_NONE for any other", + " * name. */", + "static inline int dyn_method_of(const char *m) {", + " size_t length = strlen(m);", + " if (length == 0 || length > DYN_METHOD_MAX_LENGTH) return DYN_M_NONE;", + " unsigned slot = (unsigned)(length * 7 + (unsigned char)m[0] * 31 +", + ` (unsigned char)m[length - 1]) & ${SLOTS - 1};`, + " for (;;) {", + " unsigned id = dyn_method_slots[slot];", + " if (id == 0) return DYN_M_NONE;", + " if (dyn_method_lengths[id] == length && memcmp(m, dyn_method_names[id], length) == 0) return (int)id;", + ` slot = (slot + 1) & ${SLOTS - 1};`, + " }", + "}", + "", + "#endif", + "", +].join("\n"); + +if (process.argv.includes("--check")) { + let current = ""; + try { + current = readFileSync(target, "utf8"); + } catch {} + if (current !== text) { + console.error( + "packages/runtime/src/scr_dyn_methods.h is out of date: run node scripts/gen-dyn-methods.mjs", + ); + process.exit(1); + } +} else { + writeFileSync(target, text); +} diff --git a/scripts/generate-ts7-ast-schema.mjs b/scripts/generate-ts7-ast-schema.mjs index 07af474de..3218c8962 100644 --- a/scripts/generate-ts7-ast-schema.mjs +++ b/scripts/generate-ts7-ast-schema.mjs @@ -282,6 +282,26 @@ const adaptedGetters = new Set([ "operand", "questionToken", ]); +// A getter selects its child's wire slot by kind directly: an integer +// switch, without astChildOrder's switch on the child's name. `answer` +// maps a slot to the returned expression; `missing` is returned for kinds +// without the child. +const slotCases = (name, answer, missing) => { + const kindsBySlot = new Map(); + for (const [kind, childNames] of Object.entries(protocol.childProperties)) { + const slot = childNames.indexOf(name); + if (slot < 0) continue; + if (!kindsBySlot.has(slot)) kindsBySlot.set(slot, []); + kindsBySlot.get(slot).push(kind); + } + const lines = [" switch (this.kind) {"]; + for (const [slot, kinds] of [...kindsBySlot].sort((a, b) => a[0] - b[0])) { + for (const kind of kinds) lines.push(` case ${kind}:`); + lines.push(` return ${answer(slot)};`); + } + lines.push(" default:", ` return ${missing};`, " }"); + return lines.join("\n"); +}; const getters = names .filter((name) => !adaptedGetters.has(name)) .map((name) => { @@ -292,12 +312,19 @@ const getters = names .replaceAll("RemoteNode", "AstNode"); const method = type.includes("AstNode |") && type.includes("AstNodeList") - ? "child" + ? "childAt" : type.includes("AstNodeList") - ? "childList" - : "childNode"; - return ` get ${name}(): ${type} {\n return this.${method}(${JSON.stringify(name)});\n }`; + ? "childListAt" + : "childNodeAt"; + return ` get ${name}(): ${type} {\n${slotCases(name, (slot) => `this.${method}(${slot})`, "undefined")}\n }`; }); +// The adapted getters read their raw child through the same slot switch. +const adaptedSlots = [...adaptedGetters] + .sort() + .map( + (name) => + ` private ${name}Slot(): number {\n${slotCases(name, (slot) => String(slot), "-1")}\n }`, + ); const nodeTarget = join(root, "packages/compiler/src/frontend/ts7/ast-node.ts"); const before = readFileSync(nodeTarget, "utf8"); if ( @@ -311,6 +338,7 @@ const after = before.replace( " // BEGIN GENERATED CHILD GETTERS", ` // Generated from typescript@${version}; run scripts/generate-ts7-ast-schema.mjs.`, ...getters, + ...adaptedSlots, " // END GENERATED CHILD GETTERS", ].join("\n"), ); diff --git a/scripts/split/README.md b/scripts/split/README.md new file mode 100644 index 000000000..be8c948ac --- /dev/null +++ b/scripts/split/README.md @@ -0,0 +1,24 @@ +# Split tools + +These scripts split a large source file into smaller files without changing behavior, and carry open branches across the split. Each transform is deterministic: the same config gives the same result on any version of the tree, and rehoming depends on that. + +Run them from a checkout with `node_modules` installed. That checkout is the tools directory (`--tools`) and supplies `typescript` and `oxfmt`. + +## Transforms + +- `split-module.mjs plan|apply ` moves top-level declarations into new files. `plan` computes a manifest from seed declarations, and `apply` does the move. +- `split-methods.mjs apply ` moves class methods into exported functions that take the instance as a parameter (`emitter`). The class keeps a one-line stub with the same signature, so callers don't change. +- `move-state.mjs apply ` moves a group of class fields into a feature's state object, defined in the feature's file with `defineModuleState` (`packages/compiler/src/backend/llvm/emitter-state.ts`). It rewrites `this.` and `emitter.` to `KEY.of(this).` and creates files from templates. It also applies the change's other edits as literal replacements, in order. A replacement whose text isn't found the expected number of times is reported and skipped. + +Each transform takes `--repo

` (the tree to rewrite) and `--tools `, and formats what it writes with the tools checkout's oxfmt and its `.oxfmtrc.jsonc` (`format.mjs`). The formatter reads no configuration from `--repo`, so an `oxfmt.config.ts` a branch commits is never run. The tools read and write the tree's files only at real paths inside `--repo`, and a config's templates and manifest only inside the config's directory. A path that resolves elsewhere, through a symlink or `..`, stops the tool with an error (`confine.mjs`). `configs/` holds the configs of splits that have landed. For example, `configs/loop-state/` moves the loop optimizations' state out of `LlEmitter`. + +## Rehoming an open branch + +```sh +git fetch origin pull//head:pr- +node scripts/split/rehome-split.mjs --branch pr- --onto origin/main --config scripts/split/configs//.json +``` + +The result is the branch `pr--rehomed`, a merge of `origin/main` into the branch. + +The script applies the split's transform to the branch's merge base and to its tip, then cherry-picks the difference onto `--onto`. It records the result as a merge commit with parents (branch tip, `--onto`), so it can be pushed without rewriting the branch; `--squash` makes one ordinary commit instead. Conflicts made only of import lines in the split's files resolve to both sides' imports. Any other conflict stops the script, which lists the files and leaves its scratch worktree for you to resolve. diff --git a/scripts/split/configs/loop-state/emitter-state.template b/scripts/split/configs/loop-state/emitter-state.template new file mode 100644 index 000000000..62dc47b4e --- /dev/null +++ b/scripts/split/configs/loop-state/emitter-state.template @@ -0,0 +1,24 @@ +/* Per-feature emitter state. A feature defines its state in its own file + * with defineModuleState and reads it through the returned key, so adding + * or extending a feature does not edit LlEmitter's field list or + * constructor. */ +import type { LlEmitter } from "./emitter.js"; + +/** A feature's state on an emitter, read with `key.of(emitter)`. */ +export interface EmitterStateKey { + of(emitter: LlEmitter): T; +} + +let moduleSlots = 0; + +/** State that lives for the whole module, created on first read. */ +export function defineModuleState(create: (emitter: LlEmitter) => T): EmitterStateKey { + const slot = moduleSlots++; + return { + of(emitter) { + const states = emitter.moduleStates; + if (slot >= states.length || states[slot] === undefined) states[slot] = create(emitter); + return states[slot] as T; + }, + }; +} diff --git a/scripts/split/configs/loop-state/loop-state.config.json b/scripts/split/configs/loop-state/loop-state.config.json new file mode 100644 index 000000000..e749a4b39 --- /dev/null +++ b/scripts/split/configs/loop-state/loop-state.config.json @@ -0,0 +1,29 @@ +{ + "module": "packages/compiler/src/backend/llvm/emitter.ts", + "class": "LlEmitter", + "dir": "packages/compiler/src/backend/llvm", + "create": { + "emitter-state.ts": "emitter-state.template", + "loop-state.ts": "loop-state.template" + }, + "groups": [ + { + "file": "loop-state.ts", + "key": "LOOPS", + "interfaceName": "LoopState", + "fields": ["unaliasedArrays", "denseLoopsEnabled", "denseLoopEntries", "denseLoop", "countedLoopsEnabled", "byteWindowsEnabled", "byteWindowEntries", "bytesLoopVersionsEnabled", "bytesLoopVersioning", "bytesLoopEntries", "bytesLoopFunction", "provenCountedGuards"] + } + ], + "replacements": [ + { "file": "emitter.ts", "find": "export class LlEmitter {\n", "replace": "export class LlEmitter {\n /** Per-feature state (emitter-state.ts), indexed by each key's slot. */\n readonly moduleStates: unknown[] = [];\n" }, + { "file": "emitter.ts", "find": "import { type DenseArrayLoop } from \"./dense-array-loops.js\";\n", "replace": "" }, + { "file": "emitter.ts", "find": " const initialized = LOOPS.of(this).bytesLoopEntries.get(s);\n", "replace": " const initialized = LOOPS.of(this).bytesLoopEntries!.get(s);\n" }, + { "file": "emit-statements.ts", "find": " ? LOOPS.of(emitter).denseLoop.range\n", "replace": " ? LOOPS.of(emitter).denseLoop!.range\n", "count": 2 }, + { "file": "emit-function.ts", "find": " LOOPS.of(emitter).unaliasedArrays = findUnaliasedLocalArrays(fn);\n", "replace": "" }, + { "file": "emit-function.ts", "find": " LOOPS.of(emitter).countedLoopsEnabled = emitter.debug === null && !fn.async && !fn.generator;\n", "replace": " beginFunctionLoops(emitter, fn, numericFn);\n" }, + { "file": "emit-function.ts", "find": " LOOPS.of(emitter).byteWindowsEnabled = LOOPS.of(emitter).countedLoopsEnabled;\n LOOPS.of(emitter).bytesLoopVersionsEnabled = LOOPS.of(emitter).countedLoopsEnabled;\n LOOPS.of(emitter).bytesLoopEntries = null;\n LOOPS.of(emitter).bytesLoopFunction = fn;\n LOOPS.of(emitter).denseLoopsEnabled = LOOPS.of(emitter).countedLoopsEnabled;\n LOOPS.of(emitter).denseLoop = null;\n LOOPS.of(emitter).denseLoopEntries =\n LOOPS.of(emitter).denseLoopsEnabled && numericFn.locals.some((l) => l.type.kind === \"array\")\n ? findInitializedByteLoopBindings(fn)\n : new Map();\n LOOPS.of(emitter).byteWindowEntries =\n LOOPS.of(emitter).byteWindowsEnabled && numericFn.locals.some((l) => l.type.kind === \"bytes\")\n ? findInitializedByteLoopBindings(fn)\n : new Map();\n", "replace": "" }, + { "file": "emit-function.ts", "find": "import { LOOPS } from \"./loop-state.js\";\n", "replace": "import { beginFunctionLoops, LOOPS } from \"./loop-state.js\";\n" }, + { "file": "emit-function.ts", "find": "import { findUnaliasedLocalArrays } from \"../../ir/array-length-stability.js\";\n", "replace": "" }, + { "file": "emit-function.ts", "find": "import { findInitializedByteLoopBindings } from \"./byte-windows.js\";\n", "replace": "" } + ] +} diff --git a/scripts/split/configs/loop-state/loop-state.template b/scripts/split/configs/loop-state/loop-state.template new file mode 100644 index 000000000..ae9673267 --- /dev/null +++ b/scripts/split/configs/loop-state/loop-state.template @@ -0,0 +1,34 @@ +/* The loop optimizations' per-function state: counted loops, dense array + * loops, byte windows and typed-array loop versioning, and the unaliased + * local arrays they rely on. */ +import { findUnaliasedLocalArrays } from "../../ir/array-length-stability.js"; +import type { IrFunction, IrStmt } from "../../ir/ir.js"; +import { findInitializedByteLoopBindings } from "./byte-windows.js"; +import type { DenseArrayLoop } from "./dense-array-loops.js"; +import type { LlEmitter } from "./emitter.js"; + +/** Sets the loop state for `fn` at the start of its emission; `numericFn` + * is `fn` with its initializer bindings (emitFunction). */ +export function beginFunctionLoops( + emitter: LlEmitter, + fn: IrFunction, + numericFn: IrFunction, +): void { + const loops = LOOPS.of(emitter); + loops.unaliasedArrays = findUnaliasedLocalArrays(fn); + loops.countedLoopsEnabled = emitter.debug === null && !fn.async && !fn.generator; + loops.byteWindowsEnabled = loops.countedLoopsEnabled; + loops.bytesLoopVersionsEnabled = loops.countedLoopsEnabled; + loops.bytesLoopEntries = null; + loops.bytesLoopFunction = fn; + loops.denseLoopsEnabled = loops.countedLoopsEnabled; + loops.denseLoop = null; + loops.denseLoopEntries = + loops.denseLoopsEnabled && numericFn.locals.some((l) => l.type.kind === "array") + ? findInitializedByteLoopBindings(fn) + : new Map(); + loops.byteWindowEntries = + loops.byteWindowsEnabled && numericFn.locals.some((l) => l.type.kind === "bytes") + ? findInitializedByteLoopBindings(fn) + : new Map(); +} diff --git a/scripts/split/confine.mjs b/scripts/split/confine.mjs new file mode 100644 index 000000000..14af8b85a --- /dev/null +++ b/scripts/split/confine.mjs @@ -0,0 +1,89 @@ +/* Path confinement for the split tools. A branch they rewrite can commit + * symlinks, so every file a tool reads or writes goes through a guard for + * the directory it belongs to (the checkout being rewritten, or the + * config's directory). The guard resolves the path's real location, through + * every symlink on the way, and throws when that location is outside the + * directory's real path. A path that doesn't exist yet resolves through its + * nearest existing parent, so a write can't create a file through a + * symlinked directory or a dangling symlink either. */ +import { + existsSync, + lstatSync, + mkdirSync, + readdirSync, + readFileSync, + readlinkSync, + realpathSync, + writeFileSync, +} from "node:fs"; +import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path"; + +const isInside = (root, path) => { + const rel = relative(root, path); + return rel === "" || (rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel)); +}; + +/** The real location of `path`, which need not exist: the real path of its + * nearest existing entry joined with the rest. A dangling symlink resolves + * to where its target would be. */ +function realLocation(path, hops = 0) { + const rest = []; + for (let at = resolve(path); ; at = dirname(at)) { + let stat = null; + try { + stat = lstatSync(at); + } catch (e) { + if (e.code !== "ENOENT" && e.code !== "ENOTDIR") throw e; + } + if (stat) { + try { + return join(realpathSync(at), ...rest); + } catch (e) { + if (e.code !== "ENOENT" || !stat.isSymbolicLink() || hops > 40) throw e; + return realLocation(join(resolve(dirname(at), readlinkSync(at)), ...rest), hops + 1); + } + } + if (dirname(at) === at) return resolve(path); + rest.unshift(basename(at)); + } +} + +/** A guard for the files under `root`. */ +export function confine(root) { + const lexical = resolve(root); + const real = realpathSync(lexical); + const check = (path) => { + const location = realLocation(path); + if (!isInside(real, location)) + throw new Error(`refusing ${path}: it resolves to ${location}, outside ${real}`); + return location; + }; + return { + /** The path's real location; throws when it is outside the root. */ + check, + /** True when `path` names a file under the root (before symlinks). */ + contains: (path) => isInside(lexical, resolve(path)) || isInside(real, resolve(path)), + read: (path) => readFileSync(check(path), "utf8"), + write: (path, text) => writeFileSync(check(path), text), + exists: (path) => existsSync(check(path)), + mkdir: (path) => mkdirSync(check(path), { recursive: true }), + list: (path) => readdirSync(check(path)), + }; +} + +/** Routes a TypeScript host's (or `ts.sys`-like object's) file reads under + * the guard's root through the guard; other files (the TypeScript libraries) + * read as before. */ +export function confineHost(host, guard) { + const checked = + (read) => + (fileName, ...rest) => { + if (guard.contains(fileName)) guard.check(fileName); + return read.call(host, fileName, ...rest); + }; + return { + ...host, + ...(host.getSourceFile ? { getSourceFile: checked(host.getSourceFile) } : {}), + readFile: checked(host.readFile), + }; +} diff --git a/scripts/split/format.mjs b/scripts/split/format.mjs new file mode 100644 index 000000000..10c1fbc8a --- /dev/null +++ b/scripts/split/format.mjs @@ -0,0 +1,20 @@ +/* Formatting for the split tools. oxfmt reads its configuration from the + * directory it runs in, its parents and the formatted files' directories, + * and an oxfmt.config.ts there is code it runs. The tools format files in + * a checkout they rewrite, which can be a pull request's, so they pass the + * tools checkout's own configuration and turn the nested lookup off: no + * configuration file of the rewritten checkout is read or run. */ +import { execFileSync } from "node:child_process"; +import { join } from "node:path"; + +/** Formats `files` (paths relative to `cwd`) with the tools checkout's + * oxfmt and configuration. */ +export function formatFiles(toolsDir, files, { cwd, stdio }) { + // With no file arguments oxfmt would format the whole directory. + if (files.length === 0) return; + execFileSync( + join(toolsDir, "node_modules/.bin/oxfmt"), + ["--config", join(toolsDir, ".oxfmtrc.jsonc"), "--disable-nested-config", ...files], + { cwd, stdio }, + ); +} diff --git a/scripts/split/move-state.mjs b/scripts/split/move-state.mjs new file mode 100644 index 000000000..71306f5d1 --- /dev/null +++ b/scripts/split/move-state.mjs @@ -0,0 +1,206 @@ +#!/usr/bin/env node +/* Move a group of LlEmitter fields into a feature's state object, as a + * deterministic rewrite (the same config gives the same result on any + * version of the tree, which the rehoming tool relies on). + * + * node move-state.mjs apply [--repo ] [--tools ] + * + * : + * { "module": "packages/compiler/src/backend/llvm/emitter.ts", + * "class": "LlEmitter", + * "dir": "packages/compiler/src/backend/llvm", + * "groups": [{ "file": "loop-state.ts", "key": "LOOPS", + * "fields": ["denseLoop", ...] }], + * "create": { "loop-state.ts": "loop-state.template.ts" }, + * "replacements": [{ "file": "emit-function.ts", "find": "...", "replace": "...", "count": 1 }], + * "receivers": ["this", "emitter"] } + * + * For each group: the fields' declarations leave the class (their doc + * comments and initializers move into the state object's interface and + * factory in the group's file), every `this.` and `emitter.` + * (or the config's receivers) in the directory's sources becomes + * `.of(this).` / `.of(emitter).`, and each + * rewritten file imports the key. + * The group's file must already exist and hold everything else (header, + * setup function); the tool appends the interface and key to it. */ +import { readFileSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { createRequire } from "node:module"; +import { confine } from "./confine.mjs"; +import { formatFiles } from "./format.mjs"; + +const [command, configPath, ...flags] = process.argv.slice(2); +if (command !== "apply") throw new Error("usage: move-state.mjs apply "); +const config = JSON.parse(readFileSync(configPath, "utf8")); +const flag = (name) => { + const i = flags.indexOf(name); + return i >= 0 ? flags[i + 1] : undefined; +}; +const repo = resolve(flag("--repo") ?? process.cwd()); +const toolsDir = resolve(flag("--tools") ?? process.cwd()); +const require = createRequire(join(toolsDir, "package.json")); +const ts = require("typescript"); +const modulePath = join(repo, config.module); +const dir = join(repo, config.dir); +// Every read and write stays inside the checkout, or for the templates, the +// config's directory (confine.mjs). +const checkout = confine(repo); + +// Adds an import line after the file's first block of imports (up to a +// blank line or other code). +const addImport = (text, line) => { + const sf = ts.createSourceFile("x.ts", text, ts.ScriptTarget.Latest, true); + let last; + for (const s of sf.statements) { + if (!ts.isImportDeclaration(s)) break; + if (last && /\n[ \t]*\n/.test(text.slice(last.getEnd(), s.getStart()))) break; + last = s; + } + const at = last ? last.getEnd() + 1 : 0; + return text.slice(0, at) + line + "\n" + text.slice(at); +}; +const configDir = dirname(resolve(configPath)); +const configFiles = confine(configDir); +// The names the class's instance goes by in the directory's sources. +const receivers = config.receivers ?? ["this", "emitter"]; +const touched = new Set(); +// Files the change creates, from templates next to the config (kept when a +// branch already has the file). +for (const [file, template] of Object.entries(config.create ?? {})) { + const path = join(dir, file); + if (checkout.exists(path)) continue; + checkout.write(path, configFiles.read(join(configDir, template))); + touched.add(path); +} +let moduleText = checkout.read(modulePath); +for (const group of config.groups) { + const sf = ts.createSourceFile(modulePath, moduleText, ts.ScriptTarget.Latest, true); + const cls = sf.statements.find((s) => ts.isClassDeclaration(s) && s.name?.text === config.class); + if (!cls) throw new Error(`no class ${config.class}`); + const wanted = new Set(group.fields); + const members = []; + for (const m of cls.members) { + if (ts.isPropertyDeclaration(m) && ts.isIdentifier(m.name) && wanted.has(m.name.text)) + members.push(m); + } + // A field the tree does not have yet (an older merge base) is left out, so + // the move still applies there. + const missing = group.fields.filter((f) => !members.some((m) => m.name.text === f)); + if (missing.length) + console.warn(`${group.file}: fields not in ${config.class}: ${missing.join(", ")}; skipped`); + group.present = members.map((m) => m.name.text); + // Interface members and factory entries, in the class's order. + const iface = []; + const factory = []; + for (const m of members) { + const lead = moduleText.slice(m.getFullStart(), m.getStart()).replace(/^\s*\n/, ""); + const readonly = m.modifiers?.some((x) => x.kind === ts.SyntaxKind.ReadonlyKeyword) + ? "readonly " + : ""; + const name = m.name.text; + if (!m.initializer) + throw new Error( + `${name}: a field without an initializer needs its constructor line moved by hand`, + ); + // An unannotated field takes its initializer's type: a boolean, + // number or string literal, or `new C(...)`. + const init = m.initializer; + const type = m.type + ? m.type.getText(sf) + : init.kind === ts.SyntaxKind.TrueKeyword || init.kind === ts.SyntaxKind.FalseKeyword + ? "boolean" + : ts.isNumericLiteral(init) + ? "number" + : ts.isStringLiteral(init) + ? "string" + : ts.isNewExpression(init) && init.typeArguments?.length + ? `${init.expression.getText(sf)}<${init.typeArguments.map((t) => t.getText(sf)).join(", ")}>` + : null; + if (type === null) throw new Error(`${name}: give the field a type annotation first`); + iface.push(`${lead} ${readonly}${name}: ${type};`); + factory.push(` ${name}: ${m.initializer.getText(sf)},`); + } + // Remove the declarations (with their leading comments) from the class. + const cuts = members.map((m) => [m.getFullStart(), m.getEnd()]).sort((a, b) => b[0] - a[0]); + for (const [s, e] of cuts) moduleText = moduleText.slice(0, s) + moduleText.slice(e); + const iName = group.interface ?? `${group.key[0]}${group.key.slice(1).toLowerCase()}State`; + const stateText = + `\n/** The state ${group.key} holds on each emitter. */\n` + + `export interface ${group.interfaceName ?? iName} {\n${iface.join("\n")}\n}\n\n` + + `export const ${group.key} = defineModuleState<${group.interfaceName ?? iName}>(() => ({\n${factory.join("\n")}\n}));\n`; + const groupPath = join(dir, group.file); + let groupText = checkout.read(groupPath); + if (!groupText.includes('from "./emitter-state.js"')) + groupText = addImport(groupText, `import { defineModuleState } from "./emitter-state.js";`); + groupText = groupText.trimEnd() + "\n" + stateText; + checkout.write(groupPath, groupText); + touched.add(groupPath); + group.rename = group.present.length + ? new RegExp(`(? f.endsWith(".ts"))) { + const path = join(dir, file); + let text = path === modulePath ? moduleText : checkout.read(path); + let changed = false; + for (const group of config.groups) { + if (!group.rename) continue; + const next = text.replace( + group.rename, + (_, recv, field) => `${group.key}.of(${recv}).${field}`, + ); + if (next === text) continue; + text = next; + changed = true; + if (file === group.file) continue; + const spec = `./${group.file.replace(/\.ts$/, ".js")}`; + const existing = new RegExp( + `^import \\{([^}]*)\\} from "${spec.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}";$`, + "m", + ); + const m = existing.exec(text); + if (m) { + const names = new Set( + m[1] + .split(",") + .map((s) => s.trim()) + .filter(Boolean), + ); + names.add(group.key); + text = text.replace(existing, `import { ${[...names].sort().join(", ")} } from "${spec}";`); + } else { + text = addImport(text, `import { ${group.key} } from "${spec}";`); + } + } + if (changed) { + checkout.write(path, text); + touched.add(path); + } +} +// The change's remaining edits, as literal replacements in order (the +// setup function call, imports, narrowing the rename loses). One whose text +// is not found as often as expected is reported and skipped: the branch +// changed that code, and the rehome's cherry-pick shows the difference. +let skipped = 0; +for (const r of config.replacements ?? []) { + const path = join(dir, r.file); + const text = checkout.exists(path) ? checkout.read(path) : ""; + const count = text.split(r.find).length - 1; + if (count !== (r.count ?? 1)) { + console.warn( + `${r.file}: expected ${r.count ?? 1} of ${JSON.stringify(r.find.slice(0, 60))}, found ${count}; skipped`, + ); + skipped++; + continue; + } + checkout.write(path, text.split(r.find).join(r.replace)); + touched.add(path); +} +formatFiles(toolsDir, [...touched], { cwd: repo, stdio: "inherit" }); +if (skipped) console.warn(`${skipped} replacement(s) skipped`); +console.log( + `moved ${config.groups.map((g) => g.present.length).reduce((a, b) => a + b, 0)} fields; rewrote ${touched.size} files`, +); diff --git a/scripts/split/rehome-split.mjs b/scripts/split/rehome-split.mjs new file mode 100644 index 000000000..6533301f8 --- /dev/null +++ b/scripts/split/rehome-split.mjs @@ -0,0 +1,316 @@ +#!/usr/bin/env node +/* Rehome a branch's changes across a module split. + * + * A split moves whole declarations out of one module into new files, so git + * cannot follow a branch's edits into them: merging the split reports every + * edit to a moved declaration as a conflict. This script carries the branch + * across instead. It applies the same split (split-module.mjs, + * split-methods.mjs or move-state.mjs with the split's config; each also + * formats with oxfmt) to the branch's merge base and to the branch tip, and + * cherry-picks the difference onto the target. That is a three-way merge + * whose base and branch side already use the new layout, so each hunk lands + * in the file its declaration moved to. Hunks in declarations that stay, and + * declarations the branch adds, stay in the module. + * + * node rehome-split.mjs --branch --onto --config + * [--out ] [--squash] [--scratch ] + * + * Run it from a checkout with node_modules installed (it supplies typescript + * and oxfmt). --onto is a commit that contains the split, usually main. + * The result is a new branch (default -rehomed). By default its tip + * is a merge commit with parents and : the branch's history + * is kept and the result reads as "merge main into the branch", so it can be + * pushed without rewriting the branch. --squash makes one ordinary commit on + * instead. When the cherry-pick conflicts, the script stops and + * leaves the work tree with the conflicts to resolve; see the message. */ +import { execFileSync, spawnSync } from "node:child_process"; +import { mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { createRequire } from "node:module"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { confine } from "./confine.mjs"; +import { formatFiles } from "./format.mjs"; + +const args = process.argv.slice(2); +const opt = (name, fallback) => { + const i = args.indexOf(`--${name}`); + return i >= 0 ? args[i + 1] : fallback; +}; +const has = (name) => args.includes(`--${name}`); +const branch = opt("branch"); +const onto = opt("onto"); +const configPath = opt("config"); +if (!branch || !onto || !configPath) { + console.error( + "usage: rehome-split.mjs --branch --onto --config [--out ] [--squash]", + ); + process.exit(2); +} +const out = opt("out", `${branch.replace(/^.*\//, "")}-rehomed`); +const tools = process.cwd(); +const here = dirname(fileURLToPath(import.meta.url)); +const config = JSON.parse(readFileSync(configPath, "utf8")); +// A config with "groups" moves fields into feature state (move-state.mjs), +// one with "methods" moves methods (split-methods.mjs), and any other moves +// top-level declarations (split-module.mjs). +const splitTool = join( + here, + config.groups ? "move-state.mjs" : config.class ? "split-methods.mjs" : "split-module.mjs", +); + +const git = (...a) => + execFileSync("git", a, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim(); +const gitIn = (cwd, ...a) => + execFileSync("git", a, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim(); +// Every commit the script makes is authored and committed as the branch's +// last author. +const author = (format) => git("log", "-1", `--format=${format}`, branch); +const ident = { + GIT_AUTHOR_NAME: author("%an"), + GIT_AUTHOR_EMAIL: author("%ae"), +}; +ident.GIT_COMMITTER_NAME = ident.GIT_AUTHOR_NAME; +ident.GIT_COMMITTER_EMAIL = ident.GIT_AUTHOR_EMAIL; +Object.assign(process.env, ident); + +const tip = git("rev-parse", `${branch}^{commit}`); +const target = git("rev-parse", `${onto}^{commit}`); +// The split must not be in the branch already, or there is nothing to rehome. +const moduleFile = config.module; +// With several merge bases (criss-cross history, such as a branch that +// carries another branch's commits that main merged), merge them into one +// virtual base, as git's merge does, so the cherry-pick sees the same base. +let base = git("merge-base", tip, target); +{ + const bases = git("merge-base", "--all", tip, target).split("\n").filter(Boolean); + for (const next of bases.slice(1)) { + const merged = spawnSync("git", ["merge-tree", "--write-tree", base, next], { + encoding: "utf8", + }); + const tree = merged.stdout.split("\n")[0]; + base = git("commit-tree", tree, "-p", base, "-p", next, "-m", "virtual merge base"); + } +} + +// Scratch worktrees go next to the checkout: not inside it (they would show +// as untracked files) and not under node_modules (oxfmt skips those paths). +const scratch = mkdtempSync(opt("scratch", join(dirname(tools), ".rehome-"))); +const worktree = (name, commit) => { + const dir = join(scratch, name); + git("worktree", "add", "--detach", "-q", dir, commit); + return dir; +}; +const split = (dir) => { + execFileSync( + process.execPath, + [splitTool, "apply", configPath, "--repo", dir, "--tools", tools], + { + stdio: ["ignore", "pipe", "inherit"], + }, + ); + gitIn(dir, "add", "-A"); + return gitIn(dir, "write-tree"); +}; +const commitTree = (tree, parents, message) => + execFileSync("git", ["commit-tree", tree, ...parents.flatMap((p) => ["-p", p]), "-m", message], { + encoding: "utf8", + env: { + ...process.env, + ...ident, + }, + }).trim(); + +let baseDir, tipDir; +try { + baseDir = worktree("base", base); + tipDir = worktree("tip", tip); + const splitBase = commitTree(split(baseDir), [base], "split of the merge base"); + const splitTip = commitTree(split(tipDir), [splitBase], "split of the branch tip"); + if (git("rev-parse", `${splitBase}^{tree}`) === git("rev-parse", `${splitTip}^{tree}`)) { + console.log(`${branch} has no changes relative to ${base.slice(0, 9)}`); + process.exit(0); + } + const touched = git("diff", "--name-only", splitBase, splitTip).split("\n").filter(Boolean); + console.log(`${branch}: ${touched.length} files changed in the split layout`); + // Apply the branch's changes onto the target in a worktree of its own. + const resultDir = worktree("result", target); + const pick = spawnSync( + "git", + ["-c", "merge.conflictStyle=merge", "cherry-pick", "--no-commit", splitTip], + { + cwd: resultDir, + encoding: "utf8", + }, + ); + // Moved declarations bring short import blocks, where an import main added + // and one the branch added can sit on adjacent lines. Resolve conflicts + // made only of import declarations to the union of both sides. + if (pick.status !== 0) { + // Only the files the split writes: other conflicts are the branch's own. + // A method split names its files in the config; a module split, in its manifest. + const files = config.groups + ? [ + ...config.groups.map((g) => g.file), + ...Object.keys(config.create ?? {}), + ...(config.replacements ?? []).map((r) => r.file), + ] + : config.class + ? Object.keys(config.methods) + : Object.keys( + (({ files, ...rest }) => files ?? rest)( + JSON.parse( + confine(dirname(resolve(configPath))).read( + resolve(dirname(resolve(configPath)), config.manifest), + ), + ), + ), + ); + const moduleDir = config.dir ?? dirname(moduleFile); + const splitFiles = new Set([moduleFile, ...files.map((f) => join(moduleDir, f))]); + for (const file of gitIn(resultDir, "diff", "--name-only", "--diff-filter=U") + .split("\n") + .filter(Boolean)) { + if ( + splitFiles.has(file) && + resolveImportConflicts(confine(resultDir), join(resultDir, file)) + ) { + formatFiles(tools, [file], { cwd: resultDir, stdio: ["ignore", "ignore", "inherit"] }); + gitIn(resultDir, "add", file); + console.log(`${file}: import conflicts resolved to both sides' imports`); + } + } + } + if (pick.status !== 0 && gitIn(resultDir, "diff", "--name-only", "--diff-filter=U") !== "") { + const conflicts = gitIn(resultDir, "diff", "--name-only", "--diff-filter=U"); + console.error( + `conflicts in:\n${conflicts}\n\nResolve them in ${resultDir}, then run:\n git -C ${resultDir} add -A && git -C ${resultDir} commit -m "Merge ${onto} into ${branch}" # or rerun with the resolution\nThe scratch worktrees stay until you remove ${scratch}.`, + ); + process.exit(1); + } + const tree = gitIn(resultDir, "write-tree"); + const subjects = git("log", "--format=%s", `${base}..${tip}`).split("\n").filter(Boolean); + const result = has("squash") + ? commitTree( + tree, + [target], + subjects.length === 1 + ? subjects[0] + : `${subjects.at(-1)}\n\n${subjects.map((s) => `- ${s}`).join("\n")}`, + ) + : commitTree( + tree, + [tip, target], + `Merge ${onto} into ${branch.replace(/^.*\//, "")}\n\nCarries the branch's changes to ${moduleFile} into the files its declarations moved to.`, + ); + git("branch", "-f", out, result); + console.log( + `${out} -> ${result.slice(0, 9)} (${has("squash") ? "one commit on" : "merge of"} ${onto})`, + ); + for (const dir of [baseDir, tipDir, resultDir]) git("worktree", "remove", "--force", dir); + rmSync(scratch, { recursive: true, force: true }); +} catch (error) { + console.error(error.stderr?.toString() || error.message); + console.error(`scratch worktrees left in ${scratch}`); + process.exit(1); +} + +/* Rewrites every conflict in `path` whose two sides hold only import + * declarations as the union of both: one declaration per module and kind, + * with the named imports of both sides in first-seen order, read and + * written through the result checkout's guard. Returns false, + * leaving the file unchanged, when any conflict holds other code. */ +function resolveImportConflicts(checkout, path) { + const ts = createRequire(join(tools, "package.json"))("typescript"); + const text = checkout.read(path); + const region = /^<<<<<<< [^\n]*\n([\s\S]*?)^=======\n([\s\S]*?)^>>>>>>> [^\n]*\n/gm; + const imports = (side) => { + const sf = ts.createSourceFile("side.ts", side, ts.ScriptTarget.Latest, true); + return sf.statements.every(ts.isImportDeclaration) ? sf.statements : null; + }; + let ok = true; + const imported = new Set(); + const resolved = text.replace(region, (whole, ours, theirs) => { + const a = imports(ours), + b = imports(theirs); + if (!a || !b) { + ok = false; + return whole; + } + const merged = new Map(); + for (const decl of [...a, ...b]) { + const clause = decl.importClause; + const named = + clause?.namedBindings && ts.isNamedImports(clause.namedBindings) + ? clause.namedBindings + : null; + if (!clause || !named || clause.name) { + // Side-effect, default and namespace imports are kept as written, once. + merged.set(decl.getText(), null); + continue; + } + const key = `${clause.isTypeOnly ? "type " : ""}${decl.moduleSpecifier.getText()}`; + const entry = merged.get(key) ?? { + typeOnly: clause.isTypeOnly, + from: decl.moduleSpecifier.getText(), + names: [], + }; + for (const element of named.elements) { + imported.add(element.name.text); + if (!entry.names.includes(element.getText())) entry.names.push(element.getText()); + } + merged.set(key, entry); + } + return ( + [...merged] + .map(([key, entry]) => + entry === null + ? key + : `import ${entry.typeOnly ? "type " : ""}{ ${entry.names.join(", ")} } from ${entry.from};`, + ) + .join("\n") + "\n" + ); + }); + if (!ok || resolved === text) return false; + checkout.write(path, pruneUnusedImports(ts, resolved, imported)); + return true; +} + +/* Drops the named imports among `names` that nothing outside import + * declarations references: a side's import whose use the other side's code + * replaced. The split computes each file's imports from its references, so + * this keeps a rehomed file's imports the ones the split would write. */ +function pruneUnusedImports(ts, text, names) { + const sf = ts.createSourceFile("file.ts", text, ts.ScriptTarget.Latest, true); + const used = new Set(); + const visit = (node) => { + if (ts.isImportDeclaration(node)) return; + if (ts.isIdentifier(node)) used.add(node.text); + ts.forEachChild(node, visit); + }; + visit(sf); + const edits = []; + for (const decl of sf.statements) { + const named = decl.importClause?.namedBindings; + if ( + !ts.isImportDeclaration(decl) || + !named || + !ts.isNamedImports(named) || + decl.importClause.name + ) + continue; + const kept = named.elements.filter((e) => !names.has(e.name.text) || used.has(e.name.text)); + if (kept.length === named.elements.length) continue; + const from = decl.moduleSpecifier.getText(); + const prefix = decl.importClause.isTypeOnly ? "import type" : "import"; + // A dropped declaration takes its line break with it. + const end = kept.length || text[decl.getEnd()] !== "\n" ? decl.getEnd() : decl.getEnd() + 1; + edits.push([ + decl.getStart(), + end, + kept.length ? `${prefix} { ${kept.map((e) => e.getText()).join(", ")} } from ${from};` : "", + ]); + } + for (const [start, end, replacement] of edits.reverse()) + text = text.slice(0, start) + replacement + text.slice(end); + return text; +} diff --git a/scripts/split/split-methods.mjs b/scripts/split/split-methods.mjs new file mode 100644 index 000000000..4d57644af --- /dev/null +++ b/scripts/split/split-methods.mjs @@ -0,0 +1,417 @@ +#!/usr/bin/env node +/* Move methods of one class into other files as functions of a host, as a + * pure move. + * + * node split-methods.mjs apply [--repo ] [--tools ] + * + * : + * { "tsconfig": "packages/compiler/tsconfig.json", + * "module": "packages/compiler/src/backend/llvm/emitter.ts", + * "class": "LlEmitter", + * "methods": { "emit-statements.ts": ["emitStmtBody", ...], ... }, + * "headers": { "emit-statements.ts": "/* ... *\/" } } + * + * Each listed method becomes `export function (host: , ...)` + * in its file (relative to the module's directory), with its doc comment + * and `this` replaced by the host parameter (`emitter`, or "hostName"). The class keeps a stub with the method's + * signature that calls the function, so every caller is unchanged. Members + * the moved bodies reach through `host` lose a `private` modifier (a + * compile-time check only). Each new file imports what its functions + * reference from the module's scope, with paths rebased; the module imports + * the functions and drops imports nothing uses anymore. Names the config + * lists but the class lacks are skipped. */ +import { readFileSync } from "node:fs"; +import { dirname, join, relative, resolve, posix } from "node:path"; +import { createRequire } from "node:module"; +import { confine, confineHost } from "./confine.mjs"; +import { formatFiles } from "./format.mjs"; + +const [command, configPath, ...flags] = process.argv.slice(2); +if (command !== "apply") throw new Error("usage: split-methods.mjs apply "); +const config = JSON.parse(readFileSync(configPath, "utf8")); +const flag = (name) => { + const i = flags.indexOf(name); + return i >= 0 ? flags[i + 1] : undefined; +}; +const repo = resolve(flag("--repo") ?? process.cwd()); +// Every read and write in the checkout stays inside it (confine.mjs). +const checkout = confine(repo); +const toolsDir = resolve(flag("--tools") ?? process.cwd()); +const require = createRequire(join(toolsDir, "package.json")); +const ts = require("typescript"); +const modulePath = join(repo, config.module); +const moduleDir = dirname(modulePath); +const className = config.class; +// The host parameter's name; a moved body that already uses it is refused. +const hostName = config.hostName ?? "emitter"; + +const text = checkout.read(modulePath); +const host = confineHost(ts.createCompilerHost({ noEmit: true }), checkout); +const sf = ts.createSourceFile(modulePath, text, ts.ScriptTarget.Latest, true); +const realGet = host.getSourceFile; +host.getSourceFile = (f, l, ...r) => + resolve(f) === modulePath ? sf : realGet.call(host, f, l, ...r); +const program = ts.createProgram([modulePath], { noEmit: true, noResolve: true }, host); +const checker = program.getTypeChecker(); +const source = program.getSourceFile(modulePath); + +const cls = source.statements.find((s) => ts.isClassDeclaration(s) && s.name?.text === className); +if (!cls) throw new Error(`no class ${className}`); +const memberByName = new Map(); +for (const m of cls.members) + if (m.name && ts.isIdentifier(m.name)) { + if (!memberByName.has(m.name.text)) memberByName.set(m.name.text, []); + memberByName.get(m.name.text).push(m); + } + +// Module scope: import bindings and top-level declarations. +const imports = new Map(); +const topLevel = new Map(); +for (const st of source.statements) { + if (ts.isImportDeclaration(st)) { + const c = st.importClause; + if (!c) continue; + if (c.name) imports.set(c.name.text, { st, kind: "default", typeOnly: c.isTypeOnly }); + const nb = c.namedBindings; + if (nb && ts.isNamespaceImport(nb)) + imports.set(nb.name.text, { st, kind: "namespace", typeOnly: c.isTypeOnly }); + if (nb && ts.isNamedImports(nb)) + for (const el of nb.elements) + imports.set(el.name.text, { + st, + kind: "named", + typeOnly: c.isTypeOnly || el.isTypeOnly, + imported: (el.propertyName ?? el.name).text, + }); + continue; + } + const names = []; + if ( + (ts.isFunctionDeclaration(st) || + ts.isClassDeclaration(st) || + ts.isInterfaceDeclaration(st) || + ts.isTypeAliasDeclaration(st) || + ts.isEnumDeclaration(st)) && + st.name + ) + names.push(st.name.text); + if (ts.isVariableStatement(st)) + for (const d of st.declarationList.declarations) + if (ts.isIdentifier(d.name)) names.push(d.name.text); + for (const n of names) topLevel.set(n, st); +} +const isExported = (st) => + st.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false; +const isTypeDecl = (st) => ts.isInterfaceDeclaration(st) || ts.isTypeAliasDeclaration(st); + +/** Module-scope references in `node`: name -> { kind: "import"|"local" }, and `this.` names. */ +function references(node) { + const refs = new Map(); + const members = new Set(); + const visit = (n) => { + if ( + n.kind === ts.SyntaxKind.ThisKeyword && + n.parent && + ts.isPropertyAccessExpression(n.parent) && + n.parent.expression === n + ) + members.add(n.parent.name.text); + if (ts.isIdentifier(n)) { + const p = n.parent; + const propName = + p && + ((ts.isPropertyAccessExpression(p) && p.name === n) || + (ts.isQualifiedName(p) && p.right === n) || + (ts.isPropertyAssignment(p) && p.name === n)); + if (!propName) { + let sym = checker.getSymbolAtLocation(n); + if (p && ts.isShorthandPropertyAssignment(p)) + sym = checker.getShorthandAssignmentValueSymbol(p); + const decl = sym?.declarations?.[0]; + if (decl && decl.getSourceFile() === source) { + if (ts.isImportSpecifier(decl) || ts.isNamespaceImport(decl) || ts.isImportClause(decl)) + refs.set(decl.name.text, { kind: "import" }); + else if ( + topLevel.get(n.text) && + (decl === topLevel.get(n.text) || + decl.parent?.parent === topLevel.get(n.text) || + decl.parent === topLevel.get(n.text)) + ) + refs.set(n.text, { kind: "local" }); + } + } + } + ts.forEachChild(n, visit); + }; + visit(node); + return { refs, members }; +} + +const rebase = (spec, toDir) => { + if (!spec.startsWith(".")) return spec; + let rel = posix.normalize(relative(toDir, resolve(moduleDir, spec)).split("\\").join("/")); + return rel.startsWith(".") ? rel : "./" + rel; +}; +const specTo = (fromDir, file) => { + let rel = relative(fromDir, file).split("\\").join("/").replace(/\.ts$/, ".js"); + return rel.startsWith(".") ? rel : "./" + rel; +}; + +const edits = []; // [start, end, replacement] in the module +const stubs = []; +const files = new Map(); +const publicize = new Set(); +const moduleNeeds = new Map(); // spec -> names (functions the stubs call) +const exportLocals = new Set(); +let moved = 0; + +for (const [file, names] of Object.entries(config.methods)) { + const path = join(moduleDir, file); + const dir = dirname(path); + const entry = { decls: [], named: new Map(), other: [] }; + const addNamed = (spec, name, imported, typeOnly) => { + if (!entry.named.has(spec)) entry.named.set(spec, new Map()); + const m = entry.named.get(spec); + const prev = m.get(name); + m.set(name, { imported, typeOnly: prev ? prev.typeOnly && typeOnly : typeOnly }); + }; + addNamed(specTo(dir, modulePath), className, className, true); + for (const name of names) { + const decls = memberByName.get(name); + if (!decls) { + console.error(`skip: ${className}.${name} not found`); + continue; + } + if (decls.length !== 1 || !ts.isMethodDeclaration(decls[0]) || !decls[0].body) + throw new Error(`${name}: expected one method with a body`); + const m = decls[0]; + if (m.typeParameters?.length || m.asteriskToken) + throw new Error(`${name}: generic and generator methods are not handled`); + const isAsync = m.modifiers?.some((x) => x.kind === ts.SyntaxKind.AsyncKeyword) ?? false; + // The function: doc comment, signature (initializers kept, so a + // defaulted parameter after an optional one stays valid; the stub + // passes every argument), and the body with this -> host. + const lead = text.slice(m.getFullStart(), m.getStart()).replace(/^\s*\n/, ""); + const params = m.parameters.map((p) => { + const nameText = p.name.getText(); + // A parameter typed only by its default takes the default's widened type. + const type = p.type + ? `: ${p.type.getText()}` + : p.initializer + ? `: ${checker.typeToString(checker.getBaseTypeOfLiteralType(checker.getTypeAtLocation(p.initializer)))}` + : ""; + const optional = p.questionToken ? "?" : ""; + let init = ""; + if (p.initializer) { + const initText = p.initializer.getText(); + if (/\bthis\b/.test(initText)) + throw new Error(`${name}: a parameter default that reads this is not handled`); + init = ` = ${initText}`; + } + return `${p.dotDotDotToken ? "..." : ""}${nameText}${optional}${type}${init}`; + }); + const ret = m.type ? `: ${m.type.getText()}` : ""; + const body = m.body; + const thisEdits = []; + const findThis = (n) => { + // `this` in an expression, or in a type query (`typeof this.x`). + // (In a type query `this` parses as an identifier.) + if ( + n.kind === ts.SyntaxKind.ThisKeyword || + (ts.isIdentifier(n) && n.text === "this" && ts.findAncestor(n, ts.isTypeQueryNode)) + ) + thisEdits.push(n.getStart()); + if (ts.isFunctionExpression(n) || ts.isFunctionDeclaration(n) || ts.isClassLike(n)) + throw new Error(`${name}: a nested function or class rebinds this`); + ts.forEachChild(n, findThis); + }; + ts.forEachChild(body, findThis); + const clash = (n) => { + if (ts.isIdentifier(n) && n.text === hostName) + throw new Error(`${name}: already uses the name ${hostName}`); + ts.forEachChild(n, clash); + }; + clash(m); + let bodyText = ""; + let cursor = body.getStart(); + for (const at of thisEdits.sort((a, b) => a - b)) { + bodyText += text.slice(cursor, at) + hostName; + cursor = at + 4; + } + bodyText += text.slice(cursor, body.end); + const leadOut = lead + .split("\n") + .map((l) => (l.startsWith(" ") ? l.slice(2) : l)) + .join("\n"); + const fn = `${leadOut}export ${isAsync ? "async " : ""}function ${name}(${hostName}: ${className}${params.length ? ", " + params.join(", ") : ""})${ret} ${bodyText}`; + entry.decls.push(fn); + stubs.push(m); + if (!moduleNeeds.has(specTo(moduleDir, path))) + moduleNeeds.set(specTo(moduleDir, path), new Set()); + moduleNeeds.get(specTo(moduleDir, path)).add(name); + // What the function needs. + const { refs, members } = references(m); + for (const member of members) publicize.add(member); + for (const [ref, { kind }] of refs) { + if (kind === "import") { + const imp = imports.get(ref); + const spec = rebase(imp.st.moduleSpecifier.text, dir); + if (imp.kind === "named") addNamed(spec, ref, imp.imported, imp.typeOnly); + else entry.other.push({ spec, kind: imp.kind, name: ref, typeOnly: imp.typeOnly }); + } else { + const st = topLevel.get(ref); + if (!isExported(st)) exportLocals.add(st); + addNamed(specTo(dir, modulePath), ref, ref, isTypeDecl(st)); + } + } + moved++; + } + files.set(file, entry); +} + +const renderImports = (entry) => { + const lines = []; + const seen = new Set(); + for (const o of entry.other) { + const key = `${o.kind}:${o.name}:${o.spec}`; + if (seen.has(key)) continue; + seen.add(key); + lines.push( + o.kind === "namespace" + ? `import ${o.typeOnly ? "type " : ""}* as ${o.name} from "${o.spec}";` + : `import ${o.typeOnly ? "type " : ""}${o.name} from "${o.spec}";`, + ); + } + for (const [spec, names] of entry.named) { + const all = [...names.entries()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)); + const allType = all.every(([, v]) => v.typeOnly); + const parts = all.map( + ([n, v]) => + `${!allType && v.typeOnly ? "type " : ""}${v.imported === n ? n : `${v.imported} as ${n}`}`, + ); + lines.push(`import ${allType ? "type " : ""}{ ${parts.join(", ")} } from "${spec}";`); + } + return lines; +}; + +for (const [file, entry] of files) { + if (!entry.decls.length) continue; + const path = join(moduleDir, file); + if (checkout.exists(path)) throw new Error(`${file} exists`); + const header = config.headers?.[file]; + checkout.write( + path, + [...(header ? [header] : []), ...renderImports(entry), "", entry.decls.join("\n\n"), ""].join( + "\n", + ), + ); +} + +// Each moved method leaves a stub with its signature (defaults included) +// that calls the function; one the moved bodies call loses `private`. +const stubSet = new Set(stubs); +for (const m of stubs) { + let signature = m.modifiers + ? text.slice(m.getStart(), m.body.getStart()).trimEnd() + : text.slice(m.getStart(), m.body.getStart()).trimEnd(); + if (publicize.has(m.name.text)) signature = signature.replace(/^private /, ""); + const args = m.parameters.map((p) => `${p.dotDotDotToken ? "..." : ""}${p.name.getText()}`); + const call = `${m.name.text}(this${args.length ? ", " + args.join(", ") : ""})`; + edits.push([m.getFullStart(), m.end, `\n ${signature} {\n return ${call};\n }`]); +} +// Other members the moved bodies reach through the host lose `private`. +for (const name of publicize) + for (const m of memberByName.get(name) ?? []) + if (!stubSet.has(m)) + for (const mod of m.modifiers ?? []) + if (mod.kind === ts.SyntaxKind.PrivateKeyword) + edits.push([mod.getStart(), mod.end + 1, ""]); +// Top-level declarations the new files import get `export`. +for (const st of exportLocals) edits.push([st.getStart(), st.getStart(), "export "]); + +let out = ""; +let cursor = 0; +for (const [a, b, r] of edits.sort((x, y) => x[0] - y[0])) { + out += text.slice(cursor, a) + r; + cursor = b; +} +out += text.slice(cursor); + +// Re-analyze the rewritten module for unused imports, then add the new ones. +const sf2 = ts.createSourceFile(modulePath, out, ts.ScriptTarget.Latest, true); +const host2 = confineHost(ts.createCompilerHost({ noEmit: true }), checkout); +const get2 = host2.getSourceFile; +host2.getSourceFile = (f, l, ...r) => + resolve(f) === modulePath ? sf2 : get2.call(host2, f, l, ...r); +const program2 = ts.createProgram([modulePath], { noEmit: true, noResolve: true }, host2); +const checker2 = program2.getTypeChecker(); +const src2 = program2.getSourceFile(modulePath); +const used = new Set(); +const walk = (n) => { + if ( + ts.isIdentifier(n) && + !(n.parent && ts.isImportSpecifier(n.parent)) && + !(n.parent && ts.isImportClause(n.parent)) && + !(n.parent && ts.isNamespaceImport(n.parent)) + ) { + const sym = + n.parent && ts.isShorthandPropertyAssignment(n.parent) + ? checker2.getShorthandAssignmentValueSymbol(n.parent) + : checker2.getSymbolAtLocation(n); + const d = sym?.declarations?.[0]; + if (d && (ts.isImportSpecifier(d) || ts.isImportClause(d) || ts.isNamespaceImport(d))) + used.add(d.name.text); + } + ts.forEachChild(n, walk); +}; +for (const st of src2.statements) if (!ts.isImportDeclaration(st)) walk(st); +const importEdits = []; +for (const st of src2.statements) { + if (!ts.isImportDeclaration(st) || !st.importClause) continue; + const c = st.importClause; + const def = c.name && used.has(c.name.text) ? c.name.text : null; + const nb = c.namedBindings; + const ns = nb && ts.isNamespaceImport(nb) && used.has(nb.name.text) ? nb.name.text : null; + const named = + nb && ts.isNamedImports(nb) ? nb.elements.filter((el) => used.has(el.name.text)) : []; + const all = + (c.name ? def !== null : true) && + (!nb || (ts.isNamespaceImport(nb) ? ns !== null : named.length === nb.elements.length)); + if (all) continue; + let r = ""; + if (def || ns || named.length) { + const bits = []; + if (def) bits.push(def); + if (ns) bits.push(`* as ${ns}`); + if (named.length) bits.push(`{ ${named.map((el) => el.getText()).join(", ")} }`); + r = `import ${c.isTypeOnly ? "type " : ""}${bits.join(", ")} from ${st.moduleSpecifier.getText()};`; + } + const end = r === "" && out[st.end] === "\n" ? st.end + 1 : st.end; + importEdits.push([st.getStart(), end, r]); +} +let final = ""; +cursor = 0; +for (const [a, b, r] of importEdits.sort((x, y) => x[0] - y[0])) { + final += out.slice(cursor, a) + r; + cursor = b; +} +final += out.slice(cursor); +const lastImport = [ + ...ts.createSourceFile(modulePath, final, ts.ScriptTarget.Latest, true).statements, +] + .filter(ts.isImportDeclaration) + .at(-1); +const add = [...moduleNeeds].map( + ([spec, names]) => `import { ${[...names].sort().join(", ")} } from "${spec}";`, +); +const at = lastImport ? lastImport.end : 0; +final = final.slice(0, at) + (add.length ? "\n" + add.join("\n") : "") + final.slice(at); +checkout.write(modulePath, final); +// Format what changed with the tools checkout's oxfmt, so the split and +// every rehome of it are formatted the same way. +const touched = [ + modulePath, + ...[...files].filter(([, e]) => e.decls.length).map(([f]) => join(moduleDir, f)), +]; +formatFiles(toolsDir, touched, { cwd: repo, stdio: ["ignore", "ignore", "inherit"] }); +console.log(`${moved} methods moved; ${publicize.size} members reached through host`); diff --git a/scripts/split/split-module.mjs b/scripts/split/split-module.mjs new file mode 100644 index 000000000..4b8764d80 --- /dev/null +++ b/scripts/split/split-module.mjs @@ -0,0 +1,545 @@ +#!/usr/bin/env node +/* Split a large TypeScript module into files by a manifest of top-level + * declaration names, as a pure move. + * + * node split-module.mjs plan compute the manifest from seeds + * node split-module.mjs apply extract branches, move declarations + * + * : + * { "tsconfig": "packages/compiler/tsconfig.json", + * "module": "packages/compiler/src/frontend/lowering/lower-exprs.ts", + * "extract": [{ "function": "lowerExprInner", "condition": "ts.isIdentifier(expr)", + * "name": "lowerIdentifierExpr", "exprType": "ts.Identifier" }, ...], + * "seeds": { "expressions/binary.ts": ["lowerBinary", ...], ... }, + * "manifest": "" } + * + * It runs on the checkout in the current directory, or --repo , and + * loads typescript from that checkout or --tools . + * + * extract turns `if () { ...block... }` at the top level of the + * named function into `if () return (lowerer, expr, loc);` + * and adds `function (lowerer: Lowerer, expr: , loc: SrcLoc): + * IrExpr { ...block... }` after it. The block must end in a call that never + * returns, so no path falls through. + * + * plan assigns each seed to its file, then moves every other top-level + * declaration whose referrers all sit in one file (other than the module) + * into that file, to a fixpoint. Module-level `let` bindings stay put (an + * importer cannot assign them). The manifest lists file → names. + * + * apply moves the manifest's declarations with their leading comments, + * in source order, into the new files. Each new file imports what its + * declarations reference: names from the module or another new file, and + * the module's own imports with their paths rebased. The module imports the + * moved names it still uses, re-exports the moved names that were exported, + * and drops imports nothing uses anymore. Moved declarations that something + * outside their file references get `export`. Names a manifest lists but + * the module lacks are skipped, and new declarations stay in the module, + * so the same manifest applies to an edited copy of the module. */ +import { readFileSync } from "node:fs"; +import { dirname, join, relative, resolve, posix } from "node:path"; +import { createRequire } from "node:module"; +import { confine, confineHost } from "./confine.mjs"; +import { formatFiles } from "./format.mjs"; + +const [command, configPath, ...flags] = process.argv.slice(2); +const config = JSON.parse(readFileSync(configPath, "utf8")); +const flag = (name) => { + const i = flags.indexOf(name); + return i >= 0 ? flags[i + 1] : undefined; +}; +// --repo runs on another checkout (a temporary worktree); --tools names the +// checkout whose node_modules supply typescript. +const repo = resolve(flag("--repo") ?? config.repo ?? process.cwd()); +// Every read and write stays inside the checkout, or for the manifest, the +// config's directory (confine.mjs). +const checkout = confine(repo); +const configFiles = confine(dirname(resolve(configPath))); +// The manifest path is relative to the config file. +config.manifest = resolve(dirname(resolve(configPath)), flag("--manifest") ?? config.manifest); +const toolsDir = resolve(flag("--tools") ?? config.repo ?? process.cwd()); +const require = createRequire(join(toolsDir, "package.json")); +const ts = require("typescript"); +const modulePath = join(repo, config.module); +const moduleDir = dirname(modulePath); + +function parse(text, path = modulePath) { + return ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true); +} + +/* ── extract ─────────────────────────────────────────────────────────── */ + +function extractBranches(text) { + for (const spec of config.extract ?? []) { + const sf = parse(text); + const fn = sf.statements.find( + (s) => ts.isFunctionDeclaration(s) && s.name?.text === spec.function, + ); + if (!fn) throw new Error(`no function ${spec.function}`); + const branch = fn.body.statements.find( + (s) => + ts.isIfStatement(s) && + s.expression.getText() === spec.condition && + ts.isBlock(s.thenStatement) && + !s.elseStatement, + ); + if (!branch) { + console.error(`skip: no \`if (${spec.condition}) { ... }\` in ${spec.function}`); + continue; + } + const block = branch.thenStatement; + const last = block.statements.at(-1); + if ( + !last || + !(ts.isExpressionStatement(last) || ts.isReturnStatement(last) || ts.isThrowStatement(last)) + ) + throw new Error( + `${spec.condition}: the block must end in a never-returning call, return or throw`, + ); + // The block's inner text, dedented one level (two spaces). + const inner = text.slice(block.statements[0].getFullStart(), block.statements.at(-1).end); + const body = inner + .split("\n") + .map((line) => (line.startsWith(" ") ? line.slice(2) : line)) + .join("\n") + .replace(/^\n+/, ""); + const call = `if (${spec.condition}) return ${spec.name}(lowerer, expr, loc);`; + const fnText = `\n\nfunction ${spec.name}(lowerer: Lowerer, expr: ${spec.exprType}, loc: SrcLoc): IrExpr {\n ${body.trimStart()}\n}`; + text = + text.slice(0, branch.getStart()) + + call + + text.slice(branch.end, fn.end) + + fnText + + text.slice(fn.end); + } + return text; +} + +/* ── analysis ────────────────────────────────────────────────────────── */ + +function declNames(st) { + if ( + ts.isFunctionDeclaration(st) || + ts.isClassDeclaration(st) || + ts.isInterfaceDeclaration(st) || + ts.isTypeAliasDeclaration(st) || + ts.isEnumDeclaration(st) + ) + return st.name ? [st.name.text] : []; + if (ts.isVariableStatement(st)) + return st.declarationList.declarations.flatMap((d) => + ts.isIdentifier(d.name) ? [d.name.text] : [], + ); + return []; +} +const isExported = (st) => + st.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false; +const isTypeDecl = (st) => ts.isInterfaceDeclaration(st) || ts.isTypeAliasDeclaration(st); +const isMutable = (st) => + ts.isVariableStatement(st) && !(st.declarationList.flags & ts.NodeFlags.Const); + +function inTypePosition(node) { + for (let n = node; n.parent; n = n.parent) { + // A class's `extends` names a value; `implements` and an interface's + // `extends` name types. + if (ts.isExpressionWithTypeArguments(n) && ts.isHeritageClause(n.parent)) + return !(n.parent.token === ts.SyntaxKind.ExtendsKeyword && ts.isClassLike(n.parent.parent)); + if (ts.isTypeNode(n) || ts.isTypeQueryNode?.(n)) return true; + if (ts.isStatement(n) || (ts.isExpression(n) && !ts.isIdentifier(n))) break; + } + return false; +} + +/** For the module text: its statements with names, its import bindings, and + * each declaration's references (local names and import bindings), each + * marked type-only when every reference sits in a type position. */ +function analyze(text) { + const sf = parse(text); + const host = confineHost(ts.createCompilerHost({}), checkout); + const tsconfigPath = join(repo, config.tsconfig); + checkout.check(tsconfigPath); + const tsconfig = ts.getParsedCommandLineOfConfigFile( + tsconfigPath, + {}, + { ...confineHost(ts.sys, checkout), onUnRecoverableConfigFileDiagnostic: () => {} }, + ); + const options = { ...tsconfig.options, noEmit: true }; + const realGet = host.getSourceFile; + host.getSourceFile = (fileName, lang, ...rest) => + resolve(fileName) === modulePath ? sf : realGet.call(host, fileName, lang, ...rest); + const program = ts.createProgram([modulePath], options, host); + const checker = program.getTypeChecker(); + const source = program.getSourceFile(modulePath); + + const imports = new Map(); // binding name -> { decl, spec, kind, typeOnly } + const statements = []; + for (const st of source.statements) { + if (ts.isImportDeclaration(st)) { + const clause = st.importClause; + const typeOnlyDecl = clause?.isTypeOnly ?? false; + if (clause?.name) + imports.set(clause.name.text, { + decl: st, + kind: "default", + typeOnly: typeOnlyDecl, + node: clause, + }); + const nb = clause?.namedBindings; + if (nb && ts.isNamespaceImport(nb)) + imports.set(nb.name.text, { + decl: st, + kind: "namespace", + typeOnly: typeOnlyDecl, + node: nb, + }); + if (nb && ts.isNamedImports(nb)) + for (const el of nb.elements) + imports.set(el.name.text, { + decl: st, + kind: "named", + typeOnly: typeOnlyDecl || el.isTypeOnly, + node: el, + imported: (el.propertyName ?? el.name).text, + }); + continue; + } + statements.push({ node: st, names: declNames(st) }); + } + const byNode = new Map(); + for (const s of statements) byNode.set(s.node, s); + const ownerOf = (decl) => { + for (let n = decl; n; n = n.parent) if (byNode.has(n)) return byNode.get(n); + return undefined; + }; + for (const s of statements) { + const refs = new Map(); // name -> { kind: "local"|"import", typeOnly } + const visit = (node) => { + if (ts.isIdentifier(node)) { + const parent = node.parent; + const isDeclName = + parent && + parent.name === node && + (ts.isFunctionDeclaration(parent) || + ts.isVariableDeclaration(parent) || + ts.isInterfaceDeclaration(parent) || + ts.isTypeAliasDeclaration(parent) || + ts.isParameter(parent) || + ts.isPropertyAssignment(parent) || + ts.isPropertySignature(parent) || + ts.isPropertyDeclaration(parent) || + ts.isMethodDeclaration(parent) || + ts.isBindingElement(parent)); + const isPropertyName = + parent && + ((ts.isPropertyAccessExpression(parent) && parent.name === node) || + (ts.isQualifiedName(parent) && parent.right === node)); + if (!isPropertyName) { + let sym = checker.getSymbolAtLocation(node); + if (ts.isShorthandPropertyAssignment(parent)) + sym = checker.getShorthandAssignmentValueSymbol(parent); + const decl = sym?.declarations?.[0]; + if (decl && decl.getSourceFile() === source) { + const typePos = inTypePosition(node); + let target; + if ( + ts.isImportSpecifier(decl) || + ts.isNamespaceImport(decl) || + ts.isImportClause(decl) + ) { + const name = (ts.isImportClause(decl) ? decl.name : decl.name).text; + target = { name, kind: "import" }; + } else { + const owner = ownerOf(decl); + if (owner && owner !== s && !isDeclName) { + const name = owner.names.find((n) => n === node.text) ?? node.text; + if (owner.names.includes(name)) target = { name, kind: "local" }; + } + } + if (target) { + const prev = refs.get(target.name); + refs.set(target.name, { + kind: target.kind, + typeOnly: (prev ? prev.typeOnly : true) && typePos, + }); + } + } + } + } + ts.forEachChild(node, visit); + }; + visit(s.node); + s.refs = refs; + } + return { source, statements, imports }; +} + +/* ── plan ────────────────────────────────────────────────────────────── */ + +function plan() { + const text = extractBranches(checkout.read(modulePath)); + const { statements } = analyze(text); + const fileOf = new Map(); + for (const [file, names] of Object.entries(config.seeds)) + for (const name of names) { + if (!statements.some((s) => s.names.includes(name))) + throw new Error(`seed ${name} not found`); + fileOf.set(name, file); + } + const keep = new Set(config.keep ?? []); + const referrers = new Map(); + for (const s of statements) + for (const [name, ref] of s.refs) + if (ref.kind === "local") { + if (!referrers.has(name)) referrers.set(name, new Set()); + for (const n of s.names) referrers.get(name).add(n); + } + for (let changed = true; changed;) { + changed = false; + for (const s of statements) { + if ( + s.names.length === 0 || + s.names.some((n) => fileOf.has(n) || keep.has(n)) || + isMutable(s.node) + ) + continue; + const users = new Set(); + for (const n of s.names) + for (const u of referrers.get(n) ?? []) if (!s.names.includes(u)) users.add(u); + const files = new Set([...users].map((u) => fileOf.get(u) ?? null)); + if (users.size > 0 && files.size === 1 && !files.has(null)) { + const [file] = files; + for (const n of s.names) fileOf.set(n, file); + changed = true; + } + } + } + const manifest = {}; + for (const s of statements) { + const file = s.names.map((n) => fileOf.get(n)).find(Boolean); + if (file) (manifest[file] ??= []).push(...s.names); + } + configFiles.write(config.manifest, JSON.stringify(manifest, null, 2) + "\n"); + for (const [file, names] of Object.entries(manifest)) console.log(`${file}: ${names.length}`); +} + +/* ── apply ───────────────────────────────────────────────────────────── */ + +function rebase(spec, fromDir, toDir) { + if (!spec.startsWith(".")) return spec; + let rel = posix.normalize(relative(toDir, resolve(fromDir, spec)).split("\\").join("/")); + if (!rel.startsWith(".")) rel = "./" + rel; + return rel; +} +const moduleSpecifierTo = (fromDir, file) => { + let rel = relative(fromDir, file).split("\\").join("/").replace(/\.ts$/, ".js"); + return rel.startsWith(".") ? rel : "./" + rel; +}; + +/** The statement's text with its leading comments, without leading blank lines. */ +function fullText(text, st) { + return text.slice(st.getFullStart(), st.end).replace(/^\s*\n/, ""); +} + +function apply() { + const manifest = JSON.parse(configFiles.read(config.manifest)); + let text = extractBranches(checkout.read(modulePath)); + const { source, statements, imports } = analyze(text); + const fileOf = new Map(); + for (const [file, names] of Object.entries(manifest)) for (const n of names) fileOf.set(n, file); + const stFile = (s) => s.names.map((n) => fileOf.get(n)).find(Boolean) ?? null; + const nameFile = new Map(); + const nameStmt = new Map(); + for (const s of statements) + for (const n of s.names) { + nameFile.set(n, stFile(s)); + nameStmt.set(n, s); + } + + // Which names need `export`: referenced from another file, or exported already. + const needsExport = new Set(); + for (const s of statements) + for (const [name, ref] of s.refs) + if (ref.kind === "local" && nameFile.get(name) !== stFile(s)) needsExport.add(name); + + const files = new Map(); // file -> { decls: [], imports: Map } + const fileImports = (file) => { + if (!files.has(file)) files.set(file, { decls: [], named: new Map(), other: [] }); + return files.get(file); + }; + const addNamed = (entry, spec, name, imported, typeOnly) => { + if (!entry.named.has(spec)) entry.named.set(spec, new Map()); + const m = entry.named.get(spec); + const prev = m.get(name); + m.set(name, { imported, typeOnly: prev ? prev.typeOnly && typeOnly : typeOnly }); + }; + const moduleEntry = { decls: [], named: new Map(), other: [] }; + for (const s of statements) { + const file = stFile(s); + const entry = file ? fileImports(file) : moduleEntry; + const dir = file ? dirname(join(moduleDir, file)) : moduleDir; + if (file) { + let t = fullText(text, s.node); + if (!isExported(s.node) && s.names.some((n) => needsExport.has(n))) { + const start = s.node.getStart() - s.node.getFullStart(); + const lead = text.slice(s.node.getFullStart(), s.node.getStart()).replace(/^\s*\n/, ""); + t = lead + "export " + text.slice(s.node.getStart(), s.node.end); + void start; + } + entry.decls.push(t); + } + for (const [name, ref] of s.refs) { + if (ref.kind === "local") { + const target = nameFile.get(name) ?? null; + if (target === file) continue; + const targetPath = target ? join(moduleDir, target) : modulePath; + const typeOnly = ref.typeOnly || isTypeDecl(nameStmt.get(name).node); + addNamed(entry, moduleSpecifierTo(dir, targetPath), name, name, typeOnly); + } else if (file) { + const imp = imports.get(name); + const spec = rebase(imp.decl.moduleSpecifier.text, moduleDir, dir); + if (imp.kind === "named") addNamed(entry, spec, name, imp.imported, imp.typeOnly); + else entry.other.push({ spec, kind: imp.kind, name, typeOnly: imp.typeOnly }); + } + } + } + + const renderImports = (entry) => { + const lines = []; + const seen = new Set(); + for (const o of entry.other) { + const key = `${o.kind}:${o.name}:${o.spec}`; + if (seen.has(key)) continue; + seen.add(key); + lines.push( + o.kind === "namespace" + ? `import ${o.typeOnly ? "type " : ""}* as ${o.name} from "${o.spec}";` + : `import ${o.typeOnly ? "type " : ""}${o.name} from "${o.spec}";`, + ); + } + for (const [spec, names] of entry.named) { + const all = [...names.entries()]; + const allType = all.every(([, v]) => v.typeOnly); + const parts = all + .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)) + .map( + ([name, v]) => + `${!allType && v.typeOnly ? "type " : ""}${v.imported === name ? name : `${v.imported} as ${name}`}`, + ); + lines.push(`import ${allType ? "type " : ""}{ ${parts.join(", ")} } from "${spec}";`); + } + return lines; + }; + + // New files. + for (const [file, entry] of files) { + const header = config.headers?.[file]; + const out = [ + ...(header ? [header] : []), + ...renderImports(entry), + "", + entry.decls.join("\n\n"), + "", + ].join("\n"); + const path = join(moduleDir, file); + checkout.mkdir(dirname(path)); + if (checkout.exists(path)) throw new Error(`${file} exists`); + checkout.write(path, out); + } + + // The module: drop moved declarations, rewrite its import block. + const keptRefs = new Set(); + for (const s of statements) + if (!stFile(s)) for (const [name, ref] of s.refs) if (ref.kind === "import") keptRefs.add(name); + const pieces = []; + let cursor = 0; + const removals = []; + for (const s of statements) if (stFile(s)) removals.push([s.node.getFullStart(), s.node.end]); + // Import declarations: rebuild each without bindings nothing uses. + const importEdits = []; + for (const st of source.statements) { + if (!ts.isImportDeclaration(st)) continue; + const clause = st.importClause; + if (!clause) continue; // side-effect import + const used = (n) => keptRefs.has(n); + const def = clause.name && used(clause.name.text) ? clause.name.text : null; + const nb = clause.namedBindings; + let ns = null; + let named = []; + if (nb && ts.isNamespaceImport(nb) && used(nb.name.text)) ns = nb.name.text; + if (nb && ts.isNamedImports(nb)) named = nb.elements.filter((el) => used(el.name.text)); + const keepAll = + def === (clause.name?.text ?? null) && + (!nb || (ts.isNamespaceImport(nb) ? ns !== null : named.length === nb.elements.length)); + if (keepAll) continue; + let replacement = ""; + if (def || ns || named.length) { + const bits = []; + if (def) bits.push(def); + if (ns) bits.push(`* as ${ns}`); + if (named.length) bits.push(`{ ${named.map((el) => el.getText()).join(", ")} }`); + replacement = `import ${clause.isTypeOnly ? "type " : ""}${bits.join(", ")} from ${st.moduleSpecifier.getText()};`; + } + // A removed declaration takes its line break with it. + const end = replacement === "" && text[st.end] === "\n" ? st.end + 1 : st.end; + importEdits.push([st.getStart(), end, replacement]); + } + // Declarations that stay but that a moved one references get `export`. + const exportEdits = []; + for (const s of statements) + if (!stFile(s) && !isExported(s.node) && s.names.some((n) => needsExport.has(n))) + exportEdits.push([s.node.getStart(), s.node.getStart(), "export "]); + const edits = [...removals.map(([a, b]) => [a, b, ""]), ...importEdits, ...exportEdits].sort( + (x, y) => x[0] - y[0], + ); + for (const [a, b, r] of edits) { + pieces.push(text.slice(cursor, a), r); + cursor = b; + } + pieces.push(text.slice(cursor)); + let moduleText = pieces.join(""); + // Imports of moved names the module still uses, and re-exports of moved + // names that were exported (other modules import them from here). + const addLines = renderImports(moduleEntry); + const reexports = new Map(); + for (const s of statements) { + const file = stFile(s); + if (!file || !isExported(s.node)) continue; + const spec = moduleSpecifierTo(moduleDir, join(moduleDir, file)); + if (!reexports.has(spec)) reexports.set(spec, []); + for (const n of s.names) reexports.get(spec).push({ name: n, type: isTypeDecl(s.node) }); + } + for (const [spec, names] of reexports) { + const values = names + .filter((n) => !n.type) + .map((n) => n.name) + .sort(); + const types = names + .filter((n) => n.type) + .map((n) => n.name) + .sort(); + if (values.length) addLines.push(`export { ${values.join(", ")} } from "${spec}";`); + if (types.length) addLines.push(`export type { ${types.join(", ")} } from "${spec}";`); + } + // Insert after the last import declaration. + const reparsed = parse(moduleText); + const lastImport = [...reparsed.statements].filter(ts.isImportDeclaration).at(-1); + const at = lastImport ? lastImport.end : 0; + moduleText = + moduleText.slice(0, at) + + (addLines.length ? "\n" + addLines.join("\n") : "") + + moduleText.slice(at); + moduleText = moduleText.replace(/\n{3,}/g, "\n\n"); + checkout.write(modulePath, moduleText); + // Format what changed with the tools checkout's oxfmt, so the split and + // every rehome of it are formatted the same way. + const touched = [modulePath, ...[...files.keys()].map((f) => join(moduleDir, f))]; + formatFiles(toolsDir, touched, { cwd: repo, stdio: ["ignore", "ignore", "inherit"] }); + for (const [file, entry] of files) console.log(`${file}: ${entry.decls.length} declarations`); +} + +if (command === "plan") plan(); +else if (command === "apply") apply(); +else { + console.error("usage: split-module.mjs plan|apply "); + process.exit(2); +} diff --git a/scripts/verify-native-cli.mjs b/scripts/verify-native-cli.mjs index 115573b5c..92de57938 100644 --- a/scripts/verify-native-cli.mjs +++ b/scripts/verify-native-cli.mjs @@ -94,5 +94,15 @@ if (process.argv.includes("--run")) { }).trim(); if (version !== identity.version) throw new Error(`native command reported ${version}, expected ${identity.version}`); + // tsc-ts, the default checker, runs on the distribution's own platform. + const tscTs = resolve(dirname(binary), hostPath(toolchain.tsc_ts)); + if (windows && !tscTs.endsWith("tsc-ts.exe")) + throw new Error(`tsc_ts is not tsc-ts.exe: ${tscTs}`); + const tscTsVersion = execFileSync(tscTs, ["--version"], { + encoding: "utf8", + env: { ...process.env, PATH: "" }, + }).trim(); + if (!tscTsVersion.startsWith("Version ")) + throw new Error(`the distribution's tsc-ts reported ${JSON.stringify(tscTsVersion)}`); } console.log(`${identity.name}@${identity.version}: native distribution verified`); diff --git a/scripts/zero-cost-compare.mjs b/scripts/zero-cost-compare.mjs index 3158de997..160e33438 100644 --- a/scripts/zero-cost-compare.mjs +++ b/scripts/zero-cost-compare.mjs @@ -394,7 +394,8 @@ export function dynamicTierSymbols(names) { * not yet traced to its typed root. bench/thread-slots, * bench/channel-messages and bench/worker-messages, added later, link them * through worker messages: - * node:worker_threads values are dyn objects. Every other bench workload is fully typed and must link none. The gate and + * node:worker_threads values are dyn objects. bench/dynamic-methods is + * untyped JavaScript. Every other bench workload is fully typed and must link none. The gate and * tests/harness/bench-dynamic-tier.test.ts enforce that, and the test also * fails when a listed workload stops linking them, so remove a workload here * when a leak fix makes its binary clean. */ @@ -408,6 +409,7 @@ export const DYNAMIC_WORKLOADS = new Set([ "bench/intern-table", "bench/thread-slots", "bench/channel-messages", + "bench/dynamic-methods", "bench/worker-messages", "bench/typed-array-scan-workers", "bench/log-summary", diff --git a/tests/corpus/4638-contract-program-lifetime-arrays.ts b/tests/corpus/4638-contract-program-lifetime-arrays.ts new file mode 100644 index 000000000..a50094f8b --- /dev/null +++ b/tests/corpus/4638-contract-program-lifetime-arrays.ts @@ -0,0 +1,117 @@ +import type { ProgramLifetime } from "@scriptc/types"; + +// Arrays of ProgramLifetime class instances: literals, growth, removal +// from both ends, splicing, slicing, concatenation, searching, sorting, +// filling, copying methods, holes, length changes, Sets and Maps built +// from them, arrays held in class fields, optional elements, inspection +// and JSON. Each operation sees the same instances, in the same order. + +class Sym implements ProgramLifetime { + readonly name: string; + readonly rank: number; + readonly decls: Sym[] = []; + parent: Sym | undefined = undefined; + constructor(name: string, rank: number) { + this.name = name; + this.rank = rank; + } +} + +class Scope implements ProgramLifetime { + readonly symbols: Sym[] = []; + readonly optional: (Sym | undefined)[] = []; + readonly nullable: (Sym | null)[] = []; +} + +const names = (xs: readonly (Sym | undefined | null)[]): string => + xs.map((s) => (s === undefined ? "u" : s === null ? "n" : s.name)).join(","); + +const a = new Sym("a", 3); +const b = new Sym("b", 1); +const c = new Sym("c", 2); +const d = new Sym("d", 5); +const e = new Sym("e", 4); + +// Literals, push, pop, shift and unshift. +const xs: Sym[] = [a, b, c]; +xs.push(d); +xs.push(e, a); +console.log(names(xs), xs.length); +console.log(xs.pop()?.name, xs.shift()?.name, names(xs)); +xs.unshift(e, d); +console.log(names(xs), xs[0] === e, xs.at(-1) === e); + +// Growth well past the inline slots, then reads by index. +const many: Sym[] = []; +for (let i = 0; i < 1000; i++) many.push(i % 2 === 0 ? a : new Sym(`s${i}`, i)); +let ranks = 0; +for (let i = 0; i < many.length; i++) ranks += many[i]!.rank; +console.log(many.length, ranks, many[999]!.name, many[998] === a); + +// Splice, slice, concat, spread and copying methods. +const ys = xs.slice(1, 4); +const removed = xs.splice(1, 2, a, b, c); +console.log(names(ys), names(removed), names(xs)); +const zs = [...ys, ...removed].concat(xs); +console.log(names(zs), zs.length); +console.log(names(zs.toReversed()), names(zs.with(0, c)), names(zs.toSpliced(1, 3))); + +// Searching by identity. +console.log(zs.indexOf(c), zs.lastIndexOf(c), zs.includes(d), zs.indexOf(new Sym("c", 2))); +console.log(zs.find((s) => s.rank > 3)?.name, zs.findIndex((s) => s === b), zs.some((s) => s === e)); +console.log(zs.every((s) => s.rank > 0), zs.filter((s) => s.rank % 2 === 1).length); + +// Sorting with comparators, reversing and filling. +const sorted = [...zs].sort((p, q) => p.rank - q.rank || p.name.localeCompare(q.name)); +console.log(names(sorted)); +console.log(names(zs.toSorted((p, q) => q.name.localeCompare(p.name)))); +sorted.reverse(); +console.log(names(sorted)); +const filled: Sym[] = new Array(5).fill(b); +filled.fill(c, 2, 4); +console.log(names(filled), filled.copyWithin(0, 3).map((s) => s.name).join("")); + +// Holes, length changes and iteration. +const holey: Sym[] = [a, b]; +holey[5] = c; +console.log(holey.length, 2 in holey, holey[5]?.name, holey.findIndex((s) => s === c)); +holey.length = 3; +console.log(holey.length, holey[5]); +let visited = ""; +for (const s of xs) visited += s.name; +xs.forEach((s, i) => { + visited += i; +}); +console.log(visited, names(xs.map((s) => s.parent ?? s))); + +// Arrays inside class instances, optional and nullable elements. +const scope = new Scope(); +for (const s of [a, b, c, d, e]) { + scope.symbols.push(s); + s.decls.push(s, a); + s.parent = scope.symbols[0]; +} +scope.optional.push(a, undefined, c); +scope.nullable.push(null, b, null); +scope.optional[3] = e; +console.log(names(scope.symbols), names(scope.optional), names(scope.nullable)); +console.log(scope.optional.indexOf(undefined), scope.nullable.indexOf(null), scope.optional.length); +console.log(c.decls.length, c.decls[1] === a, e.parent === a, names(e.decls)); + +// Sets and Maps built from arrays, and arrays built from them. +const set = new Set(zs); +console.log(set.size, set.has(d), names([...set])); +const byName = new Map(zs.map((s) => [s.name, s] as const)); +console.log(names([...byName.values()]), byName.get("b") === b); +const grouped = new Map(); +for (const s of many.slice(0, 10)) { + const list = grouped.get(s.rank % 3) ?? []; + list.push(s); + grouped.set(s.rank % 3, list); +} +console.log([...grouped.entries()].map(([k, v]) => `${k}:${v.length}`).join(" ")); + +// Inspection and JSON. +console.log(xs.slice(0, 2).map((s) => s.name)); +console.log(JSON.stringify(scope.nullable.map((s) => s?.rank ?? null))); +console.log(JSON.stringify([a, b].map((s) => ({ name: s.name, rank: s.rank })))); diff --git a/tests/corpus/4639-contract-program-lifetime-maps.ts b/tests/corpus/4639-contract-program-lifetime-maps.ts new file mode 100644 index 000000000..21f7da33c --- /dev/null +++ b/tests/corpus/4639-contract-program-lifetime-maps.ts @@ -0,0 +1,120 @@ +import type { ProgramLifetime } from "@scriptc/types"; + +// Maps from strings to ProgramLifetime class instances: get, set, has and +// delete, growth past the linear scan into bucket tables, deletion with +// compaction, clear, every iteration form, copies, Sets of keys and +// values, maps held in class fields, optional and nullable values, +// inspection, and a cast through unknown. Each read sees the same +// instance that was stored. + +class Sym implements ProgramLifetime { + readonly name: string; + readonly rank: number; + readonly members = new Map(); + constructor(name: string, rank: number) { + this.name = name; + this.rank = rank; + } +} + +class Scope implements ProgramLifetime { + readonly locals = new Map(); + readonly maybe = new Map(); + readonly nullable = new Map(); +} + +function show(m: ReadonlyMap): string { + return [...m].map(([k, v]) => `${k}=${v === undefined ? "u" : v === null ? "n" : v.name}`).join(","); +} + +const a = new Sym("a", 3); +const b = new Sym("b", 1); +const c = new Sym("c", 2); + +// get, set, has, delete, overwrite and size. +const m = new Map(); +m.set("x", a).set("y", b); +m.set("z", c); +console.log(show(m), m.size, m.get("y") === b, m.has("z"), m.has("w"), m.get("w")); +m.set("x", c); +console.log(show(m), m.get("x") === c, m.delete("y"), m.delete("y"), show(m)); +const seeded = new Map([ + ["p", a], + ["q", b], +]); +console.log(show(seeded), seeded.get("q")?.rank); + +// Growth into bucket tables, then deletes that compact the entries. +const big = new Map(); +const all: Sym[] = []; +for (let i = 0; i < 3000; i++) { + const s = new Sym(`s${i}`, i); + all.push(s); + big.set(s.name, s); +} +let same = 0; +for (let i = 0; i < 3000; i++) if (big.get(`s${i}`) === all[i]) same++; +console.log(big.size, same, big.get("s1234")?.rank, big.has("s3000")); +for (let i = 0; i < 3000; i++) if (i % 3 !== 0) big.delete(`s${i}`); +same = 0; +for (const [k, v] of big) if (v === all[Number(k.slice(1))] && v.rank % 3 === 0) same++; +console.log(big.size, same, big.get("s2997")?.name, big.get("s2998")); +for (let i = 0; i < 3000; i += 3) big.set(`s${i}`, all[(i + 1) % 3000]!); +console.log(big.get("s0")?.name, big.get("s2997")?.name, big.size); + +// Iteration forms. +const order: string[] = []; +m.forEach((v, k) => { + order.push(`${k}:${v.name}`); +}); +console.log(order.join(" ")); +console.log([...m.keys()].join(), [...m.values()].map((s) => s.name).join(), [...m.entries()].length); +console.log(Array.from(m.values(), (s) => s.rank).join(), Array.from(m).length); +for (const v of m.values()) v.members.set("owner", a); +console.log(c.members.get("owner") === a, a.members.size); + +// Mutation during iteration: entries added by the callback are visited. +const grow = new Map([["g0", a]]); +let visits = 0; +for (const [k] of grow) { + visits++; + if (visits < 4) grow.set(`${k}+`, b); +} +console.log(visits, show(grow)); + +// Copies, Sets of keys and values, clear. +const copy = new Map(m); +copy.set("extra", b); +console.log(show(m), show(copy), copy.get("x") === m.get("x")); +const keySet = new Set(m.keys()); +const valueSet = new Set(m.values()); +console.log([...keySet].join(), valueSet.has(c), valueSet.has(b), valueSet.size); +copy.clear(); +console.log(copy.size, copy.get("x"), show(m)); +copy.set("again", a); +console.log(show(copy)); + +// Maps in class fields, optional and nullable values. +const scope = new Scope(); +scope.locals.set("a", a); +scope.maybe.set("a", a).set("gone", undefined); +scope.nullable.set("b", b).set("none", null); +console.log(show(scope.locals), show(scope.maybe), show(scope.nullable)); +console.log(scope.maybe.has("gone"), scope.maybe.get("gone"), scope.nullable.get("none"), scope.nullable.get("b")?.name); +const v = scope.maybe.get("a"); +console.log(v === a, v?.rank); + +// Objects from entries, inspection and JSON. +const obj = Object.fromEntries(m); +console.log(Object.keys(obj).join(), obj["z"] === c); +console.log(new Map([["only", b]])); +console.log(JSON.stringify([...m.keys()]), JSON.stringify(Object.fromEntries([...m].map(([k, s]) => [k, s.rank])))); + +// Through unknown and back. +const u: unknown = m; +if (u instanceof Map) { + const back = u as Map; + const seen: string[] = []; + for (const [k, s] of back) seen.push(`${String(k)}:${(s as Sym).name}`); + console.log(back.size, seen.join(), back.get("x") === c); +} diff --git a/tests/corpus/4814-integer-or-nan-indexes.ts b/tests/corpus/4814-integer-or-nan-indexes.ts new file mode 100644 index 000000000..1bcfa3e02 --- /dev/null +++ b/tests/corpus/4814-integer-or-nan-indexes.ts @@ -0,0 +1,127 @@ +// Typed-array and array indexes that are exact integers or NaN: element +// reads that may miss, bound to locals or used inline, `charCodeAt` past +// either end, and sums of those. A NaN index reads undefined and a write +// to it is ignored; negative and too-large integers miss the same way. +function show(label: string, value: number | undefined): void { + console.log(label, value); +} + +const bytes = new Uint8Array([65, 67, 71, 84, 0, 255, 3]); +const signed = new Int8Array([-1, 2, -128, 127, 5]); +const wide = new Uint32Array([1, 4294967295, 2, 7]); +const halves = new Int16Array([-300, 4, 6]); +const table = new Int32Array(256); +for (let i = 0; i < 256; i++) table[i] = i * 3 - 100; +const list: number[] = []; +for (let i = 0; i < 300; i++) list.push(i * 0.5); + +function lookups(n: number): number { + let acc = 0; + for (let i = 0; i < n; i++) { + const c = bytes[i]!; + const t = table[c]; + acc = (acc * 7 + (t === undefined ? -1 : t)) % 1000003; + } + return acc; +} + +function listLookups(n: number): number { + let acc = 0; + for (let i = 0; i < n; i++) { + const c = bytes[i]!; + const v = list[c]; + acc = acc * 3 + (v === undefined ? -2 : v); + } + return acc; +} + +function signedIndexes(): string { + const out: string[] = []; + for (let i = -1; i <= signed.length; i++) { + const s = signed[i]!; + out.push(String(table[s]), String(list[s + 2]), String(bytes[s - 1])); + } + return out.join(","); +} + +function wideIndexes(): string { + const out: string[] = []; + for (let i = 0; i <= wide.length; i++) { + const w = wide[i]!; + out.push(String(bytes[w]), String(list[w]), String(table[w - 1])); + } + for (let i = 0; i <= halves.length; i++) { + const h = halves[i]!; + out.push(String(table[h + 301]), String(list[h])); + } + return out.join(","); +} + +function codeUnits(s: string): number { + let acc = 0; + for (let i = -2; i < s.length + 2; i++) { + const u = s.charCodeAt(i); + const t = table[u]; + acc = acc * 5 + (t === undefined ? 1 : t); + acc %= 1000003; + } + return acc; +} + +function writes(n: number): string { + const counts = new Uint16Array(8); + const tally: number[] = [0, 0, 0, 0]; + for (let i = 0; i < n; i++) { + const c = bytes[i]!; + counts[c & 7]++; + counts[c - 60] = 9; + tally[c - 65] = (tally[c - 65] ?? 0) + 1; + } + return `${Array.from(counts).join(" ")} | ${tally.join(" ")} | ${tally.length}`; +} + +function reassigned(n: number): string { + const out: number[] = []; + for (let i = 0; i < n; i++) { + let c = bytes[i]!; + if (c > 200) c = signed[i - 5]!; + out.push(table[c] ?? -7); + // A fraction makes `f` an ordinary number index. + let f = bytes[i]!; + if (i === 2) f = f / 2; + out.push(list[f] ?? -8); + } + return out.join(" "); +} + +function growing(n: number): number { + let x = bytes[0]!; + let acc = 0; + for (let i = 0; i < n; i++) { + x = x + bytes[i]!; + acc = acc + (table[x] ?? -3); + } + return acc; +} + +function hashes(n: number): string { + let h = 0; + let k = 0; + for (let i = 0; i < n; i++) { + const c = bytes[i]!; + h = (h * 31 + c) | 0; + k = ((k << 2) | (c >> 1)) & 0xffff; + } + return `${h} ${k}`; +} + +show("lookups", lookups(bytes.length)); +show("lookups past the end", lookups(bytes.length + 3)); +show("list lookups", listLookups(bytes.length + 2)); +console.log("signed", signedIndexes()); +console.log("wide", wideIndexes()); +show("code units", codeUnits("ACGTÿĀ")); +console.log("writes", writes(bytes.length + 2)); +console.log("reassigned", reassigned(bytes.length + 1)); +show("growing", growing(bytes.length + 1)); +console.log("hashes", hashes(bytes.length + 2)); diff --git a/tests/corpus/4815-touint32-wide-magnitudes.ts b/tests/corpus/4815-touint32-wide-magnitudes.ts new file mode 100644 index 000000000..ab60b8c0f --- /dev/null +++ b/tests/corpus/4815-touint32-wide-magnitudes.ts @@ -0,0 +1,79 @@ +// ToInt32 and ToUint32 (`| 0`, `>>> 0`, `^`, `<<`, `>>`) of doubles around +// 2^53 and 2^63, of multiplicative hash products that pass 2^53, and of +// NaN, the infinities and -0. +const values: number[] = [ + 2 ** 53 - 1, + 2 ** 53, + 2 ** 53 + 2, + -(2 ** 53) - 2, + 2 ** 53 * 3 + 4, + 2 ** 62, + 2 ** 62 + 2 ** 40 + 5 * 2 ** 10, + 2 ** 63 - 1024, + -(2 ** 63) + 1024, + 2 ** 63, + -(2 ** 63), + 2 ** 63 + 2048, + 2 ** 64, + 2 ** 64 - 2048, + -(2 ** 64) + 4096, + 2 ** 80 + 2 ** 33, + 1.5e300, + Infinity, + -Infinity, + NaN, + -0, + 0.75, + -2.5, + 4294967295.5, +]; + +function opaque(i: number): number { + return values[i % values.length]!; +} + +for (let i = 0; i < values.length; i++) { + const v = opaque(i); + console.log( + String(v), + v | 0, + v >>> 0, + v ^ 0x5a5a5a5a, + (v << 3) | 0, + v >> 7, + v >>> 9, + ); +} + +function fnv(bytes: Uint8Array): number { + let h = 2166136261; + for (let i = 0; i < bytes.length; i++) h = ((h ^ bytes[i]!) * 16777619) >>> 0; + return h; +} + +function mix(bytes: Uint8Array): number { + let h = 0; + for (let i = 0; i < bytes.length; i++) h = ((h ^ (bytes[i]! - 64)) * 16777619) | 0; + return h; +} + +function poly(bytes: Uint8Array): number { + let h = 7; + for (let i = 0; i < bytes.length; i++) h = (h * 1000003 + bytes[i]!) | 0; + return h; +} + +function wide(bytes: Uint8Array): number { + // Products near 2^63 and past it: the conversion of each must match. + let h = 3; + for (let i = 0; i < bytes.length; i++) h = ((h ^ bytes[i]!) * 2147483647 * (bytes[i]! * 17 + 1)) | 0; + return h; +} + +const bytes = new Uint8Array(4096); +let seed = 12345; +for (let i = 0; i < bytes.length; i++) { + seed = (seed * 1103515245 + 12345) % 2147483648; + bytes[i] = seed >>> 23; +} +console.log("fnv", fnv(bytes), "mix", mix(bytes), "poly", poly(bytes), "wide", wide(bytes)); diff --git a/tests/corpus/6349-instanceof-subclass-capsules/errors.cjs b/tests/corpus/6349-instanceof-subclass-capsules/errors.cjs new file mode 100644 index 000000000..bf32f8550 --- /dev/null +++ b/tests/corpus/6349-instanceof-subclass-capsules/errors.cjs @@ -0,0 +1,24 @@ +'use strict'; +class UndiciError extends Error { + constructor(message) { + super(message); + this.name = 'UndiciError'; + this.code = 'UND_ERR'; + } + get kind() { + return 'undici'; + } +} +class SocketError extends UndiciError { + constructor(message) { + super(message); + this.name = 'SocketError'; + } +} +class HTTPParserError extends Error { + constructor(message) { + super(message); + this.name = 'HTTPParserError'; + } +} +module.exports = { UndiciError, SocketError, HTTPParserError }; diff --git a/tests/corpus/6349-instanceof-subclass-capsules/main.ts b/tests/corpus/6349-instanceof-subclass-capsules/main.ts new file mode 100644 index 000000000..59a545198 --- /dev/null +++ b/tests/corpus/6349-instanceof-subclass-capsules/main.ts @@ -0,0 +1,27 @@ +// `instanceof` against a JavaScript class reads the prototype chain of a +// value that crossed into the dynamic tier at a base static type (here +// Error): the value is its exact class at runtime, including a subclass +// without methods of its own, which answers false rather than throwing. +import errors from "./errors.cjs"; + +function describe(err: unknown): string { + if (err instanceof errors.UndiciError) return `undici ${err.name}`; + if (err instanceof Error) return `error ${err.name}`; + return "other"; +} +function isSocket(err: unknown): boolean { + return err instanceof errors.SocketError; +} +function fail(kind: number): Error { + if (kind === 0) return new errors.HTTPParserError("bad chunk"); + if (kind === 1) return new errors.SocketError("closed"); + return new RangeError("range"); +} +for (let kind = 0; kind < 3; kind++) { + const err: Error = fail(kind); + console.log(describe(err), isSocket(err)); +} +const base: Error = new errors.UndiciError("base"); +console.log(describe(base), isSocket(base)); +const rejected = Promise.reject(fail(0)); +rejected.catch((err: unknown) => console.log("rejected", describe(err))); diff --git a/tests/corpus/6913-builtin-constructor-shorthand.ts b/tests/corpus/6913-builtin-constructor-shorthand.ts new file mode 100644 index 000000000..7fb5066ed --- /dev/null +++ b/tests/corpus/6913-builtin-constructor-shorthand.ts @@ -0,0 +1,10 @@ +// Builtin constructors in shorthand properties are the constructors themselves. +const direct: Record = { Date: Date, Map: Map, Set: Set }; +const shorthand: Record = { Date, Map, Set, Object, Promise, WeakMap }; +console.log(shorthand.Date === Date, shorthand.Map === direct.Map, shorthand.Set === direct.Set); +console.log(shorthand.Object === Object, shorthand.Promise === Promise, shorthand.WeakMap === WeakMap); +console.log(typeof shorthand.Date, typeof shorthand.Map); +function pick(table: Record, name: string): unknown { + return table[name]; +} +console.log(pick({ Map, Set }, "Set") === Set, pick({ Date }, "Date") === pick(direct, "Date")); diff --git a/tests/corpus/6914-dynamic-builtin-construct.ts b/tests/corpus/6914-dynamic-builtin-construct.ts new file mode 100644 index 000000000..c026a6691 --- /dev/null +++ b/tests/corpus/6914-dynamic-builtin-construct.ts @@ -0,0 +1,54 @@ +// `new` through values that hold builtin constructors. +const ctors: Record = { Date, Map, Set, Object, WeakMap, WeakSet }; +function construct(name: string, ...args: unknown[]): unknown { + const C = ctors[name] as any; + return new C(...args); +} +function make(C: any, ...args: unknown[]): unknown { + return new C(...args); +} +const tag = (value: unknown) => Object.prototype.toString.call(value); + +const d = construct("Date", 0) as Date; +console.log(tag(d), d.getTime(), d.toISOString()); +const d2 = make(Date, 2020, 1, 3, 4, 5, 6, 7) as Date; +console.log(d2.getFullYear(), d2.getMonth(), d2.getDate(), d2.getHours(), d2.getMilliseconds()); +const d3 = make(Date, "2001-02-03T04:05:06Z") as Date; +const d4 = make(Date, d3) as Date; +console.log(d3.getTime(), d4.getTime()); +console.log(typeof make(Date), Number.isNaN((make(Date, "nope") as Date).getTime())); + +const m = construct("Map", [["a", 1], ["b", 2]]) as Map; +console.log(tag(m), m.size, m.get("b"), [...m.keys()].join()); +const m2 = make(Map, m) as Map; +m2.set("c", 3); +console.log(m.size, m2.size, (make(Map) as Map).size, (make(Map, null) as Map).size); + +const s = construct("Set", [1, 2, 2, 3]) as Set; +console.log(tag(s), s.size, s.has(3), (make(Set, "abca") as Set).size); + +const o = construct("Object") as Record; +o.x = 1; +const same = { y: 2 }; +console.log(tag(o), JSON.stringify(o), make(Object, same) === same, tag(make(Object, null))); + +const key = {}; +const w = construct("WeakMap", [[key, "v"]]) as WeakMap; +console.log(tag(w), w.get(key), w.has({})); +const ws = make(WeakSet, [key]) as WeakSet; +console.log(tag(ws), ws.has(key)); + +try { + make(Map, [1]); +} catch (e) { + console.log((e as Error).name, (e as Error).message); +} +try { + make(WeakMap, [[1, 2]]); +} catch (e) { + console.log((e as Error).name, (e as Error).message); +} + +const DateSlot: new () => unknown = Date; +const MapSlot: new (entries: [string, number][]) => unknown = Map; +console.log(tag(new DateSlot()), (new MapSlot([["k", 1]]) as Map).get("k")); diff --git a/tests/corpus/6915-dynamic-builtin-construct.mjs b/tests/corpus/6915-dynamic-builtin-construct.mjs new file mode 100644 index 000000000..dd13aa325 --- /dev/null +++ b/tests/corpus/6915-dynamic-builtin-construct.mjs @@ -0,0 +1,16 @@ +// `new` through stored builtin constructors in JavaScript. +const table = [Date, Map, Set, Object, WeakMap, WeakSet]; +for (const C of table) { + const value = new C(); + console.log(C.name, typeof value, Object.prototype.toString.call(value), value.constructor === C); +} +const Ctor = table[0]; +console.log(new Ctor(86400000).toISOString(), new Ctor(2024, 0, 31).getDate()); +const MapCtor = table[1]; +const map = new MapCtor(Object.entries({ a: 1, b: 2 })); +map.set("c", 3); +console.log(map.size, map.get("a"), [...map.entries()].length); +const SetCtor = table[2]; +console.log(new SetCtor([...map.keys()]).size, new SetCtor([NaN, NaN, 0, -0]).size); +const named = { Date, Map, Set }; +console.log(named.Date === Date, named.Map === table[1], named.Set === table[2]); diff --git a/tests/corpus/6915-key-order-narrowed-throw.ts b/tests/corpus/6915-key-order-narrowed-throw.ts new file mode 100644 index 000000000..d4bc693f2 --- /dev/null +++ b/tests/corpus/6915-key-order-narrowed-throw.ts @@ -0,0 +1,72 @@ +// A record whose reads narrow a possibly-undefined binding (the narrowing +// ternary's throw arm stands in for a value of the record's type) still +// takes its literal's key order: a module-level accumulator typed by an +// interface declared in another order, as an event batcher builds it. +interface Entry { + id: string; + at: number; + text?: string; + stream: "out" | "err"; +} +interface Batch { + team: string; + project: string; + job: string; + origin: "task" | "worker"; + host: string; + entries: Entry[]; + sizes?: number[]; + stage?: "live" | "staging"; + branch?: string; + run?: string; + entry?: string; + path?: string; +} +interface Job { + id: string; + run?: string; + project: string; + team: string; + host: string; + stage: string | null; +} +let acc: Batch | undefined; + +function append(j: Job, text: string): void { + if (!acc) { + acc = { + run: j.run, + job: j.id, + stage: j.stage === null ? "staging" : "live", + project: j.project, + team: j.team, + origin: "task", + host: j.host, + entry: ".", + entries: [], + }; + } + acc.entries.push({ id: String(acc.entries.length), at: 1, text, stream: "out" }); + setTimeout(() => { + const batch = acc; + if (!batch) return; + acc = undefined; + for (const line of split(batch)) console.log(line); + }, 1); +} + +function split(batch: Batch): string[] { + const json = JSON.stringify(batch); + if (json.length < 200 || batch.entries.length < 2) return [json]; + const index = Math.floor(batch.entries.length / 2); + const first = { ...batch, entries: batch.entries.slice(0, index) }; + const second = { ...batch, entries: batch.entries.slice(index) }; + return [...split(first), ...split(second)]; +} + +const j: Job = { id: "job", run: "r1", project: "prj", team: "core", host: "h", stage: "live" }; +append(j, "one"); +append(j, "two"); +setTimeout(() => { + for (let i = 0; i < 6; i++) append(j, "line " + i); +}, 5); diff --git a/tests/corpus/6916-index-record-spread-overrides.ts b/tests/corpus/6916-index-record-spread-overrides.ts new file mode 100644 index 000000000..0a33b0e1b --- /dev/null +++ b/tests/corpus/6916-index-record-spread-overrides.ts @@ -0,0 +1,60 @@ +// Spreading a fixed-shape record into an index-signature record: the +// spread's keys enter in declaration order, and a field whose type does +// not fit the index slot is valid when a later property overrides it +// (its key keeps the spread's position, the override supplies the value). +interface FailureTags { + phase: string; + httpStatus: number; + retried: boolean; + region: string; +} +interface Counts { + zeta: number; + alpha: number; + mid: number; +} + +function emit(name: string, tags: { [key: string]: string }): void { + const parts: string[] = []; + for (const key of Object.keys(tags)) parts.push(key + ":" + tags[key]); + console.log(name, parts.join(","), JSON.stringify(tags)); +} + +const tags: FailureTags = { phase: "fetch", httpStatus: 503, retried: true, region: "iad1" }; +for (let attempt = 1; attempt <= 2; attempt++) { + emit("attempt_failed", { + ...tags, + attempt: String(attempt), + httpStatus: String(tags.httpStatus), + retried: tags.retried ? "yes" : "no", + }); +} + +const base: Record = { team: "core", owner: "infra" }; +emit("queue", { ...base, owner: "builds", shard: "7" }); +emit("size", { ...base }); + +const counts: Counts = { zeta: 3, alpha: 1, mid: 2 }; +const scores: Record = { ...counts, alpha: 10 }; +console.log(Object.keys(scores).join(","), JSON.stringify(scores)); +const labels: Record = { first: "a", ...{ zeta: "z", alpha: "a" }, last: "l" }; +console.log(Object.entries(labels).map(([k, v]) => k + "=" + v).join(" ")); +const flags: { [key: string]: boolean } = { ...counts, mid: true, zeta: false, alpha: true }; +console.log(JSON.stringify(flags)); + +// A source whose values are built in another order than its interface +// declares: the spread follows the order the values are built in. +interface Span { + start: number; + end: number; + label?: string; +} +function span(start: number, size: number, label?: string): Span { + return label === undefined ? { end: start + size, start } : { label, end: start + size, start }; +} +const spans: Record = { ...span(1, 4), step: 2 }; +console.log(Object.keys(spans).join(","), JSON.stringify(spans)); +const named: Record = { first: 0, ...span(5, 2, "w"), last: 9 }; +console.log(Object.keys(named).join(","), JSON.stringify(named)); +const both: Record = { ...counts, ...{ end: 3, start: 1 }, mid: 7 }; +console.log(Object.keys(both).join(","), JSON.stringify(both)); diff --git a/tests/corpus/6916-local-class-static-self-reference.mjs b/tests/corpus/6916-local-class-static-self-reference.mjs new file mode 100644 index 000000000..bc19cd0e9 --- /dev/null +++ b/tests/corpus/6916-local-class-static-self-reference.mjs @@ -0,0 +1,52 @@ +// Static fields, static blocks and static methods of function-local classes +// that read the class by its name while the class is being defined. +function registry(prefix) { + class Registry { + static label = Registry.name + ":" + prefix; + static instances = []; + static { + Registry.ready = Registry.instances.length === 0; + } + constructor(id) { + this.id = id; + Registry.instances.push(this); + } + static create(id) { + return new Registry(id); + } + } + return Registry; +} +const R = registry("main"); +R.create(1); +R.create(2); +console.log(R.label, R.ready, R.instances.map((r) => r.id)); +const R2 = registry("other"); +console.log(R2.label, R2.instances.length, R2 === R, R.instances.length); + +function counter(step) { + class Counter { + static zero = new Counter(0); + static one = Counter.zero.next(); + constructor(value) { + this.value = value; + } + next() { + return new Counter(this.value + step); + } + } + return Counter; +} +const C = counter(5); +console.log(C.zero.value, C.one.value, C.one.next().value, C.one instanceof C); + +function described() { + class Shape { + static kinds = ["circle", "square"]; + static describe() { + return Shape.kinds.map((kind) => kind + "@" + Shape.name).join(","); + } + } + return Shape.describe(); +} +console.log(described()); diff --git a/tests/corpus/6917-local-class-expression-self-reference.ts b/tests/corpus/6917-local-class-expression-self-reference.ts new file mode 100644 index 000000000..cff612d29 --- /dev/null +++ b/tests/corpus/6917-local-class-expression-self-reference.ts @@ -0,0 +1,53 @@ +// Function-local class expressions that refer to themselves by their own name. +function counter(start: number) { + const C = class Counter { + value: number; + constructor(value: number) { this.value = value; } + next(): Counter { return new Counter(this.value + 1); } + static first(): Counter { return new Counter(start); } + static isSelf(value: unknown): boolean { return value === Counter; } + }; + return C; +} +const Counter1 = counter(10); +console.log(Counter1.first().next().next().value); +console.log(Counter1.isSelf(Counter1), Counter1.isSelf(counter(1)), Counter1.name); + +function box(seed: T) { + return class Box { + value: T; + constructor(value: T) { this.value = value; } + again(): Box { return new Box(this.value); } + static of(): Box { return new Box(seed); } + }; +} +const StringBox = box("seed"); +console.log(new StringBox("x").again().value, StringBox.of().value); +const NumberBox = box(7); +console.log(NumberBox.of().again().value + 1); + +function nested(n: number) { + const Outer = class Tree { + children: Tree[] = []; + static build(depth: number): Tree { + const tree = new Tree(); + if (depth > 0) tree.children.push(Tree.build(depth - 1), Tree.build(depth - 1)); + return tree; + } + size(): number { return 1 + this.children.reduce((total, child) => total + child.size(), 0); } + }; + return Outer.build(n).size(); +} +console.log(nested(3)); + +// The only reference to the name is a shorthand property. +function registry(tag: string) { + return class Entry { + readonly tag = tag; + static table(): Record { + return { Entry }; + } + }; +} +const Entry1 = registry("e"); +console.log(Entry1.table().Entry === Entry1, new Entry1().tag); diff --git a/tests/corpus/6918-local-class-expression-self-reference.mjs b/tests/corpus/6918-local-class-expression-self-reference.mjs new file mode 100644 index 000000000..4b2440674 --- /dev/null +++ b/tests/corpus/6918-local-class-expression-self-reference.mjs @@ -0,0 +1,61 @@ +// Function-local class expressions in JavaScript whose static members and +// methods read the class by its own name. +function point(scale) { + const P = class Point { + static origin = new Point(0, 0); + static unit = Point.scaled(1); + constructor(x, y) { + this.x = x; + this.y = y; + } + static scaled(n) { + return new Point(n * scale, n * scale); + } + plus(other) { + return new Point(this.x + other.x, this.y + other.y); + } + }; + return P; +} +const P = point(3); +const sum = P.origin.plus(P.unit).plus(P.unit); +console.log(sum.x, sum.y, P.unit instanceof P, P.name); + +function selfName() { + const K = class Named { + static who() { + return Named === this; + } + }; + return [K.who(), K.name, typeof K]; +} +console.log(selfName()); + +function factory(tag) { + return class Tagged { + static count = 0; + constructor() { + Tagged.count++; + } + static make() { + return new Tagged(); + } + get tag() { + return tag + Tagged.count; + } + }; +} +const T = factory("t"); +T.make(); +console.log(T.make().tag, T.count, factory("u").count); + +// The only reference to the name is a shorthand property. +function packed() { + const K = class Packed { + static wrap() { + return { Packed }; + } + }; + return [K.wrap().Packed === K, K.wrap().Packed.name]; +} +console.log(packed()); diff --git a/tests/corpus/6919-local-class-inspect-name.mjs b/tests/corpus/6919-local-class-inspect-name.mjs new file mode 100644 index 000000000..022895591 --- /dev/null +++ b/tests/corpus/6919-local-class-inspect-name.mjs @@ -0,0 +1,51 @@ +// console.log prints instances of function-local classes with prototype +// methods under their class names. +function point() { + class Point { + constructor(x, y) { + this.x = x; + this.y = y; + } + plus(other) { + return new Point(this.x + other.x, this.y + other.y); + } + } + return Point; +} +const Point = point(); +const a = new Point(1, 2); +console.log(a, a.plus(a)); +console.log([new Point(0, 0)], { p: new Point(5, 6) }); + +function animals() { + class Animal { + constructor(name) { + this.name = name; + } + speak() { + return this.name + " makes a sound"; + } + } + class Dog extends Animal { + speak() { + return this.name + " barks"; + } + } + return [new Animal("cat"), new Dog("rex")]; +} +const [cat, dog] = animals(); +console.log(cat, dog); + +function anonymous() { + const Named = class { + value() { + return 1; + } + }; + return [new Named(), new (class { + value() { + return 2; + } + })()]; +} +console.log(anonymous()); diff --git a/tests/corpus/6921-local-subclass-override-dispatch.ts b/tests/corpus/6921-local-subclass-override-dispatch.ts new file mode 100644 index 000000000..0476e56c7 --- /dev/null +++ b/tests/corpus/6921-local-subclass-override-dispatch.ts @@ -0,0 +1,51 @@ +// Overrides declared by function-local subclasses dispatch on the receiver +// when the call site lowers before the function that declares them. +class Shape { + area(): number { return 0; } + get label(): string { return "shape"; } + describe(): string { return this.label + ":" + this.area(); } +} + +function square(side: number): Shape { + class Square extends Shape { + area(): number { return side * side; } + get label(): string { return "square"; } + } + return new Square(); +} + +function scaled(factor: number): Shape { + class Scaled extends Shape { + area(): number { return factor; } + } + class Doubled extends Scaled { + area(): number { return super.area() * 2; } + } + return new Doubled(); +} + +const shapes: Shape[] = [new Shape(), square(3), scaled(5)]; +for (const shape of shapes) console.log(shape.area(), shape.label, shape.describe()); + +function pair() { + class Base { + name(): string { return "base"; } + } + class Derived extends Base { + name(): string { return "derived"; } + } + return [new Base(), new Derived()]; +} +const [first, second] = pair(); +console.log(first.name(), second.name()); + +function counter() { + const Tally = class { + count(): number { return 1; } + }; + const Double = class extends Tally { + count(): number { return 2; } + }; + return new Double(); +} +console.log(counter().count()); diff --git a/tests/corpus/6922-local-subclass-override-dispatch.mjs b/tests/corpus/6922-local-subclass-override-dispatch.mjs new file mode 100644 index 000000000..8439c563d --- /dev/null +++ b/tests/corpus/6922-local-subclass-override-dispatch.mjs @@ -0,0 +1,51 @@ +// Overrides declared by function-local subclasses in JavaScript dispatch on +// the receiver when the call site lowers before the declaring function. +class Animal { + constructor(name) { + this.name = name; + } + speak() { + return this.name + " makes a sound"; + } +} + +function dog(name) { + class Dog extends Animal { + speak() { + return this.name + " barks"; + } + } + return new Dog(name); +} + +function animals() { + class Bird extends Animal { + speak() { + return this.name + " sings"; + } + } + return [new Animal("cat"), new Bird("robin"), dog("rex")]; +} + +for (const animal of animals()) console.log(animal.speak()); +const speak = (animal) => animal.speak(); +console.log(speak(dog("fido"))); + +function pets() { + class Pet { + constructor(name) { + this.name = name; + } + greet() { + return this.name + " waves"; + } + } + class Cat extends Pet { + greet() { + return this.name + " purrs"; + } + } + return [new Pet("ann"), new Cat("tom")]; +} +const [pet, cat] = pets(); +console.log(pet.greet(), cat.greet()); diff --git a/tests/corpus/6923-instanceof-builtin-constructor-values.mjs b/tests/corpus/6923-instanceof-builtin-constructor-values.mjs new file mode 100644 index 000000000..929b048e3 --- /dev/null +++ b/tests/corpus/6923-instanceof-builtin-constructor-values.mjs @@ -0,0 +1,13 @@ +// `instanceof` against builtin constructors held as values, with instances of +// program classes and native values on the left. +class Dog { + constructor(name) { + this.name = name; + } +} +class Puppy extends Dog {} +const kinds = [Map, Set, RegExp, Promise, WeakMap, WeakSet, Date]; +const values = []; +values.push(new Dog("rex"), new Puppy("bit"), new Map(), new Set(), /x/g, Promise.resolve(1)); +values.push(new WeakMap(), new WeakSet(), new Date(0), { name: "plain" }, [1, 2]); +for (const C of kinds) console.log(C.name, values.map((v) => (v instanceof C ? 1 : 0)).join("")); diff --git a/tests/corpus/6924-instanceof-class-values-native.mjs b/tests/corpus/6924-instanceof-class-values-native.mjs new file mode 100644 index 000000000..82e8bdcf2 --- /dev/null +++ b/tests/corpus/6924-instanceof-class-values-native.mjs @@ -0,0 +1,19 @@ +// `instanceof` against program classes held as values, with native builtin +// values on the left: their prototype chains hold no program class. +class Animal { + constructor(name) { + this.name = name; + } +} +class Dog extends Animal {} +class Cat extends Animal {} +const classes = { Animal: Animal, Dog: Dog, Cat: Cat }; +const values = []; +values.push(new Map(), new Set(), /x/, Promise.resolve(1), new Date(0), new WeakMap(), new WeakSet()); +values.push(new Dog("rex"), new Cat("tom"), new Animal("any"), {}, [], null); +for (const name of Object.keys(classes)) { + const C = classes[name]; + console.log(name, values.map((v) => (v instanceof C ? 1 : 0)).join("")); +} +const holder = { type: Dog }; +console.log(new Map() instanceof holder.type, new Dog("a") instanceof holder.type); diff --git a/tests/corpus/6925-instanceof-dynamic-constructors.ts b/tests/corpus/6925-instanceof-dynamic-constructors.ts new file mode 100644 index 000000000..0a39b92b2 --- /dev/null +++ b/tests/corpus/6925-instanceof-dynamic-constructors.ts @@ -0,0 +1,64 @@ +// `instanceof` in TypeScript against right-hand sides that are not class names: +// constructor-typed parameters, constructors read from tables as any, builtin +// constructors held as values, and class values held in properties. +class Animal { + name: string; + constructor(name: string) { this.name = name; } +} +class Dog extends Animal { + bark(): string { return this.name + " barks"; } +} +class Plain { v = 1; } + +// Constructor-typed parameters. +function isA(value: unknown, C: new (...args: never[]) => unknown): boolean { + return value instanceof C; +} +const dog = new Dog("rex"); +console.log(isA(dog, Animal), isA(dog, Dog), isA(new Animal("a"), Dog), isA(new Plain(), Animal), isA(dog, Plain)); +console.log(isA(new Date(0), Date), isA(new Map(), Map), isA({}, Map), isA("text", Animal), isA(null, Animal)); + +// Narrowing through a constructor-typed value. +function describe(value: Animal, C: typeof Dog): string { + if (value instanceof C) return value.bark(); + return value.name; +} +console.log(describe(dog, Dog), describe(new Animal("cat"), Dog)); + +// A table of constructors read as any. +const table: Record = { Date, Map, Set, RegExp, Promise, WeakMap, WeakSet, Dog }; +const samples: unknown[] = [new Date(0), new Map(), new Set(), /x/, Promise.resolve(1), new WeakMap(), new WeakSet(), [1], dog, { a: 1 }]; +for (const name of Object.keys(table)) { + const C = table[name]; + console.log(name, samples.map((s) => (s instanceof C ? 1 : 0)).join("")); +} + +// A right-hand side that is not callable throws Node's TypeError. +const notCallable: any = {}; +try { + console.log(dog instanceof notCallable); +} catch (e) { + console.log((e as Error).name, (e as Error).message); +} + +// Typed native values passed as unknown, against builtin constructors held as any. +const typedValues: [string, unknown][] = [ + ["map", new Map([["a", 1]])], + ["set", new Set([1])], + ["regex", /a/g], + ["promise", Promise.resolve(1)], + ["weak", new WeakMap()], + ["date", new Date(5)], + ["array", [1]], + ["record", { a: 1 }], +]; +const builtins: Record = { Map, Set, RegExp, Promise, WeakMap, Date }; +for (const [label, value] of typedValues) { + console.log(label, Object.keys(builtins).map((name) => (value instanceof builtins[name] ? 1 : 0)).join("")); +} + +// Class values held in properties, against native values and instances. +const holder = { type: Dog, base: Animal }; +const map = new Map(); +const pattern = /x/; +console.log(map instanceof holder.type, pattern instanceof holder.base, dog instanceof holder.type, dog instanceof holder.base); diff --git a/tests/corpus/6926-dynamic-collection-seeds.mjs b/tests/corpus/6926-dynamic-collection-seeds.mjs new file mode 100644 index 000000000..a48fab894 --- /dev/null +++ b/tests/corpus/6926-dynamic-collection-seeds.mjs @@ -0,0 +1,28 @@ +// Set, Map, WeakSet and Object.fromEntries over checked-dynamic iterables: +// native iterators, generators, objects with a Symbol.iterator method and +// collections, as seeds that reach the runtime untyped. +function* pairs() { yield ["g", 1]; yield ["h", 2]; } +function* values() { yield 1; yield 2; yield 2; } +const iterable = { *[Symbol.iterator]() { yield ["i", 3]; } }; +const valueIterable = { *[Symbol.iterator]() { yield "a"; yield "b"; } }; +function attempt(label, make) { + try { console.log(label, make()); } catch (e) { console.log(label, "threw", e.name, e.message); } +} +function viaSet(x) { return new Set(x).size; } +function viaMap(x) { return [...new Map(x).keys()].join(); } +function viaWeakSet(x) { return new WeakSet(x).has(key); } +const key = {}; +const m = new Map([["a", 1], ["b", 2]]); +attempt("set keys()", () => viaSet(m.keys())); +attempt("set values()", () => viaSet(new Set([1, 2]).values())); +attempt("set generator", () => viaSet(values())); +attempt("set iterable", () => viaSet(valueIterable)); +attempt("set string", () => viaSet("abca")); +attempt("set number", () => viaSet(5)); +attempt("map generator", () => viaMap(pairs())); +attempt("map iterable", () => viaMap(iterable)); +attempt("map set", () => viaMap(new Set([["s", 1]]))); +attempt("map entries()", () => viaMap(m.entries())); +attempt("map bad entry", () => viaMap([1])); +attempt("weakset generator", () => viaWeakSet((function* () { yield key; })())); +attempt("fromEntries generator", () => JSON.stringify(Object.fromEntries(pairs()))); diff --git a/tests/corpus/6927-dynamic-collection-seeds.ts b/tests/corpus/6927-dynamic-collection-seeds.ts new file mode 100644 index 000000000..c57433d57 --- /dev/null +++ b/tests/corpus/6927-dynamic-collection-seeds.ts @@ -0,0 +1,9 @@ +// Set and Map seeds held as any: native iterators and generators. +function* values(): Generator { yield 1; yield 2; yield 2; } +function* pairs(): Generator<[string, number]> { yield ["a", 1]; yield ["b", 2]; } +const m = new Map([["x", 1], ["y", 2]]); +const anyKeys: any = m.keys(); +const anyGen: any = values(); +const anyPairs: any = pairs(); +console.log(new Set(anyKeys).size, new Set(anyGen).size, new Map(anyPairs).size); +console.log(new Set(values()).size, new Map(pairs()).get("b"), new Set(m.values()).size); diff --git a/tests/corpus/6987-array-view-named-properties.ts b/tests/corpus/6987-array-view-named-properties.ts index ee357d072..faba7dc90 100644 --- a/tests/corpus/6987-array-view-named-properties.ts +++ b/tests/corpus/6987-array-view-named-properties.ts @@ -102,13 +102,3 @@ for (const key in visited) { console.log("visited key", key); if (key === "first") delete (visited as unknown as Bag).second; } - -// A non-configurable numeric-looking property refuses the delete and stays. -const fixed: string[] = ["a"]; -Object.defineProperty(fixed as unknown as Bag, "-3", { value: "kept", enumerable: true }); -try { - delete (fixed as unknown as Bag)["-3"]; -} catch (e) { - console.log((e as Error).name); -} -console.log((fixed as unknown as Bag)["-3"], "-3" in (fixed as unknown as Bag)); diff --git a/tests/corpus/6998-typed-array-while-loop-versions.ts b/tests/corpus/6998-typed-array-while-loop-versions.ts new file mode 100644 index 000000000..383a2437f --- /dev/null +++ b/tests/corpus/6998-typed-array-while-loop-versions.ts @@ -0,0 +1,149 @@ +// While loops that scan typed arrays with a counter below a bound: scanners +// with nested loops and `&&` conditions, `| 0` increments, look-behind and +// look-ahead reads, writes, DataView reads, string-length bounds, and +// bounds or starts past the array, fractional, negative, -0 or NaN. + +function words(bytes: Uint8Array, end: number): number { + let pos = 0; + let count = 0; + while (pos < end) { + while (pos < end && (bytes[pos] === 32 || bytes[pos] === 10)) pos++; + if (pos < end) count++; + while (pos < end && bytes[pos] !== 32 && bytes[pos] !== 10) pos++; + } + return count; +} + +function identifiers(chars: Uint16Array, start: number, end: number): string { + let pos = start; + let count = 0; + let mix = 0; + while (pos < end) { + let hash = 0x811c9dc5 | 0; + while (pos < end) { + const c = chars[pos]!; + if (!((c >= 97 && c <= 122) || c === 95)) break; + hash = Math.imul(hash ^ c, 0x01000193); + pos = (pos + 1) | 0; + } + mix = (mix + hash) | 0; + count++; + pos = (pos + 1) | 0; + } + return `${count} ${mix >>> 0} ${pos}`; +} + +function around(values: Int16Array, start: number, end: number): number[] { + const out: number[] = []; + let i = start; + while (i <= end) { + out.push(values[i - 1]! + values[i + 2]! * 2); + i += 1; + } + return out; +} + +function fill(out: Float32Array, end: number, value: number): number { + let i = 0; + while (i < end) { + out[i] = value + i; + if (i < end - 1 && out[i] > 2) { + i += 2; + continue; + } + i++; + } + return i; +} + +function afterInner(bytes: Uint8Array): number { + let pos = 0; + let sum = 0; + while (pos < bytes.length) { + while (pos < bytes.length && bytes[pos] !== 0) pos++; + // The inner loop may stop at the length: this read stays checked. + sum += bytes[pos] ?? -1; + pos++; + } + return sum; +} + +function firstAbove(samples: Float64Array, limit: number): number { + let i = 0; + while (i < samples.length) { + if (samples[i]! > limit) return i; + i = i + 1; + } + return -1; +} + +function halves(view: DataView, end: number): number { + let at = 0; + let total = 0; + while (at < end) { + total += view.getUint16(at, true); + at += 2; + } + return total; +} + +function letters(text: string, codes: Uint8Array): number { + let i = 0; + let sum = 0; + while (i < text.length) { + sum += codes[i]! * (text.charCodeAt(i) & 7); + i++; + } + return sum; +} + +function notCounters(bytes: Uint8Array): string { + let i = 1; + let doubled = 0; + while (i < bytes.length) { + doubled += bytes[i]!; + i = i * 2; + } + let j = bytes.length - 1; + let down = 0; + while (j < bytes.length && j >= 0) { + down += bytes[j]!; + j--; + } + return `${doubled} ${down}`; +} + +const text = "let alpha = beta_gamma\nreturn delta "; +const bytes = new Uint8Array(text.length); +const chars = new Uint16Array(text.length); +for (let i = 0; i < text.length; i++) { + bytes[i] = text.charCodeAt(i); + chars[i] = text.charCodeAt(i); +} +console.log("words", words(bytes, bytes.length), words(bytes, 10), words(bytes, bytes.length + 5), words(bytes, 7.5), words(bytes, NaN)); +console.log("identifiers", identifiers(chars, 0, chars.length)); +console.log("identifiers past end", identifiers(chars, 4, chars.length + 3)); +console.log("identifiers starts", identifiers(chars, -2, 9), identifiers(chars, 2.5, 12), identifiers(chars, -0, 6)); + +const values = new Int16Array([5, -6, 7, -8, 9, -10, 11]); +console.log("around", around(values, 1, 4).join(","), around(values, 0, 6).join(","), around(values, -1, 1).join(",")); + +const floats = new Float32Array(6); +console.log("fill", fill(floats, 6, 0.5), floats.join(",")); +const short = new Float32Array(3); +console.log("fill past end", fill(short, 5, 1), short.join(",")); + +console.log("after inner", afterInner(new Uint8Array([1, 2, 0, 3, 0, 4, 5]))); +console.log("first above", firstAbove(new Float64Array([0.5, 1.5, 2.5]), 1), firstAbove(new Float64Array([0.5]), 1)); + +const buffer = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); +const view = new DataView(buffer.buffer); +console.log("halves", halves(view, 8)); +try { + console.log("halves past end", halves(view, 10)); +} catch (error) { + console.log((error as Error).name, (error as Error).message); +} + +console.log("letters", letters("abcdef", new Uint8Array([1, 2, 3, 4, 5, 6])), letters("abcdefgh", new Uint8Array([1, 2, 3]))); +console.log("not counters", notCounters(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]))); diff --git a/tests/corpus/7002-empty-array-contextual-slots/main.ts b/tests/corpus/7002-empty-array-contextual-slots/main.ts new file mode 100644 index 000000000..ec08a297a --- /dev/null +++ b/tests/corpus/7002-empty-array-contextual-slots/main.ts @@ -0,0 +1,58 @@ +// Empty array literals whose own type is `never[]`: `return []` in an async +// function (its contextual type is `string[] | PromiseLike`) and +// an object literal's `files: []` field returned or passed where typed +// arrays are declared. Each value holds the declared element type. +interface Listing { + files: string[]; + sizes: number[]; +} + +async function readNames(text: string): Promise { + try { + return JSON.parse(text) as string[]; + } catch { + return []; + } +} + +const loadIds = async (fail: boolean): Promise => { + if (fail) return []; + return [1, 2]; +}; + +function emptyListing(): Listing { + const listing = { files: [], sizes: [] }; + return listing; +} + +function index(): Record<"files" | "dirs", string[]> { + const out = { files: [], dirs: [] }; + return out; +} + +function total(listing: Listing): number { + return listing.sizes.reduce((sum, size) => sum + size, 0); +} + +async function main(): Promise { + const names = await readNames("not json"); + names.push("fallback"); + console.log(names, (await readNames('["a","b"]')).join("+")); + const ids = await loadIds(true); + ids.push(7); + console.log(ids, await loadIds(false)); + + const listing = emptyListing(); + listing.files.push("a.txt"); + listing.sizes.push(3, 4); + console.log(listing, total(listing)); + + const dirs = index(); + dirs.dirs.push("src"); + console.log(JSON.stringify(dirs), dirs.files.length); + + const blank = { files: [], sizes: [] }; + console.log(total(blank), JSON.stringify(blank)); +} + +main(); diff --git a/tests/corpus/7005-dyn-constructor-instanceof/main.ts b/tests/corpus/7005-dyn-constructor-instanceof/main.ts new file mode 100644 index 000000000..21fcbbd26 --- /dev/null +++ b/tests/corpus/7005-dyn-constructor-instanceof/main.ts @@ -0,0 +1,12 @@ +// A package's own functions, read off an object the checker cannot type, +// on the right of `instanceof`: Node walks the value's prototype chain for +// an ES5 constructor's prototype, at module load and in calls, and throws a +// TypeError for an arrow function, which has no prototype, when the value +// is an object. +import { arrowCheck, arrowObjects, describe, rejects, tags } from "tag-types"; + +console.log(tags()); +console.log(describe("{}"), describe("[1]"), describe("null"), describe('"s"')); +console.log(rejects('{"tag":"!x"}')); +console.log(arrowCheck("{}"), arrowCheck("1")); +console.log(arrowObjects()); diff --git a/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/index.d.ts b/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/index.d.ts new file mode 100644 index 000000000..5b451adfa --- /dev/null +++ b/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/index.d.ts @@ -0,0 +1,5 @@ +export declare function describe(json: string): string; +export declare function tags(): string; +export declare function rejects(json: string): string; +export declare function arrowCheck(json: string): string; +export declare function arrowObjects(): string; diff --git a/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/index.js b/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/index.js new file mode 100644 index 000000000..81165bd58 --- /dev/null +++ b/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/index.js @@ -0,0 +1,71 @@ +"use strict"; +// A bundler's lazy module object holding an ES5 constructor: the checker +// cannot type the object, so the constructor is an untyped value where +// `instanceof` tests it. +var types = {}; +var hasTypes; +function requireTypes() { + if (hasTypes) return types; + hasTypes = 1; + function Type(tag, options) { + this.tag = tag; + this.kind = (options && options.kind) || "scalar"; + } + function Schema(list) { + this.list = list; + } + types.Type = Type; + types.Schema = Schema; + types.make = (tag) => new Type(tag); + return types; +} +var loaded = requireTypes(); +var Type = loaded.Type; +var Schema = loaded.Schema; +var make = loaded.make; + +function compileList(list) { + var tags = []; + list.forEach(function (item) { + if (!(item instanceof Type)) throw new Error("not a Type: " + JSON.stringify(item)); + tags.push(item.tag + ":" + item.kind); + }); + return tags; +} +// Runs when the module loads, as js-yaml's default schema does. +var defaults = new Schema(compileList([new Type("!str"), new Type("!seq", { kind: "sequence" })])); + +module.exports.describe = function (json) { + var value = JSON.parse(json); + return [value instanceof Type, value instanceof Schema, defaults instanceof Schema, defaults instanceof Type].join(","); +}; +module.exports.tags = function () { + return defaults.list.join(" "); +}; +function rejects(json) { + try { + compileList([JSON.parse(json)]); + return "accepted"; + } catch (error) { + return error.message; + } +} +module.exports.rejects = rejects; +// An arrow function has no prototype: Node throws a TypeError for an object. +module.exports.arrowCheck = function (json) { + try { + return String(JSON.parse(json) instanceof make); + } catch (error) { + return error.name + ": " + error.message; + } +}; +// Every object reaches the TypeError, typed arrays, promises and Maps too. +module.exports.arrowObjects = function () { + return [new Uint8Array(2), Promise.resolve(1), new Map(), [1]].map(function (value) { + try { + return String(value instanceof make); + } catch (error) { + return error.name; + } + }).join(","); +}; diff --git a/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/package.json b/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/package.json new file mode 100644 index 000000000..a469a45f3 --- /dev/null +++ b/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/package.json @@ -0,0 +1 @@ +{ "name": "tag-types", "version": "1.0.0", "main": "./index.js", "types": "./index.d.ts" } diff --git a/tests/corpus/7006-evolving-array-aliases.ts b/tests/corpus/7006-evolving-array-aliases.ts new file mode 100644 index 000000000..fce1d0946 --- /dev/null +++ b/tests/corpus/7006-evolving-array-aliases.ts @@ -0,0 +1,127 @@ +// An unannotated `const xs = []` inside a function whose writes store values +// of more than one element type: an alias taken between the writes +// (`const alias = xs`) and an alias of that alias name the same array, so +// they see every later write, as in Node. A write of another element type +// after reads that agree on one type also keeps the array untyped. +function chain(): string { + const xs = []; + xs.push(1); + const a = xs; + const b = a; + xs.push("two"); + b.push(3); + return `${xs.join(",")} ${a.length} ${b.length} ${a === xs} ${b === xs}`; +} +function viaAlias(): string { + const xs = []; + xs.push("a"); + const alias = xs; + alias.push("b"); + xs.push(2); + return `${xs.join(",")} ${alias.join(",")}`; +} +function letAlias(): string { + const xs = []; + xs.push(true); + let alias = xs; + xs.push("t"); + const seen = alias.length; + alias = []; + return `${seen} ${alias.length} ${xs.length}`; +} +function nested(flag: boolean): string { + const xs = []; + xs.push(1); + if (flag) { + const inner = xs; + xs.push("n"); + return `${inner.length} ${inner.join(":")}`; + } + return `${xs.length}`; +} +function elementWrite(): string { + const xs = []; + xs.push(1); + const alias = xs; + xs[1] = "one"; + return `${alias.length} ${alias.join("+")}`; +} +function agreeing(): string { + const xs = []; + xs.push(1); + const alias = xs; + xs.push(2); + return `${alias.length} ${alias === xs} ${alias.reduce((sum, v) => sum + v, 0)}`; +} +function total(values: number[]): number { + return values.length; +} +function passed(): string { + const xs = []; + xs.push(1); + const alias = xs; + const n = total(alias); + xs.push("z"); + return `${n} ${alias.length}`; +} +console.log(chain()); +console.log(viaAlias(), "|", letAlias()); +console.log(nested(true), "|", nested(false), "|", elementWrite()); +console.log(agreeing(), "|", passed()); + +function spreadSame(): number { + const xs = []; + xs.push(1); + const more = [2, 3]; + xs.push(...more); + return xs.reduce((sum, v) => sum + v, 0); +} +console.log(spreadSame()); + +// An object or array literal that holds the array between the writes +// (`{ items: xs }`, `[xs]`, a shorthand property, a nested literal), a +// binding of such a literal, and a name destructured from one hold the +// same array, so they see the later writes. +function objectHolder(): string { + const xs = []; + xs.push(1); + const box = { items: xs, label: "box" }; + xs.push("two"); + box.items.push(3); + return `${box.items.length} ${box.items === xs} ${box.items[1]} ${box.label} ${xs.join(",")}`; +} +function arrayHolder(): string { + const xs = []; + xs.push(1); + const rows = [xs, [5]]; + xs.push("two"); + let total = 0; + for (const row of rows) total += row.length; + return `${rows[0].length} ${rows[0] === xs} ${rows[0][1]} ${total}`; +} +function shorthandHolder(): string { + const xs = []; + xs.push(1); + const box = { xs, count: 3 }; + xs.push("two"); + return `${box.xs.length} ${box.xs === xs} ${box.count}`; +} +function nestedHolder(): string { + const xs = []; + xs.push(1); + const box = { inner: [xs], pair: [{ items: xs }] }; + const outer = { box }; + xs.push("two"); + return `${box.inner[0].length} ${box.pair[0].items === xs} ${outer.box.inner[0][1]}`; +} +function holderAliases(): string { + const xs = []; + xs.push(1); + const box = { items: xs }; + const other = box; + const { items } = box; + xs.push("two"); + return `${other.items.length} ${items.length} ${items === xs} ${JSON.stringify(other)}`; +} +console.log(objectHolder(), "|", arrayHolder(), "|", shorthandHolder()); +console.log(nestedHolder(), "|", holderAliases()); diff --git a/tests/corpus/7007-filter-written-type-predicates.ts b/tests/corpus/7007-filter-written-type-predicates.ts new file mode 100644 index 000000000..b75b5727d --- /dev/null +++ b/tests/corpus/7007-filter-written-type-predicates.ts @@ -0,0 +1,108 @@ +// `.filter` with a written type predicate: an annotated callback +// (`(p): p is Cat => ...`) or a guard function passed as a value. The +// result holds the predicate's type: a union arm, a union of arms, a +// number out of `unknown`, a record or tuple with a narrower field, or a +// subclass. An inferred predicate that keeps several arms narrows too. +interface Cat { + kind: "cat"; + meow: string; +} +interface Dog { + kind: "dog"; + bark: number; +} +interface Fish { + kind: "fish"; + fins: number; +} +type Pet = Cat | Dog | Fish; +const pets: Pet[] = [ + { kind: "cat", meow: "m" }, + { kind: "dog", bark: 2 }, + { kind: "fish", fins: 3 }, + { kind: "dog", bark: 5 }, +]; + +const cats = pets.filter((p): p is Cat => p.kind === "cat"); +console.log(cats.length, cats[0]!.meow); +const furry = pets.filter((p): p is Cat | Dog => p.kind !== "fish"); +console.log(furry.map((p) => (p.kind === "cat" ? p.meow : p.bark)).join(",")); +const notFish = pets.filter((p) => p.kind !== "fish"); +console.log(notFish.length); + +function isDog(p: Pet): p is Dog { + return p.kind === "dog"; +} +console.log(pets.filter(isDog).map((d) => d.bark * 10).join(",")); + +const isNumber = (v: unknown): v is number => typeof v === "number"; +const values: unknown[] = JSON.parse('[1, "a", 2, null, 4]'); +console.log(values.filter(isNumber).reduce((sum, n) => sum + n, 0)); + +const rows: unknown[] = JSON.parse('[{"id":1},{"id":"x"},{"id":3}]'); +const withIds = rows.filter( + (r): r is { id: number } => typeof (r as { id: unknown }).id === "number", +); +console.log(withIds.map((r) => r.id + 1).join(",")); + +const entries: [string, unknown][] = [ + ["a", 1], + ["b", "x"], + ["c", 3], +]; +const numeric = entries.filter((e): e is [string, number] => typeof e[1] === "number"); +console.log(numeric.map(([key, value]) => `${key}${value * 2}`).join(" ")); + +class Shape { + name: string; + constructor(name: string) { + this.name = name; + } +} +class Circle extends Shape { + r: number; + constructor(r: number) { + super("circle"); + this.r = r; + } +} +const shapes: Shape[] = [new Shape("square"), new Circle(2), new Circle(3)]; +const circles = shapes.filter((s): s is Circle => s instanceof Circle); +console.log(circles.length, circles.map((c) => c.r).join("+")); + +const mixed: (string | number | boolean)[] = ["a", 1, true, "b"]; +console.log(mixed.filter((v): v is string => typeof v === "string").join("")); +console.log(mixed.filter((v): v is string | number => typeof v !== "boolean").length); + +const pair: [string | number, string | number] = ["x", 4]; +console.log(pair.filter((v): v is number => typeof v === "number").map((n) => n + 1).join()); + +const optional: (string | number | undefined)[] = ["a", 1, undefined, 2]; +const defined = optional.filter((v) => v !== undefined); +console.log(defined.length, defined.map((v) => typeof v).join(",")); +function isText(v: string | undefined): v is string { + return typeof v === "string"; +} +const maybe: (string | undefined)[] = ["a", undefined, "b"]; +console.log(maybe.filter(isText).join("-")); + +// Narrowing a record field keeps each object's own keys, in its own order, +// and an optional field the object lacks stays absent. +interface Row { + id: unknown; + note?: string; + label: string; +} +interface NumericRow { + id: number; + note?: string; + label: string; +} +const tableRows: Row[] = [ + { label: "a", id: 1 }, + { label: "b", id: "s" }, + { label: "c", id: 3, note: "n" }, +]; +for (const row of tableRows.filter((r): r is NumericRow => typeof r.id === "number")) { + console.log(Object.keys(row).join(","), JSON.stringify(row), "note" in row, row.id + 1); +} diff --git a/tests/corpus/7008-js-unit-initialized-properties/main.ts b/tests/corpus/7008-js-unit-initialized-properties/main.ts new file mode 100644 index 000000000..3ac113d6a --- /dev/null +++ b/tests/corpus/7008-js-unit-initialized-properties/main.ts @@ -0,0 +1,8 @@ +// A JavaScript package whose object literals start a property as +// `undefined` or `null` and later store records, numbers and arrays in it, +// as the diff package's path list does (`{ oldPos: -1, lastComponent: +// undefined }`). Node runs the later writes; each property holds them. +import { describe, session } from "edit-paths"; + +console.log(describe()); +console.log(session()); diff --git a/tests/corpus/7008-js-unit-initialized-properties/node_modules/edit-paths/index.d.ts b/tests/corpus/7008-js-unit-initialized-properties/node_modules/edit-paths/index.d.ts new file mode 100644 index 000000000..68e2b409b --- /dev/null +++ b/tests/corpus/7008-js-unit-initialized-properties/node_modules/edit-paths/index.d.ts @@ -0,0 +1,2 @@ +export declare function describe(): string; +export declare function session(): string; diff --git a/tests/corpus/7008-js-unit-initialized-properties/node_modules/edit-paths/index.js b/tests/corpus/7008-js-unit-initialized-properties/node_modules/edit-paths/index.js new file mode 100644 index 000000000..ec3e7c4d0 --- /dev/null +++ b/tests/corpus/7008-js-unit-initialized-properties/node_modules/edit-paths/index.js @@ -0,0 +1,43 @@ +"use strict"; +// A Myers-style path list, as the diff package keeps it: each path starts +// with `lastComponent: undefined` and later holds a linked list of records, +// and a state object starts with `null` fields that later hold other values. +function addToPath(path, added, removed, oldPosInc) { + var last = path.lastComponent; + if (last && last.added === added && last.removed === removed) { + return { oldPos: path.oldPos + oldPosInc, lastComponent: { count: last.count + 1, added: added, removed: removed, previousComponent: last.previousComponent } }; + } + return { oldPos: path.oldPos + oldPosInc, lastComponent: { count: 1, added: added, removed: removed, previousComponent: last } }; +} +function extractCommon(basePath, common) { + if (common) basePath.lastComponent = { count: common, previousComponent: basePath.lastComponent, added: false, removed: false }; + basePath.oldPos += common; +} +function components(lastComponent) { + var list = []; + while (lastComponent) { + list.push(lastComponent); + var next = lastComponent.previousComponent; + delete lastComponent.previousComponent; + lastComponent = next; + } + return list.reverse(); +} +function describe() { + var bestPath = [{ oldPos: -1, lastComponent: undefined }]; + extractCommon(bestPath[0], 1); + var path = addToPath(bestPath[0], false, true, 1); + path = addToPath(path, true, false, 0); + path = addToPath(path, true, false, 0); + return components(path.lastComponent).map(function (c) { + return (c.added ? "+" : c.removed ? "-" : "=") + c.count + ("previousComponent" in c ? "!" : ""); + }).join(" "); +} +function session() { + var state = { user: null, retries: null, tags: undefined }; + state.user = { name: "ada" }; + state.retries = 3; + state.tags = ["a", "b"]; + return state.user.name + " " + state.retries + " " + state.tags.join("+") + " " + JSON.stringify(state); +} +module.exports = { describe: describe, session: session }; diff --git a/tests/corpus/7008-js-unit-initialized-properties/node_modules/edit-paths/package.json b/tests/corpus/7008-js-unit-initialized-properties/node_modules/edit-paths/package.json new file mode 100644 index 000000000..98648c289 --- /dev/null +++ b/tests/corpus/7008-js-unit-initialized-properties/node_modules/edit-paths/package.json @@ -0,0 +1 @@ +{ "name": "edit-paths", "version": "1.0.0", "main": "./index.js", "types": "./index.d.ts" } diff --git a/tests/corpus/7009-finalization-registry-wasm-loader/main.ts b/tests/corpus/7009-finalization-registry-wasm-loader/main.ts new file mode 100644 index 000000000..f844b0183 --- /dev/null +++ b/tests/corpus/7009-finalization-registry-wasm-loader/main.ts @@ -0,0 +1,39 @@ +// A package loaded the way wasm-bindgen's web target loads: FinalizationRegistry +// guards and registrations run at load and in constructors, and free() +// unregisters. Registrations keep their bookkeeping; cleanup +// callbacks run at times only the garbage collector decides, so this program +// prints nothing from them, as under Node. +import { Counter, initSync, released_total } from "counter-wasm"; + +// (module (func (export "add") (param i32 i32) (result i32) local.get 0 local.get 1 i32.add)) +const bytes = new Uint8Array([ + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x07, 0x01, 0x60, 0x02, 0x7f, 0x7f, 0x01, + 0x7f, 0x03, 0x02, 0x01, 0x00, 0x07, 0x07, 0x01, 0x03, 0x61, 0x64, 0x64, 0x00, 0x00, 0x0a, 0x09, + 0x01, 0x07, 0x00, 0x20, 0x00, 0x20, 0x01, 0x6a, 0x0b, +]); +initSync(bytes); + +const a = new Counter(10); +a.increment(5); +const b = new Counter(2); +b.increment(1); +console.log(a.value, b.value); +b.free(); +a.free(); +console.log(a.value, released_total()); + +const registry = new FinalizationRegistry((held) => console.log("cleanup", held)); +const token = {}; +registry.register({}, "first", token); +registry.register({ id: 2 }, "second", token); +registry.register({ id: 3 }, "third"); +console.log(registry.unregister(token), registry.unregister(token), registry.unregister({})); +try { + new FinalizationRegistry(() => {}).register(token, token); +} catch (error) { + console.log((error as Error).name, (error as Error).message); +} +console.log(typeof FinalizationRegistry, registry instanceof FinalizationRegistry); +// A name the registry does not have reads as undefined. +console.log((registry as any).label); +console.log(Object.prototype.toString.call(registry), registry); diff --git a/tests/corpus/7009-finalization-registry-wasm-loader/node_modules/counter-wasm/counter_wasm.d.ts b/tests/corpus/7009-finalization-registry-wasm-loader/node_modules/counter-wasm/counter_wasm.d.ts new file mode 100644 index 000000000..525dec86f --- /dev/null +++ b/tests/corpus/7009-finalization-registry-wasm-loader/node_modules/counter-wasm/counter_wasm.d.ts @@ -0,0 +1,9 @@ +export declare class Counter { + constructor(start: number); + free(): void; + increment(by: number): number; + readonly value: number; + [Symbol.dispose](): void; +} +export declare function released_total(): number; +export declare function initSync(bytes: Uint8Array): unknown; diff --git a/tests/corpus/7009-finalization-registry-wasm-loader/node_modules/counter-wasm/counter_wasm.js b/tests/corpus/7009-finalization-registry-wasm-loader/node_modules/counter-wasm/counter_wasm.js new file mode 100644 index 000000000..92715f743 --- /dev/null +++ b/tests/corpus/7009-finalization-registry-wasm-loader/node_modules/counter-wasm/counter_wasm.js @@ -0,0 +1,61 @@ +// A loader in the shape wasm-bindgen's web target generates: the module's +// wrapper classes register with a FinalizationRegistry when they are +// created and unregister when free() releases them, and a TextEncoder +// polyfill guard runs at load. +let wasm; +const cachedTextEncoder = new TextEncoder(); +if (!("encodeInto" in cachedTextEncoder)) { + cachedTextEncoder.encodeInto = function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { read: arg.length, written: buf.length }; + }; +} +let released = 0; +const CounterFinalization = + typeof FinalizationRegistry === "undefined" + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry((ptr) => wasm.add(ptr >>> 0, 1)); + +export class Counter { + static __wrap(ptr) { + const obj = Object.create(Counter.prototype); + obj.__wbg_ptr = ptr; + CounterFinalization.register(obj, obj.__wbg_ptr, obj); + return obj; + } + constructor(start) { + this.__wbg_ptr = wasm.add(start, 0); + CounterFinalization.register(this, this.__wbg_ptr, this); + } + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + CounterFinalization.unregister(this); + return ptr; + } + free() { + const ptr = this.__destroy_into_raw(); + released += wasm.add(ptr, 0); + } + increment(by) { + this.__wbg_ptr = wasm.add(this.__wbg_ptr, by); + return this.__wbg_ptr; + } + get value() { + return this.__wbg_ptr; + } +} +if (Symbol.dispose) Counter.prototype[Symbol.dispose] = Counter.prototype.free; + +export function released_total() { + return released; +} + +export function initSync(bytes) { + if (wasm !== undefined) return wasm; + const module = new WebAssembly.Module(bytes); + const instance = new WebAssembly.Instance(module, {}); + wasm = instance.exports; + return wasm; +} diff --git a/tests/corpus/7009-finalization-registry-wasm-loader/node_modules/counter-wasm/package.json b/tests/corpus/7009-finalization-registry-wasm-loader/node_modules/counter-wasm/package.json new file mode 100644 index 000000000..d86e34137 --- /dev/null +++ b/tests/corpus/7009-finalization-registry-wasm-loader/node_modules/counter-wasm/package.json @@ -0,0 +1 @@ +{ "name": "counter-wasm", "version": "1.0.0", "type": "module", "main": "./counter_wasm.js", "types": "./counter_wasm.d.ts" } diff --git a/tests/corpus/7011-for-in-dictionary-deletes.ts b/tests/corpus/7011-for-in-dictionary-deletes.ts new file mode 100644 index 000000000..91dac573b --- /dev/null +++ b/tests/corpus/7011-for-in-dictionary-deletes.ts @@ -0,0 +1,42 @@ +// for-in over a string-keyed dictionary visits the keys present when the +// walk starts, in insertion order, and skips a key deleted before its +// turn; keys added during the walk are not visited. A key deleted and +// added again before its turn is visited. +const d: Record = {}; +for (let i = 0; i < 8; i++) d["k" + i] = i; +const seen: string[] = []; +for (const key in d) { + seen.push(key); + if (key === "k1") { + delete d["k3"]; + delete d["k0"]; + d["k8"] = 8; + } + if (key === "k2") { + delete d["k5"]; + d["k5"] = 50; + } + if (key === "k4") delete d["k4"]; +} +console.log(seen.join(",")); +console.log(Object.keys(d).join(","), JSON.stringify(d)); + +// A numeric-looking key is walked in integer order before string keys. +const n: Record = {}; +n["b"] = "x"; +n["2"] = "y"; +n["10"] = "z"; +n["a"] = "w"; +const order: string[] = []; +for (const key in n) { + order.push(key); + if (key === "2") delete n["a"]; +} +console.log(order.join(",")); + +// A larger dictionary walked many times. +const big: Record = {}; +for (let i = 0; i < 3000; i++) big["e" + i] = i; +let total = 0; +for (let pass = 0; pass < 20; pass++) for (const key in big) total += big[key]!; +console.log(total); diff --git a/tests/corpus/7014-js-never-array-literal-fields/main.ts b/tests/corpus/7014-js-never-array-literal-fields/main.ts new file mode 100644 index 000000000..cf6d235c0 --- /dev/null +++ b/tests/corpus/7014-js-never-array-literal-fields/main.ts @@ -0,0 +1,10 @@ +// A JavaScript package whose object literals start an array property as +// `[]` inside a literal that maps to a record, as zod 3's parse context does +// (`{ common: { issues: [] }, path: params?.path || [] }`), and later push +// records onto it. Node runs the pushes; each array holds them. +import { Schema, rows } from "mini"; + +const s = new Schema(); +console.log(JSON.stringify(s.safeParse(1)), JSON.stringify(s.safeParse("a"))); +console.log(JSON.stringify(s.safeParse(2, { path: ["root"] }))); +console.log(rows()); diff --git a/tests/corpus/7014-js-never-array-literal-fields/node_modules/mini/index.d.ts b/tests/corpus/7014-js-never-array-literal-fields/node_modules/mini/index.d.ts new file mode 100644 index 000000000..5925684c0 --- /dev/null +++ b/tests/corpus/7014-js-never-array-literal-fields/node_modules/mini/index.d.ts @@ -0,0 +1,5 @@ +export interface Issue { code: string; path: (string | number)[]; message: string } +export declare class Schema { + safeParse(data: unknown, params?: { async?: boolean; path?: (string | number)[] }): { success: true; data: string } | { success: false; issues: Issue[] }; +} +export declare function rows(): string; diff --git a/tests/corpus/7014-js-never-array-literal-fields/node_modules/mini/index.js b/tests/corpus/7014-js-never-array-literal-fields/node_modules/mini/index.js new file mode 100644 index 000000000..47d24c8ad --- /dev/null +++ b/tests/corpus/7014-js-never-array-literal-fields/node_modules/mini/index.js @@ -0,0 +1,31 @@ +// A parse context as zod 3 keeps it: the issue list starts as `[]` in a +// nested literal, and the path is the caller's array or `[]`. +export function makeIssue(params) { + const { path, issueData } = params; + const fullPath = [...path, ...(issueData.path || [])]; + return { ...issueData, path: fullPath, message: "m" }; +} +export function addIssue(ctx, issueData) { + const issue = makeIssue({ issueData, path: ctx.path }); + ctx.common.issues.push(issue); +} +export class Schema { + safeParse(data, params) { + const ctx = { + common: { issues: [], async: params?.async ?? false }, + path: params?.path || [], + data, + }; + if (typeof data !== "string") addIssue(ctx, { code: "invalid_type" }); + return ctx.common.issues.length === 0 ? { success: true, data } : { success: false, issues: ctx.common.issues }; + } +} +// Array properties of records in an array literal and in a deeper literal, +// filled after construction. +export function rows() { + const table = { rows: [{ cells: [] }, { cells: [] }], meta: { tags: { list: [] } }, size: 2 }; + table.rows[0].cells.push({ v: 1 }); + table.rows[1].cells.push("text", 3); + table.meta.tags.list.push({ name: "t" }, null); + return JSON.stringify(table); +} diff --git a/tests/corpus/7014-js-never-array-literal-fields/node_modules/mini/package.json b/tests/corpus/7014-js-never-array-literal-fields/node_modules/mini/package.json new file mode 100644 index 000000000..be8bc49a2 --- /dev/null +++ b/tests/corpus/7014-js-never-array-literal-fields/node_modules/mini/package.json @@ -0,0 +1 @@ +{ "name": "mini", "version": "1.0.0", "type": "module", "main": "./index.js", "types": "./index.d.ts" } diff --git a/tests/corpus/7020-conditional-spread-fields.ts b/tests/corpus/7020-conditional-spread-fields.ts new file mode 100644 index 000000000..61c250174 --- /dev/null +++ b/tests/corpus/7020-conditional-spread-fields.ts @@ -0,0 +1,137 @@ +// Conditional spreads that carry several properties, and conditional +// spreads of a record value against an empty arm. The condition runs +// once, the carried values run only when their arm is taken and in +// property order, and the keys appear at the spread's position. +const log: string[] = []; +function note(label: string, value: T): T { + log.push(label); + return value; +} + +interface Extra { + region: string; + zone: number; +} + +function build(flag: boolean, model: string | undefined, extra: Extra | undefined) { + return { + name: note("name", "tool"), + ...(note("cond", flag) ? { verbose: note("verbose", true), depth: note("depth", 2) } : {}), + ...(model ? { model } : {}), + ...(extra ? extra : {}), + last: note("last", 0), + }; +} + +for (const [flag, model, extra] of [ + [true, "m1", { region: "eu", zone: 3 }], + [false, undefined, undefined], + [true, undefined, { region: "us", zone: 1 }], +] as const) { + log.length = 0; + const out = build(flag, model, extra); + console.log(JSON.stringify(out), Object.keys(out).join(","), log.join(" ")); + console.log("verbose" in out, out.depth ?? "no depth", out.region ?? "no region"); +} + +// The carrying arm can be the false arm, and `c && { ... }` carries +// several properties too. +function flags(quiet: boolean, color: boolean) { + return { + ...(quiet ? {} : { level: "info", prefix: "[app]" }), + ...(color && { fg: "green", bg: "black", bold: true }), + }; +} +console.log(JSON.stringify(flags(false, true)), JSON.stringify(flags(true, false))); + +// A record arm in either position, against `{}`, `undefined` or `null`. +interface Auth { + user: string; + token?: string; +} +const auth: Auth = { user: "u", token: "t" }; +const bare: Auth = { user: "v" }; +function request(useAuth: boolean, source: Auth) { + return { + url: "/x", + ...(useAuth ? source : {}), + ...(useAuth ? undefined : { anonymous: true }), + }; +} +console.log(JSON.stringify(request(true, auth)), JSON.stringify(request(false, auth))); +console.log(JSON.stringify(request(true, bare)), Object.keys(request(true, bare)).join(",")); +function fallback(primary: Auth | null, secondary: Auth) { + return { ...(primary === null ? secondary : null), id: 1 }; +} +console.log(JSON.stringify(fallback(null, auth)), JSON.stringify(fallback(bare, auth))); + +// A record arm read through a call runs only when its arm is taken. +let reads = 0; +function current(): Extra { + reads++; + return { region: "ap", zone: reads }; +} +const a = { ...(reads > 0 ? current() : {}), tag: "a" }; +const b = { ...(reads === 0 ? current() : {}), tag: "b" }; +console.log(JSON.stringify(a), JSON.stringify(b), reads); + +// Into a declared shape with optional fields. +interface Options { + cwd: string; + shell?: boolean; + timeout?: number; + env?: string; +} +function options(detached: boolean, env: string | undefined): Options { + return { cwd: "/", ...(detached ? { shell: false, timeout: 5 } : {}), ...(env ? { env } : {}) }; +} +console.log(JSON.stringify(options(true, "prod")), JSON.stringify(options(false, undefined))); + +// Inside 'any'-typed object literals, where the key is absent when the +// empty arm is taken. +function anyLiterals(): void { + function opts(aspect: string | undefined): any { + const o: any = { + mode: "M", + ...(aspect ? { cfg: { aspect } } : {}), + }; + return o; + } + const a = opts("1:1"); + console.log(`${a.mode} ${a.cfg.aspect} ${typeof a.cfg}`); + const b = opts(undefined); + console.log(`${b.mode} ${typeof b.cfg}`); + + // Several carried properties. + function multi(on: boolean): any { + return { base: 1, ...(on ? { x: "X", y: "Y" } : {}) }; + } + const m1 = multi(true); + const m2 = multi(false); + console.log(`${m1.base} ${m1.x} ${m1.y}`); + console.log(`${m2.base} ${typeof m2.x} ${typeof m2.y}`); + + // Reversed orientation and string-literal keys. + function rev(quiet: boolean): any { + return { kind: "r", ...(quiet ? {} : { "log-level": "loud" }) }; + } + console.log(`${rev(false)["log-level"]}`); + console.log(`${typeof rev(true)["log-level"]}`); + + // The spread mid-literal: properties around it evaluate exactly once and + // land in engine key order. + let sides = 0; + function side(v: number): number { + sides++; + return v; + } + function ordered(flag: boolean): any { + const o: any = { first: side(1), ...(flag ? { mid: 2 } : {}), last: side(3) }; + return o; + } + const o1 = ordered(true); + const o2 = ordered(false); + console.log(`${o1.first} ${o1.mid} ${o1.last}`); + console.log(`${o2.first} ${typeof o2.mid} ${o2.last} ${sides}`); +} +anyLiterals(); diff --git a/tests/corpus/7021-promise-all-uniform-tuple.ts b/tests/corpus/7021-promise-all-uniform-tuple.ts new file mode 100644 index 000000000..461351992 --- /dev/null +++ b/tests/corpus/7021-promise-all-uniform-tuple.ts @@ -0,0 +1,46 @@ +// `const pair = await Promise.all([p, q])` over entries of one promise +// type: the checker types the binding as a tuple, and the values arrive +// on the same microtask turn as Node's, in entry order whatever the +// settlement order. +const order: string[] = []; +function later(label: string, turns: number): Promise { + return new Promise((resolve) => { + let left = turns; + const step = (): void => { + if (left-- === 0) { + order.push(`settle ${label}`); + resolve(`${label}!`); + } else queueMicrotask(step); + }; + step(); + }); +} +async function count(n: number): Promise { + return n * 10; +} + +async function main(): Promise { + let ticking = true; + void (async () => { + for (let i = 0; i < 20 && ticking; i++) { + order.push(`t${i}`); + await null; + } + })(); + const bodies = await Promise.all([later("a", 3), later("b", 1)]); + order.push(`bodies ${bodies[0]} ${bodies[1]} ${bodies.length}`); + const numbers = await Promise.all([count(1), count(2), count(3)]); + const [x, y, z] = numbers; + order.push(`numbers ${x + y + z} ${numbers.length}`); + const annotated: [string, string] = await Promise.all([later("c", 0), later("d", 0)]); + order.push(`annotated ${annotated.join("+")}`); + ticking = false; + console.log(order.join("\n")); + try { + const failed = await Promise.all([later("e", 2), Promise.reject(new Error("no"))]); + console.log("unreachable", failed[0]); + } catch (e) { + console.log("caught", (e as Error).message); + } +} +void main(); diff --git a/tests/corpus/7022-promise-all-dynamic-arrays.ts b/tests/corpus/7022-promise-all-dynamic-arrays.ts new file mode 100644 index 000000000..dca5cdcde --- /dev/null +++ b/tests/corpus/7022-promise-all-dynamic-arrays.ts @@ -0,0 +1,41 @@ +// Promise.all over arrays built from 'any' values: the argument is a +// runtime value, so the combinator reads it with Node's iteration and +// resolves each entry (promises, thenables and plain values). +interface Check { + name: string; + ok: boolean; +} +async function check(name: string): Promise { + await null; + return { name, ok: name.length > 2 }; +} + +async function main(): Promise { + const raw: any = JSON.parse('{"items":[{"name":"abc"},{"name":"x"}],"ids":[1,2,3]}'); + const results = await Promise.all(raw.items.map((item: any) => check(item.name))); + for (const r of results) console.log(r.name, r.ok); + const doubled = await Promise.all(raw.ids.map(async (id: number) => id * 2)); + console.log(doubled.join(","), doubled.length); + const mixed = await Promise.all(raw.ids.map((id: number) => (id % 2 ? id : Promise.resolve(-id)))); + console.log(JSON.stringify(mixed)); + const parsed: unknown = JSON.parse('["a","bb","ccc"]'); + const list = Array.isArray(parsed) ? parsed : []; + const lengths = await Promise.all(list.map(async (s) => String(s).length)); + console.log(lengths, lengths.reduce((a, b) => a + b, 0)); + const empty: any = []; + console.log(JSON.stringify(await Promise.all(empty))); + try { + await Promise.all(raw.items.map(async (item: any) => { + if (item.name === "x") throw new Error(`bad ${item.name}`); + return item.name; + })); + } catch (e) { + console.log("rejected", (e as Error).message); + } + try { + await Promise.all(raw.missing); + } catch (e) { + console.log("not iterable", e instanceof TypeError); + } +} +void main(); diff --git a/tests/corpus/7023-union-object-patterns.ts b/tests/corpus/7023-union-object-patterns.ts new file mode 100644 index 000000000..8717e892a --- /dev/null +++ b/tests/corpus/7023-union-object-patterns.ts @@ -0,0 +1,63 @@ +// Object patterns over unions of records and classes read the fields +// every arm declares, in parameters, declarations and loops. +interface Add { + kind: "add"; + path: string; + content: string; + mode?: number; +} +interface Remove { + kind: "remove"; + path: string; + mode?: number; +} +type Change = Add | Remove; + +function describe({ kind, path: file, mode = 0o644 }: Change): string { + return `${kind}:${file}:${mode.toString(8)}`; +} +const changes: Change[] = [ + { kind: "add", path: "a.txt", content: "x" }, + { kind: "remove", path: "b.txt", mode: 0o600 }, +]; +for (const change of changes) console.log(describe(change)); +for (const { kind, path } of changes) console.log(kind, path.length); +function applyChange(change: Change): number { + const { path } = change; + let { kind } = change; + if (kind === "add") kind = "remove"; + return path.length + kind.length; +} +console.log(applyChange(changes[0]!), applyChange(changes[1]!)); + +// The source runs once, and the fields read in pattern order. +let reads = 0; +function next(): Change { + reads++; + return reads % 2 ? changes[0]! : changes[1]!; +} +const { path: p1, kind: k1 } = next(); +const { kind: k2 } = next(); +console.log(p1, k1, k2, reads); + +class Circle { + name: string; + size: number; + constructor(name: string, size: number) { + this.name = name; + this.size = size; + } +} +class Square { + name: string; + size: number; + corners = 4; + constructor(name: string, size: number) { + this.name = name; + this.size = size; + } +} +function label({ name, size }: Circle | Square): string { + return `${name}/${size}`; +} +console.log(label(new Circle("c", 2)), label(new Square("s", 3))); diff --git a/tests/corpus/7024-async-return-fresh-collections.ts b/tests/corpus/7024-async-return-fresh-collections.ts new file mode 100644 index 000000000..8343e028b --- /dev/null +++ b/tests/corpus/7024-async-return-fresh-collections.ts @@ -0,0 +1,48 @@ +// Async functions can return an empty array literal (where the result is +// inferred) or a new Map or Set: the value takes the result's element +// types, as a synchronous return does. +import { existsSync, readdirSync } from "node:fs"; + +async function listJson(dir: string) { + if (!existsSync(dir)) return []; + return readdirSync(dir).filter((name) => name.endsWith(".json")); +} + +async function childPaths(root: string) { + if (root === "") { + return []; + } + const found = [root, `${root}/b`]; + return found; +} + +async function index(names: string[]): Promise> { + if (names.length === 0) return new Map(); + const byName = new Map(); + for (const name of names) byName.set(name, name.length); + return byName; +} + +async function unique(words: string[]): Promise> { + if (words.length === 0) return new Set(); + return new Set(words); +} + +const pick = async (flag: boolean) => (flag ? [] : ["a", "b"]); + +async function main(): Promise { + const missing = await listJson("/nonexistent-dir-for-test"); + missing.push("pushed"); + console.log(missing, missing.length); + console.log(await childPaths(""), await childPaths("/root")); + const empty = await index([]); + empty.set("late", 4); + console.log(empty.size, empty.get("late"), (await index(["ab", "c"])).get("ab")); + const none = await unique([]); + none.add("z"); + console.log([...none], [...(await unique(["x", "x", "y"]))]); + const first = await pick(true); + first.push("c"); + console.log(first, await pick(false)); +} +void main(); diff --git a/tests/corpus/7025-child-listener-chaining.ts b/tests/corpus/7025-child-listener-chaining.ts new file mode 100644 index 000000000..317c87db1 --- /dev/null +++ b/tests/corpus/7025-child-listener-chaining.ts @@ -0,0 +1,42 @@ +// child.on() and child.once() return the child, so registrations chain +// and the result can be stored or passed on. The receiver evaluates once, +// before the listener. Each child starts after the previous one closed. +import { spawn } from "node:child_process"; + +const events: string[] = []; +let lookups = 0; +const children = [spawn("sh", ["-c", "exit 3"], { stdio: "ignore" })]; +function first() { + lookups++; + return children[0]!; +} + +const returned = first() + .on("exit", (code) => events.push(`exit ${code}`)) + .once("close", (code, signal) => { + events.push(`close ${code} ${signal}`); + console.log(events.join(", "), lookups); + second(); + }) + .on("error", (err) => events.push(`error ${err.message}`)); +console.log(returned === children[0], lookups); + +function second(): void { + spawn("sh", ["-c", "exit 0"], { stdio: "ignore" }) + .on("spawn", () => console.log("spawned")) + .on("exit", (code) => console.log("second exit", code)) + .on("close", () => third()); +} + +function third(): void { + const missing = spawn("/nonexistent/command-for-test", [], { stdio: "ignore" }).on( + "error", + (e) => console.log("missing", (e as NodeJS.ErrnoException).code), + ); + console.log(typeof missing.pid); + const detached = spawn("sh", ["-c", "exit 0"], { detached: true, stdio: "ignore" }).once( + "error", + (e) => console.log("spawn failed", e.message), + ); + detached.unref(); +} diff --git a/tests/corpus/7026-url-object-patterns.ts b/tests/corpus/7026-url-object-patterns.ts new file mode 100644 index 000000000..c2bc59958 --- /dev/null +++ b/tests/corpus/7026-url-object-patterns.ts @@ -0,0 +1,27 @@ +// Object patterns over URLs read the URL's getters in pattern order, +// with renames and defaults, in declarations and parameters. +let built = 0; +function parse(text: string): URL { + built++; + return new URL(text); +} +const { protocol, hostname, port } = parse("http://localhost:8080/path?q=1#top"); +console.log(protocol, hostname, port, built); +const { host: where, pathname: path = "/", search, hash, href } = parse("https://example.com/a/b?x=1&y=2#h"); +console.log(where, path, search, hash, href, built); + +function origin({ protocol: scheme, host, username, password }: URL): string { + return `${scheme}//${username}:${password}@${host}`; +} +console.log(origin(new URL("ftp://user:secret@files.example.org:2121/dir"))); + +const { searchParams } = new URL("http://h/?a=1&b=2"); +searchParams.append("c", "3"); +console.log(searchParams.toString(), [...searchParams.keys()].join(",")); + +const { origin: o, port: p } = new URL("https://[::1]:8443/"); +console.log(o, p); +for (const text of ["http://a.test:1", "http://b.test"]) { + const { hostname: name, port: listen } = new URL(text); + console.log(name, listen === "" ? "(default)" : listen); +} diff --git a/tests/corpus/7027-search-params-conditional-init.ts b/tests/corpus/7027-search-params-conditional-init.ts new file mode 100644 index 000000000..580d6c039 --- /dev/null +++ b/tests/corpus/7027-search-params-conditional-init.ts @@ -0,0 +1,21 @@ +// new URLSearchParams(c ? a : b) builds from the arm the condition +// selects; each arm may be a record literal, a string, pairs or another +// URLSearchParams. +let checks = 0; +function flag(value: boolean): boolean { + checks++; + return value; +} +for (const on of [true, false]) { + const params = new URLSearchParams(flag(on) ? { code: "x y", state: "s&t" } : {}); + console.log(params.toString(), params.size); + const query = new URLSearchParams(on ? "a=1&b=2" : [["c", "3"]]); + console.log(query.toString()); + const base = new URLSearchParams("keep=1"); + const copy = new URLSearchParams(on ? base : undefined); + copy.append("more", "2"); + console.log(copy.toString(), base.toString()); + const nested = new URLSearchParams(on ? (checks > 1 ? { n: "2" } : { n: "1" }) : "?empty="); + console.log(nested.toString()); +} +console.log(checks); diff --git a/tests/corpus/7028-non-empty-rest-tuples.ts b/tests/corpus/7028-non-empty-rest-tuples.ts new file mode 100644 index 000000000..5d00952ab --- /dev/null +++ b/tests/corpus/7028-non-empty-rest-tuples.ts @@ -0,0 +1,38 @@ +// Tuples of required positions followed by a rest element of the same +// type (`[string, ...string[]]`, the non-empty list) behave as arrays of +// that type. +type Hosts = [string, ...string[]]; +const LOCAL_HOSTS: Hosts = ["127.0.0.1", "::1", "localhost"]; +const single: [string, ...string[]] = ["only"]; +const pair: readonly [number, number, ...number[]] = [1, 2]; + +function first(hosts: Hosts): string { + return hosts[0]; +} +function isLocalHost(host: string): boolean { + return LOCAL_HOSTS.includes(host); +} +console.log(first(LOCAL_HOSTS), first(single), LOCAL_HOSTS.length, single.length); +console.log(isLocalHost("::1"), isLocalHost("10.0.0.1"), LOCAL_HOSTS.indexOf("localhost")); +console.log(LOCAL_HOSTS.slice(1).join(","), LOCAL_HOSTS.map((h) => h.length)); +for (const host of LOCAL_HOSTS) console.log(host); +const [head, ...rest] = LOCAL_HOSTS; +console.log(head, rest, JSON.stringify(LOCAL_HOSTS)); +single.push("more"); +console.log(single, single[1], pair[0] + pair[1], pair.length); + +function hostsFrom(text: string): Hosts { + const parts = text.split(","); + const out: Hosts = [parts[0]!]; + for (const part of parts.slice(1)) out.push(part); + return out; +} +const parsed = hostsFrom("a,b,c"); +console.log(parsed, parsed.length, `${parsed}`); + +function join(...names: [string, ...string[]]): string { + return names.join("+"); +} +console.log(join("x"), join("x", "y", "z"), join(...LOCAL_HOSTS)); +const lists: Hosts[] = [LOCAL_HOSTS, single]; +console.log(lists.map((l) => l[0]).join(" ")); diff --git a/tests/corpus/7029-flatmap-union-results.ts b/tests/corpus/7029-flatmap-union-results.ts new file mode 100644 index 000000000..efeae4d8f --- /dev/null +++ b/tests/corpus/7029-flatmap-union-results.ts @@ -0,0 +1,44 @@ +// flatMap callbacks that return a value or an array: an array result +// appends its elements (holes skipped), any other result appends itself, +// and an empty `[]` arm appends nothing. +const words = ["a", "", "c", "", "e"]; +console.log(words.flatMap((s) => (s ? s : []))); +console.log(words.flatMap((s, i) => (s ? [s, String(i)] : []))); + +function expand(line: string): string | string[] { + return line.includes(",") ? line.split(",") : line; +} +console.log(["x", "y,z", "w"].flatMap(expand), ["x", "y,z"].flatMap((l) => expand(l)).length); + +const numbers = [1, 2, 3, 4]; +console.log(numbers.flatMap((n) => (n % 2 ? n : [n, n * 10]))); +const sparse: number[] = []; +sparse[0] = 1; +sparse[2] = 3; +console.log(numbers.flatMap((n) => (n === 2 ? sparse : n))); + +interface Entry { + name: string; + tags?: string[]; +} +const entries: Entry[] = [{ name: "a", tags: ["t1", "t2"] }, { name: "b" }]; +console.log(entries.flatMap((e) => e.tags ?? []), entries.flatMap((e) => e.tags ?? e.name)); +let calls = 0; +const out = words.flatMap((s) => { + calls++; + return s.length > 0 ? s.toUpperCase() : []; +}); +console.log(out, calls); + +// One union result type with an empty arm in one callback and a populated +// array arm in another. +const counts = [1, 2, 3]; +console.log(counts.flatMap((n) => (n % 2 ? n : [])), counts.flatMap((n) => (n % 2 ? n : [n, n]))); + +// Array results keep present undefined elements, and an undefined result +// appends itself. +const withGaps: (number | undefined)[] = [4, undefined, 6]; +const maybe: (number | undefined)[] = [1, 2, undefined]; +console.log(maybe.flatMap((n) => (n === 2 ? withGaps : n))); +const gaps: (string | undefined)[] = ["x", undefined]; +console.log(["a", "b"].flatMap((s) => (s === "b" ? gaps : s))); diff --git a/tests/corpus/7030-spawn-sync-env.ts b/tests/corpus/7030-spawn-sync-env.ts new file mode 100644 index 000000000..0ab001b8f --- /dev/null +++ b/tests/corpus/7030-spawn-sync-env.ts @@ -0,0 +1,47 @@ +// spawnSync's env option replaces the child's environment, alone or with +// the other options. +import { spawnSync } from "node:child_process"; +import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const merged = spawnSync("sh", ["-c", 'echo "$GREETING $HOME_SET"'], { + env: { ...process.env, GREETING: "hello", HOME_SET: process.env.HOME ? "yes" : "no" }, + encoding: "utf8", +}); +console.log(merged.stdout.trim(), merged.status); + +const only = spawnSync("/bin/sh", ["-c", 'echo "${GREETING:-unset} ${HOME:-nohome}"'], { + env: { GREETING: "only" }, + encoding: "utf8", +}); +console.log(only.stdout.trim()); + +const empty = spawnSync("/bin/sh", ["-c", "env | grep -c ."], { env: {}, encoding: "utf8" }); +console.log(Number(empty.stdout.trim()) <= 3, empty.status); + +const quiet = spawnSync("sh", ["-c", 'echo "$MODE" >&2; exit 4'], { + env: { PATH: process.env.PATH ?? "/usr/bin:/bin", MODE: "loud" }, + stdio: ["ignore", "pipe", "pipe"], + timeout: 5000, +}); +console.log(quiet.status, quiet.stderr.toString().trim(), quiet.stdout.toString() === ""); + +const missing = spawnSync("/nonexistent/tool", [], { env: { A: "1" } }); +console.log(missing.status, missing.error?.message); + +// The command is found on the replacement environment's PATH, or on the +// default path when the replacement has none, not on the parent's PATH. +const dir = mkdtempSync(join(tmpdir(), "scr-spawn-path-")); +writeFileSync(join(dir, "child-only-tool"), "#!/bin/sh\necho child-only\n"); +chmodSync(join(dir, "child-only-tool"), 0o755); +writeFileSync(join(dir, "not-executable"), "#!/bin/sh\necho never\n"); +const viaPath = spawnSync("child-only-tool", [], { env: { PATH: `/nonexistent:${dir}` }, encoding: "utf8" }); +console.log(viaPath.status, viaPath.stdout.trim()); +const defaultPath = spawnSync("sh", ["-c", "echo default"], { env: { A: "1" }, encoding: "utf8" }); +console.log(defaultPath.status, defaultPath.stdout.trim()); +const notOnPath = spawnSync("sh", ["-c", "echo never"], { env: { PATH: dir }, encoding: "utf8" }); +console.log(notOnPath.status, notOnPath.error?.message); +const denied = spawnSync("not-executable", [], { env: { PATH: dir }, encoding: "utf8" }); +console.log(denied.status, denied.error?.message); +rmSync(dir, { recursive: true }); diff --git a/tests/corpus/7033-any-fields-into-declared-records.ts b/tests/corpus/7033-any-fields-into-declared-records.ts new file mode 100644 index 000000000..89c0f69e8 --- /dev/null +++ b/tests/corpus/7033-any-fields-into-declared-records.ts @@ -0,0 +1,28 @@ +// Records whose fields TypeScript types `any` convert into declared +// record types: each such field is checked against the declared type, +// as a typed binding initialized from the value is. +interface Transfer { + source: string; + targetId: string; + bytes: number; + tags: string[]; +} +const meta: any = JSON.parse('{"id":"x-1","size":4,"tags":["a","b"]}'); + +const mapped: Transfer[] = ["a", "b"].map((s, i) => ({ + source: s, + targetId: meta.id, + bytes: meta.size + i, + tags: meta.tags, +})); +console.log(JSON.stringify(mapped)); + +function build(): Transfer { + const record = { source: "c", targetId: meta.id, bytes: meta.size, tags: [] as string[] }; + return record; +} +console.log(JSON.stringify(build())); + +const list: Transfer[] = []; +for (const entry of [{ source: "d", targetId: meta.id, bytes: 1, tags: meta.tags }]) list.push(entry); +console.log(list.length, list[0]!.targetId.toUpperCase(), list[0]!.tags.length); diff --git a/tests/corpus/7034-optional-field-union-routes.ts b/tests/corpus/7034-optional-field-union-routes.ts new file mode 100644 index 000000000..0fb93ff8d --- /dev/null +++ b/tests/corpus/7034-optional-field-union-routes.ts @@ -0,0 +1,26 @@ +// A record whose `T | undefined` field is the one field that tells two +// arms of a union apart converts into that union: a value picks the arm +// that declares the field, and undefined picks the arm without it. +type Summary = + | { written: number; errors: string[]; ignored: number } + | { written: number; ignored: number }; + +function summarize(written: number, ignored: number, errors: string[]): Summary { + return { written, errors: errors.length > 0 ? errors : undefined, ignored }; +} +function errorCount(summary: Summary): number { + return "errors" in summary && summary.errors !== undefined ? summary.errors.length : 0; +} +for (const errors of [[], ["e1", "e2"]]) { + const summary = summarize(3, 1, errors); + console.log(JSON.stringify(summary), errorCount(summary), summary.written + summary.ignored); +} + +type Lookup = { name: string; value: number; source: string } | { name: string; source: string }; +const values = new Map([["a", 1]]); +const lookups: Lookup[] = ["a", "b"].map((name) => ({ + name, + value: values.get(name), + source: "map", +})); +console.log(JSON.stringify(lookups)); diff --git a/tests/corpus/7036-child-env-path-lookup.ts b/tests/corpus/7036-child-env-path-lookup.ts new file mode 100644 index 000000000..97c93d835 --- /dev/null +++ b/tests/corpus/7036-child-env-path-lookup.ts @@ -0,0 +1,53 @@ +// spawn and execFileSync with an env option find the command on the +// replacement environment's PATH, or on the default path when it has +// none, as Node does; a relative PATH entry is read from the cwd option. +import { execFileSync, spawn } from "node:child_process"; +import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const dir = mkdtempSync(join(tmpdir(), "scr-child-path-")); +mkdirSync(join(dir, "tools")); +writeFileSync(join(dir, "tools", "child-tool"), "#!/bin/sh\necho child-tool $1\n"); +chmodSync(join(dir, "tools", "child-tool"), 0o755); + +// An earlier PATH entry holding a script whose interpreter is missing +// moves the lookup on to the next entry, as execvp does. +mkdirSync(join(dir, "broken")); +writeFileSync(join(dir, "broken", "child-tool"), "#!/nonexistent/interpreter\n"); +chmodSync(join(dir, "broken", "child-tool"), 0o755); + +const show = (e: unknown): string => { + const err = e as NodeJS.ErrnoException; + return `${err.code} ${err.message.replace(dir, "")}`; +}; + +console.log(execFileSync("child-tool", ["sync"], { env: { PATH: join(dir, "tools") }, encoding: "utf8" }).trim()); +console.log(execFileSync("sh", ["-c", "echo default"], { env: { A: "1" }, encoding: "utf8" }).trim()); +try { + execFileSync("sh", ["-c", "echo never"], { env: { PATH: "/nonexistent" }, encoding: "utf8" }); +} catch (e) { + console.log(show(e)); +} +const twoEntries = `${join(dir, "broken")}:${join(dir, "tools")}`; +console.log(execFileSync("child-tool", ["fallthrough"], { env: { PATH: twoEntries }, encoding: "utf8" }).trim()); +try { + execFileSync("child-tool", [], { env: { PATH: join(dir, "broken") }, encoding: "utf8" }); +} catch (e) { + console.log(show(e)); +} +console.log( + execFileSync("child-tool", ["relative"], { env: { PATH: "tools" }, cwd: dir, encoding: "utf8" }).trim(), +); + +const child = spawn("child-tool", ["async"], { env: { PATH: twoEntries }, stdio: ["ignore", "pipe", "ignore"] }); +let out = ""; +child.stdout!.on("data", (chunk: Buffer) => (out += chunk.toString())); +child.on("close", (code) => { + console.log(out.trim(), code); + const missing = spawn("sh", ["-c", "true"], { env: { PATH: "/nonexistent" }, stdio: "ignore" }); + missing.on("error", (e) => { + console.log("error", show(e)); + rmSync(dir, { recursive: true }); + }); +}); diff --git a/tests/corpus/7037-js-var-open-writes/main.ts b/tests/corpus/7037-js-var-open-writes/main.ts new file mode 100644 index 000000000..cc0612974 --- /dev/null +++ b/tests/corpus/7037-js-var-open-writes/main.ts @@ -0,0 +1,18 @@ +// A JavaScript package whose `var` bindings start as one type and later +// hold another, as js-yaml's timestamp and error code does: a number that +// becomes a string and back (`fraction = match[7].slice(0, 3)`), an object +// literal that gains a member (`mark.snippet = snippet(mark)`), and `+=` of +// a string onto a number. Node runs every write; each binding holds it. +import { errorAt, millis, label, total } from "scalar-marks"; + +console.log(millis("2001-12-14t21:59:43.10"), millis("2001-12-14t21:59:43"), millis("1970-01-01t00:00:00.12345")); +for (const [input, at] of [["a: [1", 5], ["key: 'open", 10], ["a:\n [1", 7], ["ok", 1]] as const) { + try { + console.log(errorAt(input, at, "unexpected end")); + } catch (e) { + const err = e as { name: string; message: string; mark: { line: number; column: number; snippet: string } }; + console.log(err.name, "|", err.message, "|", err.mark.line, err.mark.column, JSON.stringify(err.mark.snippet)); + } +} +console.log(label(3), label(0)); +console.log(total([1, 2, 3]), total(["a", "b"]), total([1, "b", 2])); diff --git a/tests/corpus/7037-js-var-open-writes/node_modules/scalar-marks/index.d.ts b/tests/corpus/7037-js-var-open-writes/node_modules/scalar-marks/index.d.ts new file mode 100644 index 000000000..0e0f8252c --- /dev/null +++ b/tests/corpus/7037-js-var-open-writes/node_modules/scalar-marks/index.d.ts @@ -0,0 +1,4 @@ +export declare function millis(data: string): number; +export declare function errorAt(input: string, position: number, message: string): string; +export declare function label(n: number): string; +export declare function total(values: (number | string)[]): number | string; diff --git a/tests/corpus/7037-js-var-open-writes/node_modules/scalar-marks/index.js b/tests/corpus/7037-js-var-open-writes/node_modules/scalar-marks/index.js new file mode 100644 index 000000000..7d6b6116c --- /dev/null +++ b/tests/corpus/7037-js-var-open-writes/node_modules/scalar-marks/index.js @@ -0,0 +1,81 @@ +"use strict"; +var TIMESTAMP = new RegExp( + '^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)' + + '[Tt]([0-9][0-9]?):([0-9][0-9]):([0-9][0-9])(?:\\.([0-9]*))?$'); + +// The milliseconds of a timestamp: the fraction starts as the number 0, +// holds its digits as a string while it is padded, and ends as a number. +function millis(data) { + var match, fraction = 0, delta = null; + match = TIMESTAMP.exec(data); + if (match === null) throw new Error('Date resolve error'); + if (match[7]) { + fraction = match[7].slice(0, 3); + while (fraction.length < 3) { + fraction += '0'; + } + fraction = +fraction; + } + if (delta) fraction -= delta; + return Date.UTC(+match[1], +match[2] - 1, +match[3], +match[4], +match[5], +match[6], fraction); +} + +function State(input) { + this.input = input; + this.position = 0; + this.line = 0; + this.lineStart = 0; +} + +function snippet(mark) { + var start = mark.position, text = mark.buffer; + return text.slice(0, start) + '<' + text.slice(start) + '>'; +} + +function MarkError(reason, mark) { + Error.call(this); + this.name = 'MarkError'; + this.reason = reason; + this.mark = mark; + this.message = reason + ' (' + (mark.line + 1) + ':' + (mark.column + 1) + ')'; +} +MarkError.prototype = Object.create(Error.prototype); +MarkError.prototype.constructor = MarkError; + +// The error's mark is an object literal that gains its snippet afterwards. +function generateError(state, message) { + var mark = { + buffer: state.input, + position: state.position, + line: state.line, + column: state.position - state.lineStart + }; + mark.snippet = snippet(mark); + return new MarkError(message, mark); +} + +function errorAt(input, position, message) { + var state = new State(input); + for (var i = 0; i < position && i < input.length; i++) { + state.position++; + if (input[i] === '\n') { state.line++; state.lineStart = state.position; } + } + if (state.position >= input.length) throw generateError(state, message); + return 'no error at ' + state.position; +} + +// A number binding that `+=` turns into a string. +function label(n) { + var width = 10; + width += 'px'; + return n + ' ' + width; +} + +// A sum that becomes a string concatenation once a string arrives. +function total(values) { + var sum = 0; + for (var i = 0; i < values.length; i++) sum += values[i]; + return sum; +} + +module.exports = { millis: millis, errorAt: errorAt, label: label, total: total }; diff --git a/tests/corpus/7037-js-var-open-writes/node_modules/scalar-marks/package.json b/tests/corpus/7037-js-var-open-writes/node_modules/scalar-marks/package.json new file mode 100644 index 000000000..ade0a9891 --- /dev/null +++ b/tests/corpus/7037-js-var-open-writes/node_modules/scalar-marks/package.json @@ -0,0 +1 @@ +{ "name": "scalar-marks", "version": "1.0.0", "main": "./index.js", "types": "./index.d.ts" } diff --git a/tests/corpus/7038-js-var-later-writes.mjs b/tests/corpus/7038-js-var-later-writes.mjs new file mode 100644 index 000000000..48b85d77a --- /dev/null +++ b/tests/corpus/7038-js-var-later-writes.mjs @@ -0,0 +1,59 @@ +// JavaScript `var` and `let` bindings that later hold another type than +// their initializer's, which nothing checks in a JavaScript file: Node +// runs each write, and a function returning the binding returns what it +// holds. +function retyped(input) { + var value = 0; + value = input.slice(0, 2); + return value; +} + +function padded(digits) { + var fraction = 0; + if (digits) { + fraction = digits.slice(0, 3); + while (fraction.length < 3) fraction += "0"; + fraction = +fraction; + } + return fraction + 1; +} + +function suffixed(n) { + var width = n * 2; + width += "px"; + return width; +} + +function letRetyped(record) { + let current = 1; + current = record.name; + return current; +} + +function grown(line) { + var mark = { line: line, column: line - 1 }; + mark.text = "at " + (mark.line + 1); + return mark; +} + +async function awaitedTotal(values) { + var total = 0; + for (var i = 0; i < values.length; i++) total += await values[i]; + console.log("awaited", total, typeof total); + return total; +} + +function counted(values) { + var count = 0; + for (var i = 0; i < values.length; i++) count += values[i]; + return count; +} + +console.log(retyped("abc"), typeof retyped("abc")); +console.log(padded("5"), padded("12345"), padded("")); +console.log(suffixed(4), typeof suffixed(4)); +console.log(letRetyped({ name: "x" }), letRetyped({ name: 2 })); +console.log(JSON.stringify(grown(3)), Object.keys(grown(0)).join(",")); +console.log(counted([1, 2]), counted([1, "a", 2])); +await awaitedTotal([1, Promise.resolve(2)]); +await awaitedTotal([1, Promise.resolve("b")]); diff --git a/tests/corpus/7060-array-numeric-keys.cjs b/tests/corpus/7060-array-numeric-keys.cjs new file mode 100644 index 000000000..5a1978cd9 --- /dev/null +++ b/tests/corpus/7060-array-numeric-keys.cjs @@ -0,0 +1,50 @@ +// Number keys that are not array indices ("-1", "1.5", "NaN", "Infinity") +// are ordinary properties of an array. Writes and reads through an untyped +// function, through a string key, and through the array itself all reach +// the same property, and key walks list them after the indices. + +function put(target, key, value) { + target[key] = value; +} +function get(target, key) { + return target[key]; +} +function has(target, key) { + return key in target; +} +function drop(target, key) { + delete target[key]; +} + +const empty = []; +empty[-1] = "x"; +empty[1.5] = "y"; +empty[NaN] = "z"; +console.log(empty[-1], empty[1.5], empty[NaN], empty.length); +console.log(empty["-1"], empty["1.5"], empty["NaN"]); + +const words = ["p", "q"]; +put(words, -1, "x"); +put(words, 1.5, "y"); +put(words, NaN, "z"); +put(words, Infinity, "inf"); +console.log(get(words, -1), get(words, 1.5), get(words, NaN), get(words, Infinity)); +console.log(words[-1], words[1.5], words[NaN], words[Infinity], words["-1"], words.length); +console.log(get(words, "-1"), get(words, "1.50"), get(words, "-0"), has(words, "1.5")); +console.log(Object.keys(words), JSON.stringify(words)); +for (const key in words) console.log("key", key); + +words[-1] = "again"; +console.log(get(words, -1), words["-1"]); +put(words, "label", "named"); +console.log(get(words, "label"), get(words, -1)); +drop(words, -1); +console.log(words[-1], has(words, -1), get(words, 1.5)); +console.log(Object.keys(words)); + +const numbers = [1, 2]; +put(numbers, -1, 10); +put(numbers, 2.5, 20); +console.log(numbers[-1], get(numbers, -1), numbers[2.5], numbers.length); +numbers.push(3); +console.log(get(numbers, -1), numbers.join(",")); diff --git a/tests/corpus/7061-array-object-keys.ts b/tests/corpus/7061-array-object-keys.ts new file mode 100644 index 000000000..93691a78f --- /dev/null +++ b/tests/corpus/7061-array-object-keys.ts @@ -0,0 +1,28 @@ +// Object.keys, Object.values and Object.entries of a typed array list the +// present indices, then its numeric properties ("-1", "1.5"), then its +// named properties, as in Node. Holes are skipped. + +type Bag = Record; + +const words: string[] = ["p", "q"]; +words[-1] = "x"; +(words as unknown as Bag)["1.5"] = "y"; +(words as unknown as Bag).label = "named"; +console.log(Object.keys(words), Object.values(words), Object.entries(words)); +console.log((words as unknown as Bag)["-1"], words[1.5], "1.5" in (words as unknown as Bag)); +for (const key of Object.keys(words)) console.log(key, key.length); + +const holes: (number | undefined)[] = [1, , 3, undefined]; +console.log(Object.keys(holes), Object.values(holes), Object.entries(holes)); + +const empty: number[] = []; +console.log(Object.keys(empty).length, Object.entries(empty)); + +interface Point { + x: number; +} +const points: Point[] = [{ x: 1 }, { x: 2 }]; +console.log(Object.keys(points), Object.values(points)); +const keys: string[] = Object.keys(points); +keys.push("extra"); +console.log(keys.join("|"), Object.keys(points).length); diff --git a/tests/corpus/7062-inspect-array-properties.ts b/tests/corpus/7062-inspect-array-properties.ts new file mode 100644 index 000000000..7cd5ca984 --- /dev/null +++ b/tests/corpus/7062-inspect-array-properties.ts @@ -0,0 +1,61 @@ +// console.log of an array lists its named and numeric properties after the +// elements, as Node's util.inspect does: typed arrays written through an +// unknown view, regex match results, arrays from JSON.parse, holes, Node's +// column grouping and the 100-entry limit. + +type Bag = Record; + +const named: number[] = [1, 2, 3]; +(named as unknown as Bag).extra = "named"; +console.log(named); + +const words: string[] = ["a"]; +(words as unknown as Bag)["my-key"] = { deep: [1, { x: 1 }] }; +console.log(words, [words]); + +const empty: number[] = []; +(empty as unknown as Bag).x = 1; +console.log(empty); + +const numeric: number[] = [1, 2]; +numeric[-1] = 5; +numeric[1.5] = 6; +console.log(numeric); + +const holes: (number | undefined)[] = [1, , 3]; +(holes as unknown as Bag).tail = true; +console.log(holes); + +const grid: number[] = []; +for (let i = 0; i < 30; i++) grid.push(i * 7); +(grid as unknown as Bag).label = "grid"; +console.log(grid); + +const long: number[] = []; +for (let i = 0; i < 120; i++) long.push(i); +(long as unknown as Bag).label = "long"; +console.log(long); + +const full: number[] = []; +for (let i = 0; i < 100; i++) full.push(i); +(full as unknown as Bag).label = "full"; +console.log(full); + +const match = /b(?c)?/.exec("abc"); +console.log(match); +const plainMatch = /x/.exec("axb"); +console.log(plainMatch); + +const parsed = JSON.parse("[1,2,3,4,5,6,7,8]") as unknown as Bag; +parsed.extra = "named"; +console.log(parsed); + +const plain: number[] = [1, 2, 3]; +console.log(plain, [] as number[]); + +const accessed: number[] = [1, 2]; +const accessedBag = accessed as unknown as Bag; +Object.defineProperty(accessedBag, "both", { get: () => 1, set: (_v: unknown) => {}, enumerable: true }); +Object.defineProperty(accessedBag, "getter", { get: () => 2, enumerable: true }); +Object.defineProperty(accessedBag, "setter", { set: (_v: unknown) => {}, enumerable: true }); +console.log(accessed); diff --git a/tests/corpus/7143-optional-read-integer-index.ts b/tests/corpus/7143-optional-read-integer-index.ts new file mode 100644 index 000000000..43fd59a6e --- /dev/null +++ b/tests/corpus/7143-optional-read-integer-index.ts @@ -0,0 +1,70 @@ +// Element reads that may be undefined, at indices with a proven integer +// range: loop counters with an unknown limit, `i | 0`, `x >>> 0`, negative +// counters, -0, indices past the length and the capacity, and copies +// `a[x] = b[y]!` that meet holes. +function copy(src: number[], n: number): number[] { + const out: number[] = new Array(n); + for (let i = 0; i < n; i++) out[i] = src[i]!; + return out; +} +const src: number[] = [1, 2, 3, 4]; +const copied = copy(src, 6); +console.log(copied.length, copied.join(","), 4 in copied, copied[5] === undefined); + +const holes: number[] = new Array(5); +holes[0] = 10; +holes[3] = 13; +const fromHoles = copy(holes, 5); +console.log(fromHoles.join(","), 1 in fromHoles, 3 in fromHoles); + +function show(a: (string | undefined)[], from: number, to: number): string { + const out: string[] = []; + for (let i = from; i < to; i++) { + const v = a[i]; + out.push(v === undefined ? "_" : v); + } + return out.join(","); +} +const words: (string | undefined)[] = ["a", undefined, "c"]; +words[5] = "f"; +console.log(show(words, -3, 8)); +console.log(show(words, 0, 0), show(words, 2, 1)); + +function masked(a: number[], keys: number[]): string { + const out: string[] = []; + for (const k of keys) { + const v = a[k | 0]; + const w = a[k >>> 0]; + out.push(`${v === undefined ? "-" : v}/${w === undefined ? "-" : w}`); + } + return out.join(" "); +} +const values: number[] = [5, 6, 7]; +console.log(masked(values, [0, 1.9, 2, 3, -1, -0, 4294967297, 2 ** 31])); + +const negativeZero = -0; +console.log(values[negativeZero], values[0 * -1], Object.is(negativeZero, -0)); + +// A counter that starts below zero and one that passes the capacity. +let total = 0; +for (let i = -2; i < values.length + 3; i++) total += values[i] ?? 100; +console.log(total); +const big: number[] = []; +for (let k = 0; k < 20; k++) big.push(k); +big.length = 10; +let seen = 0; +for (let i = 0; i < 40; i++) if (big[i] !== undefined) seen++; +console.log(seen, big.length); + +// Rotations through the read, with a hole that becomes undefined. +function rotate(p: number[], r: number): void { + const first = p[0]!; + for (let i = 0; i < r; i++) p[i] = p[i + 1]!; + p[r] = first; +} +const ring: number[] = new Array(4); +ring[0] = 0; +ring[1] = 1; +ring[3] = 3; +rotate(ring, 3); +console.log(ring.join(","), 1 in ring, ring[1] === undefined); diff --git a/tests/corpus/7144-paired-while-counters.ts b/tests/corpus/7144-paired-while-counters.ts new file mode 100644 index 000000000..2198d213f --- /dev/null +++ b/tests/corpus/7144-paired-while-counters.ts @@ -0,0 +1,244 @@ +// While loops with two counters closing in on each other (`lo++; hi--;` at +// the end of the body): reversals over arrays, typed arrays and strings, +// either comparison direction, inclusive bounds, larger steps, parameters as +// counters, early exits, unproven starts, and the shapes that keep the loop +// as it is (a continue, a counter read after the loop, a captured counter, +// a counter the body writes). +function reverse(a: number[], from: number, to: number): number[] { + let lo = from; + let hi = to; + while (lo < hi) { + const t = a[lo]!; + a[lo] = a[hi]!; + a[hi] = t; + lo++; + hi--; + } + return a; +} +const base = (): number[] => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; +console.log(reverse(base(), 0, 9).join(",")); +console.log(reverse(base(), 2, 6).join(",")); +console.log(reverse(base(), 6, 2).join(",")); +console.log(reverse(base(), 3, 3).join(",")); +console.log(reverse(base(), -0, 2).join(","), reverse(base(), 7, -0).join(",")); + +// The parameters are the counters; the updates come in the other order. +function reverseRange(a: Float64Array, lo: number, hi: number): string { + while (hi > lo) { + const t = a[lo]!; + a[lo] = a[hi]!; + a[hi] = t; + hi -= 1; + ++lo; + } + return a.join(","); +} +console.log(reverseRange(new Float64Array([1, 2, 3, 4, 5, 6]), 0, 5)); +console.log(reverseRange(new Float64Array([1, 2, 3, 4, 5, 6]), 1, 3.5)); +console.log(reverseRange(new Float64Array([1, 2, 3, 4, 5, 6]), -2, 2)); +console.log(reverseRange(new Float64Array([1, 2, 3, 4, 5, 6]), NaN, 4), reverseRange(new Float64Array([1, 2]), 0, NaN)); + +function bytesReversed(text: string): string { + const bytes = new TextEncoder().encode(text); + let i = 0, + j = bytes.length - 1; + while (i < j) { + const t = bytes[i]!; + bytes[i] = bytes[j]!; + bytes[j] = t; + i++; + j--; + } + return new TextDecoder().decode(bytes); +} +console.log(bytesReversed("hello, world"), bytesReversed(""), bytesReversed("x")); + +// Unproven starts read through charCodeAt (which truncates its index). +function codes(s: string, from: number, to: number): string { + const out: number[] = []; + let lo = from; + let hi = to; + while (lo <= hi) { + out.push(s.charCodeAt(lo), s.charCodeAt(hi), lo, hi); + lo += 2; + hi -= 3; + } + return out.join(","); +} +const word = "abcdefghijkl"; +console.log(codes(word, 0, 11), "|", codes(word, 0.5, 9.25), "|", codes(word, 4, 4)); +console.log(codes(word, 2 ** 53 - 3, 2 ** 53), "|", codes(word, -3, 2), "|", codes(word, 5, 1)); + +// Negative zero starts stay negative zero. +function signs(text: string, from: number): string { + const out: string[] = []; + let lo = from; + let hi = text.length - 1; + while (lo < hi) { + out.push(`${Object.is(lo, -0)}:${text.charCodeAt(lo)}`); + lo++; + hi--; + } + let a = -0; + let b = 2; + while (a < b) { + out.push(`${Object.is(a, -0)}:${text.charCodeAt(b)}`); + a++; + b--; + } + return out.join(" "); +} +console.log(signs("abcd", -0), signs("abcd", 0)); + +function isPalindrome(s: string): boolean { + let i = 0; + let j = s.length - 1; + while (i < j) { + if (s.charCodeAt(i) !== s.charCodeAt(j)) return false; + i++; + j--; + } + return true; +} +console.log(isPalindrome("racecar"), isPalindrome("abca"), isPalindrome(""), isPalindrome("ab")); + +function firstMismatch(a: number[]): number { + let found = -1; + let lo = 0; + let hi = a.length - 1; + while (lo < hi) { + const left = a[lo]!; + if (left !== a[hi]!) { + found = lo; + break; + } + lo++; + hi--; + } + return found; +} +console.log(firstMismatch([1, 2, 3, 2, 1]), firstMismatch([1, 2, 3, 4, 2, 1]), firstMismatch([])); + +// A labeled jump to an outer loop leaves the inner loop from its middle. +function rows(grid: number[][]): string { + const out: string[] = []; + outer: for (const row of grid) { + let lo = 0; + let hi = row.length - 1; + while (lo < hi) { + const left = row[lo]!; + const right = row[hi]!; + if (left === 0) continue outer; + if (right === 0) break outer; + out.push(`${left}${right}`); + lo++; + hi--; + } + out.push("|"); + } + return out.join(" "); +} +console.log(rows([[1, 2, 3, 4], [0, 5, 6], [7, 8, 9, 1], [2, 3, 0, 4]])); + +// Repeated entries: the counters are declared inside an outer loop. Each +// pass reverses the prefix that ends at the index the first element holds, +// until the first element is 0, and the passes are counted. +function prefixReversals(values: number[]): number { + const copy = values.slice(); + let count = 0; + let end = copy[0]!; + while (end !== 0) { + let i = 0; + let j = end; + while (i < j) { + const t = copy[i]!; + copy[i] = copy[j]!; + copy[j] = t; + i++; + j--; + } + count++; + end = copy[0]!; + } + return count; +} +console.log( + prefixReversals([3, 1, 2, 0]), + prefixReversals([0, 1, 2]), + prefixReversals([4, 3, 2, 1, 0]), + prefixReversals([2, 0, 1]), +); + +// Holes and present undefined values move as they are. +const sparse: (number | undefined)[] = [1, , 3, undefined, 5]; +{ + let lo = 0; + let hi = sparse.length - 1; + while (lo < hi) { + const t = sparse[lo]; + sparse[lo] = sparse[hi]; + sparse[hi] = t; + lo++; + hi--; + } +} +console.log(sparse, 1 in sparse, 3 in sparse); + +// These keep the while loop: a continue skips the trailing updates, a +// counter is read after the loop, a closure captures a counter, and the +// body writes a counter. +function skipping(n: number): string { + const out: number[] = []; + let lo = 0; + let hi = n; + while (lo < hi) { + if (lo % 3 === 0) { + lo += 2; + continue; + } + out.push(lo, hi); + lo++; + hi--; + } + return out.join(","); +} +console.log(skipping(12)); + +function meet(n: number): string { + let lo = 0; + let hi = n; + while (lo < hi) { + lo++; + hi--; + } + return `${lo}:${hi}`; +} +console.log(meet(9), meet(10), meet(0.5)); + +function captured(n: number): string { + const seen: (() => number)[] = []; + let lo = 0; + let hi = n; + while (lo < hi) { + seen.push(() => lo * 100 + hi); + lo++; + hi--; + } + return seen.map((f) => f()).join(","); +} +console.log(captured(5)); + +function jumps(a: number[]): string { + const out: number[] = []; + let lo = 0; + let hi = a.length - 1; + while (lo < hi) { + out.push(a[lo]!, a[hi]!); + if (a[lo]! > 5) hi = lo; + lo++; + hi--; + } + return out.join(","); +} +console.log(jumps([1, 2, 9, 4, 5, 6, 7, 8])); diff --git a/tests/corpus/7150-cipher-legacy-modes.ts b/tests/corpus/7150-cipher-legacy-modes.ts new file mode 100644 index 000000000..80edfd029 --- /dev/null +++ b/tests/corpus/7150-cipher-legacy-modes.ts @@ -0,0 +1,74 @@ +// createCipheriv and createDecipheriv in the legacy modes: AES in ECB, +// CFB, CFB8, CFB1 and OFB, triple DES (three- and two-key) in CBC, ECB, +// CFB, CFB8, CFB1 and OFB, and bare chacha20. Fixed keys, IVs and inputs +// give Node's exact ciphertexts; chunked updates, string encodings, +// disabled padding and chacha20's 64-bit counter carry round-trip. +import { createCipheriv, createDecipheriv } from "node:crypto"; + +function bytes(length: number, seed: number): Buffer { + const out = Buffer.alloc(length); + for (let i = 0; i < length; i++) out[i] = (i * 29 + seed) & 0xff; + return out; +} + +const plaintext = Buffer.from("The five boxing wizards jump quickly; sphinx of black quartz, judge my vow."); + +function encrypt(name: string, key: Buffer, iv: Buffer | null, data: Buffer, chunk: number): Buffer { + const cipher = createCipheriv(name, key, iv); + const parts: Buffer[] = []; + for (let off = 0; off < data.length; off += chunk) parts.push(cipher.update(data.subarray(off, off + chunk))); + parts.push(cipher.final()); + return Buffer.concat(parts); +} + +function decrypt(name: string, key: Buffer, iv: Buffer | null, data: Buffer, chunk: number): Buffer { + const decipher = createDecipheriv(name, key, iv); + const parts: Buffer[] = []; + for (let off = 0; off < data.length; off += chunk) parts.push(decipher.update(data.subarray(off, off + chunk))); + parts.push(decipher.final()); + return Buffer.concat(parts); +} + +const cases: [string, number, number][] = []; +for (const bits of [128, 192, 256]) + for (const mode of ["ecb", "cfb", "cfb8", "cfb1", "ofb"]) cases.push([`aes-${bits}-${mode}`, bits / 8, mode === "ecb" ? 0 : 16]); +for (const name of ["des-ede3-cbc", "des3", "des-ede3", "des-ede3-ecb", "des-ede3-cfb", "des-ede3-cfb8", "des-ede3-cfb1", "des-ede3-ofb"]) + cases.push([name, 24, name === "des-ede3" || name === "des-ede3-ecb" ? 0 : 8]); +for (const name of ["des-ede-cbc", "des-ede", "des-ede-ecb", "des-ede-cfb", "des-ede-ofb"]) + cases.push([name, 16, name === "des-ede" || name === "des-ede-ecb" ? 0 : 8]); +cases.push(["chacha20", 32, 16]); + +for (const [name, keyLength, ivLength] of cases) { + const key = bytes(keyLength, keyLength + 3); + const iv = ivLength ? bytes(ivLength, 11) : null; + const sealed = encrypt(name, key, iv, plaintext, plaintext.length); + const chunked = encrypt(name, key, iv, plaintext, 7); + const opened = decrypt(name, key, iv, sealed, 5); + console.log(name.padEnd(14), sealed.length, sealed.toString("hex").slice(0, 48), chunked.equals(sealed), opened.equals(plaintext)); +} + +// String input and output encodings through a stream mode. +const ofb = createCipheriv("aes-128-ofb", bytes(16, 1), bytes(16, 2)); +const encoded = ofb.update("68656c6c6f20776f726c64", "hex", "base64") + ofb.final("base64"); +const unofb = createDecipheriv("aes-128-ofb", bytes(16, 1), bytes(16, 2)); +console.log(encoded, unofb.update(encoded, "base64", "utf8") + unofb.final("utf8")); + +// ECB and triple-DES CBC without padding over whole blocks. +for (const [name, keyLength, ivLength, block] of [["aes-256-ecb", 32, 0, 16], ["des-ede3-cbc", 24, 8, 8]] as const) { + const cipher = createCipheriv(name, bytes(keyLength, 5), ivLength ? bytes(ivLength, 6) : null); + cipher.setAutoPadding(false); + const sealed = Buffer.concat([cipher.update(bytes(block * 3, 7)), cipher.final()]); + const decipher = createDecipheriv(name, bytes(keyLength, 5), ivLength ? bytes(ivLength, 6) : null); + decipher.setAutoPadding(false); + const opened = Buffer.concat([decipher.update(sealed), decipher.final()]); + console.log(name, "unpadded", sealed.length, sealed.toString("hex").slice(0, 32), opened.equals(bytes(block * 3, 7))); +} + +// chacha20's counter is the IV's first 8 bytes, little-endian: the low +// word carries into the high word, which wraps. +const chachaKey = bytes(32, 9); +for (const counter of ["ffffffff00000000", "ffffffffffffffff", "fffffffe01000000"]) { + const iv = Buffer.from(counter + "0102030405060708", "hex"); + const stream = encrypt("chacha20", chachaKey, iv, Buffer.alloc(200), 50); + console.log("chacha20", counter, stream.toString("hex").slice(120, 152), stream.subarray(64, 96).toString("hex")); +} diff --git a/tests/corpus/7151-cipher-legacy-errors.cjs b/tests/corpus/7151-cipher-legacy-errors.cjs new file mode 100644 index 000000000..fe9c59bcc --- /dev/null +++ b/tests/corpus/7151-cipher-legacy-errors.cjs @@ -0,0 +1,83 @@ +// createCipheriv errors for the legacy modes, in Node's order: the IV +// length each mode takes (none for ECB), the key length, OpenSSL's +// "unsupported" for the ciphers its legacy provider holds, and the block +// modes' final-block and padding failures; the AEAD-only methods throw +// Node's invalid-state errors on the stream modes. +const crypto = require("crypto"); + +function attempt(label, run) { + try { + const value = run(); + console.log(label, "ok", Buffer.isBuffer(value) ? value.toString("hex") : value); + } catch (e) { + console.log(label, e.name, e.code, e.message, e.library, e.reason, JSON.stringify(e.opensslErrorStack)); + } +} + +const k16 = Buffer.alloc(16, 1), k24 = Buffer.alloc(24, 1), k32 = Buffer.alloc(32, 1); +attempt("ecb null iv", () => crypto.createCipheriv("aes-128-ecb", k16, null).final()); +attempt("ecb empty iv", () => crypto.createCipheriv("aes-128-ecb", k16, Buffer.alloc(0)).final()); +attempt("ecb 16-byte iv", () => crypto.createCipheriv("aes-128-ecb", k16, Buffer.alloc(16))); +attempt("cfb 15-byte iv", () => crypto.createCipheriv("aes-128-cfb", k16, Buffer.alloc(15))); +attempt("cfb null iv", () => crypto.createCipheriv("aes-128-cfb", k16, null)); +attempt("ofb key 15", () => crypto.createCipheriv("aes-128-ofb", Buffer.alloc(15), Buffer.alloc(16))); +attempt("des3 16-byte iv", () => crypto.createCipheriv("des-ede3-cbc", k24, Buffer.alloc(16))); +attempt("des3 16-byte key", () => crypto.createCipheriv("des-ede3-cbc", k16, Buffer.alloc(8))); +attempt("des-ede 24-byte key", () => crypto.createCipheriv("des-ede-cbc", k24, Buffer.alloc(8))); +attempt("des-ede3 ecb with iv", () => crypto.createCipheriv("des-ede3", k24, Buffer.alloc(8))); +attempt("chacha20 12-byte iv", () => crypto.createCipheriv("chacha20", k32, Buffer.alloc(12))); +attempt("chacha20 authTagLength ignored", () => crypto.createCipheriv("chacha20", k32, Buffer.alloc(16), { authTagLength: 16 }).update("x")); +attempt("upper-case name", () => crypto.createCipheriv("DES-EDE3-CBC", k24, Buffer.alloc(8)).final()); +attempt("des-cbc", () => crypto.createCipheriv("des-cbc", Buffer.alloc(8), Buffer.alloc(8))); +attempt("des-cbc bad iv", () => crypto.createCipheriv("des-cbc", Buffer.alloc(8), Buffer.alloc(3))); +attempt("des-cbc bad key", () => crypto.createCipheriv("des-cbc", Buffer.alloc(3), Buffer.alloc(8))); +attempt("bf-cbc", () => crypto.createDecipheriv("bf-cbc", k16, Buffer.alloc(8))); +attempt("rc4", () => crypto.createCipheriv("rc4", k16, null)); +attempt("rc4 with iv", () => crypto.createCipheriv("rc4", k16, Buffer.alloc(8))); +attempt("seed-ecb", () => crypto.createCipheriv("seed-ecb", k16, null)); +attempt("unknown cipher", () => crypto.createCipheriv("aes-128-xyz", k16, Buffer.alloc(16))); + +attempt("cfb getAuthTag", () => { + const c = crypto.createCipheriv("aes-128-cfb", k16, Buffer.alloc(16)); + c.update("x"); + c.final(); + return c.getAuthTag(); +}); +attempt("ofb setAAD", () => crypto.createCipheriv("aes-128-ofb", k16, Buffer.alloc(16)).setAAD(Buffer.alloc(1))); +attempt("chacha20 setAuthTag", () => crypto.createDecipheriv("chacha20", k32, Buffer.alloc(16)).setAuthTag(Buffer.alloc(16))); +attempt("cfb update sizes", () => { + const c = crypto.createCipheriv("aes-128-cfb", k16, Buffer.alloc(16)); + return [c.update("abc").length, c.update("defghijklmnopqrstu").length, c.final().length].join(","); +}); +attempt("cfb1 setAutoPadding(false)", () => { + const c = crypto.createCipheriv("aes-128-cfb1", k16, Buffer.alloc(16)); + c.setAutoPadding(false); + return Buffer.concat([c.update("abc"), c.final()]); +}); +attempt("ecb unpadded short", () => { + const c = crypto.createCipheriv("aes-128-ecb", k16, null); + c.setAutoPadding(false); + c.update(Buffer.alloc(31, 7)); + return c.final(); +}); +attempt("des3 unpadded short", () => { + const c = crypto.createCipheriv("des-ede3-cbc", k24, Buffer.alloc(8)); + c.setAutoPadding(false); + c.update(Buffer.alloc(7)); + return c.final(); +}); +attempt("des3 bad decrypt", () => { + const d = crypto.createDecipheriv("des-ede3-cbc", k24, Buffer.alloc(8)); + d.update(Buffer.alloc(16, 9)); + return d.final(); +}); +attempt("ecb decrypt 15 bytes", () => { + const d = crypto.createDecipheriv("aes-128-ecb", k16, null); + d.update(Buffer.alloc(15)); + return d.final(); +}); +attempt("ofb update after final", () => { + const c = crypto.createCipheriv("aes-128-ofb", k16, Buffer.alloc(16)); + c.final(); + return c.update("x"); +}); diff --git a/tests/corpus/7152-crypto-prime.ts b/tests/corpus/7152-crypto-prime.ts new file mode 100644 index 000000000..79edcc502 --- /dev/null +++ b/tests/corpus/7152-crypto-prime.ts @@ -0,0 +1,96 @@ +// checkPrimeSync, checkPrime, generatePrimeSync and generatePrime: known +// primes, composites and strong pseudoprimes as bigints, Buffers, typed +// arrays, ArrayBuffers and DataViews, the checks option, and generated +// primes' length, top bits, primality, residues and safety, as +// ArrayBuffers and bigints, synchronously and through the callbacks (whose +// error parameter, typed Error | null, receives null where Node passes +// undefined). The small sizes and the add and rem forms have one possible +// result. +import { checkPrime, checkPrimeSync, generatePrime, generatePrimeSync } from "node:crypto"; + +const mersenne61 = 2n ** 61n - 1n; +const mersenne127 = 2n ** 127n - 1n; +const mersenne521 = 2n ** 521n - 1n; +const known: [string, bigint][] = [ + ["0", 0n], + ["1", 1n], + ["2", 2n], + ["3", 3n], + ["4", 4n], + ["9", 9n], + ["17863", 17863n], + ["17881", 17881n], + ["561 (Carmichael)", 561n], + ["2047 (strong pseudoprime to 2)", 2047n], + ["3215031751 (strong pseudoprime to 2, 3, 5 and 7)", 3215031751n], + ["2^61 - 1", mersenne61], + ["2^61 + 1", mersenne61 + 2n], + ["2^127 - 1", mersenne127], + ["(2^61 - 1)(2^127 - 1)", mersenne61 * mersenne127], + ["2^521 - 1", mersenne521], + ["2^521 + 1", mersenne521 + 2n], +]; +for (const [label, value] of known) console.log(label, checkPrimeSync(value)); + +console.log("Buffer 13", checkPrimeSync(Buffer.from([0, 0, 13]))); +console.log("Uint8Array 15", checkPrimeSync(new Uint8Array([15]))); +const words = new ArrayBuffer(8); +new DataView(words).setBigUint64(0, mersenne61); +console.log("ArrayBuffer 2^61 - 1", checkPrimeSync(words)); +console.log("DataView 2^61 - 1", checkPrimeSync(new DataView(words))); +console.log("empty", checkPrimeSync(new Uint8Array(0))); +console.log("checks 1", checkPrimeSync(97n, { checks: 1 })); +console.log("checks 200", checkPrimeSync(3215031751n, { checks: 200 })); + +const bitLength = (n: bigint): number => n.toString(2).length; +const fromBuffer = (buffer: ArrayBuffer): bigint => BigInt("0x" + Buffer.from(buffer).toString("hex")); + +for (const bits of [2, 3, 4]) console.log("size", bits, generatePrimeSync(bits, { bigint: true })); +const five = generatePrimeSync(5, { bigint: true }); +console.log("size 5", five === 29n || five === 31n); +for (const bits of [8, 16, 31, 32, 33, 64, 127, 256, 512]) { + const p = generatePrimeSync(bits, { bigint: true }); + console.log("size", bits, bitLength(p) === bits, p >> BigInt(bits - 2), checkPrimeSync(p)); +} +const buffer = generatePrimeSync(96); +console.log("ArrayBuffer", buffer instanceof ArrayBuffer, buffer.byteLength, checkPrimeSync(buffer), bitLength(fromBuffer(buffer))); +console.log("bigint false", generatePrimeSync(40, { bigint: false }).byteLength); + +for (const bits of [3, 6, 7]) console.log("safe", bits, generatePrimeSync(bits, { safe: true, bigint: true })); +for (const bits of [16, 64, 128]) { + const p = generatePrimeSync(bits, { safe: true, bigint: true }); + console.log("safe", bits, bitLength(p) === bits, p >> BigInt(bits - 2), checkPrimeSync(p), checkPrimeSync((p - 1n) / 2n)); +} + +console.log("add 200 rem 1", generatePrimeSync(8, { add: 200n, rem: 1n, bigint: true })); +console.log("add 128", generatePrimeSync(8, { add: 128n, bigint: true })); +console.log("add 16 rem 3", generatePrimeSync(5, { add: 16n, rem: 3n, bigint: true })); +const safe4 = generatePrimeSync(5, { add: 4n, safe: true, bigint: true }); +console.log("add 4 safe", safe4 === 23n || safe4 === 47n); +for (const [label, add, rem] of [ + ["add 12 rem 11", 12n, 11n], + ["add 6", 6n, undefined], + ["add 1000 rem 7", 1000n, 7n], +] as const) { + const p = generatePrimeSync(48, rem === undefined ? { add, bigint: true } : { add, rem, bigint: true }); + console.log(label, p % add, checkPrimeSync(p), bitLength(p) >= 48); +} +const viaBuffer = generatePrimeSync(64, { add: Buffer.from([0x01, 0x00]), rem: new Uint8Array([0x0f]), bigint: true }); +console.log("add and rem as bytes", viaBuffer % 256n, checkPrimeSync(viaBuffer)); +const safeAdd = generatePrimeSync(64, { add: 24n, rem: 23n, safe: true, bigint: true }); +console.log("safe add 24 rem 23", safeAdd % 24n, checkPrimeSync((safeAdd - 1n) / 2n)); +console.log("rem without add", generatePrimeSync(16, { rem: 5n, bigint: true }) < 65536n); + +generatePrime(64, (err, prime) => { + console.log("callback", err ?? "no error", prime instanceof ArrayBuffer, prime.byteLength, checkPrimeSync(prime)); + generatePrime(128, { safe: true, bigint: true }, (err2, safe) => { + console.log("callback safe", err2 ?? "no error", bitLength(safe), checkPrimeSync((safe - 1n) / 2n)); + checkPrime(safe, (err3, result) => { + console.log("checkPrime", err3 ?? "no error", result); + checkPrime(Buffer.from([1, 1]), { checks: 3 }, (err4, result2) => { + console.log("checkPrime 257", err4 ?? "no error", result2); + }); + }); + }); +}); +console.log("sync done"); diff --git a/tests/corpus/7153-crypto-prime-errors.cjs b/tests/corpus/7153-crypto-prime-errors.cjs new file mode 100644 index 000000000..ff32e6e09 --- /dev/null +++ b/tests/corpus/7153-crypto-prime-errors.cjs @@ -0,0 +1,80 @@ +// checkPrime, checkPrimeSync, generatePrime and generatePrimeSync errors from +// JavaScript: argument types and ranges, the options bag, add and rem, and +// OpenSSL's generation failures, synchronously and through the callbacks. +const crypto = require("crypto"); + +function attempt(label, run) { + try { + const value = run(); + console.log(label, "ok", typeof value === "bigint" ? `${value}n` : value instanceof ArrayBuffer ? `ArrayBuffer(${value.byteLength})` : value); + } catch (e) { + console.log(label, e.name, e.code, e.message, JSON.stringify(Object.keys(e))); + } +} + +attempt("check string", () => crypto.checkPrimeSync("7")); +attempt("check number", () => crypto.checkPrimeSync(7)); +attempt("check negative", () => crypto.checkPrimeSync(-7n)); +attempt("check large negative", () => crypto.checkPrimeSync(-(2n ** 40n))); +attempt("check options null", () => crypto.checkPrimeSync(7n, null)); +attempt("check options array", () => crypto.checkPrimeSync(7n, [])); +attempt("check options string", () => crypto.checkPrimeSync(7n, "x")); +attempt("check checks string", () => crypto.checkPrimeSync(7n, { checks: "1" })); +attempt("check checks fraction", () => crypto.checkPrimeSync(7n, { checks: 1.5 })); +attempt("check checks -1", () => crypto.checkPrimeSync(7n, { checks: -1 })); +attempt("check checks 2^31", () => crypto.checkPrimeSync(7n, { checks: 2 ** 31 })); +attempt("check checks -0", () => crypto.checkPrimeSync(7n, { checks: -0 })); +attempt("check checks 10", () => crypto.checkPrimeSync(7n, { checks: 10 })); +attempt("check no callback", () => crypto.checkPrime(7n)); +attempt("check callback string", () => crypto.checkPrime(7n, {}, "cb")); +attempt("check async string", () => crypto.checkPrime("7", () => {})); +attempt("check async options null", () => crypto.checkPrime(7n, null, () => {})); + +attempt("size string", () => crypto.generatePrimeSync("8")); +attempt("size fraction", () => crypto.generatePrimeSync(8.5)); +attempt("size 0", () => crypto.generatePrimeSync(0)); +attempt("size 2^31", () => crypto.generatePrimeSync(2 ** 31)); +attempt("size NaN", () => crypto.generatePrimeSync(NaN)); +attempt("size 1", () => crypto.generatePrimeSync(1)); +attempt("size 2 safe", () => crypto.generatePrimeSync(2, { safe: true })); +attempt("size 4 safe", () => crypto.generatePrimeSync(4, { safe: true })); +attempt("size 5 safe", () => crypto.generatePrimeSync(5, { safe: true })); +attempt("options null", () => crypto.generatePrimeSync(8, null)); +attempt("options number", () => crypto.generatePrimeSync(8, 1)); +attempt("safe string", () => crypto.generatePrimeSync(8, { safe: "yes" })); +attempt("bigint number", () => crypto.generatePrimeSync(8, { bigint: 1 })); +attempt("add number", () => crypto.generatePrimeSync(8, { add: 6 })); +attempt("add string", () => crypto.generatePrimeSync(8, { add: "6" })); +attempt("add negative", () => crypto.generatePrimeSync(8, { add: -6n })); +attempt("rem number", () => crypto.generatePrimeSync(8, { add: 6n, rem: 1 })); +attempt("rem negative", () => crypto.generatePrimeSync(8, { add: 6n, rem: -1n })); +attempt("add too long", () => crypto.generatePrimeSync(8, { add: 1024n })); +attempt("add zero", () => crypto.generatePrimeSync(8, { add: 0n })); +attempt("add zero bytes", () => crypto.generatePrimeSync(8, { add: new Uint8Array(0) })); +attempt("rem equal add", () => crypto.generatePrimeSync(8, { add: 6n, rem: 6n })); +attempt("rem above add", () => crypto.generatePrimeSync(8, { add: 6n, rem: 7n })); +attempt("rem without add", () => crypto.generatePrimeSync(8, { rem: 1000n, bigint: true }) < 256n); +attempt("add 1 bigint", () => crypto.generatePrimeSync(8, { add: 1n, bigint: true }) < 256n); +attempt("gen no callback", () => crypto.generatePrime(8)); +attempt("gen options callback", () => crypto.generatePrime(8, {})); +attempt("gen size string", () => crypto.generatePrime("8", () => {})); +attempt("gen add too long", () => crypto.generatePrime(8, { add: 1024n }, () => {})); +attempt("gen options null", () => crypto.generatePrime(8, null, () => {})); + +// The callback forms' generation errors and results, one after another. +crypto.generatePrime(1, function (err, prime) { + console.log("gen 1 callback", arguments.length, err.name, err.code, err.message, JSON.stringify(Object.keys(err)), prime); + crypto.generatePrime(8, { add: 0n }, function (err2, prime2) { + console.log("gen add 0 callback", arguments.length, err2.message, prime2); + crypto.generatePrime(3, { safe: true, bigint: true }, function (err3, prime3) { + console.log("gen 3 safe callback", arguments.length, err3, prime3); + crypto.checkPrime(7n, function (err4, result) { + console.log("check 7 callback", arguments.length, err4, result); + crypto.checkPrime(new Uint8Array([0, 0, 9]), { checks: 3 }, function (err5, result2) { + console.log("check 9 callback", arguments.length, err5, result2); + }); + }); + }); + }); +}); +console.log("sync end"); diff --git a/tests/corpus/7184-reduce-runtime-accumulator.ts b/tests/corpus/7184-reduce-runtime-accumulator.ts new file mode 100644 index 000000000..bd2c8e4d9 --- /dev/null +++ b/tests/corpus/7184-reduce-runtime-accumulator.ts @@ -0,0 +1,24 @@ +// Array reduce keeps the values its callback and initial value produce at +// run time: an initial value read from a missing element is undefined, a +// callback can produce another primitive through `??`, and a parenthesized +// callback widens like a bare one. +const noStrings: string[] = []; +const noNumbers: number[] = []; + +const fromMissingInit = ([] as number[]).reduce((_acc, _value) => "ok", noStrings[0]); +console.log(typeof fromMissingInit, fromMissingInit); + +const nonEmpty = [1, 2].reduce((acc, value) => acc + value, noNumbers[0]); +console.log(typeof nonEmpty, nonEmpty); + +const fallback = [1, 2].reduce((_acc, _value) => noNumbers[0] ?? "fallback"); +console.log(typeof fallback, fallback); + +const parenthesized = [1, 2].reduce((acc, x) => (x === 1 ? noStrings[0] : acc + "!"), ""); +console.log(parenthesized); + +const wrapped = [1, 2].reduce(((acc, x) => (x === 1 ? noStrings[0] : acc + "!")), ""); +console.log(wrapped); + +const right = ["a", "b"].reduceRight((acc, s) => acc + s, noStrings[0]); +console.log(right); diff --git a/tests/corpus/7185-array-callback-arguments-length.ts b/tests/corpus/7185-array-callback-arguments-length.ts new file mode 100644 index 000000000..ae5a8d847 --- /dev/null +++ b/tests/corpus/7185-array-callback-arguments-length.ts @@ -0,0 +1,42 @@ +// `arguments.length` in a function-expression callback counts every +// argument the array method passes: value, index and array, plus the +// accumulator for reduce. One stored in a fixed-arity function type counts +// that type's parameters, which every call passes. +const seen: number[] = []; +[10, 20].forEach(function (value) { + seen.push(arguments.length, value); +}); +console.log(seen.join(",")); + +console.log( + [1, 2] + .map(function (value) { + return `${value}:${arguments.length}`; + }) + .join(" "), +); + +console.log([5].filter(function () { + return arguments.length === 3; +}).length); + +console.log([1].reduce(function (acc, value) { + return acc + value + arguments.length; +}, 0)); + +console.log(["x", "y"].reduceRight(function (acc: string, value: string) { + return `${acc}${value}${arguments.length}`; +}, "")); + +console.log([3, 4].some(function (value) { + return arguments.length === 3 && value === 4; +})); + +const sum: (a: number, b: number) => number = function (a) { + return a + arguments.length; +}; +console.log(sum(1, 2)); + +console.log([3, 1, 2].sort(function (a, b) { + return arguments.length === 2 ? a - b : 0; +}).join(",")); diff --git a/tests/corpus/7246-stream-construct.ts b/tests/corpus/7246-stream-construct.ts new file mode 100644 index 000000000..7cfb82e49 --- /dev/null +++ b/tests/corpus/7246-stream-construct.ts @@ -0,0 +1,156 @@ +// Deferred construction in typed code: the construct option and the +// _construct method on Readable, Writable, Duplex and Transform. Writes +// and reads wait for the callback, an error from it destroys the stream, +// a destroy while constructing reports both errors as an AggregateError, +// a throw inside construct is its error, and a second callback is +// ERR_MULTIPLE_CALLBACK. +import { EventEmitter } from "node:events"; +import { Duplex, Readable, Transform, Writable } from "node:stream"; + +const log: string[] = []; + +function ticks(count: number): void { + let i = 0; + const step = (): void => { + log.push("t" + i); + if (++i < count) process.nextTick(step); + }; + process.nextTick(step); +} + +class Store extends Writable { + private ready = false; + _construct(callback: (error?: Error | null) => void): void { + log.push("store construct"); + setTimeout(() => { + this.ready = true; + log.push("store ready"); + callback(); + }, 2); + } + _write(chunk: Buffer, _enc: string, cb: (error?: Error | null) => void): void { + log.push(`store write ${chunk.toString()} ready=${this.ready}`); + cb(); + } + _final(cb: (error?: Error | null) => void): void { + log.push("store final"); + cb(); + } +} + +class Pair extends Duplex { + constructor() { + super({ readableObjectMode: true }); + } + _construct(callback: (error?: Error | null) => void): void { + log.push("pair construct"); + process.nextTick(callback); + } + _read(): void {} + _write(chunk: Buffer, _enc: string, cb: (error?: Error | null) => void): void { + this.push({ got: chunk.toString() }); + cb(); + } + _final(cb: (error?: Error | null) => void): void { + this.push(null); + cb(); + } +} + +function closed(stream: EventEmitter): Promise { + return new Promise((resolve) => stream.on("close", () => resolve())); +} + +async function main(): Promise { + ticks(4); + const store = new Store(); + log.push("write " + store.write("a")); + store.end("b"); + store.on("finish", () => log.push("store finish")); + await closed(store); + + const source = new Readable({ + construct(cb: (error?: Error | null) => void) { + log.push("source construct"); + process.nextTick(cb); + }, + read() { + log.push("source read"); + this.push("x"); + this.push(null); + }, + }); + source.on("data", (c: Buffer) => log.push("source data " + c.toString())); + await closed(source); + + const pair = new Pair(); + pair.on("data", (v: { got: string }) => log.push("pair data " + v.got)); + pair.write("p"); + pair.end(); + await closed(pair); + + const upper = new Transform({ + construct(cb: (error?: Error | null) => void) { + log.push("upper construct"); + cb(); + }, + transform(c: Buffer, _e: string, cb: (error: Error | null, data?: Buffer) => void) { + cb(null, Buffer.from(c.toString().toUpperCase())); + }, + }); + upper.on("data", (c: Buffer) => log.push("upper " + c.toString())); + upper.end("shout"); + await closed(upper); + + const failing = new Writable({ + construct(cb: (error?: Error | null) => void) { + cb(new Error("no target")); + }, + write(_c: Buffer, _e: string, cb: (error?: Error | null) => void) { + cb(); + }, + }); + failing.on("error", (e: Error) => log.push("failing " + e.message)); + await closed(failing); + + const raced = new Writable({ + construct(cb: (error?: Error | null) => void) { + setTimeout(() => cb(new Error("late")), 2); + }, + write(_c: Buffer, _e: string, cb: (error?: Error | null) => void) { + cb(); + }, + destroy(err: Error | null, cb: (error?: Error | null) => void) { + log.push("raced destroy " + (err ? err.message : "none")); + cb(err); + }, + }); + raced.on("error", (e: Error) => log.push(`raced ${e.name} ${e.message}`)); + raced.destroy(new Error("early")); + await closed(raced); + + const thrown = new Writable({ + construct() { + throw new Error("thrown in construct"); + }, + write(_c: Buffer, _e: string, cb: (error?: Error | null) => void) { + cb(); + }, + }); + thrown.on("error", (e: Error) => log.push("thrown " + e.message)); + await closed(thrown); + + const twice = new Writable({ + construct(cb: (error?: Error | null) => void) { + cb(); + cb(); + }, + write(_c: Buffer, _e: string, cb: (error?: Error | null) => void) { + cb(); + }, + }); + twice.on("error", (e: Error) => log.push("twice " + (e as NodeJS.ErrnoException).code)); + await closed(twice); + console.log(log.join("\n")); +} +main(); diff --git a/tests/corpus/7247-stream-construct.mjs b/tests/corpus/7247-stream-construct.mjs new file mode 100644 index 000000000..355116fa7 --- /dev/null +++ b/tests/corpus/7247-stream-construct.mjs @@ -0,0 +1,101 @@ +// Deferred construction in JavaScript: construct in an options literal, +// in an options value read at run time, and _construct on a constructor +// function's prototype, with the write, read, error, destroy-while- +// constructing, throwing and double-callback paths. +import * as stream from "node:stream"; +import { Writable, Readable } from "node:stream"; +import * as util from "node:util"; +const log = []; +const tick = (n) => { let i = 0; const f = () => { log.push("t" + i); if (++i < n) process.nextTick(f); }; process.nextTick(f); }; +tick(4); +const wopts = { + construct(cb) { log.push("construct"); setTimeout(() => { log.push("constructed"); cb(); }, 2); }, + write(c, e, cb) { log.push("write " + c + "/" + e); cb(); }, + final(cb) { log.push("final"); cb(); }, +}; +const w = new Writable(dyn(wopts)); +log.push("write ret " + w.write("a")); +w.end("b"); +w.on("finish", () => log.push("finish")); +w.on("close", () => { log.push("close"); next(); }); +function next() { + const ropts = { construct(cb) { log.push("r construct"); process.nextTick(cb); }, read() { log.push("read"); r.push("x"); r.push(null); } }; + const r = new Readable(dyn(ropts)); + r.on("data", (c) => log.push("data " + c)); + r.on("end", () => { log.push("r end"); errs(); }); +} +function errs() { + const eo = { construct(cb) { cb(new Error("boom")); }, write(c, e2, cb) { cb(); } }; + const e = new Writable(dyn(eo)); + e.on("error", (err) => log.push("error " + err.message)); + e.on("close", () => { log.push("e close"); destroyDuring(); }); +} +function destroyDuring() { + const dopts = { construct(cb) { setTimeout(() => cb(new Error("late")), 2); }, write(c, e, cb) { cb(); }, destroy(err, cb) { log.push("destroy " + (err && err.message)); cb(err); } }; + const d = new Writable(dyn(dopts)); + d.on("error", (err) => log.push("d error " + err.name + " " + err.message + " " + (err.errors ? err.errors.map((x) => x.message).join("+") : ""))); + d.on("close", () => { log.push("d close"); throwing(); }); + d.destroy(new Error("early")); +} +function throwing() { + const to = { construct(cb) { throw new Error("thrown"); }, write(c, e, cb) { cb(); } }; + const t = new Writable(dyn(to)); + t.on("error", (err) => log.push("t error " + err.message)); + t.on("close", () => { log.push("t close"); twice(); }); +} +function twice() { + const o = { construct(cb) { cb(); cb(); }, write(c, e, cb) { cb(); } }; + const t = new Writable(dyn(o)); + t.on("error", (err) => log.push("twice error " + err.code)); + t.on("close", () => { log.push("twice close"); viaLiteral(); }); + setTimeout(() => t.destroy(), 5); +} +function dyn(o) { + return o; +} +function viaLiteral() { + const lit = new Writable({ + construct(cb) { + log.push("literal construct " + (this === lit)); + setImmediate(cb); + }, + write(chunk, enc, cb) { + log.push(`literal write ${chunk}/${enc}`); + cb(); + }, + }); + lit.end("z", () => { + log.push("literal end"); + viaConstructor(); + }); +} +function viaConstructor() { + function Bad() { + stream.Readable.call(this); + } + util.inherits(Bad, stream.Readable); + Bad.prototype._construct = function () { + throw new Error("ctor threw"); + }; + Bad.prototype._read = function () {}; + function Late(opts) { + stream.Writable.call(this, opts); + } + util.inherits(Late, stream.Writable); + Late.prototype._construct = function (cb) { + log.push("late construct " + (this instanceof Late)); + setTimeout(cb, 1); + }; + Late.prototype._write = function (chunk, enc, cb) { + log.push("late write " + chunk); + cb(); + }; + const late = new Late(); + late.write("q"); + late.end(() => { + log.push("late end"); + const bad = new Bad(); + bad.on("error", (e) => log.push("bad " + e.message)); + bad.on("close", () => console.log(log.join("\n"))); + }); +} diff --git a/tests/corpus/7248-stream-writev.ts b/tests/corpus/7248-stream-writev.ts new file mode 100644 index 000000000..2df298d6d --- /dev/null +++ b/tests/corpus/7248-stream-writev.ts @@ -0,0 +1,124 @@ +// Batched writes in typed code: the writev option and the _writev method +// on Writable and Duplex. Writes buffered behind a write in flight or a +// cork reach writev as one [{ chunk, encoding }] batch whose callback +// runs the writes' callbacks in order; a stream with only writev sends +// each lone write as a batch of one; a later `stream._writev = fn` +// takes over; an object-mode side batches its values. +import { EventEmitter } from "node:events"; +import { Duplex, Writable } from "node:stream"; + +interface Entry { + chunk: Buffer; + encoding: string; +} + +interface ValueEntry { + chunk: unknown; + encoding: string | undefined; +} + +const log: string[] = []; + +function finished(stream: EventEmitter): Promise { + return new Promise((resolve) => stream.on("finish", () => resolve())); +} + +class Batcher extends Writable { + _write(chunk: Buffer, _enc: string, cb: (error?: Error | null) => void): void { + log.push("batcher write " + chunk.toString()); + setTimeout(cb, 1); + } + _writev(chunks: Entry[], cb: (error?: Error | null) => void): void { + log.push(`batcher writev ${chunks.map((c) => c.chunk.toString() + "/" + c.encoding).join(",")}`); + setTimeout(cb, 1); + } +} + +class Echo extends Duplex { + _read(): void {} + _write(chunk: Buffer, _enc: string, cb: (error?: Error | null) => void): void { + this.push(chunk); + cb(); + } + _writev(chunks: Entry[], cb: (error?: Error | null) => void): void { + this.push(Buffer.concat(chunks.map((c) => c.chunk))); + cb(); + } + _final(cb: (error?: Error | null) => void): void { + this.push(null); + cb(); + } +} + +async function main(): Promise { + const batcher = new Batcher(); + batcher.write("a", () => log.push("a done")); + batcher.write("b", () => log.push("b done")); + batcher.write("c"); + batcher.write(Buffer.from("d"), () => log.push("d done")); + batcher.end(() => log.push("batcher end")); + await finished(batcher); + + const corked = new Writable({ + writev(chunks: Entry[], cb: (error?: Error | null) => void) { + log.push(`corked ${chunks.length}: ${chunks.map((c) => c.chunk.toString("hex")).join(" ")}`); + cb(); + }, + }); + corked.cork(); + corked.write("x"); + corked.write("ff", "hex"); + corked.write("y"); + corked.uncork(); + corked.write("z", () => log.push("z done")); + corked.end(); + await finished(corked); + + const late = new Writable({ + write(c: Buffer, _e: string, cb: () => void) { + log.push("late write " + c.toString()); + cb(); + }, + }); + late._writev = (chunks: Entry[], cb: (error?: Error | null) => void) => { + log.push("late writev " + chunks.length); + cb(); + }; + late.cork(); + late.write("1"); + late.write("2"); + late.uncork(); + late.end(); + await finished(late); + + const echo = new Echo(); + const echoed: string[] = []; + echo.on("data", (c: Buffer) => echoed.push(c.toString())); + echo.cork(); + echo.write("e1"); + echo.write("e2"); + process.nextTick(() => { + echo.uncork(); + echo.end("e3"); + }); + await new Promise((resolve) => echo.on("end", resolve)); + log.push("echoed " + echoed.join(",")); + + const values = new Writable({ + objectMode: true, + writev(chunks: ValueEntry[], cb: (error?: Error | null) => void) { + log.push( + "values " + chunks.map((c) => `${JSON.stringify(c.chunk)}/${String(c.encoding)}`).join(","), + ); + cb(); + }, + }); + values.cork(); + values.write({ n: 1 }); + values.write([2], "utf8"); + values.uncork(); + values.end(); + await finished(values); + console.log(log.join("\n")); +} +main(); diff --git a/tests/corpus/7249-buffer-index-of-candidates.ts b/tests/corpus/7249-buffer-index-of-candidates.ts new file mode 100644 index 000000000..ae6c2ae23 --- /dev/null +++ b/tests/corpus/7249-buffer-index-of-candidates.ts @@ -0,0 +1,27 @@ +// Buffer indexOf, lastIndexOf and includes with needles whose first byte +// recurs: a candidate whose later bytes differ is skipped, matches at the +// first and last positions are found, byteOffset clamps and counts from the +// end, and the search reads only the buffer's own window of its memory. +const text = Buffer.from("aab aaab aaaab ab\nab"); +console.log(text.indexOf("ab"), text.indexOf("aab"), text.indexOf("aaab"), text.indexOf("aaaab"), text.indexOf("aaaaab")); +console.log(text.lastIndexOf("ab"), text.lastIndexOf("aab"), text.lastIndexOf("aaab"), text.lastIndexOf("b\na")); +console.log(text.indexOf("ab", 2), text.indexOf("ab", -3), text.indexOf("ab", 100), text.indexOf("ab", -100)); +console.log(text.lastIndexOf("ab", 5), text.lastIndexOf("ab", -4), text.lastIndexOf("ab", 0), text.lastIndexOf("aa", 1)); +console.log(text.indexOf(10), text.indexOf(97, 3), text.lastIndexOf(98), text.lastIndexOf(97, -2), text.includes("b a")); +console.log(text.indexOf(""), text.indexOf("", 4), text.indexOf("", 100), text.lastIndexOf(""), text.indexOf("x")); +console.log(text.indexOf(Buffer.from("a\nab")), text.indexOf("ab\nab"), text.indexOf("ab\nabc"), text.lastIndexOf("aab", 4)); + +// A window into a larger buffer never sees the bytes outside it. +const whole = Buffer.from("xxneedlexx needle needlexx"); +const window = whole.subarray(3, 22); +console.log(window.toString(), window.indexOf("needle"), window.lastIndexOf("needle"), window.indexOf("xx"), window.includes("xne")); +console.log(window.indexOf("eedle", 1), window.lastIndexOf("eed"), window.indexOf("needlex"), window.lastIndexOf("n", 0)); + +// A long search with many first-byte candidates. +const long = Buffer.alloc(20000, "ab"); +long.write("abc", 19990); +console.log(long.indexOf("abc"), long.lastIndexOf("aba"), long.indexOf("abc", 19991), long.lastIndexOf("abc"), long.indexOf("bb")); +const lines: number[] = []; +const fasta = Buffer.from(">ONE\nACGT\nTTGA\n>TWO\nGGCC\n"); +for (let at = fasta.indexOf(10); at !== -1; at = fasta.indexOf(10, at + 1)) lines.push(at); +console.log(lines.join(","), fasta.indexOf(62, 1), fasta.lastIndexOf(62)); diff --git a/tests/corpus/7249-stream-writev.cjs b/tests/corpus/7249-stream-writev.cjs new file mode 100644 index 000000000..fb6d01960 --- /dev/null +++ b/tests/corpus/7249-stream-writev.cjs @@ -0,0 +1,126 @@ +// Batched writes in JavaScript: the writev option in a literal and in an +// options value, and _writev on a constructor function's prototype. The +// batch entries carry chunk, encoding and callback (Node's nop when the +// write passed none), allBuffers says whether every chunk is a Buffer, +// and a stream with only _writev sends a lone write as { chunk, encoding }. +const stream = require("node:stream"); +const util = require("node:util"); + +const log = []; +const describe = (chunks) => + chunks + .map((c) => { + const chunk = Buffer.isBuffer(c.chunk) ? "B:" + c.chunk.toString() : `${typeof c.chunk}:${c.chunk}`; + const callback = typeof c.callback === "function" ? c.callback.name || "fn" : String(c.callback); + return `${chunk}/${c.encoding}/${callback}`; + }) + .join(",") + + ` all=${chunks.allBuffers} keys=${Object.keys(chunks[0]).join(".")}`; + +function viaLiteral(next) { + const w = new stream.Writable({ + write(chunk, enc, cb) { + log.push("write " + chunk); + setTimeout(cb, 1); + }, + writev(chunks, cb) { + log.push("writev " + describe(chunks)); + setTimeout(cb, 1); + }, + }); + w.write("a", () => log.push("cb a")); + w.write("b", () => log.push("cb b")); + w.write("c"); + w.write(Buffer.from("d"), () => log.push("cb d")); + w.end(() => log.push("end")); + w.on("finish", () => { + log.push("finish"); + next(); + }); +} + +function dyn(value) { + return value; +} + +function viaValue(next) { + const options = { + writev(chunks, cb) { + log.push("value " + describe(chunks)); + cb(); + }, + }; + const v = new stream.Writable(dyn(options)); + v.cork(); + v.write("x"); + v.write("6869", "hex"); + v.uncork(); + v.write("z", () => log.push("z cb")); + v.end(); + v.on("finish", () => { + log.push("value finish"); + next(); + }); +} + +function Sink(opts) { + stream.Writable.call(this, opts); +} +util.inherits(Sink, stream.Writable); +Sink.prototype._writev = function (chunks, cb) { + log.push("sink " + (this instanceof Sink) + " " + describe(chunks)); + cb(); +}; + +function viaConstructor(next) { + const s = new Sink({ decodeStrings: false }); + s.cork(); + s.write("a"); + s.write("6869", "hex"); + s.write(Buffer.from("b")); + s.uncork(); + s.write("lone"); + s.end(() => { + log.push("sink end"); + next(); + }); +} + +// The writev option shadows the prototype's _writev: Node's constructor +// assigns it to the instance (this._writev = options.writev). +function viaOptionOverProto(next) { + function W(opts) { + stream.Writable.call(this, opts); + } + Object.setPrototypeOf(W.prototype, stream.Writable.prototype); + Object.setPrototypeOf(W, stream.Writable); + W.prototype._write = function (chunk, enc, cb) { + log.push("proto _write " + String(chunk)); + cb(); + }; + W.prototype._writev = function (chunks, cb) { + log.push("proto _writev " + chunks.length); + cb(); + }; + // (built without a second `new` of a constructor function) + const w = Object.create(W.prototype); + W.call(w, { + writev(chunks, cb) { + log.push("option writev " + chunks.map((c) => String(c.chunk)).join(",") + " " + (this === w)); + cb(); + }, + }); + log.push("own _writev " + Object.prototype.hasOwnProperty.call(w, "_writev")); + w.cork(); + w.write("a"); + w.write("b"); + w.uncork(); + w.end(() => { + log.push("option end"); + next(); + }); +} + +viaLiteral(() => + viaValue(() => viaConstructor(() => viaOptionOverProto(() => console.log(log.join("\n"))))), +); diff --git a/tests/corpus/7250-stream-decode-strings.ts b/tests/corpus/7250-stream-decode-strings.ts new file mode 100644 index 000000000..57e84aff2 --- /dev/null +++ b/tests/corpus/7250-stream-decode-strings.ts @@ -0,0 +1,89 @@ +// decodeStrings: false and the write-side default encoding in typed code: +// a string write reaches _write and _transform as the string with its +// encoding (its UTF-16 length counts toward writableLength), Buffers keep +// 'buffer', defaultEncoding decides how write(string) encodes (and the +// name _write sees when strings stay strings), and setDefaultEncoding() +// lowercases, validates and chains. +import { PassThrough, Transform, Writable } from "node:stream"; + +const log: string[] = []; + +function describe(chunk: string | Buffer, enc: string): string { + return typeof chunk === "string" ? `string:${chunk}/${enc}` : `B:${chunk.toString("hex")}/${enc}`; +} + +class Keeper extends Writable { + seen: string[] = []; + constructor() { + super({ decodeStrings: false, highWaterMark: 4 }); + } + _write(chunk: string | Buffer, enc: string, cb: (error?: Error | null) => void): void { + this.seen.push(describe(chunk, enc)); + setTimeout(cb, 1); + } +} + +async function main(): Promise { + const keeper = new Keeper(); + log.push("ret " + keeper.write("héllo")); + log.push("length " + keeper.writableLength); + keeper.write("6869", "hex"); + keeper.write(Buffer.from("zz")); + keeper.end("end", "latin1"); + await new Promise((resolve) => keeper.on("finish", resolve)); + log.push("keeper " + keeper.seen.join(" ")); + + const hex = new Writable({ + defaultEncoding: "hex", + write(chunk: Buffer, enc: string, cb: (error?: Error | null) => void) { + log.push(`hex ${chunk.toString()} ${enc}`); + cb(); + }, + }); + hex.write("6869"); + hex.write("41", "utf8"); + hex.setDefaultEncoding("BASE64").write("aGk="); + try { + hex.setDefaultEncoding("nope"); + } catch (e) { + log.push(`setDefaultEncoding ${(e as NodeJS.ErrnoException).code} ${(e as Error).message}`); + } + log.push("chains " + (hex.setDefaultEncoding("utf8") === hex)); + hex.end("ok"); + + const both = new Writable({ + decodeStrings: false, + defaultEncoding: "LATIN1", + write(chunk: string | Buffer, enc: string, cb: (error?: Error | null) => void) { + log.push("both " + describe(chunk, enc)); + cb(); + }, + }); + both.write("x"); + both.setDefaultEncoding("UTF-8"); + both.write("y"); + both.end(); + + const t = new Transform({ + decodeStrings: false, + transform(chunk: string | Buffer, enc: string, cb: (error: Error | null, data?: string | Buffer) => void) { + log.push("transform " + describe(chunk, enc)); + cb(null, chunk); + }, + }); + t.on("data", (c: Buffer) => log.push("transform out " + c.toString("hex"))); + t.write("abc"); + t.end("ff", "hex"); + + const p = new PassThrough({ decodeStrings: false }); + p.on("data", (c: Buffer) => log.push("pass " + c.toString())); + p.end("q"); + + try { + new Writable({ defaultEncoding: "bogus" }); + } catch (e) { + log.push("constructor " + (e as NodeJS.ErrnoException).code); + } + setTimeout(() => console.log(log.join("\n")), 10); +} +main(); diff --git a/tests/corpus/7251-stream-write-encodings.mjs b/tests/corpus/7251-stream-write-encodings.mjs new file mode 100644 index 000000000..5c4242ca8 --- /dev/null +++ b/tests/corpus/7251-stream-write-encodings.mjs @@ -0,0 +1,104 @@ +// Write encodings in JavaScript: decodeStrings: false and defaultEncoding +// in options literals and options values, setDefaultEncoding(), and +// strings written in other encodings to constructor-function streams. +import * as stream from "node:stream"; +import { PassThrough, Transform, Writable } from "node:stream"; +import * as util from "node:util"; + +const log = []; +const describe = (c, e) => `${Buffer.isBuffer(c) ? "B:" + c.toString("hex") : typeof c + ":" + c}/${e}`; +const dyn = (value) => value; + +const keep = new Writable({ decodeStrings: false, highWaterMark: 4, write(c, e, cb) { log.push("keep " + describe(c, e)); cb(); } }); +log.push("ret " + keep.write("héllo") + " length " + keep.writableLength); +keep.write("6869", "hex"); +keep.write(Buffer.from("zz")); +keep.end("end", "latin1"); + +const valueOpts = { decodeStrings: false, defaultEncoding: "HEX", write(c, e, cb) { log.push("value " + describe(c, e)); cb(); } }; +const fromValue = new Writable(dyn(valueOpts)); +fromValue.write("ab"); +fromValue.write(Buffer.from("c")); + +const hex = new Writable({ defaultEncoding: "hex", write(c, e, cb) { log.push("hex " + describe(c, e)); cb(); } }); +hex.write("6869"); +hex.write("41", "utf8"); +hex.setDefaultEncoding("BASE64"); +hex.write("aGk="); +try { + hex.setDefaultEncoding("nope"); +} catch (e) { + log.push("setDefaultEncoding " + e.code + " " + e.message); +} +log.push("chains " + (hex.setDefaultEncoding("utf8") === hex)); + +const t = new Transform({ decodeStrings: false, transform(c, e, cb) { log.push("t " + describe(c, e)); cb(null, c); } }); +t.on("data", (c) => log.push("t data " + describe(c, "-"))); +t.write("abc"); +t.write("ff", "hex"); +const p = new PassThrough({ decodeStrings: false }); +p.on("data", (c) => log.push("p data " + describe(c, "-"))); +p.write("q"); + +const om = new Writable({ objectMode: true, defaultEncoding: "hex", write(c, e, cb) { log.push("om " + describe(c, e)); cb(); } }); +om.write("s"); +try { + new Writable({ defaultEncoding: "bogus" }); +} catch (e) { + log.push("constructor " + e.code); +} + +function Sink() { + stream.Writable.call(this); +} +util.inherits(Sink, stream.Writable); +Sink.prototype._write = function (c, e, cb) { + log.push("sink " + describe(c, e)); + cb(); +}; +const sink = new Sink(); +sink.write("6869", "hex"); +sink.setDefaultEncoding("latin1"); +sink.write("é"); +sink.end("YQ==", "base64"); + +// A typed array or DataView chunk reaches _write and _writev as a Buffer +// over the same bytes (Stream._uint8ArrayToBuffer), also with +// decodeStrings: false; in object mode it stays as it is. +const views = new stream.Writable({ + decodeStrings: false, + write(c, e, cb) { + log.push(`view ${Buffer.isBuffer(c)} ${e} ${c.toString()}`); + cb(); + }, +}); +views.write(new Uint8Array([104, 105])); +views.write(new Uint16Array([0x6a6b])); +views.write(new DataView(new Uint8Array([108]).buffer)); +const batch = new stream.Writable({ + writev(chunks, cb) { + log.push(`view batch ${chunks.map((c) => `${Buffer.isBuffer(c.chunk)}:${c.encoding}:${c.chunk}`).join(" ")}`); + cb(); + }, +}); +batch.cork(); +batch.write(new Uint8Array([97])); +batch.write(new Uint8Array([98])); +batch.uncork(); +const source = new Uint8Array([1, 2]); +new stream.Writable({ + write(c, e, cb) { + log.push(`view plain ${Buffer.isBuffer(c)} ${e} ${c.buffer === source.buffer}`); + cb(); + }, +}).write(source); +new stream.Writable({ + objectMode: true, + write(c, e, cb) { + log.push(`view object ${Buffer.isBuffer(c)} ${c instanceof Uint8Array}`); + cb(); + }, +}).write(new Uint8Array([3])); +log.push(`view source ${Buffer.isBuffer(source)}`); + +setTimeout(() => console.log(log.join("\n")), 5); diff --git a/tests/corpus/7252-duplex-from-generator.ts b/tests/corpus/7252-duplex-from-generator.ts new file mode 100644 index 000000000..488748021 --- /dev/null +++ b/tests/corpus/7252-duplex-from-generator.ts @@ -0,0 +1,69 @@ +// Duplex.from over an async generator function in typed code: the +// generator reads what is written to the Duplex and its values are the +// readable side. Both sides are in object mode with a highWaterMark of +// 1; each write's callback runs once the generator asks for the next +// value, and end() reaches the generator as the end of its source. +import { Duplex } from "node:stream"; + +const log: string[] = []; + +async function* numbered(source: AsyncIterable): AsyncGenerator { + let n = 0; + for await (const chunk of source) { + n++; + log.push(`gen got ${String(chunk)}`); + yield `${n}:${String(chunk)}`; + } + log.push("gen done"); + yield `total ${n}`; +} + +async function collect(d: Duplex): Promise { + const out: string[] = []; + for await (const chunk of d) out.push(String(chunk)); + return out; +} + +async function basic(): Promise { + const d = Duplex.from(numbered); + log.push(`sides ${d.readable} ${d.writable}`); + log.push(`object mode ${d.readableObjectMode} ${d.writableObjectMode}`); + log.push(`hwm ${d.readableHighWaterMark} ${d.writableHighWaterMark}`); + d.write("a", () => log.push("write a done")); + d.write("b", () => log.push("write b done")); + d.end("c", () => log.push("end done")); + const out = await collect(d); + log.push(`read ${out.join(",")}`); + log.push(`after ${d.readableEnded} ${d.writableFinished} ${d.destroyed}`); +} + +async function values(): Promise { + // object-mode writes keep their values + const d = Duplex.from(async function* (source: AsyncIterable): AsyncGenerator { + for await (const v of source) yield typeof v + " " + JSON.stringify(v); + }); + d.write({ k: 1 }); + d.write([2, 3]); + d.write(4); + d.end(); + log.push(`values ${(await collect(d)).join(" | ")}`); +} + +async function earlyDestroy(): Promise { + const d = Duplex.from(numbered); + d.on("error", (e: Error) => log.push(`error ${e.message}`)); + d.on("close", () => log.push(`close ${d.destroyed}`)); + d.write("x"); + await new Promise((resolve) => setTimeout(resolve, 5)); + d.destroy(new Error("stop")); + await new Promise((resolve) => setTimeout(resolve, 5)); +} + +async function main(): Promise { + await basic(); + await values(); + await earlyDestroy(); + for (const line of log) console.log(line); +} + +void main(); diff --git a/tests/corpus/7253-duplex-from.mjs b/tests/corpus/7253-duplex-from.mjs new file mode 100644 index 000000000..14556348a --- /dev/null +++ b/tests/corpus/7253-duplex-from.mjs @@ -0,0 +1,48 @@ +// Duplex.from in JavaScript: an async generator function, an array, a +// generator object and an async generator object. Over an iterable the +// result is read-only (writable false); over a function it reads what is +// written. The log records the order of the write callbacks, the reads +// and the stream events against each other. +import { Duplex } from "node:stream"; + +const log = []; + +async function* echo(source) { + for await (const chunk of source) { + log.push(`echo ${chunk}`); + yield chunk.toUpperCase(); + } +} + +const fromFn = Duplex.from(echo); +fromFn.on("finish", () => log.push("fn finish")); +fromFn.on("end", () => log.push("fn end")); +fromFn.on("close", () => log.push("fn close")); +fromFn.write("one", () => log.push("fn write one cb")); +fromFn.write("two", () => log.push("fn write two cb")); +fromFn.end(() => log.push("fn end cb")); +for await (const chunk of fromFn) log.push(`fn read ${chunk}`); + +const fromArray = Duplex.from(["p", "q"]); +log.push(`array ${fromArray.readable} ${fromArray.writable} ${fromArray.readableObjectMode}`); +for await (const chunk of fromArray) log.push(`array ${chunk}`); + +function* gen() { + yield "g1"; + yield "g2"; +} +const fromGen = Duplex.from(gen()); +log.push(`gen ${fromGen.writable}`); +for await (const chunk of fromGen) log.push(`gen ${chunk}`); + +async function* agen() { + yield 1; + await null; + yield 2; +} +const fromAgen = Duplex.from(agen()); +fromAgen.on("data", (v) => log.push(`agen data ${v}`)); +await new Promise((resolve) => fromAgen.on("end", resolve)); +log.push(`agen ended ${fromAgen.readableEnded}`); + +console.log(log.join("\n")); diff --git a/tests/corpus/7254-compose-functions.ts b/tests/corpus/7254-compose-functions.ts new file mode 100644 index 000000000..4a2ba7189 --- /dev/null +++ b/tests/corpus/7254-compose-functions.ts @@ -0,0 +1,56 @@ +// stream.compose with async generator function stages in typed code: a +// function at either end becomes Duplex.from of it, functions between +// streams run as pipeline stages, and one stage is Duplex.from of it. +// The composed Duplex writes to the first stage and reads from the last; +// its object modes follow the first stage's writable side and the last +// stage's readable side. +import { Duplex, PassThrough, Transform, compose } from "node:stream"; + +const log: string[] = []; + +async function* upper(source: AsyncIterable): AsyncGenerator { + for await (const chunk of source) yield String(chunk).toUpperCase(); +} + +async function* numbered(source: AsyncIterable): AsyncGenerator { + let n = 0; + for await (const chunk of source) yield `${++n}.${String(chunk)}`; +} + +async function drain(name: string, d: Duplex, chunks: string[]): Promise { + log.push(`${name} r=${d.readable} w=${d.writable} ro=${d.readableObjectMode} wo=${d.writableObjectMode}`); + const out: string[] = []; + d.on("data", (c: unknown) => out.push(String(c))); + const ended = new Promise((resolve) => d.on("end", () => resolve())); + for (const c of chunks) d.write(c); + d.end(); + await ended; + log.push(`${name} ${out.join(" ")}`); +} + +async function main(): Promise { + // functions only: Duplex.from at both ends + await drain("fns", compose(upper, numbered), ["a", "b"]); + // a function between two object-mode streams + await drain( + "middle", + compose(new PassThrough({ objectMode: true }), upper, new PassThrough({ objectMode: true })), + ["c", "d"], + ); + // a byte stream before a function: the Duplex writes bytes + const bytes = compose(new PassThrough(), numbered); + await drain("bytes", bytes, ["e"]); + // a function before a Transform + const tail = new Transform({ + objectMode: true, + transform(chunk: unknown, _enc: string, cb: (err?: Error | null, data?: unknown) => void): void { + cb(null, `<${String(chunk)}>`); + }, + }); + await drain("transform", compose(upper, tail), ["f", "g"]); + // one stage + await drain("single", compose(numbered), ["h", "i", "j"]); + for (const line of log) console.log(line); +} + +void main(); diff --git a/tests/corpus/7255-compose-stages.mjs b/tests/corpus/7255-compose-stages.mjs new file mode 100644 index 000000000..ef6542ece --- /dev/null +++ b/tests/corpus/7255-compose-stages.mjs @@ -0,0 +1,55 @@ +// stream.compose in JavaScript with an iterable or generator first stage +// (the Duplex is not writable), a stage that throws (the Duplex errors), +// a destroyed composition (the stages see an AbortError), and compose() +// with no stages (ERR_MISSING_ARGS). +import { PassThrough, compose } from "node:stream"; + +const log = []; + +const double = async function* (source) { + for await (const n of source) yield n * 2; +}; + +const fromArray = compose([1, 2, 3], double); +log.push(`array r=${fromArray.readable} w=${fromArray.writable}`); +for await (const n of fromArray) log.push(`array ${n}`); + +function* counter() { + yield 10; + yield 20; +} +const fromGen = compose(counter(), double, async function* (source) { + for await (const n of source) yield `n=${n}`; +}); +for await (const s of fromGen) log.push(`gen ${s}`); + +const failing = compose(new PassThrough({ objectMode: true }), async function* (source) { + for await (const chunk of source) { + if (chunk === "stop") throw new Error("stage failed on " + chunk); + yield chunk; + } +}, new PassThrough({ objectMode: true })); +failing.on("data", (c) => log.push(`failing data ${c}`)); +failing.on("error", (e) => log.push(`failing error ${e.message}`)); +failing.on("close", () => log.push(`failing close ${failing.destroyed}`)); +failing.write("go"); +failing.write("stop"); +await new Promise((resolve) => failing.on("close", resolve)); + +const stopped = compose(new PassThrough({ objectMode: true }), double); +stopped.on("error", (e) => log.push(`stopped error ${e.name} ${e.code}`)); +stopped.on("close", () => log.push("stopped close")); +stopped.write(4); +stopped.once("data", (n) => { + log.push(`stopped data ${n}`); + stopped.destroy(); +}); +await new Promise((resolve) => stopped.on("close", resolve)); + +try { + compose(); +} catch (e) { + log.push(`empty ${e.code} ${e.message}`); +} + +console.log(log.join("\n")); diff --git a/tests/corpus/7256-readable-compose.mjs b/tests/corpus/7256-readable-compose.mjs new file mode 100644 index 000000000..a9622173e --- /dev/null +++ b/tests/corpus/7256-readable-compose.mjs @@ -0,0 +1,53 @@ +// readable.compose(stage, { signal }): compose(this, stage), with the +// signal aborting the composed Duplex. The stage may be an async +// generator function or a stream; the result reads like the stage's +// output and is not writable when the receiver heads it. +import { PassThrough, Readable, Transform } from "node:stream"; + +const log = []; + +const WORDS = ["alpha", "beta", "gamma"]; +const lengths = async function* (source) { + for await (const w of source) yield `${w}:${w.length}`; +}; + +const source = Readable.from(WORDS); +const viaFn = source.compose(lengths); +log.push(`fn r=${viaFn.readable} w=${viaFn.writable} ro=${viaFn.readableObjectMode}`); +for await (const s of viaFn) log.push(`fn ${s}`); + +const shout = new Transform({ + objectMode: true, + transform(chunk, _enc, cb) { + cb(null, chunk.toUpperCase()); + }, +}); +const source2 = Readable.from(WORDS); +const viaStream = source2.compose(shout); +log.push(`stream ${(await viaStream.toArray()).join(",")}`); + +const pass = new PassThrough({ objectMode: true }); +const chained = pass.compose(lengths).compose(async function* (source) { + for await (const s of source) yield `[${s}]`; +}); +chained.on("data", (s) => log.push(`chained ${s}`)); +pass.write("x"); +pass.end("yz"); +await new Promise((resolve) => chained.on("end", resolve)); + +const ac = new AbortController(); +const signal = ac.signal; +const slow = new Readable({ objectMode: true, read() {} }); +const aborted = slow.compose(lengths, { signal }); +aborted.on("data", (s) => log.push(`aborted data ${s}`)); +aborted.on("error", (e) => log.push(`aborted error ${e.name} ${e.code}`)); +aborted.on("close", () => log.push(`aborted close ${aborted.destroyed}`)); +slow.push("one"); +setTimeout(() => ac.abort(), 10); +await new Promise((resolve) => aborted.on("close", resolve)); + +const source3 = Readable.from(WORDS); +const unsignaled = source3.compose(lengths, undefined); +log.push(`undefined options ${(await unsignaled.toArray()).length}`); + +console.log(log.join("\n")); diff --git a/tests/corpus/7257-pipeline-generator-destination.mjs b/tests/corpus/7257-pipeline-generator-destination.mjs new file mode 100644 index 000000000..0996e06eb --- /dev/null +++ b/tests/corpus/7257-pipeline-generator-destination.mjs @@ -0,0 +1,61 @@ +// pipeline() whose destination is a function: an async generator +// destination is pumped into the object-mode PassThrough pipeline() +// returns, and an async function destination's resolved value is written +// to it (null and undefined are not). The callback runs with the error +// and the destination's value; a generator yielding null fails the +// pipeline with ERR_STREAM_NULL_VALUES. +import { Readable, pipeline } from "node:stream"; + +const log = []; + +const out = pipeline( + Readable.from(["a", "b", "c"]), + async function* (source) { + for await (const chunk of source) yield { chunk, at: log.length }; + }, + (err, value) => log.push(`gen cb ${err} ${value}`), +); +log.push(`gen out r=${out.readable} w=${out.writable} ro=${out.readableObjectMode} wo=${out.writableObjectMode}`); +for await (const item of out) log.push(`gen read ${JSON.stringify(item)}`); + +const summary = pipeline( + Readable.from([3, 4]), + async function* (source) { + for await (const n of source) yield n * n; + }, + async function (source) { + let total = 0; + for await (const n of source) total += n; + return { total }; + }, + (err, value) => log.push(`fn cb ${err} ${JSON.stringify(value)}`), +); +summary.on("data", (v) => log.push(`fn data ${JSON.stringify(v)}`)); +summary.on("end", () => log.push("fn end")); +await new Promise((resolve) => summary.on("close", resolve)); + +const nothing = pipeline( + Readable.from(["x"]), + async function (source) { + for await (const _ of source); + return undefined; + }, + (err) => log.push(`undefined cb ${err}`), +); +nothing.on("data", () => log.push("undefined data")); +nothing.on("end", () => log.push("undefined end")); +await new Promise((resolve) => nothing.on("close", resolve)); + +const bad = pipeline( + Readable.from(["g"]), + async function* (source) { + for await (const chunk of source) yield chunk; + yield null; + }, + (err) => log.push(`null cb ${err && err.code}`), +); +bad.on("data", (c) => log.push(`null data ${c}`)); +bad.on("error", (e) => log.push(`null error ${e.code}`)); +await new Promise((resolve) => bad.on("close", resolve)); + +console.log(log.join("\n")); diff --git a/tests/corpus/7258-pipeline-generator-destination.ts b/tests/corpus/7258-pipeline-generator-destination.ts new file mode 100644 index 000000000..b22b68dce --- /dev/null +++ b/tests/corpus/7258-pipeline-generator-destination.ts @@ -0,0 +1,47 @@ +// pipeline() with a function destination in typed code. @types/node and +// the fallback declarations type the result as the destination stream; +// for a function destination it is the object-mode PassThrough that +// pipeline() returns, read here as a PassThrough. +import { PassThrough, Readable, pipeline } from "node:stream"; + +const log: string[] = []; + +async function* tokens(source: AsyncIterable): AsyncGenerator { + for await (const line of source) { + for (const word of String(line).split(" ")) yield word; + } +} + +async function* tagged(source: AsyncIterable): AsyncGenerator<{ word: string; n: number }> { + let n = 0; + for await (const word of source) yield { word, n: ++n }; +} + +async function main(): Promise { + const out = pipeline(Readable.from(["to be", "or not"]), tokens, tagged, (err: Error | null) => { + log.push(`callback ${err === null || err === undefined ? "ok" : err.message}`); + }) as PassThrough; + log.push(`object mode ${out.readableObjectMode} ${out.writableObjectMode}`); + for await (const item of out) { + const entry = item as { word: string; n: number }; + log.push(`${entry.n} ${entry.word}`); + } + log.push(`ended ${out.readableEnded}`); + + const counted = pipeline( + Readable.from(["x", "y", "z"]), + async function (source: AsyncIterable): Promise { + let n = 0; + for await (const _ of source) n++; + return n; + }, + (err: Error | null) => log.push(`count callback ${err === null || err === undefined}`), + ) as PassThrough; + const values: unknown[] = []; + counted.on("data", (v: unknown) => values.push(v)); + await new Promise((resolve) => counted.on("end", () => resolve())); + log.push(`count values ${JSON.stringify(values)}`); + for (const line of log) console.log(line); +} + +void main(); diff --git a/tests/corpus/7259-promises-pipeline-stage-options.cjs b/tests/corpus/7259-promises-pipeline-stage-options.cjs new file mode 100644 index 000000000..a4a3ce396 --- /dev/null +++ b/tests/corpus/7259-promises-pipeline-stage-options.cjs @@ -0,0 +1,65 @@ +// stream/promises pipeline() with function stages or an iterable source +// and the options argument: end: false leaves the destination open, a +// signal aborts the pipeline (the stages see their own signal abort), an +// async function destination's value resolves the promise, and a value +// that is not an AbortSignal rejects with ERR_INVALID_ARG_TYPE. +const { PassThrough, Writable } = require("node:stream"); +const { pipeline } = require("node:stream/promises"); + +const log = []; + +const upper = async function* (source) { + for await (const chunk of source) yield String(chunk).toUpperCase(); +}; + +function sink(name) { + return new Writable({ + objectMode: true, + write(chunk, _enc, cb) { + log.push(`${name} got ${chunk}`); + cb(); + }, + }); +} + +async function main() { + const joined = await pipeline(["a", "b"], upper, async function (source) { + let s = ""; + for await (const c of source) s += c; + return s; + }, { end: true }); + log.push(`joined ${joined}`); + + const open = new PassThrough({ objectMode: true }); + await pipeline(["k"], upper, open, { end: false }); + log.push(`end false: ended ${open.writableEnded}`); + open.end("last"); + const rest = []; + for await (const c of open) rest.push(c); + log.push(`end false: read ${rest.join(",")}`); + + const ac = new AbortController(); + const slow = async function* (source, { signal }) { + signal.addEventListener("abort", () => log.push(`stage signal aborted ${signal.aborted}`)); + for await (const chunk of source) { + yield chunk; + await new Promise((resolve) => setTimeout(resolve, 30)); + } + }; + const aborting = pipeline(["s1", "s2", "s3"], slow, sink("abort"), { signal: ac.signal }); + setTimeout(() => ac.abort(), 10); + try { + await aborting; + } catch (e) { + log.push(`aborted ${e.name} ${e.code}`); + } + + try { + await pipeline(["v"], upper, sink("bad"), { signal: "nope" }); + } catch (e) { + log.push(`bad signal ${e.code}`); + } + console.log(log.join("\n")); +} + +main(); diff --git a/tests/corpus/7261-acyclic-result-builders.ts b/tests/corpus/7261-acyclic-result-builders.ts new file mode 100644 index 000000000..b6a98fce4 --- /dev/null +++ b/tests/corpus/7261-acyclic-result-builders.ts @@ -0,0 +1,86 @@ +// Records built by functions that return object literals, as tree and list +// builders are written. The backend gives such a function a result-slot +// entry that writes the literal's fields into a caller's frame storage, and +// those stores do not make the record's fields mutable: a type no store +// changes after construction stays acyclic and needs no cycle-collector +// header. This pins what such programs print: rebuilt trees, a shared +// subtree (a DAG), a builder whose result the caller only projects (so the +// entry runs), a list longer than the runtime's teardown depth budget, and +// a record type the program writes after construction, whose rings only +// the collector frees. The sanitized lane asserts every object is freed. + +interface Node { + value: number; + left: Node | null; + right: Node | null; +} + +function leaf(value: number): Node { + return { value, left: null, right: null }; +} + +function branch(value: number, left: Node | null, right: Node | null): Node { + return { value, left, right }; +} + +function tree(depth: number, value: number): Node { + return depth === 0 ? leaf(value) : branch(value, tree(depth - 1, value * 2), tree(depth - 1, value * 2 + 1)); +} + +function sum(node: Node | null): number { + return node === null ? 0 : node.value + sum(node.left) + sum(node.right); +} + +let total = 0; +for (let generation = 0; generation < 8; generation++) total += sum(tree(8, generation + 1)); +console.log("trees", total); + +// A subtree shared by both branches is released once. +const shared = tree(3, 1); +const dag = branch(0, shared, shared); +console.log("dag", sum(dag), JSON.stringify(branch(1, leaf(2), null))); + +// Only projected: the result lives in the caller's frame, which releases +// the subtree it holds. +const top = branch(5, tree(2, 1), null); +console.log("top", top.value, sum(top.left), top.right === null); + +interface Cell { + n: number; + next: Cell | null; +} + +function cons(n: number, next: Cell | null): Cell { + return { n, next }; +} + +let list: Cell | null = null; +for (let i = 0; i < 5000; i++) list = cons(i, list); +let length = 0; +let last = -1; +for (let c = list; c !== null; c = c.next) { + length++; + last = c.n; +} +console.log("list", length, last); +list = null; + +// A record type written after construction keeps its cycle-collector +// header: these rings are garbage only the collector can free. +interface Ring { + id: number; + next: Ring | null; +} + +function ring(id: number, next: Ring | null): Ring { + return { id, next }; +} + +let ids = 0; +for (let i = 0; i < 200; i++) { + const a = ring(i, null); + const b = ring(i + 1, a); + a.next = b; + ids += a.next.id + b.next!.id; +} +console.log("rings", ids); diff --git a/tests/corpus/7262-fresh-literal-untyped-param/lib.js b/tests/corpus/7262-fresh-literal-untyped-param/lib.js new file mode 100644 index 000000000..687caf5c2 --- /dev/null +++ b/tests/corpus/7262-fresh-literal-untyped-param/lib.js @@ -0,0 +1,12 @@ +// zod 4's normalizeParams: moves `message` to `error` and deletes it. +export function normalize(params) { + if (params?.message !== undefined) params.error = params.message; + delete params.message; + return params; +} + +export function strip(options) { + for (const key of Object.keys(options)) if (options[key] === undefined) delete options[key]; + options.stripped = true; + return options; +} diff --git a/tests/corpus/7262-fresh-literal-untyped-param/main.ts b/tests/corpus/7262-fresh-literal-untyped-param/main.ts new file mode 100644 index 000000000..c242c7150 --- /dev/null +++ b/tests/corpus/7262-fresh-literal-untyped-param/main.ts @@ -0,0 +1,18 @@ +// A fresh TypeScript object literal handed to an untyped JavaScript +// parameter, or to a parameter type that declares its properties optional, +// is the plain object the callee may reshape: deleting a property works as +// in Node instead of failing on a record layout that requires it. +import { normalize, strip } from "./lib.js"; + +console.log(JSON.stringify(normalize({ message: "must change at least one field" }))); +console.log(JSON.stringify(normalize({ message: "kept", note: 1 }))); +const n = 2; +console.log(JSON.stringify(strip({ a: n, b: undefined, c: "x" }))); + + +// A package-declared parameter whose type makes `message` optional, on a +// method the package's JavaScript defines (zod's `.refine(fn, { message })`). +import { schema } from "params-kit"; +const s = schema("book"); +console.log(s.refine("plain", { message: "must change at least one field" })); +console.log(s.refine("with path", { message: "bad", path: ["a", "b"] })); diff --git a/tests/corpus/7262-fresh-literal-untyped-param/node_modules/params-kit/lib/index.d.ts b/tests/corpus/7262-fresh-literal-untyped-param/node_modules/params-kit/lib/index.d.ts new file mode 100644 index 000000000..608534249 --- /dev/null +++ b/tests/corpus/7262-fresh-literal-untyped-param/node_modules/params-kit/lib/index.d.ts @@ -0,0 +1,10 @@ +export interface RefineParams { + message?: string; + error?: unknown; + path?: string[]; +} +export interface Schema { + name: string; + refine(label: string, params?: string | RefineParams): string; +} +export declare function schema(name: string): Schema; diff --git a/tests/corpus/7262-fresh-literal-untyped-param/node_modules/params-kit/lib/index.js b/tests/corpus/7262-fresh-literal-untyped-param/node_modules/params-kit/lib/index.js new file mode 100644 index 000000000..2433d9129 --- /dev/null +++ b/tests/corpus/7262-fresh-literal-untyped-param/node_modules/params-kit/lib/index.js @@ -0,0 +1,11 @@ +// zod 4's normalizeParams on an instance method, behind a package declaration. +function normalizeParams(params) { + if (params?.message !== undefined) params.error = params.message; + delete params.message; + return params; +} +export function schema(name) { + const inst = { name }; + inst.refine = (label, params) => `${inst.name}.${label}: ${JSON.stringify(normalizeParams(params))}`; + return inst; +} diff --git a/tests/corpus/7262-fresh-literal-untyped-param/node_modules/params-kit/package.json b/tests/corpus/7262-fresh-literal-untyped-param/node_modules/params-kit/package.json new file mode 100644 index 000000000..1addbb8d0 --- /dev/null +++ b/tests/corpus/7262-fresh-literal-untyped-param/node_modules/params-kit/package.json @@ -0,0 +1 @@ +{ "name": "params-kit", "version": "1.0.0", "type": "module", "main": "./lib/index.js", "types": "./lib/index.d.ts" } diff --git a/tests/corpus/7263-error-accessor-object-helper.ts b/tests/corpus/7263-error-accessor-object-helper.ts new file mode 100644 index 000000000..8e8a4a532 --- /dev/null +++ b/tests/corpus/7263-error-accessor-object-helper.ts @@ -0,0 +1,54 @@ +// An accessor installed over an Error's own `message` and `name` by +// helpers whose target parameter is typed `object`, `{}` or generic: +// typed reads, writes and String() go through the accessors. +class ValidationError extends Error {} + +function defineLazyMessage(target: object, compute: () => string): void { + let cached: string | undefined; + Object.defineProperty(target, "message", { + get() { + cached ??= compute(); + return cached; + }, + set(value: string) { + cached = `set:${value}`; + }, + enumerable: true, + configurable: true, + }); +} + +function defineName(target: {}, name: string): void { + Object.defineProperty(target, "name", { get: () => name, configurable: true }); +} + +function defineFixedMessage(target: T, message: string): T { + Object.defineProperty(target, "message", { get: () => message, configurable: true }); + return target; +} + +let computed = 0; +const e = new ValidationError("orig"); +defineLazyMessage(e, () => { + computed++; + return "too small"; +}); +console.log(e.message, computed); +console.log(String(e), computed); +e.message = "typed"; +console.log(e.message, Object.keys(e).join()); + +const named = new ValidationError("bad input"); +defineName(named, "ZodError"); +console.log(named.name, String(named)); +const fixed = defineFixedMessage(new ValidationError("orig"), "fixed"); +console.log(fixed.message, String(fixed)); + +const plain = new Error("start"); +defineLazyMessage(plain, () => "computed"); +console.log(plain.message, `${plain}`); +try { + throw plain; +} catch (caught) { + console.log(caught instanceof Error && caught.message); +} diff --git a/tests/corpus/7301-running-extrema-branches.ts b/tests/corpus/7301-running-extrema-branches.ts new file mode 100644 index 000000000..9941ce538 --- /dev/null +++ b/tests/corpus/7301-running-extrema-branches.ts @@ -0,0 +1,169 @@ +// Running maximum and minimum scans over ascending, descending, alternating, +// pseudo-random and NaN-holding data: plain for, while, do-while and for-of +// loops, `<=` and `>=` ties, an argmax that records its index, typed array +// and array element reads, callbacks that update captured bindings, a scan +// nested in an outer loop, an update nested in another branch, and the clamp +// and saturating updates that are not running extrema. + +function shapes(n: number): [string, Float64Array][] { + const ascending = new Float64Array(n); + const descending = new Float64Array(n); + const alternating = new Float64Array(n); + const random = new Float64Array(n); + const holes = new Float64Array(n); + let x = 12345; + for (let i = 0; i < n; i++) { + ascending[i] = i; + descending[i] = n - i; + alternating[i] = i % 2 === 0 ? 0 : i; + x = (x * 1103515245 + 12345) % 2147483648; + random[i] = (x % 1000) - 500; + holes[i] = i % 7 === 3 ? NaN : (x % 97) - 48; + } + return [ + ["ascending", ascending], + ["descending", descending], + ["alternating", alternating], + ["random", random], + ["nan", holes], + ]; +} + +function argmax(a: Float64Array): string { + let max = -Infinity; + let at = -1; + for (let i = 0; i < a.length; i++) { + const v = a[i]!; + if (v > max) { + max = v; + at = i; + } + } + return `${max}@${at}`; +} + +function lastMin(a: Float64Array): string { + let min = Infinity; + let at = -1; + let i = 0; + while (i < a.length) { + if (a[i]! <= min) { + min = a[i]!; + at = i; + } + i++; + } + return `${min}@${at}`; +} + +function bothEnds(values: number[]): string { + let lo = Infinity; + let hi = -Infinity; + for (const v of values) { + if (lo > v) lo = v; + if (v >= hi) hi = v; + } + return `${lo}..${hi}`; +} + +function firstDrop(a: Float64Array): number { + let floor = a[0]!; + let i = 0; + do { + if (a[i]! < floor) floor = a[i]!; + i++; + } while (i < a.length); + return floor; +} + +function callbackPeak(a: Float64Array): string { + let max = -Infinity; + let at = -1; + a.forEach((v, i) => { + if (v > max) { + max = v; + at = i; + } + }); + return `${max}@${at}`; +} + +function arrayCallbackMin(values: number[]): number { + let min = Infinity; + values.forEach((v) => { + if (v < min) min = v; + }); + return min; +} + +function rowMaxima(a: Float64Array, width: number): string { + const out: number[] = []; + for (let row = 0; row * width < a.length; row++) { + let best = -Infinity; + for (let j = row * width; j < Math.min(a.length, (row + 1) * width); j++) { + if (a[j]! > best) best = a[j]!; + } + out.push(best); + } + return out.join(","); +} + +function longestNonNegativeRun(a: Float64Array): number { + let longest = 0; + let run = 0; + for (let i = 0; i < a.length; i++) { + if (a[i]! >= 0) { + run++; + if (run > longest) longest = run; + } else run = 0; + } + return longest; +} + +function clampSum(a: Float64Array, hi: number): number { + let s = 0; + for (let i = 0; i < a.length; i++) { + let v = a[i]!; + if (hi < v) v = hi; + s += v; + } + return s; +} + +function saturate(a: Float64Array, hi: number): number { + let v = 0; + for (let i = 0; i < a.length; i++) { + v += a[i]!; + if (hi < v) v = hi; + } + return v; +} + +for (const [name, a] of shapes(40)) { + const values = Array.from(a); + console.log( + name, + argmax(a), + lastMin(a), + bothEnds(values), + firstDrop(a), + callbackPeak(a), + arrayCallbackMin(values), + ); + console.log( + " rows", + rowMaxima(a, 7), + "run", + longestNonNegativeRun(a), + "clamp", + clampSum(a, 5), + "saturate", + saturate(a, 30), + ); +} + +const signed = new Float64Array([0, -0, 0, -0]); +let top = -Infinity; +for (let i = 0; i < signed.length; i++) if (signed[i]! >= top) top = signed[i]!; +console.log("signed zero", Object.is(top, -0), argmax(signed), lastMin(signed)); +console.log("empty", argmax(new Float64Array(0)), bothEnds([]), arrayCallbackMin([])); diff --git a/tests/corpus/7302-string-scan-loop-versions.ts b/tests/corpus/7302-string-scan-loop-versions.ts new file mode 100644 index 000000000..3408e048e --- /dev/null +++ b/tests/corpus/7302-string-scan-loop-versions.ts @@ -0,0 +1,192 @@ +// charCodeAt scans in counted for loops over ASCII strings, Latin-1 and BMP +// text, surrogate pairs and lone surrogates, empty strings, neighbors at +// i - 1 and i + 1, loops that stop early or read past the end, strings built +// by concatenation and slicing, a scan of a slice local, two strings read at +// one index, a string rebound inside or after the loop, nested scans, a +// module-level scan, and while loops over a counter below the length: word +// and identifier scans, peeks past the counter, a counter from a field, and +// reads at or past the end. + +function spaces(s: string): number { + let n = 0; + for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) === 32) n++; + return n; +} + +function hash(s: string): number { + let h = 0; + for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0; + return h; +} + +function rises(s: string): number { + let n = 0; + for (let i = 1; i < s.length; i++) if (s.charCodeAt(i) > s.charCodeAt(i - 1)) n++; + return n; +} + +function peaks(s: string): number { + let n = 0; + for (let i = 1; i < s.length - 1; i++) { + const c = s.charCodeAt(i); + if (c > s.charCodeAt(i - 1) && c > s.charCodeAt(i + 1)) n++; + } + return n; +} + +function firstDigit(s: string): number { + for (let i = 0; i < s.length; i++) { + const c = s.charCodeAt(i); + if (c >= 48 && c <= 57) return i; + } + return -1; +} + +function pastEnd(s: string): string { + let sum = 0; + for (let i = 0; i <= s.length; i++) sum += s.charCodeAt(i); + return String(sum); +} + +function sameAt(a: string, b: string): number { + let n = 0; + const end = Math.min(a.length, b.length); + for (let i = 0; i < end; i++) if (a.charCodeAt(i) === b.charCodeAt(i)) n++; + return n; +} + +function stepped(s: string): number { + let h = 7; + for (let i = s.length - 1; i >= 0; i -= 2) h = (h * 33) ^ s.charCodeAt(i); + return h; +} + +function slicedScan(s: string, from: number, to: number): number { + const piece = s.slice(from, to); + let n = piece.length; + for (let i = 0; i < piece.length; i++) n += piece.charCodeAt(i); + return n; +} + +function rebound(s: string): number { + let n = 0; + for (let i = 0; i < s.length; i++) { + n += s.charCodeAt(i); + if (i === 2) s = s.toUpperCase(); + } + return n; +} + +function words(s: string): number { + let pos = 0, + n = 0; + const end = s.length; + while (pos < end) { + while (pos < end && s.charCodeAt(pos) === 32) pos++; + if (pos >= end) break; + n++; + while (pos < end && s.charCodeAt(pos) !== 32) pos++; + } + return n; +} + +function identifiers(s: string): number { + let pos = 0, + total = 0; + while (pos < s.length) { + const c = s.charCodeAt(pos); + if (c >= 97 && c <= 122) { + const start = pos; + while (pos < s.length && s.charCodeAt(pos) >= 97 && s.charCodeAt(pos) <= 122) pos++; + total += pos - start; + } else pos = (pos + 1) | 0; + } + return total; +} + +function pairs(s: string): number { + let pos = 0, + n = 0; + while (pos < s.length) { + if (s.charCodeAt(pos) === s.charCodeAt(pos + 1)) n++; + pos += 1; + } + return n; +} + +function throughEnd(s: string): string { + let pos = 0, + sum = 0; + while (pos <= s.length) { + sum += s.charCodeAt(pos); + pos++; + } + return String(sum); +} + +class Cursor { + pos = 0; + src: string; + constructor(src: string) { + this.src = src; + } + skip(): number { + const src = this.src; + let pos = this.pos; + while (pos < src.length && src.charCodeAt(pos) !== 44) pos++; + this.pos = pos + 1; + return pos; + } +} + +function fields(s: string): string { + const cursor = new Cursor(s); + const ends: number[] = []; + while (cursor.pos < s.length) ends.push(cursor.skip()); + return ends.join(","); +} + +const samples = [ + "the quick brown fox jumps over the lazy dog", + "", + " ", + "abc123 def", + "café au lait", + "日本語のテキスト と 空白", + "emoji 😀 and 🎉 here", + "lone \uD800 surrogate \uDFFF end", + "zigzag azbycxdw", +]; +for (const s of samples) { + console.log( + JSON.stringify(s), + spaces(s), + hash(s), + rises(s), + peaks(s), + firstDigit(s), + pastEnd(s), + stepped(s), + rebound(s), + slicedScan(s, 2, 9), + ); + console.log(" ", words(s), identifiers(s), pairs(s), throughEnd(s), fields(s)); +} + +let built = ""; +for (let k = 0; k < 40; k++) built += String.fromCharCode(65 + (k % 26)) + (k % 3 === 0 ? " " : ""); +const sliced = built.slice(5, 30); +console.log("built", spaces(built), hash(built), rises(sliced), peaks(sliced), sameAt(built, sliced)); +console.log("pairs", sameAt("hello world", "help, world!"), sameAt("abc", ""), sameAt("é", "e")); + +let total = 0; +for (const s of samples) { + for (let i = 0; i < s.length; i++) total = (total + s.charCodeAt(i) * (i + 1)) | 0; +} +console.log("nested", total); + +let text = "module level scan, with commas, and spaces"; +let commas = 0; +for (let i = 0; i < text.length; i++) if (text.charCodeAt(i) === 44) commas++; +text = "rebound after the loop"; +console.log("module", commas, text.length, spaces(text)); diff --git a/tests/corpus/7303-string-char-loop-versions.ts b/tests/corpus/7303-string-char-loop-versions.ts new file mode 100644 index 000000000..f6c21fc59 --- /dev/null +++ b/tests/corpus/7303-string-char-loop-versions.ts @@ -0,0 +1,184 @@ +// s[i] and s.charAt(i) in counted for loops: equality and switch tests with +// one-unit literals, character locals compared with ranges of one-unit +// literals on either side, characters used as strings (pushed, concatenated, +// map keys, compared with each other), reads past the end, fractional, +// negative and -0 indices, ASCII, Latin-1, BMP, surrogate pairs and lone +// surrogates, empty strings, a character local with other uses, a local +// compared with a longer literal, a string rebound in the loop and a +// module-level loop. + +function spacesIndex(s: string): number { + let n = 0; + for (let i = 0; i < s.length; i++) if (s[i] === " ") n++; + return n; +} + +function nonSpaces(s: string): number { + let n = 0; + for (let i = 0; i < s.length; i++) if (s.charAt(i) !== " ") n++; + return n; +} + +function kinds(s: string): string { + let letters = 0, + digits = 0, + upper = 0, + other = 0; + for (let i = 0; i < s.length; i++) { + const c = s[i]; + if (c >= "a" && c <= "z") letters++; + else if ("0" <= c && "9" >= c) digits++; + else if (c > "@" && c < "[") upper++; + else other++; + } + return `${letters}/${digits}/${upper}/${other}`; +} + +function punctuation(s: string): number { + let n = 0; + for (let i = 0; i < s.length; i++) { + const c = s.charAt(i); + switch (c) { + case ".": + case ",": + n += 1; + break; + case "!": + n += 10; + break; + case "é": + n += 100; + break; + default: + if (c === "?") n += 1000; + } + } + return n; +} + +function switchIndex(s: string): number { + let n = 0; + for (let i = 0; i < s.length; i++) { + switch (s[i]) { + case "a": + n++; + break; + case "b": + n += 2; + break; + } + } + return n; +} + +function pastEnd(s: string): string { + let below = 0, + atMost = 0, + above = 0, + atLeast = 0; + for (let i = 0; i <= s.length + 1; i++) { + const c = s.charAt(i); + if (c < "a") below++; + if (c <= "z") atMost++; + if (c > " ") above++; + if (c >= "\0") atLeast++; + } + return `${below} ${atMost} ${above} ${atLeast}`; +} + +function oddIndices(s: string): string { + const out: string[] = []; + for (let i = -2; i < 3; i++) out.push(JSON.stringify(s.charAt(i + 0.5))); + out.push(JSON.stringify(s.charAt(-0)), JSON.stringify(s.charAt(NaN))); + const c = s.charAt(-1); + out.push(String(c < "a"), String(c >= "\0")); + return out.join(","); +} + +function chars(s: string): string { + const out: string[] = []; + for (let i = 0; i < s.length; i++) out.push(s.charAt(i)); + let reversed = ""; + for (let i = s.length - 1; i >= 0; i--) reversed += s[i]; + return `${out.join("|")} ${reversed}`; +} + +function counts(s: string): string { + const seen = new Map(); + for (let i = 0; i < s.length; i++) { + const c = s[i]; + seen.set(c, (seen.get(c) ?? 0) + 1); + } + return [...seen].map(([c, n]) => `${c}${n}`).join(""); +} + +function sameAt(a: string, b: string): number { + let n = 0; + const end = Math.min(a.length, b.length); + for (let i = 0; i < end; i++) if (a[i] === b[i]) n++; + return n; +} + +function words(s: string): string { + let n = 0; + let last = ""; + for (let i = 0; i < s.length; i++) { + const c = s[i]; + if (c === " ") n++; + last = c; + } + return `${n}:${last}`; +} + +function longer(s: string): number { + let n = 0; + for (let i = 0; i < s.length; i++) { + const c = s.charAt(i); + if (c === "ab" || c < "b") n++; + } + return n; +} + +function rebound(s: string): number { + let n = 0; + for (let i = 0; i < s.length; i++) { + if (s[i] === "x") n++; + if (i === 1) s = s + "xx"; + } + return n; +} + +const samples = [ + "the quick brown fox, jumps over the lazy dog!", + "", + " ", + "abc123 DEF? xyz", + "café, au lait.", + "日本語のテキスト と 空白", + "emoji 😀 and 🎉 here!", + "lone \uD800 surrogate \uDFFF end", + "aabbab ba", +]; +for (const s of samples) { + console.log( + JSON.stringify(s), + spacesIndex(s), + nonSpaces(s), + kinds(s), + punctuation(s), + switchIndex(s), + pastEnd(s), + oddIndices(s), + ); + console.log(" ", chars(s), counts(s), words(s), longer(s), rebound(s)); +} + +let built = ""; +for (let k = 0; k < 40; k++) built += String.fromCharCode(97 + (k % 26)) + (k % 4 === 0 ? " " : ""); +console.log("built", spacesIndex(built), kinds(built), sameAt(built, built.slice(3)), counts(built)); +console.log("pairs", sameAt("hello world", "help, world!"), sameAt("abc", ""), sameAt("é", "e")); + +const text = "module level: scan, with commas, and spaces"; +let commas = 0; +for (let i = 0; i < text.length; i++) if (text[i] === ",") commas++; +console.log("module", commas); diff --git a/tests/corpus/7322-union-scalar-equality.ts b/tests/corpus/7322-union-scalar-equality.ts new file mode 100644 index 000000000..82e0564fc --- /dev/null +++ b/tests/corpus/7322-union-scalar-equality.ts @@ -0,0 +1,68 @@ +// Strict equality and inequality between a `number | undefined` or +// `boolean | undefined` value and a number or boolean: element reads of +// holes, past the end and of present undefined, NaN, -0 and +0, and +// Object.is, in branches, loops and expressions. + +function zeros(a: number[], at: number[]): string { + const hits: string[] = []; + for (const i of at) { + const v = a[i]; + hits.push(`${i}:${v === 0 ? "eq" : "ne"}${v !== 0 ? "!" : ""}${0 === v ? "r" : ""}`); + } + return hits.join(" "); +} + +const holey: number[] = [0, -0, 1]; +holey[5] = 0; +console.log(zeros(holey, [0, 1, 2, 3, 4, 5, 6, -1])); +console.log(zeros([NaN, 0.5, -1, 0], [0, 1, 2, 3])); + +function nanAndZeros(a: (number | undefined)[]): string { + return a + .map((v) => `${v === -0}${v === 0}${Object.is(v, -0)}${Object.is(v, 0)}${Object.is(v, NaN)}`) + .join(" "); +} +console.log(nanAndZeros([0, -0, NaN, undefined, 1])); + +function flags(a: (boolean | undefined)[]): string { + let t = 0; + let f = 0; + let notTrue = 0; + for (let i = 0; i < a.length + 2; i++) { + if (a[i] === true) t++; + if (a[i] === false) f++; + if (a[i] !== true) notTrue++; + } + return `${t} ${f} ${notTrue}`; +} +const bools: (boolean | undefined)[] = [true, false, undefined, true]; +bools[7] = false; +console.log(flags(bools)); + +// A permutation walk that stops at a zero element, as the flip loops of a +// counting benchmark do. +function walk(p: number[], start: number): number { + let steps = 0; + let at = start; + while (p[at] !== 0 && steps < 20) { + at = p[at]!; + steps++; + } + return steps; +} +console.log(walk([0, 3, 1, 2], 1), walk([1, 2, 3, 0], 0), walk([2, 2, 2], 0)); + +class Slot { + value: number | undefined = undefined; + on: boolean | undefined = undefined; +} +const slots = [new Slot(), new Slot(), new Slot()]; +slots[1]!.value = 0; +slots[1]!.on = true; +slots[2]!.value = -0; +slots[2]!.on = false; +console.log( + slots + .map((s) => `${s.value === 0}${s.value !== 0}${s.on === true}${s.on === false}${s.on !== false}`) + .join(" "), +); diff --git a/tests/corpus/7323-loop-bound-parameter-versions.ts b/tests/corpus/7323-loop-bound-parameter-versions.ts new file mode 100644 index 000000000..cdbb8cfc4 --- /dev/null +++ b/tests/corpus/7323-loop-bound-parameter-versions.ts @@ -0,0 +1,88 @@ +// Functions whose number parameters bound their loops, called with values +// no caller proves to be int32s: integers, fractions, NaN, -0, infinities, +// and integers past the int32 range. Every call prints Node's result, +// including -0 read back through 1 / n, a reassigned parameter, a thrown +// error, a void result and a function passed as a value. + +function show(label: string, value: number): void { + console.log(label, Object.is(value, -0) ? "-0" : String(value)); +} + +function sumTo(n: number): number { + let s = 0; + for (let i = 0; i < n; i++) s += i; + return s; +} + +function fill(n: number, out: number[]): void { + for (let i = 0; i < n; i++) out.push(i * 2); +} + +function sign(n: number): number { + let steps = 0; + for (let i = 0; i < n && steps < 4; i++) steps++; + return 1 / n + steps; +} + +function countdown(n: number, step: number): number { + let k = 0; + while (n > 0) { + n -= step; + k++; + if (k > 100) break; + } + return k * 1000 + n; +} + +function checked(n: number, limit: number): number { + let s = 0; + let over = -1; + for (let i = 0; i <= n; i++) { + if (i > limit && over < 0) over = i; + s += i; + } + if (over >= 0) throw new RangeError(`past ${limit} at ${over}`); + return s; +} + +function fillTo(n: number, out: number[]): number { + for (let i = 0; i < n; i++) out[i] = i * 3; + return out.length; +} + +function window2(a: number[], from: number, to: number): string { + const parts: string[] = []; + for (let i = from; i < to; i++) parts.push(String(a[i])); + return parts.join(","); +} + +// Values arrive as strings and JSON, so no caller proves them int32s. +const inputs: number[] = JSON.parse("[0, 1, 5, 7.5, -3, -0, 2147483647, 2147483648, -2147483649, 1e300]"); +inputs.push(NaN, Infinity, -Infinity, Number("12"), Number.parseFloat("3.25")); + +for (const n of inputs) { + const small = Math.abs(n) > 100 ? (n > 0 ? 3 : -3) : n; + show(`sumTo ${n}`, sumTo(small)); + show(`sign ${n}`, sign(n)); + show(`countdown ${n}`, countdown(small, 2)); + const out: number[] = []; + fill(small, out); + const dense: number[] = []; + const length = fillTo(small, dense); + console.log(`fill ${n}`, out.join(" "), length, dense.join(" ")); +} + +for (const n of [3, 4.5, 6]) { + try { + show(`checked ${n}`, checked(n, 4)); + } catch (e) { + console.log(`checked ${n} threw`, (e as Error).message); + } +} + +const data = [10, 20, 30, 40, 50]; +for (const [from, to] of JSON.parse("[[0, 2], [1.5, 4], [-1, 1], [3, 9], [2, 2]]") as [number, number][]) + console.log(`window ${from}..${to}`, window2(data, from, to)); + +const fns: ((n: number) => number)[] = [sumTo, (n) => sumTo(n) * 2]; +console.log(fns.map((f) => f(Number("4"))).join(" "), [1, 2.5, 3].map(sumTo).join(" ")); diff --git a/tests/corpus/7324-loop-bound-parameter-window-sums.ts b/tests/corpus/7324-loop-bound-parameter-window-sums.ts new file mode 100644 index 000000000..094e9848c --- /dev/null +++ b/tests/corpus/7324-loop-bound-parameter-window-sums.ts @@ -0,0 +1,33 @@ +// Sliding-window sums whose length and width are number parameters read +// from JSON, so no caller proves them int32s: int32 sizes run the +// function's int32 version, and a fractional width, a length past the +// int32 range, -0 and NaN run its ordinary body. + +function windowSums(n: number, width: number, values: number[], out: number[]): number { + let total = 0; + for (let i = 0; i + width <= n; i++) { + let s = 0; + for (let k = 0; k < width; k++) s += values[i + k]!; + out[i] = s; + total += s; + } + return total; +} + +const values: number[] = []; +for (let i = 0; i < 4000; i++) values.push((i * 37) % 101); + +const runs = JSON.parse( + "[[4000, 64, 3], [4000, 1, 2], [100, 2.5, 2], [10, 0, 1], [-0, 3, 1], [4000, 3000000000, 1], [3000000000, 4, 1], [12, 20, 1]]", +) as [number, number, number][]; +runs.push([NaN, 2, 1], [Number("250"), Number("8"), 2]); +for (const [n, width, rounds] of runs) { + const out: number[] = []; + const length = Math.min(Number.isFinite(n) ? n : 0, 4000); + for (let i = 0; i < length; i++) out.push(0); + let total = 0; + // A length past the values reads holes: keep that run within them. + const limit = n > 4000 ? 4000 : n; + for (let r = 0; r < rounds; r++) total += windowSums(limit, width, values, out); + console.log(`${n} ${width} ${rounds}:`, total, out.length, out.slice(0, 3).join(",")); +} diff --git a/tests/corpus/7325-array-index-products-near-2-53.ts b/tests/corpus/7325-array-index-products-near-2-53.ts new file mode 100644 index 000000000..41e60b09d --- /dev/null +++ b/tests/corpus/7325-array-index-products-near-2-53.ts @@ -0,0 +1,82 @@ +// Element reads and writes indexed by products of int32 values plus or +// minus int32 terms. The products can pass the int32 range and 2^53, where +// a double product rounds: an index that lands in an array's range reads +// and writes that element, and every other one (negative, past the length, +// rounded past 2^53) keeps the property key Node computes from the double. + +function show(label: string, value: unknown): void { + console.log(label, Object.is(value, -0) ? "-0" : String(value)); +} + +function read(a: number[], x: number, y: number, k: number): number | undefined { + return a[x * y + k]; +} + +function readBack(a: number[], x: number, y: number, k: number): number | undefined { + return a[x * y - k]; +} + +function write(a: number[], x: number, y: number, k: number, v: number): void { + a[x * y + k] = v; +} + +function rows(n: number, cols: number, a: number[]): number { + let s = 0; + for (let i = 0; i < n; i++) for (let j = 0; j < cols; j++) s += a[i * cols + j]! * (j + 1); + return s; +} + +const a: number[] = []; +for (let i = 0; i < 64; i++) a.push(i * 10); + +const big = 2147483647; +const half = 65536; + +// In range. +show("3*5+2", read(a, 3 | 0, 5 | 0, 2 | 0)); +show("7*-1+9", read(a, 7 | 0, -1 | 0, 9 | 0)); +show("-0 index", read(a, -1 | 0, 0 | 0, 0 | 0)); +// The product passes the int32 range; the difference lands in range. +show("2^16*2^15-2^31+12", readBack(a, half | 0, 32768 | 0, (-2147483648 * -1 - 12) | 0)); +show("(2^31-1)*1-(2^31-1)+5", read(a, big | 0, 1 | 0, -2147483642 | 0)); +show("2^16*2^16-big+20", readBack(a, half | 0, half | 0, (big - 20) | 0)); +// Out of range or negative: holes and named keys. +show("past end", read(a, 8 | 0, 8 | 0, 1 | 0)); +show("negative", read(a, -3 | 0, 5 | 0, 0 | 0)); +show("2^31*2^31", read(a, -2147483648 | 0, -2147483648 | 0, 7 | 0)); + +// Products around 2^53, where the double product rounds. +const keys: number[] = []; +const x = 134217727; // 2^27 - 1 +const y = 67108865; // 2^26 + 1: x * y = 2^53 + 2^26 - 1 +const near: [number, number, number][] = [ + [x, y, 0], + [x, y, 1], + [x, y, -67108864], + [x, y, -67108863], + [94906265, 94906265, 0], + [94906267, 94906267, -3], + [-2147483648, 2147483647, 2147483647], +]; +for (const [p, q, k] of near) { + const sparse: number[] = []; + write(sparse, p | 0, q | 0, k | 0, 1); + // The key the double computes; a local holds only the double. + const key: number = p * q + k; + keys.push(key); + show(`write ${p}*${q}+${k}`, `${sparse[key]} ${sparse[key + 1]} ${sparse[key - 1]} ${sparse.length}`); + show(`read ${p}*${q}+${k}`, read(sparse, p | 0, q | 0, k | 0)); +} +show("keys", keys.join(",")); + +// A difference of two products above 2^53 keeps the double's rounding. +function cancel(sparse: number[], p: number, q: number, r: number, s: number): number | undefined { + return sparse[p * q - r * s]; +} +const cancelled: number[] = [7, 8, 9]; +show("cancel", cancel(cancelled, x | 0, y | 0, x | 0, y | 0)); +show("cancel+1", cancel(cancelled, x | 0, y | 0, (9007199254740992 / 67108864) | 0, 67108864 | 0)); + +// Row-major sums with a width from JSON. +const dims = JSON.parse("[[8, 8], [4, 16], [16, 4], [3, 7]]") as [number, number][]; +for (const [n, cols] of dims) show(`rows ${n}x${cols}`, rows(n | 0, cols | 0, a)); diff --git a/tests/corpus/7326-int32-element-arrays.ts b/tests/corpus/7326-int32-element-arrays.ts new file mode 100644 index 000000000..f5dc0d787 --- /dev/null +++ b/tests/corpus/7326-int32-element-arrays.ts @@ -0,0 +1,165 @@ +// Local number arrays whose every stored element is an int32: reads, writes, +// holes, present undefined, out-of-range and noncanonical indexes, growth, +// pop, the fill form, reassignment, and arrays that do not qualify. + +function compose(n: number, rounds: number): number { + const p: number[] = []; + const q: number[] = []; + for (let i = 0; i < n; i++) p.push((i * 7919 + 3) & (n - 1)); + for (let i = 0; i < n; i++) q.push(i); + let check = 0; + for (let r = 0; r < rounds; r++) { + for (let i = 0; i < n; i++) q[i] = p[q[i]!]!; + check = (check + q[r % n]!) | 0; + } + return check; +} + +function insertionSort(n: number): string { + const a: number[] = []; + let seed = 12345; + for (let i = 0; i < n; i++) { + seed = (seed * 1103515245 + 12345) & 0x7fffffff; + a.push((seed % 2001) - 1000); + } + for (let i = 1; i < n; i++) { + for (let j = i; j > 0 && a[j - 1]! > a[j]!; j--) { + const t = a[j]!; + a[j] = a[j - 1]!; + a[j - 1] = t; + } + } + let sorted = true; + for (let i = 1; i < n; i++) if (a[i - 1]! > a[i]!) sorted = false; + return `${sorted} ${a[0]} ${a[n >> 1]} ${a[n - 1]} ${a.length}`; +} + +function reads(): string[] { + const a = [7, -3, 2147483647, -2147483648, 0]; + const out: string[] = []; + out.push(`${a[0]} ${a[1]} ${a[2]} ${a[3]} ${a[4]} ${a[5]}`); + out.push(`${a[2]! + 1} ${a[3]! - 1} ${a[1]! / 2} ${a[2]! * 2}`); + const idx = [0, 4, 5, -1, 1.5, NaN, -0, 2 ** 32, Infinity]; + for (const i of idx) out.push(`${i}: ${a[i]} ${a[i] === undefined} ${i in a}`); + out.push(`len ${a.length}`); + return out; +} + +function holes(): string[] { + const a = [1, 2, 3]; + const out: string[] = []; + a[6] = 9; + out.push(`len ${a.length} ${a[3]} ${a[6]} ${4 in a} ${6 in a}`); + a[4] = -5; + let sum = 0, + present = 0; + for (let i = 0; i < a.length; i++) { + if (a[i] !== undefined) { + sum += a[i]!; + present++; + } + } + out.push(`sum ${sum} present ${present}`); + // A hole read through a non-null assertion stores a present undefined. + const q = [10, 20, 30]; + q[1] = a[3]!; + out.push(`q ${q[0]} ${q[1]} ${q[2]} ${1 in q} ${q.length}`); + q[1] = 21; + out.push(`q ${q[1]} ${(q[0]! + q[1]!) | 0}`); + return out; +} + +function sparse(): string[] { + const a = [1, 2]; + const out: string[] = []; + a[100000] = 42; + a[3] = 4; + out.push(`len ${a.length} ${a[100000]} ${a[3]} ${a[2]} ${a[99999]}`); + a[100000] = -42; + out.push(`${a[100000]} ${a[100001]}`); + return out; +} + +function growth(n: number): string[] { + const a: number[] = []; + const out: string[] = []; + for (let i = 0; i < n; i++) a.push((i * i) | 0); + out.push(`len ${a.length} ${a[0]} ${a[n - 1]} ${a[n]}`); + let sum = 0; + while (a.length > n / 2) sum = (sum + a.pop()!) | 0; + out.push(`popped ${sum} len ${a.length} last ${a[a.length - 1]}`); + for (let i = 0; i < 3; i++) a.push(i - 3); + out.push(`len ${a.length} ${a[a.length - 3]} ${a[a.length - 1]}`); + while (a.length > 0) a.pop(); + out.push(`empty ${a.length} ${a.pop()} ${a[0]}`); + a.push(5); + out.push(`again ${a.length} ${a[0]}`); + return out; +} + +function filled(n: number): string[] { + const counts = new Array(n).fill(0) as number[]; + const marks: number[] = new Array(n).fill(-1); + for (let i = 0; i < 3 * n; i++) { + const k = ((i * 37) % n) | 0; + counts[k] = (i * 3 - 7) | 0; + marks[(k + 1) % n] = k; + } + let a = 0, + b = 0; + for (let i = 0; i < n; i++) { + a = (a + counts[i]!) | 0; + b = (b + marks[i]!) | 0; + } + return [`fill ${counts.length} ${a} ${b} ${counts[n]} ${marks[0]}`]; +} + +function reassigned(rounds: number): string { + let a = [1, 2, 3]; + let total = 0; + for (let r = 0; r < rounds; r++) { + if (r % 3 === 0) a = [r, r + 1]; + else a.push(r); + total = (total + a[a.length - 1]! + a.length) | 0; + } + return `reassigned ${total} ${a.length} ${a[0]}`; +} + +// These arrays do not qualify; they print the same values as before. +function disqualified(): string[] { + const out: string[] = []; + const negZero = [0, -0]; + out.push(`${Object.is(negZero[1], -0)} ${1 / negZero[1]!}`); + const fraction = [1, 2]; + fraction[1] = 2.5; + out.push(`${fraction[1]}`); + const big = [1]; + big[0] = big[0]! + 2147483647; + out.push(`${big[0]}`); + const joined = [3, 1, 2]; + joined.sort((x, y) => x - y); + out.push(joined.join(",")); + const escaped = [4, 5]; + const alias = escaped; + alias[0] = 6; + out.push(`${escaped[0]} ${alias[1]}`); + const counter = [0]; + for (let i = 0; i < 10; i++) counter[0] = counter[0]! + 1; + out.push(`${counter[0]}`); + return out; +} + +const config = JSON.parse('{"n": 256, "rounds": 50, "sort": 300}') as { + n: number; + rounds: number; + sort: number; +}; +console.log(compose(config.n | 0, config.rounds | 0)); +console.log(insertionSort(config.sort | 0)); +for (const line of reads()) console.log(line); +for (const line of holes()) console.log(line); +for (const line of sparse()) console.log(line); +for (const line of growth(config.n | 0)) console.log(line); +for (const line of filled(17)) console.log(line); +console.log(reassigned(config.rounds | 0)); +for (const line of disqualified()) console.log(line); diff --git a/tests/corpus/7327-int32-array-widening.ts b/tests/corpus/7327-int32-array-widening.ts new file mode 100644 index 000000000..798292c11 --- /dev/null +++ b/tests/corpus/7327-int32-array-widening.ts @@ -0,0 +1,186 @@ +// Local number arrays with checked int32 stores: `new Array(n).fill(v)` +// arrays whose stores are exact integers but not proven int32s. A fill of +// at least 4096 elements creates int32 slots, and the first store past the +// int32 range widens the array to doubles in place; a shorter fill creates +// doubles. Each case runs at both sizes. Arrays with a store that has no +// exact integer range (a product of two int32s, a fraction, a value that may +// be -0), and arrays allocated as literals, keep doubles throughout. + +function counters(n: number, rounds: number): string { + const c: number[] = new Array(n).fill(0); + for (let r = 0; r < rounds; r++) + for (let i = 0; i < c.length; i++) c[i] = c[i]! + ((i & 3) + 1) * 1000000; + return `counters ${n} ${c[0]} ${c[1]} ${c[2]} ${c[3]} ${c[n - 1]} ${c.length}`; +} + +// The store at i = 45 passes 2^31 and widens the array partway through a +// loop; the remaining passes store doubles. +function denseLoop(n: number, k: number): string { + const a: number[] = new Array(n).fill(0); + for (let i = 0; i < a.length; i++) a[i] = i + k + 2147483600; + let s = 0; + for (let i = 0; i < a.length; i++) s = (s + a[i]!) % 1000003; + return `dense ${n} ${a[44]} ${a[45]} ${a[n - 1]} ${s}`; +} + +// Checked stores whose values stay int32: no widening. +function bfs(n: number): string { + const dist: number[] = new Array(n).fill(-1); + const queue: number[] = [0]; + dist[0] = 0; + for (let h = 0; h < queue.length; h++) { + const u = queue[h]!; + const edges = [(u + 1) & (n - 1), (u * 5 + 1) & (n - 1)]; + for (let j = 0; j < edges.length; j++) { + const v = edges[j]!; + if (dist[v]! < 0) { + dist[v] = dist[u]! + 1; + queue.push(v); + } + } + } + let far = 0, + sum = 0; + for (let i = 0; i < n; i++) { + if (dist[i]! > far) far = dist[i]!; + sum += dist[i]!; + } + return `bfs ${n} ${far} ${sum} ${queue.length}`; +} + +// A fill value that is not an int32 widens the new array before the fill. +function filledPastInt32(n: number, k: number): string { + const a: number[] = new Array(n).fill(k * 1000000000); + a[1] = a[1]! + 1; + const b: number[] = new Array(n).fill(k); + b[2] = b[2]! * 2; + return `filled ${n} ${a[0]} ${a[1]} ${a.length} ${b[0]} ${b[2]}`; +} + +// A chain of sums crosses 2^31 partway through; the second loop runs after +// the array widened. +function chain(n: number, step: number): string { + const a: number[] = new Array(n).fill(0); + for (let i = 1; i < n; i++) a[i] = a[i - 1]! + (i < 16 ? step : 1); + let s = 0; + for (let i = 0; i < n; i++) s = (s + (a[i]! % 1000003)) % 1000003; + for (let i = 1; i < n; i++) a[i] = a[i]! - a[i - 1]!; + return `chain ${n} ${step} ${a[1]} ${a[5]} ${a[6]} ${a[n - 1]} ${s}`; +} + +// Two arrays in one loop: one widens partway through, the other keeps +// int32 slots; then both are read in another loop. +function twoArrays(n: number, m: number, k: number): string { + const small: number[] = new Array(n).fill(1); + const big: number[] = new Array(m).fill(k); + const count = Math.min(n, m); + for (let i = 1; i < count; i++) { + small[i] = small[i - 1]! + 1; + big[i] = big[i - 1]! + (i < 8 ? 700000000 : 1); + } + let s = 0, + t = 0; + for (let i = 0; i < count; i++) { + s += small[i]!; + t = (t + (big[i]! % 997)) % 997; + } + return `two ${n} ${m} ${small[count - 1]} ${big[2]} ${big[3]} ${big[count - 1]} ${s} ${t}`; +} + +// Pushes that pass 2^31 widen the array before they append. +function pushes(n: number, k: number): string { + const a: number[] = new Array(n).fill(k); + for (let i = 0; i < 8; i++) a.push(a[a.length - 1]! + 900000000); + let s = 0; + for (let i = 0; i < a.length; i++) s = (s + (a[i]! % 10007)) % 10007; + return `pushes ${n} ${a.length} ${a[n]} ${a[n + 2]} ${a[n + 7]} ${s}`; +} + +function controlFlow(n: number, k: number): string { + const a: number[] = new Array(n).fill(k); + let stores = 0; + for (let i = 1; i < n; i++) { + if (i % 2 === 0) continue; + a[i] = a[i - 1]! + 1000000000 * (i % 4); + stores++; + if (a[i]! > 2500000000) break; + } + let i = 0; + while (i < n && a[i]! < 2000000000) i++; + let j = n; + do j--; + while (j > 0 && a[j] === k); + return `flow ${n} ${stores} ${a[1]} ${a[2]} ${a[3]} ${i} ${j}`; +} + +function readOnly(n: number, wide: boolean): string { + const a: number[] = new Array(n).fill(2); + if (wide) a[0] = a[0]! + 3000000000; + let s = 0, + present = 0; + for (let i = 0; i < n + 2; i++) { + s += a[i] ?? 0; + if (i in a) present++; + } + return `read ${n} ${wide} ${s} ${present}`; +} + +function holes(n: number): string { + // Holes, present undefined and sparse entries survive widening. + const a: number[] = new Array(n).fill(3); + a[n + 3] = a[1]! + 1; + a[n + 2] = a[n + 3]! - 1; + a[2 * n + 100000] = a[0]! + 1; + a[n + 1] = a[n + 2]! + 3000000000; + return `holes ${n} ${a.length} ${a[n]} ${n in a} ${a[n + 1]} ${a[n + 2]} ${a[2 * n + 100000]}`; +} + +// These arrays never take int32 slots: a fractional store, a product of +// two int32s, -0, an escape, and arrays allocated as literals. +function neverInt32(n: number, x: number, y: number): string[] { + const f: number[] = new Array(n).fill(1); + f[0] = f[0]! / x; + const m: number[] = new Array(n).fill(3); + m[1] = m[1]! * y; + const z: number[] = new Array(n).fill(4); + z[1] = z[1]! % 2; + z[2] = -z[0]! % 2; + const j: number[] = new Array(n).fill(3); + j[0] = j[0]! + 1; + const lit = [1, 2, 3]; + lit[0] = lit[0]! + 3000000000; + const pushed: number[] = []; + for (let i = 0; i < 4; i++) pushed.push(i * 1000000000); + pushed[0] = pushed[3]! + pushed[2]!; + return [ + `never ${f[0]} ${m[1]} ${z[1]} ${Object.is(z[2], -0)} ${j.slice(0, 2).join("|")}`, + `literals ${lit[0]} ${lit[2]} ${pushed[0]} ${pushed.length}`, + ]; +} + +const config = JSON.parse('{"small": 64, "large": 8192, "k": 3, "y": 2000000000}') as { + small: number; + large: number; + k: number; + y: number; +}; +const lines: string[] = []; +for (const n of [config.small | 0, config.large | 0]) { + lines.push(counters(n, 1000)); + lines.push(denseLoop(n, config.k | 0)); + lines.push(bfs(n)); + lines.push(filledPastInt32(n, config.k | 0)); + lines.push(chain(n, 400000000)); + lines.push(chain(n, 100000000)); + lines.push(twoArrays(n, n, config.k | 0)); + lines.push(pushes(n, config.k | 0)); + lines.push(controlFlow(n, config.k | 0)); + lines.push(readOnly(n, false)); + lines.push(readOnly(n, true)); + lines.push(holes(n)); +} +// One long array and one short: their kinds differ on entry to the loop. +lines.push(twoArrays(config.large | 0, config.small | 0, config.k | 0)); +lines.push(twoArrays(config.small | 0, config.large | 0, config.k | 0)); +lines.push(...neverInt32(config.small | 0, config.k, config.y | 0)); +for (const line of lines) console.log(line); diff --git a/tests/corpus/7362-globalthis-intersection-singleton.ts b/tests/corpus/7362-globalthis-intersection-singleton.ts new file mode 100644 index 000000000..4c6eb997d --- /dev/null +++ b/tests/corpus/7362-globalthis-intersection-singleton.ts @@ -0,0 +1,46 @@ +// The global-singleton spelling: `globalThis as typeof globalThis & { key?: T }` +// names the global object itself (it also carries the global's own +// WebAssembly member), so a value stored through one view survives the +// call and is visible through every other view. +class Repository { + private items = new Map(); + add(name: string): number { + this.items.set(name, this.items.size + 1); + return this.items.size; + } + get size(): number { + return this.items.size; + } +} + +let constructed = 0; +const repoKey = "__exampleRepo"; +type WithRepo = typeof globalThis & { [repoKey]?: Repository }; + +function repo(): Repository { + const g = globalThis as WithRepo; + g[repoKey] ??= (constructed++, new Repository()); + return g[repoKey]; +} + +repo().add("keyboard"); +repo().add("mouse"); +console.log(constructed, repo().size, repo() === repo()); + +// a named member through a second spelling of the same intersection +const counters = globalThis as typeof globalThis & { exampleHits?: number; exampleLabel?: string }; +counters.exampleHits = 1; +counters.exampleHits += 2; +counters.exampleLabel = "ready"; +console.log( + (globalThis as typeof globalThis & { exampleHits?: number }).exampleHits, + (globalThis as unknown as { exampleLabel?: string }).exampleLabel, +); + +// the view is the global object +console.log(counters.globalThis === globalThis, (globalThis as WithRepo) === globalThis); + +// many reads of the global object in a loop see the same object +let same = 0; +for (let i = 0; i < 1000; i++) if ((globalThis as WithRepo)[repoKey] === repo()) same++; +console.log(same, constructed); diff --git a/tests/corpus/7363-json-stringify-live-capsules.ts b/tests/corpus/7363-json-stringify-live-capsules.ts new file mode 100644 index 000000000..5522ede5f --- /dev/null +++ b/tests/corpus/7363-json-stringify-live-capsules.ts @@ -0,0 +1,73 @@ +// JSON.stringify of typed records and arrays reached through `unknown`: +// members are read from the native values in key order, toJSON receives the +// member key (an index string inside arrays), undefined members drop out of +// objects and become null in arrays, and the key list is fixed before any +// toJSON runs. +interface Product { + id: string; + name: string; + priceCents: number; + tags: string[]; + note?: string; +} + +function stringify(value: unknown, space?: number): string { + return JSON.stringify(value, undefined, space); +} + +const products: Product[] = [ + { id: "p1", name: "Keyboard", priceCents: 12900, tags: ["hardware", "input"] }, + { id: "p2", name: "Quote \"and\" slash \\", priceCents: 0.5, tags: [], note: "café ☕" }, +]; +console.log(stringify({ products, count: products.length })); +console.log(stringify(products, 2)); +const second: Product = products[1]!; +delete second.note; +console.log(stringify(second)); + +class Money { + cents: number; + constructor(cents: number) { + this.cents = cents; + } + toJSON(key: string): string { + return `${key}:${(this.cents / 100).toFixed(2)}`; + } +} +const prices: Money[] = [new Money(100), new Money(2550)]; +console.log(stringify(prices), stringify({ total: new Money(5) })); + +const numbers: number[] = [1, -0, NaN, Infinity, 2.5, 1e21]; +console.log(stringify(numbers)); +const maybe: (string | undefined)[] = ["a", undefined, "c"]; +console.log(stringify(maybe), stringify({ maybe })); +const nested: number[][] = [[1, 2], [], [3]]; +console.log(stringify(nested, 1)); + +// a toJSON that deletes a later optional field: the field was present when +// the key list was taken, so its (now undefined) value is dropped +interface Order { + first: { toJSON(): string }; + note?: string; +} +const order: Order = { first: { toJSON: () => "x" }, note: "kept?" }; +order.first = { + toJSON: () => { + delete order.note; + return "first"; + }, +}; +console.log(stringify(order)); + +// circular structures through capsules report the path +interface Node { + name: string; + children: Node[]; +} +const root: Node = { name: "root", children: [] }; +root.children.push({ name: "leaf", children: [root] }); +try { + stringify(root); +} catch (error) { + console.log((error as Error).message); +} diff --git a/tests/corpus/7364-new-target-late-instantiations.ts b/tests/corpus/7364-new-target-late-instantiations.ts new file mode 100644 index 000000000..3fa7ff94a --- /dev/null +++ b/tests/corpus/7364-new-target-late-instantiations.ts @@ -0,0 +1,71 @@ +// new.target in a constructor whose subclasses are generic classes +// instantiated only in bodies lowered later, and classes whose own static +// `name` field replaces their name. +class Plain { + readonly label: string; + readonly plain: boolean; + constructor() { + this.label = new.target.name; + this.plain = new.target === Plain; + } +} +class Sub extends Plain { + readonly v: T; + constructor(v: T) { + super(); + this.v = v; + } +} + +class Box { + readonly label: string; + readonly exact: boolean; + readonly value: T; + constructor(value: T) { + this.value = value; + this.label = new.target.name; + this.exact = new.target === Leaf; + } +} +class Tagged extends Box {} +class Leaf extends Tagged {} + +class Named { + static name = "custom"; + readonly label: string; + constructor() { + this.label = new.target.name; + } +} +class Child extends Named {} +class Other extends Named { + static name = "other"; +} +class Family { + static name = "family"; + readonly label: string; + readonly v: T; + constructor(v: T) { + this.label = new.target.name; + this.v = v; + } +} + +function later(): void { + const n = new Sub(1); + const p = new Plain(); + console.log(n.label, new Sub("s").label, p.label, n.plain, p.plain); + const t = new Tagged(1); + const s = new Tagged("a"); + const b = new Box(2); + const l = new Leaf(3); + console.log(t.label, s.label, b.label, l.label, t.exact, l.exact, b.exact); + console.log(new Family(true).label); +} + +// The bases construct here first; their subclasses only in later(). +console.log(new Plain().label, new Box(false).label); +console.log(new Named().label, new Child().label, new Other().label); +Named.name = "changed"; +console.log(new Named().label, new Child().label, new Other().label); +later(); diff --git a/tests/corpus/7365-class-methods-untyped-computed-writes/main.ts b/tests/corpus/7365-class-methods-untyped-computed-writes/main.ts new file mode 100644 index 000000000..c5475b5d9 --- /dev/null +++ b/tests/corpus/7365-class-methods-untyped-computed-writes/main.ts @@ -0,0 +1,87 @@ +// Typed classes next to untyped computed writes. Calls through a typed +// receiver keep the method while its prototype slot holds the declared +// method, and observe a replacement made by untyped code (on the prototype +// or as an own property of one instance) exactly as Node does. +import { patch, tally, unpatch } from "./untyped.js"; + +class Repository { + private items = new Map(); + add(name: string, cents: number): number { + this.items.set(name, cents); + return this.items.size; + } + list(): string[] { + return [...this.items.keys()]; + } + total(): number { + let sum = 0; + for (const cents of this.items.values()) sum += cents; + return sum; + } +} + +const words: string[] = ["list", "add", "list", "total"]; +console.log(JSON.stringify(tally(words))); + +const repo = new Repository(); +repo.add("keyboard", 12900); +repo.add("mouse", 4900); +let sum = 0; +for (let i = 0; i < 1000; i++) sum += repo.total(); +console.log(repo.list().join(","), sum); + +// untyped code replaces the method on the prototype +const proto: unknown = Object.getPrototypeOf(repo); +patch(proto, "total", function (this: Repository): number { + return -1; +}); +console.log(repo.total(), new Repository().total()); +unpatch(proto, "total"); +try { + repo.total(); +} catch (error) { + console.log("after delete:", error instanceof TypeError); +} + +// an own replacement on one instance shadows the prototype for that instance only +const other = new Repository(); +other.add("cable", 900); +patch(other, "list", () => ["shadowed"]); +console.log(other.list().join(","), repo.list().join(",")); +unpatch(other, "list"); +console.log(other.list().join(",")); + +// restoring the original value through the prototype brings back the method +const saved: unknown = (proto as Record)["list"]; +patch(proto, "list", () => ["patched"]); +console.log(repo.list().join(",")); +patch(proto, "list", saved); +console.log(repo.list().join(",")); + +// a spread argument drains its iterator through the iterator's current `next` +class Bag { + sum(...values: number[]): number { + let total = 0; + for (const value of values) total += value; + return total; + } +} +class Countdown { + private left: number; + constructor(left: number) { + this.left = left; + } + next(): IteratorResult { + return this.left > 0 ? { done: false, value: this.left-- } : { done: true, value: undefined }; + } + [Symbol.iterator](): Countdown { + return this; + } +} +const bag = new Bag(); +console.log(bag.sum(...[1, 2, 3]), bag.sum(...new Countdown(3))); +const replaced = new Countdown(3); +patch(replaced, "next", function (): IteratorResult { + return { done: true, value: undefined }; +}); +console.log(bag.sum(...replaced)); diff --git a/tests/corpus/7365-class-methods-untyped-computed-writes/untyped.js b/tests/corpus/7365-class-methods-untyped-computed-writes/untyped.js new file mode 100644 index 000000000..3c0d20189 --- /dev/null +++ b/tests/corpus/7365-class-methods-untyped-computed-writes/untyped.js @@ -0,0 +1,15 @@ +// Untyped helpers with computed writes whose keys TypeScript cannot know. +// They put every class method name in play for the method census. +export function tally(words) { + const counts = {}; + for (const word of words) counts[word] = (counts[word] || 0) + 1; + return counts; +} + +export function patch(target, name, impl) { + target[name] = impl; +} + +export function unpatch(target, name) { + delete target[name]; +} diff --git a/tests/corpus/7366-typed-global-view-arrays.ts b/tests/corpus/7366-typed-global-view-arrays.ts new file mode 100644 index 000000000..e2d9b23ff --- /dev/null +++ b/tests/corpus/7366-typed-global-view-arrays.ts @@ -0,0 +1,48 @@ +// Arrays stored on the global object through a typed view are the same +// arrays every typed read returns: writes through one read (push, index +// assignment, sort) are visible through the next, as in Node. +type Store = typeof globalThis & { __exampleCache?: number[]; __exampleNames?: string[] }; + +function cache(): number[] { + const g = globalThis as Store; + g.__exampleCache ??= [1]; + return g.__exampleCache; +} + +cache().push(2); +cache().push(3); +cache()[0] = 10; +console.log(cache().length, cache().join(","), cache() === cache()); + +const view = globalThis as Store; +view.__exampleNames = ["b", "a"]; +const names = view.__exampleNames; +names.push("c"); +names.sort(); +console.log(view.__exampleNames.join(","), names === view.__exampleNames); + +// the same array seen through an untyped read +const raw = (globalThis as unknown as Record)["__exampleCache"]; +console.log(Array.isArray(raw), JSON.stringify(raw), raw === cache()); + +// a literal of another element type stays its own value +const counts = globalThis as typeof globalThis & { __exampleCounts?: number[][] }; +counts.__exampleCounts = [[1], [2, 3]]; +counts.__exampleCounts[1]!.push(4); +console.log(JSON.stringify(counts.__exampleCounts)); + +// a parenthesized or `satisfies`-wrapped literal is kept the same way +type Wrapped = typeof globalThis & { __exampleWrapped?: number[]; __exampleSatisfied?: number[] }; +function wrapped(): number[] { + const g = globalThis as Wrapped; + g.__exampleWrapped ??= ([5]); + return g.__exampleWrapped; +} +function satisfied(): number[] { + const g = globalThis as Wrapped; + g.__exampleSatisfied ||= [7] satisfies number[]; + return g.__exampleSatisfied; +} +wrapped().push(6); +satisfied().push(8); +console.log(wrapped().join(","), wrapped() === wrapped(), satisfied().join(","), satisfied() === satisfied()); diff --git a/tests/corpus/7380-iterable-values.ts b/tests/corpus/7380-iterable-values.ts new file mode 100644 index 000000000..0c4284285 --- /dev/null +++ b/tests/corpus/7380-iterable-values.ts @@ -0,0 +1,122 @@ +// Iterable values: an array, Set, Map or string stays the same object +// (multi-pass, same identity, same inspection), and a generator or a +// Map/Set/array iterator stays single-pass. for-of, spread, Array.from and +// the Set/Map constructors consume every source. + +function sum(xs: Iterable): number { + let s = 0; + for (const x of xs) s += x; + return s; +} + +function* nums(): Generator { + yield 10; + yield 20; +} + +const arr = [1, 2, 3]; +const scores = new Map([ + ["a", 4], + ["b", 5], +]); +console.log(sum(arr), sum(new Set([1, 1, 2])), sum(scores.values()), sum(nums()), sum(arr.values())); + +// An array stays the same multi-pass object. +const it: Iterable = arr; +console.log(sum(it), sum(it), it === arr, typeof it, it); + +// A generator stays single-pass. +const once: Iterable = nums(); +console.log(sum(once), sum(once), typeof once); + +console.log([...it], Array.from(nums() as Iterable), new Set(it).size); + +// A function returning either a collection iterator or a fresh array. +function pick(rows: Map, ids: string[] | null): Iterable { + if (ids === null) return rows.values(); + const out: number[] = []; + for (const id of ids) out.push(rows.get(id) ?? 0); + return out; +} +let total = 0; +for (const v of pick(scores, null)) total += v; +for (const v of pick(scores, ["b", "b", "z"])) total += v; +console.log(total); + +// Strings iterate by code point; Maps iterate [key, value] entries. +function upper(s: Iterable): string[] { + const out: string[] = []; + for (const w of s) out.push(w.toUpperCase()); + return out; +} +console.log(upper("héllo😀"), upper(["a", "b"]), upper(new Set(["x"]))); +function pairs(p: Iterable<[string, number]>): string { + let out = ""; + for (const [k, v] of p) out += `${k}=${v};`; + return out; +} +console.log(pairs(scores), pairs([["x", 1]]), pairs(scores.entries()), pairs(new Set<[string, number]>([["s", 9]]))); + +// Constructors and Object.fromEntries take any Iterable. +function toMap(p: Iterable<[string, number]>): Map { + return new Map(p); +} +function toSet(xs: Iterable): Set { + return new Set(xs); +} +console.log(toMap([["a", 1], ["b", 2]]), toMap(scores), Object.fromEntries(toMap(new Set<[string, number]>([["c", 3]])))); +console.log(toSet([3, 3, 4]), toSet(new Set([9])), toSet(nums())); + +// Fields, generator functions declared to return Iterable, nested Iterables. +class Holder { + source: Iterable; + constructor(source: Iterable) { + this.source = source; + } + joined(): string { + let out = ""; + for (const s of this.source) out += s; + return out; + } +} +const holder = new Holder(["a", "b"]); +console.log(holder.joined(), holder.joined()); +holder.source = new Set(["x", "y"]); +console.log(holder.joined(), holder); + +function* range(n: number): Iterable { + for (let i = 0; i < n; i++) yield i; +} +console.log([...range(4)], sum(range(5))); + +const grid: Iterable[] = [[1, 2], new Set([3]), range(3)]; +let cells = 0; +for (const row of grid) for (const c of row) cells += c; +console.log(cells, grid.length); + +// A checked cast of an iterator value. +const entries = new Map([["k", 1]]).entries() as Iterable<[string, number]>; +console.log(toMap(entries)); + +// Optional Iterables narrow to the Iterable once proven present. +function totalOf(xs?: Iterable): number { + if (xs === undefined) return -1; + let s = 0; + for (const x of xs) s += x; + return s; +} +function orEmpty(xs: Iterable | null): number[] { + return xs === null ? [] : [...xs]; +} +console.log(totalOf(), totalOf([1, 2]), totalOf(new Set([5])), orEmpty(null), orEmpty(range(2))); +const byName = new Map>([ + ["a", [1, 2]], + ["b", new Set([3])], +]); +for (const k of ["a", "b", "c"]) { + const got = byName.get(k); + if (got !== undefined) console.log(k, [...got]); + else console.log(k, "none"); +} +const top = byName.get("a"); +console.log(top !== undefined ? [...top] : "none", byName); diff --git a/tests/corpus/7381-iterable-forof-semantics.ts b/tests/corpus/7381-iterable-forof-semantics.ts new file mode 100644 index 000000000..10f1d1d2f --- /dev/null +++ b/tests/corpus/7381-iterable-forof-semantics.ts @@ -0,0 +1,122 @@ +// for-of over Iterable keeps the semantics of the held source: an array +// is walked live, a generator is closed on break, return and throw (its +// finally blocks run) but not when it is exhausted or throws itself, and +// labels, closures, await and yield work in the loop body. + +function* noisy(tag: string): Generator { + try { + yield 1; + yield 2; + yield 3; + } finally { + console.log("closed", tag); + } +} + +const early: Iterable = noisy("break"); +for (const v of early) { + if (v === 2) break; + console.log("v", v); +} + +function firstOver(src: Iterable, n: number): number | undefined { + for (const x of src) if (x > n) return x; + return undefined; +} +console.log(firstOver([1, 5, 9], 4), firstOver(noisy("return"), 1), firstOver(noisy("exhausted"), 9)); + +try { + for (const v of noisy("throw") as Iterable) { + if (v === 1) throw new Error("boom"); + } +} catch (e) { + console.log("caught", (e as Error).message); +} + +function* failing(): Generator { + try { + yield 1; + throw new RangeError("generator failed"); + } finally { + console.log("failing finally"); + } +} +try { + for (const v of failing() as Iterable) console.log("got", v); +} catch (e) { + console.log(String(e)); +} + +// The array is read live: elements pushed during the walk are visited. +const grow: number[] = [1, 2]; +let visits = 0; +for (const v of grow as Iterable) { + if (grow.length < 5) grow.push(v * 10); + visits++; +} +console.log(visits, grow); + +// Holes read undefined when T holds it. +function show(xs: Iterable): string { + const out: string[] = []; + for (const x of xs) out.push(String(x)); + return out.join(","); +} +console.log(show([1, , 3]), show([undefined, 2])); + +// Labels, closures over the per-iteration binding, try/finally, a class +// declared in the body, and an existing binding as the loop target. +function run(src: Iterable, tag: string): void { + const fns: (() => number)[] = []; + let acc = 0; + outer: for (const x of src) { + fns.push(() => x * 10); + try { + if (x === 2) continue outer; + class Local { + v = x; + twice(): number { + return this.v * 2; + } + } + acc += new Local().twice(); + const inner = (y: number): number => y + x; + acc += inner(1); + } finally { + acc += 100; + } + } + console.log(tag, acc, fns.map((f) => f()).join(",")); +} +function* three(): Generator { + yield 1; + yield 2; + yield 3; +} +run([1, 2, 3], "array"); +run(three(), "generator"); +run(new Set([1, 2, 3]), "set"); + +let product = 0; +outer: for (const a of [1, 2, 3] as Iterable) { + for (const b of new Set([10, 20]) as Iterable) { + if (b === 20) continue outer; + if (a === 3) break outer; + product += a * b; + } +} +let target = 0; +for (target of [7, 8] as Iterable) { +} +console.log(product, target); + +// await and yield inside the loop body. +async function asyncSum(src: Iterable): Promise { + let s = 0; + for (const x of src) s += await Promise.resolve(x); + return s; +} +function* doubled(src: Iterable): Generator { + for (const x of src) yield x * 2; +} +asyncSum([4, 5]).then((v) => console.log("async", v, [...doubled([1, 2])], [...doubled(new Set([7]))])); diff --git a/tests/corpus/7382-iterable-generic-repository.ts b/tests/corpus/7382-iterable-generic-repository.ts new file mode 100644 index 000000000..3e9e62742 --- /dev/null +++ b/tests/corpus/7382-iterable-generic-repository.ts @@ -0,0 +1,57 @@ +// A generic repository whose query candidates are an Iterable: the +// live row map's values() iterator when nothing is indexed, or an array of +// index hits. Methods returning Iterable keep the class compilable. + +interface Row { + readonly id: string; + readonly tag: string; + readonly n: number; +} + +class Repository { + private readonly rows = new Map(); + private readonly index = new Map(); + + private readonly key: (row: T) => string; + + constructor(key: (row: T) => string) { + this.key = key; + } + + insert(row: T): void { + this.rows.set(row.id, row); + const k = this.key(row); + const ids = this.index.get(k); + if (ids === undefined) this.index.set(k, [row.id]); + else ids.push(row.id); + } + + all(): Iterable { + return this.rows.values(); + } + + candidates(key: string | null): Iterable { + if (key === null) return this.rows.values(); + const out: T[] = []; + for (const id of this.index.get(key) ?? []) { + const row = this.rows.get(id); + if (row !== undefined) out.push(row); + } + return out; + } + + query(key: string | null, where: (row: T) => boolean): T[] { + const matches: T[] = []; + for (const row of this.candidates(key)) if (where(row)) matches.push(row); + return matches; + } +} + +const repo = new Repository((row) => row.tag); +for (let i = 0; i < 12; i++) repo.insert({ id: `r${i}`, tag: i % 3 === 0 ? "red" : "blue", n: i * i }); +console.log(repo.query(null, (r) => r.n > 50).map((r) => r.id).join(",")); +console.log(repo.query("red", (r) => r.n % 2 === 0).map((r) => r.id).join(",")); +console.log(repo.query("green", () => true).length, [...repo.all()].length); +let sum = 0; +for (const row of repo.candidates("blue")) sum += row.n; +console.log(sum, Array.from(repo.candidates(null)).slice(0, 2)); diff --git a/tests/corpus/7383-iterable-union-sources.ts b/tests/corpus/7383-iterable-union-sources.ts new file mode 100644 index 000000000..3c393b256 --- /dev/null +++ b/tests/corpus/7383-iterable-union-sources.ts @@ -0,0 +1,51 @@ +// Iterable unions without an array arm, and optional or nullable Iterable +// slots that take a generator whose return channel is not void. + +function* evens(limit: number): Generator { + for (let i = 0; i <= limit; i += 2) yield i; +} + +// A union of a Set and a generator is an iterable union with no array arm: +// for-of walks every arm through the generator loop. +function total(src: Set | Generator): number { + let sum = 0; + for (const v of src) sum += v; + return sum; +} +console.log(total(new Set([1, 2, 3])), total(evens(6))); + +const labelled = (src: Set | Generator): string[] => { + const out: string[] = []; + outer: for (const v of src) { + if (v > 4) break outer; + out.push(`#${v}`); + } + return out; +}; +console.log(labelled(new Set([1, 3, 5])).join(" "), labelled(evens(10)).join(" ")); + +// A generator that returns a value: its return channel is dropped when it +// flows into an Iterable, required, optional or nullable alike. +function* countdown(from: number): Generator { + for (let i = from; i > 0; i--) yield i; + return "liftoff"; +} + +function joined(xs: Iterable): string { + return [...xs].join(","); +} + +function maybeJoined(xs?: Iterable): string { + return xs === undefined ? "none" : [...xs].join(","); +} + +function nullableJoined(xs: Iterable | null): string { + if (xs === null) return "null"; + let out = ""; + for (const x of xs) out += `${x};`; + return out; +} + +console.log(joined(countdown(3))); +console.log(maybeJoined(countdown(2)), maybeJoined(), maybeJoined(undefined), maybeJoined([7, 8])); +console.log(nullableJoined(countdown(4)), nullableJoined(null), nullableJoined(new Set([9]))); diff --git a/tests/corpus/7400-record-keyof-union-read.ts b/tests/corpus/7400-record-keyof-union-read.ts new file mode 100644 index 000000000..4a62a5067 --- /dev/null +++ b/tests/corpus/7400-record-keyof-union-read.ts @@ -0,0 +1,41 @@ +// Keyed reads of records whose fields have different types, with a key +// typed as `keyof` the record: the read is the union of the field types. +interface Book { + readonly id: string; + readonly title: string; + readonly year: number; + readonly tags: readonly string[]; + readonly available: boolean; +} +type BookPatch = Partial>; + +function changedKeys(current: Book, patch: BookPatch): string[] { + const changed: string[] = []; + for (const key of Object.keys(patch) as (keyof BookPatch)[]) { + const before = current[key]; + const after = patch[key]; + if (after === undefined) continue; + if (Array.isArray(before) && Array.isArray(after)) { + if (before.join("|") !== after.join("|")) changed.push(key); + } else if (before !== after) changed.push(key); + } + return changed; +} + +function describe(book: Book, key: keyof Book): string { + const value = book[key]; + if (typeof value === "string") return `${key}: string ${value}`; + if (typeof value === "number") return `${key}: number ${value.toFixed(1)}`; + if (typeof value === "boolean") return `${key}: boolean ${value ? "yes" : "no"}`; + return `${key}: list of ${value.length} [${value.join(", ")}]`; +} + +const book: Book = { id: "b1", title: "Atlas", year: 1999, tags: ["maps", "travel"], available: true }; +console.log(changedKeys(book, { title: "Atlas", year: 2001 })); +console.log(changedKeys(book, { tags: ["maps", "travel"], available: false })); +console.log(changedKeys(book, { tags: ["maps"], title: "Atlas II", year: undefined })); +for (const key of ["id", "title", "year", "tags", "available"] as const) console.log(describe(book, key)); +const keys: (keyof Book)[] = ["year", "tags"]; +const values = keys.map((k) => book[k]); +console.log(values, values.length); +console.log(JSON.stringify(Object.fromEntries(keys.map((k) => [k, book[k]])))); diff --git a/tests/corpus/7401-any-spread-into-typed-literal.ts b/tests/corpus/7401-any-spread-into-typed-literal.ts new file mode 100644 index 000000000..c159a813c --- /dev/null +++ b/tests/corpus/7401-any-spread-into-typed-literal.ts @@ -0,0 +1,32 @@ +// An object literal spreading an untyped (`any`) source into a typed slot +// copies the source's runtime properties, then checks the copy into the +// slot's type like any other `any` value. +interface Filter { + readonly genre?: string; + readonly sort: string; + readonly offset: number; + readonly limit: number; +} + +function untyped(text: string): any { + return JSON.parse(text); +} + +function describe(filter: Filter): string { + return `${filter.genre ?? "(any genre)"} sorted by ${filter.sort}, ${filter.offset}..${filter.offset + filter.limit}`; +} + +function main(): void { + const query = untyped('{"genre":"poetry","sort":"title","offset":20,"limit":10}'); + console.log(describe({ ...query, offset: 0, limit: 100 })); + console.log(describe({ offset: 5, ...query })); + const merged: Filter = { sort: "added", offset: 0, limit: 20, ...untyped('{"limit":3}') }; + console.log(merged.sort, merged.offset, merged.limit, merged.genre === undefined); + const copy: { a: number; b: string } = { ...untyped('{"a":1,"b":"x"}'), b: "y" }; + console.log(copy.a + 1, copy.b, JSON.stringify(copy)); + let latest: { a: number } = { a: 0 }; + latest = { ...untyped('{"a":4}') }; + console.log(latest.a); +} + +main(); diff --git a/tests/corpus/7401-program-lifetime-array-teardown.ts b/tests/corpus/7401-program-lifetime-array-teardown.ts new file mode 100644 index 000000000..64e23118a --- /dev/null +++ b/tests/corpus/7401-program-lifetime-array-teardown.ts @@ -0,0 +1,102 @@ +import type { ProgramLifetime } from "@scriptc/types"; + +// Arrays whose elements need no release when the array is destroyed: +// ProgramLifetime instances (plain and `| undefined` elements), numbers and +// booleans. They are created and dropped by literals, push, slice, filter, +// map, concat, splice, sort and spread, kept in fields, closures and maps, +// nested in other arrays, given holes and far indices, and +// mixed with arrays of ordinary objects, whose elements are released. + +class Node implements ProgramLifetime { + readonly id: number; + next: Node | undefined = undefined; + readonly kids: Node[] = []; + constructor(id: number) { + this.id = id; + } +} + +class Leaf extends Node { + weight = 2; +} + +class Temp { + readonly label: string; + constructor(label: string) { + this.label = label; + } +} + +const nodes: Node[] = []; +for (let i = 0; i < 40; i++) nodes.push(i % 3 === 0 ? new Leaf(i) : new Node(i)); +for (let i = 1; i < nodes.length; i++) nodes[i - 1]!.next = nodes[i]; +for (const n of nodes) for (let k = 1; k <= n.id % 4; k++) n.kids.push(nodes[(n.id + k) % 40]!); + +function ids(list: readonly (Node | undefined)[]): string { + return list.map((n) => (n === undefined ? "-" : String(n.id))).join(","); +} + +// Temporary arrays from every array operation, dropped at once. +let total = 0; +for (let round = 0; round < 200; round++) { + const picked = nodes.filter((n) => n.id % 7 === round % 7); + const mapped = picked.map((n) => n.next); + const joined = picked.concat(nodes.slice(round % 10, (round % 10) + 3)); + const spliced = joined.slice(); + spliced.splice(1, 2, nodes[round % 40]!); + const sorted = [...spliced].sort((a, b) => b.id - a.id); + const optional: (Node | undefined)[] = [nodes[round % 40], undefined, mapped[0]]; + total += picked.length + mapped.length + joined.length + sorted.length + optional.length; + if (round % 50 === 0) console.log(round, ids(sorted), ids(optional), ids(mapped)); +} +console.log("temporaries", total); + +// Numbers and booleans. +let sum = 0; +for (let round = 0; round < 100; round++) { + const values = [round, round * 2, round * 3]; + const flags = values.map((v) => v % 2 === 0); + sum += values.reduce((a, b) => a + b, 0) + flags.filter((f) => f).length; +} +console.log("scalars", sum); + +// Holes and far indices keep the full teardown. +const holey: Node[] = []; +holey[3] = nodes[3]!; +holey[0] = nodes[0]!; +const far: Node[] = [nodes[1]!]; +far[4_000_000_000] = nodes[2]!; +console.log("holey", holey.length, 1 in holey, ids([holey[0], holey[3]])); +console.log("far", far.length, far[4_000_000_000]?.id, far[0]?.id); + +// Kept arrays: a field, a closure, a map, an array of arrays. +class Holder { + items: Node[] = []; +} +const holder = new Holder(); +for (let i = 0; i < 5; i++) holder.items = nodes.slice(i, i + 4); +const later = ((kept: Node[]) => () => ids(kept))(nodes.filter((n) => n instanceof Leaf)); +const byKey = new Map(); +for (let i = 0; i < 10; i++) byKey.set(`k${i % 3}`, nodes.slice(i, i + 2)); +const nested: Node[][] = []; +for (let i = 0; i < 6; i++) nested.push(nodes.slice(i, i + i)); +nested.splice(2, 3); +console.log("field", ids(holder.items)); +console.log("closure", later()); +console.log("map", [...byKey].map(([k, v]) => `${k}:${ids(v)}`).join(" ")); +console.log("nested", nested.map((list) => ids(list)).join(" | ")); + +// Arrays of ordinary objects still release their elements. +let labels = ""; +for (let round = 0; round < 50; round++) { + const temps = [new Temp(`t${round}`), new Temp(`u${round}`)]; + const kept = temps.filter((t) => t.label.endsWith("0")); + labels += kept.map((t) => t.label).join(""); +} +console.log("objects", labels); + +// Kids lists reached through the graph. +let walked = 0; +for (let at: Node | undefined = nodes[0]; at !== undefined; at = at.next) + walked += at.kids.filter((k) => k.id > at!.id).length; +console.log("walked", walked, nodes[39]!.kids.length, (nodes[3] as Leaf).weight); diff --git a/tests/corpus/7402-unknown-array-methods.ts b/tests/corpus/7402-unknown-array-methods.ts new file mode 100644 index 000000000..e374dd40f --- /dev/null +++ b/tests/corpus/7402-unknown-array-methods.ts @@ -0,0 +1,14 @@ +// Methods on arrays of unknown elements, which are stored checked-dynamic: +// direct `unknown[]` bindings and generic bodies instantiated at `unknown`. +const values: unknown[] = [1, "two", 3, { four: 4 }]; +const removed = values.splice(1, 2); +console.log(values, removed, removed.length); +values.splice(1, 0, "inserted", null); +console.log(values, values.indexOf(null), values.includes("inserted")); +console.log(values.slice(1).reverse(), values.join("|")); + +function takeAt(items: readonly T[], index: number): T[] { + const pool = [...items]; + return pool.splice(index, 1); +} +console.log(takeAt([10, 20, 30], 1), takeAt([true, "x", 7], 2)); diff --git a/tests/corpus/7403-bigint-of-typed-arrays.mjs b/tests/corpus/7403-bigint-of-typed-arrays.mjs new file mode 100644 index 000000000..9cf813d9a --- /dev/null +++ b/tests/corpus/7403-bigint-of-typed-arrays.mjs @@ -0,0 +1,62 @@ +// BigInt of a typed array converts the array to its elements joined with +// "," and parses that text, as in Node: one element reads as its value, +// an empty array as 0n, and anything else throws the SyntaxError for the +// joined text. Every element kind, negative and clamped values, float +// formatting, views and the results in arithmetic. A Buffer converts +// through its UTF-8 text instead, also when a typed array slot holds it, +// and a DataView through "[object DataView]". + +const one = [ + new Int8Array([-5]), + new Uint8Array([7]), + new Uint8ClampedArray([300]), + new Int16Array([-30000]), + new Uint16Array([65535]), + new Int32Array([-2147483648]), + new Uint32Array([4294967295]), + new Float32Array([-0]), + new Float64Array([2 ** 60]), +]; +for (const view of one) console.log(view.constructor.name, BigInt(view)); + +const empty = [new Uint8Array(0), new Float64Array([]), new Int32Array(8).subarray(3, 3)]; +for (const view of empty) console.log("empty", view.constructor.name, BigInt(view)); + +const bad = [ + new Uint8Array([1, 2]), + new Float64Array([1.5]), + new Float32Array([NaN]), + new Float64Array([1e21]), + new Int16Array([3, -4, 5]), + new Float64Array([Infinity]), +]; +for (const view of bad) { + try { + console.log("unexpected", BigInt(view)); + } catch (e) { + console.log(view.constructor.name, e instanceof SyntaxError, e.message); + } +} + +const bytes = new Uint8Array([9, 8, 7]); +const window = bytes.subarray(1, 2); +let total = 0n; +for (let i = 0; i < 100; i++) { + bytes[1] = i; + total += BigInt(window) * 3n; +} +console.log("window", BigInt(window), total, BigInt(bytes.subarray(2)) ** 2n); + +function convert(label, make) { + try { + console.log(label, BigInt(make())); + } catch (e) { + console.log(label, e instanceof SyntaxError, e.message); + } +} +convert("buffer", () => Buffer.from("12")); +convert("spaced buffer", () => Buffer.from(" -7 ")); +convert("empty buffer", () => Buffer.alloc(0)); +convert("fraction buffer", () => Buffer.from("1.5")); +convert("data view", () => new DataView(new ArrayBuffer(2))); +for (const view of [new Uint8Array([5]), Buffer.from("64")]) console.log("slot", BigInt(view)); diff --git a/tests/corpus/7403-dyn-string-at.mjs b/tests/corpus/7403-dyn-string-at.mjs new file mode 100644 index 000000000..68e865000 --- /dev/null +++ b/tests/corpus/7403-dyn-string-at.mjs @@ -0,0 +1,9 @@ +// String.prototype.at on strings held in untyped (checked-dynamic) values, +// as Hono's mergePath reads `base?.at(-1)`. +const mergePath = (base, sub, ...rest) => { + if (rest.length) sub = mergePath(sub, ...rest); + return `${base?.[0] === "/" ? "" : "/"}${base}${sub === "/" ? "" : `${base?.at(-1) === "/" ? "" : "/"}${sub?.[0] === "/" ? sub.slice(1) : sub}`}`; +}; +console.log(mergePath("/api", "/users"), mergePath("api/", "/"), mergePath("/a", "b", "/c")); +const values = JSON.parse(`["hello", "", "a\\ud83d\\ude00b"]`); +for (const v of values) console.log(JSON.stringify([v.at(0), v.at(-1), v.at(1), v.at(99), v.at(-99), v.at(1.7), v.at()])); diff --git a/tests/corpus/7420-for-await-close.ts b/tests/corpus/7420-for-await-close.ts new file mode 100644 index 000000000..52930a02c --- /dev/null +++ b/tests/corpus/7420-for-await-close.ts @@ -0,0 +1,100 @@ +// Leaving a for await loop over an async generator early closes it, as +// AsyncIteratorClose does: return, throw and break inside the body, a +// labeled continue to an outer loop, and a return() at a yield in the +// body all await the generator's return(), so its finally runs before the +// loop's caller continues. An exhausted generator and a rejected next() +// are not closed. After a throw, an error from closing is dropped; after +// a break or return it propagates. +const log: string[] = []; + +async function* numbers(tag: string, failClose = false): AsyncGenerator { + try { + yield 1; + yield 2; + yield 3; + } finally { + log.push(`${tag} finally`); + if (failClose) throw new Error(`${tag} close failed`); + } +} + +async function* slowClose(tag: string): AsyncGenerator { + try { + yield 1; + } finally { + await null; + log.push(`${tag} finally after await`); + } +} + +async function firstOf(tag: string): Promise { + for await (const n of numbers(tag)) return n * 10; + return -1; +} + +async function throwsInside(tag: string): Promise { + for await (const n of numbers(tag)) throw new Error(`${tag} body ${n}`); +} + +async function* takeOne(tag: string): AsyncGenerator { + for await (const n of numbers(tag)) { + yield n; + return; + } +} + +async function* rethrowing(tag: string): AsyncGenerator { + for await (const n of numbers(tag)) { + if (n === 2) throw new Error(`${tag} at ${n}`); + yield `${tag} ${n}`; + } +} + +async function main(): Promise { + log.push(`first ${await firstOf("return")}`); + try { + await throwsInside("throw"); + } catch (e) { + log.push(`caught ${(e as Error).message}`); + } + for await (const n of takeOne("generator-return")) log.push(`took ${n}`); + try { + for await (const s of rethrowing("generator-throw")) log.push(s); + } catch (e) { + log.push(`caught ${(e as Error).message}`); + } + const early = takeOne("consumer-return"); + for await (const n of early) { + log.push(`consumer got ${n}`); + break; + } + try { + for await (const n of numbers("break-close-fails", true)) { + log.push(`break at ${n}`); + break; + } + } catch (e) { + log.push(`caught ${(e as Error).message}`); + } + try { + for await (const n of numbers("throw-close-fails", true)) throw new Error(`body ${n}`); + } catch (e) { + log.push(`caught ${(e as Error).message}`); + } + void Promise.resolve().then(() => log.push("microtask")); + for await (const n of slowClose("slow")) { + log.push(`slow ${n}`); + break; + } + log.push("after slow"); + outer: for (const k of [1, 2]) { + for await (const n of numbers(`labeled ${k}`)) { + log.push(`labeled ${k} got ${n}`); + continue outer; + } + } + for await (const n of numbers("exhausted")) log.push(`exhausted ${n}`); + for (const line of log) console.log(line); +} + +void main(); diff --git a/tests/corpus/7421-for-await-close.mjs b/tests/corpus/7421-for-await-close.mjs new file mode 100644 index 000000000..c891a2084 --- /dev/null +++ b/tests/corpus/7421-for-await-close.mjs @@ -0,0 +1,53 @@ +// The same early exits in JavaScript: a for await loop that returns, +// throws or breaks closes its async generator, including one that reaches +// the loop as an untyped parameter, and a source whose return() is still +// running finishes before the code after the loop. +const log = []; + +async function* letters(tag) { + try { + yield "a"; + yield "b"; + } finally { + log.push(`${tag} finally`); + } +} + +async function firstLetter(source) { + for await (const c of source) return c; +} + +async function failOn(source, target) { + for await (const c of source) { + if (c === target) throw new Error(`failed on ${c}`); + log.push(`kept ${c}`); + } +} + +async function* upperUntil(source, stop) { + for await (const c of source) { + if (c === stop) return; + yield c.toUpperCase(); + } +} + +log.push(`first ${await firstLetter(letters("param-return"))}`); +try { + await failOn(letters("param-throw"), "b"); +} catch (e) { + log.push(`caught ${e.message}`); +} +for await (const c of upperUntil(letters("stage-return"), "b")) log.push(`upper ${c}`); +let ticks = 0; +const tick = () => { + log.push(`tick ${ticks}`); + if (++ticks < 4) process.nextTick(tick); +}; +process.nextTick(tick); +for await (const c of letters("break")) { + log.push(`break ${c}`); + break; +} +log.push("after break"); +await new Promise((resolve) => setTimeout(resolve, 1)); +console.log(log.join("\n")); diff --git a/tests/corpus/7422-for-await-untyped-streams.mjs b/tests/corpus/7422-for-await-untyped-streams.mjs new file mode 100644 index 000000000..b7dd21072 --- /dev/null +++ b/tests/corpus/7422-for-await-untyped-streams.mjs @@ -0,0 +1,38 @@ +// for await over a native stream that reaches the loop as an untyped +// value iterates it as a typed loop does, in the same tick order as Node: +// whole reads, an early return (the stream is destroyed), a break, an +// object-mode Transform and a stream error. +import { Readable, PassThrough, Transform } from "node:stream"; +const log = []; +async function readAll(s, tag) { + for await (const c of s) log.push(`${tag} ${typeof c} ${String(c)}`); + log.push(`${tag} done`); +} +async function first(s, tag) { + for await (const c of s) return `${tag} first ${String(c)}`; +} +async function breaks(s, tag) { + for await (const c of s) { log.push(`${tag} got ${String(c)}`); break; } +} +async function failing(s, tag) { + try { for await (const c of s) log.push(`${tag} ${c}`); } catch (e) { log.push(`${tag} caught ${e.message}`); } +} +let n = 0; +const ticker = () => { log.push(`tick ${n}`); if (++n < 6) process.nextTick(ticker); }; +process.nextTick(ticker); +await readAll(Readable.from(["a", "b"]), "arr"); +const r = Readable.from(["x", "y", "z"]); +r.on("close", () => log.push(`r close ${r.destroyed}`)); +log.push(await first(r, "r")); +const pt = new PassThrough(); +pt.on("close", () => log.push("pt close")); +pt.end("bytes"); +await breaks(pt, "pt"); +const t = new Transform({ objectMode: true, transform(c, e, cb) { cb(null, { v: c }); } }); +t.end(1); +await (async (s) => { for await (const c of s) log.push(`t ${JSON.stringify(c)}`); })(t); +const bad = new Readable({ read() { this.destroy(new Error("boom")); } }); +await failing(bad, "bad"); +await readAll(["plain", "array"], "dyn"); +await new Promise((res) => setTimeout(res, 5)); +console.log(log.join("\n")); diff --git a/tests/corpus/7423-stream-untyped-bindings.mjs b/tests/corpus/7423-stream-untyped-bindings.mjs new file mode 100644 index 000000000..23b9e1411 --- /dev/null +++ b/tests/corpus/7423-stream-untyped-bindings.mjs @@ -0,0 +1,33 @@ +// Streams held where the lowering keeps a checked-dynamic value: a +// module-level binding of a stream method's result on a value that a +// JavaScript function returns, an element read of an array of streams, +// and readable.compose on a stream whose function is typed with JSDoc. +import { PassThrough, Readable, Transform } from "node:stream"; + +const words = () => Readable.from(["alpha", "beta"]); +const shouted = words().map((w) => w.toUpperCase()); +console.log("shouted", shouted.readable, shouted.readableObjectMode); +for await (const w of shouted) console.log("shouted", w); + +const counted = words().filter((w) => w.length > 4); +counted.on("data", (w) => console.log("counted", w)); +await new Promise((resolve) => counted.on("end", resolve)); +console.log("counted ended", counted.readableEnded); + +const streams = [new PassThrough({ objectMode: true }), new Transform({ + objectMode: true, + transform(chunk, _enc, cb) { + cb(null, { wrapped: chunk }); + }, +})]; +streams[1].end(7); +for await (const v of streams[1]) console.log("element", JSON.stringify(v)); +streams[0].end("done"); +for await (const v of streams[0]) console.log("element", v); + +/** @returns {Readable} */ +const typedWords = () => Readable.from(["x", "yy"]); +const lengths = typedWords().compose(async function* (source) { + for await (const w of source) yield `${w}:${w.length}`; +}); +console.log("composed", (await lengths.toArray()).join(",")); diff --git a/tests/corpus/7424-duplex-from-streams.mjs b/tests/corpus/7424-duplex-from-streams.mjs new file mode 100644 index 000000000..6f5d43460 --- /dev/null +++ b/tests/corpus/7424-duplex-from-streams.mjs @@ -0,0 +1,47 @@ +// Duplex.from over a lone Readable, a lone Writable and a { readable, +// writable } pair, as duplexify.js's _duplexify wraps them: the Duplex +// reads through the Readable and writes through the Writable, takes each +// side's object mode from the stream behind it, and closes once both ends +// finish; destroying it without an error destroys the streams behind it. +import { Duplex, PassThrough, Readable, Writable } from "node:stream"; +const log = []; +const r = Readable.from(["a", "b"]); +const dr = Duplex.from(r); +log.push(`dr ${dr.readable} ${dr.writable} ${dr.readableObjectMode} ${dr.writableObjectMode}`); +dr.on("end", () => log.push("dr end")); +dr.on("close", () => log.push("dr close")); +for await (const c of dr) log.push(`dr ${c}`); +const got = []; +const w = new Writable({ write(c, e, cb) { got.push(String(c)); log.push(`w write ${c}`); cb(); } }); +const dw = Duplex.from(w); +log.push(`dw ${dw.readable} ${dw.writable} ${dw.writableObjectMode}`); +dw.on("finish", () => log.push("dw finish")); +dw.on("close", () => log.push("dw close")); +dw.write("x", () => log.push("dw cb x")); +dw.end("y", () => log.push("dw end cb")); +await new Promise((res) => dw.on("close", res)); +const inner = new PassThrough({ objectMode: true }); +const out = new PassThrough({ objectMode: true }); +const pair = Duplex.from({ readable: out, writable: inner }); +log.push(`pair ${pair.readable} ${pair.writable} ${pair.readableObjectMode} ${pair.writableObjectMode}`); +inner.on("data", (v) => { log.push(`inner ${JSON.stringify(v)}`); out.write({ echo: v }); }); +inner.on("finish", () => { log.push("inner finish"); out.end(); }); +pair.on("data", (v) => log.push(`pair data ${JSON.stringify(v)}`)); +pair.write({ n: 1 }); +pair.end({ n: 2 }); +await new Promise((res) => pair.on("close", res)); +log.push("pair closed"); +const killed = Duplex.from(new PassThrough()); +killed.on("error", (e) => log.push(`killed error ${e.name}`)); +killed.on("close", () => log.push(`killed close ${killed.destroyed}`)); +killed.destroy(); +await new Promise((res) => setTimeout(res, 5)); +// The pair's members are evaluated in source order; a pair still open at +// exit is left as it is. +const order = []; +const open = Duplex.from({ + writable: (order.push("writable"), new Writable({ write(c, e, cb) { cb(); } })), + readable: (order.push("readable"), Readable.from(["x"])), +}); +log.push(`pair order ${order.join(",")} ${open.readable} ${open.writable}`); +console.log(log.join("\n")); diff --git a/tests/corpus/7425-duplex-from-streams.ts b/tests/corpus/7425-duplex-from-streams.ts new file mode 100644 index 000000000..a9d85b797 --- /dev/null +++ b/tests/corpus/7425-duplex-from-streams.ts @@ -0,0 +1,72 @@ +// Duplex.from over streams in typed code: a Readable that fails destroys +// the Duplex with its error, a slow Writable's callbacks and the end +// callback follow its pace, a pair whose writable member is an async +// generator function's Duplex, compose() with an async function as its +// last stage, and a Duplex over a promise. +import { Duplex, PassThrough, Readable, Writable, compose } from "node:stream"; +const log: string[] = []; +async function main(): Promise { + const failing = new Readable({ read(): void { this.destroy(new Error("source failed")); } }); + const df = Duplex.from(failing); + df.on("error", (e: Error) => log.push(`df error ${e.message}`)); + df.on("close", () => log.push(`df close ${df.destroyed}`)); + df.resume(); + await new Promise((res) => df.on("close", res)); + const slow = new Writable({ + highWaterMark: 2, + write(c: Buffer, _e: string, cb: (err?: Error | null) => void): void { + log.push(`slow write ${c.toString()}`); + setTimeout(cb, 2); + }, + }); + const ds = Duplex.from(slow); + log.push(`write1 ${ds.write("aa", () => log.push("cb aa"))}`); + log.push(`write2 ${ds.write("bb", () => log.push("cb bb"))}`); + ds.on("drain", () => log.push("ds drain")); + ds.end(() => log.push("ds end cb")); + await new Promise((res) => ds.on("finish", res)); + log.push("ds finished"); + const upper = new PassThrough(); + const dp = Duplex.from({ + readable: Readable.from(["r1", "r2"]), + writable: async function* (source: AsyncIterable): AsyncGenerator { + for await (const c of source) log.push(`gen got ${String(c)}`); + yield "unused"; + }, + }); + dp.on("data", (c: unknown) => log.push(`dp data ${String(c)}`)); + dp.write("w1"); + dp.end(); + await new Promise((res) => dp.on("end", res)); + upper.end(); + await more(); + for (const line of log) console.log(line); +} +async function more(): Promise { + const sink = compose(new PassThrough({ objectMode: true }), async function (source: AsyncIterable): Promise { + for await (const c of source) log.push(`sink ${String(c)}`); + }); + log.push(`sink r=${sink.readable} w=${sink.writable}`); + sink.write("p"); + sink.end("q"); + await new Promise((res) => sink.on("finish", res)); + log.push("sink finished"); + const fromReadable: Duplex = Duplex.from(Readable.from([1, 2])); + const values: unknown[] = []; + for await (const v of fromReadable) values.push(v); + log.push(`readable ${JSON.stringify(values)}`); + const written: string[] = []; + const target = new Writable({ + write(c: Buffer, _e: string, cb: (err?: Error | null) => void): void { + written.push(c.toString()); + cb(); + }, + }); + const fromWritable: Duplex = Duplex.from(target); + fromWritable.end("only"); + await new Promise((res) => fromWritable.on("finish", res)); + log.push(`writable ${written.join(",")}`); + const value: Duplex = Duplex.from(Promise.resolve("resolved")); + for await (const v of value) log.push(`promise ${String(v)}`); +} +void main(); diff --git a/tests/corpus/7426-duplex-from-promise.mjs b/tests/corpus/7426-duplex-from-promise.mjs new file mode 100644 index 000000000..46d19586b --- /dev/null +++ b/tests/corpus/7426-duplex-from-promise.mjs @@ -0,0 +1,27 @@ +// Duplex.from over a promise: a Duplex that is not writable and reads the +// promise's value once it settles (nothing for null), then ends; a +// rejection destroys it with the reason. The events interleave with +// microtasks and nextTick callbacks as in Node. +import { Duplex } from "node:stream"; +const log = []; +const tick = (n) => { if (n < 5) { log.push(`tick ${n}`); process.nextTick(() => tick(n + 1)); } }; +const later = (v, ms) => new Promise((res) => setTimeout(() => res(v), ms)); +const a = Duplex.from(Promise.resolve({ answer: 42 })); +log.push(`a ${a.readable} ${a.writable} ${a.readableObjectMode}`); +a.on("data", (v) => log.push(`a data ${JSON.stringify(v)}`)); +a.on("end", () => log.push("a end")); +a.on("close", () => log.push("a close")); +Promise.resolve().then(() => log.push("microtask 1")).then(() => log.push("microtask 2")); +tick(0); +await new Promise((res) => a.on("close", res)); +const b = Duplex.from(later(null, 1)); +b.on("data", () => log.push("b data")); +b.on("end", () => log.push("b end")); +await new Promise((res) => b.on("close", res)); +const c = Duplex.from(Promise.reject(new Error("nope"))); +c.on("error", (e) => log.push(`c error ${e.message}`)); +c.on("close", () => log.push(`c close ${c.destroyed}`)); +await new Promise((res) => c.on("close", res)); +const d = Duplex.from(later("text", 1)); +for await (const v of d) log.push(`d ${v}`); +console.log(log.join("\n")); diff --git a/tests/corpus/7427-duplex-from-async-function.mjs b/tests/corpus/7427-duplex-from-async-function.mjs new file mode 100644 index 000000000..66ce35f9a --- /dev/null +++ b/tests/corpus/7427-duplex-from-async-function.mjs @@ -0,0 +1,40 @@ +// Duplex.from over an async function: a Duplex that is only writable, whose +// writes reach the function's source and whose end waits for the +// function's promise. A value other than null or undefined is +// ERR_INVALID_RETURN_VALUE, a return before the input is consumed aborts +// the Duplex, and a throw destroys it. +import { Duplex } from "node:stream"; +const log = []; +const sink = Duplex.from(async function (source) { + for await (const c of source) log.push(`sink got ${c}`); + log.push("sink done"); +}); +log.push(`sink ${sink.readable} ${sink.writable} ${sink.writableObjectMode}`); +sink.on("finish", () => log.push("sink finish")); +sink.on("close", () => log.push("sink close")); +sink.write("a", () => log.push("cb a")); +sink.end("b", () => log.push("end cb")); +await new Promise((res) => sink.on("close", res)); +const bad = Duplex.from(async function (source) { + for await (const c of source) log.push(`bad got ${c}`); + return 5; +}); +bad.on("error", (e) => log.push(`bad error ${e.code} ${e.message}`)); +bad.on("close", () => log.push("bad close")); +bad.end("x"); +await new Promise((res) => bad.on("close", res)); +const early = Duplex.from(async function (source) { + for await (const c of source) { log.push(`early got ${c}`); break; } +}); +early.on("error", (e) => log.push(`early error ${e.name}`)); +early.on("close", () => log.push(`early close ${early.destroyed}`)); +early.write("1"); +early.write("2"); +await new Promise((res) => early.on("close", res)); +const failing = Duplex.from(async function (source) { + for await (const c of source) throw new Error("fail " + c); +}); +failing.on("error", (e) => log.push(`failing error ${e.message}`)); +failing.write("z"); +await new Promise((res) => failing.on("close", res)); +console.log(log.join("\n")); diff --git a/tests/corpus/7428-web-stream-sources.mjs b/tests/corpus/7428-web-stream-sources.mjs new file mode 100644 index 000000000..72d98e5f8 --- /dev/null +++ b/tests/corpus/7428-web-stream-sources.mjs @@ -0,0 +1,41 @@ +// A web ReadableStream as an async iterable source: Readable.from reads +// its chunks through the stream's iterator (pull-based and start-based +// sources, Uint8Array chunks in byte mode, an error from the controller, +// and an early destroy that cancels the stream), and it heads compose() +// and pipeline() like any async iterable. +import { Readable, Writable, compose, pipeline } from "node:stream"; +const log = []; +let n = 0; +const pulled = new ReadableStream({ + pull(c) { n++; log.push(`pull ${n}`); if (n <= 3) c.enqueue(`p${n}`); else c.close(); }, + cancel(reason) { log.push(`cancel ${reason}`); }, +}); +for await (const c of Readable.from(pulled)) log.push(`got ${c}`); +const bytes = new ReadableStream({ start(c) { c.enqueue(new TextEncoder().encode("hi")); c.enqueue(new TextEncoder().encode("!")); c.close(); } }); +const br = Readable.from(bytes, { objectMode: false }); +br.on("data", (b) => log.push(`bytes ${Buffer.isBuffer(b)} ${b.toString()}`)); +await new Promise((res) => br.on("end", res)); +const failing = new ReadableStream({ start(c) { c.enqueue("ok"); c.error(new Error("web failed")); } }); +const fr = Readable.from(failing); +fr.on("data", (c) => log.push(`failing data ${c}`)); +fr.on("error", (e) => log.push(`failing error ${e.message}`)); +await new Promise((res) => fr.on("close", res)); +let m = 0; +const endless = new ReadableStream({ + pull(c) { c.enqueue(`e${++m}`); }, + cancel(reason) { log.push(`endless cancel ${reason === undefined ? "undefined" : reason}`); }, +}); +const er = Readable.from(endless); +er.once("data", (c) => { log.push(`endless ${c}`); er.destroy(); }); +await new Promise((res) => er.on("close", res)); +await new Promise((res) => setTimeout(res, 5)); +const head = compose(new ReadableStream({ start(c) { c.enqueue("c1"); c.enqueue("c2"); c.close(); } }), async function* (s) { for await (const c of s) yield `${c}!`; }); +log.push(`compose ${(await head.toArray()).join(",")}`); +await new Promise((resolve) => + pipeline( + new ReadableStream({ start(c) { c.enqueue("q1"); c.enqueue("q2"); c.close(); } }), + new Writable({ objectMode: true, write(c, e, cb) { log.push(`pipeline ${c}`); cb(); } }), + (err) => { log.push(`pipeline callback ${err}`); resolve(undefined); }, + ), +); +console.log(log.join("\n")); diff --git a/tests/corpus/7429-web-stream-sources.ts b/tests/corpus/7429-web-stream-sources.ts new file mode 100644 index 000000000..8243b6873 --- /dev/null +++ b/tests/corpus/7429-web-stream-sources.ts @@ -0,0 +1,42 @@ +// Web ReadableStreams as sources in typed code: Readable.from over a +// ReadableStream, interleaved with nextTick callbacks, and a +// ReadableStream heading compose() into an async generator. +import { Readable, compose } from "node:stream"; + +const log: string[] = []; + +function words(): ReadableStream { + return new ReadableStream({ + start(controller): void { + controller.enqueue("typed"); + controller.enqueue("web"); + controller.close(); + }, + }); +} + +async function main(): Promise { + let n = 0; + const tick = (): void => { + log.push(`tick ${n}`); + if (++n < 4) process.nextTick(tick); + }; + process.nextTick(tick); + const r = Readable.from(words()); + log.push(`object mode ${r.readableObjectMode}`); + for await (const c of r) log.push(`word ${String(c)}`); + const bytes = new ReadableStream({ + start(controller): void { + controller.enqueue(new TextEncoder().encode("ab")); + controller.enqueue(new TextEncoder().encode("cd")); + controller.close(); + }, + }); + const lengths = compose(bytes, async function* (source: AsyncIterable): AsyncGenerator { + for await (const chunk of source) yield `${chunk.length}`; + }); + log.push(`lengths ${(await lengths.toArray()).join(",")}`); + for (const line of log) console.log(line); +} + +void main(); diff --git a/tests/corpus/7430-readable-from-web.mjs b/tests/corpus/7430-readable-from-web.mjs new file mode 100644 index 000000000..017556f9d --- /dev/null +++ b/tests/corpus/7430-readable-from-web.mjs @@ -0,0 +1,42 @@ +// Readable.fromWeb: a Readable over a web ReadableStream's reader, in +// byte mode by default (Uint8Array chunks arrive as Buffers), in object +// mode, and with an encoding; an early destroy cancels the stream with +// null, and an error from the stream destroys the Readable. Duplex.from +// and compose() over a lone web ReadableStream read it the same way. +import { Duplex, Readable, compose } from "node:stream"; +const log = []; +const tick = (n) => { if (n < 6) { log.push(`tick ${n}`); process.nextTick(() => tick(n + 1)); } }; +const web = new ReadableStream({ start(c) { c.enqueue(new TextEncoder().encode("ab")); c.enqueue(new TextEncoder().encode("cd")); c.close(); } }); +const r = Readable.fromWeb(web); +log.push(`r ${r.readableObjectMode} ${r.readableHighWaterMark}`); +r.on("data", (b) => log.push(`data ${Buffer.isBuffer(b)} ${b.toString()}`)); +r.on("end", () => log.push("end")); +r.on("close", () => log.push("close")); +tick(0); +await new Promise((res) => r.on("close", res)); +const words = new ReadableStream({ start(c) { c.enqueue("x"); c.enqueue("y"); c.close(); } }); +const o = Readable.fromWeb(words, { objectMode: true, highWaterMark: 1 }); +for await (const v of o) log.push(`obj ${v}`); +const text = new ReadableStream({ start(c) { c.enqueue(new TextEncoder().encode("héllo")); c.close(); } }); +const t = Readable.fromWeb(text, { encoding: "utf8" }); +for await (const v of t) log.push(`text ${typeof v} ${v}`); +let m = 0; +const endless = new ReadableStream({ pull(c) { c.enqueue(new Uint8Array([++m])); }, cancel(reason) { log.push(`cancel ${reason && reason.name}`); } }); +const e = Readable.fromWeb(endless); +e.once("data", (b) => { log.push(`endless ${b[0]}`); e.destroy(); }); +e.on("close", () => log.push("endless close")); +await new Promise((res) => e.on("close", res)); +await new Promise((res) => setTimeout(res, 5)); +const failing = new ReadableStream({ start(c) { c.error(new Error("web broke")); } }); +const f = Readable.fromWeb(failing); +f.on("error", (err) => log.push(`failing error ${err.message}`)); +f.on("close", () => log.push("failing close")); +f.resume(); +await new Promise((res) => f.on("close", res)); +const d = Duplex.from(new ReadableStream({ start(c) { c.enqueue(new TextEncoder().encode("dx")); c.close(); } })); +log.push(`d ${d.readable} ${d.writable} ${d.readableObjectMode}`); +d.on("data", (b) => log.push(`d data ${b.toString()}`)); +await new Promise((res) => d.on("close", res)); +const one = compose(new ReadableStream({ start(c) { c.enqueue(new TextEncoder().encode("one")); c.close(); } })); +for await (const b of one) log.push(`one ${b.toString()}`); +console.log(log.join("\n")); diff --git a/tests/corpus/7431-readable-from-web.ts b/tests/corpus/7431-readable-from-web.ts new file mode 100644 index 000000000..23e7d2871 --- /dev/null +++ b/tests/corpus/7431-readable-from-web.ts @@ -0,0 +1,47 @@ +// Readable.fromWeb in typed code: a ReadableStream read as +// Buffers, a ReadableStream in object mode with a highWaterMark of +// 1, and an AbortSignal that destroys the Readable and cancels the stream. +import { Readable } from "node:stream"; + +const log: string[] = []; + +async function main(): Promise { + const bytes = new ReadableStream({ + start(controller): void { + controller.enqueue(new TextEncoder().encode("web")); + controller.enqueue(new TextEncoder().encode("bytes")); + controller.close(); + }, + }); + const r = Readable.fromWeb(bytes); + for await (const chunk of r) log.push(`bytes ${Buffer.isBuffer(chunk)} ${String(chunk)}`); + const words = new ReadableStream({ + start(controller): void { + controller.enqueue("alpha"); + controller.enqueue("beta"); + controller.close(); + }, + }); + const o = Readable.fromWeb(words, { objectMode: true, highWaterMark: 1 }); + log.push(`object mode ${o.readableObjectMode} ${o.readableHighWaterMark}`); + for await (const word of o) log.push(`word ${String(word)}`); + const ac = new AbortController(); + const signal = ac.signal; + let n = 0; + const endless = new ReadableStream({ + pull(controller): void { + controller.enqueue(new Uint8Array([++n])); + }, + cancel(reason: unknown): void { + log.push(`cancelled ${(reason as Error).name}`); + }, + }); + const a = Readable.fromWeb(endless, { signal }); + a.on("error", (e: Error) => log.push(`aborted ${e.name}`)); + a.once("data", () => ac.abort()); + await new Promise((resolve) => a.on("close", () => resolve())); + await new Promise((resolve) => setTimeout(resolve, 5)); + for (const line of log) console.log(line); +} + +void main(); diff --git a/tests/corpus/7432-readable-push-views.mjs b/tests/corpus/7432-readable-push-views.mjs new file mode 100644 index 000000000..9aca435b6 --- /dev/null +++ b/tests/corpus/7432-readable-push-views.mjs @@ -0,0 +1,16 @@ +// readable.push() in byte mode turns a Uint8Array or another typed array +// into a Buffer over the same bytes (Stream._uint8ArrayToBuffer); the +// pushed array itself is left as it was. +import { Readable } from "node:stream"; +const u = new Uint8Array([100, 101]); +const r = new Readable({ read() {} }); +r.push(u); r.push(new Uint16Array([0x6766])); r.push(null); +for await (const b of r) console.log(Buffer.isBuffer(b), b.toString(), b.length); +console.log("source still", Buffer.isBuffer(u)); +const r1 = new Readable({ read() {} }); +r1.push(new Uint8Array([97, 98])); +r1.push(null); +for await (const b of r1) console.log(Buffer.isBuffer(b), b.toString()); +const r2 = new Readable({ read() {} }); +r2.on("data", (b) => console.log("data", Buffer.isBuffer(b), b.toString())); +r2.push(new Uint8Array([99])); r2.push(null); diff --git a/tests/corpus/7433-readable-operator-signal.mjs b/tests/corpus/7433-readable-operator-signal.mjs new file mode 100644 index 000000000..0fc23c2d0 --- /dev/null +++ b/tests/corpus/7433-readable-operator-signal.mjs @@ -0,0 +1,41 @@ +// The Readable operators pass their callbacks { signal } as the second +// argument; map and filter's signal aborts when the options' signal +// does, reduce's signal is aborted once reduce settles, and map may +// return any value. +import { Readable } from "node:stream"; +const log = []; +log.push(JSON.stringify(await Readable.from([1, 2, 3]).map((n) => ({ n, sq: n * n })).toArray())); +log.push(JSON.stringify(await Readable.from(["a", "bb"]).map((s) => s.length).toArray())); +log.push(JSON.stringify(await Readable.from([1, 2]).map((n, { signal }) => `${n}:${signal.aborted}`).toArray())); +log.push(JSON.stringify(await Readable.from([1, 2, 3, 4]).filter(async (n, opts) => n % 2 === 0 && !opts.signal.aborted).toArray())); +log.push(String(await Readable.from([1, 2, 3]).reduce((acc, n, { signal }) => acc + n + (signal.aborted ? 100 : 0), 0))); +let kept; +await Readable.from([1]).reduce((acc, n, { signal }) => { kept = signal; return acc + n; }, 0); +log.push(`reduce signal after ${kept.aborted}`); +let mapKept; +await Readable.from([1]).map((n, { signal }) => { mapKept = signal; return n; }).toArray(); +log.push(`map signal after ${mapKept.aborted}`); +const ac = new AbortController(); +const events = []; +const p = Readable.from([1, 2, 3]).map(async (n, { signal }) => { + signal.addEventListener("abort", () => events.push(`abort ${n}`)); + await new Promise((r) => setTimeout(r, 10)); + return n; +}, { signal: ac.signal }).toArray(); +setTimeout(() => ac.abort(), 5); +try { await p; } catch (e) { log.push(`map aborted ${e.name}`); } +log.push(events.join(",")); +const dyn = [Readable.from([4, 5])][0]; +log.push(JSON.stringify(await dyn.map((n, { signal }) => n + Number(signal.aborted)).toArray())); +let seen = 0; +await Readable.from([5, 6]).forEach((n, { signal }) => { if (signal) seen++; }); +log.push(`forEach ${seen}`); +log.push(String(await Readable.from([1, 2, 3]).some((n, { signal }) => n > 2 && !signal.aborted))); +log.push(String(await Readable.from([1, 2, 3]).find((n, { signal }) => n > 1 && !signal.aborted))); +log.push(String(await Readable.from([1, 2, 3]).every((n, { signal }) => n > 0 && !signal.aborted))); +// A callback forwarded through an untyped parameter gets { signal } too. +const mapWith = (fn) => Readable.from([1, 2]).map(fn).toArray(); +log.push(JSON.stringify(await mapWith((n, { signal }) => `${n}:${signal.aborted}`))); +const reduceWith = (fn) => Readable.from([1, 2]).reduce(fn, 0); +log.push(String(await reduceWith((acc, n, { signal }) => acc + n + (signal.aborted ? 10 : 0)))); +console.log(log.join("\n")); diff --git a/tests/corpus/7434-readable-operator-values.ts b/tests/corpus/7434-readable-operator-values.ts new file mode 100644 index 000000000..49f7a0fa9 --- /dev/null +++ b/tests/corpus/7434-readable-operator-values.ts @@ -0,0 +1,32 @@ +// Typed Readable operators: map returns numbers and records, flatMap +// takes generator functions and sets, the callbacks may declare the +// { signal } argument, and read() on an object-mode stream answers the +// values pushed. +import { Readable } from "node:stream"; +async function* pair(n: number): AsyncGenerator { + yield n; + yield n * 100; +} +function* letters(s: string): Generator { + for (const c of s) yield c; +} +async function main(): Promise { + const squares = await Readable.from([1, 2, 3]).map((n: number) => n * n).toArray(); + const records = await Readable.from(["a", "bb"]).map((s: string) => ({ s, len: s.length })).toArray(); + const pairs = await Readable.from([1, 2]).flatMap(pair).toArray(); + const chars = await Readable.from(["hi"]).flatMap(letters).toArray(); + const sets = await Readable.from([7]).flatMap((n: number) => new Set([n, n + 1])).toArray(); + const signals = await Readable.from([1]).map((n: number, opts?: { signal?: AbortSignal }) => `${n}:${opts?.signal?.aborted}`).toArray(); + const total = await Readable.from([1, 2]).reduce((acc: number, n: number, opts?: { signal?: AbortSignal }) => acc + n + (opts?.signal?.aborted ? 1 : 0), 0); + console.log(JSON.stringify({ squares, records, pairs, chars, sets, signals, total })); + const r = new Readable({ objectMode: true, read(): void {} }); + r.push({ a: 1 }); + r.push(5); + r.push(null); + r.once("readable", () => { + const first: unknown = r.read(); + const second: unknown = r.read(); + console.log(JSON.stringify(first), second, r.read()); + }); +} +void main(); diff --git a/tests/corpus/7435-readable-flatmap-iterables.mjs b/tests/corpus/7435-readable-flatmap-iterables.mjs new file mode 100644 index 000000000..f0ee79b5a --- /dev/null +++ b/tests/corpus/7435-readable-flatmap-iterables.mjs @@ -0,0 +1,29 @@ +// flatMap over what the callback returns: async and sync generators, a +// Set, a Map, a Readable, and a value that is not iterable (V8's +// TypeError); the generator's steps interleave with process.nextTick as +// in Node, breaking out of the loop runs the inner generator's finally, +// and an inner throw rejects toArray(). +import { Readable } from "node:stream"; +const log = []; +log.push(JSON.stringify(await Readable.from(["x"]).flatMap(async function* (s) { yield s; yield s + s; }).toArray())); +log.push(JSON.stringify(await Readable.from([2]).flatMap(function* (n) { yield n; yield -n; }).toArray())); +log.push(JSON.stringify(await Readable.from([1, 2]).flatMap((n) => new Set([n, n])).toArray())); +log.push(JSON.stringify(await Readable.from(["p"]).flatMap((s) => Readable.from([s, s.toUpperCase()])).toArray())); +log.push(JSON.stringify(await Readable.from([3]).flatMap((n) => new Map([[n, "v"]])).toArray())); +log.push(JSON.stringify(await Readable.from([{ k: 1 }]).flatMap((o) => [o, { k: o.k + 1 }]).toArray())); +try { await Readable.from([1]).flatMap(() => 5).toArray(); } catch (e) { log.push(`${e.name} ${e.message}`); } +try { await Readable.from([1]).flatMap(() => null).toArray(); } catch (e) { log.push(`${e.name} ${e.message}`); } +const order = []; +let n = 0; +const tick = () => { order.push(`t${n}`); if (++n < 5) process.nextTick(tick); }; +process.nextTick(tick); +for await (const v of Readable.from([1]).flatMap(async function* (k) { order.push("gen start"); yield k; order.push("gen mid"); yield k + 1; })) order.push(`v${v}`); +log.push(order.join(" ")); +const closed = []; +const early = Readable.from([1, 2]).flatMap(async function* (k) { try { yield k; yield k * 10; } finally { closed.push(`finally ${k}`); } }); +for await (const v of early) { closed.push(`got ${v}`); break; } +log.push(closed.join(" ")); +const thrown = []; +try { await Readable.from([1]).flatMap(async function* () { yield 1; throw new Error("inner boom"); }).toArray(); } catch (e) { thrown.push(e.message); } +log.push(thrown.join(" ")); +console.log(log.join("\n")); diff --git a/tests/corpus/7436-readable-read-any-value.mjs b/tests/corpus/7436-readable-read-any-value.mjs new file mode 100644 index 000000000..5e19b16c8 --- /dev/null +++ b/tests/corpus/7436-readable-read-any-value.mjs @@ -0,0 +1,17 @@ +// readable.read() answers any value on an object-mode stream, and a +// Buffer or a string (with an encoding) in byte mode. +import { Readable, PassThrough } from "node:stream"; +const r = new Readable({ objectMode: true, read() {} }); +r.push({ a: 1 }); r.push(2); r.push("three"); r.push([4]); r.push(null); +r.once("readable", () => { let v; while ((v = r.read()) !== null) console.log("read", JSON.stringify(v)); }); +const p = new PassThrough({ objectMode: true }); +p.write([1, 2]); +p.end(); +p.once("readable", () => console.log("pt", JSON.stringify(p.read()), p.read())); +const b = new PassThrough(); +b.end("bytes!"); +b.once("readable", () => { const c = b.read(3); console.log("bytes", Buffer.isBuffer(c), String(c), String(b.read())); }); +const e = new PassThrough(); +e.setEncoding("utf8"); +e.end("text"); +e.once("readable", () => { const t = e.read(); console.log("encoded", typeof t, t); }); diff --git a/tests/corpus/7437-duplex-from-signal.mjs b/tests/corpus/7437-duplex-from-signal.mjs new file mode 100644 index 000000000..263225bbb --- /dev/null +++ b/tests/corpus/7437-duplex-from-signal.mjs @@ -0,0 +1,54 @@ +// A Duplex.from body may take { signal } (duplexify.js's fromAsyncGen): +// destroy aborts it with the destroy error, or with an AbortError when +// there is none, and compose passes it to its function stages. +import { Duplex, Readable, compose } from "node:stream"; +import { once } from "node:events"; + +const d = Duplex.from(async function* (source, { signal }) { + console.log("body", typeof signal, signal.aborted); + signal.addEventListener("abort", () => console.log("abort event", signal.aborted, signal.reason)); + for await (const chunk of source) yield String(chunk).toUpperCase(); + console.log("source ended"); +}); +d.on("data", (c) => console.log("data", c)); +d.write("a"); +d.write("b"); +await new Promise((r) => setTimeout(r, 10)); +d.destroy(); +await once(d, "close"); +console.log("closed d"); + +const seen = []; +const fin = Duplex.from(async function (source, { signal }) { + signal.addEventListener("abort", () => console.log("abort", seen.join(","))); + for await (const chunk of source) seen.push(String(chunk)); + console.log("done", signal.aborted); +}); +fin.on("finish", () => console.log("finish")); +fin.write("1"); +fin.write("2"); +fin.end(); +await once(fin, "close"); +console.log("closed fin"); + +const boom = new Error("boom"); +const e = Duplex.from(async function (source, { signal }) { + signal.addEventListener("abort", () => console.log("e abort", String(signal.reason), signal.reason === boom)); + await new Promise((r) => setTimeout(r, 50)); +}); +e.on("error", (err) => console.log("e error", err === boom)); +setTimeout(() => e.destroy(boom), 5); +await new Promise((r) => e.on("close", r)); +console.log("closed e"); + +const c = compose( + async function* (source, { signal }) { + for await (const chunk of source) yield `${chunk}!${signal.aborted}`; + }, + async function* (source, { signal }) { + for await (const chunk of source) yield chunk.toUpperCase(); + }, +); +Readable.from(["a", "b"]).pipe(c); +c.on("data", (x) => console.log("compose", String(x))); +c.on("end", () => console.log("compose end")); diff --git a/tests/corpus/7438-duplex-from-signal.ts b/tests/corpus/7438-duplex-from-signal.ts new file mode 100644 index 000000000..358bd3e68 --- /dev/null +++ b/tests/corpus/7438-duplex-from-signal.ts @@ -0,0 +1,26 @@ +// Typed Duplex.from bodies that declare the options argument: an async +// generator function and an async function. +import { Duplex } from "node:stream"; + +const d = Duplex.from(async function* (source: AsyncIterable, opts: { signal: AbortSignal }) { + opts.signal.addEventListener("abort", () => console.log("aborted", opts.signal.aborted)); + for await (const chunk of source) { + yield `<${chunk}>`; + } +}); +d.on("data", (c: string) => console.log("data", c)); +d.on("end", () => console.log("end")); +d.on("close", () => { + console.log("close"); + const w = Duplex.from(async function (source: AsyncIterable, { signal }: { signal: AbortSignal }): Promise { + let n = 0; + for await (const chunk of source) n += chunk.length; + console.log("total", n, signal.aborted); + }); + w.on("finish", () => console.log("finish")); + w.on("close", () => console.log("w close")); + w.write("abc"); + w.end("de"); +}); +d.write("x"); +d.end("y"); diff --git a/tests/corpus/7439-readable-read-string-chunks.ts b/tests/corpus/7439-readable-read-string-chunks.ts new file mode 100644 index 000000000..2fc5dbed6 --- /dev/null +++ b/tests/corpus/7439-readable-read-string-chunks.ts @@ -0,0 +1,17 @@ +// A typed read() answers the strings a stream holds: the first value a +// lazy Readable.from pull over a parsed array produces, and the decoded +// text once _read calls setEncoding(). +import { Readable } from "node:stream"; +const r = Readable.from(JSON.parse('["j1", "j2"]')); +const chunk = r.read(); +console.log("first", typeof chunk, chunk === null ? "null" : chunk.toString("hex")); +r.once("readable", () => console.log("later", JSON.stringify(r.read()))); +const e = new Readable({ + read() { + this.setEncoding("utf8"); + this.push(Buffer.from("ab")); + this.push(null); + }, +}); +const text = e.read(); +console.log("encoded", typeof text, text === null ? "null" : text.toString("hex")); diff --git a/tests/corpus/7440-fromentries-aliased-entries.ts b/tests/corpus/7440-fromentries-aliased-entries.ts new file mode 100644 index 000000000..f7c4c7e21 --- /dev/null +++ b/tests/corpus/7440-fromentries-aliased-entries.ts @@ -0,0 +1,12 @@ +// `Object.fromEntries(...) as T` over entries whose values are native +// arrays keeps their identity: both fields hold the one array the entries +// name, so a push through one is seen through the other and the original +// (corpus 6467 covers the scalar and checked-dynamic cases). +type Scores = { a: number[]; b: number[] }; +const shared = [10]; +const scores = Object.fromEntries([ + ["a", shared], + ["b", shared], +]) as Scores; +scores.a.push(11); +console.log(shared.length, scores.b.length); diff --git a/tests/corpus/7441-untyped-receiver-own-string-method-names.ts b/tests/corpus/7441-untyped-receiver-own-string-method-names.ts new file mode 100644 index 000000000..632ceb16f --- /dev/null +++ b/tests/corpus/7441-untyped-receiver-own-string-method-names.ts @@ -0,0 +1,38 @@ +// An untyped receiver calling a String.prototype method name holds a +// string or a checked-dynamic object with its own method of that name +// (zod's schemas assign `trim`, `toLowerCase` and `startsWith` in their +// constructors, so `z.string().trim().min(1)` chains schema calls). +// Each call dispatches on the receiver's runtime kind. +interface Field { + value: string; + checks: string[]; +} + +function field(value: string, checks: string[]): any { + const self: Field & Record = { value, checks }; + self["trim"] = () => field(value.trim(), [...checks, "trim"]); + self["toLowerCase"] = () => field(value.toLowerCase(), [...checks, "lower"]); + self["startsWith"] = (prefix: string) => checks.length > 0 && value.startsWith(prefix); + self["min"] = (n: number) => field(value, [...checks, `min(${n})`]); + return self; +} + +const chained = field(" Hello World ", []).trim().toLowerCase().min(1); +console.log(JSON.stringify(chained.value), chained.checks.join(",")); +console.log(field("abc", ["x"]).startsWith("ab"), field("abc", []).startsWith("ab")); + +const text: any = " Mixed Case "; +console.log(JSON.stringify(text.trim()), text.toLowerCase(), text.startsWith(" M")); + +const parsed: any = JSON.parse('{"name":" padded "}'); +console.log(JSON.stringify(parsed.name.trim())); + +for (const value of [field(" a ", []), " b ", 42, null]) { + const v: any = value; + try { + const out = v.trim(); + console.log(typeof out === "string" ? JSON.stringify(out) : `field ${JSON.stringify(out.value)}`); + } catch (error) { + console.log(error instanceof TypeError, (error as Error).message); + } +} diff --git a/tests/corpus/7445-crypto-key-pair-options.mjs b/tests/corpus/7445-crypto-key-pair-options.mjs new file mode 100644 index 000000000..2e8f7e8b3 --- /dev/null +++ b/tests/corpus/7445-crypto-key-pair-options.mjs @@ -0,0 +1,117 @@ +// generateKeyPairSync and generateKeyPair check their arguments in Node's +// order: the callback, the type, the options object, the public and then +// the private key encoding, and then the options of the key type. The +// encodings follow Node's parseKeyEncoding rules for formats, types, +// ciphers and passphrases. +import * as crypto from "node:crypto"; + +function attempt(label, run) { + try { + const result = run(); + console.log(label, result === undefined ? "returned" : Object.keys(result)); + } catch (e) { + console.log(label, "throws", e.name, e.code, e.message); + } +} + +const ec = (options) => () => crypto.generateKeyPairSync("ec", { namedCurve: "P-256", ...options }); +const rsa = (options) => () => crypto.generateKeyPairSync("rsa", { modulusLength: 1024, ...options }); + +// The type, then the options object, then the encodings, then the rest. +attempt("number type", () => crypto.generateKeyPairSync(5, 7)); +attempt("unknown type, bad encoding", () => crypto.generateKeyPairSync("foo", { publicKeyEncoding: 5 })); +attempt("unknown type, number options", () => crypto.generateKeyPairSync("foo", 5)); +attempt("unknown type, null options", () => crypto.generateKeyPairSync("foo", null)); +attempt("unknown type, array options", () => crypto.generateKeyPairSync("foo", [])); +attempt("unknown type", () => crypto.generateKeyPairSync("foo")); +attempt("upper-case types", () => crypto.generateKeyPairSync("RSA", {})); +attempt("upper-case Ed25519", () => crypto.generateKeyPairSync("Ed25519")); +attempt("upper-case ML-DSA", () => crypto.generateKeyPairSync("ML-DSA-44")); +attempt("unknown type, pkcs1", () => crypto.generateKeyPairSync("foo", { publicKeyEncoding: { format: "pem", type: "pkcs1" } })); +attempt("rsa, bad modulusLength and encoding", () => crypto.generateKeyPairSync("rsa", { modulusLength: "x", publicKeyEncoding: 5 })); +attempt("rsa, bad modulusLength and private encoding", () => crypto.generateKeyPairSync("rsa", { modulusLength: "x", privateKeyEncoding: 5 })); +attempt("rsa, negative modulusLength and format", () => crypto.generateKeyPairSync("rsa", { modulusLength: -1, publicKeyEncoding: { format: "x" } })); +attempt("rsa, bad exponent and encoding", () => crypto.generateKeyPairSync("rsa", { modulusLength: 1024, publicExponent: "x", publicKeyEncoding: 5 })); +attempt("rsa-pss, bad encoding", () => crypto.generateKeyPairSync("rsa-pss", { publicKeyEncoding: 5 })); +attempt("rsa-pss, bad modulusLength", () => crypto.generateKeyPairSync("rsa-pss", { modulusLength: "x" })); +attempt("dsa, bad encoding", () => crypto.generateKeyPairSync("dsa", { publicKeyEncoding: 5 })); +attempt("dsa, no options", () => crypto.generateKeyPairSync("dsa")); +attempt("dh, number options", () => crypto.generateKeyPairSync("dh", 5)); +attempt("ec, no curve and bad encoding", () => crypto.generateKeyPairSync("ec", { publicKeyEncoding: 5 })); +attempt("ec, unknown curve and bad encoding", () => crypto.generateKeyPairSync("ec", { namedCurve: "nope", publicKeyEncoding: 5 })); +attempt("ec, number curve", () => crypto.generateKeyPairSync("ec", { namedCurve: 5 })); +attempt("ec, lower-case NIST name", () => crypto.generateKeyPairSync("ec", { namedCurve: "p-256" })); +attempt("ec, unknown curve and paramEncoding", () => crypto.generateKeyPairSync("ec", { namedCurve: "nope", paramEncoding: "bogus" })); +attempt("ec, paramEncoding bogus", ec({ paramEncoding: "bogus" })); +attempt("ec, paramEncoding named", () => crypto.generateKeyPairSync("ec", { namedCurve: "P-256", paramEncoding: "named" })); +attempt("ec, paramEncoding null", () => crypto.generateKeyPairSync("ec", { namedCurve: "P-256", paramEncoding: null })); +attempt("ed25519, no options", () => crypto.generateKeyPairSync("ed25519")); +attempt("ed25519, null options", () => crypto.generateKeyPairSync("ed25519", null)); +attempt("ed25519, number options", () => crypto.generateKeyPairSync("ed25519", 5)); +attempt("ed25519, bad encoding", () => crypto.generateKeyPairSync("ed25519", { publicKeyEncoding: 5 })); + +// Encodings. +attempt("both encodings invalid", rsa({ publicKeyEncoding: { format: "x" }, privateKeyEncoding: 5 })); +attempt("array encoding", rsa({ publicKeyEncoding: [] })); +attempt("null encodings", () => crypto.generateKeyPairSync("ec", { namedCurve: "P-256", publicKeyEncoding: null, privateKeyEncoding: null })); +attempt("der without type", rsa({ publicKeyEncoding: { format: "der" } })); +attempt("pem without type", rsa({ privateKeyEncoding: { format: "pem" } })); +attempt("jwk without type", () => crypto.generateKeyPairSync("ec", { namedCurve: "P-256", publicKeyEncoding: { format: "jwk" }, privateKeyEncoding: { format: "jwk" } })); +attempt("jwk with bad type", rsa({ publicKeyEncoding: { format: "jwk", type: "bogus" } })); +attempt("jwk with pkcs1 for ec", ec({ publicKeyEncoding: { format: "jwk", type: "pkcs1" } })); +attempt("public sec1", rsa({ publicKeyEncoding: { format: "pem", type: "sec1" } })); +attempt("public pkcs8", rsa({ publicKeyEncoding: { format: "pem", type: "pkcs8" } })); +attempt("private spki", rsa({ privateKeyEncoding: { format: "pem", type: "spki" } })); +attempt("private sec1 for rsa", rsa({ privateKeyEncoding: { format: "pem", type: "sec1" } })); +attempt("public pkcs1 for ec", ec({ publicKeyEncoding: { format: "pem", type: "pkcs1" } })); +attempt("private pkcs1 for ec", ec({ privateKeyEncoding: { format: "der", type: "pkcs1" } })); +attempt("public pkcs1 for ed25519", () => crypto.generateKeyPairSync("ed25519", { publicKeyEncoding: { format: "pem", type: "pkcs1" } })); +attempt("private sec1 for ed25519", () => crypto.generateKeyPairSync("ed25519", { privateKeyEncoding: { format: "pem", type: "sec1" } })); +attempt("raw-public as private", ec({ privateKeyEncoding: { format: "raw-public" } })); +attempt("raw-private as public", ec({ publicKeyEncoding: { format: "raw-private" } })); +attempt("raw-seed as public", ec({ publicKeyEncoding: { format: "raw-seed" } })); +attempt("raw-public with bad type", ec({ publicKeyEncoding: { format: "raw-public", type: "x" } })); +attempt("raw-private with a type", ec({ privateKeyEncoding: { format: "raw-private", type: "pkcs8" } })); +attempt("raw-private with a passphrase", ec({ privateKeyEncoding: { format: "raw-private", passphrase: "x" } })); +attempt("raw-seed with a cipher", ec({ privateKeyEncoding: { format: "raw-seed", cipher: "aes-128-cbc" } })); +attempt("number cipher", rsa({ privateKeyEncoding: { format: "pem", type: "pkcs8", cipher: 5 } })); +attempt("passphrase without cipher", rsa({ privateKeyEncoding: { format: "pem", type: "pkcs8", passphrase: "x" } })); +attempt("passphrase with null cipher", rsa({ privateKeyEncoding: { format: "pem", type: "pkcs8", cipher: null, passphrase: "x" } })); +attempt("der pkcs1 with a cipher", rsa({ privateKeyEncoding: { format: "der", type: "pkcs1", cipher: "aes-128-cbc", passphrase: "x" } })); +attempt("der sec1 with a cipher", ec({ privateKeyEncoding: { format: "der", type: "sec1", cipher: "aes-128-cbc", passphrase: "x" } })); +attempt("number passphrase", rsa({ privateKeyEncoding: { format: "pem", type: "pkcs8", cipher: "aes-128-cbc", passphrase: 5 } })); +attempt("missing passphrase", rsa({ privateKeyEncoding: { format: "pem", type: "pkcs8", cipher: "aes-128-cbc" } })); +attempt("encoding errors before the modulusLength", () => crypto.generateKeyPairSync("rsa", { modulusLength: 1.5, privateKeyEncoding: { format: "pem", type: "pkcs8", passphrase: "x" } })); + +// generateKeyPair checks its callback first, reads a function in the +// options position as the callback, and throws argument errors. +attempt("async, options as the callback", () => crypto.generateKeyPair("rsa", { modulusLength: 1024 })); +attempt("async, undefined callback", () => crypto.generateKeyPair("rsa", { modulusLength: 1024 }, undefined)); +attempt("async, number callback", () => crypto.generateKeyPair("rsa", { modulusLength: 1024 }, 5)); +attempt("async, number type without callback", () => crypto.generateKeyPair(5, {})); +attempt("async, unknown type", () => crypto.generateKeyPair("foo", undefined, () => console.log("never"))); +attempt("async, ec without options", () => crypto.generateKeyPair("ec", () => console.log("never"))); +attempt("async, bad encoding", () => crypto.generateKeyPair("ec", { namedCurve: "P-256", publicKeyEncoding: { format: "pem", type: "pkcs1" } }, () => console.log("never"))); +attempt("async, shifted callback", () => + crypto.generateKeyPair("ed25519", (err, publicKey, privateKey) => { + console.log("shifted callback", err, publicKey.asymmetricKeyType, privateKey.type); + crypto.generateKeyPair( + "ed25519", + (err2, publicKey2) => { + console.log("options callback", err2, publicKey2.type); + // A callable Proxy is a function, in either position. + const proxied = new Proxy( + (err3, publicKey3) => { + console.log("proxy callback", err3, publicKey3.type); + crypto.generateKeyPair("ed25519", proxiedOptions); + }, + {}, + ); + const proxiedOptions = new Proxy((err4, publicKey4) => console.log("proxy options callback", err4, publicKey4.type), {}); + crypto.generateKeyPair("ed25519", {}, proxied); + }, + () => console.log("never"), + ); + }), +); +console.log("returned"); diff --git a/tests/corpus/7446-crypto-rsa-key-pair-errors.ts b/tests/corpus/7446-crypto-rsa-key-pair-errors.ts new file mode 100644 index 000000000..ca5416bcd --- /dev/null +++ b/tests/corpus/7446-crypto-rsa-key-pair-errors.ts @@ -0,0 +1,72 @@ +// RSA key generation parameters: publicExponent's validateUint32 checks +// and its 65537 default, and the errors OpenSSL reports for modulus +// lengths and exponents it does not generate, in OpenSSL's order. +// generateKeyPairSync throws those errors and generateKeyPair passes them +// to its callback, alone, in a later macrotask. +import * as crypto from "node:crypto"; + +type RsaOptions = { modulusLength: number; publicExponent?: unknown }; + +function attempt(label: string, options: RsaOptions): void { + try { + const { publicKey } = crypto.generateKeyPairSync("rsa", options as crypto.RSAKeyPairKeyObjectOptions); + const details = publicKey.asymmetricKeyDetails!; + console.log(label, details.modulusLength, details.publicExponent); + } catch (e) { + const error = e as NodeJS.ErrnoException; + console.log(label, "throws", error.name, error.code, error.message); + } +} + +attempt("exponent 1.5", { modulusLength: 1024, publicExponent: 1.5 }); +attempt("exponent NaN", { modulusLength: 1024, publicExponent: NaN }); +attempt("exponent Infinity", { modulusLength: 1024, publicExponent: Infinity }); +attempt("exponent -1", { modulusLength: 1024, publicExponent: -1 }); +attempt("exponent 2 ** 32", { modulusLength: 1024, publicExponent: 2 ** 32 }); +attempt("exponent string", { modulusLength: 1024, publicExponent: "3" }); +attempt("exponent bigint", { modulusLength: 1024, publicExponent: 3n }); +attempt("exponent null", { modulusLength: 1024, publicExponent: null }); +attempt("exponent undefined", { modulusLength: 1024, publicExponent: undefined }); +attempt("exponent 3", { modulusLength: 1024, publicExponent: 3 }); +attempt("exponent 17", { modulusLength: 1024, publicExponent: 17 }); +attempt("exponent 0", { modulusLength: 1024, publicExponent: 0 }); +attempt("exponent -0", { modulusLength: 1024, publicExponent: -0 }); +attempt("exponent 1", { modulusLength: 1024, publicExponent: 1 }); +attempt("exponent 2", { modulusLength: 1024, publicExponent: 2 }); +attempt("exponent 4", { modulusLength: 1024, publicExponent: 4 }); +attempt("exponent 2 ** 31", { modulusLength: 1024, publicExponent: 2 ** 31 }); +attempt("modulusLength 0", { modulusLength: 0 }); +attempt("modulusLength -0", { modulusLength: -0 }); +attempt("modulusLength 100", { modulusLength: 100 }); +attempt("modulusLength 511", { modulusLength: 511 }); +attempt("modulusLength 2 ** 31", { modulusLength: 2 ** 31 }); +attempt("modulusLength 2 ** 32 - 1", { modulusLength: 2 ** 32 - 1 }); +attempt("modulusLength 100, exponent 4", { modulusLength: 100, publicExponent: 4 }); +attempt("modulusLength 100, exponent 0", { modulusLength: 100, publicExponent: 0 }); +attempt("modulusLength 2 ** 31, exponent 0", { modulusLength: 2 ** 31, publicExponent: 0 }); +attempt("modulusLength 2 ** 31, exponent 4", { modulusLength: 2 ** 31, publicExponent: 4 }); + +function later(label: string, options: RsaOptions, next: () => void): void { + try { + crypto.generateKeyPair("rsa", options as crypto.RSAKeyPairKeyObjectOptions, (...args: unknown[]) => { + const error = args[0] as Error | null; + console.log(label, "callback", args.length, error === null ? null : [error.name, error.message]); + next(); + }); + console.log(label, "returned"); + } catch (e) { + console.log(label, "throws", (e as Error).message); + next(); + } +} + +later("async exponent 4", { modulusLength: 1024, publicExponent: 4 }, () => + later("async modulusLength 100", { modulusLength: 100 }, () => + later("async exponent 0", { modulusLength: 1024, publicExponent: 0 }, () => + later("async exponent 1.5", { modulusLength: 1024, publicExponent: 1.5 }, () => + later("async exponent 3", { modulusLength: 1024, publicExponent: 3 }, () => console.log("done")), + ), + ), + ), +); +process.nextTick(() => console.log("tick")); diff --git a/tests/corpus/7480-builtin-constructor-in.js b/tests/corpus/7480-builtin-constructor-in.js new file mode 100644 index 000000000..781a00105 --- /dev/null +++ b/tests/corpus/7480-builtin-constructor-in.js @@ -0,0 +1,9 @@ +// Feature probes on builtin constructor globals answer like Node. +const E = "captureStackTrace" in Error ? Error : null; +console.log(E === Error, "stackTraceLimit" in Error, "nope" in Error); +console.log("captureStackTrace" in TypeError, "keys" in Object, "fromAsync" in Array, "withResolvers" in Promise); +console.log("call" in Map, "hasOwnProperty" in Set, "groupBy" in Map, "groupBy" in Set, "now" in Date); +const capture = "captureStackTrace" in Error ? Error.captureStackTrace : () => {}; +const target = {}; +capture(target); +console.log(typeof target.stack); diff --git a/tests/corpus/7480-class-call-without-new.ts b/tests/corpus/7480-class-call-without-new.ts new file mode 100644 index 000000000..82b3b8452 --- /dev/null +++ b/tests/corpus/7480-class-call-without-new.ts @@ -0,0 +1,60 @@ +// Function.prototype.call and apply invoke a class constructor without +// `new`, which throws a TypeError naming the class after the arguments are +// evaluated. A declared static `call` answers instead. +class Base { + n = 1; +} +class Derived extends Base {} +class Pair { + left: string; + right: string; + constructor(left: string, right: string) { + this.left = left; + this.right = right; + } +} +class WithCall { + static call(value: number): number { + return value + 1; + } +} + +function report(label: string, run: () => unknown): void { + try { + run(); + console.log(label, "returned"); + } catch (error) { + console.log(label, error instanceof TypeError, (error as Error).message); + } +} + +const receiver = new Base(); +report("call", () => Base.call(receiver)); +report("apply", () => Base.apply(receiver, [])); +report("derived", () => Derived.call(new Derived())); +report("arguments", () => Pair.call(new Pair("a", "b"), "c", "d")); + +const order: string[] = []; +function effect(label: string, value: T): T { + order.push(label); + return value; +} +report("ordered", () => Pair.call(effect("this", new Pair("a", "b")), effect("left", "x"), effect("right", "y"))); +console.log(order.join(",")); + +function local(): void { + class Inner { + x = 2; + } + report("local", () => Inner.call(new Inner())); +} +local(); + +class Statics { + static run(): void { + report("static this", () => this.call(new Statics())); + } +} +Statics.run(); + +console.log(WithCall.call(4)); diff --git a/tests/corpus/7481-class-call-without-new.mjs b/tests/corpus/7481-class-call-without-new.mjs new file mode 100644 index 000000000..4734f0491 --- /dev/null +++ b/tests/corpus/7481-class-call-without-new.mjs @@ -0,0 +1,63 @@ +// An ES5 constructor that calls a class constructor as its base, and the +// other ways of calling a class without `new`, throw Node's TypeError. +class Base { + constructor() { + this.n = 1; + } +} + +function report(label, run) { + try { + run(); + console.log(label, "returned"); + } catch (error) { + console.log(label, error instanceof TypeError, error.message); + } +} + +function Sub() { + Base.call(this); + this.m = 2; +} +report("es5 subclass", () => new Sub()); + +function Applied() { + Base.apply(this, arguments); +} +report("es5 apply", () => new Applied(1, 2)); + +report("apply null", () => Base.apply(null, null)); +report("apply non-object", () => Base.apply(null, 5)); +report("apply array-like", () => Base.apply({}, { length: 1, 0: "a" })); + +const order = []; +function effect(label, value) { + order.push(label); + return value; +} +report("ordered", () => Base.call(effect("this", {}), effect("a", 1), effect("b", 2))); +console.log(order.join(",")); + +function viaValue(C) { + report("value " + C.name, () => C.call({})); +} +class Derived extends Base {} +viaValue(Base); +viaValue(Derived); + +class Configured {} +Configured.defaults = { size: 1 }; +function Wrapped() { + Configured.call(this, Configured.defaults); +} +report("expando statics", () => new Wrapped()); + +const Anonymous = class {}; +report("anonymous", () => Anonymous.call({})); + +class WithCall { + static call(value) { + return "own " + value; + } +} +console.log(WithCall.call(4)); diff --git a/tests/corpus/7481-class-runtime-heritage-object-error.js b/tests/corpus/7481-class-runtime-heritage-object-error.js new file mode 100644 index 000000000..32e17c640 --- /dev/null +++ b/tests/corpus/7481-class-runtime-heritage-object-error.js @@ -0,0 +1,51 @@ +function make(name, params) { + const Parent = (params && params.Parent) || Object; + class Definition extends Parent { + describe() { + return `${name}:${this.kind}`; + } + } + return Definition; +} +const A = make("A"); +const a = new A(); +a.kind = "plain"; +console.log(typeof a, a instanceof A, a.describe(), JSON.stringify(a), Object.keys(a)); +const E = make("E", { Parent: Error }); +const e = new E("boom"); +e.kind = "error"; +console.log(e instanceof E, e.message, typeof e.stack, e.describe(), Object.keys(e), String(e)); +const Parent = E; +console.log(e instanceof Parent, new E() .message === "", Object.prototype.hasOwnProperty.call(new E(), "message")); +try { + throw new E("thrown"); +} catch (err) { + console.log(err instanceof E, err.message); +} +console.log(e instanceof Error, new A() instanceof Error); +try { + throw new E("caught"); +} catch (err) { + console.log(err instanceof Error, err.message); +} +// The cause option is read like Error's: inherited and accessor causes +// count, and a throwing getter's exception propagates. +const inherited = new E("inherited", Object.create({ cause: "from prototype" })); +console.log(inherited.cause, Object.prototype.hasOwnProperty.call(inherited, "cause")); +let reads = 0; +const accessor = new E("accessor", { + get cause() { + reads++; + return "from getter"; + }, +}); +console.log(accessor.cause, reads, "cause" in new E("none", {}), "cause" in new E("undef", { cause: undefined })); +try { + new E("throws", { + get cause() { + throw new Error("getter failed"); + }, + }); +} catch (err) { + console.log(err.message); +} diff --git a/tests/corpus/7482-define-property-function-class-values.js b/tests/corpus/7482-define-property-function-class-values.js new file mode 100644 index 000000000..7a682adab --- /dev/null +++ b/tests/corpus/7482-define-property-function-class-values.js @@ -0,0 +1,35 @@ +const desc = { value: undefined, enumerable: false }; +function factory(name) { + class Definition {} + Object.defineProperty(Definition, "name", { value: name }); + function init(inst, def) { + if (!inst._meta) { + desc.value = { def, traits: new Set() }; + try { + Object.defineProperty(inst, "_meta", desc); + } finally { + desc.value = undefined; + } + } + inst._meta.traits.add(name); + } + function _(def) { + const inst = this; + init(inst, def); + return inst; + } + Object.defineProperty(_, "init", { value: init }); + Object.defineProperty(_, Symbol.hasInstance, { + value: (inst) => !!(inst && inst._meta && inst._meta.traits.has(name)), + }); + Object.defineProperty(_, "name", { value: name }); + return { ctor: _, Definition }; +} +const { ctor: Thing, Definition } = factory("Thing"); +console.log(Definition.name, Thing.name, typeof Thing.init); +const t = new Thing({ a: 1 }); +console.log(Object.keys(t), JSON.stringify(t), t._meta.def.a, [...t._meta.traits]); +console.log(t instanceof Thing, {} instanceof Thing); +const plain = {}; +Thing.init(plain, { b: 2 }); +console.log(plain instanceof Thing, Object.keys(plain), plain._meta.def.b); diff --git a/tests/corpus/7483-constructor-factory-traits.js b/tests/corpus/7483-constructor-factory-traits.js new file mode 100644 index 000000000..e022f0672 --- /dev/null +++ b/tests/corpus/7483-constructor-factory-traits.js @@ -0,0 +1,58 @@ +const _desc = { value: undefined, enumerable: false }; +function $constructor(name, initializer, params) { + const proto = {}; + function Internals(def) { + this.def = def; + this.constr = _; + this.traits = new Set(); + } + Internals.prototype = proto; + function init(inst, def) { + if (!inst._zod) { + _desc.value = new Internals(def); + try { + Object.defineProperty(inst, "_zod", _desc); + } finally { + _desc.value = undefined; + } + } else if (inst._zod.traits.has(name)) { + return; + } + inst._zod.traits.add(name); + initializer(inst, def); + } + const Parent = params?.Parent ?? Object; + class Definition extends Parent {} + Object.defineProperty(Definition, "name", { value: name }); + function _(def) { + const inst = params?.Parent ? new Definition() : this; + init(inst, def); + return inst; + } + Object.defineProperty(_, "init", { value: init }); + Object.defineProperty(_, Symbol.hasInstance, { + value: (inst) => { + if (params?.Parent && inst instanceof params.Parent) return true; + return inst?._zod?.traits?.has(name); + }, + }); + Object.defineProperty(_, "name", { value: name }); + return _; +} +const $Base = $constructor("$Base", (inst, def) => { + inst.kind = def.kind; + inst._zod.parse = (v) => (typeof v === def.kind ? { value: v } : { issues: [`expected ${def.kind}`] }); +}); +const $Str = $constructor("$Str", (inst, def) => { + $Base.init(inst, def); + inst.min = def.min; +}); +const $Err = $constructor("$Err", (inst, def) => { + inst.issues = def; + inst.message = JSON.stringify(def); +}, { Parent: Error }); +const s = new $Str({ kind: "string", min: 2 }); +console.log(s.kind, s.min, [...s._zod.traits], s instanceof $Str, s instanceof $Base, Object.keys(s)); +console.log(JSON.stringify(s._zod.parse("ab")), JSON.stringify(s._zod.parse(3))); +const e = new $Err(["bad"]); +console.log(e instanceof $Err, e.issues, e.message, $Str.name, $Err.name); diff --git a/tests/corpus/7484-hoisted-function-captures-later-class.js b/tests/corpus/7484-hoisted-function-captures-later-class.js new file mode 100644 index 000000000..2272882af --- /dev/null +++ b/tests/corpus/7484-hoisted-function-captures-later-class.js @@ -0,0 +1,23 @@ +function f(params) { + function early() { + return make; + } + const Parent = params?.Parent ?? Object; + class D extends Parent {} + function make() { + return new D(); + } + return early()(); +} +console.log(typeof f(), typeof f({ Parent: Error })); +function g() { + function early() { + return make; + } + class E {} + function make() { + return new E(); + } + return early()(); +} +console.log(typeof g()); diff --git a/tests/corpus/7485-global-object-own-properties.js b/tests/corpus/7485-global-object-own-properties.js new file mode 100644 index 000000000..a9a72be18 --- /dev/null +++ b/tests/corpus/7485-global-object-own-properties.js @@ -0,0 +1,22 @@ +var _a; +globalThis.__direct = { n: 1 }; +console.log(typeof globalThis.__direct, globalThis.__direct.n); + +(_a = globalThis).__alias ?? (_a.__alias = { n: 2 }); +console.log(typeof globalThis.__alias); +const g = globalThis; +g.__viaConst = { n: 3 }; +console.log(typeof globalThis.__viaConst); +globalThis.__nullish ??= { n: 4 }; +console.log(typeof globalThis.__nullish); + +(_a = globalThis).__demo_config ?? (_a.__demo_config = {}); +const globalConfig = globalThis.__demo_config; +function config(next) { + if (next) Object.assign(globalConfig, next); + return globalConfig; +} +config({ localeError: (issue) => `bad ${issue}` }); +console.log(typeof globalConfig, typeof globalConfig.localeError, config().localeError("x")); +const pp = globalThis.__demo_config?.postProcessor; +console.log(pp === undefined, globalThis.__demo_config === globalConfig); diff --git a/tests/corpus/7486-prototype-accessor-string-method-names.js b/tests/corpus/7486-prototype-accessor-string-method-names.js new file mode 100644 index 000000000..9920717ea --- /dev/null +++ b/tests/corpus/7486-prototype-accessor-string-method-names.js @@ -0,0 +1,53 @@ +function own(inst, key, value, enumerable = true) { + Object.defineProperty(inst, key, { configurable: true, writable: true, enumerable, value }); + return value; +} +function defineBound(proto, key, fn) { + Object.defineProperty(proto, key, { + configurable: true, + get() { + return this == null ? fn : own(this, key, fn.bind(this)); + }, + set(value) { + own(this, key, value); + }, + }); +} +function members(proto, table) { + for (const key in table) { + const desc = Object.getOwnPropertyDescriptor(table, key); + if (desc.get) Object.defineProperty(proto, key, { ...desc, enumerable: false }); + else defineBound(proto, key, desc.value); + } +} +function Schema(def) { + this.def = def; +} +let installed = false; +function make(def) { + const inst = new Schema(def); + if (!installed) { + installed = true; + members(Object.getPrototypeOf(inst), { + min(n) { return make({ ...this.def, min: n }); }, + max(n) { return make({ ...this.def, max: n }); }, + trim() { return make({ ...this.def, trim: true }); }, + get size() { return Object.keys(this.def).length; }, + }); + } + return inst; +} +const s = make({ kind: "string" }).min(1).max(200).trim(); +console.log(JSON.stringify(s.def), s.size, Object.keys(s), typeof s.trim); +const detached = make({ kind: "x" }).max; +console.log(JSON.stringify(detached(3).def)); +const proto2 = {}; +Object.defineProperty(proto2, "aaa", { configurable: true, get() { return () => "custom-aaa"; } }); +Object.defineProperty(proto2, "trim", { configurable: true, get() { return () => "custom-trim"; } }); +Object.defineProperty(proto2, "trimx", { configurable: true, get() { return () => "custom-trimx"; } }); +Object.defineProperty(proto2, "slice", { configurable: true, get() { return () => "custom-slice"; } }); +const o2 = Object.create(proto2); +console.log(o2.aaa()); +console.log(o2.trimx()); +console.log(o2.slice()); +console.log(o2.trim()); diff --git a/tests/corpus/7487-namespace-spread-untyped/all.js b/tests/corpus/7487-namespace-spread-untyped/all.js new file mode 100644 index 000000000..e317afdab --- /dev/null +++ b/tests/corpus/7487-namespace-spread-untyped/all.js @@ -0,0 +1,2 @@ +export * from "./shapes.js"; +export const own = "own"; diff --git a/tests/corpus/7487-namespace-spread-untyped/extra.js b/tests/corpus/7487-namespace-spread-untyped/extra.js new file mode 100644 index 000000000..8c62c2dd0 --- /dev/null +++ b/tests/corpus/7487-namespace-spread-untyped/extra.js @@ -0,0 +1,2 @@ +export const tag = (s) => `<${s}>`; +export function zeta() { return "z"; } diff --git a/tests/corpus/7487-namespace-spread-untyped/main.js b/tests/corpus/7487-namespace-spread-untyped/main.js new file mode 100644 index 000000000..2352e844d --- /dev/null +++ b/tests/corpus/7487-namespace-spread-untyped/main.js @@ -0,0 +1,16 @@ +import * as shapes from "./shapes.js"; +import * as extra from "./extra.js"; +// A namespace spread into an untyped object (zod's local +// `const z = { ..._schemas, ..._checks, iso: _iso }`). +function untyped(value) { + return value; +} +const z = untyped({ ...shapes, ...extra, extra }); +console.log(Object.keys(z).join(",")); +for (const key of ["circle", "square"]) console.log(key, z[key](2).area.toFixed(2)); +console.log(z.VERSION, typeof z.extra.tag, z.extra.tag("x"), new z.Box(4).size); +// A namespace with star re-exports holds the re-exported members too. +import * as all from "./all.js"; +const w = untyped({ ...all }); +console.log(Object.keys(w).join(","), w.circle(1).area.toFixed(2), w.own, new w.Box(2).size); +for (const key of ["circle", "own", "VERSION", "missing"]) console.log(key, typeof all[key]); diff --git a/tests/corpus/7487-namespace-spread-untyped/shapes.js b/tests/corpus/7487-namespace-spread-untyped/shapes.js new file mode 100644 index 000000000..a8b5cbfc2 --- /dev/null +++ b/tests/corpus/7487-namespace-spread-untyped/shapes.js @@ -0,0 +1,4 @@ +export const VERSION = "1.2"; +export function circle(r) { return { kind: "circle", area: Math.PI * r * r }; } +export function square(s) { return { kind: "square", area: s * s }; } +export class Box { constructor(size) { this.size = size; } } diff --git a/tests/corpus/7488-math-spread-untyped.js b/tests/corpus/7488-math-spread-untyped.js new file mode 100644 index 000000000..028135d41 --- /dev/null +++ b/tests/corpus/7488-math-spread-untyped.js @@ -0,0 +1,69 @@ +// Math.max/Math.min spreading an untyped iterable (zod's +// `Math.min(...caps.map((c) => c[0]))`). +function caps(list) { + return list.map((c) => c[0]); +} +console.log(Math.min(...caps([[5], [2], [9]])), Math.max(...caps([[5], [2], [9]]))); +console.log(Math.min(...caps([])), Math.max(...caps([]))); +console.log(Math.min(...caps([["3"], [true], [null]])), Math.max(...caps([["7"], [undefined]]))); +console.log(Object.is(Math.max(...caps([[-0], [0]])), 0), Object.is(Math.min(...caps([[0], [-0]])), -0)); +const order = []; +const valued = (n) => ({ valueOf() { order.push(n); return n; } }); +console.log(Math.max(...caps([[valued(1)], [NaN], [valued(3)]])), order.join(",")); +try { + Math.min(...caps([[{ valueOf() { throw new Error("boom"); } }]])); +} catch (e) { + console.log(e.message); +} +const sources = { none: undefined, nil: null, num: 5, obj: {}, str: "48" }; +function pick(k) { + return sources[k]; +} +for (const k of Object.keys(sources)) { + try { + console.log(Math.max(...sources[k])); + } catch (e) { + console.log(e.constructor.name, e.message); + } + try { + console.log(Math.min(...pick(k))); + } catch (e) { + console.log(e.constructor.name, e.message); + } +} + +// Iterables that step through the iterator protocol, reached through an +// untyped lookup: a generator, an object with a Symbol.iterator method, a +// Set, a Map's keys() and an iterator that ends early. +function* generate(values) { + for (const value of values) yield value; +} +const makers = { + generator: () => generate([4, "1", 7]), + custom: () => ({ + *[Symbol.iterator]() { + yield 10; + yield -2; + }, + }), + set: () => new Set([3, 8, 3]), + keys: () => new Map([[2, "a"], [6, "b"]]).keys(), + empty: () => generate([]), +}; +for (const k of Object.keys(makers)) { + console.log(k, Math.min(...makers[k]()), Math.max(...makers[k]())); +} +const stepped = []; +const counting = { + [Symbol.iterator]() { + let n = 0; + return { + next() { + stepped.push(n); + return n < 3 ? { value: n++, done: false } : { value: undefined, done: true }; + }, + }; + }, +}; +const lookup = { counting }; +console.log(Math.max(...lookup.counting), stepped.join(",")); diff --git a/tests/corpus/7489-math-spread-generator-only.js b/tests/corpus/7489-math-spread-generator-only.js new file mode 100644 index 000000000..168057aac --- /dev/null +++ b/tests/corpus/7489-math-spread-generator-only.js @@ -0,0 +1,8 @@ +// Math.max and Math.min as the only consumers of an untyped generator: the +// spread steps the generator, whose values fold with ToNumber as in Node. +function* generate(limit) { + for (let i = 1; i <= limit; i++) yield i % 2 === 0 ? String(i * 3) : i; +} +const lookup = { generate }; +console.log(Math.max(...lookup.generate(4)), Math.min(...lookup.generate(4))); +console.log(Math.max(...lookup.generate(0)), Math.min(...lookup.generate(1))); diff --git a/tests/corpus/7540-undefined-element-index-keys.ts b/tests/corpus/7540-undefined-element-index-keys.ts new file mode 100644 index 000000000..8da1b591b --- /dev/null +++ b/tests/corpus/7540-undefined-element-index-keys.ts @@ -0,0 +1,53 @@ +// An element index the checker types as a number can be undefined at run +// time (an element read from a holey array). JavaScript then names the +// array's "undefined" property, not "NaN": plain, compound, update and +// destructuring writes store it, a write used as a value yields the value, +// and reads (number, string, record and array elements) read it back. +const holey: number[] = [1]; +holey[3] = 3; + +const values: number[] = [0, 0, 0, 0]; +values[NaN] = 10; +for (const i of holey) values[i] = 7; +console.log(values[NaN], values.join(",")); +let total = 0; +for (const i of holey) total += values[i]!; +console.log(total); + +const counts: number[] = [0, 0, 0, 0]; +counts[NaN] = 10; +let trace = ""; +for (const i of holey) trace += counts[i]++ + ","; +for (const i of holey) counts[i] += 2; +for (const i of holey) trace += --counts[i] + ";"; +console.log(counts[NaN], counts.join(","), trace); + +let last = 0; +for (const i of holey) last = values[i] = 5; +for (const i of holey) [values[i]] = [8]; +console.log(last, values[NaN], values.join(",")); + +const names: string[] = ["a", "b", "c", "d"]; +for (const i of holey) names[i] += "!"; +const seen: (string | undefined)[] = []; +for (const i of holey) seen.push(names[i]); +console.log(names.join("|"), seen.join("|")); + +interface Point { + x: number; +} +const points: Point[] = [{ x: 0 }, { x: 1 }]; +const shared: Point = { x: 9 }; +let same = ""; +for (const i of holey) { + points[i] = shared; + same += String(points[i] === shared) + " "; +} +shared.x = 10; +for (const i of holey) same += String(points[i]?.x) + " "; +console.log(same); + +const fresh: number[] = [1, 2]; +let missing = ""; +for (const i of holey) missing += String(fresh[i]) + " "; +console.log(missing); diff --git a/tests/corpus/7541-prototype-identity-stand-ins.ts b/tests/corpus/7541-prototype-identity-stand-ins.ts new file mode 100644 index 000000000..81b1e8c71 --- /dev/null +++ b/tests/corpus/7541-prototype-identity-stand-ins.ts @@ -0,0 +1,22 @@ +// Instances held as unknown, of a class whose chain has no prototype data +// (a subclass without methods, or one extending Error), still share one +// prototype: comparing the prototypes of two instances of the same class is +// true, and of instances of different classes is false. +class Base { + tag = "b"; +} +class Sub extends Base {} +class Other extends Base {} +class Oops extends Error {} +const bases: Base[] = [new Sub(), new Sub(), new Other()]; +const errors: Error[] = [new Oops("a"), new Oops("b")]; +const first: unknown = bases[0]; +const second: unknown = bases[1]; +const other: unknown = bases[2]; +const e1: unknown = errors[0]; +const e2: unknown = errors[1]; +console.log(Object.getPrototypeOf(first) === Object.getPrototypeOf(second)); +console.log(Object.getPrototypeOf(first) === Object.getPrototypeOf(other)); +console.log(Object.getPrototypeOf(e1) === Object.getPrototypeOf(e2)); +console.log(Object.getPrototypeOf(first) === Object.getPrototypeOf(e1)); +console.log(first instanceof Sub, other instanceof Sub, e1 instanceof Oops, e1 instanceof Error); diff --git a/tests/corpus/7542-prototype-identity-without-data.ts b/tests/corpus/7542-prototype-identity-without-data.ts new file mode 100644 index 000000000..9ac1209f8 --- /dev/null +++ b/tests/corpus/7542-prototype-identity-without-data.ts @@ -0,0 +1,19 @@ +// Instances held as unknown, of classes whose chains have no prototype data +// a program can name (no methods, or a subclass of Error): each class's +// instances share one prototype, and different classes' differ, as in Node. +class Base { + tag = "b"; +} +class Sub extends Base {} +class Other extends Base {} +class Oops extends Error {} +const bases: Base[] = [new Sub(), new Sub(), new Other(), new Base()]; +const errors: Error[] = [new Oops("a"), new Oops("b")]; +const [a, b, c, d] = bases.map((value): unknown => value); +const e1: unknown = errors[0]; +const e2: unknown = errors[1]; +console.log(Object.getPrototypeOf(a) === Object.getPrototypeOf(b)); +console.log(Object.getPrototypeOf(a) === Object.getPrototypeOf(c)); +console.log(Object.getPrototypeOf(a) === Object.getPrototypeOf(d)); +console.log(Object.getPrototypeOf(e1) === Object.getPrototypeOf(e2)); +console.log(Object.getPrototypeOf(a) === Object.getPrototypeOf(e1)); diff --git a/tests/corpus/7544-local-class-prototype-identity.ts b/tests/corpus/7544-local-class-prototype-identity.ts new file mode 100644 index 000000000..fc465f0ff --- /dev/null +++ b/tests/corpus/7544-local-class-prototype-identity.ts @@ -0,0 +1,23 @@ +// A function-local class is a new class at each evaluation of its +// declaration: instances from separate calls of the factory have different +// prototypes, while instances from one call share theirs. +function make() { + class Local { + tag = "l"; + } + return new Local(); +} +const first: unknown = make(); +const second: unknown = make(); +console.log(Object.getPrototypeOf(first) === Object.getPrototypeOf(second)); +console.log(Object.getPrototypeOf(first) === Object.getPrototypeOf(first)); +function pair() { + class Twin { + n = 1; + } + return [new Twin(), new Twin()]; +} +const [t1, t2] = pair().map((value): unknown => value); +const [t3] = pair().map((value): unknown => value); +console.log(Object.getPrototypeOf(t1) === Object.getPrototypeOf(t2)); +console.log(Object.getPrototypeOf(t1) === Object.getPrototypeOf(t3)); diff --git a/tests/corpus/7560-new-target-constructors.ts b/tests/corpus/7560-new-target-constructors.ts new file mode 100644 index 000000000..9e94200bf --- /dev/null +++ b/tests/corpus/7560-new-target-constructors.ts @@ -0,0 +1,55 @@ +// new.target in class constructors: the class under construction, through +// inherited and explicit subclass constructors, Error subclasses, arrows, +// identity comparisons and class values. +abstract class AppError extends Error { + abstract readonly status: number; + constructor(message: string) { + super(message); + this.name = new.target.name; + } +} +class NotFound extends AppError { + readonly status = 404; +} +class Invalid extends AppError { + readonly status = 400; + readonly field: string; + constructor(field: string, detail: number) { + super(`bad ${field} ${detail}`); + this.field = field; + } +} +class Strict extends Invalid {} +const errors: AppError[] = [new NotFound("missing"), new Invalid("isbn", 3), new Strict("title", 1)]; +for (const e of errors) console.log(e.name, e.status, e.message, String(e), e instanceof Invalid); + +class Shape { + readonly kind: string; + readonly exact: boolean; + constructor() { + this.kind = new.target.name; + this.exact = new.target === Shape; + const probe = () => new.target !== Circle; + console.log("probe", probe(), new.target == Square, new.target === undefined); + } + describe(): string { + return `${this.kind}${this.exact ? " (exact)" : ""}`; + } +} +class Circle extends Shape {} +class Square extends Shape { + constructor() { + super(); + console.log("square ctor sees", new.target.name); + } +} +for (const s of [new Shape(), new Circle(), new Square()]) console.log(s.describe()); + +class Node2 { + static make(): Node2 { return new this(); } + readonly origin: typeof Node2; + constructor() { this.origin = new.target; } + where(): string | undefined { return typeof (() => 1)() === "number" ? String(this.origin.name) : undefined; } +} +class Leaf extends Node2 {} +console.log(new Node2().origin === Node2, new Leaf().origin === Leaf, new Leaf().where(), Node2.make().origin.name); diff --git a/tests/corpus/7561-new-target-generic-registry.ts b/tests/corpus/7561-new-target-generic-registry.ts new file mode 100644 index 000000000..17866d143 --- /dev/null +++ b/tests/corpus/7561-new-target-generic-registry.ts @@ -0,0 +1,32 @@ +// new.target in generic classes and as a construction registry key. +class Box { + readonly label: string; + readonly value: T; + constructor(value: T) { + this.value = value; + this.label = `${new.target.name}<${typeof value}>`; + } +} +class NumBox extends Box { + constructor(n: number) { + super(n * 2); + } +} +const a = new Box("s"); +const b = new NumBox(4); +console.log(a.label, b.label, b.value); + +class Registry { + static readonly created: string[] = []; + constructor() { + Registry.created.push(new.target.name); + if (new.target === Registry) console.log("plain registry"); + } +} +class Users extends Registry {} +class Books extends Registry {} +new Users(); +new Books(); +new Registry(); +new Books(); +console.log(Registry.created.join(",")); diff --git a/tests/corpus/7565-iterable-replaceable-method-results.ts b/tests/corpus/7565-iterable-replaceable-method-results.ts new file mode 100644 index 000000000..4e71a0dcc --- /dev/null +++ b/tests/corpus/7565-iterable-replaceable-method-results.ts @@ -0,0 +1,76 @@ +// A method returning Iterable whose name an untyped write can replace +// (`holder.candidates = ...` through `any`) is called through the +// instance's property lookup: the native method runs while the slot is +// unchanged, and a replacement installed on an instance runs instead, +// its result iterated as an Iterable. + +interface Lookup { + readonly index: string; + readonly key: string; +} +interface Query { + readonly lookups?: readonly Lookup[]; +} +interface Row { + id: string; + n: number; +} + +class Repo { + private readonly rows = new Map(); + add(row: T): void { + this.rows.set(row.id, row); + } + candidates(lookups: readonly Lookup[]): Iterable { + if (lookups.length === 0) return this.rows.values(); + const out: T[] = []; + for (const row of this.rows.values()) if (lookups.every((l) => l.key === row.id)) out.push(row); + return out; + } + query(query: Query): T[] { + const matches: T[] = []; + for (const row of this.candidates(query.lookups ?? [])) matches.push(row); + return matches; + } + ids(query: Query): string { + let out = ""; + for (const row of this.candidates(query.lookups ?? [])) out += (out ? "," : "") + row.id; + return out; + } +} + +function seed(): Repo { + const r = new Repo(); + r.add({ id: "a", n: 1 }); + r.add({ id: "b", n: 2 }); + r.add({ id: "c", n: 3 }); + return r; +} + +const r = seed(); +console.log(r.query({}).length, r.query({ lookups: [{ index: "i", key: "b" }] })); +console.log(r.ids({}), r.ids({ lookups: [{ index: "i", key: "c" }] })); + +// An untyped write of the same name: on an unrelated object first, then +// on one Repo instance, whose own property shadows the class method. +const holder: any = {}; +holder.candidates = (): Row[] => []; +console.log(typeof holder.candidates); + +const replaced = seed(); +const untyped: any = replaced; +untyped.candidates = function (lookups: readonly Lookup[]): Row[] { + return [{ id: "z" + lookups.length, n: 9 }]; +}; +console.log(replaced.query({}), replaced.query({ lookups: [{ index: "i", key: "a" }] })); +console.log(replaced.ids({})); + +// A generator replacement is single-pass, as in JavaScript. +untyped.candidates = function* (): Generator { + yield { id: "g1", n: 10 }; + yield { id: "g2", n: 11 }; +}; +console.log(replaced.query({}).map((row) => row.id + ":" + row.n)); + +// The untouched instance still runs the class method. +console.log(r.ids({})); diff --git a/tests/corpus/7600-navigator-optional-chain.js b/tests/corpus/7600-navigator-optional-chain.js new file mode 100644 index 000000000..5c1dac442 --- /dev/null +++ b/tests/corpus/7600-navigator-optional-chain.js @@ -0,0 +1,7 @@ +// Node's navigator global read through optional chains (zod 4's +// `navigator?.userAgent?.includes("Cloudflare")` probe): navigator is +// never nullish, so each chain answers like the plain member read. +const cloudflare = typeof navigator !== "undefined" && navigator?.userAgent?.includes("Cloudflare"); +console.log(cloudflare); +console.log(navigator?.userAgent === navigator.userAgent, navigator?.userAgent?.startsWith("Node.js/")); +console.log(navigator?.userAgent?.split("/")[0] ?? "none"); diff --git a/tests/corpus/7602-error-stack-limit-value.js b/tests/corpus/7602-error-stack-limit-value.js new file mode 100644 index 000000000..4b3f8cc7e --- /dev/null +++ b/tests/corpus/7602-error-stack-limit-value.js @@ -0,0 +1,18 @@ +// Error.stackTraceLimit through the Error constructor held as a value +// (zod 4's `const E = "captureStackTrace" in Error ? Error : null`): reads +// and writes reach the same limit as the static member. +const E = "captureStackTrace" in Error ? Error : null; +console.log(typeof E, E === Error, E.stackTraceLimit, Error.stackTraceLimit); +const saved = E.stackTraceLimit; +E.stackTraceLimit = 0; +console.log(Error.stackTraceLimit, E.stackTraceLimit, typeof saved === "number"); +console.log(new Error("no frames").stack); +try { + E.stackTraceLimit = 3; + console.log(Error.stackTraceLimit); +} finally { + E.stackTraceLimit = saved; +} +console.log(E.stackTraceLimit, Error.stackTraceLimit); +Error.stackTraceLimit = 4; +console.log(E.stackTraceLimit); diff --git a/tests/corpus/7603-error-stack-limit-own.js b/tests/corpus/7603-error-stack-limit-own.js new file mode 100644 index 000000000..dfd4b7cdf --- /dev/null +++ b/tests/corpus/7603-error-stack-limit-own.js @@ -0,0 +1,13 @@ +// An own stackTraceLimit on the Error constructor held as a value (a +// non-number assigned, or one defined with Object.defineProperty) answers +// reads through the value, and writes to it keep its descriptor. +const E = /** @type {any} */ (Error); +E.stackTraceLimit = "deep"; +console.log(E.stackTraceLimit, typeof E.stackTraceLimit); +E.stackTraceLimit = 2; +console.log(E.stackTraceLimit, Error.stackTraceLimit); +Object.defineProperty(E, "stackTraceLimit", { value: 6, writable: false, configurable: true }); +try { + E.stackTraceLimit = 1; +} catch {} +console.log(E.stackTraceLimit); diff --git a/tests/corpus/7604-error-stack-limit-frozen.js b/tests/corpus/7604-error-stack-limit-frozen.js new file mode 100644 index 000000000..9c73d1403 --- /dev/null +++ b/tests/corpus/7604-error-stack-limit-frozen.js @@ -0,0 +1,9 @@ +// A frozen Error keeps stackTraceLimit read-only through the constructor +// held as a value. +const E = /** @type {any} */ (Error); +E.stackTraceLimit = 3; +Object.freeze(E); +try { + E.stackTraceLimit = 1; +} catch {} +console.log(E.stackTraceLimit, Error.stackTraceLimit); diff --git a/tests/corpus/7610-key-order-json.ts b/tests/corpus/7610-key-order-json.ts new file mode 100644 index 000000000..0cd3dff61 --- /dev/null +++ b/tests/corpus/7610-key-order-json.ts @@ -0,0 +1,31 @@ +// JSON.stringify of object literals that write the same keys in different +// orders: each value serializes in the order its literal wrote, as in Node. +type Point = { x: number; y: number; label?: string }; + +const a = { id: 1, name: "a", ok: true }; +const b = { name: "b", ok: false, id: 2 }; +const c = { ok: true, id: 3, name: "c" }; +for (const value of [a, b, c]) console.log(JSON.stringify(value)); +console.log(JSON.stringify([a, b, c])); +console.log(JSON.stringify({ first: b, rest: [c, a] }, null, 2)); + +// Optional keys the literal leaves out, and writes to keys it has. +const p: Point = { y: 2, x: 1 }; +const q: Point = { label: "q", x: 3, y: 4 }; +const r: Point = { x: 5, y: 6, label: "r" }; +q.x = 30; +r.label = "r2"; +for (const point of [p, q, r]) console.log(JSON.stringify(point)); + +// A function typed by one literal's shape receiving the others. +function describe(value: { id: number; name: string; ok: boolean }): string { + return JSON.stringify(value); +} +console.log(describe(c), describe(a)); + +// JSON.parse into the same record type keeps each text's own key order. +type Entry = { id: number; name: string; ok: boolean }; +for (const text of ['{"ok":true,"name":"p","id":7}', '{"id":8,"name":"q","ok":false}']) { + const parsed = JSON.parse(text) as Entry; + console.log(JSON.stringify(parsed), Object.keys(parsed).join(","), describe(parsed)); +} diff --git a/tests/corpus/7611-error-message-accessor.ts b/tests/corpus/7611-error-message-accessor.ts new file mode 100644 index 000000000..c4528e488 --- /dev/null +++ b/tests/corpus/7611-error-message-accessor.ts @@ -0,0 +1,43 @@ +// An accessor defined over an Error subclass instance's own `message` +// (zod 4's lazy error message): typed reads and writes, String(), and +// reads through an untyped alias all go through the accessor, and a later +// data definition replaces it. +class E extends Error {} +const e = new E("orig"); +let store = "lazy"; +let reads = 0; +Object.defineProperty(e, "message", { + get() { + reads++; + return store; + }, + set(value: string) { + store = `set:${value}`; + }, + enumerable: true, + configurable: true, +}); +console.log(e.message, String(e), reads); +e.message = "typed"; +console.log(e.message, store); +const loose: any = e; +console.log(loose.message); +loose.message = "loose"; +console.log(e.message, Object.keys(e).join()); +const desc = Object.getOwnPropertyDescriptor(e, "message")!; +console.log(typeof desc.get, typeof desc.set, desc.enumerable); +Object.defineProperty(e, "message", { value: "data", writable: true, configurable: true }); +console.log(e.message, String(e), reads); +e.message = "after"; +console.log(e.message, store); + +class Named extends Error {} +const n = new Named("m"); +const loosen: any = n; +Object.defineProperty(loosen, "name", { get() { return "Custom"; }, configurable: true }); +console.log(n.name, String(n)); + +class Plain extends Error {} +const p = new Plain("kept"); +p.message = "changed"; +console.log(p.message, String(p)); diff --git a/tests/corpus/7611-key-order-keys-values-entries.ts b/tests/corpus/7611-key-order-keys-values-entries.ts new file mode 100644 index 000000000..29f725c24 --- /dev/null +++ b/tests/corpus/7611-key-order-keys-values-entries.ts @@ -0,0 +1,19 @@ +// Object.keys, Object.values, Object.entries and Object.fromEntries over +// object literals that write the same keys in different orders. +const a = { id: 1, name: "a", ok: true }; +const b = { name: "b", ok: false, id: 2 }; +const c = { ok: true, id: 3, name: "c" }; +for (const value of [a, b, c]) { + console.log(Object.keys(value).join(",")); + console.log(Object.values(value).join(",")); + console.log(Object.entries(value).map(([key, item]) => `${key}=${String(item)}`).join(" ")); + console.log(JSON.stringify(Object.fromEntries(Object.entries(value)))); +} + +type Row = { year: number; title?: string; score: number }; +const rows: Row[] = [ + { score: 1, year: 2001 }, + { title: "t", year: 2002, score: 2 }, + { year: 2003, score: 3, title: "u" }, +]; +for (const row of rows) console.log(Object.keys(row).join(","), Object.values(row).length); diff --git a/tests/corpus/7612-error-message-accessor-lazy.js b/tests/corpus/7612-error-message-accessor-lazy.js new file mode 100644 index 000000000..26d54c09c --- /dev/null +++ b/tests/corpus/7612-error-message-accessor-lazy.js @@ -0,0 +1,36 @@ +// zod 4's lazy error message: an initializer defines a shared accessor +// descriptor over `message` on an instance whose class extends a runtime +// Error parent, and the message is computed on first read and cached. +function _getMessage() { + const internals = this._zod; + internals.message ?? (internals.message = JSON.stringify(internals.def)); + return internals.message; +} +function _setMessage(value) { + this._zod.message = value; +} +const _messageDesc = { get: _getMessage, set: _setMessage, enumerable: true, configurable: true }; +function initializer(inst, def) { + Object.defineProperty(inst, "_zod", { value: { def }, enumerable: false }); + inst.name = "$ZodError"; + Object.defineProperty(inst, "message", _messageDesc); +} +function make(Parent, def) { + class Definition extends Parent {} + const inst = new Definition(); + initializer(inst, def); + return inst; +} +const err = make(Error, [{ code: "too_small", path: ["a"] }]); +console.log(err._zod.message, err.message, err._zod.message === err.message); +console.log(String(err), Object.keys(err).join()); +err.message = "replaced"; +console.log(err.message, err._zod.message, `${err}`); +try { + throw err; +} catch (caught) { + console.log(caught === err, caught.message, caught instanceof Error); +} +const plain = new Error("start"); +Object.defineProperty(plain, "message", { get() { return "computed"; }, configurable: true }); +console.log(plain.message, String(plain)); diff --git a/tests/corpus/7612-key-order-for-in.ts b/tests/corpus/7612-key-order-for-in.ts new file mode 100644 index 000000000..89ed16edb --- /dev/null +++ b/tests/corpus/7612-key-order-for-in.ts @@ -0,0 +1,15 @@ +// for-in over object literals that write the same keys in different +// orders, and over values a function received. +const a = { id: 1, name: "a", ok: true }; +const b = { name: "b", ok: false, id: 2 }; +const c = { ok: true, id: 3, name: "c" }; +function keysOf(value: { id: number; name: string; ok: boolean }): string { + const keys: string[] = []; + for (const key in value) keys.push(key); + return keys.join(","); +} +for (const value of [a, b, c]) { + const seen: string[] = []; + for (const key in value) seen.push(key.toUpperCase()); + console.log(seen.join(" "), keysOf(value)); +} diff --git a/tests/corpus/7613-key-order-inspect.ts b/tests/corpus/7613-key-order-inspect.ts new file mode 100644 index 000000000..6a11eaf2c --- /dev/null +++ b/tests/corpus/7613-key-order-inspect.ts @@ -0,0 +1,12 @@ +import { inspect } from "node:util"; + +// console.log and util.inspect of object literals that write the same keys +// in different orders, alone and nested. +const a = { id: 1, name: "a", ok: true }; +const b = { name: "b", ok: false, id: 2 }; +const c = { ok: true, id: 3, name: "c" }; +console.log(a, b, c); +console.log([a, b, c]); +console.log({ outer: { inner: b }, list: [c] }); +console.log(inspect(b, { depth: 0 }), inspect({ deep: { deeper: c } }, { depth: 0 })); +console.log(`${inspect(c)} / ${inspect(a)}`); diff --git a/tests/corpus/7613-local-class-prototype-reflection.js b/tests/corpus/7613-local-class-prototype-reflection.js new file mode 100644 index 000000000..a48bd305c --- /dev/null +++ b/tests/corpus/7613-local-class-prototype-reflection.js @@ -0,0 +1,55 @@ +// Prototype reflection on instances of a function-local class without +// methods (zod 4's `$constructor` Definition): the prototype is the class's +// shared prototype object, it chains to the parent's, and members defined +// on it reach every instance. +const installed = new WeakSet(); +function factory(name, Parent) { + class Definition extends Parent {} + Object.defineProperty(Definition, "name", { value: name }); + return function make(label) { + const inst = new Definition(); + inst.label = label; + const proto = Object.getPrototypeOf(inst); + console.log(name, proto === Definition.prototype, proto === Object.getPrototypeOf(new Definition())); + if (!installed.has(proto)) { + installed.add(proto); + Object.defineProperty(proto, "describe", { + configurable: true, + enumerable: false, + get() { + return () => `${name}:${this.label}`; + }, + }); + console.log("installed", name); + } + return inst; + }; +} +const makeError = factory("Failure", Error); +const makePlain = factory("Thing", Object); +const a = makeError("a"); +const b = makeError("b"); +const c = makePlain("c"); +console.log(a.describe(), b.describe(), c.describe()); +console.log(a instanceof Error, c instanceof Error, Object.keys(a).join(), Object.keys(c).join()); +console.log(Object.getPrototypeOf(a) === Object.getPrototypeOf(b), Object.getPrototypeOf(a) === Object.getPrototypeOf(c)); +console.log(typeof Object.getPrototypeOf(c).describe, "describe" in a, Object.hasOwn(a, "describe")); + +// The prototype has its own non-enumerable `constructor`, the class value +// of that evaluation, as every class prototype does. +function local(Parent) { + class Definition extends Parent {} + const proto = Object.getPrototypeOf(new Definition()); + const descriptor = Object.getOwnPropertyDescriptor(proto, "constructor"); + console.log( + proto.constructor === Definition, + Object.hasOwn(proto, "constructor"), + JSON.stringify(Object.getOwnPropertyNames(proto)), + descriptor.value === Definition, + descriptor.writable, + descriptor.enumerable, + descriptor.configurable, + ); + return Definition; +} +console.log(local(Object) !== local(Error)); diff --git a/tests/corpus/7614-key-order-copies.ts b/tests/corpus/7614-key-order-copies.ts new file mode 100644 index 000000000..77cbb3b5b --- /dev/null +++ b/tests/corpus/7614-key-order-copies.ts @@ -0,0 +1,51 @@ +// Copies of object literals that write the same keys in different orders: +// spreads, spreads with other keys before and after, rest bindings, +// Object.assign and structuredClone keep each value's own order. +const a = { id: 1, name: "a", ok: true }; +const b = { name: "b", ok: false, id: 2 }; +const c = { ok: true, id: 3, name: "c" }; +for (const value of [a, b, c]) { + const copy = { ...value }; + const changed = { ...value, id: 10 }; + const after = { ...value, extra: "e" }; + const before = { first: 0, ...value }; + const { name, ...rest } = value; + const assigned = Object.assign({}, value); + const cloned = structuredClone(value); + console.log(JSON.stringify(copy), JSON.stringify(changed)); + console.log(JSON.stringify(after), Object.keys(before).join(",")); + console.log(name, JSON.stringify(rest), Object.keys(rest).join(",")); + console.log(JSON.stringify(assigned), JSON.stringify(cloned)); +} +// Keys after a spread that the spread value may lack: a key the value has +// keeps its place, and a key it lacks goes after the value's keys. +type Options = { id: number; label?: string; size?: number; tag?: string }; +const options: Options[] = [ + { id: 1, size: 2 }, + { size: 3, id: 2 }, + { label: "c", id: 3, tag: "t" }, + { tag: "u", size: 4, id: 4 }, +]; +for (const value of options) { + const merged = { ...value, tag: "x", label: "y", done: true }; + console.log(Object.keys(merged).join(","), JSON.stringify(merged)); +} +// A spread between explicit keys takes the order of its own value, not of +// another value of the same type that a key reads, and the order the value +// has at the spread, before a later key moves the value's keys. +type Pair = { a?: number; b: number }; +const pairs: Pair[] = [ + { b: 2, a: 1 }, + { a: 1, b: 2 }, +]; +function moveA(pair: Pair): number { + delete pair.a; + pair.a = 5; + return 0; +} +for (const value of pairs) + for (const other of pairs) { + const between = { first: other.b, ...value, last: other.b }; + const moved = { ...value, z: moveA(value) }; + console.log(Object.keys(between).join(","), Object.keys(moved).join(",")); + } diff --git a/tests/corpus/7614-npm-local-class-per-instance/main.ts b/tests/corpus/7614-npm-local-class-per-instance/main.ts new file mode 100644 index 000000000..259c162d0 --- /dev/null +++ b/tests/corpus/7614-npm-local-class-per-instance/main.ts @@ -0,0 +1,26 @@ +import { $constructor } from "ctor-kit"; + +const Base = $constructor("Base", (inst: any, def: any) => { + inst.kind = def.kind; +}); +const Failure = $constructor( + "Failure", + (inst: any, def: any) => { + inst.name = "Failure"; + inst.issues = def; + }, + { Parent: Error }, +); +const Coded = $constructor( + "Coded", + (inst: any, def: any) => { + inst.code = def; + }, + { Parent: TypeError }, +); +const base = new Base({ kind: "plain" }); +const failure = new Failure([1, 2]); +const coded = new Coded(7); +console.log(base.kind, base._zod.def.kind, base._zod.constr === Base); +console.log(failure.name, failure.issues.length, failure instanceof Error, failure._zod.constr === Failure); +console.log(coded.code, coded instanceof TypeError, coded instanceof Error, coded._zod.traits.has("Coded")); diff --git a/tests/corpus/7614-npm-local-class-per-instance/node_modules/ctor-kit/index.d.ts b/tests/corpus/7614-npm-local-class-per-instance/node_modules/ctor-kit/index.d.ts new file mode 100644 index 000000000..3233861c5 --- /dev/null +++ b/tests/corpus/7614-npm-local-class-per-instance/node_modules/ctor-kit/index.d.ts @@ -0,0 +1,5 @@ +export declare function $constructor( + name: string, + initializer: (inst: any, def: any) => void, + params?: { Parent: ErrorConstructor | TypeErrorConstructor }, +): any; diff --git a/tests/corpus/7614-npm-local-class-per-instance/node_modules/ctor-kit/index.js b/tests/corpus/7614-npm-local-class-per-instance/node_modules/ctor-kit/index.js new file mode 100644 index 000000000..9aca97175 --- /dev/null +++ b/tests/corpus/7614-npm-local-class-per-instance/node_modules/ctor-kit/index.js @@ -0,0 +1,34 @@ +// zod 4's `$constructor`, reduced: the factory is lowered once per +// argument-type tuple, and `_`, hoisted above `class Definition` and +// reached early through `Internals`, constructs its own call's class. +const _desc = { value: undefined, enumerable: false }; +function newError(Definition) { + return new Definition(); +} +export function $constructor(name, initializer, params) { + function Internals(def) { + this.def = def; + this.constr = _; + this.traits = new Set(); + } + function init(inst, def) { + if (!inst._zod) { + _desc.value = new Internals(def); + Object.defineProperty(inst, "_zod", _desc); + _desc.value = undefined; + } + inst._zod.traits.add(name); + initializer(inst, def); + } + const Parent = params?.Parent ?? Object; + class Definition extends Parent {} + Object.defineProperty(Definition, "name", { value: name }); + function _(def) { + const inst = params?.Parent ? newError(Definition) : this; + init(inst, def); + return inst; + } + Object.defineProperty(_, "init", { value: init }); + Object.defineProperty(_, "name", { value: name }); + return _; +} diff --git a/tests/corpus/7614-npm-local-class-per-instance/node_modules/ctor-kit/package.json b/tests/corpus/7614-npm-local-class-per-instance/node_modules/ctor-kit/package.json new file mode 100644 index 000000000..444df971a --- /dev/null +++ b/tests/corpus/7614-npm-local-class-per-instance/node_modules/ctor-kit/package.json @@ -0,0 +1 @@ +{ "name": "ctor-kit", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/corpus/7615-key-order-dynamic.ts b/tests/corpus/7615-key-order-dynamic.ts new file mode 100644 index 000000000..55970cb40 --- /dev/null +++ b/tests/corpus/7615-key-order-dynamic.ts @@ -0,0 +1,30 @@ +import { MessageChannel } from "node:worker_threads"; + +// Object literals that write the same keys in different orders, seen +// through untyped values: their keys, JSON text, own property names, and a +// MessageChannel message. +const a = { id: 1, name: "a", ok: true }; +const b = { name: "b", ok: false, id: 2 }; +const c = { ok: true, id: 3, name: "c" }; +const values: unknown[] = [a, b, c]; +for (const value of values) { + const object = value as Record; + console.log(Object.keys(object).join(","), JSON.stringify(object)); + console.log(Object.getOwnPropertyNames(object).join(",")); +} +const { port1, port2 } = new MessageChannel(); +let received = 0; +port1.on("message", (message: unknown) => { + console.log("message", JSON.stringify(message), Object.keys(message as object).join(",")); + if (++received === 3) port1.close(); +}); +for (const value of [a, b, c]) port2.postMessage(value); + +// A nested value seen through an untyped view keeps its identity and its +// own key order, and writes through the view reach it. +const holder = { inner: b, tag: "h" }; +const view = holder as unknown as Record; +const inner = view.inner as Record; +console.log(view.inner === view.inner, Object.keys(inner).join(",")); +inner.id = 20; +console.log(b.id, JSON.stringify(b), JSON.stringify(view)); diff --git a/tests/corpus/7615-local-class-prototype-reflection-ts.ts b/tests/corpus/7615-local-class-prototype-reflection-ts.ts new file mode 100644 index 000000000..92d3b5668 --- /dev/null +++ b/tests/corpus/7615-local-class-prototype-reflection-ts.ts @@ -0,0 +1,14 @@ +// Prototype reflection on an instance of a TypeScript function-local class +// without methods: one shared prototype per class evaluation. +function make(label: string): { label: string } { + class Box { + label = ""; + } + const box = new Box(); + box.label = label; + return box; +} +const a = make("a"); +const b = make("b"); +const pa = Object.getPrototypeOf(a); +console.log(pa === Object.getPrototypeOf(a), pa === Object.getPrototypeOf(b), typeof pa, Object.keys(a).join()); diff --git a/tests/corpus/7616-key-order-late-writes.ts b/tests/corpus/7616-key-order-late-writes.ts new file mode 100644 index 000000000..4166999d7 --- /dev/null +++ b/tests/corpus/7616-key-order-late-writes.ts @@ -0,0 +1,44 @@ +// Optional keys added after construction go last, as in Node, also for +// shapes whose literals write their keys in different orders: a write of a +// missing key, a delete and a later write, and writes in a loop. +type Item = { id: number; name?: string; note?: string; count: number }; + +const items: Item[] = [ + { id: 1, count: 0 }, + { count: 1, name: "b", id: 2 }, + { note: "c", id: 3, count: 2 }, +]; +items[0]!.note = "added"; +items[0]!.name = "later"; +items[1]!.name = "kept"; +delete items[2]!.note; +items[2]!.note = "again"; +for (const item of items) { + if (item.count > 0) item.name ??= "filled"; + console.log(JSON.stringify(item), Object.keys(item).join(",")); +} +const built: Item = { count: 9, id: 9 }; +for (let i = 0; i < 2; i++) { + if (i === 1) built.note = `n${i}`; + console.log(Object.keys(built).join(",")); +} +console.log(built); +// A write's value runs first, so a key it adds comes before the written +// key. +const nested: Item = { count: 3, id: 4 }; +nested.name = ((nested.note = "first"), "second"); +console.log(Object.keys(nested).join(","), JSON.stringify(nested)); +// A value seen as a JavaScript value enumerates later writes in order. +const seen: Item = { id: 5, count: 4 }; +const view: unknown = seen; +console.log(Object.keys(view as object).join(",")); +seen.note = "n"; +seen.name = "m"; +console.log(Object.keys(view as object).join(","), JSON.stringify(view)); +// The write goes to the object the receiver held before the value ran, +// even when the value assigns the receiver's binding. +let target: Item = { id: 6, count: 5 }; +const before = target; +const after: Item = { count: 6, id: 7 }; +target.name = ((target = after), "moved"); +console.log(Object.keys(before).join(","), Object.keys(after).join(","), JSON.stringify(target)); diff --git a/tests/corpus/7621-js-class-field-methods.js b/tests/corpus/7621-js-class-field-methods.js new file mode 100644 index 000000000..cd0a5be99 --- /dev/null +++ b/tests/corpus/7621-js-class-field-methods.js @@ -0,0 +1,96 @@ +// Untyped code calling methods on an instance of a JS class whose own +// properties live in a property bag: methods declared as arrow-function +// fields (own properties), prototype methods, prototype methods replaced or +// shadowed after construction, accessors on the prototype, and calls of +// members that are not functions. Each call must see what the instance +// holds at that moment and bind `this` to the instance. +'use strict'; + +class Context { + status = 200; + headers = {}; + label = 'ctx'; + header = (name, value) => { + this.headers[name] = value; + return this; + }; + json = (body) => `${this.status} ${JSON.stringify(body)} ${JSON.stringify(this.headers)}`; + constructor(label) { + this.label = label; + } + describe() { + return `${this.label}/${this.status}`; + } + get upper() { + return this.label.toUpperCase(); + } + shout() { + return this.upper + '!'; + } +} + +// Constant method names: the calls a framework's untyped code makes. +const calls = { + describe: (t) => t.describe(), + header: (t, name, value) => t.header(name, value), + json: (t, body) => t.json(body), + shout: (t) => t.shout(), + extra: (t, n) => t.extra(n), + status: (t) => t.status(), + missing: (t) => t.missing(), + upper: (t) => t.upper(), +}; + +function call(target, method, ...args) { + return calls[method](target, ...args); +} + +function put(target, key, value) { + target[key] = value; +} + +const c = new Context('first'); +console.log(call(c, 'describe')); +console.log(call(call(c, 'header', 'x-a', '1'), 'json', { ok: true })); +put(c, 'status', 404); +console.log(call(c, 'json', [1, 2]), call(c, 'describe'), call(c, 'shout')); + +// A field method replaced through untyped code, and an own property that +// shadows a prototype method. +put(c, 'json', (body) => `replaced ${body}`); +put(c, 'describe', function () { + return `own describe of ${this.label}`; +}); +console.log(call(c, 'json', 'x'), call(c, 'describe')); + +// A prototype method replaced after construction reaches every instance +// that does not shadow it. +const d = new Context('second'); +Context.prototype.describe = function () { + return `patched ${this.label}`; +}; +console.log(call(d, 'describe'), call(c, 'describe')); + +// A prototype method added after construction. +Context.prototype.extra = function (n) { + return `${this.label} extra ${n}`; +}; +console.log(call(d, 'extra', 3)); + +// Members that are not functions. +for (const name of ['status', 'missing', 'upper']) { + try { + call(d, name); + } catch (error) { + console.log(error instanceof TypeError, error.message); + } +} + +// Many calls in a row stay consistent with the instance's current state. +let out = ''; +for (let i = 0; i < 4; i++) { + call(d, 'header', `h${i}`, String(i)); + put(d, 'status', 200 + i); + out += call(d, 'json', i) + '\n'; +} +process.stdout.write(out); diff --git a/tests/corpus/7731-npm-any-string-index/main.ts b/tests/corpus/7731-npm-any-string-index/main.ts new file mode 100644 index 000000000..21e7d1c10 --- /dev/null +++ b/tests/corpus/7731-npm-any-string-index/main.ts @@ -0,0 +1,9 @@ +// An implicit-any package function specialized for a string argument indexes +// the string (`base?.[0]`), as hono's mergePath does. +import { App, mergePath } from "merge-path"; +console.log(new App().request("x")); +console.log(new App().request(new URL("http://h/p"))); +console.log(new App().basePath("/api").basePath("v1/")._basePath); +console.log(mergePath("/api/", "/"), mergePath("a", "b", "/c")); +console.log(new App().basePath("").basePath("").request("")); +console.log(mergePath("", "x"), mergePath("/", "/"), mergePath("//", "y/")); diff --git a/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/index.d.ts b/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/index.d.ts new file mode 100644 index 000000000..aa5142307 --- /dev/null +++ b/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/index.d.ts @@ -0,0 +1,2 @@ +export declare const mergePath: (...paths: string[]) => string; +export declare class App { _basePath: string; request: (input: string | URL) => string; basePath(path: string): App; } diff --git a/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/index.js b/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/index.js new file mode 100644 index 000000000..6a5c7c86b --- /dev/null +++ b/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/index.js @@ -0,0 +1,10 @@ +import { mergePath } from "./url.js"; +class App { + _basePath = "/"; + request = (input) => { + input = input.toString(); + return mergePath("/", input); + }; + basePath(path) { const s = new App(); s._basePath = mergePath(this._basePath, path); return s; } +} +export { App, mergePath }; diff --git a/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/package.json b/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/package.json new file mode 100644 index 000000000..ddc3db4ef --- /dev/null +++ b/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/package.json @@ -0,0 +1 @@ +{ "name": "merge-path", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/url.js b/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/url.js new file mode 100644 index 000000000..e27469714 --- /dev/null +++ b/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/url.js @@ -0,0 +1,10 @@ +/** +* Merge paths. +* @param {string[]} ...paths - The paths to merge. +* @returns {string} The merged path. +*/ +const mergePath = (base, sub, ...rest) => { + if (rest.length) sub = mergePath(sub, ...rest); + return `${base?.[0] === "/" ? "" : "/"}${base}${sub === "/" ? "" : `${base?.[base.length - 1] === "/" ? "" : "/"}${sub?.[0] === "/" ? sub.slice(1) : sub}`}`; +}; +export { mergePath }; diff --git a/tests/corpus/7732-untyped-match-pattern.js b/tests/corpus/7732-untyped-match-pattern.js new file mode 100644 index 000000000..2079bb8ef --- /dev/null +++ b/tests/corpus/7732-untyped-match-pattern.js @@ -0,0 +1,15 @@ +// String.prototype.match and search with an untyped pattern: a RegExp is used as is, +// any other value builds new RegExp(value), as hono's matcher does. +const matchers = { GET: [/^\/a\/([^/]+)$()|^\/b$()/, [0, 1], {}] }; +function match(method, path) { + const matcher = matchers[method] || matchers["ALL"]; + const m = path.match(matcher[0]); + if (!m) return null; + return [m.indexOf("", 1), m.length, m[1], m.index]; +} +console.log(JSON.stringify(match("GET", "/a/xy"))); +console.log(JSON.stringify(match("GET", "/b"))); +console.log(JSON.stringify(match("GET", "/c"))); +const g = { re: /o/g, s: "o.", n: null }; +function m2(s, k) { return s.match(g[k]); } +console.log(JSON.stringify(m2("foo boo", "re")), JSON.stringify(m2("go.o", "s")), JSON.stringify(m2("null", "n"))); diff --git a/tests/corpus/7733-property-bag-late-prototype.js b/tests/corpus/7733-property-bag-late-prototype.js new file mode 100644 index 000000000..f88212b62 --- /dev/null +++ b/tests/corpus/7733-property-bag-late-prototype.js @@ -0,0 +1,52 @@ +// A class whose property bag is first created without prototype data, while a local +// class with an untyped base rewrites the shared bag dispatch: the store gains the +// prototype later, so inherited methods stay readable (hono's SmartRouter with zod). +function $constructor(name, initializer, params) { + const Parent = params?.Parent ?? Object; + class Definition extends Parent {} + Object.defineProperty(Definition, "name", { value: name }); + function _(def) { + const inst = params?.Parent ? new Definition() : this; + inst.def = def; + initializer(inst, def); + return inst; + } + Object.defineProperty(_, "name", { value: name }); + return _; +} +class Smart { + name = "S"; + #inner = []; + constructor(init) { this.#inner = init.routers; } + add(x) { this.#inner.push(x); } + match(m) { + const r = this.#inner[0]; + this.match = r.match.bind(r); + this.name = `S + ${r.name}`; + return "first " + m; + } +} +class Inner { name = "Inner"; match(m) { return "inner " + m; } } +class App { + router; + constructor(options = {}) { + const { strict, ...rest } = options; + Object.assign(this, rest); + this.router = options.router ?? new Smart({ routers: [new Inner()] }); + } + dispatch(m) { return this.router.match(m); } +} +const app = new App(); +console.log(Object.keys(app.router).join(",")); +const $Err = $constructor("$Err", (inst, def) => { inst.issues = def.issues; }, { Parent: Error }); +const e = new $Err({ issues: [1] }); +console.log(Object.keys(e).join(","), e.issues.length); +const run = [(a) => a.dispatch("a"), (a) => a.dispatch("b")]; +for (const f of run) console.log(f(app), app.router.name); +const holder = JSON.parse("{}"); +holder.r = new Smart({ routers: [new Inner()] }); +console.log(typeof holder.r.match, "match" in holder.r, Object.keys(holder.r).join(",")); +console.log(holder.r.match("c"), holder.r.match("d"), holder.r.name); +const scratch = JSON.parse("{\"name\":\"x\",\"other\":1}"); +delete scratch.name; +console.log(Object.keys(scratch).join(",")); diff --git a/tests/corpus/7734-caught-instanceof-class-binding.js b/tests/corpus/7734-caught-instanceof-class-binding.js new file mode 100644 index 000000000..4332daab0 --- /dev/null +++ b/tests/corpus/7734-caught-instanceof-class-binding.js @@ -0,0 +1,42 @@ +// A catch binding tested against a class held by a binding (var/const class expression), +// as hono's SmartRouter tests UnsupportedPathError. +var UnsupportedPathError = class extends Error {}; +class Strict { + add(p) { if (p.includes("*")) throw new UnsupportedPathError(); return p; } +} +class Loose { add(p) { return "loose " + p; } } +function pick(p) { + const routers = [new Strict(), new Loose()]; + for (const r of routers) { + try { + return r.add(p); + } catch (e) { + if (e instanceof UnsupportedPathError) continue; + throw e; + } + } +} +console.log(pick("/a"), pick("/b/*")); +try { throw new Error("x"); } catch (e) { console.log(e instanceof UnsupportedPathError, e instanceof Error); } +const u = new UnsupportedPathError("m"); +console.log(u instanceof UnsupportedPathError, u.message, u.name, u instanceof Error); + +// A binding that holds a subclass of the class it was declared with: the +// test reads the class the binding holds at run time. +class BaseError extends Error {} +class DerivedError extends BaseError {} +class LeafError extends DerivedError {} +var Current = BaseError; +function caughtIs(value) { + try { + throw value; + } catch (e) { + return e instanceof Current; + } +} +const samples = () => [new BaseError("b"), new DerivedError("d"), new LeafError("l"), new Error("e"), "text"]; +console.log("base", samples().map(caughtIs).join(",")); +Current = DerivedError; +console.log("derived", samples().map(caughtIs).join(",")); +Current = LeafError; +console.log("leaf", samples().map(caughtIs).join(",")); diff --git a/tests/corpus/7735-untyped-headers-iterators.js b/tests/corpus/7735-untyped-headers-iterators.js new file mode 100644 index 000000000..40937fe5c --- /dev/null +++ b/tests/corpus/7735-untyped-headers-iterators.js @@ -0,0 +1,23 @@ +// Headers entries/keys/values on an untyped Headers value: live iterators over the +// sorted list, with set-cookie values one by one, as hono's Context copies headers. +const holder = JSON.parse("{}"); +holder.res = new Response("body", { headers: { "x-b": "2", "X-A": "1", "set-cookie": "s=1" } }); +holder.res.headers.append("set-cookie", "t=2"); +const headers = holder.res.headers; +for (const [k, v] of headers.entries()) console.log("entry", k, v); +console.log(JSON.stringify([...headers.keys()]), JSON.stringify([...headers.values()])); +const it = headers.entries(); +console.log(JSON.stringify(it.next())); +headers.append("a-first", "0"); +headers.append("z-last", "9"); +console.log(JSON.stringify(it.next()), JSON.stringify(it.next())); +const rest = []; +for (const entry of it) rest.push(entry.join("=")); +console.log(rest.join(" "), JSON.stringify(it.next())); +const copy = JSON.parse("{}"); +copy.res = new Response(null, { status: 204 }); +for (const [k, v] of headers.entries()) { + if (k === "set-cookie") continue; + copy.res.headers.set(k, v); +} +console.log(JSON.stringify([...copy.res.headers.entries()])); diff --git a/tests/corpus/7738-private-logical-assignment-untyped.js b/tests/corpus/7738-private-logical-assignment-untyped.js new file mode 100644 index 000000000..b0ceaea86 --- /dev/null +++ b/tests/corpus/7738-private-logical-assignment-untyped.js @@ -0,0 +1,56 @@ +// Logical assignments to a private field of another instance whose type +// JavaScript leaves open, the way a router trie numbers its parameter +// nodes on first use (`next.#varIndex ??= context.varIndex++`). They read +// and write the brand-checked field, and evaluate the right side only +// when they store it. +class Node { + #varIndex; + #label = ""; + #count = 0; + #children = Object.create(null); + + child(key, context) { + let next = this.#children[key]; + if (!next) next = this.#children[key] = new Node(); + next.#varIndex ??= context.varIndex++; + next.#label ||= `node:${key}`; + next.#count &&= next.#count + 1; + next.#count ||= 1; + return next; + } + + describe() { + const kind = typeof this.#varIndex === "number" ? "param" : "static"; + return `${kind} ${this.#varIndex} ${this.#label} ${this.#count}`; + } + + static reset(node, other) { + other.#label &&= ""; + other.#varIndex ??= -1; + return [node.#label, other.#label, other.#varIndex]; + } + + static peek(value) { + return value.#varIndex ??= 99; + } +} + +const context = { varIndex: 0 }; +const root = new Node(); +const a = root.child("a", context); +const b = root.child("b", context); +const again = root.child("a", context); +console.log(a === again, context.varIndex); +console.log(root.describe()); +console.log(a.describe()); +console.log(b.describe()); +console.log(JSON.stringify(Node.reset(root, a))); +console.log(a.describe()); + +for (const value of [{}, null]) { + try { + Node.peek(value); + } catch (error) { + console.log(error instanceof TypeError, error.message); + } +} diff --git a/tests/coverage-fixtures/blocked-signature-body.ts b/tests/coverage-fixtures/blocked-signature-body.ts new file mode 100644 index 000000000..2c37484e0 --- /dev/null +++ b/tests/coverage-fixtures/blocked-signature-body.ts @@ -0,0 +1,19 @@ +// Functions whose signatures cannot compile: the report still lists what +// their bodies would report. A use of the blocked parameter names the +// declaration; the body's own unsupported statement reports itself, with +// the reached diagnostics for a function the entry calls and as unreached +// for one nothing calls. +function measure(target: WeakRef, label: string): number { + const extra = new WeakRef({ label }); + console.log(target); + return label.length + (extra === undefined ? 0 : 1); +} +function unused(target: WeakRef): number { + const extra = new WeakRef({ target }); + return extra === undefined ? 0 : 1; +} +console.log(measure(new WeakRef({}), "abc")); +// A declared `this` parameter gives the body its receiver. +function described(this: { label: string }, target: WeakRef): string { + return this.label + String(target); +} diff --git a/tests/coverage-fixtures/divergences.ts b/tests/coverage-fixtures/divergences.ts index 83c3ede49..0cf8bcae5 100644 --- a/tests/coverage-fixtures/divergences.ts +++ b/tests/coverage-fixtures/divergences.ts @@ -20,14 +20,18 @@ console.log(tagged.label); const view: Labeled = tagged; console.log(view === tagged); -// Key order: values of one type are built in two orders and the program -// observes them, so the type keeps its declaration order. +// Key order: values of one type are built in two orders, and a spread sets +// an optional key its source lacks, so the values cannot record their own +// orders and the type keeps its declaration order. interface Point { x: number; y: number; + label?: string; } const point: Point = { y: 1, x: 2 }; -console.log(JSON.stringify(point), JSON.stringify({ x: 3, y: 4 })); +const other: Point = { x: 3, y: 4 }; +const labeled: Point = { ...point, label: "p" }; +console.log(JSON.stringify(point), JSON.stringify(other), JSON.stringify(labeled)); // 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/startup-fences.js b/tests/coverage-fixtures/startup-fences.js new file mode 100644 index 000000000..22aad4e0a --- /dev/null +++ b/tests/coverage-fixtures/startup-fences.js @@ -0,0 +1,9 @@ +// Deferred fences in a module's top-level code run when the module loads; +// a fence inside a function runs when the function is called. +"use strict"; +var value = JSON.parse("{}"); +console.log(value instanceof Object); +function check(other) { + return other instanceof Object; +} +console.log(check([])); diff --git a/tests/coverage-fixtures/unreached-js-fence.js b/tests/coverage-fixtures/unreached-js-fence.js index 549e633d3..f900e5834 100644 --- a/tests/coverage-fixtures/unreached-js-fence.js +++ b/tests/coverage-fixtures/unreached-js-fence.js @@ -1,8 +1,8 @@ // An exported function nothing on the entry path calls: a statement in it -// that can only fence at runtime still reports its diagnostic, so no -// failed statement goes unexplained. +// that does not compile (a boolean pushed onto the numbers in `items`) +// still reports its diagnostic, so no failed statement goes unexplained. export function collect(values, flag) { - var out = { items: [] }; + var out = { items: [0] }; var x; out.items.push((x = +flag, x * 0 === 0) ? x : flag, !!flag); return out; diff --git a/tests/diagnostics/arguments-length-self-call.ts b/tests/diagnostics/arguments-length-self-call.ts new file mode 100644 index 000000000..3afc2cb33 --- /dev/null +++ b/tests/diagnostics/arguments-length-self-call.ts @@ -0,0 +1,8 @@ +// `arguments.length` in a function expression stored in a slot with more +// parameters counts that slot's arguments, but a call through the +// expression's own name passes only the declared ones: no constant answers +// both, so the read fences. +const count: (a: number, b: number) => number = function self(a: number): number { + return a > 0 ? self(a - 1) + arguments.length : arguments.length; +}; +console.log(count(2, 0)); diff --git a/tests/diagnostics/class-call-shadowed-static.ts b/tests/diagnostics/class-call-shadowed-static.ts new file mode 100644 index 000000000..53a5dc9ad --- /dev/null +++ b/tests/diagnostics/class-call-shadowed-static.ts @@ -0,0 +1,15 @@ +// Through a class value that may hold a subclass, `C.call(...)` reaches the +// subclass's own static `call` when it declares one, so the call is not the +// constructor call that throws Node's TypeError. +class Base { + n = 1; +} +class OwnCall extends Base { + static call(value: unknown): string { + return `own call ${typeof value}`; + } +} +function callThrough(C: typeof Base): unknown { + return C.call(new Base()); +} +console.log(callThrough(OwnCall)); diff --git a/tests/diagnostics/destructuring-fences.ts b/tests/diagnostics/destructuring-fences.ts index e42f8a07a..0ee60579d 100644 --- a/tests/diagnostics/destructuring-fences.ts +++ b/tests/diagnostics/destructuring-fences.ts @@ -38,12 +38,12 @@ interface E1 { } interface E2 { kind: "two"; - b: string; + b: number; label: string; } function fromUnion(u: E1 | E2): string { - const { b } = u; // union source: narrow first - return b; + const { b } = u; // union source whose arms type the field differently: narrow first + return String(b); } console.log(fromUnion({ kind: "one", b: "s", count: 1 })); diff --git a/tests/diagnostics/filter-narrow.ts b/tests/diagnostics/filter-narrow.ts deleted file mode 100644 index 45452e212..000000000 --- a/tests/diagnostics/filter-narrow.ts +++ /dev/null @@ -1,22 +0,0 @@ -// Narrowing filters re-tag ONLY what the runtime test proved: an inferred -// single-arm predicate or filter(Boolean). Everything else stays fenced — -// multi-arm targets (the re-tag doesn't exist), hand-written predicates -// (unchecked assertions), and predicate VALUES (their annotation is -// written by definition). -const xs: (string | number | undefined)[] = ["a", 1, undefined]; - -// Multi-arm narrowing: string | number is not a single arm. -const defined = xs.filter((x) => x !== undefined); -console.log(defined.length); - -// A hand-written predicate on an inline callback. -const ys: (string | undefined)[] = ["a", undefined]; -const strings = ys.filter((x): x is string => typeof x === "string"); -console.log(strings.length); - -// A predicate VALUE (declared function with a written predicate). -function isString(x: string | undefined): x is string { - return typeof x === "string"; -} -const viaValue = ys.filter(isString); -console.log(viaValue.length); diff --git a/tests/diagnostics/fromentries-aliased-entries.ts b/tests/diagnostics/fromentries-aliased-entries.ts deleted file mode 100644 index ee5cc5338..000000000 --- a/tests/diagnostics/fromentries-aliased-entries.ts +++ /dev/null @@ -1,13 +0,0 @@ -// `Object.fromEntries(...) as T` keeps the object only when the entries' -// values are scalars or checked-dynamic values. Native arrays and records -// in the entries would be copied, severing their identity, so the -// assertion keeps the exact-shape fence (corpus 6467 covers the cases that -// lower). -type Scores = { a: number[]; b: number[] }; -const shared = [10]; -const scores = Object.fromEntries([ - ["a", shared], - ["b", shared], -]) as Scores; -scores.a.push(11); -console.log(shared.length, scores.b.length); diff --git a/tests/diagnostics/iterable-sources.ts b/tests/diagnostics/iterable-sources.ts new file mode 100644 index 000000000..c93029cce --- /dev/null +++ b/tests/diagnostics/iterable-sources.ts @@ -0,0 +1,17 @@ +// An Iterable value holds an array, Set, Map, string or iterator. A +// class instance with its own [Symbol.iterator] and a typed array have no +// representation there, so the conversion site reports them by name. +class Countdown implements Iterable { + *[Symbol.iterator](): Iterator { + yield 2; + yield 1; + } +} +function sum(xs: Iterable): number { + let s = 0; + for (const x of xs) s += x; + return s; +} +console.log(sum(new Countdown())); +console.log(sum(new Uint8Array([1, 2]))); +export const marker: number = 1; diff --git a/tests/diagnostics/iterator-slot-fences.ts b/tests/diagnostics/iterator-slot-fences.ts index 759929615..e82437d4d 100644 --- a/tests/diagnostics/iterator-slot-fences.ts +++ b/tests/diagnostics/iterator-slot-fences.ts @@ -1,6 +1,6 @@ // Iterator-typed slots hold generators and built-in collection iterators. -// A class implementing the iterator protocol stays a pointed fence there, -// and Iterable annotations have no lowering yet. +// A class implementing the iterator protocol stays a pointed fence there +// (Iterable values: corpus 7380-7383). class Countdown implements Iterator { n = 2; @@ -12,10 +12,3 @@ function firstOf(it: Iterator): void { console.log(it.next().value); } firstOf(new Countdown()); - -function total(values: Iterable): number { - let sum = 0; - for (const v of values) sum += v; - return sum; -} -console.log(total([1, 2])); diff --git a/tests/diagnostics/local-class-boundaries.ts b/tests/diagnostics/local-class-boundaries.ts index 2c43e9900..ac1afb2c2 100644 --- a/tests/diagnostics/local-class-boundaries.ts +++ b/tests/diagnostics/local-class-boundaries.ts @@ -1,10 +1,8 @@ function privateBrand() { return class { #value = 1; has(other: object): boolean { return #value in other; } }; } -function selfName() { return class Local { copy() { return new Local(); } }; } class Base {} function derived() { return class extends Base {}; } function generic() { return class { read(value: T): T { return value; } }; } privateBrand(); -selfName(); derived(); generic(); // End of unsupported local-class forms. diff --git a/tests/diagnostics/new-target-late-local-subclass.ts b/tests/diagnostics/new-target-late-local-subclass.ts new file mode 100644 index 000000000..658a7148e --- /dev/null +++ b/tests/diagnostics/new-target-late-local-subclass.ts @@ -0,0 +1,15 @@ +// A function-local subclass refuses new.target in its base's constructor +// also when lowering collects it after that constructor: make() is lowered +// after the top-level code that constructs Base. +class Base { + readonly label: string; + constructor() { + this.label = new.target.name; + } +} +console.log(new Base().label); +function make(): Base { + const Local = class extends Base {}; + return new Local(); +} +console.log(make().label); diff --git a/tests/diagnostics/new-target-local-subclass.ts b/tests/diagnostics/new-target-local-subclass.ts new file mode 100644 index 000000000..a5f153049 --- /dev/null +++ b/tests/diagnostics/new-target-local-subclass.ts @@ -0,0 +1,5 @@ +// A function-local subclass makes a new class per evaluation, so the +// base constructor's new.target has no static dispatch. +class A { constructor() { console.log(new.target.name); } } +function make() { class Local extends A {} return new Local(); } +make(); diff --git a/tests/diagnostics/new-target-plain-function.ts b/tests/diagnostics/new-target-plain-function.ts new file mode 100644 index 000000000..da592e084 --- /dev/null +++ b/tests/diagnostics/new-target-plain-function.ts @@ -0,0 +1,3 @@ +// new.target in a plain function has no invocation record to read. +function F(this: unknown) { console.log(new.target === undefined); } +F.call(null); diff --git a/tests/diagnostics/new-target-value-generic-subclass.ts b/tests/diagnostics/new-target-value-generic-subclass.ts new file mode 100644 index 000000000..2107a39a5 --- /dev/null +++ b/tests/diagnostics/new-target-value-generic-subclass.ts @@ -0,0 +1,5 @@ +// new.target as a class value where a subclass is generic: its +// instantiations have no common class value. +class A { readonly t: typeof A; constructor() { this.t = new.target; } } +class B extends A { readonly v: T | undefined = undefined; } +console.log(new B().t.name); diff --git a/tests/diagnostics/new-target-value-signatures.ts b/tests/diagnostics/new-target-value-signatures.ts new file mode 100644 index 000000000..af0c906e5 --- /dev/null +++ b/tests/diagnostics/new-target-value-signatures.ts @@ -0,0 +1,5 @@ +// new.target as a class value needs every subclass to construct with the +// base's signature; B's constructor takes a parameter A's does not. +class A { readonly t: typeof A; constructor() { this.t = new.target; } } +class B extends A { constructor(readonly n: number) { super(); } } +console.log(new B(1).t.name); diff --git a/tests/diagnostics/retired-engine-1123-any-conditional-spread.ts b/tests/diagnostics/retired-engine-1123-any-conditional-spread.ts deleted file mode 100644 index 03043a32f..000000000 --- a/tests/diagnostics/retired-engine-1123-any-conditional-spread.ts +++ /dev/null @@ -1,49 +0,0 @@ -// The conditional-spread idiom inside `any`-typed object literals: -// `{ a, ...(c ? { k: v } : {}) }` builds one of two engine literals behind -// the test, so the key is truly ABSENT when the empty arm is taken — -// engine-exact (`typeof o.k` answers "undefined", JSON drops it), with the -// shared properties evaluating exactly once. - -function opts(aspect: string | undefined): any { - const o: any = { - mode: "M", - ...(aspect ? { cfg: { aspect } } : {}), - }; - return o; -} -const a = opts("1:1"); -console.log(`${a.mode} ${a.cfg.aspect} ${typeof a.cfg}`); -const b = opts(undefined); -console.log(`${b.mode} ${typeof b.cfg}`); - -// Multi-property carrier arms are fine in the island form. -function multi(on: boolean): any { - return { base: 1, ...(on ? { x: "X", y: "Y" } : {}) }; -} -const m1 = multi(true); -const m2 = multi(false); -console.log(`${m1.base} ${m1.x} ${m1.y}`); -console.log(`${m2.base} ${typeof m2.x} ${typeof m2.y}`); - -// Reversed orientation and string-literal keys. -function rev(quiet: boolean): any { - return { kind: "r", ...(quiet ? {} : { "log-level": "loud" }) }; -} -console.log(`${rev(false)["log-level"]}`); -console.log(`${typeof rev(true)["log-level"]}`); - -// The spread mid-literal: properties around it evaluate exactly once and -// land in engine key order. -let sides = 0; -function side(v: number): number { - sides++; - return v; -} -function ordered(flag: boolean): any { - const o: any = { first: side(1), ...(flag ? { mid: 2 } : {}), last: side(3) }; - return o; -} -const o1 = ordered(true); -const o2 = ordered(false); -console.log(`${o1.first} ${o1.mid} ${o1.last}`); -console.log(`${o2.first} ${typeof o2.mid} ${o2.last} ${sides}`); diff --git a/tests/fixtures/node-types/fenced.ts b/tests/fixtures/node-types/fenced.ts index 644e901f4..27e68a618 100644 --- a/tests/fixtures/node-types/fenced.ts +++ b/tests/fixtures/node-types/fenced.ts @@ -70,12 +70,12 @@ http2.createSecureServer({ allowHTTP1: true, cert: "pem", key: "pem", streamRese http2.connect("https://localhost"); /* The crypto surface beyond the lowered slice (randomness, the hash * chain, the introspection statics): RSA encryption names the lowered - * asymmetric surface, symmetric ciphers the missing cipher stack, prime - * checks theirs, and setFips the FIPS truth (getFips() lowers to 0). */ -import { checkPrimeSync, createCipheriv, publicEncrypt, setFips } from "node:crypto"; + * asymmetric surface, symmetric ciphers the missing cipher stack, the + * secure heap the generic hint, and setFips the FIPS truth (getFips() lowers to 0). */ +import { createCipheriv, publicEncrypt, secureHeapUsed, setFips } from "node:crypto"; publicEncrypt("key", Buffer.alloc(1)); createCipheriv("aes-128-ccm", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); -checkPrimeSync(7n); +secureHeapUsed(); setFips(false); fetch("https://example.invalid/", { integrity: "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", diff --git a/tests/fixtures/npm/cases/chalk5-builder/main.ts b/tests/fixtures/npm/cases/chalk5-builder/main.ts new file mode 100644 index 000000000..2a4fff61b --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/main.ts @@ -0,0 +1,48 @@ +// chalk 5 builders used as values (the package compiles from scriptc's +// built-in shim): builders in function slots and arrays, styles picked at +// run time, builders in record fields called with any number of arguments, +// visible at level 0, chalkStderr, and the root's own call. +import chalk, { Chalk, chalkStderr, type ChalkInstance, type ForegroundColorName } from "chalk"; + +const show = (label: string, value: string): void => { + console.log(label + " " + JSON.stringify(value)); +}; + +const c = new Chalk({ level: 1 }); +const paint: (text: string) => string = c.green; +show("slot", paint("go")); +const styles: ChalkInstance[] = [c.red, c.bold, c.underline.blue]; +show("array", styles.map((style) => style("x")).join("|")); +const byName = (name: ForegroundColorName): ChalkInstance => { + switch (name) { + case "red": + return c.red; + case "green": + return c.green; + default: + return c.white; + } +}; +show("picked", byName("red")("r") + byName("green")("g") + byName("cyan")("w")); +let chain: ChalkInstance = c; +for (const step of ["bold", "italic", "yellow"]) { + chain = step === "bold" ? chain.bold : step === "italic" ? chain.italic : chain.yellow; +} +show("built", chain("chain")); +show("cached", c.red === c.red ? "same" : "different"); +show("nested close", c.red("a" + c.blue("b") + "c" + c.red("d") + "e")); +show("bg nested", c.bgRed("a" + c.bgBlue("b") + "c")); + +const off = new Chalk({ level: 0 }); +show("visible off", off.visible("v") + "|" + off.red.visible("v") + "|" + off.red("plain")); +show("root call", c("a", 1, true) + "|" + c() + "|" + c(undefined) + "|" + c(null, "n")); +show("builder args", c.red(undefined) + "|" + c.red(null) + "|" + c.red(1, 2) + "|" + c.red()); +const theme = { error: c.red, note: c.bold.blue }; +show("field args", theme.error("a", "b") + "|" + theme.note("x", 1, true) + "|" + theme.error("one") + "|" + theme.error()); +show("multiline", c.cyan("one\r\ntwo\nthree")); +show("stderr level", String(chalkStderr.level >= 0)); +show("rgb levels", [1, 2, 3].map((level) => new Chalk({ level: level as 1 | 2 | 3 }).rgb(255, 136, 0)("o")).join("|")); +show("hex bg", new Chalk({ level: 2 }).bgHex("#abcdef")("h") + new Chalk({ level: 1 }).bgHex("123")("i")); +c.level = 0; +show("after level 0", paint("go") + "|" + c.red("r")); +show("default red", chalk.level >= 0 ? "ok" : "bad"); diff --git a/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/license b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/license new file mode 100644 index 000000000..fa7ceba3e --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/license @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) Sindre Sorhus (https://sindresorhus.com) + +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/fixtures/npm/cases/chalk5-builder/node_modules/chalk/package.json b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/package.json new file mode 100644 index 000000000..c9e0dc52b --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/package.json @@ -0,0 +1,83 @@ +{ + "name": "chalk", + "version": "5.6.2", + "description": "Terminal string styling done right", + "license": "MIT", + "repository": "chalk/chalk", + "funding": "https://github.com/chalk/chalk?sponsor=1", + "type": "module", + "main": "./source/index.js", + "exports": "./source/index.js", + "imports": { + "#ansi-styles": "./source/vendor/ansi-styles/index.js", + "#supports-color": { + "node": "./source/vendor/supports-color/index.js", + "default": "./source/vendor/supports-color/browser.js" + } + }, + "types": "./source/index.d.ts", + "sideEffects": false, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "scripts": { + "test": "xo && c8 ava && tsd", + "bench": "matcha benchmark.js" + }, + "files": [ + "source", + "!source/index.test-d.ts" + ], + "keywords": [ + "color", + "colour", + "colors", + "terminal", + "console", + "cli", + "string", + "ansi", + "style", + "styles", + "tty", + "formatting", + "rgb", + "256", + "shell", + "xterm", + "log", + "logging", + "command-line", + "text" + ], + "devDependencies": { + "@types/node": "^16.11.10", + "ava": "^3.15.0", + "c8": "^7.10.0", + "color-convert": "^2.0.1", + "execa": "^6.0.0", + "log-update": "^5.0.0", + "matcha": "^0.7.0", + "tsd": "^0.19.0", + "xo": "^0.57.0", + "yoctodelay": "^2.0.0" + }, + "xo": { + "rules": { + "unicorn/prefer-string-slice": "off", + "@typescript-eslint/consistent-type-imports": "off", + "@typescript-eslint/consistent-type-exports": "off", + "@typescript-eslint/consistent-type-definitions": "off", + "unicorn/expiring-todo-comments": "off" + } + }, + "c8": { + "reporter": [ + "text", + "lcov" + ], + "exclude": [ + "source/vendor" + ] + } +} diff --git a/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/index.d.ts b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/index.d.ts new file mode 100644 index 000000000..8295d9252 --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/index.d.ts @@ -0,0 +1,325 @@ +// TODO: Make it this when TS suports that. +// import {ModifierName, ForegroundColor, BackgroundColor, ColorName} from '#ansi-styles'; +// import {ColorInfo, ColorSupportLevel} from '#supports-color'; +import { + ModifierName, + ForegroundColorName, + BackgroundColorName, + ColorName, +} from './vendor/ansi-styles/index.js'; +import {ColorInfo, ColorSupportLevel} from './vendor/supports-color/index.js'; + +export interface Options { + /** + Specify the color support for Chalk. + + By default, color support is automatically detected based on the environment. + + Levels: + - `0` - All colors disabled. + - `1` - Basic 16 colors support. + - `2` - ANSI 256 colors support. + - `3` - Truecolor 16 million colors support. + */ + readonly level?: ColorSupportLevel; +} + +/** +Return a new Chalk instance. +*/ +export const Chalk: new (options?: Options) => ChalkInstance; // eslint-disable-line @typescript-eslint/naming-convention + +export interface ChalkInstance { + (...text: unknown[]): string; + + /** + The color support for Chalk. + + By default, color support is automatically detected based on the environment. + + Levels: + - `0` - All colors disabled. + - `1` - Basic 16 colors support. + - `2` - ANSI 256 colors support. + - `3` - Truecolor 16 million colors support. + */ + level: ColorSupportLevel; + + /** + Use RGB values to set text color. + + @example + ``` + import chalk from 'chalk'; + + chalk.rgb(222, 173, 237); + ``` + */ + rgb: (red: number, green: number, blue: number) => this; + + /** + Use HEX value to set text color. + + @param color - Hexadecimal value representing the desired color. + + @example + ``` + import chalk from 'chalk'; + + chalk.hex('#DEADED'); + ``` + */ + hex: (color: string) => this; + + /** + Use an [8-bit unsigned number](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit) to set text color. + + @example + ``` + import chalk from 'chalk'; + + chalk.ansi256(201); + ``` + */ + ansi256: (index: number) => this; + + /** + Use RGB values to set background color. + + @example + ``` + import chalk from 'chalk'; + + chalk.bgRgb(222, 173, 237); + ``` + */ + bgRgb: (red: number, green: number, blue: number) => this; + + /** + Use HEX value to set background color. + + @param color - Hexadecimal value representing the desired color. + + @example + ``` + import chalk from 'chalk'; + + chalk.bgHex('#DEADED'); + ``` + */ + bgHex: (color: string) => this; + + /** + Use a [8-bit unsigned number](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit) to set background color. + + @example + ``` + import chalk from 'chalk'; + + chalk.bgAnsi256(201); + ``` + */ + bgAnsi256: (index: number) => this; + + /** + Modifier: Reset the current style. + */ + readonly reset: this; + + /** + Modifier: Make the text bold. + */ + readonly bold: this; + + /** + Modifier: Make the text have lower opacity. + */ + readonly dim: this; + + /** + Modifier: Make the text italic. *(Not widely supported)* + */ + readonly italic: this; + + /** + Modifier: Put a horizontal line below the text. *(Not widely supported)* + */ + readonly underline: this; + + /** + Modifier: Put a horizontal line above the text. *(Not widely supported)* + */ + readonly overline: this; + + /** + Modifier: Invert background and foreground colors. + */ + readonly inverse: this; + + /** + Modifier: Print the text but make it invisible. + */ + readonly hidden: this; + + /** + Modifier: Puts a horizontal line through the center of the text. *(Not widely supported)* + */ + readonly strikethrough: this; + + /** + Modifier: Print the text only when Chalk has a color level above zero. + + Can be useful for things that are purely cosmetic. + */ + readonly visible: this; + + readonly black: this; + readonly red: this; + readonly green: this; + readonly yellow: this; + readonly blue: this; + readonly magenta: this; + readonly cyan: this; + readonly white: this; + + /* + Alias for `blackBright`. + */ + readonly gray: this; + + /* + Alias for `blackBright`. + */ + readonly grey: this; + + readonly blackBright: this; + readonly redBright: this; + readonly greenBright: this; + readonly yellowBright: this; + readonly blueBright: this; + readonly magentaBright: this; + readonly cyanBright: this; + readonly whiteBright: this; + + readonly bgBlack: this; + readonly bgRed: this; + readonly bgGreen: this; + readonly bgYellow: this; + readonly bgBlue: this; + readonly bgMagenta: this; + readonly bgCyan: this; + readonly bgWhite: this; + + /* + Alias for `bgBlackBright`. + */ + readonly bgGray: this; + + /* + Alias for `bgBlackBright`. + */ + readonly bgGrey: this; + + readonly bgBlackBright: this; + readonly bgRedBright: this; + readonly bgGreenBright: this; + readonly bgYellowBright: this; + readonly bgBlueBright: this; + readonly bgMagentaBright: this; + readonly bgCyanBright: this; + readonly bgWhiteBright: this; +} + +/** +Main Chalk object that allows to chain styles together. + +Call the last one as a method with a string argument. + +Order doesn't matter, and later styles take precedent in case of a conflict. + +This simply means that `chalk.red.yellow.green` is equivalent to `chalk.green`. +*/ +declare const chalk: ChalkInstance; + +export const supportsColor: ColorInfo; + +export const chalkStderr: typeof chalk; +export const supportsColorStderr: typeof supportsColor; + +export { + ModifierName, ForegroundColorName, BackgroundColorName, ColorName, + modifierNames, foregroundColorNames, backgroundColorNames, colorNames, +// } from '#ansi-styles'; +} from './vendor/ansi-styles/index.js'; + +export { + ColorInfo, + ColorSupport, + ColorSupportLevel, +// } from '#supports-color'; +} from './vendor/supports-color/index.js'; + +// TODO: Remove these aliases in the next major version +/** +@deprecated Use `ModifierName` instead. + +Basic modifier names. +*/ +export type Modifiers = ModifierName; + +/** +@deprecated Use `ForegroundColorName` instead. + +Basic foreground color names. + +[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support) +*/ +export type ForegroundColor = ForegroundColorName; + +/** +@deprecated Use `BackgroundColorName` instead. + +Basic background color names. + +[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support) +*/ +export type BackgroundColor = BackgroundColorName; + +/** +@deprecated Use `ColorName` instead. + +Basic color names. The combination of foreground and background color names. + +[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support) +*/ +export type Color = ColorName; + +/** +@deprecated Use `modifierNames` instead. + +Basic modifier names. +*/ +export const modifiers: readonly Modifiers[]; + +/** +@deprecated Use `foregroundColorNames` instead. + +Basic foreground color names. +*/ +export const foregroundColors: readonly ForegroundColor[]; + +/** +@deprecated Use `backgroundColorNames` instead. + +Basic background color names. +*/ +export const backgroundColors: readonly BackgroundColor[]; + +/** +@deprecated Use `colorNames` instead. + +Basic color names. The combination of foreground and background color names. +*/ +export const colors: readonly Color[]; + +export default chalk; diff --git a/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/index.js b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/index.js new file mode 100644 index 000000000..8bc993da5 --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/index.js @@ -0,0 +1,225 @@ +import ansiStyles from '#ansi-styles'; +import supportsColor from '#supports-color'; +import { // eslint-disable-line import/order + stringReplaceAll, + stringEncaseCRLFWithFirstIndex, +} from './utilities.js'; + +const {stdout: stdoutColor, stderr: stderrColor} = supportsColor; + +const GENERATOR = Symbol('GENERATOR'); +const STYLER = Symbol('STYLER'); +const IS_EMPTY = Symbol('IS_EMPTY'); + +// `supportsColor.level` → `ansiStyles.color[name]` mapping +const levelMapping = [ + 'ansi', + 'ansi', + 'ansi256', + 'ansi16m', +]; + +const styles = Object.create(null); + +const applyOptions = (object, options = {}) => { + if (options.level && !(Number.isInteger(options.level) && options.level >= 0 && options.level <= 3)) { + throw new Error('The `level` option should be an integer from 0 to 3'); + } + + // Detect level if not set manually + const colorLevel = stdoutColor ? stdoutColor.level : 0; + object.level = options.level === undefined ? colorLevel : options.level; +}; + +export class Chalk { + constructor(options) { + // eslint-disable-next-line no-constructor-return + return chalkFactory(options); + } +} + +const chalkFactory = options => { + const chalk = (...strings) => strings.join(' '); + applyOptions(chalk, options); + + Object.setPrototypeOf(chalk, createChalk.prototype); + + return chalk; +}; + +function createChalk(options) { + return chalkFactory(options); +} + +Object.setPrototypeOf(createChalk.prototype, Function.prototype); + +for (const [styleName, style] of Object.entries(ansiStyles)) { + styles[styleName] = { + get() { + const builder = createBuilder(this, createStyler(style.open, style.close, this[STYLER]), this[IS_EMPTY]); + Object.defineProperty(this, styleName, {value: builder}); + return builder; + }, + }; +} + +styles.visible = { + get() { + const builder = createBuilder(this, this[STYLER], true); + Object.defineProperty(this, 'visible', {value: builder}); + return builder; + }, +}; + +const getModelAnsi = (model, level, type, ...arguments_) => { + if (model === 'rgb') { + if (level === 'ansi16m') { + return ansiStyles[type].ansi16m(...arguments_); + } + + if (level === 'ansi256') { + return ansiStyles[type].ansi256(ansiStyles.rgbToAnsi256(...arguments_)); + } + + return ansiStyles[type].ansi(ansiStyles.rgbToAnsi(...arguments_)); + } + + if (model === 'hex') { + return getModelAnsi('rgb', level, type, ...ansiStyles.hexToRgb(...arguments_)); + } + + return ansiStyles[type][model](...arguments_); +}; + +const usedModels = ['rgb', 'hex', 'ansi256']; + +for (const model of usedModels) { + styles[model] = { + get() { + const {level} = this; + return function (...arguments_) { + const styler = createStyler(getModelAnsi(model, levelMapping[level], 'color', ...arguments_), ansiStyles.color.close, this[STYLER]); + return createBuilder(this, styler, this[IS_EMPTY]); + }; + }, + }; + + const bgModel = 'bg' + model[0].toUpperCase() + model.slice(1); + styles[bgModel] = { + get() { + const {level} = this; + return function (...arguments_) { + const styler = createStyler(getModelAnsi(model, levelMapping[level], 'bgColor', ...arguments_), ansiStyles.bgColor.close, this[STYLER]); + return createBuilder(this, styler, this[IS_EMPTY]); + }; + }, + }; +} + +const proto = Object.defineProperties(() => {}, { + ...styles, + level: { + enumerable: true, + get() { + return this[GENERATOR].level; + }, + set(level) { + this[GENERATOR].level = level; + }, + }, +}); + +const createStyler = (open, close, parent) => { + let openAll; + let closeAll; + if (parent === undefined) { + openAll = open; + closeAll = close; + } else { + openAll = parent.openAll + open; + closeAll = close + parent.closeAll; + } + + return { + open, + close, + openAll, + closeAll, + parent, + }; +}; + +const createBuilder = (self, _styler, _isEmpty) => { + // Single argument is hot path, implicit coercion is faster than anything + // eslint-disable-next-line no-implicit-coercion + const builder = (...arguments_) => applyStyle(builder, (arguments_.length === 1) ? ('' + arguments_[0]) : arguments_.join(' ')); + + // We alter the prototype because we must return a function, but there is + // no way to create a function with a different prototype + Object.setPrototypeOf(builder, proto); + + builder[GENERATOR] = self; + builder[STYLER] = _styler; + builder[IS_EMPTY] = _isEmpty; + + return builder; +}; + +const applyStyle = (self, string) => { + if (self.level <= 0 || !string) { + return self[IS_EMPTY] ? '' : string; + } + + let styler = self[STYLER]; + + if (styler === undefined) { + return string; + } + + const {openAll, closeAll} = styler; + if (string.includes('\u001B')) { + while (styler !== undefined) { + // Replace any instances already present with a re-opening code + // otherwise only the part of the string until said closing code + // will be colored, and the rest will simply be 'plain'. + string = stringReplaceAll(string, styler.close, styler.open); + + styler = styler.parent; + } + } + + // We can move both next actions out of loop, because remaining actions in loop won't have + // any/visible effect on parts we add here. Close the styling before a linebreak and reopen + // after next line to fix a bleed issue on macOS: https://github.com/chalk/chalk/pull/92 + const lfIndex = string.indexOf('\n'); + if (lfIndex !== -1) { + string = stringEncaseCRLFWithFirstIndex(string, closeAll, openAll, lfIndex); + } + + return openAll + string + closeAll; +}; + +Object.defineProperties(createChalk.prototype, styles); + +const chalk = createChalk(); +export const chalkStderr = createChalk({level: stderrColor ? stderrColor.level : 0}); + +export { + modifierNames, + foregroundColorNames, + backgroundColorNames, + colorNames, + + // TODO: Remove these aliases in the next major version + modifierNames as modifiers, + foregroundColorNames as foregroundColors, + backgroundColorNames as backgroundColors, + colorNames as colors, +} from './vendor/ansi-styles/index.js'; + +export { + stdoutColor as supportsColor, + stderrColor as supportsColorStderr, +}; + +export default chalk; diff --git a/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/utilities.js b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/utilities.js new file mode 100644 index 000000000..4366dee0d --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/utilities.js @@ -0,0 +1,33 @@ +// TODO: When targeting Node.js 16, use `String.prototype.replaceAll`. +export function stringReplaceAll(string, substring, replacer) { + let index = string.indexOf(substring); + if (index === -1) { + return string; + } + + const substringLength = substring.length; + let endIndex = 0; + let returnValue = ''; + do { + returnValue += string.slice(endIndex, index) + substring + replacer; + endIndex = index + substringLength; + index = string.indexOf(substring, endIndex); + } while (index !== -1); + + returnValue += string.slice(endIndex); + return returnValue; +} + +export function stringEncaseCRLFWithFirstIndex(string, prefix, postfix, index) { + let endIndex = 0; + let returnValue = ''; + do { + const gotCR = string[index - 1] === '\r'; + returnValue += string.slice(endIndex, (gotCR ? index - 1 : index)) + prefix + (gotCR ? '\r\n' : '\n') + postfix; + endIndex = index + 1; + index = string.indexOf('\n', endIndex); + } while (index !== -1); + + returnValue += string.slice(endIndex); + return returnValue; +} diff --git a/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/ansi-styles/index.d.ts b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/ansi-styles/index.d.ts new file mode 100644 index 000000000..58f133abe --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/ansi-styles/index.d.ts @@ -0,0 +1,236 @@ +export interface CSPair { // eslint-disable-line @typescript-eslint/naming-convention + /** + The ANSI terminal control sequence for starting this style. + */ + readonly open: string; + + /** + The ANSI terminal control sequence for ending this style. + */ + readonly close: string; +} + +export interface ColorBase { + /** + The ANSI terminal control sequence for ending this color. + */ + readonly close: string; + + ansi(code: number): string; + + ansi256(code: number): string; + + ansi16m(red: number, green: number, blue: number): string; +} + +export interface Modifier { + /** + Resets the current color chain. + */ + readonly reset: CSPair; + + /** + Make text bold. + */ + readonly bold: CSPair; + + /** + Emitting only a small amount of light. + */ + readonly dim: CSPair; + + /** + Make text italic. (Not widely supported) + */ + readonly italic: CSPair; + + /** + Make text underline. (Not widely supported) + */ + readonly underline: CSPair; + + /** + Make text overline. + + Supported on VTE-based terminals, the GNOME terminal, mintty, and Git Bash. + */ + readonly overline: CSPair; + + /** + Inverse background and foreground colors. + */ + readonly inverse: CSPair; + + /** + Prints the text, but makes it invisible. + */ + readonly hidden: CSPair; + + /** + Puts a horizontal line through the center of the text. (Not widely supported) + */ + readonly strikethrough: CSPair; +} + +export interface ForegroundColor { + readonly black: CSPair; + readonly red: CSPair; + readonly green: CSPair; + readonly yellow: CSPair; + readonly blue: CSPair; + readonly cyan: CSPair; + readonly magenta: CSPair; + readonly white: CSPair; + + /** + Alias for `blackBright`. + */ + readonly gray: CSPair; + + /** + Alias for `blackBright`. + */ + readonly grey: CSPair; + + readonly blackBright: CSPair; + readonly redBright: CSPair; + readonly greenBright: CSPair; + readonly yellowBright: CSPair; + readonly blueBright: CSPair; + readonly cyanBright: CSPair; + readonly magentaBright: CSPair; + readonly whiteBright: CSPair; +} + +export interface BackgroundColor { + readonly bgBlack: CSPair; + readonly bgRed: CSPair; + readonly bgGreen: CSPair; + readonly bgYellow: CSPair; + readonly bgBlue: CSPair; + readonly bgCyan: CSPair; + readonly bgMagenta: CSPair; + readonly bgWhite: CSPair; + + /** + Alias for `bgBlackBright`. + */ + readonly bgGray: CSPair; + + /** + Alias for `bgBlackBright`. + */ + readonly bgGrey: CSPair; + + readonly bgBlackBright: CSPair; + readonly bgRedBright: CSPair; + readonly bgGreenBright: CSPair; + readonly bgYellowBright: CSPair; + readonly bgBlueBright: CSPair; + readonly bgCyanBright: CSPair; + readonly bgMagentaBright: CSPair; + readonly bgWhiteBright: CSPair; +} + +export interface ConvertColor { + /** + Convert from the RGB color space to the ANSI 256 color space. + + @param red - (`0...255`) + @param green - (`0...255`) + @param blue - (`0...255`) + */ + rgbToAnsi256(red: number, green: number, blue: number): number; + + /** + Convert from the RGB HEX color space to the RGB color space. + + @param hex - A hexadecimal string containing RGB data. + */ + hexToRgb(hex: string): [red: number, green: number, blue: number]; + + /** + Convert from the RGB HEX color space to the ANSI 256 color space. + + @param hex - A hexadecimal string containing RGB data. + */ + hexToAnsi256(hex: string): number; + + /** + Convert from the ANSI 256 color space to the ANSI 16 color space. + + @param code - A number representing the ANSI 256 color. + */ + ansi256ToAnsi(code: number): number; + + /** + Convert from the RGB color space to the ANSI 16 color space. + + @param red - (`0...255`) + @param green - (`0...255`) + @param blue - (`0...255`) + */ + rgbToAnsi(red: number, green: number, blue: number): number; + + /** + Convert from the RGB HEX color space to the ANSI 16 color space. + + @param hex - A hexadecimal string containing RGB data. + */ + hexToAnsi(hex: string): number; +} + +/** +Basic modifier names. +*/ +export type ModifierName = keyof Modifier; + +/** +Basic foreground color names. + +[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support) +*/ +export type ForegroundColorName = keyof ForegroundColor; + +/** +Basic background color names. + +[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support) +*/ +export type BackgroundColorName = keyof BackgroundColor; + +/** +Basic color names. The combination of foreground and background color names. + +[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support) +*/ +export type ColorName = ForegroundColorName | BackgroundColorName; + +/** +Basic modifier names. +*/ +export const modifierNames: readonly ModifierName[]; + +/** +Basic foreground color names. +*/ +export const foregroundColorNames: readonly ForegroundColorName[]; + +/** +Basic background color names. +*/ +export const backgroundColorNames: readonly BackgroundColorName[]; + +/* +Basic color names. The combination of foreground and background color names. +*/ +export const colorNames: readonly ColorName[]; + +declare const ansiStyles: { + readonly modifier: Modifier; + readonly color: ColorBase & ForegroundColor; + readonly bgColor: ColorBase & BackgroundColor; + readonly codes: ReadonlyMap; +} & ForegroundColor & BackgroundColor & Modifier & ConvertColor; + +export default ansiStyles; diff --git a/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/ansi-styles/index.js b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/ansi-styles/index.js new file mode 100644 index 000000000..eaa7bed6c --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/ansi-styles/index.js @@ -0,0 +1,223 @@ +const ANSI_BACKGROUND_OFFSET = 10; + +const wrapAnsi16 = (offset = 0) => code => `\u001B[${code + offset}m`; + +const wrapAnsi256 = (offset = 0) => code => `\u001B[${38 + offset};5;${code}m`; + +const wrapAnsi16m = (offset = 0) => (red, green, blue) => `\u001B[${38 + offset};2;${red};${green};${blue}m`; + +const styles = { + modifier: { + reset: [0, 0], + // 21 isn't widely supported and 22 does the same thing + bold: [1, 22], + dim: [2, 22], + italic: [3, 23], + underline: [4, 24], + overline: [53, 55], + inverse: [7, 27], + hidden: [8, 28], + strikethrough: [9, 29], + }, + color: { + black: [30, 39], + red: [31, 39], + green: [32, 39], + yellow: [33, 39], + blue: [34, 39], + magenta: [35, 39], + cyan: [36, 39], + white: [37, 39], + + // Bright color + blackBright: [90, 39], + gray: [90, 39], // Alias of `blackBright` + grey: [90, 39], // Alias of `blackBright` + redBright: [91, 39], + greenBright: [92, 39], + yellowBright: [93, 39], + blueBright: [94, 39], + magentaBright: [95, 39], + cyanBright: [96, 39], + whiteBright: [97, 39], + }, + bgColor: { + bgBlack: [40, 49], + bgRed: [41, 49], + bgGreen: [42, 49], + bgYellow: [43, 49], + bgBlue: [44, 49], + bgMagenta: [45, 49], + bgCyan: [46, 49], + bgWhite: [47, 49], + + // Bright color + bgBlackBright: [100, 49], + bgGray: [100, 49], // Alias of `bgBlackBright` + bgGrey: [100, 49], // Alias of `bgBlackBright` + bgRedBright: [101, 49], + bgGreenBright: [102, 49], + bgYellowBright: [103, 49], + bgBlueBright: [104, 49], + bgMagentaBright: [105, 49], + bgCyanBright: [106, 49], + bgWhiteBright: [107, 49], + }, +}; + +export const modifierNames = Object.keys(styles.modifier); +export const foregroundColorNames = Object.keys(styles.color); +export const backgroundColorNames = Object.keys(styles.bgColor); +export const colorNames = [...foregroundColorNames, ...backgroundColorNames]; + +function assembleStyles() { + const codes = new Map(); + + for (const [groupName, group] of Object.entries(styles)) { + for (const [styleName, style] of Object.entries(group)) { + styles[styleName] = { + open: `\u001B[${style[0]}m`, + close: `\u001B[${style[1]}m`, + }; + + group[styleName] = styles[styleName]; + + codes.set(style[0], style[1]); + } + + Object.defineProperty(styles, groupName, { + value: group, + enumerable: false, + }); + } + + Object.defineProperty(styles, 'codes', { + value: codes, + enumerable: false, + }); + + styles.color.close = '\u001B[39m'; + styles.bgColor.close = '\u001B[49m'; + + styles.color.ansi = wrapAnsi16(); + styles.color.ansi256 = wrapAnsi256(); + styles.color.ansi16m = wrapAnsi16m(); + styles.bgColor.ansi = wrapAnsi16(ANSI_BACKGROUND_OFFSET); + styles.bgColor.ansi256 = wrapAnsi256(ANSI_BACKGROUND_OFFSET); + styles.bgColor.ansi16m = wrapAnsi16m(ANSI_BACKGROUND_OFFSET); + + // From https://github.com/Qix-/color-convert/blob/3f0e0d4e92e235796ccb17f6e85c72094a651f49/conversions.js + Object.defineProperties(styles, { + rgbToAnsi256: { + value(red, green, blue) { + // We use the extended greyscale palette here, with the exception of + // black and white. normal palette only has 4 greyscale shades. + if (red === green && green === blue) { + if (red < 8) { + return 16; + } + + if (red > 248) { + return 231; + } + + return Math.round(((red - 8) / 247) * 24) + 232; + } + + return 16 + + (36 * Math.round(red / 255 * 5)) + + (6 * Math.round(green / 255 * 5)) + + Math.round(blue / 255 * 5); + }, + enumerable: false, + }, + hexToRgb: { + value(hex) { + const matches = /[a-f\d]{6}|[a-f\d]{3}/i.exec(hex.toString(16)); + if (!matches) { + return [0, 0, 0]; + } + + let [colorString] = matches; + + if (colorString.length === 3) { + colorString = [...colorString].map(character => character + character).join(''); + } + + const integer = Number.parseInt(colorString, 16); + + return [ + /* eslint-disable no-bitwise */ + (integer >> 16) & 0xFF, + (integer >> 8) & 0xFF, + integer & 0xFF, + /* eslint-enable no-bitwise */ + ]; + }, + enumerable: false, + }, + hexToAnsi256: { + value: hex => styles.rgbToAnsi256(...styles.hexToRgb(hex)), + enumerable: false, + }, + ansi256ToAnsi: { + value(code) { + if (code < 8) { + return 30 + code; + } + + if (code < 16) { + return 90 + (code - 8); + } + + let red; + let green; + let blue; + + if (code >= 232) { + red = (((code - 232) * 10) + 8) / 255; + green = red; + blue = red; + } else { + code -= 16; + + const remainder = code % 36; + + red = Math.floor(code / 36) / 5; + green = Math.floor(remainder / 6) / 5; + blue = (remainder % 6) / 5; + } + + const value = Math.max(red, green, blue) * 2; + + if (value === 0) { + return 30; + } + + // eslint-disable-next-line no-bitwise + let result = 30 + ((Math.round(blue) << 2) | (Math.round(green) << 1) | Math.round(red)); + + if (value === 2) { + result += 60; + } + + return result; + }, + enumerable: false, + }, + rgbToAnsi: { + value: (red, green, blue) => styles.ansi256ToAnsi(styles.rgbToAnsi256(red, green, blue)), + enumerable: false, + }, + hexToAnsi: { + value: hex => styles.ansi256ToAnsi(styles.hexToAnsi256(hex)), + enumerable: false, + }, + }); + + return styles; +} + +const ansiStyles = assembleStyles(); + +export default ansiStyles; diff --git a/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/browser.d.ts b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/browser.d.ts new file mode 100644 index 000000000..0e8a9ccb8 --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/browser.d.ts @@ -0,0 +1 @@ +export {default} from './index.js'; diff --git a/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/browser.js b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/browser.js new file mode 100644 index 000000000..fbb6ce0fc --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/browser.js @@ -0,0 +1,34 @@ +/* eslint-env browser */ + +const level = (() => { + if (!('navigator' in globalThis)) { + return 0; + } + + if (globalThis.navigator.userAgentData) { + const brand = navigator.userAgentData.brands.find(({brand}) => brand === 'Chromium'); + if (brand && brand.version > 93) { + return 3; + } + } + + if (/\b(Chrome|Chromium)\//.test(globalThis.navigator.userAgent)) { + return 1; + } + + return 0; +})(); + +const colorSupport = level !== 0 && { + level, + hasBasic: true, + has256: level >= 2, + has16m: level >= 3, +}; + +const supportsColor = { + stdout: colorSupport, + stderr: colorSupport, +}; + +export default supportsColor; diff --git a/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/index.d.ts b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/index.d.ts new file mode 100644 index 000000000..db44a7851 --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/index.d.ts @@ -0,0 +1,55 @@ +import type {WriteStream} from 'node:tty'; + +export type Options = { + /** + Whether `process.argv` should be sniffed for `--color` and `--no-color` flags. + + @default true + */ + readonly sniffFlags?: boolean; +}; + +/** +Levels: +- `0` - All colors disabled. +- `1` - Basic 16 colors support. +- `2` - ANSI 256 colors support. +- `3` - Truecolor 16 million colors support. +*/ +export type ColorSupportLevel = 0 | 1 | 2 | 3; + +/** +Detect whether the terminal supports color. +*/ +export type ColorSupport = { + /** + The color level. + */ + level: ColorSupportLevel; + + /** + Whether basic 16 colors are supported. + */ + hasBasic: boolean; + + /** + Whether ANSI 256 colors are supported. + */ + has256: boolean; + + /** + Whether Truecolor 16 million colors are supported. + */ + has16m: boolean; +}; + +export type ColorInfo = ColorSupport | false; + +export function createSupportsColor(stream?: WriteStream, options?: Options): ColorInfo; + +declare const supportsColor: { + stdout: ColorInfo; + stderr: ColorInfo; +}; + +export default supportsColor; diff --git a/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/index.js b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/index.js new file mode 100644 index 000000000..265d7f858 --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/index.js @@ -0,0 +1,190 @@ +import process from 'node:process'; +import os from 'node:os'; +import tty from 'node:tty'; + +// From: https://github.com/sindresorhus/has-flag/blob/main/index.js +/// function hasFlag(flag, argv = globalThis.Deno?.args ?? process.argv) { +function hasFlag(flag, argv = globalThis.Deno ? globalThis.Deno.args : process.argv) { + const prefix = flag.startsWith('-') ? '' : (flag.length === 1 ? '-' : '--'); + const position = argv.indexOf(prefix + flag); + const terminatorPosition = argv.indexOf('--'); + return position !== -1 && (terminatorPosition === -1 || position < terminatorPosition); +} + +const {env} = process; + +let flagForceColor; +if ( + hasFlag('no-color') + || hasFlag('no-colors') + || hasFlag('color=false') + || hasFlag('color=never') +) { + flagForceColor = 0; +} else if ( + hasFlag('color') + || hasFlag('colors') + || hasFlag('color=true') + || hasFlag('color=always') +) { + flagForceColor = 1; +} + +function envForceColor() { + if ('FORCE_COLOR' in env) { + if (env.FORCE_COLOR === 'true') { + return 1; + } + + if (env.FORCE_COLOR === 'false') { + return 0; + } + + return env.FORCE_COLOR.length === 0 ? 1 : Math.min(Number.parseInt(env.FORCE_COLOR, 10), 3); + } +} + +function translateLevel(level) { + if (level === 0) { + return false; + } + + return { + level, + hasBasic: true, + has256: level >= 2, + has16m: level >= 3, + }; +} + +function _supportsColor(haveStream, {streamIsTTY, sniffFlags = true} = {}) { + const noFlagForceColor = envForceColor(); + if (noFlagForceColor !== undefined) { + flagForceColor = noFlagForceColor; + } + + const forceColor = sniffFlags ? flagForceColor : noFlagForceColor; + + if (forceColor === 0) { + return 0; + } + + if (sniffFlags) { + if (hasFlag('color=16m') + || hasFlag('color=full') + || hasFlag('color=truecolor')) { + return 3; + } + + if (hasFlag('color=256')) { + return 2; + } + } + + // Check for Azure DevOps pipelines. + // Has to be above the `!streamIsTTY` check. + if ('TF_BUILD' in env && 'AGENT_NAME' in env) { + return 1; + } + + if (haveStream && !streamIsTTY && forceColor === undefined) { + return 0; + } + + const min = forceColor || 0; + + if (env.TERM === 'dumb') { + return min; + } + + if (process.platform === 'win32') { + // Windows 10 build 10586 is the first Windows release that supports 256 colors. + // Windows 10 build 14931 is the first release that supports 16m/TrueColor. + const osRelease = os.release().split('.'); + if ( + Number(osRelease[0]) >= 10 + && Number(osRelease[2]) >= 10_586 + ) { + return Number(osRelease[2]) >= 14_931 ? 3 : 2; + } + + return 1; + } + + if ('CI' in env) { + if (['GITHUB_ACTIONS', 'GITEA_ACTIONS', 'CIRCLECI'].some(key => key in env)) { + return 3; + } + + if (['TRAVIS', 'APPVEYOR', 'GITLAB_CI', 'BUILDKITE', 'DRONE'].some(sign => sign in env) || env.CI_NAME === 'codeship') { + return 1; + } + + return min; + } + + if ('TEAMCITY_VERSION' in env) { + return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION) ? 1 : 0; + } + + if (env.COLORTERM === 'truecolor') { + return 3; + } + + if (env.TERM === 'xterm-kitty') { + return 3; + } + + if (env.TERM === 'xterm-ghostty') { + return 3; + } + + if (env.TERM === 'wezterm') { + return 3; + } + + if ('TERM_PROGRAM' in env) { + const version = Number.parseInt((env.TERM_PROGRAM_VERSION || '').split('.')[0], 10); + + switch (env.TERM_PROGRAM) { + case 'iTerm.app': { + return version >= 3 ? 3 : 2; + } + + case 'Apple_Terminal': { + return 2; + } + // No default + } + } + + if (/-256(color)?$/i.test(env.TERM)) { + return 2; + } + + if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(env.TERM)) { + return 1; + } + + if ('COLORTERM' in env) { + return 1; + } + + return min; +} + +export function createSupportsColor(stream, options = {}) { + const level = _supportsColor(stream, { + streamIsTTY: stream && stream.isTTY, + ...options, + }); + + return translateLevel(level); +} + +const supportsColor = { + stdout: createSupportsColor({isTTY: tty.isatty(1)}), + stderr: createSupportsColor({isTTY: tty.isatty(2)}), +}; + +export default supportsColor; diff --git a/tests/fixtures/npm/cases/chalk5-builder/package.json b/tests/fixtures/npm/cases/chalk5-builder/package.json new file mode 100644 index 000000000..3804daa8d --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/package.json @@ -0,0 +1 @@ +{ "name": "chalk5-case", "private": true, "type": "module" } diff --git a/tests/fixtures/npm/cases/decl-pass-through/main.ts b/tests/fixtures/npm/cases/decl-pass-through/main.ts new file mode 100644 index 000000000..a8168edc4 --- /dev/null +++ b/tests/fixtures/npm/cases/decl-pass-through/main.ts @@ -0,0 +1,16 @@ +// Package functions declared to return their argument's own type +// (`(obj: T) => T`) hand back the argument, or a proxy +// standing in for it, and the package tracks that object's identity: those +// results stay the package's objects. Reads of data the declarations +// describe (`point(): Point`) are still typed. +import { byReference, equals, mark, point } from "ref-marks"; + +const a = byReference({ n: 42 }); +const b = byReference({ n: 42 }); +console.log(equals(a, a), equals(a, b), a.n + b.n); +const m = mark({ n: 1 }); +const m2 = mark({ n: 1 }); +console.log(equals(m, m), equals(m, m2)); +const p = point(1, 2); +const q = point(1, 2); +console.log(equals(p, q), p.x + q.y); diff --git a/tests/fixtures/npm/cases/decl-pass-through/node_modules/ref-marks/lib/index.d.ts b/tests/fixtures/npm/cases/decl-pass-through/node_modules/ref-marks/lib/index.d.ts new file mode 100644 index 000000000..aa0df42f4 --- /dev/null +++ b/tests/fixtures/npm/cases/decl-pass-through/node_modules/ref-marks/lib/index.d.ts @@ -0,0 +1,8 @@ +export interface Point { + x: number; + y: number; +} +export declare function byReference(obj: T): T; +export declare const mark: (obj: T) => T; +export declare function equals(a: unknown, b: unknown): boolean; +export declare function point(x: number, y: number): Point; diff --git a/tests/fixtures/npm/cases/decl-pass-through/node_modules/ref-marks/lib/index.js b/tests/fixtures/npm/cases/decl-pass-through/node_modules/ref-marks/lib/index.js new file mode 100644 index 000000000..796efb19b --- /dev/null +++ b/tests/fixtures/npm/cases/decl-pass-through/node_modules/ref-marks/lib/index.js @@ -0,0 +1,18 @@ +const marked = new WeakSet(); +export function byReference(obj) { + const proxy = new Proxy(obj, {}); + marked.add(proxy); + return proxy; +} +export function mark(obj) { + marked.add(obj); + return obj; +} +export function equals(a, b) { + if (a === b) return true; + if (marked.has(a) || marked.has(b)) return false; + return JSON.stringify(a) === JSON.stringify(b); +} +export function point(x, y) { + return { x, y }; +} diff --git a/tests/fixtures/npm/cases/decl-pass-through/node_modules/ref-marks/package.json b/tests/fixtures/npm/cases/decl-pass-through/node_modules/ref-marks/package.json new file mode 100644 index 000000000..5ae95f315 --- /dev/null +++ b/tests/fixtures/npm/cases/decl-pass-through/node_modules/ref-marks/package.json @@ -0,0 +1 @@ +{ "name": "ref-marks", "version": "1.0.0", "type": "module", "main": "./lib/index.js", "types": "./lib/index.d.ts" } diff --git a/tests/fixtures/npm/cases/decl-pass-through/package.json b/tests/fixtures/npm/cases/decl-pass-through/package.json new file mode 100644 index 000000000..4895e9aec --- /dev/null +++ b/tests/fixtures/npm/cases/decl-pass-through/package.json @@ -0,0 +1 @@ +{ "name": "decl-pass-through-case", "private": true, "type": "module" } diff --git a/tests/fixtures/npm/cases/hot-shots-buffered/main.ts b/tests/fixtures/npm/cases/hot-shots-buffered/main.ts new file mode 100644 index 000000000..d17ce1167 --- /dev/null +++ b/tests/fixtures/npm/cases/hot-shots-buffered/main.ts @@ -0,0 +1,80 @@ +// The hot-shots StatsD client (compiled from scriptc's built-in shim) in +// buffered mode: metrics join one datagram until maxBufferSize, close +// flushes the rest, child clients share the buffer. Also explicit sample +// rates of 1, the telegraf line format, stat arrays with a callback, and +// suffixes. +import * as dgram from "node:dgram"; +import { StatsD } from "hot-shots"; + +const server = dgram.createSocket("udp4"); +const received: string[] = []; +let waiting: (() => void) | null = null; +let want = 0; +server.on("message", (msg) => { + received.push(msg.toString()); + if (waiting !== null && received.length >= want) { + const done = waiting; + waiting = null; + done(); + } +}); + +function datagrams(count: number): Promise { + want = count; + return new Promise((resolve) => { + if (received.length >= want) resolve(); + else waiting = resolve; + }); +} + +function flush(label: string): void { + console.log("--", label); + for (const line of received.splice(0)) console.log(JSON.stringify(line)); +} + +async function run(port: number): Promise { + const buffered = new StatsD({ + host: "127.0.0.1", + port, + maxBufferSize: 64, + bufferFlushInterval: 60000, + suffix: ".s", + globalTags: ["team:core"], + }); + buffered.increment("one"); + buffered.increment("two", 2); + buffered.gauge("three", 3); + buffered.histogram("four", 4, ["h:1"]); + const child = buffered.childClient({ prefix: "kid.", suffix: ".x" }); + child.increment("five"); + await new Promise((resolve) => buffered.close(() => resolve())); + await datagrams(2); + flush("buffered"); + + const telegraf = new StatsD({ host: "127.0.0.1", port, telegraf: true, globalTags: { region: "eu" } }); + telegraf.increment("hits", 1, 1, { route: "/x" }); + telegraf.timing("lat", 7, ["kind:a"]); + await datagrams(2); + flush("telegraf"); + + const plain = new StatsD({ host: "127.0.0.1", port, prefix: "p." }); + await new Promise((resolve) => { + plain.increment(["a", "b", "c"], 1, 1, ["t:1"], (err: unknown, bytes: unknown) => { + console.log("array sent", err, bytes); + resolve(); + }); + }); + plain.set("users", 42, 1); + plain.unique("visitors", "v1"); + plain.decrement("down", 3, ["d:1"]); + plain.decrement("gone"); + await datagrams(7); + flush("plain"); + await new Promise((resolve) => { + telegraf.close(() => plain.close(() => resolve())); + }); +} + +server.bind(0, "127.0.0.1", () => { + void run(server.address().port).then(() => server.close(() => console.log("server closed"))); +}); diff --git a/tests/fixtures/npm/cases/hot-shots-buffered/package.json b/tests/fixtures/npm/cases/hot-shots-buffered/package.json new file mode 100644 index 000000000..5ffd9800b --- /dev/null +++ b/tests/fixtures/npm/cases/hot-shots-buffered/package.json @@ -0,0 +1 @@ +{ "type": "module" } diff --git a/tests/fixtures/npm/cases/hot-shots-send-error/main.ts b/tests/fixtures/npm/cases/hot-shots-send-error/main.ts new file mode 100644 index 000000000..e829c5e33 --- /dev/null +++ b/tests/fixtures/npm/cases/hot-shots-send-error/main.ts @@ -0,0 +1,27 @@ +// The hot-shots StatsD client (compiled from scriptc's built-in shim) when a +// send fails: with a callback or an errorHandler the error goes there; with +// neither, the client logs it and emits it on the socket, where an 'error' +// event nobody listens for ends the process with status 1. +import { StatsD } from "hot-shots"; + +// A datagram larger than udp allows fails with EMSGSIZE before it leaves +// the host; port 9 keeps the message the same on every run. +const big = "x".repeat(70000); + +const withCallback = new StatsD({ host: "127.0.0.1", port: 9 }); +withCallback.increment(big, 1, 1, [], (err: Error | null | undefined) => { + console.log("callback:", err ? err.message : "no error"); + withCallback.close(); + + const withHandler = new StatsD({ + host: "127.0.0.1", + port: 9, + errorHandler: (error: Error) => { + console.log("errorHandler:", error.message); + withHandler.close(); + console.log("without a callback or errorHandler"); + new StatsD({ host: "127.0.0.1", port: 9 }).increment(big); + }, + }); + withHandler.increment(big); +}); diff --git a/tests/fixtures/npm/cases/hot-shots-send-error/package.json b/tests/fixtures/npm/cases/hot-shots-send-error/package.json new file mode 100644 index 000000000..5ffd9800b --- /dev/null +++ b/tests/fixtures/npm/cases/hot-shots-send-error/package.json @@ -0,0 +1 @@ +{ "type": "module" } diff --git a/tests/fixtures/npm/cases/hot-shots-tcp/main.ts b/tests/fixtures/npm/cases/hot-shots-tcp/main.ts new file mode 100644 index 000000000..0812c90f1 --- /dev/null +++ b/tests/fixtures/npm/cases/hot-shots-tcp/main.ts @@ -0,0 +1,33 @@ +// The hot-shots StatsD client over tcp (compiled from scriptc's built-in +// shim): a send completes once the socket has written its line, so close() +// waits for lines that a full socket buffer holds back, and the receiver +// gets every line before the connection ends. +import * as net from "node:net"; +import { StatsD } from "hot-shots"; + +const LINES = 20000; +const name = "m".repeat(1000); +let bytes = 0; +let ended: () => void = () => {}; +const serverDone = new Promise((resolve) => { + ended = resolve; +}); +const server = net.createServer((socket) => { + socket.pause(); + setTimeout(() => socket.resume(), 150); + socket.on("data", (chunk: Buffer) => { + bytes += chunk.length; + }); + socket.on("end", () => ended()); +}); + +server.listen(0, "127.0.0.1", () => { + const port = (server.address() as net.AddressInfo).port; + const client = new StatsD({ host: "127.0.0.1", port, protocol: "tcp" }); + for (let i = 0; i < LINES; i++) client.increment(name); + const closed = new Promise((resolve) => client.close(() => resolve())); + void Promise.all([closed, serverDone]).then(() => { + console.log("closed; received", bytes, "of", LINES * (name.length + 5), "bytes"); + server.close(() => console.log("server closed")); + }); +}); diff --git a/tests/fixtures/npm/cases/hot-shots-tcp/package.json b/tests/fixtures/npm/cases/hot-shots-tcp/package.json new file mode 100644 index 000000000..5ffd9800b --- /dev/null +++ b/tests/fixtures/npm/cases/hot-shots-tcp/package.json @@ -0,0 +1 @@ +{ "type": "module" } diff --git a/tests/fixtures/npm/cases/js-own-tostring/main.ts b/tests/fixtures/npm/cases/js-own-tostring/main.ts new file mode 100644 index 000000000..9793b2baa --- /dev/null +++ b/tests/fixtures/npm/cases/js-own-tostring/main.ts @@ -0,0 +1,16 @@ +// A JavaScript package (the TypeScript namespace downlevel zod uses) whose +// namespace object assigns its own `toString` returning string | undefined; +// Number.parseInt with one argument; and a `then` whose handler result the +// declared union types (zod's ParseStatus.mergeArray pattern). +import { check, direct, mergeLater, note, show, showRadix, wholePart } from "nsutil"; + +console.log(String(direct("a")), String(direct({ message: "b" })), String(direct(undefined))); +console.log(JSON.stringify(check(undefined)), JSON.stringify(check("m"))); +console.log(wholePart(1.23), wholePart(0.35), wholePart(-4.5), wholePart(12)); +const values: unknown[] = [12, "s", true, 1.5]; +console.log(values.map((value) => show(value)).join(" "), showRadix(5, 2), note.length); +async function main(): Promise { + console.log(JSON.stringify(await mergeLater([Promise.resolve(1), Promise.resolve("two")], { ok: true }))); + console.log(JSON.stringify(await mergeLater([Promise.resolve(3), Promise.resolve("four")], { ok: false }))); +} +void main(); diff --git a/tests/fixtures/npm/cases/js-own-tostring/node_modules/nsutil/dist/errorUtil.d.ts b/tests/fixtures/npm/cases/js-own-tostring/node_modules/nsutil/dist/errorUtil.d.ts new file mode 100644 index 000000000..bc9b033ff --- /dev/null +++ b/tests/fixtures/npm/cases/js-own-tostring/node_modules/nsutil/dist/errorUtil.d.ts @@ -0,0 +1,9 @@ +export declare namespace errorUtil { + type ErrMessage = string | { + message?: string | undefined; + }; + const errToObj: (message?: ErrMessage) => { + message?: string | undefined; + }; + const toString: (message?: ErrMessage) => string | undefined; +} diff --git a/tests/fixtures/npm/cases/js-own-tostring/node_modules/nsutil/dist/errorUtil.js b/tests/fixtures/npm/cases/js-own-tostring/node_modules/nsutil/dist/errorUtil.js new file mode 100644 index 000000000..1b48d927b --- /dev/null +++ b/tests/fixtures/npm/cases/js-own-tostring/node_modules/nsutil/dist/errorUtil.js @@ -0,0 +1,5 @@ +export var errorUtil; +(function (errorUtil) { + errorUtil.errToObj = (message) => typeof message === "string" ? { message } : message || {}; + errorUtil.toString = (message) => typeof message === "string" ? message : message?.message; +})(errorUtil || (errorUtil = {})); diff --git a/tests/fixtures/npm/cases/js-own-tostring/node_modules/nsutil/dist/index.d.ts b/tests/fixtures/npm/cases/js-own-tostring/node_modules/nsutil/dist/index.d.ts new file mode 100644 index 000000000..18f135cad --- /dev/null +++ b/tests/fixtures/npm/cases/js-own-tostring/node_modules/nsutil/dist/index.d.ts @@ -0,0 +1,9 @@ +import { errorUtil } from "./errorUtil.js"; +export declare function check(message?: errorUtil.ErrMessage): { kind: "int"; message?: string }; +export declare function direct(message?: errorUtil.ErrMessage): string | undefined; +export declare function wholePart(value: number): number; +export type Merged = { status: "valid"; value: unknown[] } | { status: "aborted" }; +export declare function mergeLater(items: Promise[], status: { ok: boolean }): Promise; +export declare const note: string; +export declare function show(value: unknown): string; +export declare function showRadix(value: number, radix: number): string; diff --git a/tests/fixtures/npm/cases/js-own-tostring/node_modules/nsutil/dist/index.js b/tests/fixtures/npm/cases/js-own-tostring/node_modules/nsutil/dist/index.js new file mode 100644 index 000000000..105187f49 --- /dev/null +++ b/tests/fixtures/npm/cases/js-own-tostring/node_modules/nsutil/dist/index.js @@ -0,0 +1,28 @@ +import { errorUtil } from "./errorUtil.js"; +export function check(message) { + return { kind: "int", message: errorUtil.toString(message) }; +} +export function direct(message) { + return errorUtil.toString(message); +} +export function wholePart(value) { + return Number.parseInt(String(value).replace(".", "")) + Number.parseInt("0x1f"); +} +export function mergeLater(items, status) { + return Promise.all(items).then((results) => status.ok ? { status: "valid", value: results } : INVALID); +} +const INVALID = Object.freeze({ status: "aborted" }); +// Only an assignment to this binding's own toString counts: not this +// comment's value.toString = (x) => x, a string, or another function's +// parameter of the same name. +export const note = "value.toString = (x) => x"; +export function show(value) { + return value.toString(); +} +export function showRadix(value, radix) { + return value.toString(radix); +} +export function patch(value) { + value.toString = () => "patched"; + return value; +} diff --git a/tests/fixtures/npm/cases/js-own-tostring/node_modules/nsutil/package.json b/tests/fixtures/npm/cases/js-own-tostring/node_modules/nsutil/package.json new file mode 100644 index 000000000..a2757b8a1 --- /dev/null +++ b/tests/fixtures/npm/cases/js-own-tostring/node_modules/nsutil/package.json @@ -0,0 +1 @@ +{ "name": "nsutil", "version": "1.0.0", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts" } diff --git a/tests/fixtures/npm/cases/js-own-tostring/package.json b/tests/fixtures/npm/cases/js-own-tostring/package.json new file mode 100644 index 000000000..5ffd9800b --- /dev/null +++ b/tests/fixtures/npm/cases/js-own-tostring/package.json @@ -0,0 +1 @@ +{ "type": "module" } diff --git a/tests/fixtures/npm/cases/jsdoc-unresolved-generic-return/main.ts b/tests/fixtures/npm/cases/jsdoc-unresolved-generic-return/main.ts new file mode 100644 index 000000000..ce6ef5a94 --- /dev/null +++ b/tests/fixtures/npm/cases/jsdoc-unresolved-generic-return/main.ts @@ -0,0 +1,19 @@ +// A published bundle's generic factory whose JSDoc names its result by a +// type that only the package's .d.ts declares (hono's `bearerAuth`: +// `@template E` + `@returns {MiddlewareHandler}`), passed straight into +// a generic registration method. The unresolved JSDoc result is not a +// contract, so the call's result is checked-dynamic. +import { guard, Router } from "jsdocmw"; + +const router = new Router(); +router.on("/admin", guard({ token: "s3cret" }), async (ctx, next) => { + ctx.log.push(`handler ${ctx.path}`); + await next(); +}); +const open = guard({ token: "" }); +router.on("/open", open, async (ctx) => { + ctx.log.push(`open handler ${ctx.path}`); +}); +console.log(await router.dispatch("/admin", "s3cret")); +console.log(await router.dispatch("/admin", "nope")); +console.log(await router.dispatch("/open", "")); diff --git a/tests/fixtures/npm/cases/types-boundary/main.ts b/tests/fixtures/npm/cases/types-boundary/main.ts new file mode 100644 index 000000000..c33e6e9bf --- /dev/null +++ b/tests/fixtures/npm/cases/types-boundary/main.ts @@ -0,0 +1,19 @@ +// A plain-JavaScript package typed by an @types package: rows the +// declarations type as plain data become native records; the cursor, a +// constructor-function instance its declarations describe with an +// interface, keeps its JavaScript-world typing. +import { parseRows, cursor, type Row } from "rowparse"; + +const rows: Row[] = parseRows("name,qty\napple,3\npear,5\nfig,2\n"); +let total = 0; +for (const r of rows) total += Number(r.cells["qty"]); +console.log("total", total, rows.length); +const widest = rows.reduce((w, r) => Math.max(w, r.width), 0); +console.log("widest", widest); +rows[0]!.cells["qty"] = "30"; +console.log("first", JSON.stringify(rows[0]), Object.keys(rows[1]!.cells).join("|")); +const sorted = [...rows].sort((a, b) => a.cells["name"]!.localeCompare(b.cells["name"]!)).map((r) => r.line); +console.log("sorted", sorted.join(",")); +const c = cursor("abcdef"); +c.pos = 2; +console.log("cursor", c.remaining(), c.text); diff --git a/tests/fixtures/npm/cases/types-boundary/node_modules/@types/rowparse/index.d.ts b/tests/fixtures/npm/cases/types-boundary/node_modules/@types/rowparse/index.d.ts new file mode 100644 index 000000000..8e099794d --- /dev/null +++ b/tests/fixtures/npm/cases/types-boundary/node_modules/@types/rowparse/index.d.ts @@ -0,0 +1,12 @@ +export type Row = { + line: number; + cells: { [column: string]: string }; + width: number; +}; +export declare function parseRows(text: string): Row[]; +export interface Cursor { + text: string; + pos: number; + remaining(): number; +} +export declare function cursor(text: string): Cursor; diff --git a/tests/fixtures/npm/cases/types-boundary/node_modules/@types/rowparse/package.json b/tests/fixtures/npm/cases/types-boundary/node_modules/@types/rowparse/package.json new file mode 100644 index 000000000..7ce3bcc0e --- /dev/null +++ b/tests/fixtures/npm/cases/types-boundary/node_modules/@types/rowparse/package.json @@ -0,0 +1 @@ +{ "name": "@types/rowparse", "version": "1.0.0", "types": "index.d.ts" } diff --git a/tests/fixtures/npm/cases/types-boundary/node_modules/rowparse/index.js b/tests/fixtures/npm/cases/types-boundary/node_modules/rowparse/index.js new file mode 100644 index 000000000..4a53c8481 --- /dev/null +++ b/tests/fixtures/npm/cases/types-boundary/node_modules/rowparse/index.js @@ -0,0 +1,26 @@ +"use strict"; +// Plain JavaScript: rows are built key by key, so inference sees nothing. +function Cursor(text) { + this.text = text; + this.pos = 0; +} +Cursor.prototype.remaining = function () { + return this.text.length - this.pos; +}; +function parseRows(text) { + var lines = String(text).split("\n").filter(Boolean); + var header = lines[0].split(","); + var rows = []; + for (var i = 1; i < lines.length; i++) { + var cells = lines[i].split(","); + var row = {}; + for (var j = 0; j < header.length; j++) row[header[j]] = cells[j]; + rows.push({ line: i, cells: row, width: cells.length }); + } + return rows; +} +function cursor(text) { + return new Cursor(text); +} +exports.parseRows = parseRows; +exports.cursor = cursor; diff --git a/tests/fixtures/npm/cases/types-boundary/node_modules/rowparse/package.json b/tests/fixtures/npm/cases/types-boundary/node_modules/rowparse/package.json new file mode 100644 index 000000000..da66597bc --- /dev/null +++ b/tests/fixtures/npm/cases/types-boundary/node_modules/rowparse/package.json @@ -0,0 +1 @@ +{ "name": "rowparse", "version": "1.0.0", "main": "index.js" } diff --git a/tests/fixtures/npm/cases/types-boundary/package.json b/tests/fixtures/npm/cases/types-boundary/package.json new file mode 100644 index 000000000..bae20e44c --- /dev/null +++ b/tests/fixtures/npm/cases/types-boundary/package.json @@ -0,0 +1 @@ +{ "name": "types-boundary-case", "private": true, "type": "module" } diff --git a/tests/fixtures/npm/cases/zod3-adoption/main.ts b/tests/fixtures/npm/cases/zod3-adoption/main.ts new file mode 100644 index 000000000..d21ff69ee --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/main.ts @@ -0,0 +1,48 @@ +// Adoption at the declaration-typed boundary: objects package code still +// references (a cache, weak keys, parsed documents, a cycle through an +// unknown field) keep their identity when they cross into typed code. +import { z } from "zod"; + +const Row = z.object({ id: z.number(), name: z.string(), tags: z.array(z.string()), meta: z.unknown() }); +type Row = z.infer; + +// A package-side cache: the same objects come back from every call. +const cache: unknown[] = []; +const weak = new WeakMap(); +const Cached = z.custom<{ id: number; tags: string[] }>().transform((v) => v); +for (let i = 0; i < 4; i++) { + const raw = JSON.parse(`{"id":${i},"name":"n${i}","tags":["a","b"],"meta":{"deep":[1,2,{"x":${i}}]}}`) as unknown; + cache.push(raw); + weak.set(raw as object, "w" + i); +} +let sum = 0; +for (let round = 0; round < 200; round++) { + for (let i = 0; i < cache.length; i++) { + const r: Row = Row.parse(cache[i]); + sum += r.id + r.tags.length; + const again = Row.parse(cache[i]); + if (round === 0) console.log("fresh", r === again, r.name, JSON.stringify(r.meta)); + } +} +console.log("sum", sum); +// The package keeps the source objects; z.custom passes them through. +const kept = cache.map((c) => Cached.parse(c)); +kept[0]!.tags.push("c"); +console.log("kept", JSON.stringify(cache[0]), kept[0] === Cached.parse(cache[0])); +console.log("weak", weak.get(cache[1] as object), weak.has(cache[2] as object)); +// A cycle through an unknown field. +const AsRow = z.custom(); +const cyc: Row = { id: 9, name: "c", tags: [], meta: null }; +cyc.meta = cyc; +const back = AsRow.parse(cyc); +back.id = 10; +console.log("cycle", cyc.id, back === cyc, back.meta === cyc); +// A tuple's elements keep their identity too: the package-side array +// still holds the element that typed code mutates. +const Pair = z.custom<[{ id: number; tags: string[] }, string]>(); +const pairSource = JSON.parse('[{"id":1,"tags":["a"]},"s"]') as unknown[]; +const pairHead = pairSource[0]; +const pair = Pair.parse(pairSource); +pair[0].tags.push("b"); +pair[0].id = 2; +console.log("tuple", JSON.stringify(pairHead), JSON.stringify(pairSource), pair[1]); diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/LICENSE b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/LICENSE new file mode 100644 index 000000000..c06579627 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Colin McDonnell + +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/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/index.js b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/index.js new file mode 100644 index 000000000..8352ab713 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/index.js @@ -0,0 +1,3 @@ +import z3 from "./v3/index.js"; +export * from "./v3/index.js"; +export default z3; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/package.json b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/package.json new file mode 100644 index 000000000..aead43de3 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} \ No newline at end of file diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/ZodError.js b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/ZodError.js new file mode 100644 index 000000000..c72ce5288 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/ZodError.js @@ -0,0 +1,132 @@ +import { util } from "./helpers/util.js"; +export const ZodIssueCode = util.arrayToEnum([ + "invalid_type", + "invalid_literal", + "custom", + "invalid_union", + "invalid_union_discriminator", + "invalid_enum_value", + "unrecognized_keys", + "invalid_arguments", + "invalid_return_type", + "invalid_date", + "invalid_string", + "too_small", + "too_big", + "invalid_intersection_types", + "not_multiple_of", + "not_finite", +]); +export const quotelessJson = (obj) => { + const json = JSON.stringify(obj, null, 2); + return json.replace(/"([^"]+)":/g, "$1:"); +}; +export class ZodError extends Error { + get errors() { + return this.issues; + } + constructor(issues) { + super(); + this.issues = []; + this.addIssue = (sub) => { + this.issues = [...this.issues, sub]; + }; + this.addIssues = (subs = []) => { + this.issues = [...this.issues, ...subs]; + }; + const actualProto = new.target.prototype; + if (Object.setPrototypeOf) { + // eslint-disable-next-line ban/ban + Object.setPrototypeOf(this, actualProto); + } + else { + this.__proto__ = actualProto; + } + this.name = "ZodError"; + this.issues = issues; + } + format(_mapper) { + const mapper = _mapper || + function (issue) { + return issue.message; + }; + const fieldErrors = { _errors: [] }; + const processError = (error) => { + for (const issue of error.issues) { + if (issue.code === "invalid_union") { + issue.unionErrors.map(processError); + } + else if (issue.code === "invalid_return_type") { + processError(issue.returnTypeError); + } + else if (issue.code === "invalid_arguments") { + processError(issue.argumentsError); + } + else if (issue.path.length === 0) { + fieldErrors._errors.push(mapper(issue)); + } + else { + let curr = fieldErrors; + let i = 0; + while (i < issue.path.length) { + const el = issue.path[i]; + const terminal = i === issue.path.length - 1; + if (!terminal) { + curr[el] = curr[el] || { _errors: [] }; + // if (typeof el === "string") { + // curr[el] = curr[el] || { _errors: [] }; + // } else if (typeof el === "number") { + // const errorArray: any = []; + // errorArray._errors = []; + // curr[el] = curr[el] || errorArray; + // } + } + else { + curr[el] = curr[el] || { _errors: [] }; + curr[el]._errors.push(mapper(issue)); + } + curr = curr[el]; + i++; + } + } + } + }; + processError(this); + return fieldErrors; + } + static assert(value) { + if (!(value instanceof ZodError)) { + throw new Error(`Not a ZodError: ${value}`); + } + } + toString() { + return this.message; + } + get message() { + return JSON.stringify(this.issues, util.jsonStringifyReplacer, 2); + } + get isEmpty() { + return this.issues.length === 0; + } + flatten(mapper = (issue) => issue.message) { + const fieldErrors = {}; + const formErrors = []; + for (const sub of this.issues) { + if (sub.path.length > 0) { + fieldErrors[sub.path[0]] = fieldErrors[sub.path[0]] || []; + fieldErrors[sub.path[0]].push(mapper(sub)); + } + else { + formErrors.push(mapper(sub)); + } + } + return { formErrors, fieldErrors }; + } + get formErrors() { + return this.flatten(); + } +} +ZodError.create = (issues) => { + const error = new ZodError(issues); + return error; +}; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/errors.js b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/errors.js new file mode 100644 index 000000000..26fdf628a --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/errors.js @@ -0,0 +1,9 @@ +import defaultErrorMap from "./locales/en.js"; +let overrideErrorMap = defaultErrorMap; +export { defaultErrorMap }; +export function setErrorMap(map) { + overrideErrorMap = map; +} +export function getErrorMap() { + return overrideErrorMap; +} diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/external.js b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/external.js new file mode 100644 index 000000000..f0a4be4c7 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/external.js @@ -0,0 +1,6 @@ +export * from "./errors.js"; +export * from "./helpers/parseUtil.js"; +export * from "./helpers/typeAliases.js"; +export * from "./helpers/util.js"; +export * from "./types.js"; +export * from "./ZodError.js"; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/enumUtil.js b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/enumUtil.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/enumUtil.js @@ -0,0 +1 @@ +export {}; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/errorUtil.js b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/errorUtil.js new file mode 100644 index 000000000..e1640d1a4 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/errorUtil.js @@ -0,0 +1,6 @@ +export var errorUtil; +(function (errorUtil) { + errorUtil.errToObj = (message) => typeof message === "string" ? { message } : message || {}; + // biome-ignore lint: + errorUtil.toString = (message) => typeof message === "string" ? message : message?.message; +})(errorUtil || (errorUtil = {})); diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/parseUtil.js b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/parseUtil.js new file mode 100644 index 000000000..91b2afb81 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/parseUtil.js @@ -0,0 +1,109 @@ +import { getErrorMap } from "../errors.js"; +import defaultErrorMap from "../locales/en.js"; +export const makeIssue = (params) => { + const { data, path, errorMaps, issueData } = params; + const fullPath = [...path, ...(issueData.path || [])]; + const fullIssue = { + ...issueData, + path: fullPath, + }; + if (issueData.message !== undefined) { + return { + ...issueData, + path: fullPath, + message: issueData.message, + }; + } + let errorMessage = ""; + const maps = errorMaps + .filter((m) => !!m) + .slice() + .reverse(); + for (const map of maps) { + errorMessage = map(fullIssue, { data, defaultError: errorMessage }).message; + } + return { + ...issueData, + path: fullPath, + message: errorMessage, + }; +}; +export const EMPTY_PATH = []; +export function addIssueToContext(ctx, issueData) { + const overrideMap = getErrorMap(); + const issue = makeIssue({ + issueData: issueData, + data: ctx.data, + path: ctx.path, + errorMaps: [ + ctx.common.contextualErrorMap, // contextual error map is first priority + ctx.schemaErrorMap, // then schema-bound map if available + overrideMap, // then global override map + overrideMap === defaultErrorMap ? undefined : defaultErrorMap, // then global default map + ].filter((x) => !!x), + }); + ctx.common.issues.push(issue); +} +export class ParseStatus { + constructor() { + this.value = "valid"; + } + dirty() { + if (this.value === "valid") + this.value = "dirty"; + } + abort() { + if (this.value !== "aborted") + this.value = "aborted"; + } + static mergeArray(status, results) { + const arrayValue = []; + for (const s of results) { + if (s.status === "aborted") + return INVALID; + if (s.status === "dirty") + status.dirty(); + arrayValue.push(s.value); + } + return { status: status.value, value: arrayValue }; + } + static async mergeObjectAsync(status, pairs) { + const syncPairs = []; + for (const pair of pairs) { + const key = await pair.key; + const value = await pair.value; + syncPairs.push({ + key, + value, + }); + } + return ParseStatus.mergeObjectSync(status, syncPairs); + } + static mergeObjectSync(status, pairs) { + const finalObject = {}; + for (const pair of pairs) { + const { key, value } = pair; + if (key.status === "aborted") + return INVALID; + if (value.status === "aborted") + return INVALID; + if (key.status === "dirty") + status.dirty(); + if (value.status === "dirty") + status.dirty(); + if (key.value !== "__proto__" && (typeof value.value !== "undefined" || pair.alwaysSet)) { + finalObject[key.value] = value.value; + } + } + return { status: status.value, value: finalObject }; + } +} +export const INVALID = Object.freeze({ + status: "aborted", +}); +export const DIRTY = (value) => ({ status: "dirty", value }); +export const OK = (value) => ({ status: "valid", value }); +export const isAborted = (x) => x.status === "aborted"; +export const isDirty = (x) => x.status === "dirty"; +export const isValid = (x) => x.status === "valid"; +export const isAsync = (x) => typeof Promise !== "undefined" && x instanceof Promise; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/partialUtil.js b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/partialUtil.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/partialUtil.js @@ -0,0 +1 @@ +export {}; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/typeAliases.js b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/typeAliases.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/typeAliases.js @@ -0,0 +1 @@ +export {}; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/util.js b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/util.js new file mode 100644 index 000000000..b9641f29b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/util.js @@ -0,0 +1,133 @@ +export var util; +(function (util) { + util.assertEqual = (_) => { }; + function assertIs(_arg) { } + util.assertIs = assertIs; + function assertNever(_x) { + throw new Error(); + } + util.assertNever = assertNever; + util.arrayToEnum = (items) => { + const obj = {}; + for (const item of items) { + obj[item] = item; + } + return obj; + }; + util.getValidEnumValues = (obj) => { + const validKeys = util.objectKeys(obj).filter((k) => typeof obj[obj[k]] !== "number"); + const filtered = {}; + for (const k of validKeys) { + filtered[k] = obj[k]; + } + return util.objectValues(filtered); + }; + util.objectValues = (obj) => { + return util.objectKeys(obj).map(function (e) { + return obj[e]; + }); + }; + util.objectKeys = typeof Object.keys === "function" // eslint-disable-line ban/ban + ? (obj) => Object.keys(obj) // eslint-disable-line ban/ban + : (object) => { + const keys = []; + for (const key in object) { + if (Object.prototype.hasOwnProperty.call(object, key)) { + keys.push(key); + } + } + return keys; + }; + util.find = (arr, checker) => { + for (const item of arr) { + if (checker(item)) + return item; + } + return undefined; + }; + util.isInteger = typeof Number.isInteger === "function" + ? (val) => Number.isInteger(val) // eslint-disable-line ban/ban + : (val) => typeof val === "number" && Number.isFinite(val) && Math.floor(val) === val; + function joinValues(array, separator = " | ") { + return array.map((val) => (typeof val === "string" ? `'${val}'` : val)).join(separator); + } + util.joinValues = joinValues; + util.jsonStringifyReplacer = (_, value) => { + if (typeof value === "bigint") { + return value.toString(); + } + return value; + }; +})(util || (util = {})); +export var objectUtil; +(function (objectUtil) { + objectUtil.mergeShapes = (first, second) => { + return { + ...first, + ...second, // second overwrites first + }; + }; +})(objectUtil || (objectUtil = {})); +export const ZodParsedType = util.arrayToEnum([ + "string", + "nan", + "number", + "integer", + "float", + "boolean", + "date", + "bigint", + "symbol", + "function", + "undefined", + "null", + "array", + "object", + "unknown", + "promise", + "void", + "never", + "map", + "set", +]); +export const getParsedType = (data) => { + const t = typeof data; + switch (t) { + case "undefined": + return ZodParsedType.undefined; + case "string": + return ZodParsedType.string; + case "number": + return Number.isNaN(data) ? ZodParsedType.nan : ZodParsedType.number; + case "boolean": + return ZodParsedType.boolean; + case "function": + return ZodParsedType.function; + case "bigint": + return ZodParsedType.bigint; + case "symbol": + return ZodParsedType.symbol; + case "object": + if (Array.isArray(data)) { + return ZodParsedType.array; + } + if (data === null) { + return ZodParsedType.null; + } + if (data.then && typeof data.then === "function" && data.catch && typeof data.catch === "function") { + return ZodParsedType.promise; + } + if (typeof Map !== "undefined" && data instanceof Map) { + return ZodParsedType.map; + } + if (typeof Set !== "undefined" && data instanceof Set) { + return ZodParsedType.set; + } + if (typeof Date !== "undefined" && data instanceof Date) { + return ZodParsedType.date; + } + return ZodParsedType.object; + default: + return ZodParsedType.unknown; + } +}; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/index.js b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/index.js new file mode 100644 index 000000000..a9dab5738 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/index.js @@ -0,0 +1,4 @@ +import * as z from "./external.js"; +export * from "./external.js"; +export { z }; +export default z; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/locales/en.js b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/locales/en.js new file mode 100644 index 000000000..3e97ad51a --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/locales/en.js @@ -0,0 +1,107 @@ +import { ZodIssueCode } from "../ZodError.js"; +import { util, ZodParsedType } from "../helpers/util.js"; +const errorMap = (issue, _ctx) => { + let message; + switch (issue.code) { + case ZodIssueCode.invalid_type: + if (issue.received === ZodParsedType.undefined) { + message = "Required"; + } + else { + message = `Expected ${issue.expected}, received ${issue.received}`; + } + break; + case ZodIssueCode.invalid_literal: + message = `Invalid literal value, expected ${JSON.stringify(issue.expected, util.jsonStringifyReplacer)}`; + break; + case ZodIssueCode.unrecognized_keys: + message = `Unrecognized key(s) in object: ${util.joinValues(issue.keys, ", ")}`; + break; + case ZodIssueCode.invalid_union: + message = `Invalid input`; + break; + case ZodIssueCode.invalid_union_discriminator: + message = `Invalid discriminator value. Expected ${util.joinValues(issue.options)}`; + break; + case ZodIssueCode.invalid_enum_value: + message = `Invalid enum value. Expected ${util.joinValues(issue.options)}, received '${issue.received}'`; + break; + case ZodIssueCode.invalid_arguments: + message = `Invalid function arguments`; + break; + case ZodIssueCode.invalid_return_type: + message = `Invalid function return type`; + break; + case ZodIssueCode.invalid_date: + message = `Invalid date`; + break; + case ZodIssueCode.invalid_string: + if (typeof issue.validation === "object") { + if ("includes" in issue.validation) { + message = `Invalid input: must include "${issue.validation.includes}"`; + if (typeof issue.validation.position === "number") { + message = `${message} at one or more positions greater than or equal to ${issue.validation.position}`; + } + } + else if ("startsWith" in issue.validation) { + message = `Invalid input: must start with "${issue.validation.startsWith}"`; + } + else if ("endsWith" in issue.validation) { + message = `Invalid input: must end with "${issue.validation.endsWith}"`; + } + else { + util.assertNever(issue.validation); + } + } + else if (issue.validation !== "regex") { + message = `Invalid ${issue.validation}`; + } + else { + message = "Invalid"; + } + break; + case ZodIssueCode.too_small: + if (issue.type === "array") + message = `Array must contain ${issue.exact ? "exactly" : issue.inclusive ? `at least` : `more than`} ${issue.minimum} element(s)`; + else if (issue.type === "string") + message = `String must contain ${issue.exact ? "exactly" : issue.inclusive ? `at least` : `over`} ${issue.minimum} character(s)`; + else if (issue.type === "number") + message = `Number must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${issue.minimum}`; + else if (issue.type === "date") + message = `Date must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${new Date(Number(issue.minimum))}`; + else + message = "Invalid input"; + break; + case ZodIssueCode.too_big: + if (issue.type === "array") + message = `Array must contain ${issue.exact ? `exactly` : issue.inclusive ? `at most` : `less than`} ${issue.maximum} element(s)`; + else if (issue.type === "string") + message = `String must contain ${issue.exact ? `exactly` : issue.inclusive ? `at most` : `under`} ${issue.maximum} character(s)`; + else if (issue.type === "number") + message = `Number must be ${issue.exact ? `exactly` : issue.inclusive ? `less than or equal to` : `less than`} ${issue.maximum}`; + else if (issue.type === "bigint") + message = `BigInt must be ${issue.exact ? `exactly` : issue.inclusive ? `less than or equal to` : `less than`} ${issue.maximum}`; + else if (issue.type === "date") + message = `Date must be ${issue.exact ? `exactly` : issue.inclusive ? `smaller than or equal to` : `smaller than`} ${new Date(Number(issue.maximum))}`; + else + message = "Invalid input"; + break; + case ZodIssueCode.custom: + message = `Invalid input`; + break; + case ZodIssueCode.invalid_intersection_types: + message = `Intersection results could not be merged`; + break; + case ZodIssueCode.not_multiple_of: + message = `Number must be a multiple of ${issue.multipleOf}`; + break; + case ZodIssueCode.not_finite: + message = "Number must be finite"; + break; + default: + message = _ctx.defaultError; + util.assertNever(issue); + } + return { message }; +}; +export default errorMap; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/standard-schema.js b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/standard-schema.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/standard-schema.js @@ -0,0 +1 @@ +export {}; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/types.js b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/types.js new file mode 100644 index 000000000..de1e6213e --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/types.js @@ -0,0 +1,3691 @@ +import { ZodError, ZodIssueCode, } from "./ZodError.js"; +import { defaultErrorMap, getErrorMap } from "./errors.js"; +import { errorUtil } from "./helpers/errorUtil.js"; +import { DIRTY, INVALID, OK, ParseStatus, addIssueToContext, isAborted, isAsync, isDirty, isValid, makeIssue, } from "./helpers/parseUtil.js"; +import { util, ZodParsedType, getParsedType } from "./helpers/util.js"; +class ParseInputLazyPath { + constructor(parent, value, path, key) { + this._cachedPath = []; + this.parent = parent; + this.data = value; + this._path = path; + this._key = key; + } + get path() { + if (!this._cachedPath.length) { + if (Array.isArray(this._key)) { + this._cachedPath.push(...this._path, ...this._key); + } + else { + this._cachedPath.push(...this._path, this._key); + } + } + return this._cachedPath; + } +} +const handleResult = (ctx, result) => { + if (isValid(result)) { + return { success: true, data: result.value }; + } + else { + if (!ctx.common.issues.length) { + throw new Error("Validation failed but no issues detected."); + } + return { + success: false, + get error() { + if (this._error) + return this._error; + const error = new ZodError(ctx.common.issues); + this._error = error; + return this._error; + }, + }; + } +}; +function processCreateParams(params) { + if (!params) + return {}; + const { errorMap, invalid_type_error, required_error, description } = params; + if (errorMap && (invalid_type_error || required_error)) { + throw new Error(`Can't use "invalid_type_error" or "required_error" in conjunction with custom error map.`); + } + if (errorMap) + return { errorMap: errorMap, description }; + const customMap = (iss, ctx) => { + const { message } = params; + if (iss.code === "invalid_enum_value") { + return { message: message ?? ctx.defaultError }; + } + if (typeof ctx.data === "undefined") { + return { message: message ?? required_error ?? ctx.defaultError }; + } + if (iss.code !== "invalid_type") + return { message: ctx.defaultError }; + return { message: message ?? invalid_type_error ?? ctx.defaultError }; + }; + return { errorMap: customMap, description }; +} +export class ZodType { + get description() { + return this._def.description; + } + _getType(input) { + return getParsedType(input.data); + } + _getOrReturnCtx(input, ctx) { + return (ctx || { + common: input.parent.common, + data: input.data, + parsedType: getParsedType(input.data), + schemaErrorMap: this._def.errorMap, + path: input.path, + parent: input.parent, + }); + } + _processInputParams(input) { + return { + status: new ParseStatus(), + ctx: { + common: input.parent.common, + data: input.data, + parsedType: getParsedType(input.data), + schemaErrorMap: this._def.errorMap, + path: input.path, + parent: input.parent, + }, + }; + } + _parseSync(input) { + const result = this._parse(input); + if (isAsync(result)) { + throw new Error("Synchronous parse encountered promise."); + } + return result; + } + _parseAsync(input) { + const result = this._parse(input); + return Promise.resolve(result); + } + parse(data, params) { + const result = this.safeParse(data, params); + if (result.success) + return result.data; + throw result.error; + } + safeParse(data, params) { + const ctx = { + common: { + issues: [], + async: params?.async ?? false, + contextualErrorMap: params?.errorMap, + }, + path: params?.path || [], + schemaErrorMap: this._def.errorMap, + parent: null, + data, + parsedType: getParsedType(data), + }; + const result = this._parseSync({ data, path: ctx.path, parent: ctx }); + return handleResult(ctx, result); + } + "~validate"(data) { + const ctx = { + common: { + issues: [], + async: !!this["~standard"].async, + }, + path: [], + schemaErrorMap: this._def.errorMap, + parent: null, + data, + parsedType: getParsedType(data), + }; + if (!this["~standard"].async) { + try { + const result = this._parseSync({ data, path: [], parent: ctx }); + return isValid(result) + ? { + value: result.value, + } + : { + issues: ctx.common.issues, + }; + } + catch (err) { + if (err?.message?.toLowerCase()?.includes("encountered")) { + this["~standard"].async = true; + } + ctx.common = { + issues: [], + async: true, + }; + } + } + return this._parseAsync({ data, path: [], parent: ctx }).then((result) => isValid(result) + ? { + value: result.value, + } + : { + issues: ctx.common.issues, + }); + } + async parseAsync(data, params) { + const result = await this.safeParseAsync(data, params); + if (result.success) + return result.data; + throw result.error; + } + async safeParseAsync(data, params) { + const ctx = { + common: { + issues: [], + contextualErrorMap: params?.errorMap, + async: true, + }, + path: params?.path || [], + schemaErrorMap: this._def.errorMap, + parent: null, + data, + parsedType: getParsedType(data), + }; + const maybeAsyncResult = this._parse({ data, path: ctx.path, parent: ctx }); + const result = await (isAsync(maybeAsyncResult) ? maybeAsyncResult : Promise.resolve(maybeAsyncResult)); + return handleResult(ctx, result); + } + refine(check, message) { + const getIssueProperties = (val) => { + if (typeof message === "string" || typeof message === "undefined") { + return { message }; + } + else if (typeof message === "function") { + return message(val); + } + else { + return message; + } + }; + return this._refinement((val, ctx) => { + const result = check(val); + const setError = () => ctx.addIssue({ + code: ZodIssueCode.custom, + ...getIssueProperties(val), + }); + if (typeof Promise !== "undefined" && result instanceof Promise) { + return result.then((data) => { + if (!data) { + setError(); + return false; + } + else { + return true; + } + }); + } + if (!result) { + setError(); + return false; + } + else { + return true; + } + }); + } + refinement(check, refinementData) { + return this._refinement((val, ctx) => { + if (!check(val)) { + ctx.addIssue(typeof refinementData === "function" ? refinementData(val, ctx) : refinementData); + return false; + } + else { + return true; + } + }); + } + _refinement(refinement) { + return new ZodEffects({ + schema: this, + typeName: ZodFirstPartyTypeKind.ZodEffects, + effect: { type: "refinement", refinement }, + }); + } + superRefine(refinement) { + return this._refinement(refinement); + } + constructor(def) { + /** Alias of safeParseAsync */ + this.spa = this.safeParseAsync; + this._def = def; + this.parse = this.parse.bind(this); + this.safeParse = this.safeParse.bind(this); + this.parseAsync = this.parseAsync.bind(this); + this.safeParseAsync = this.safeParseAsync.bind(this); + this.spa = this.spa.bind(this); + this.refine = this.refine.bind(this); + this.refinement = this.refinement.bind(this); + this.superRefine = this.superRefine.bind(this); + this.optional = this.optional.bind(this); + this.nullable = this.nullable.bind(this); + this.nullish = this.nullish.bind(this); + this.array = this.array.bind(this); + this.promise = this.promise.bind(this); + this.or = this.or.bind(this); + this.and = this.and.bind(this); + this.transform = this.transform.bind(this); + this.brand = this.brand.bind(this); + this.default = this.default.bind(this); + this.catch = this.catch.bind(this); + this.describe = this.describe.bind(this); + this.pipe = this.pipe.bind(this); + this.readonly = this.readonly.bind(this); + this.isNullable = this.isNullable.bind(this); + this.isOptional = this.isOptional.bind(this); + this["~standard"] = { + version: 1, + vendor: "zod", + validate: (data) => this["~validate"](data), + }; + } + optional() { + return ZodOptional.create(this, this._def); + } + nullable() { + return ZodNullable.create(this, this._def); + } + nullish() { + return this.nullable().optional(); + } + array() { + return ZodArray.create(this); + } + promise() { + return ZodPromise.create(this, this._def); + } + or(option) { + return ZodUnion.create([this, option], this._def); + } + and(incoming) { + return ZodIntersection.create(this, incoming, this._def); + } + transform(transform) { + return new ZodEffects({ + ...processCreateParams(this._def), + schema: this, + typeName: ZodFirstPartyTypeKind.ZodEffects, + effect: { type: "transform", transform }, + }); + } + default(def) { + const defaultValueFunc = typeof def === "function" ? def : () => def; + return new ZodDefault({ + ...processCreateParams(this._def), + innerType: this, + defaultValue: defaultValueFunc, + typeName: ZodFirstPartyTypeKind.ZodDefault, + }); + } + brand() { + return new ZodBranded({ + typeName: ZodFirstPartyTypeKind.ZodBranded, + type: this, + ...processCreateParams(this._def), + }); + } + catch(def) { + const catchValueFunc = typeof def === "function" ? def : () => def; + return new ZodCatch({ + ...processCreateParams(this._def), + innerType: this, + catchValue: catchValueFunc, + typeName: ZodFirstPartyTypeKind.ZodCatch, + }); + } + describe(description) { + const This = this.constructor; + return new This({ + ...this._def, + description, + }); + } + pipe(target) { + return ZodPipeline.create(this, target); + } + readonly() { + return ZodReadonly.create(this); + } + isOptional() { + return this.safeParse(undefined).success; + } + isNullable() { + return this.safeParse(null).success; + } +} +const cuidRegex = /^c[^\s-]{8,}$/i; +const cuid2Regex = /^[0-9a-z]+$/; +const ulidRegex = /^[0-9A-HJKMNP-TV-Z]{26}$/i; +// const uuidRegex = +// /^([a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[a-f0-9]{4}-[a-f0-9]{12}|00000000-0000-0000-0000-000000000000)$/i; +const uuidRegex = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/i; +const nanoidRegex = /^[a-z0-9_-]{21}$/i; +const jwtRegex = /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$/; +const durationRegex = /^[-+]?P(?!$)(?:(?:[-+]?\d+Y)|(?:[-+]?\d+[.,]\d+Y$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:(?:[-+]?\d+W)|(?:[-+]?\d+[.,]\d+W$))?(?:(?:[-+]?\d+D)|(?:[-+]?\d+[.,]\d+D$))?(?:T(?=[\d+-])(?:(?:[-+]?\d+H)|(?:[-+]?\d+[.,]\d+H$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:[-+]?\d+(?:[.,]\d+)?S)?)??$/; +// from https://stackoverflow.com/a/46181/1550155 +// old version: too slow, didn't support unicode +// const emailRegex = /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i; +//old email regex +// const emailRegex = /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@((?!-)([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{1,})[^-<>()[\].,;:\s@"]$/i; +// eslint-disable-next-line +// const emailRegex = +// /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\])|(\[IPv6:(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))\])|([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])*(\.[A-Za-z]{2,})+))$/; +// const emailRegex = +// /^[a-zA-Z0-9\.\!\#\$\%\&\'\*\+\/\=\?\^\_\`\{\|\}\~\-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; +// const emailRegex = +// /^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$/i; +const emailRegex = /^(?!\.)(?!.*\.\.)([A-Z0-9_'+\-\.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i; +// const emailRegex = +// /^[a-z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-z0-9-]+(?:\.[a-z0-9\-]+)*$/i; +// from https://thekevinscott.com/emojis-in-javascript/#writing-a-regular-expression +const _emojiRegex = `^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$`; +let emojiRegex; +// faster, simpler, safer +const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/; +const ipv4CidrRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/(3[0-2]|[12]?[0-9])$/; +// const ipv6Regex = +// /^(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))$/; +const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/; +const ipv6CidrRegex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/; +// https://stackoverflow.com/questions/7860392/determine-if-string-is-in-base64-using-javascript +const base64Regex = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/; +// https://base64.guru/standards/base64url +const base64urlRegex = /^([0-9a-zA-Z-_]{4})*(([0-9a-zA-Z-_]{2}(==)?)|([0-9a-zA-Z-_]{3}(=)?))?$/; +// simple +// const dateRegexSource = `\\d{4}-\\d{2}-\\d{2}`; +// no leap year validation +// const dateRegexSource = `\\d{4}-((0[13578]|10|12)-31|(0[13-9]|1[0-2])-30|(0[1-9]|1[0-2])-(0[1-9]|1\\d|2\\d))`; +// with leap year validation +const dateRegexSource = `((\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\\d|3[01])|(0[469]|11)-(0[1-9]|[12]\\d|30)|(02)-(0[1-9]|1\\d|2[0-8])))`; +const dateRegex = new RegExp(`^${dateRegexSource}$`); +function timeRegexSource(args) { + let secondsRegexSource = `[0-5]\\d`; + if (args.precision) { + secondsRegexSource = `${secondsRegexSource}\\.\\d{${args.precision}}`; + } + else if (args.precision == null) { + secondsRegexSource = `${secondsRegexSource}(\\.\\d+)?`; + } + const secondsQuantifier = args.precision ? "+" : "?"; // require seconds if precision is nonzero + return `([01]\\d|2[0-3]):[0-5]\\d(:${secondsRegexSource})${secondsQuantifier}`; +} +function timeRegex(args) { + return new RegExp(`^${timeRegexSource(args)}$`); +} +// Adapted from https://stackoverflow.com/a/3143231 +export function datetimeRegex(args) { + let regex = `${dateRegexSource}T${timeRegexSource(args)}`; + const opts = []; + opts.push(args.local ? `Z?` : `Z`); + if (args.offset) + opts.push(`([+-]\\d{2}:?\\d{2})`); + regex = `${regex}(${opts.join("|")})`; + return new RegExp(`^${regex}$`); +} +function isValidIP(ip, version) { + if ((version === "v4" || !version) && ipv4Regex.test(ip)) { + return true; + } + if ((version === "v6" || !version) && ipv6Regex.test(ip)) { + return true; + } + return false; +} +function isValidJWT(jwt, alg) { + if (!jwtRegex.test(jwt)) + return false; + try { + const [header] = jwt.split("."); + // Convert base64url to base64 + const base64 = header + .replace(/-/g, "+") + .replace(/_/g, "/") + .padEnd(header.length + ((4 - (header.length % 4)) % 4), "="); + const decoded = JSON.parse(atob(base64)); + if (typeof decoded !== "object" || decoded === null) + return false; + if ("typ" in decoded && decoded?.typ !== "JWT") + return false; + if (!decoded.alg) + return false; + if (alg && decoded.alg !== alg) + return false; + return true; + } + catch { + return false; + } +} +function isValidCidr(ip, version) { + if ((version === "v4" || !version) && ipv4CidrRegex.test(ip)) { + return true; + } + if ((version === "v6" || !version) && ipv6CidrRegex.test(ip)) { + return true; + } + return false; +} +export class ZodString extends ZodType { + _parse(input) { + if (this._def.coerce) { + input.data = String(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.string) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.string, + received: ctx.parsedType, + }); + return INVALID; + } + const status = new ParseStatus(); + let ctx = undefined; + for (const check of this._def.checks) { + if (check.kind === "min") { + if (input.data.length < check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: check.value, + type: "string", + inclusive: true, + exact: false, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "max") { + if (input.data.length > check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: check.value, + type: "string", + inclusive: true, + exact: false, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "length") { + const tooBig = input.data.length > check.value; + const tooSmall = input.data.length < check.value; + if (tooBig || tooSmall) { + ctx = this._getOrReturnCtx(input, ctx); + if (tooBig) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: check.value, + type: "string", + inclusive: true, + exact: true, + message: check.message, + }); + } + else if (tooSmall) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: check.value, + type: "string", + inclusive: true, + exact: true, + message: check.message, + }); + } + status.dirty(); + } + } + else if (check.kind === "email") { + if (!emailRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "email", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "emoji") { + if (!emojiRegex) { + emojiRegex = new RegExp(_emojiRegex, "u"); + } + if (!emojiRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "emoji", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "uuid") { + if (!uuidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "uuid", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "nanoid") { + if (!nanoidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "nanoid", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "cuid") { + if (!cuidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cuid", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "cuid2") { + if (!cuid2Regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cuid2", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "ulid") { + if (!ulidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "ulid", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "url") { + try { + new URL(input.data); + } + catch { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "url", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "regex") { + check.regex.lastIndex = 0; + const testResult = check.regex.test(input.data); + if (!testResult) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "regex", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "trim") { + input.data = input.data.trim(); + } + else if (check.kind === "includes") { + if (!input.data.includes(check.value, check.position)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: { includes: check.value, position: check.position }, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "toLowerCase") { + input.data = input.data.toLowerCase(); + } + else if (check.kind === "toUpperCase") { + input.data = input.data.toUpperCase(); + } + else if (check.kind === "startsWith") { + if (!input.data.startsWith(check.value)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: { startsWith: check.value }, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "endsWith") { + if (!input.data.endsWith(check.value)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: { endsWith: check.value }, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "datetime") { + const regex = datetimeRegex(check); + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: "datetime", + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "date") { + const regex = dateRegex; + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: "date", + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "time") { + const regex = timeRegex(check); + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: "time", + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "duration") { + if (!durationRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "duration", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "ip") { + if (!isValidIP(input.data, check.version)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "ip", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "jwt") { + if (!isValidJWT(input.data, check.alg)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "jwt", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "cidr") { + if (!isValidCidr(input.data, check.version)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cidr", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "base64") { + if (!base64Regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "base64", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "base64url") { + if (!base64urlRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "base64url", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else { + util.assertNever(check); + } + } + return { status: status.value, value: input.data }; + } + _regex(regex, validation, message) { + return this.refinement((data) => regex.test(data), { + validation, + code: ZodIssueCode.invalid_string, + ...errorUtil.errToObj(message), + }); + } + _addCheck(check) { + return new ZodString({ + ...this._def, + checks: [...this._def.checks, check], + }); + } + email(message) { + return this._addCheck({ kind: "email", ...errorUtil.errToObj(message) }); + } + url(message) { + return this._addCheck({ kind: "url", ...errorUtil.errToObj(message) }); + } + emoji(message) { + return this._addCheck({ kind: "emoji", ...errorUtil.errToObj(message) }); + } + uuid(message) { + return this._addCheck({ kind: "uuid", ...errorUtil.errToObj(message) }); + } + nanoid(message) { + return this._addCheck({ kind: "nanoid", ...errorUtil.errToObj(message) }); + } + cuid(message) { + return this._addCheck({ kind: "cuid", ...errorUtil.errToObj(message) }); + } + cuid2(message) { + return this._addCheck({ kind: "cuid2", ...errorUtil.errToObj(message) }); + } + ulid(message) { + return this._addCheck({ kind: "ulid", ...errorUtil.errToObj(message) }); + } + base64(message) { + return this._addCheck({ kind: "base64", ...errorUtil.errToObj(message) }); + } + base64url(message) { + // base64url encoding is a modification of base64 that can safely be used in URLs and filenames + return this._addCheck({ + kind: "base64url", + ...errorUtil.errToObj(message), + }); + } + jwt(options) { + return this._addCheck({ kind: "jwt", ...errorUtil.errToObj(options) }); + } + ip(options) { + return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) }); + } + cidr(options) { + return this._addCheck({ kind: "cidr", ...errorUtil.errToObj(options) }); + } + datetime(options) { + if (typeof options === "string") { + return this._addCheck({ + kind: "datetime", + precision: null, + offset: false, + local: false, + message: options, + }); + } + return this._addCheck({ + kind: "datetime", + precision: typeof options?.precision === "undefined" ? null : options?.precision, + offset: options?.offset ?? false, + local: options?.local ?? false, + ...errorUtil.errToObj(options?.message), + }); + } + date(message) { + return this._addCheck({ kind: "date", message }); + } + time(options) { + if (typeof options === "string") { + return this._addCheck({ + kind: "time", + precision: null, + message: options, + }); + } + return this._addCheck({ + kind: "time", + precision: typeof options?.precision === "undefined" ? null : options?.precision, + ...errorUtil.errToObj(options?.message), + }); + } + duration(message) { + return this._addCheck({ kind: "duration", ...errorUtil.errToObj(message) }); + } + regex(regex, message) { + return this._addCheck({ + kind: "regex", + regex: regex, + ...errorUtil.errToObj(message), + }); + } + includes(value, options) { + return this._addCheck({ + kind: "includes", + value: value, + position: options?.position, + ...errorUtil.errToObj(options?.message), + }); + } + startsWith(value, message) { + return this._addCheck({ + kind: "startsWith", + value: value, + ...errorUtil.errToObj(message), + }); + } + endsWith(value, message) { + return this._addCheck({ + kind: "endsWith", + value: value, + ...errorUtil.errToObj(message), + }); + } + min(minLength, message) { + return this._addCheck({ + kind: "min", + value: minLength, + ...errorUtil.errToObj(message), + }); + } + max(maxLength, message) { + return this._addCheck({ + kind: "max", + value: maxLength, + ...errorUtil.errToObj(message), + }); + } + length(len, message) { + return this._addCheck({ + kind: "length", + value: len, + ...errorUtil.errToObj(message), + }); + } + /** + * Equivalent to `.min(1)` + */ + nonempty(message) { + return this.min(1, errorUtil.errToObj(message)); + } + trim() { + return new ZodString({ + ...this._def, + checks: [...this._def.checks, { kind: "trim" }], + }); + } + toLowerCase() { + return new ZodString({ + ...this._def, + checks: [...this._def.checks, { kind: "toLowerCase" }], + }); + } + toUpperCase() { + return new ZodString({ + ...this._def, + checks: [...this._def.checks, { kind: "toUpperCase" }], + }); + } + get isDatetime() { + return !!this._def.checks.find((ch) => ch.kind === "datetime"); + } + get isDate() { + return !!this._def.checks.find((ch) => ch.kind === "date"); + } + get isTime() { + return !!this._def.checks.find((ch) => ch.kind === "time"); + } + get isDuration() { + return !!this._def.checks.find((ch) => ch.kind === "duration"); + } + get isEmail() { + return !!this._def.checks.find((ch) => ch.kind === "email"); + } + get isURL() { + return !!this._def.checks.find((ch) => ch.kind === "url"); + } + get isEmoji() { + return !!this._def.checks.find((ch) => ch.kind === "emoji"); + } + get isUUID() { + return !!this._def.checks.find((ch) => ch.kind === "uuid"); + } + get isNANOID() { + return !!this._def.checks.find((ch) => ch.kind === "nanoid"); + } + get isCUID() { + return !!this._def.checks.find((ch) => ch.kind === "cuid"); + } + get isCUID2() { + return !!this._def.checks.find((ch) => ch.kind === "cuid2"); + } + get isULID() { + return !!this._def.checks.find((ch) => ch.kind === "ulid"); + } + get isIP() { + return !!this._def.checks.find((ch) => ch.kind === "ip"); + } + get isCIDR() { + return !!this._def.checks.find((ch) => ch.kind === "cidr"); + } + get isBase64() { + return !!this._def.checks.find((ch) => ch.kind === "base64"); + } + get isBase64url() { + // base64url encoding is a modification of base64 that can safely be used in URLs and filenames + return !!this._def.checks.find((ch) => ch.kind === "base64url"); + } + get minLength() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min; + } + get maxLength() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max; + } +} +ZodString.create = (params) => { + return new ZodString({ + checks: [], + typeName: ZodFirstPartyTypeKind.ZodString, + coerce: params?.coerce ?? false, + ...processCreateParams(params), + }); +}; +// https://stackoverflow.com/questions/3966484/why-does-modulus-operator-return-fractional-number-in-javascript/31711034#31711034 +function floatSafeRemainder(val, step) { + const valDecCount = (val.toString().split(".")[1] || "").length; + const stepDecCount = (step.toString().split(".")[1] || "").length; + const decCount = valDecCount > stepDecCount ? valDecCount : stepDecCount; + const valInt = Number.parseInt(val.toFixed(decCount).replace(".", "")); + const stepInt = Number.parseInt(step.toFixed(decCount).replace(".", "")); + return (valInt % stepInt) / 10 ** decCount; +} +export class ZodNumber extends ZodType { + constructor() { + super(...arguments); + this.min = this.gte; + this.max = this.lte; + this.step = this.multipleOf; + } + _parse(input) { + if (this._def.coerce) { + input.data = Number(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.number) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.number, + received: ctx.parsedType, + }); + return INVALID; + } + let ctx = undefined; + const status = new ParseStatus(); + for (const check of this._def.checks) { + if (check.kind === "int") { + if (!util.isInteger(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: "integer", + received: "float", + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "min") { + const tooSmall = check.inclusive ? input.data < check.value : input.data <= check.value; + if (tooSmall) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: check.value, + type: "number", + inclusive: check.inclusive, + exact: false, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "max") { + const tooBig = check.inclusive ? input.data > check.value : input.data >= check.value; + if (tooBig) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: check.value, + type: "number", + inclusive: check.inclusive, + exact: false, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "multipleOf") { + if (floatSafeRemainder(input.data, check.value) !== 0) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.not_multiple_of, + multipleOf: check.value, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "finite") { + if (!Number.isFinite(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.not_finite, + message: check.message, + }); + status.dirty(); + } + } + else { + util.assertNever(check); + } + } + return { status: status.value, value: input.data }; + } + gte(value, message) { + return this.setLimit("min", value, true, errorUtil.toString(message)); + } + gt(value, message) { + return this.setLimit("min", value, false, errorUtil.toString(message)); + } + lte(value, message) { + return this.setLimit("max", value, true, errorUtil.toString(message)); + } + lt(value, message) { + return this.setLimit("max", value, false, errorUtil.toString(message)); + } + setLimit(kind, value, inclusive, message) { + return new ZodNumber({ + ...this._def, + checks: [ + ...this._def.checks, + { + kind, + value, + inclusive, + message: errorUtil.toString(message), + }, + ], + }); + } + _addCheck(check) { + return new ZodNumber({ + ...this._def, + checks: [...this._def.checks, check], + }); + } + int(message) { + return this._addCheck({ + kind: "int", + message: errorUtil.toString(message), + }); + } + positive(message) { + return this._addCheck({ + kind: "min", + value: 0, + inclusive: false, + message: errorUtil.toString(message), + }); + } + negative(message) { + return this._addCheck({ + kind: "max", + value: 0, + inclusive: false, + message: errorUtil.toString(message), + }); + } + nonpositive(message) { + return this._addCheck({ + kind: "max", + value: 0, + inclusive: true, + message: errorUtil.toString(message), + }); + } + nonnegative(message) { + return this._addCheck({ + kind: "min", + value: 0, + inclusive: true, + message: errorUtil.toString(message), + }); + } + multipleOf(value, message) { + return this._addCheck({ + kind: "multipleOf", + value: value, + message: errorUtil.toString(message), + }); + } + finite(message) { + return this._addCheck({ + kind: "finite", + message: errorUtil.toString(message), + }); + } + safe(message) { + return this._addCheck({ + kind: "min", + inclusive: true, + value: Number.MIN_SAFE_INTEGER, + message: errorUtil.toString(message), + })._addCheck({ + kind: "max", + inclusive: true, + value: Number.MAX_SAFE_INTEGER, + message: errorUtil.toString(message), + }); + } + get minValue() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min; + } + get maxValue() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max; + } + get isInt() { + return !!this._def.checks.find((ch) => ch.kind === "int" || (ch.kind === "multipleOf" && util.isInteger(ch.value))); + } + get isFinite() { + let max = null; + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "finite" || ch.kind === "int" || ch.kind === "multipleOf") { + return true; + } + else if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + else if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return Number.isFinite(min) && Number.isFinite(max); + } +} +ZodNumber.create = (params) => { + return new ZodNumber({ + checks: [], + typeName: ZodFirstPartyTypeKind.ZodNumber, + coerce: params?.coerce || false, + ...processCreateParams(params), + }); +}; +export class ZodBigInt extends ZodType { + constructor() { + super(...arguments); + this.min = this.gte; + this.max = this.lte; + } + _parse(input) { + if (this._def.coerce) { + try { + input.data = BigInt(input.data); + } + catch { + return this._getInvalidInput(input); + } + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.bigint) { + return this._getInvalidInput(input); + } + let ctx = undefined; + const status = new ParseStatus(); + for (const check of this._def.checks) { + if (check.kind === "min") { + const tooSmall = check.inclusive ? input.data < check.value : input.data <= check.value; + if (tooSmall) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + type: "bigint", + minimum: check.value, + inclusive: check.inclusive, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "max") { + const tooBig = check.inclusive ? input.data > check.value : input.data >= check.value; + if (tooBig) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + type: "bigint", + maximum: check.value, + inclusive: check.inclusive, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "multipleOf") { + if (input.data % check.value !== BigInt(0)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.not_multiple_of, + multipleOf: check.value, + message: check.message, + }); + status.dirty(); + } + } + else { + util.assertNever(check); + } + } + return { status: status.value, value: input.data }; + } + _getInvalidInput(input) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.bigint, + received: ctx.parsedType, + }); + return INVALID; + } + gte(value, message) { + return this.setLimit("min", value, true, errorUtil.toString(message)); + } + gt(value, message) { + return this.setLimit("min", value, false, errorUtil.toString(message)); + } + lte(value, message) { + return this.setLimit("max", value, true, errorUtil.toString(message)); + } + lt(value, message) { + return this.setLimit("max", value, false, errorUtil.toString(message)); + } + setLimit(kind, value, inclusive, message) { + return new ZodBigInt({ + ...this._def, + checks: [ + ...this._def.checks, + { + kind, + value, + inclusive, + message: errorUtil.toString(message), + }, + ], + }); + } + _addCheck(check) { + return new ZodBigInt({ + ...this._def, + checks: [...this._def.checks, check], + }); + } + positive(message) { + return this._addCheck({ + kind: "min", + value: BigInt(0), + inclusive: false, + message: errorUtil.toString(message), + }); + } + negative(message) { + return this._addCheck({ + kind: "max", + value: BigInt(0), + inclusive: false, + message: errorUtil.toString(message), + }); + } + nonpositive(message) { + return this._addCheck({ + kind: "max", + value: BigInt(0), + inclusive: true, + message: errorUtil.toString(message), + }); + } + nonnegative(message) { + return this._addCheck({ + kind: "min", + value: BigInt(0), + inclusive: true, + message: errorUtil.toString(message), + }); + } + multipleOf(value, message) { + return this._addCheck({ + kind: "multipleOf", + value, + message: errorUtil.toString(message), + }); + } + get minValue() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min; + } + get maxValue() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max; + } +} +ZodBigInt.create = (params) => { + return new ZodBigInt({ + checks: [], + typeName: ZodFirstPartyTypeKind.ZodBigInt, + coerce: params?.coerce ?? false, + ...processCreateParams(params), + }); +}; +export class ZodBoolean extends ZodType { + _parse(input) { + if (this._def.coerce) { + input.data = Boolean(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.boolean) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.boolean, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodBoolean.create = (params) => { + return new ZodBoolean({ + typeName: ZodFirstPartyTypeKind.ZodBoolean, + coerce: params?.coerce || false, + ...processCreateParams(params), + }); +}; +export class ZodDate extends ZodType { + _parse(input) { + if (this._def.coerce) { + input.data = new Date(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.date) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.date, + received: ctx.parsedType, + }); + return INVALID; + } + if (Number.isNaN(input.data.getTime())) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_date, + }); + return INVALID; + } + const status = new ParseStatus(); + let ctx = undefined; + for (const check of this._def.checks) { + if (check.kind === "min") { + if (input.data.getTime() < check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + message: check.message, + inclusive: true, + exact: false, + minimum: check.value, + type: "date", + }); + status.dirty(); + } + } + else if (check.kind === "max") { + if (input.data.getTime() > check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + message: check.message, + inclusive: true, + exact: false, + maximum: check.value, + type: "date", + }); + status.dirty(); + } + } + else { + util.assertNever(check); + } + } + return { + status: status.value, + value: new Date(input.data.getTime()), + }; + } + _addCheck(check) { + return new ZodDate({ + ...this._def, + checks: [...this._def.checks, check], + }); + } + min(minDate, message) { + return this._addCheck({ + kind: "min", + value: minDate.getTime(), + message: errorUtil.toString(message), + }); + } + max(maxDate, message) { + return this._addCheck({ + kind: "max", + value: maxDate.getTime(), + message: errorUtil.toString(message), + }); + } + get minDate() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min != null ? new Date(min) : null; + } + get maxDate() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max != null ? new Date(max) : null; + } +} +ZodDate.create = (params) => { + return new ZodDate({ + checks: [], + coerce: params?.coerce || false, + typeName: ZodFirstPartyTypeKind.ZodDate, + ...processCreateParams(params), + }); +}; +export class ZodSymbol extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.symbol) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.symbol, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodSymbol.create = (params) => { + return new ZodSymbol({ + typeName: ZodFirstPartyTypeKind.ZodSymbol, + ...processCreateParams(params), + }); +}; +export class ZodUndefined extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.undefined) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.undefined, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodUndefined.create = (params) => { + return new ZodUndefined({ + typeName: ZodFirstPartyTypeKind.ZodUndefined, + ...processCreateParams(params), + }); +}; +export class ZodNull extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.null) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.null, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodNull.create = (params) => { + return new ZodNull({ + typeName: ZodFirstPartyTypeKind.ZodNull, + ...processCreateParams(params), + }); +}; +export class ZodAny extends ZodType { + constructor() { + super(...arguments); + // to prevent instances of other classes from extending ZodAny. this causes issues with catchall in ZodObject. + this._any = true; + } + _parse(input) { + return OK(input.data); + } +} +ZodAny.create = (params) => { + return new ZodAny({ + typeName: ZodFirstPartyTypeKind.ZodAny, + ...processCreateParams(params), + }); +}; +export class ZodUnknown extends ZodType { + constructor() { + super(...arguments); + // required + this._unknown = true; + } + _parse(input) { + return OK(input.data); + } +} +ZodUnknown.create = (params) => { + return new ZodUnknown({ + typeName: ZodFirstPartyTypeKind.ZodUnknown, + ...processCreateParams(params), + }); +}; +export class ZodNever extends ZodType { + _parse(input) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.never, + received: ctx.parsedType, + }); + return INVALID; + } +} +ZodNever.create = (params) => { + return new ZodNever({ + typeName: ZodFirstPartyTypeKind.ZodNever, + ...processCreateParams(params), + }); +}; +export class ZodVoid extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.undefined) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.void, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodVoid.create = (params) => { + return new ZodVoid({ + typeName: ZodFirstPartyTypeKind.ZodVoid, + ...processCreateParams(params), + }); +}; +export class ZodArray extends ZodType { + _parse(input) { + const { ctx, status } = this._processInputParams(input); + const def = this._def; + if (ctx.parsedType !== ZodParsedType.array) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.array, + received: ctx.parsedType, + }); + return INVALID; + } + if (def.exactLength !== null) { + const tooBig = ctx.data.length > def.exactLength.value; + const tooSmall = ctx.data.length < def.exactLength.value; + if (tooBig || tooSmall) { + addIssueToContext(ctx, { + code: tooBig ? ZodIssueCode.too_big : ZodIssueCode.too_small, + minimum: (tooSmall ? def.exactLength.value : undefined), + maximum: (tooBig ? def.exactLength.value : undefined), + type: "array", + inclusive: true, + exact: true, + message: def.exactLength.message, + }); + status.dirty(); + } + } + if (def.minLength !== null) { + if (ctx.data.length < def.minLength.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: def.minLength.value, + type: "array", + inclusive: true, + exact: false, + message: def.minLength.message, + }); + status.dirty(); + } + } + if (def.maxLength !== null) { + if (ctx.data.length > def.maxLength.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: def.maxLength.value, + type: "array", + inclusive: true, + exact: false, + message: def.maxLength.message, + }); + status.dirty(); + } + } + if (ctx.common.async) { + return Promise.all([...ctx.data].map((item, i) => { + return def.type._parseAsync(new ParseInputLazyPath(ctx, item, ctx.path, i)); + })).then((result) => { + return ParseStatus.mergeArray(status, result); + }); + } + const result = [...ctx.data].map((item, i) => { + return def.type._parseSync(new ParseInputLazyPath(ctx, item, ctx.path, i)); + }); + return ParseStatus.mergeArray(status, result); + } + get element() { + return this._def.type; + } + min(minLength, message) { + return new ZodArray({ + ...this._def, + minLength: { value: minLength, message: errorUtil.toString(message) }, + }); + } + max(maxLength, message) { + return new ZodArray({ + ...this._def, + maxLength: { value: maxLength, message: errorUtil.toString(message) }, + }); + } + length(len, message) { + return new ZodArray({ + ...this._def, + exactLength: { value: len, message: errorUtil.toString(message) }, + }); + } + nonempty(message) { + return this.min(1, message); + } +} +ZodArray.create = (schema, params) => { + return new ZodArray({ + type: schema, + minLength: null, + maxLength: null, + exactLength: null, + typeName: ZodFirstPartyTypeKind.ZodArray, + ...processCreateParams(params), + }); +}; +function deepPartialify(schema) { + if (schema instanceof ZodObject) { + const newShape = {}; + for (const key in schema.shape) { + const fieldSchema = schema.shape[key]; + newShape[key] = ZodOptional.create(deepPartialify(fieldSchema)); + } + return new ZodObject({ + ...schema._def, + shape: () => newShape, + }); + } + else if (schema instanceof ZodArray) { + return new ZodArray({ + ...schema._def, + type: deepPartialify(schema.element), + }); + } + else if (schema instanceof ZodOptional) { + return ZodOptional.create(deepPartialify(schema.unwrap())); + } + else if (schema instanceof ZodNullable) { + return ZodNullable.create(deepPartialify(schema.unwrap())); + } + else if (schema instanceof ZodTuple) { + return ZodTuple.create(schema.items.map((item) => deepPartialify(item))); + } + else { + return schema; + } +} +export class ZodObject extends ZodType { + constructor() { + super(...arguments); + this._cached = null; + /** + * @deprecated In most cases, this is no longer needed - unknown properties are now silently stripped. + * If you want to pass through unknown properties, use `.passthrough()` instead. + */ + this.nonstrict = this.passthrough; + // extend< + // Augmentation extends ZodRawShape, + // NewOutput extends util.flatten<{ + // [k in keyof Augmentation | keyof Output]: k extends keyof Augmentation + // ? Augmentation[k]["_output"] + // : k extends keyof Output + // ? Output[k] + // : never; + // }>, + // NewInput extends util.flatten<{ + // [k in keyof Augmentation | keyof Input]: k extends keyof Augmentation + // ? Augmentation[k]["_input"] + // : k extends keyof Input + // ? Input[k] + // : never; + // }> + // >( + // augmentation: Augmentation + // ): ZodObject< + // extendShape, + // UnknownKeys, + // Catchall, + // NewOutput, + // NewInput + // > { + // return new ZodObject({ + // ...this._def, + // shape: () => ({ + // ...this._def.shape(), + // ...augmentation, + // }), + // }) as any; + // } + /** + * @deprecated Use `.extend` instead + * */ + this.augment = this.extend; + } + _getCached() { + if (this._cached !== null) + return this._cached; + const shape = this._def.shape(); + const keys = util.objectKeys(shape); + this._cached = { shape, keys }; + return this._cached; + } + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.object) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.object, + received: ctx.parsedType, + }); + return INVALID; + } + const { status, ctx } = this._processInputParams(input); + const { shape, keys: shapeKeys } = this._getCached(); + const extraKeys = []; + if (!(this._def.catchall instanceof ZodNever && this._def.unknownKeys === "strip")) { + for (const key in ctx.data) { + if (!shapeKeys.includes(key)) { + extraKeys.push(key); + } + } + } + const pairs = []; + for (const key of shapeKeys) { + const keyValidator = shape[key]; + const value = ctx.data[key]; + pairs.push({ + key: { status: "valid", value: key }, + value: keyValidator._parse(new ParseInputLazyPath(ctx, value, ctx.path, key)), + alwaysSet: key in ctx.data, + }); + } + if (this._def.catchall instanceof ZodNever) { + const unknownKeys = this._def.unknownKeys; + if (unknownKeys === "passthrough") { + for (const key of extraKeys) { + pairs.push({ + key: { status: "valid", value: key }, + value: { status: "valid", value: ctx.data[key] }, + }); + } + } + else if (unknownKeys === "strict") { + if (extraKeys.length > 0) { + addIssueToContext(ctx, { + code: ZodIssueCode.unrecognized_keys, + keys: extraKeys, + }); + status.dirty(); + } + } + else if (unknownKeys === "strip") { + } + else { + throw new Error(`Internal ZodObject error: invalid unknownKeys value.`); + } + } + else { + // run catchall validation + const catchall = this._def.catchall; + for (const key of extraKeys) { + const value = ctx.data[key]; + pairs.push({ + key: { status: "valid", value: key }, + value: catchall._parse(new ParseInputLazyPath(ctx, value, ctx.path, key) //, ctx.child(key), value, getParsedType(value) + ), + alwaysSet: key in ctx.data, + }); + } + } + if (ctx.common.async) { + return Promise.resolve() + .then(async () => { + const syncPairs = []; + for (const pair of pairs) { + const key = await pair.key; + const value = await pair.value; + syncPairs.push({ + key, + value, + alwaysSet: pair.alwaysSet, + }); + } + return syncPairs; + }) + .then((syncPairs) => { + return ParseStatus.mergeObjectSync(status, syncPairs); + }); + } + else { + return ParseStatus.mergeObjectSync(status, pairs); + } + } + get shape() { + return this._def.shape(); + } + strict(message) { + errorUtil.errToObj; + return new ZodObject({ + ...this._def, + unknownKeys: "strict", + ...(message !== undefined + ? { + errorMap: (issue, ctx) => { + const defaultError = this._def.errorMap?.(issue, ctx).message ?? ctx.defaultError; + if (issue.code === "unrecognized_keys") + return { + message: errorUtil.errToObj(message).message ?? defaultError, + }; + return { + message: defaultError, + }; + }, + } + : {}), + }); + } + strip() { + return new ZodObject({ + ...this._def, + unknownKeys: "strip", + }); + } + passthrough() { + return new ZodObject({ + ...this._def, + unknownKeys: "passthrough", + }); + } + // const AugmentFactory = + // (def: Def) => + // ( + // augmentation: Augmentation + // ): ZodObject< + // extendShape, Augmentation>, + // Def["unknownKeys"], + // Def["catchall"] + // > => { + // return new ZodObject({ + // ...def, + // shape: () => ({ + // ...def.shape(), + // ...augmentation, + // }), + // }) as any; + // }; + extend(augmentation) { + return new ZodObject({ + ...this._def, + shape: () => ({ + ...this._def.shape(), + ...augmentation, + }), + }); + } + /** + * Prior to zod@1.0.12 there was a bug in the + * inferred type of merged objects. Please + * upgrade if you are experiencing issues. + */ + merge(merging) { + const merged = new ZodObject({ + unknownKeys: merging._def.unknownKeys, + catchall: merging._def.catchall, + shape: () => ({ + ...this._def.shape(), + ...merging._def.shape(), + }), + typeName: ZodFirstPartyTypeKind.ZodObject, + }); + return merged; + } + // merge< + // Incoming extends AnyZodObject, + // Augmentation extends Incoming["shape"], + // NewOutput extends { + // [k in keyof Augmentation | keyof Output]: k extends keyof Augmentation + // ? Augmentation[k]["_output"] + // : k extends keyof Output + // ? Output[k] + // : never; + // }, + // NewInput extends { + // [k in keyof Augmentation | keyof Input]: k extends keyof Augmentation + // ? Augmentation[k]["_input"] + // : k extends keyof Input + // ? Input[k] + // : never; + // } + // >( + // merging: Incoming + // ): ZodObject< + // extendShape>, + // Incoming["_def"]["unknownKeys"], + // Incoming["_def"]["catchall"], + // NewOutput, + // NewInput + // > { + // const merged: any = new ZodObject({ + // unknownKeys: merging._def.unknownKeys, + // catchall: merging._def.catchall, + // shape: () => + // objectUtil.mergeShapes(this._def.shape(), merging._def.shape()), + // typeName: ZodFirstPartyTypeKind.ZodObject, + // }) as any; + // return merged; + // } + setKey(key, schema) { + return this.augment({ [key]: schema }); + } + // merge( + // merging: Incoming + // ): //ZodObject = (merging) => { + // ZodObject< + // extendShape>, + // Incoming["_def"]["unknownKeys"], + // Incoming["_def"]["catchall"] + // > { + // // const mergedShape = objectUtil.mergeShapes( + // // this._def.shape(), + // // merging._def.shape() + // // ); + // const merged: any = new ZodObject({ + // unknownKeys: merging._def.unknownKeys, + // catchall: merging._def.catchall, + // shape: () => + // objectUtil.mergeShapes(this._def.shape(), merging._def.shape()), + // typeName: ZodFirstPartyTypeKind.ZodObject, + // }) as any; + // return merged; + // } + catchall(index) { + return new ZodObject({ + ...this._def, + catchall: index, + }); + } + pick(mask) { + const shape = {}; + for (const key of util.objectKeys(mask)) { + if (mask[key] && this.shape[key]) { + shape[key] = this.shape[key]; + } + } + return new ZodObject({ + ...this._def, + shape: () => shape, + }); + } + omit(mask) { + const shape = {}; + for (const key of util.objectKeys(this.shape)) { + if (!mask[key]) { + shape[key] = this.shape[key]; + } + } + return new ZodObject({ + ...this._def, + shape: () => shape, + }); + } + /** + * @deprecated + */ + deepPartial() { + return deepPartialify(this); + } + partial(mask) { + const newShape = {}; + for (const key of util.objectKeys(this.shape)) { + const fieldSchema = this.shape[key]; + if (mask && !mask[key]) { + newShape[key] = fieldSchema; + } + else { + newShape[key] = fieldSchema.optional(); + } + } + return new ZodObject({ + ...this._def, + shape: () => newShape, + }); + } + required(mask) { + const newShape = {}; + for (const key of util.objectKeys(this.shape)) { + if (mask && !mask[key]) { + newShape[key] = this.shape[key]; + } + else { + const fieldSchema = this.shape[key]; + let newField = fieldSchema; + while (newField instanceof ZodOptional) { + newField = newField._def.innerType; + } + newShape[key] = newField; + } + } + return new ZodObject({ + ...this._def, + shape: () => newShape, + }); + } + keyof() { + return createZodEnum(util.objectKeys(this.shape)); + } +} +ZodObject.create = (shape, params) => { + return new ZodObject({ + shape: () => shape, + unknownKeys: "strip", + catchall: ZodNever.create(), + typeName: ZodFirstPartyTypeKind.ZodObject, + ...processCreateParams(params), + }); +}; +ZodObject.strictCreate = (shape, params) => { + return new ZodObject({ + shape: () => shape, + unknownKeys: "strict", + catchall: ZodNever.create(), + typeName: ZodFirstPartyTypeKind.ZodObject, + ...processCreateParams(params), + }); +}; +ZodObject.lazycreate = (shape, params) => { + return new ZodObject({ + shape, + unknownKeys: "strip", + catchall: ZodNever.create(), + typeName: ZodFirstPartyTypeKind.ZodObject, + ...processCreateParams(params), + }); +}; +export class ZodUnion extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + const options = this._def.options; + function handleResults(results) { + // return first issue-free validation if it exists + for (const result of results) { + if (result.result.status === "valid") { + return result.result; + } + } + for (const result of results) { + if (result.result.status === "dirty") { + // add issues from dirty option + ctx.common.issues.push(...result.ctx.common.issues); + return result.result; + } + } + // return invalid + const unionErrors = results.map((result) => new ZodError(result.ctx.common.issues)); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_union, + unionErrors, + }); + return INVALID; + } + if (ctx.common.async) { + return Promise.all(options.map(async (option) => { + const childCtx = { + ...ctx, + common: { + ...ctx.common, + issues: [], + }, + parent: null, + }; + return { + result: await option._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: childCtx, + }), + ctx: childCtx, + }; + })).then(handleResults); + } + else { + let dirty = undefined; + const issues = []; + for (const option of options) { + const childCtx = { + ...ctx, + common: { + ...ctx.common, + issues: [], + }, + parent: null, + }; + const result = option._parseSync({ + data: ctx.data, + path: ctx.path, + parent: childCtx, + }); + if (result.status === "valid") { + return result; + } + else if (result.status === "dirty" && !dirty) { + dirty = { result, ctx: childCtx }; + } + if (childCtx.common.issues.length) { + issues.push(childCtx.common.issues); + } + } + if (dirty) { + ctx.common.issues.push(...dirty.ctx.common.issues); + return dirty.result; + } + const unionErrors = issues.map((issues) => new ZodError(issues)); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_union, + unionErrors, + }); + return INVALID; + } + } + get options() { + return this._def.options; + } +} +ZodUnion.create = (types, params) => { + return new ZodUnion({ + options: types, + typeName: ZodFirstPartyTypeKind.ZodUnion, + ...processCreateParams(params), + }); +}; +///////////////////////////////////////////////////// +///////////////////////////////////////////////////// +////////// ////////// +////////// ZodDiscriminatedUnion ////////// +////////// ////////// +///////////////////////////////////////////////////// +///////////////////////////////////////////////////// +const getDiscriminator = (type) => { + if (type instanceof ZodLazy) { + return getDiscriminator(type.schema); + } + else if (type instanceof ZodEffects) { + return getDiscriminator(type.innerType()); + } + else if (type instanceof ZodLiteral) { + return [type.value]; + } + else if (type instanceof ZodEnum) { + return type.options; + } + else if (type instanceof ZodNativeEnum) { + // eslint-disable-next-line ban/ban + return util.objectValues(type.enum); + } + else if (type instanceof ZodDefault) { + return getDiscriminator(type._def.innerType); + } + else if (type instanceof ZodUndefined) { + return [undefined]; + } + else if (type instanceof ZodNull) { + return [null]; + } + else if (type instanceof ZodOptional) { + return [undefined, ...getDiscriminator(type.unwrap())]; + } + else if (type instanceof ZodNullable) { + return [null, ...getDiscriminator(type.unwrap())]; + } + else if (type instanceof ZodBranded) { + return getDiscriminator(type.unwrap()); + } + else if (type instanceof ZodReadonly) { + return getDiscriminator(type.unwrap()); + } + else if (type instanceof ZodCatch) { + return getDiscriminator(type._def.innerType); + } + else { + return []; + } +}; +export class ZodDiscriminatedUnion extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.object) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.object, + received: ctx.parsedType, + }); + return INVALID; + } + const discriminator = this.discriminator; + const discriminatorValue = ctx.data[discriminator]; + const option = this.optionsMap.get(discriminatorValue); + if (!option) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_union_discriminator, + options: Array.from(this.optionsMap.keys()), + path: [discriminator], + }); + return INVALID; + } + if (ctx.common.async) { + return option._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + } + else { + return option._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + } + } + get discriminator() { + return this._def.discriminator; + } + get options() { + return this._def.options; + } + get optionsMap() { + return this._def.optionsMap; + } + /** + * The constructor of the discriminated union schema. Its behaviour is very similar to that of the normal z.union() constructor. + * However, it only allows a union of objects, all of which need to share a discriminator property. This property must + * have a different value for each object in the union. + * @param discriminator the name of the discriminator property + * @param types an array of object schemas + * @param params + */ + static create(discriminator, options, params) { + // Get all the valid discriminator values + const optionsMap = new Map(); + // try { + for (const type of options) { + const discriminatorValues = getDiscriminator(type.shape[discriminator]); + if (!discriminatorValues.length) { + throw new Error(`A discriminator value for key \`${discriminator}\` could not be extracted from all schema options`); + } + for (const value of discriminatorValues) { + if (optionsMap.has(value)) { + throw new Error(`Discriminator property ${String(discriminator)} has duplicate value ${String(value)}`); + } + optionsMap.set(value, type); + } + } + return new ZodDiscriminatedUnion({ + typeName: ZodFirstPartyTypeKind.ZodDiscriminatedUnion, + discriminator, + options, + optionsMap, + ...processCreateParams(params), + }); + } +} +function mergeValues(a, b) { + const aType = getParsedType(a); + const bType = getParsedType(b); + if (a === b) { + return { valid: true, data: a }; + } + else if (aType === ZodParsedType.object && bType === ZodParsedType.object) { + const bKeys = util.objectKeys(b); + const sharedKeys = util.objectKeys(a).filter((key) => bKeys.indexOf(key) !== -1); + const newObj = { ...a, ...b }; + for (const key of sharedKeys) { + const sharedValue = mergeValues(a[key], b[key]); + if (!sharedValue.valid) { + return { valid: false }; + } + newObj[key] = sharedValue.data; + } + return { valid: true, data: newObj }; + } + else if (aType === ZodParsedType.array && bType === ZodParsedType.array) { + if (a.length !== b.length) { + return { valid: false }; + } + const newArray = []; + for (let index = 0; index < a.length; index++) { + const itemA = a[index]; + const itemB = b[index]; + const sharedValue = mergeValues(itemA, itemB); + if (!sharedValue.valid) { + return { valid: false }; + } + newArray.push(sharedValue.data); + } + return { valid: true, data: newArray }; + } + else if (aType === ZodParsedType.date && bType === ZodParsedType.date && +a === +b) { + return { valid: true, data: a }; + } + else { + return { valid: false }; + } +} +export class ZodIntersection extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + const handleParsed = (parsedLeft, parsedRight) => { + if (isAborted(parsedLeft) || isAborted(parsedRight)) { + return INVALID; + } + const merged = mergeValues(parsedLeft.value, parsedRight.value); + if (!merged.valid) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_intersection_types, + }); + return INVALID; + } + if (isDirty(parsedLeft) || isDirty(parsedRight)) { + status.dirty(); + } + return { status: status.value, value: merged.data }; + }; + if (ctx.common.async) { + return Promise.all([ + this._def.left._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }), + this._def.right._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }), + ]).then(([left, right]) => handleParsed(left, right)); + } + else { + return handleParsed(this._def.left._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }), this._def.right._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + })); + } + } +} +ZodIntersection.create = (left, right, params) => { + return new ZodIntersection({ + left: left, + right: right, + typeName: ZodFirstPartyTypeKind.ZodIntersection, + ...processCreateParams(params), + }); +}; +// type ZodTupleItems = [ZodTypeAny, ...ZodTypeAny[]]; +export class ZodTuple extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.array) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.array, + received: ctx.parsedType, + }); + return INVALID; + } + if (ctx.data.length < this._def.items.length) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: this._def.items.length, + inclusive: true, + exact: false, + type: "array", + }); + return INVALID; + } + const rest = this._def.rest; + if (!rest && ctx.data.length > this._def.items.length) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: this._def.items.length, + inclusive: true, + exact: false, + type: "array", + }); + status.dirty(); + } + const items = [...ctx.data] + .map((item, itemIndex) => { + const schema = this._def.items[itemIndex] || this._def.rest; + if (!schema) + return null; + return schema._parse(new ParseInputLazyPath(ctx, item, ctx.path, itemIndex)); + }) + .filter((x) => !!x); // filter nulls + if (ctx.common.async) { + return Promise.all(items).then((results) => { + return ParseStatus.mergeArray(status, results); + }); + } + else { + return ParseStatus.mergeArray(status, items); + } + } + get items() { + return this._def.items; + } + rest(rest) { + return new ZodTuple({ + ...this._def, + rest, + }); + } +} +ZodTuple.create = (schemas, params) => { + if (!Array.isArray(schemas)) { + throw new Error("You must pass an array of schemas to z.tuple([ ... ])"); + } + return new ZodTuple({ + items: schemas, + typeName: ZodFirstPartyTypeKind.ZodTuple, + rest: null, + ...processCreateParams(params), + }); +}; +export class ZodRecord extends ZodType { + get keySchema() { + return this._def.keyType; + } + get valueSchema() { + return this._def.valueType; + } + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.object) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.object, + received: ctx.parsedType, + }); + return INVALID; + } + const pairs = []; + const keyType = this._def.keyType; + const valueType = this._def.valueType; + for (const key in ctx.data) { + pairs.push({ + key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, key)), + value: valueType._parse(new ParseInputLazyPath(ctx, ctx.data[key], ctx.path, key)), + alwaysSet: key in ctx.data, + }); + } + if (ctx.common.async) { + return ParseStatus.mergeObjectAsync(status, pairs); + } + else { + return ParseStatus.mergeObjectSync(status, pairs); + } + } + get element() { + return this._def.valueType; + } + static create(first, second, third) { + if (second instanceof ZodType) { + return new ZodRecord({ + keyType: first, + valueType: second, + typeName: ZodFirstPartyTypeKind.ZodRecord, + ...processCreateParams(third), + }); + } + return new ZodRecord({ + keyType: ZodString.create(), + valueType: first, + typeName: ZodFirstPartyTypeKind.ZodRecord, + ...processCreateParams(second), + }); + } +} +export class ZodMap extends ZodType { + get keySchema() { + return this._def.keyType; + } + get valueSchema() { + return this._def.valueType; + } + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.map) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.map, + received: ctx.parsedType, + }); + return INVALID; + } + const keyType = this._def.keyType; + const valueType = this._def.valueType; + const pairs = [...ctx.data.entries()].map(([key, value], index) => { + return { + key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, [index, "key"])), + value: valueType._parse(new ParseInputLazyPath(ctx, value, ctx.path, [index, "value"])), + }; + }); + if (ctx.common.async) { + const finalMap = new Map(); + return Promise.resolve().then(async () => { + for (const pair of pairs) { + const key = await pair.key; + const value = await pair.value; + if (key.status === "aborted" || value.status === "aborted") { + return INVALID; + } + if (key.status === "dirty" || value.status === "dirty") { + status.dirty(); + } + finalMap.set(key.value, value.value); + } + return { status: status.value, value: finalMap }; + }); + } + else { + const finalMap = new Map(); + for (const pair of pairs) { + const key = pair.key; + const value = pair.value; + if (key.status === "aborted" || value.status === "aborted") { + return INVALID; + } + if (key.status === "dirty" || value.status === "dirty") { + status.dirty(); + } + finalMap.set(key.value, value.value); + } + return { status: status.value, value: finalMap }; + } + } +} +ZodMap.create = (keyType, valueType, params) => { + return new ZodMap({ + valueType, + keyType, + typeName: ZodFirstPartyTypeKind.ZodMap, + ...processCreateParams(params), + }); +}; +export class ZodSet extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.set) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.set, + received: ctx.parsedType, + }); + return INVALID; + } + const def = this._def; + if (def.minSize !== null) { + if (ctx.data.size < def.minSize.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: def.minSize.value, + type: "set", + inclusive: true, + exact: false, + message: def.minSize.message, + }); + status.dirty(); + } + } + if (def.maxSize !== null) { + if (ctx.data.size > def.maxSize.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: def.maxSize.value, + type: "set", + inclusive: true, + exact: false, + message: def.maxSize.message, + }); + status.dirty(); + } + } + const valueType = this._def.valueType; + function finalizeSet(elements) { + const parsedSet = new Set(); + for (const element of elements) { + if (element.status === "aborted") + return INVALID; + if (element.status === "dirty") + status.dirty(); + parsedSet.add(element.value); + } + return { status: status.value, value: parsedSet }; + } + const elements = [...ctx.data.values()].map((item, i) => valueType._parse(new ParseInputLazyPath(ctx, item, ctx.path, i))); + if (ctx.common.async) { + return Promise.all(elements).then((elements) => finalizeSet(elements)); + } + else { + return finalizeSet(elements); + } + } + min(minSize, message) { + return new ZodSet({ + ...this._def, + minSize: { value: minSize, message: errorUtil.toString(message) }, + }); + } + max(maxSize, message) { + return new ZodSet({ + ...this._def, + maxSize: { value: maxSize, message: errorUtil.toString(message) }, + }); + } + size(size, message) { + return this.min(size, message).max(size, message); + } + nonempty(message) { + return this.min(1, message); + } +} +ZodSet.create = (valueType, params) => { + return new ZodSet({ + valueType, + minSize: null, + maxSize: null, + typeName: ZodFirstPartyTypeKind.ZodSet, + ...processCreateParams(params), + }); +}; +export class ZodFunction extends ZodType { + constructor() { + super(...arguments); + this.validate = this.implement; + } + _parse(input) { + const { ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.function) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.function, + received: ctx.parsedType, + }); + return INVALID; + } + function makeArgsIssue(args, error) { + return makeIssue({ + data: args, + path: ctx.path, + errorMaps: [ctx.common.contextualErrorMap, ctx.schemaErrorMap, getErrorMap(), defaultErrorMap].filter((x) => !!x), + issueData: { + code: ZodIssueCode.invalid_arguments, + argumentsError: error, + }, + }); + } + function makeReturnsIssue(returns, error) { + return makeIssue({ + data: returns, + path: ctx.path, + errorMaps: [ctx.common.contextualErrorMap, ctx.schemaErrorMap, getErrorMap(), defaultErrorMap].filter((x) => !!x), + issueData: { + code: ZodIssueCode.invalid_return_type, + returnTypeError: error, + }, + }); + } + const params = { errorMap: ctx.common.contextualErrorMap }; + const fn = ctx.data; + if (this._def.returns instanceof ZodPromise) { + // Would love a way to avoid disabling this rule, but we need + // an alias (using an arrow function was what caused 2651). + // eslint-disable-next-line @typescript-eslint/no-this-alias + const me = this; + return OK(async function (...args) { + const error = new ZodError([]); + const parsedArgs = await me._def.args.parseAsync(args, params).catch((e) => { + error.addIssue(makeArgsIssue(args, e)); + throw error; + }); + const result = await Reflect.apply(fn, this, parsedArgs); + const parsedReturns = await me._def.returns._def.type + .parseAsync(result, params) + .catch((e) => { + error.addIssue(makeReturnsIssue(result, e)); + throw error; + }); + return parsedReturns; + }); + } + else { + // Would love a way to avoid disabling this rule, but we need + // an alias (using an arrow function was what caused 2651). + // eslint-disable-next-line @typescript-eslint/no-this-alias + const me = this; + return OK(function (...args) { + const parsedArgs = me._def.args.safeParse(args, params); + if (!parsedArgs.success) { + throw new ZodError([makeArgsIssue(args, parsedArgs.error)]); + } + const result = Reflect.apply(fn, this, parsedArgs.data); + const parsedReturns = me._def.returns.safeParse(result, params); + if (!parsedReturns.success) { + throw new ZodError([makeReturnsIssue(result, parsedReturns.error)]); + } + return parsedReturns.data; + }); + } + } + parameters() { + return this._def.args; + } + returnType() { + return this._def.returns; + } + args(...items) { + return new ZodFunction({ + ...this._def, + args: ZodTuple.create(items).rest(ZodUnknown.create()), + }); + } + returns(returnType) { + return new ZodFunction({ + ...this._def, + returns: returnType, + }); + } + implement(func) { + const validatedFunc = this.parse(func); + return validatedFunc; + } + strictImplement(func) { + const validatedFunc = this.parse(func); + return validatedFunc; + } + static create(args, returns, params) { + return new ZodFunction({ + args: (args ? args : ZodTuple.create([]).rest(ZodUnknown.create())), + returns: returns || ZodUnknown.create(), + typeName: ZodFirstPartyTypeKind.ZodFunction, + ...processCreateParams(params), + }); + } +} +export class ZodLazy extends ZodType { + get schema() { + return this._def.getter(); + } + _parse(input) { + const { ctx } = this._processInputParams(input); + const lazySchema = this._def.getter(); + return lazySchema._parse({ data: ctx.data, path: ctx.path, parent: ctx }); + } +} +ZodLazy.create = (getter, params) => { + return new ZodLazy({ + getter: getter, + typeName: ZodFirstPartyTypeKind.ZodLazy, + ...processCreateParams(params), + }); +}; +export class ZodLiteral extends ZodType { + _parse(input) { + if (input.data !== this._def.value) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + received: ctx.data, + code: ZodIssueCode.invalid_literal, + expected: this._def.value, + }); + return INVALID; + } + return { status: "valid", value: input.data }; + } + get value() { + return this._def.value; + } +} +ZodLiteral.create = (value, params) => { + return new ZodLiteral({ + value: value, + typeName: ZodFirstPartyTypeKind.ZodLiteral, + ...processCreateParams(params), + }); +}; +function createZodEnum(values, params) { + return new ZodEnum({ + values, + typeName: ZodFirstPartyTypeKind.ZodEnum, + ...processCreateParams(params), + }); +} +export class ZodEnum extends ZodType { + _parse(input) { + if (typeof input.data !== "string") { + const ctx = this._getOrReturnCtx(input); + const expectedValues = this._def.values; + addIssueToContext(ctx, { + expected: util.joinValues(expectedValues), + received: ctx.parsedType, + code: ZodIssueCode.invalid_type, + }); + return INVALID; + } + if (!this._cache) { + this._cache = new Set(this._def.values); + } + if (!this._cache.has(input.data)) { + const ctx = this._getOrReturnCtx(input); + const expectedValues = this._def.values; + addIssueToContext(ctx, { + received: ctx.data, + code: ZodIssueCode.invalid_enum_value, + options: expectedValues, + }); + return INVALID; + } + return OK(input.data); + } + get options() { + return this._def.values; + } + get enum() { + const enumValues = {}; + for (const val of this._def.values) { + enumValues[val] = val; + } + return enumValues; + } + get Values() { + const enumValues = {}; + for (const val of this._def.values) { + enumValues[val] = val; + } + return enumValues; + } + get Enum() { + const enumValues = {}; + for (const val of this._def.values) { + enumValues[val] = val; + } + return enumValues; + } + extract(values, newDef = this._def) { + return ZodEnum.create(values, { + ...this._def, + ...newDef, + }); + } + exclude(values, newDef = this._def) { + return ZodEnum.create(this.options.filter((opt) => !values.includes(opt)), { + ...this._def, + ...newDef, + }); + } +} +ZodEnum.create = createZodEnum; +export class ZodNativeEnum extends ZodType { + _parse(input) { + const nativeEnumValues = util.getValidEnumValues(this._def.values); + const ctx = this._getOrReturnCtx(input); + if (ctx.parsedType !== ZodParsedType.string && ctx.parsedType !== ZodParsedType.number) { + const expectedValues = util.objectValues(nativeEnumValues); + addIssueToContext(ctx, { + expected: util.joinValues(expectedValues), + received: ctx.parsedType, + code: ZodIssueCode.invalid_type, + }); + return INVALID; + } + if (!this._cache) { + this._cache = new Set(util.getValidEnumValues(this._def.values)); + } + if (!this._cache.has(input.data)) { + const expectedValues = util.objectValues(nativeEnumValues); + addIssueToContext(ctx, { + received: ctx.data, + code: ZodIssueCode.invalid_enum_value, + options: expectedValues, + }); + return INVALID; + } + return OK(input.data); + } + get enum() { + return this._def.values; + } +} +ZodNativeEnum.create = (values, params) => { + return new ZodNativeEnum({ + values: values, + typeName: ZodFirstPartyTypeKind.ZodNativeEnum, + ...processCreateParams(params), + }); +}; +export class ZodPromise extends ZodType { + unwrap() { + return this._def.type; + } + _parse(input) { + const { ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.promise && ctx.common.async === false) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.promise, + received: ctx.parsedType, + }); + return INVALID; + } + const promisified = ctx.parsedType === ZodParsedType.promise ? ctx.data : Promise.resolve(ctx.data); + return OK(promisified.then((data) => { + return this._def.type.parseAsync(data, { + path: ctx.path, + errorMap: ctx.common.contextualErrorMap, + }); + })); + } +} +ZodPromise.create = (schema, params) => { + return new ZodPromise({ + type: schema, + typeName: ZodFirstPartyTypeKind.ZodPromise, + ...processCreateParams(params), + }); +}; +export class ZodEffects extends ZodType { + innerType() { + return this._def.schema; + } + sourceType() { + return this._def.schema._def.typeName === ZodFirstPartyTypeKind.ZodEffects + ? this._def.schema.sourceType() + : this._def.schema; + } + _parse(input) { + const { status, ctx } = this._processInputParams(input); + const effect = this._def.effect || null; + const checkCtx = { + addIssue: (arg) => { + addIssueToContext(ctx, arg); + if (arg.fatal) { + status.abort(); + } + else { + status.dirty(); + } + }, + get path() { + return ctx.path; + }, + }; + checkCtx.addIssue = checkCtx.addIssue.bind(checkCtx); + if (effect.type === "preprocess") { + const processed = effect.transform(ctx.data, checkCtx); + if (ctx.common.async) { + return Promise.resolve(processed).then(async (processed) => { + if (status.value === "aborted") + return INVALID; + const result = await this._def.schema._parseAsync({ + data: processed, + path: ctx.path, + parent: ctx, + }); + if (result.status === "aborted") + return INVALID; + if (result.status === "dirty") + return DIRTY(result.value); + if (status.value === "dirty") + return DIRTY(result.value); + return result; + }); + } + else { + if (status.value === "aborted") + return INVALID; + const result = this._def.schema._parseSync({ + data: processed, + path: ctx.path, + parent: ctx, + }); + if (result.status === "aborted") + return INVALID; + if (result.status === "dirty") + return DIRTY(result.value); + if (status.value === "dirty") + return DIRTY(result.value); + return result; + } + } + if (effect.type === "refinement") { + const executeRefinement = (acc) => { + const result = effect.refinement(acc, checkCtx); + if (ctx.common.async) { + return Promise.resolve(result); + } + if (result instanceof Promise) { + throw new Error("Async refinement encountered during synchronous parse operation. Use .parseAsync instead."); + } + return acc; + }; + if (ctx.common.async === false) { + const inner = this._def.schema._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + if (inner.status === "aborted") + return INVALID; + if (inner.status === "dirty") + status.dirty(); + // return value is ignored + executeRefinement(inner.value); + return { status: status.value, value: inner.value }; + } + else { + return this._def.schema._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }).then((inner) => { + if (inner.status === "aborted") + return INVALID; + if (inner.status === "dirty") + status.dirty(); + return executeRefinement(inner.value).then(() => { + return { status: status.value, value: inner.value }; + }); + }); + } + } + if (effect.type === "transform") { + if (ctx.common.async === false) { + const base = this._def.schema._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + if (!isValid(base)) + return INVALID; + const result = effect.transform(base.value, checkCtx); + if (result instanceof Promise) { + throw new Error(`Asynchronous transform encountered during synchronous parse operation. Use .parseAsync instead.`); + } + return { status: status.value, value: result }; + } + else { + return this._def.schema._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }).then((base) => { + if (!isValid(base)) + return INVALID; + return Promise.resolve(effect.transform(base.value, checkCtx)).then((result) => ({ + status: status.value, + value: result, + })); + }); + } + } + util.assertNever(effect); + } +} +ZodEffects.create = (schema, effect, params) => { + return new ZodEffects({ + schema, + typeName: ZodFirstPartyTypeKind.ZodEffects, + effect, + ...processCreateParams(params), + }); +}; +ZodEffects.createWithPreprocess = (preprocess, schema, params) => { + return new ZodEffects({ + schema, + effect: { type: "preprocess", transform: preprocess }, + typeName: ZodFirstPartyTypeKind.ZodEffects, + ...processCreateParams(params), + }); +}; +export { ZodEffects as ZodTransformer }; +export class ZodOptional extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType === ZodParsedType.undefined) { + return OK(undefined); + } + return this._def.innerType._parse(input); + } + unwrap() { + return this._def.innerType; + } +} +ZodOptional.create = (type, params) => { + return new ZodOptional({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodOptional, + ...processCreateParams(params), + }); +}; +export class ZodNullable extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType === ZodParsedType.null) { + return OK(null); + } + return this._def.innerType._parse(input); + } + unwrap() { + return this._def.innerType; + } +} +ZodNullable.create = (type, params) => { + return new ZodNullable({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodNullable, + ...processCreateParams(params), + }); +}; +export class ZodDefault extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + let data = ctx.data; + if (ctx.parsedType === ZodParsedType.undefined) { + data = this._def.defaultValue(); + } + return this._def.innerType._parse({ + data, + path: ctx.path, + parent: ctx, + }); + } + removeDefault() { + return this._def.innerType; + } +} +ZodDefault.create = (type, params) => { + return new ZodDefault({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodDefault, + defaultValue: typeof params.default === "function" ? params.default : () => params.default, + ...processCreateParams(params), + }); +}; +export class ZodCatch extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + // newCtx is used to not collect issues from inner types in ctx + const newCtx = { + ...ctx, + common: { + ...ctx.common, + issues: [], + }, + }; + const result = this._def.innerType._parse({ + data: newCtx.data, + path: newCtx.path, + parent: { + ...newCtx, + }, + }); + if (isAsync(result)) { + return result.then((result) => { + return { + status: "valid", + value: result.status === "valid" + ? result.value + : this._def.catchValue({ + get error() { + return new ZodError(newCtx.common.issues); + }, + input: newCtx.data, + }), + }; + }); + } + else { + return { + status: "valid", + value: result.status === "valid" + ? result.value + : this._def.catchValue({ + get error() { + return new ZodError(newCtx.common.issues); + }, + input: newCtx.data, + }), + }; + } + } + removeCatch() { + return this._def.innerType; + } +} +ZodCatch.create = (type, params) => { + return new ZodCatch({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodCatch, + catchValue: typeof params.catch === "function" ? params.catch : () => params.catch, + ...processCreateParams(params), + }); +}; +export class ZodNaN extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.nan) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.nan, + received: ctx.parsedType, + }); + return INVALID; + } + return { status: "valid", value: input.data }; + } +} +ZodNaN.create = (params) => { + return new ZodNaN({ + typeName: ZodFirstPartyTypeKind.ZodNaN, + ...processCreateParams(params), + }); +}; +export const BRAND = Symbol("zod_brand"); +export class ZodBranded extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + const data = ctx.data; + return this._def.type._parse({ + data, + path: ctx.path, + parent: ctx, + }); + } + unwrap() { + return this._def.type; + } +} +export class ZodPipeline extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.common.async) { + const handleAsync = async () => { + const inResult = await this._def.in._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + if (inResult.status === "aborted") + return INVALID; + if (inResult.status === "dirty") { + status.dirty(); + return DIRTY(inResult.value); + } + else { + return this._def.out._parseAsync({ + data: inResult.value, + path: ctx.path, + parent: ctx, + }); + } + }; + return handleAsync(); + } + else { + const inResult = this._def.in._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + if (inResult.status === "aborted") + return INVALID; + if (inResult.status === "dirty") { + status.dirty(); + return { + status: "dirty", + value: inResult.value, + }; + } + else { + return this._def.out._parseSync({ + data: inResult.value, + path: ctx.path, + parent: ctx, + }); + } + } + } + static create(a, b) { + return new ZodPipeline({ + in: a, + out: b, + typeName: ZodFirstPartyTypeKind.ZodPipeline, + }); + } +} +export class ZodReadonly extends ZodType { + _parse(input) { + const result = this._def.innerType._parse(input); + const freeze = (data) => { + if (isValid(data)) { + data.value = Object.freeze(data.value); + } + return data; + }; + return isAsync(result) ? result.then((data) => freeze(data)) : freeze(result); + } + unwrap() { + return this._def.innerType; + } +} +ZodReadonly.create = (type, params) => { + return new ZodReadonly({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodReadonly, + ...processCreateParams(params), + }); +}; +//////////////////////////////////////// +//////////////////////////////////////// +////////// ////////// +////////// z.custom ////////// +////////// ////////// +//////////////////////////////////////// +//////////////////////////////////////// +function cleanParams(params, data) { + const p = typeof params === "function" ? params(data) : typeof params === "string" ? { message: params } : params; + const p2 = typeof p === "string" ? { message: p } : p; + return p2; +} +export function custom(check, _params = {}, +/** + * @deprecated + * + * Pass `fatal` into the params object instead: + * + * ```ts + * z.string().custom((val) => val.length > 5, { fatal: false }) + * ``` + * + */ +fatal) { + if (check) + return ZodAny.create().superRefine((data, ctx) => { + const r = check(data); + if (r instanceof Promise) { + return r.then((r) => { + if (!r) { + const params = cleanParams(_params, data); + const _fatal = params.fatal ?? fatal ?? true; + ctx.addIssue({ code: "custom", ...params, fatal: _fatal }); + } + }); + } + if (!r) { + const params = cleanParams(_params, data); + const _fatal = params.fatal ?? fatal ?? true; + ctx.addIssue({ code: "custom", ...params, fatal: _fatal }); + } + return; + }); + return ZodAny.create(); +} +export { ZodType as Schema, ZodType as ZodSchema }; +export const late = { + object: ZodObject.lazycreate, +}; +export var ZodFirstPartyTypeKind; +(function (ZodFirstPartyTypeKind) { + ZodFirstPartyTypeKind["ZodString"] = "ZodString"; + ZodFirstPartyTypeKind["ZodNumber"] = "ZodNumber"; + ZodFirstPartyTypeKind["ZodNaN"] = "ZodNaN"; + ZodFirstPartyTypeKind["ZodBigInt"] = "ZodBigInt"; + ZodFirstPartyTypeKind["ZodBoolean"] = "ZodBoolean"; + ZodFirstPartyTypeKind["ZodDate"] = "ZodDate"; + ZodFirstPartyTypeKind["ZodSymbol"] = "ZodSymbol"; + ZodFirstPartyTypeKind["ZodUndefined"] = "ZodUndefined"; + ZodFirstPartyTypeKind["ZodNull"] = "ZodNull"; + ZodFirstPartyTypeKind["ZodAny"] = "ZodAny"; + ZodFirstPartyTypeKind["ZodUnknown"] = "ZodUnknown"; + ZodFirstPartyTypeKind["ZodNever"] = "ZodNever"; + ZodFirstPartyTypeKind["ZodVoid"] = "ZodVoid"; + ZodFirstPartyTypeKind["ZodArray"] = "ZodArray"; + ZodFirstPartyTypeKind["ZodObject"] = "ZodObject"; + ZodFirstPartyTypeKind["ZodUnion"] = "ZodUnion"; + ZodFirstPartyTypeKind["ZodDiscriminatedUnion"] = "ZodDiscriminatedUnion"; + ZodFirstPartyTypeKind["ZodIntersection"] = "ZodIntersection"; + ZodFirstPartyTypeKind["ZodTuple"] = "ZodTuple"; + ZodFirstPartyTypeKind["ZodRecord"] = "ZodRecord"; + ZodFirstPartyTypeKind["ZodMap"] = "ZodMap"; + ZodFirstPartyTypeKind["ZodSet"] = "ZodSet"; + ZodFirstPartyTypeKind["ZodFunction"] = "ZodFunction"; + ZodFirstPartyTypeKind["ZodLazy"] = "ZodLazy"; + ZodFirstPartyTypeKind["ZodLiteral"] = "ZodLiteral"; + ZodFirstPartyTypeKind["ZodEnum"] = "ZodEnum"; + ZodFirstPartyTypeKind["ZodEffects"] = "ZodEffects"; + ZodFirstPartyTypeKind["ZodNativeEnum"] = "ZodNativeEnum"; + ZodFirstPartyTypeKind["ZodOptional"] = "ZodOptional"; + ZodFirstPartyTypeKind["ZodNullable"] = "ZodNullable"; + ZodFirstPartyTypeKind["ZodDefault"] = "ZodDefault"; + ZodFirstPartyTypeKind["ZodCatch"] = "ZodCatch"; + ZodFirstPartyTypeKind["ZodPromise"] = "ZodPromise"; + ZodFirstPartyTypeKind["ZodBranded"] = "ZodBranded"; + ZodFirstPartyTypeKind["ZodPipeline"] = "ZodPipeline"; + ZodFirstPartyTypeKind["ZodReadonly"] = "ZodReadonly"; +})(ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {})); +// requires TS 4.4+ +class Class { + constructor(..._) { } +} +const instanceOfType = ( +// const instanceOfType = any>( +cls, params = { + message: `Input not instance of ${cls.name}`, +}) => custom((data) => data instanceof cls, params); +const stringType = ZodString.create; +const numberType = ZodNumber.create; +const nanType = ZodNaN.create; +const bigIntType = ZodBigInt.create; +const booleanType = ZodBoolean.create; +const dateType = ZodDate.create; +const symbolType = ZodSymbol.create; +const undefinedType = ZodUndefined.create; +const nullType = ZodNull.create; +const anyType = ZodAny.create; +const unknownType = ZodUnknown.create; +const neverType = ZodNever.create; +const voidType = ZodVoid.create; +const arrayType = ZodArray.create; +const objectType = ZodObject.create; +const strictObjectType = ZodObject.strictCreate; +const unionType = ZodUnion.create; +const discriminatedUnionType = ZodDiscriminatedUnion.create; +const intersectionType = ZodIntersection.create; +const tupleType = ZodTuple.create; +const recordType = ZodRecord.create; +const mapType = ZodMap.create; +const setType = ZodSet.create; +const functionType = ZodFunction.create; +const lazyType = ZodLazy.create; +const literalType = ZodLiteral.create; +const enumType = ZodEnum.create; +const nativeEnumType = ZodNativeEnum.create; +const promiseType = ZodPromise.create; +const effectsType = ZodEffects.create; +const optionalType = ZodOptional.create; +const nullableType = ZodNullable.create; +const preprocessType = ZodEffects.createWithPreprocess; +const pipelineType = ZodPipeline.create; +const ostring = () => stringType().optional(); +const onumber = () => numberType().optional(); +const oboolean = () => booleanType().optional(); +export const coerce = { + string: ((arg) => ZodString.create({ ...arg, coerce: true })), + number: ((arg) => ZodNumber.create({ ...arg, coerce: true })), + boolean: ((arg) => ZodBoolean.create({ + ...arg, + coerce: true, + })), + bigint: ((arg) => ZodBigInt.create({ ...arg, coerce: true })), + date: ((arg) => ZodDate.create({ ...arg, coerce: true })), +}; +export { anyType as any, arrayType as array, bigIntType as bigint, booleanType as boolean, dateType as date, discriminatedUnionType as discriminatedUnion, effectsType as effect, enumType as enum, functionType as function, instanceOfType as instanceof, intersectionType as intersection, lazyType as lazy, literalType as literal, mapType as map, nanType as nan, nativeEnumType as nativeEnum, neverType as never, nullType as null, nullableType as nullable, numberType as number, objectType as object, oboolean, onumber, optionalType as optional, ostring, pipelineType as pipeline, preprocessType as preprocess, promiseType as promise, recordType as record, setType as set, strictObjectType as strictObject, stringType as string, symbolType as symbol, effectsType as transformer, tupleType as tuple, undefinedType as undefined, unionType as union, unknownType as unknown, voidType as void, }; +export const NEVER = INVALID; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/index.d.ts b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/index.d.ts new file mode 100644 index 000000000..8352ab713 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/index.d.ts @@ -0,0 +1,3 @@ +import z3 from "./v3/index.js"; +export * from "./v3/index.js"; +export default z3; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/ZodError.d.ts b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/ZodError.d.ts new file mode 100644 index 000000000..4e71355a3 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/ZodError.d.ts @@ -0,0 +1,164 @@ +import type { Primitive } from "./helpers/typeAliases.js"; +import { util, type ZodParsedType } from "./helpers/util.js"; +import type { TypeOf, ZodType } from "./index.js"; +type allKeys = T extends any ? keyof T : never; +export type inferFlattenedErrors, U = string> = typeToFlattenedError, U>; +export type typeToFlattenedError = { + formErrors: U[]; + fieldErrors: { + [P in allKeys]?: U[]; + }; +}; +export declare const ZodIssueCode: { + invalid_type: "invalid_type"; + invalid_literal: "invalid_literal"; + custom: "custom"; + invalid_union: "invalid_union"; + invalid_union_discriminator: "invalid_union_discriminator"; + invalid_enum_value: "invalid_enum_value"; + unrecognized_keys: "unrecognized_keys"; + invalid_arguments: "invalid_arguments"; + invalid_return_type: "invalid_return_type"; + invalid_date: "invalid_date"; + invalid_string: "invalid_string"; + too_small: "too_small"; + too_big: "too_big"; + invalid_intersection_types: "invalid_intersection_types"; + not_multiple_of: "not_multiple_of"; + not_finite: "not_finite"; +}; +export type ZodIssueCode = keyof typeof ZodIssueCode; +export type ZodIssueBase = { + path: (string | number)[]; + message?: string | undefined; +}; +export interface ZodInvalidTypeIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_type; + expected: ZodParsedType; + received: ZodParsedType; +} +export interface ZodInvalidLiteralIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_literal; + expected: unknown; + received: unknown; +} +export interface ZodUnrecognizedKeysIssue extends ZodIssueBase { + code: typeof ZodIssueCode.unrecognized_keys; + keys: string[]; +} +export interface ZodInvalidUnionIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_union; + unionErrors: ZodError[]; +} +export interface ZodInvalidUnionDiscriminatorIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_union_discriminator; + options: Primitive[]; +} +export interface ZodInvalidEnumValueIssue extends ZodIssueBase { + received: string | number; + code: typeof ZodIssueCode.invalid_enum_value; + options: (string | number)[]; +} +export interface ZodInvalidArgumentsIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_arguments; + argumentsError: ZodError; +} +export interface ZodInvalidReturnTypeIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_return_type; + returnTypeError: ZodError; +} +export interface ZodInvalidDateIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_date; +} +export type StringValidation = "email" | "url" | "emoji" | "uuid" | "nanoid" | "regex" | "cuid" | "cuid2" | "ulid" | "datetime" | "date" | "time" | "duration" | "ip" | "cidr" | "base64" | "jwt" | "base64url" | { + includes: string; + position?: number | undefined; +} | { + startsWith: string; +} | { + endsWith: string; +}; +export interface ZodInvalidStringIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_string; + validation: StringValidation; +} +export interface ZodTooSmallIssue extends ZodIssueBase { + code: typeof ZodIssueCode.too_small; + minimum: number | bigint; + inclusive: boolean; + exact?: boolean; + type: "array" | "string" | "number" | "set" | "date" | "bigint"; +} +export interface ZodTooBigIssue extends ZodIssueBase { + code: typeof ZodIssueCode.too_big; + maximum: number | bigint; + inclusive: boolean; + exact?: boolean; + type: "array" | "string" | "number" | "set" | "date" | "bigint"; +} +export interface ZodInvalidIntersectionTypesIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_intersection_types; +} +export interface ZodNotMultipleOfIssue extends ZodIssueBase { + code: typeof ZodIssueCode.not_multiple_of; + multipleOf: number | bigint; +} +export interface ZodNotFiniteIssue extends ZodIssueBase { + code: typeof ZodIssueCode.not_finite; +} +export interface ZodCustomIssue extends ZodIssueBase { + code: typeof ZodIssueCode.custom; + params?: { + [k: string]: any; + }; +} +export type DenormalizedError = { + [k: string]: DenormalizedError | string[]; +}; +export type ZodIssueOptionalMessage = ZodInvalidTypeIssue | ZodInvalidLiteralIssue | ZodUnrecognizedKeysIssue | ZodInvalidUnionIssue | ZodInvalidUnionDiscriminatorIssue | ZodInvalidEnumValueIssue | ZodInvalidArgumentsIssue | ZodInvalidReturnTypeIssue | ZodInvalidDateIssue | ZodInvalidStringIssue | ZodTooSmallIssue | ZodTooBigIssue | ZodInvalidIntersectionTypesIssue | ZodNotMultipleOfIssue | ZodNotFiniteIssue | ZodCustomIssue; +export type ZodIssue = ZodIssueOptionalMessage & { + fatal?: boolean | undefined; + message: string; +}; +export declare const quotelessJson: (obj: any) => string; +type recursiveZodFormattedError = T extends [any, ...any[]] ? { + [K in keyof T]?: ZodFormattedError; +} : T extends any[] ? { + [k: number]: ZodFormattedError; +} : T extends object ? { + [K in keyof T]?: ZodFormattedError; +} : unknown; +export type ZodFormattedError = { + _errors: U[]; +} & recursiveZodFormattedError>; +export type inferFormattedError, U = string> = ZodFormattedError, U>; +export declare class ZodError extends Error { + issues: ZodIssue[]; + get errors(): ZodIssue[]; + constructor(issues: ZodIssue[]); + format(): ZodFormattedError; + format(mapper: (issue: ZodIssue) => U): ZodFormattedError; + static create: (issues: ZodIssue[]) => ZodError; + static assert(value: unknown): asserts value is ZodError; + toString(): string; + get message(): string; + get isEmpty(): boolean; + addIssue: (sub: ZodIssue) => void; + addIssues: (subs?: ZodIssue[]) => void; + flatten(): typeToFlattenedError; + flatten(mapper?: (issue: ZodIssue) => U): typeToFlattenedError; + get formErrors(): typeToFlattenedError; +} +type stripPath = T extends any ? util.OmitKeys : never; +export type IssueData = stripPath & { + path?: (string | number)[]; + fatal?: boolean | undefined; +}; +export type ErrorMapCtx = { + defaultError: string; + data: any; +}; +export type ZodErrorMap = (issue: ZodIssueOptionalMessage, _ctx: ErrorMapCtx) => { + message: string; +}; +export {}; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/errors.d.ts b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/errors.d.ts new file mode 100644 index 000000000..52eacf72e --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/errors.d.ts @@ -0,0 +1,5 @@ +import type { ZodErrorMap } from "./ZodError.js"; +import defaultErrorMap from "./locales/en.js"; +export { defaultErrorMap }; +export declare function setErrorMap(map: ZodErrorMap): void; +export declare function getErrorMap(): ZodErrorMap; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/external.d.ts b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/external.d.ts new file mode 100644 index 000000000..f0a4be4c7 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/external.d.ts @@ -0,0 +1,6 @@ +export * from "./errors.js"; +export * from "./helpers/parseUtil.js"; +export * from "./helpers/typeAliases.js"; +export * from "./helpers/util.js"; +export * from "./types.js"; +export * from "./ZodError.js"; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/enumUtil.d.ts b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/enumUtil.d.ts new file mode 100644 index 000000000..fad28a711 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/enumUtil.d.ts @@ -0,0 +1,8 @@ +export declare namespace enumUtil { + type UnionToIntersectionFn = (T extends unknown ? (k: () => T) => void : never) extends (k: infer Intersection) => void ? Intersection : never; + type GetUnionLast = UnionToIntersectionFn extends () => infer Last ? Last : never; + type UnionToTuple = [T] extends [never] ? Tuple : UnionToTuple>, [GetUnionLast, ...Tuple]>; + type CastToStringTuple = T extends [string, ...string[]] ? T : never; + export type UnionToTupleString = CastToStringTuple>; + export {}; +} diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/errorUtil.d.ts b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/errorUtil.d.ts new file mode 100644 index 000000000..bc9b033ff --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/errorUtil.d.ts @@ -0,0 +1,9 @@ +export declare namespace errorUtil { + type ErrMessage = string | { + message?: string | undefined; + }; + const errToObj: (message?: ErrMessage) => { + message?: string | undefined; + }; + const toString: (message?: ErrMessage) => string | undefined; +} diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/parseUtil.d.ts b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/parseUtil.d.ts new file mode 100644 index 000000000..e4dc1d7b3 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/parseUtil.d.ts @@ -0,0 +1,78 @@ +import type { IssueData, ZodErrorMap, ZodIssue } from "../ZodError.js"; +import type { ZodParsedType } from "./util.js"; +export declare const makeIssue: (params: { + data: any; + path: (string | number)[]; + errorMaps: ZodErrorMap[]; + issueData: IssueData; +}) => ZodIssue; +export type ParseParams = { + path: (string | number)[]; + errorMap: ZodErrorMap; + async: boolean; +}; +export type ParsePathComponent = string | number; +export type ParsePath = ParsePathComponent[]; +export declare const EMPTY_PATH: ParsePath; +export interface ParseContext { + readonly common: { + readonly issues: ZodIssue[]; + readonly contextualErrorMap?: ZodErrorMap | undefined; + readonly async: boolean; + }; + readonly path: ParsePath; + readonly schemaErrorMap?: ZodErrorMap | undefined; + readonly parent: ParseContext | null; + readonly data: any; + readonly parsedType: ZodParsedType; +} +export type ParseInput = { + data: any; + path: (string | number)[]; + parent: ParseContext; +}; +export declare function addIssueToContext(ctx: ParseContext, issueData: IssueData): void; +export type ObjectPair = { + key: SyncParseReturnType; + value: SyncParseReturnType; +}; +export declare class ParseStatus { + value: "aborted" | "dirty" | "valid"; + dirty(): void; + abort(): void; + static mergeArray(status: ParseStatus, results: SyncParseReturnType[]): SyncParseReturnType; + static mergeObjectAsync(status: ParseStatus, pairs: { + key: ParseReturnType; + value: ParseReturnType; + }[]): Promise>; + static mergeObjectSync(status: ParseStatus, pairs: { + key: SyncParseReturnType; + value: SyncParseReturnType; + alwaysSet?: boolean; + }[]): SyncParseReturnType; +} +export interface ParseResult { + status: "aborted" | "dirty" | "valid"; + data: any; +} +export type INVALID = { + status: "aborted"; +}; +export declare const INVALID: INVALID; +export type DIRTY = { + status: "dirty"; + value: T; +}; +export declare const DIRTY: (value: T) => DIRTY; +export type OK = { + status: "valid"; + value: T; +}; +export declare const OK: (value: T) => OK; +export type SyncParseReturnType = OK | DIRTY | INVALID; +export type AsyncParseReturnType = Promise>; +export type ParseReturnType = SyncParseReturnType | AsyncParseReturnType; +export declare const isAborted: (x: ParseReturnType) => x is INVALID; +export declare const isDirty: (x: ParseReturnType) => x is OK | DIRTY; +export declare const isValid: (x: ParseReturnType) => x is OK; +export declare const isAsync: (x: ParseReturnType) => x is AsyncParseReturnType; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/partialUtil.d.ts b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/partialUtil.d.ts new file mode 100644 index 000000000..d07657a25 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/partialUtil.d.ts @@ -0,0 +1,8 @@ +import type { ZodArray, ZodNullable, ZodObject, ZodOptional, ZodRawShape, ZodTuple, ZodTupleItems, ZodTypeAny } from "../types.js"; +export declare namespace partialUtil { + type DeepPartial = T extends ZodObject ? ZodObject<{ + [k in keyof T["shape"]]: ZodOptional>; + }, T["_def"]["unknownKeys"], T["_def"]["catchall"]> : T extends ZodArray ? ZodArray, Card> : T extends ZodOptional ? ZodOptional> : T extends ZodNullable ? ZodNullable> : T extends ZodTuple ? { + [k in keyof Items]: Items[k] extends ZodTypeAny ? DeepPartial : never; + } extends infer PI ? PI extends ZodTupleItems ? ZodTuple : never : never : T; +} diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/typeAliases.d.ts b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/typeAliases.d.ts new file mode 100644 index 000000000..32df022ca --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/typeAliases.d.ts @@ -0,0 +1,2 @@ +export type Primitive = string | number | symbol | bigint | boolean | null | undefined; +export type Scalars = Primitive | Primitive[]; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/util.d.ts b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/util.d.ts new file mode 100644 index 000000000..eeaaeee1c --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/util.d.ts @@ -0,0 +1,85 @@ +export declare namespace util { + type AssertEqual = (() => V extends T ? 1 : 2) extends () => V extends U ? 1 : 2 ? true : false; + export type isAny = 0 extends 1 & T ? true : false; + export const assertEqual: (_: AssertEqual) => void; + export function assertIs(_arg: T): void; + export function assertNever(_x: never): never; + export type Omit = Pick>; + export type OmitKeys = Pick>; + export type MakePartial = Omit & Partial>; + export type Exactly = T & Record, never>; + export type InexactPartial = { + [k in keyof T]?: T[k] | undefined; + }; + export const arrayToEnum: (items: U) => { [k in U[number]]: k; }; + export const getValidEnumValues: (obj: any) => any[]; + export const objectValues: (obj: any) => any[]; + export const objectKeys: ObjectConstructor["keys"]; + export const find: (arr: T[], checker: (arg: T) => any) => T | undefined; + export type identity = objectUtil.identity; + export type flatten = objectUtil.flatten; + export type noUndefined = T extends undefined ? never : T; + export const isInteger: NumberConstructor["isInteger"]; + export function joinValues(array: T, separator?: string): string; + export const jsonStringifyReplacer: (_: string, value: any) => any; + export {}; +} +export declare namespace objectUtil { + export type MergeShapes = keyof U & keyof V extends never ? U & V : { + [k in Exclude]: U[k]; + } & V; + type optionalKeys = { + [k in keyof T]: undefined extends T[k] ? k : never; + }[keyof T]; + type requiredKeys = { + [k in keyof T]: undefined extends T[k] ? never : k; + }[keyof T]; + export type addQuestionMarks = { + [K in requiredKeys]: T[K]; + } & { + [K in optionalKeys]?: T[K]; + } & { + [k in keyof T]?: unknown; + }; + export type identity = T; + export type flatten = identity<{ + [k in keyof T]: T[k]; + }>; + export type noNeverKeys = { + [k in keyof T]: [T[k]] extends [never] ? never : k; + }[keyof T]; + export type noNever = identity<{ + [k in noNeverKeys]: k extends keyof T ? T[k] : never; + }>; + export const mergeShapes: (first: U, second: T) => T & U; + export type extendShape = keyof A & keyof B extends never ? A & B : { + [K in keyof A as K extends keyof B ? never : K]: A[K]; + } & { + [K in keyof B]: B[K]; + }; + export {}; +} +export declare const ZodParsedType: { + string: "string"; + nan: "nan"; + number: "number"; + integer: "integer"; + float: "float"; + boolean: "boolean"; + date: "date"; + bigint: "bigint"; + symbol: "symbol"; + function: "function"; + undefined: "undefined"; + null: "null"; + array: "array"; + object: "object"; + unknown: "unknown"; + promise: "promise"; + void: "void"; + never: "never"; + map: "map"; + set: "set"; +}; +export type ZodParsedType = keyof typeof ZodParsedType; +export declare const getParsedType: (data: any) => ZodParsedType; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/index.d.ts b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/index.d.ts new file mode 100644 index 000000000..a9dab5738 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/index.d.ts @@ -0,0 +1,4 @@ +import * as z from "./external.js"; +export * from "./external.js"; +export { z }; +export default z; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/locales/en.d.ts b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/locales/en.d.ts new file mode 100644 index 000000000..b2bea1ec4 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/locales/en.d.ts @@ -0,0 +1,3 @@ +import { type ZodErrorMap } from "../ZodError.js"; +declare const errorMap: ZodErrorMap; +export default errorMap; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/standard-schema.d.ts b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/standard-schema.d.ts new file mode 100644 index 000000000..7ddff3b7f --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/standard-schema.d.ts @@ -0,0 +1,102 @@ +/** + * The Standard Schema interface. + */ +export type StandardSchemaV1 = { + /** + * The Standard Schema properties. + */ + readonly "~standard": StandardSchemaV1.Props; +}; +export declare namespace StandardSchemaV1 { + /** + * The Standard Schema properties interface. + */ + export interface Props { + /** + * The version number of the standard. + */ + readonly version: 1; + /** + * The vendor name of the schema library. + */ + readonly vendor: string; + /** + * Validates unknown input values. + */ + readonly validate: (value: unknown) => Result | Promise>; + /** + * Inferred types associated with the schema. + */ + readonly types?: Types | undefined; + } + /** + * The result interface of the validate function. + */ + export type Result = SuccessResult | FailureResult; + /** + * The result interface if validation succeeds. + */ + export interface SuccessResult { + /** + * The typed output value. + */ + readonly value: Output; + /** + * The non-existent issues. + */ + readonly issues?: undefined; + } + /** + * The result interface if validation fails. + */ + export interface FailureResult { + /** + * The issues of failed validation. + */ + readonly issues: ReadonlyArray; + } + /** + * The issue interface of the failure output. + */ + export interface Issue { + /** + * The error message of the issue. + */ + readonly message: string; + /** + * The path of the issue, if any. + */ + readonly path?: ReadonlyArray | undefined; + } + /** + * The path segment interface of the issue. + */ + export interface PathSegment { + /** + * The key representing a path segment. + */ + readonly key: PropertyKey; + } + /** + * The Standard Schema types interface. + */ + export interface Types { + /** + * The input type of the schema. + */ + readonly input: Input; + /** + * The output type of the schema. + */ + readonly output: Output; + } + /** + * Infers the input type of a Standard Schema. + */ + export type InferInput = NonNullable["input"]; + /** + * Infers the output type of a Standard Schema. + */ + export type InferOutput = NonNullable["output"]; + export {}; +} diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/types.d.ts b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/types.d.ts new file mode 100644 index 000000000..301e09128 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/types.d.ts @@ -0,0 +1,1031 @@ +import { type IssueData, type StringValidation, type ZodCustomIssue, ZodError, type ZodErrorMap } from "./ZodError.js"; +import type { enumUtil } from "./helpers/enumUtil.js"; +import { errorUtil } from "./helpers/errorUtil.js"; +import { type AsyncParseReturnType, INVALID, type ParseContext, type ParseInput, type ParseParams, type ParseReturnType, ParseStatus, type SyncParseReturnType } from "./helpers/parseUtil.js"; +import type { partialUtil } from "./helpers/partialUtil.js"; +import type { Primitive } from "./helpers/typeAliases.js"; +import { util, type objectUtil } from "./helpers/util.js"; +import type { StandardSchemaV1 } from "./standard-schema.js"; +export interface RefinementCtx { + addIssue: (arg: IssueData) => void; + path: (string | number)[]; +} +export type ZodRawShape = { + [k: string]: ZodTypeAny; +}; +export type ZodTypeAny = ZodType; +export type TypeOf> = T["_output"]; +export type input> = T["_input"]; +export type output> = T["_output"]; +export type { TypeOf as infer }; +export type CustomErrorParams = Partial>; +export interface ZodTypeDef { + errorMap?: ZodErrorMap | undefined; + description?: string | undefined; +} +export type RawCreateParams = { + errorMap?: ZodErrorMap | undefined; + invalid_type_error?: string | undefined; + required_error?: string | undefined; + message?: string | undefined; + description?: string | undefined; +} | undefined; +export type ProcessedCreateParams = { + errorMap?: ZodErrorMap | undefined; + description?: string | undefined; +}; +export type SafeParseSuccess = { + success: true; + data: Output; + error?: never; +}; +export type SafeParseError = { + success: false; + error: ZodError; + data?: never; +}; +export type SafeParseReturnType = SafeParseSuccess | SafeParseError; +export declare abstract class ZodType { + readonly _type: Output; + readonly _output: Output; + readonly _input: Input; + readonly _def: Def; + get description(): string | undefined; + "~standard": StandardSchemaV1.Props; + abstract _parse(input: ParseInput): ParseReturnType; + _getType(input: ParseInput): string; + _getOrReturnCtx(input: ParseInput, ctx?: ParseContext | undefined): ParseContext; + _processInputParams(input: ParseInput): { + status: ParseStatus; + ctx: ParseContext; + }; + _parseSync(input: ParseInput): SyncParseReturnType; + _parseAsync(input: ParseInput): AsyncParseReturnType; + parse(data: unknown, params?: util.InexactPartial): Output; + safeParse(data: unknown, params?: util.InexactPartial): SafeParseReturnType; + "~validate"(data: unknown): StandardSchemaV1.Result | Promise>; + parseAsync(data: unknown, params?: util.InexactPartial): Promise; + safeParseAsync(data: unknown, params?: util.InexactPartial): Promise>; + /** Alias of safeParseAsync */ + spa: (data: unknown, params?: util.InexactPartial) => Promise>; + refine(check: (arg: Output) => arg is RefinedOutput, message?: string | CustomErrorParams | ((arg: Output) => CustomErrorParams)): ZodEffects; + refine(check: (arg: Output) => unknown | Promise, message?: string | CustomErrorParams | ((arg: Output) => CustomErrorParams)): ZodEffects; + refinement(check: (arg: Output) => arg is RefinedOutput, refinementData: IssueData | ((arg: Output, ctx: RefinementCtx) => IssueData)): ZodEffects; + refinement(check: (arg: Output) => boolean, refinementData: IssueData | ((arg: Output, ctx: RefinementCtx) => IssueData)): ZodEffects; + _refinement(refinement: RefinementEffect["refinement"]): ZodEffects; + superRefine(refinement: (arg: Output, ctx: RefinementCtx) => arg is RefinedOutput): ZodEffects; + superRefine(refinement: (arg: Output, ctx: RefinementCtx) => void): ZodEffects; + superRefine(refinement: (arg: Output, ctx: RefinementCtx) => Promise): ZodEffects; + constructor(def: Def); + optional(): ZodOptional; + nullable(): ZodNullable; + nullish(): ZodOptional>; + array(): ZodArray; + promise(): ZodPromise; + or(option: T): ZodUnion<[this, T]>; + and(incoming: T): ZodIntersection; + transform(transform: (arg: Output, ctx: RefinementCtx) => NewOut | Promise): ZodEffects; + default(def: util.noUndefined): ZodDefault; + default(def: () => util.noUndefined): ZodDefault; + brand(brand?: B): ZodBranded; + catch(def: Output): ZodCatch; + catch(def: (ctx: { + error: ZodError; + input: Input; + }) => Output): ZodCatch; + describe(description: string): this; + pipe(target: T): ZodPipeline; + readonly(): ZodReadonly; + isOptional(): boolean; + isNullable(): boolean; +} +export type IpVersion = "v4" | "v6"; +export type ZodStringCheck = { + kind: "min"; + value: number; + message?: string | undefined; +} | { + kind: "max"; + value: number; + message?: string | undefined; +} | { + kind: "length"; + value: number; + message?: string | undefined; +} | { + kind: "email"; + message?: string | undefined; +} | { + kind: "url"; + message?: string | undefined; +} | { + kind: "emoji"; + message?: string | undefined; +} | { + kind: "uuid"; + message?: string | undefined; +} | { + kind: "nanoid"; + message?: string | undefined; +} | { + kind: "cuid"; + message?: string | undefined; +} | { + kind: "includes"; + value: string; + position?: number | undefined; + message?: string | undefined; +} | { + kind: "cuid2"; + message?: string | undefined; +} | { + kind: "ulid"; + message?: string | undefined; +} | { + kind: "startsWith"; + value: string; + message?: string | undefined; +} | { + kind: "endsWith"; + value: string; + message?: string | undefined; +} | { + kind: "regex"; + regex: RegExp; + message?: string | undefined; +} | { + kind: "trim"; + message?: string | undefined; +} | { + kind: "toLowerCase"; + message?: string | undefined; +} | { + kind: "toUpperCase"; + message?: string | undefined; +} | { + kind: "jwt"; + alg?: string; + message?: string | undefined; +} | { + kind: "datetime"; + offset: boolean; + local: boolean; + precision: number | null; + message?: string | undefined; +} | { + kind: "date"; + message?: string | undefined; +} | { + kind: "time"; + precision: number | null; + message?: string | undefined; +} | { + kind: "duration"; + message?: string | undefined; +} | { + kind: "ip"; + version?: IpVersion | undefined; + message?: string | undefined; +} | { + kind: "cidr"; + version?: IpVersion | undefined; + message?: string | undefined; +} | { + kind: "base64"; + message?: string | undefined; +} | { + kind: "base64url"; + message?: string | undefined; +}; +export interface ZodStringDef extends ZodTypeDef { + checks: ZodStringCheck[]; + typeName: ZodFirstPartyTypeKind.ZodString; + coerce: boolean; +} +export declare function datetimeRegex(args: { + precision?: number | null; + offset?: boolean; + local?: boolean; +}): RegExp; +export declare class ZodString extends ZodType { + _parse(input: ParseInput): ParseReturnType; + protected _regex(regex: RegExp, validation: StringValidation, message?: errorUtil.ErrMessage): ZodEffects; + _addCheck(check: ZodStringCheck): ZodString; + email(message?: errorUtil.ErrMessage): ZodString; + url(message?: errorUtil.ErrMessage): ZodString; + emoji(message?: errorUtil.ErrMessage): ZodString; + uuid(message?: errorUtil.ErrMessage): ZodString; + nanoid(message?: errorUtil.ErrMessage): ZodString; + cuid(message?: errorUtil.ErrMessage): ZodString; + cuid2(message?: errorUtil.ErrMessage): ZodString; + ulid(message?: errorUtil.ErrMessage): ZodString; + base64(message?: errorUtil.ErrMessage): ZodString; + base64url(message?: errorUtil.ErrMessage): ZodString; + jwt(options?: { + alg?: string; + message?: string | undefined; + }): ZodString; + ip(options?: string | { + version?: IpVersion; + message?: string | undefined; + }): ZodString; + cidr(options?: string | { + version?: IpVersion; + message?: string | undefined; + }): ZodString; + datetime(options?: string | { + message?: string | undefined; + precision?: number | null; + offset?: boolean; + local?: boolean; + }): ZodString; + date(message?: string): ZodString; + time(options?: string | { + message?: string | undefined; + precision?: number | null; + }): ZodString; + duration(message?: errorUtil.ErrMessage): ZodString; + regex(regex: RegExp, message?: errorUtil.ErrMessage): ZodString; + includes(value: string, options?: { + message?: string; + position?: number; + }): ZodString; + startsWith(value: string, message?: errorUtil.ErrMessage): ZodString; + endsWith(value: string, message?: errorUtil.ErrMessage): ZodString; + min(minLength: number, message?: errorUtil.ErrMessage): ZodString; + max(maxLength: number, message?: errorUtil.ErrMessage): ZodString; + length(len: number, message?: errorUtil.ErrMessage): ZodString; + /** + * Equivalent to `.min(1)` + */ + nonempty(message?: errorUtil.ErrMessage): ZodString; + trim(): ZodString; + toLowerCase(): ZodString; + toUpperCase(): ZodString; + get isDatetime(): boolean; + get isDate(): boolean; + get isTime(): boolean; + get isDuration(): boolean; + get isEmail(): boolean; + get isURL(): boolean; + get isEmoji(): boolean; + get isUUID(): boolean; + get isNANOID(): boolean; + get isCUID(): boolean; + get isCUID2(): boolean; + get isULID(): boolean; + get isIP(): boolean; + get isCIDR(): boolean; + get isBase64(): boolean; + get isBase64url(): boolean; + get minLength(): number | null; + get maxLength(): number | null; + static create: (params?: RawCreateParams & { + coerce?: true; + }) => ZodString; +} +export type ZodNumberCheck = { + kind: "min"; + value: number; + inclusive: boolean; + message?: string | undefined; +} | { + kind: "max"; + value: number; + inclusive: boolean; + message?: string | undefined; +} | { + kind: "int"; + message?: string | undefined; +} | { + kind: "multipleOf"; + value: number; + message?: string | undefined; +} | { + kind: "finite"; + message?: string | undefined; +}; +export interface ZodNumberDef extends ZodTypeDef { + checks: ZodNumberCheck[]; + typeName: ZodFirstPartyTypeKind.ZodNumber; + coerce: boolean; +} +export declare class ZodNumber extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams & { + coerce?: boolean; + }) => ZodNumber; + gte(value: number, message?: errorUtil.ErrMessage): ZodNumber; + min: (value: number, message?: errorUtil.ErrMessage) => ZodNumber; + gt(value: number, message?: errorUtil.ErrMessage): ZodNumber; + lte(value: number, message?: errorUtil.ErrMessage): ZodNumber; + max: (value: number, message?: errorUtil.ErrMessage) => ZodNumber; + lt(value: number, message?: errorUtil.ErrMessage): ZodNumber; + protected setLimit(kind: "min" | "max", value: number, inclusive: boolean, message?: string): ZodNumber; + _addCheck(check: ZodNumberCheck): ZodNumber; + int(message?: errorUtil.ErrMessage): ZodNumber; + positive(message?: errorUtil.ErrMessage): ZodNumber; + negative(message?: errorUtil.ErrMessage): ZodNumber; + nonpositive(message?: errorUtil.ErrMessage): ZodNumber; + nonnegative(message?: errorUtil.ErrMessage): ZodNumber; + multipleOf(value: number, message?: errorUtil.ErrMessage): ZodNumber; + step: (value: number, message?: errorUtil.ErrMessage) => ZodNumber; + finite(message?: errorUtil.ErrMessage): ZodNumber; + safe(message?: errorUtil.ErrMessage): ZodNumber; + get minValue(): number | null; + get maxValue(): number | null; + get isInt(): boolean; + get isFinite(): boolean; +} +export type ZodBigIntCheck = { + kind: "min"; + value: bigint; + inclusive: boolean; + message?: string | undefined; +} | { + kind: "max"; + value: bigint; + inclusive: boolean; + message?: string | undefined; +} | { + kind: "multipleOf"; + value: bigint; + message?: string | undefined; +}; +export interface ZodBigIntDef extends ZodTypeDef { + checks: ZodBigIntCheck[]; + typeName: ZodFirstPartyTypeKind.ZodBigInt; + coerce: boolean; +} +export declare class ZodBigInt extends ZodType { + _parse(input: ParseInput): ParseReturnType; + _getInvalidInput(input: ParseInput): INVALID; + static create: (params?: RawCreateParams & { + coerce?: boolean; + }) => ZodBigInt; + gte(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + min: (value: bigint, message?: errorUtil.ErrMessage) => ZodBigInt; + gt(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + lte(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + max: (value: bigint, message?: errorUtil.ErrMessage) => ZodBigInt; + lt(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + protected setLimit(kind: "min" | "max", value: bigint, inclusive: boolean, message?: string): ZodBigInt; + _addCheck(check: ZodBigIntCheck): ZodBigInt; + positive(message?: errorUtil.ErrMessage): ZodBigInt; + negative(message?: errorUtil.ErrMessage): ZodBigInt; + nonpositive(message?: errorUtil.ErrMessage): ZodBigInt; + nonnegative(message?: errorUtil.ErrMessage): ZodBigInt; + multipleOf(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + get minValue(): bigint | null; + get maxValue(): bigint | null; +} +export interface ZodBooleanDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodBoolean; + coerce: boolean; +} +export declare class ZodBoolean extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams & { + coerce?: boolean; + }) => ZodBoolean; +} +export type ZodDateCheck = { + kind: "min"; + value: number; + message?: string | undefined; +} | { + kind: "max"; + value: number; + message?: string | undefined; +}; +export interface ZodDateDef extends ZodTypeDef { + checks: ZodDateCheck[]; + coerce: boolean; + typeName: ZodFirstPartyTypeKind.ZodDate; +} +export declare class ZodDate extends ZodType { + _parse(input: ParseInput): ParseReturnType; + _addCheck(check: ZodDateCheck): ZodDate; + min(minDate: Date, message?: errorUtil.ErrMessage): ZodDate; + max(maxDate: Date, message?: errorUtil.ErrMessage): ZodDate; + get minDate(): Date | null; + get maxDate(): Date | null; + static create: (params?: RawCreateParams & { + coerce?: boolean; + }) => ZodDate; +} +export interface ZodSymbolDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodSymbol; +} +export declare class ZodSymbol extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodSymbol; +} +export interface ZodUndefinedDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodUndefined; +} +export declare class ZodUndefined extends ZodType { + _parse(input: ParseInput): ParseReturnType; + params?: RawCreateParams; + static create: (params?: RawCreateParams) => ZodUndefined; +} +export interface ZodNullDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodNull; +} +export declare class ZodNull extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodNull; +} +export interface ZodAnyDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodAny; +} +export declare class ZodAny extends ZodType { + _any: true; + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodAny; +} +export interface ZodUnknownDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodUnknown; +} +export declare class ZodUnknown extends ZodType { + _unknown: true; + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodUnknown; +} +export interface ZodNeverDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodNever; +} +export declare class ZodNever extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodNever; +} +export interface ZodVoidDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodVoid; +} +export declare class ZodVoid extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodVoid; +} +export interface ZodArrayDef extends ZodTypeDef { + type: T; + typeName: ZodFirstPartyTypeKind.ZodArray; + exactLength: { + value: number; + message?: string | undefined; + } | null; + minLength: { + value: number; + message?: string | undefined; + } | null; + maxLength: { + value: number; + message?: string | undefined; + } | null; +} +export type ArrayCardinality = "many" | "atleastone"; +export type arrayOutputType = Cardinality extends "atleastone" ? [T["_output"], ...T["_output"][]] : T["_output"][]; +export declare class ZodArray extends ZodType, ZodArrayDef, Cardinality extends "atleastone" ? [T["_input"], ...T["_input"][]] : T["_input"][]> { + _parse(input: ParseInput): ParseReturnType; + get element(): T; + min(minLength: number, message?: errorUtil.ErrMessage): this; + max(maxLength: number, message?: errorUtil.ErrMessage): this; + length(len: number, message?: errorUtil.ErrMessage): this; + nonempty(message?: errorUtil.ErrMessage): ZodArray; + static create: (schema: El, params?: RawCreateParams) => ZodArray; +} +export type ZodNonEmptyArray = ZodArray; +export type UnknownKeysParam = "passthrough" | "strict" | "strip"; +export interface ZodObjectDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodObject; + shape: () => T; + catchall: Catchall; + unknownKeys: UnknownKeys; +} +export type mergeTypes = { + [k in keyof A | keyof B]: k extends keyof B ? B[k] : k extends keyof A ? A[k] : never; +}; +export type objectOutputType = objectUtil.flatten>> & CatchallOutput & PassthroughType; +export type baseObjectOutputType = { + [k in keyof Shape]: Shape[k]["_output"]; +}; +export type objectInputType = objectUtil.flatten> & CatchallInput & PassthroughType; +export type baseObjectInputType = objectUtil.addQuestionMarks<{ + [k in keyof Shape]: Shape[k]["_input"]; +}>; +export type CatchallOutput = ZodType extends T ? unknown : { + [k: string]: T["_output"]; +}; +export type CatchallInput = ZodType extends T ? unknown : { + [k: string]: T["_input"]; +}; +export type PassthroughType = T extends "passthrough" ? { + [k: string]: unknown; +} : unknown; +export type deoptional = T extends ZodOptional ? deoptional : T extends ZodNullable ? ZodNullable> : T; +export type SomeZodObject = ZodObject; +export type noUnrecognized = { + [k in keyof Obj]: k extends keyof Shape ? Obj[k] : never; +}; +export declare class ZodObject, Input = objectInputType> extends ZodType, Input> { + private _cached; + _getCached(): { + shape: T; + keys: string[]; + }; + _parse(input: ParseInput): ParseReturnType; + get shape(): T; + strict(message?: errorUtil.ErrMessage): ZodObject; + strip(): ZodObject; + passthrough(): ZodObject; + /** + * @deprecated In most cases, this is no longer needed - unknown properties are now silently stripped. + * If you want to pass through unknown properties, use `.passthrough()` instead. + */ + nonstrict: () => ZodObject; + extend(augmentation: Augmentation): ZodObject, UnknownKeys, Catchall>; + /** + * @deprecated Use `.extend` instead + * */ + augment: (augmentation: Augmentation) => ZodObject, UnknownKeys, Catchall>; + /** + * Prior to zod@1.0.12 there was a bug in the + * inferred type of merged objects. Please + * upgrade if you are experiencing issues. + */ + merge(merging: Incoming): ZodObject, Incoming["_def"]["unknownKeys"], Incoming["_def"]["catchall"]>; + setKey(key: Key, schema: Schema): ZodObject; + catchall(index: Index): ZodObject; + pick>(mask: Mask): ZodObject>, UnknownKeys, Catchall>; + omit>(mask: Mask): ZodObject, UnknownKeys, Catchall>; + /** + * @deprecated + */ + deepPartial(): partialUtil.DeepPartial; + partial(): ZodObject<{ + [k in keyof T]: ZodOptional; + }, UnknownKeys, Catchall>; + partial>(mask: Mask): ZodObject : T[k]; + }>, UnknownKeys, Catchall>; + required(): ZodObject<{ + [k in keyof T]: deoptional; + }, UnknownKeys, Catchall>; + required>(mask: Mask): ZodObject : T[k]; + }>, UnknownKeys, Catchall>; + keyof(): ZodEnum>; + static create: (shape: Shape, params?: RawCreateParams) => ZodObject, objectInputType>; + static strictCreate: (shape: Shape, params?: RawCreateParams) => ZodObject; + static lazycreate: (shape: () => Shape, params?: RawCreateParams) => ZodObject; +} +export type AnyZodObject = ZodObject; +export type ZodUnionOptions = Readonly<[ZodTypeAny, ...ZodTypeAny[]]>; +export interface ZodUnionDef> extends ZodTypeDef { + options: T; + typeName: ZodFirstPartyTypeKind.ZodUnion; +} +export declare class ZodUnion extends ZodType, T[number]["_input"]> { + _parse(input: ParseInput): ParseReturnType; + get options(): T; + static create: >(types: Options, params?: RawCreateParams) => ZodUnion; +} +export type ZodDiscriminatedUnionOption = ZodObject<{ + [key in Discriminator]: ZodTypeAny; +} & ZodRawShape, UnknownKeysParam, ZodTypeAny>; +export interface ZodDiscriminatedUnionDef[] = ZodDiscriminatedUnionOption[]> extends ZodTypeDef { + discriminator: Discriminator; + options: Options; + optionsMap: Map>; + typeName: ZodFirstPartyTypeKind.ZodDiscriminatedUnion; +} +export declare class ZodDiscriminatedUnion[]> extends ZodType, ZodDiscriminatedUnionDef, input> { + _parse(input: ParseInput): ParseReturnType; + get discriminator(): Discriminator; + get options(): Options; + get optionsMap(): Map>; + /** + * The constructor of the discriminated union schema. Its behaviour is very similar to that of the normal z.union() constructor. + * However, it only allows a union of objects, all of which need to share a discriminator property. This property must + * have a different value for each object in the union. + * @param discriminator the name of the discriminator property + * @param types an array of object schemas + * @param params + */ + static create, + ...ZodDiscriminatedUnionOption[] + ]>(discriminator: Discriminator, options: Types, params?: RawCreateParams): ZodDiscriminatedUnion; +} +export interface ZodIntersectionDef extends ZodTypeDef { + left: T; + right: U; + typeName: ZodFirstPartyTypeKind.ZodIntersection; +} +export declare class ZodIntersection extends ZodType, T["_input"] & U["_input"]> { + _parse(input: ParseInput): ParseReturnType; + static create: (left: TSchema, right: USchema, params?: RawCreateParams) => ZodIntersection; +} +export type ZodTupleItems = [ZodTypeAny, ...ZodTypeAny[]]; +export type AssertArray = T extends any[] ? T : never; +export type OutputTypeOfTuple = AssertArray<{ + [k in keyof T]: T[k] extends ZodType ? T[k]["_output"] : never; +}>; +export type OutputTypeOfTupleWithRest = Rest extends ZodTypeAny ? [...OutputTypeOfTuple, ...Rest["_output"][]] : OutputTypeOfTuple; +export type InputTypeOfTuple = AssertArray<{ + [k in keyof T]: T[k] extends ZodType ? T[k]["_input"] : never; +}>; +export type InputTypeOfTupleWithRest = Rest extends ZodTypeAny ? [...InputTypeOfTuple, ...Rest["_input"][]] : InputTypeOfTuple; +export interface ZodTupleDef extends ZodTypeDef { + items: T; + rest: Rest; + typeName: ZodFirstPartyTypeKind.ZodTuple; +} +export type AnyZodTuple = ZodTuple<[ZodTypeAny, ...ZodTypeAny[]] | [], ZodTypeAny | null>; +export declare class ZodTuple extends ZodType, ZodTupleDef, InputTypeOfTupleWithRest> { + _parse(input: ParseInput): ParseReturnType; + get items(): T; + rest(rest: RestSchema): ZodTuple; + static create: (schemas: Items, params?: RawCreateParams) => ZodTuple; +} +export interface ZodRecordDef extends ZodTypeDef { + valueType: Value; + keyType: Key; + typeName: ZodFirstPartyTypeKind.ZodRecord; +} +export type KeySchema = ZodType; +export type RecordType = [string] extends [K] ? Record : [number] extends [K] ? Record : [symbol] extends [K] ? Record : [BRAND] extends [K] ? Record : Partial>; +export declare class ZodRecord extends ZodType, ZodRecordDef, RecordType> { + get keySchema(): Key; + get valueSchema(): Value; + _parse(input: ParseInput): ParseReturnType; + get element(): Value; + static create(valueType: Value, params?: RawCreateParams): ZodRecord; + static create(keySchema: Keys, valueType: Value, params?: RawCreateParams): ZodRecord; +} +export interface ZodMapDef extends ZodTypeDef { + valueType: Value; + keyType: Key; + typeName: ZodFirstPartyTypeKind.ZodMap; +} +export declare class ZodMap extends ZodType, ZodMapDef, Map> { + get keySchema(): Key; + get valueSchema(): Value; + _parse(input: ParseInput): ParseReturnType; + static create: (keyType: KeySchema, valueType: ValueSchema, params?: RawCreateParams) => ZodMap; +} +export interface ZodSetDef extends ZodTypeDef { + valueType: Value; + typeName: ZodFirstPartyTypeKind.ZodSet; + minSize: { + value: number; + message?: string | undefined; + } | null; + maxSize: { + value: number; + message?: string | undefined; + } | null; +} +export declare class ZodSet extends ZodType, ZodSetDef, Set> { + _parse(input: ParseInput): ParseReturnType; + min(minSize: number, message?: errorUtil.ErrMessage): this; + max(maxSize: number, message?: errorUtil.ErrMessage): this; + size(size: number, message?: errorUtil.ErrMessage): this; + nonempty(message?: errorUtil.ErrMessage): ZodSet; + static create: (valueType: ValueSchema, params?: RawCreateParams) => ZodSet; +} +export interface ZodFunctionDef = ZodTuple, Returns extends ZodTypeAny = ZodTypeAny> extends ZodTypeDef { + args: Args; + returns: Returns; + typeName: ZodFirstPartyTypeKind.ZodFunction; +} +export type OuterTypeOfFunction, Returns extends ZodTypeAny> = Args["_input"] extends Array ? (...args: Args["_input"]) => Returns["_output"] : never; +export type InnerTypeOfFunction, Returns extends ZodTypeAny> = Args["_output"] extends Array ? (...args: Args["_output"]) => Returns["_input"] : never; +export declare class ZodFunction, Returns extends ZodTypeAny> extends ZodType, ZodFunctionDef, InnerTypeOfFunction> { + _parse(input: ParseInput): ParseReturnType; + parameters(): Args; + returnType(): Returns; + args[0]>(...items: Items): ZodFunction, Returns>; + returns>(returnType: NewReturnType): ZodFunction; + implement>(func: F): ReturnType extends Returns["_output"] ? (...args: Args["_input"]) => ReturnType : OuterTypeOfFunction; + strictImplement(func: InnerTypeOfFunction): InnerTypeOfFunction; + validate: >(func: F) => ReturnType extends Returns["_output"] ? (...args: Args["_input"]) => ReturnType : OuterTypeOfFunction; + static create(): ZodFunction, ZodUnknown>; + static create>(args: T): ZodFunction; + static create(args: T, returns: U): ZodFunction; + static create, U extends ZodTypeAny = ZodUnknown>(args: T, returns: U, params?: RawCreateParams): ZodFunction; +} +export interface ZodLazyDef extends ZodTypeDef { + getter: () => T; + typeName: ZodFirstPartyTypeKind.ZodLazy; +} +export declare class ZodLazy extends ZodType, ZodLazyDef, input> { + get schema(): T; + _parse(input: ParseInput): ParseReturnType; + static create: (getter: () => Inner, params?: RawCreateParams) => ZodLazy; +} +export interface ZodLiteralDef extends ZodTypeDef { + value: T; + typeName: ZodFirstPartyTypeKind.ZodLiteral; +} +export declare class ZodLiteral extends ZodType, T> { + _parse(input: ParseInput): ParseReturnType; + get value(): T; + static create: (value: Value, params?: RawCreateParams) => ZodLiteral; +} +export type ArrayKeys = keyof any[]; +export type Indices = Exclude; +export type EnumValues = readonly [T, ...T[]]; +export type Values = { + [k in T[number]]: k; +}; +export interface ZodEnumDef extends ZodTypeDef { + values: T; + typeName: ZodFirstPartyTypeKind.ZodEnum; +} +export type Writeable = { + -readonly [P in keyof T]: T[P]; +}; +export type FilterEnum = Values extends [] ? [] : Values extends [infer Head, ...infer Rest] ? Head extends ToExclude ? FilterEnum : [Head, ...FilterEnum] : never; +export type typecast = A extends T ? A : never; +declare function createZodEnum>(values: T, params?: RawCreateParams): ZodEnum>; +declare function createZodEnum(values: T, params?: RawCreateParams): ZodEnum; +export declare class ZodEnum extends ZodType, T[number]> { + _cache: Set | undefined; + _parse(input: ParseInput): ParseReturnType; + get options(): T; + get enum(): Values; + get Values(): Values; + get Enum(): Values; + extract(values: ToExtract, newDef?: RawCreateParams): ZodEnum>; + exclude(values: ToExclude, newDef?: RawCreateParams): ZodEnum>, [string, ...string[]]>>; + static create: typeof createZodEnum; +} +export interface ZodNativeEnumDef extends ZodTypeDef { + values: T; + typeName: ZodFirstPartyTypeKind.ZodNativeEnum; +} +export type EnumLike = { + [k: string]: string | number; + [nu: number]: string; +}; +export declare class ZodNativeEnum extends ZodType, T[keyof T]> { + _cache: Set | undefined; + _parse(input: ParseInput): ParseReturnType; + get enum(): T; + static create: (values: Elements, params?: RawCreateParams) => ZodNativeEnum; +} +export interface ZodPromiseDef extends ZodTypeDef { + type: T; + typeName: ZodFirstPartyTypeKind.ZodPromise; +} +export declare class ZodPromise extends ZodType, ZodPromiseDef, Promise> { + unwrap(): T; + _parse(input: ParseInput): ParseReturnType; + static create: (schema: Inner, params?: RawCreateParams) => ZodPromise; +} +export type Refinement = (arg: T, ctx: RefinementCtx) => any; +export type SuperRefinement = (arg: T, ctx: RefinementCtx) => void | Promise; +export type RefinementEffect = { + type: "refinement"; + refinement: (arg: T, ctx: RefinementCtx) => any; +}; +export type TransformEffect = { + type: "transform"; + transform: (arg: T, ctx: RefinementCtx) => any; +}; +export type PreprocessEffect = { + type: "preprocess"; + transform: (arg: T, ctx: RefinementCtx) => any; +}; +export type Effect = RefinementEffect | TransformEffect | PreprocessEffect; +export interface ZodEffectsDef extends ZodTypeDef { + schema: T; + typeName: ZodFirstPartyTypeKind.ZodEffects; + effect: Effect; +} +export declare class ZodEffects, Input = input> extends ZodType, Input> { + innerType(): T; + sourceType(): T; + _parse(input: ParseInput): ParseReturnType; + static create: (schema: I, effect: Effect, params?: RawCreateParams) => ZodEffects; + static createWithPreprocess: (preprocess: (arg: unknown, ctx: RefinementCtx) => unknown, schema: I, params?: RawCreateParams) => ZodEffects; +} +export { ZodEffects as ZodTransformer }; +export interface ZodOptionalDef extends ZodTypeDef { + innerType: T; + typeName: ZodFirstPartyTypeKind.ZodOptional; +} +export type ZodOptionalType = ZodOptional; +export declare class ZodOptional extends ZodType, T["_input"] | undefined> { + _parse(input: ParseInput): ParseReturnType; + unwrap(): T; + static create: (type: Inner, params?: RawCreateParams) => ZodOptional; +} +export interface ZodNullableDef extends ZodTypeDef { + innerType: T; + typeName: ZodFirstPartyTypeKind.ZodNullable; +} +export type ZodNullableType = ZodNullable; +export declare class ZodNullable extends ZodType, T["_input"] | null> { + _parse(input: ParseInput): ParseReturnType; + unwrap(): T; + static create: (type: Inner, params?: RawCreateParams) => ZodNullable; +} +export interface ZodDefaultDef extends ZodTypeDef { + innerType: T; + defaultValue: () => util.noUndefined; + typeName: ZodFirstPartyTypeKind.ZodDefault; +} +export declare class ZodDefault extends ZodType, ZodDefaultDef, T["_input"] | undefined> { + _parse(input: ParseInput): ParseReturnType; + removeDefault(): T; + static create: (type: Inner, params: RawCreateParams & { + default: Inner["_input"] | (() => util.noUndefined); + }) => ZodDefault; +} +export interface ZodCatchDef extends ZodTypeDef { + innerType: T; + catchValue: (ctx: { + error: ZodError; + input: unknown; + }) => T["_input"]; + typeName: ZodFirstPartyTypeKind.ZodCatch; +} +export declare class ZodCatch extends ZodType, unknown> { + _parse(input: ParseInput): ParseReturnType; + removeCatch(): T; + static create: (type: Inner, params: RawCreateParams & { + catch: Inner["_output"] | (() => Inner["_output"]); + }) => ZodCatch; +} +export interface ZodNaNDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodNaN; +} +export declare class ZodNaN extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodNaN; +} +export interface ZodBrandedDef extends ZodTypeDef { + type: T; + typeName: ZodFirstPartyTypeKind.ZodBranded; +} +export declare const BRAND: unique symbol; +export type BRAND = { + [BRAND]: { + [k in T]: true; + }; +}; +export declare class ZodBranded extends ZodType, ZodBrandedDef, T["_input"]> { + _parse(input: ParseInput): ParseReturnType; + unwrap(): T; +} +export interface ZodPipelineDef extends ZodTypeDef { + in: A; + out: B; + typeName: ZodFirstPartyTypeKind.ZodPipeline; +} +export declare class ZodPipeline extends ZodType, A["_input"]> { + _parse(input: ParseInput): ParseReturnType; + static create(a: ASchema, b: BSchema): ZodPipeline; +} +type BuiltIn = (((...args: any[]) => any) | (new (...args: any[]) => any)) | { + readonly [Symbol.toStringTag]: string; +} | Date | Error | Generator | Promise | RegExp; +type MakeReadonly = T extends Map ? ReadonlyMap : T extends Set ? ReadonlySet : T extends [infer Head, ...infer Tail] ? readonly [Head, ...Tail] : T extends Array ? ReadonlyArray : T extends BuiltIn ? T : Readonly; +export interface ZodReadonlyDef extends ZodTypeDef { + innerType: T; + typeName: ZodFirstPartyTypeKind.ZodReadonly; +} +export declare class ZodReadonly extends ZodType, ZodReadonlyDef, MakeReadonly> { + _parse(input: ParseInput): ParseReturnType; + static create: (type: Inner, params?: RawCreateParams) => ZodReadonly; + unwrap(): T; +} +type CustomParams = CustomErrorParams & { + fatal?: boolean; +}; +export declare function custom(check?: (data: any) => any, _params?: string | CustomParams | ((input: any) => CustomParams), +/** + * @deprecated + * + * Pass `fatal` into the params object instead: + * + * ```ts + * z.string().custom((val) => val.length > 5, { fatal: false }) + * ``` + * + */ +fatal?: boolean): ZodType; +export { ZodType as Schema, ZodType as ZodSchema }; +export declare const late: { + object: (shape: () => Shape, params?: RawCreateParams) => ZodObject; +}; +export declare enum ZodFirstPartyTypeKind { + ZodString = "ZodString", + ZodNumber = "ZodNumber", + ZodNaN = "ZodNaN", + ZodBigInt = "ZodBigInt", + ZodBoolean = "ZodBoolean", + ZodDate = "ZodDate", + ZodSymbol = "ZodSymbol", + ZodUndefined = "ZodUndefined", + ZodNull = "ZodNull", + ZodAny = "ZodAny", + ZodUnknown = "ZodUnknown", + ZodNever = "ZodNever", + ZodVoid = "ZodVoid", + ZodArray = "ZodArray", + ZodObject = "ZodObject", + ZodUnion = "ZodUnion", + ZodDiscriminatedUnion = "ZodDiscriminatedUnion", + ZodIntersection = "ZodIntersection", + ZodTuple = "ZodTuple", + ZodRecord = "ZodRecord", + ZodMap = "ZodMap", + ZodSet = "ZodSet", + ZodFunction = "ZodFunction", + ZodLazy = "ZodLazy", + ZodLiteral = "ZodLiteral", + ZodEnum = "ZodEnum", + ZodEffects = "ZodEffects", + ZodNativeEnum = "ZodNativeEnum", + ZodOptional = "ZodOptional", + ZodNullable = "ZodNullable", + ZodDefault = "ZodDefault", + ZodCatch = "ZodCatch", + ZodPromise = "ZodPromise", + ZodBranded = "ZodBranded", + ZodPipeline = "ZodPipeline", + ZodReadonly = "ZodReadonly" +} +export type ZodFirstPartySchemaTypes = ZodString | ZodNumber | ZodNaN | ZodBigInt | ZodBoolean | ZodDate | ZodUndefined | ZodNull | ZodAny | ZodUnknown | ZodNever | ZodVoid | ZodArray | ZodObject | ZodUnion | ZodDiscriminatedUnion | ZodIntersection | ZodTuple | ZodRecord | ZodMap | ZodSet | ZodFunction | ZodLazy | ZodLiteral | ZodEnum | ZodEffects | ZodNativeEnum | ZodOptional | ZodNullable | ZodDefault | ZodCatch | ZodPromise | ZodBranded | ZodPipeline | ZodReadonly | ZodSymbol; +declare abstract class Class { + constructor(..._: any[]); +} +declare const instanceOfType: (cls: T, params?: CustomParams) => ZodType, ZodTypeDef, InstanceType>; +declare const stringType: (params?: RawCreateParams & { + coerce?: true; +}) => ZodString; +declare const numberType: (params?: RawCreateParams & { + coerce?: boolean; +}) => ZodNumber; +declare const nanType: (params?: RawCreateParams) => ZodNaN; +declare const bigIntType: (params?: RawCreateParams & { + coerce?: boolean; +}) => ZodBigInt; +declare const booleanType: (params?: RawCreateParams & { + coerce?: boolean; +}) => ZodBoolean; +declare const dateType: (params?: RawCreateParams & { + coerce?: boolean; +}) => ZodDate; +declare const symbolType: (params?: RawCreateParams) => ZodSymbol; +declare const undefinedType: (params?: RawCreateParams) => ZodUndefined; +declare const nullType: (params?: RawCreateParams) => ZodNull; +declare const anyType: (params?: RawCreateParams) => ZodAny; +declare const unknownType: (params?: RawCreateParams) => ZodUnknown; +declare const neverType: (params?: RawCreateParams) => ZodNever; +declare const voidType: (params?: RawCreateParams) => ZodVoid; +declare const arrayType: (schema: El, params?: RawCreateParams) => ZodArray; +declare const objectType: (shape: Shape, params?: RawCreateParams) => ZodObject, objectInputType>; +declare const strictObjectType: (shape: Shape, params?: RawCreateParams) => ZodObject; +declare const unionType: >(types: Options, params?: RawCreateParams) => ZodUnion; +declare const discriminatedUnionType: typeof ZodDiscriminatedUnion.create; +declare const intersectionType: (left: TSchema, right: USchema, params?: RawCreateParams) => ZodIntersection; +declare const tupleType: (schemas: Items, params?: RawCreateParams) => ZodTuple; +declare const recordType: typeof ZodRecord.create; +declare const mapType: (keyType: KeySchema, valueType: ValueSchema, params?: RawCreateParams) => ZodMap; +declare const setType: (valueType: ValueSchema, params?: RawCreateParams) => ZodSet; +declare const functionType: typeof ZodFunction.create; +declare const lazyType: (getter: () => Inner, params?: RawCreateParams) => ZodLazy; +declare const literalType: (value: Value, params?: RawCreateParams) => ZodLiteral; +declare const enumType: typeof createZodEnum; +declare const nativeEnumType: (values: Elements, params?: RawCreateParams) => ZodNativeEnum; +declare const promiseType: (schema: Inner, params?: RawCreateParams) => ZodPromise; +declare const effectsType: (schema: I, effect: Effect, params?: RawCreateParams) => ZodEffects; +declare const optionalType: (type: Inner, params?: RawCreateParams) => ZodOptional; +declare const nullableType: (type: Inner, params?: RawCreateParams) => ZodNullable; +declare const preprocessType: (preprocess: (arg: unknown, ctx: RefinementCtx) => unknown, schema: I, params?: RawCreateParams) => ZodEffects; +declare const pipelineType: typeof ZodPipeline.create; +declare const ostring: () => ZodOptional; +declare const onumber: () => ZodOptional; +declare const oboolean: () => ZodOptional; +export declare const coerce: { + string: (typeof ZodString)["create"]; + number: (typeof ZodNumber)["create"]; + boolean: (typeof ZodBoolean)["create"]; + bigint: (typeof ZodBigInt)["create"]; + date: (typeof ZodDate)["create"]; +}; +export { anyType as any, arrayType as array, bigIntType as bigint, booleanType as boolean, dateType as date, discriminatedUnionType as discriminatedUnion, effectsType as effect, enumType as enum, functionType as function, instanceOfType as instanceof, intersectionType as intersection, lazyType as lazy, literalType as literal, mapType as map, nanType as nan, nativeEnumType as nativeEnum, neverType as never, nullType as null, nullableType as nullable, numberType as number, objectType as object, oboolean, onumber, optionalType as optional, ostring, pipelineType as pipeline, preprocessType as preprocess, promiseType as promise, recordType as record, setType as set, strictObjectType as strictObject, stringType as string, symbolType as symbol, effectsType as transformer, tupleType as tuple, undefinedType as undefined, unionType as union, unknownType as unknown, voidType as void, }; +export declare const NEVER: never; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/package.json b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/package.json new file mode 100644 index 000000000..e02b87614 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/package.json @@ -0,0 +1,120 @@ +{ + "name": "zod", + "version": "3.25.67", + "type": "module", + "author": "Colin McDonnell ", + "description": "TypeScript-first schema declaration and validation library with static type inference", + "files": ["dist", "v3", "v4", "v4-mini"], + "funding": "https://github.com/sponsors/colinhacks", + "homepage": "https://zod.dev", + "keywords": ["typescript", "schema", "validation", "type", "inference"], + "license": "MIT", + "sideEffects": false, + "main": "./dist/cjs/index.js", + "types": "./dist/types/index.d.ts", + "module": "./dist/esm/index.js", + "exports": { + ".": { + "import": { + "@zod/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "@zod/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "default": "./dist/cjs/index.js" + } + }, + "./v3": { + "import": { + "@zod/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "@zod/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "default": "./dist/cjs/index.js" + } + }, + "./v4": { + "import": { + "@zod/source": "./src/v4/index.ts", + "types": "./dist/types/v4/index.d.ts", + "default": "./dist/esm/v4/index.js" + }, + "require": { + "@zod/source": "./src/v4/index.ts", + "types": "./dist/types/v4/index.d.ts", + "default": "./dist/cjs/v4/index.js" + } + }, + "./v4-mini": { + "@zod/source": "./src/v4/mini/index.ts", + "types": "./dist/types/v4/mini/index.d.ts", + "import": "./dist/esm/v4/mini/index.js", + "require": "./dist/cjs/v4/mini/index.js" + }, + "./v4/mini": { + "@zod/source": "./src/v4/mini/index.ts", + "types": "./dist/types/v4/mini/index.d.ts", + "import": "./dist/esm/v4/mini/index.js", + "require": "./dist/cjs/v4/mini/index.js" + }, + "./v4/core": { + "@zod/source": "./src/v4/core/index.ts", + "types": "./dist/types/v4/core/index.d.ts", + "import": "./dist/esm/v4/core/index.js", + "require": "./dist/cjs/v4/core/index.js" + }, + "./v4/locales": { + "@zod/source": "./src/v4/locales/index.ts", + "types": "./dist/types/v4/locales/index.d.ts", + "import": "./dist/esm/v4/locales/index.js", + "require": "./dist/cjs/v4/locales/index.js" + }, + "./v4/locales/*": { + "@zod/source": "./src/v4/locales/*", + "types": "./dist/types/v4/locales/*", + "import": "./dist/esm/v4/locales/*", + "require": "./dist/cjs/v4/locales/*" + }, + "./package.json": "./package.json" + }, + "tshy": { + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./v3": "./src/v3/index.ts", + "./v4": "./src/v4/index.ts", + "./v4-mini": "./src/v4/mini/index.ts", + "./v4/core": "./src/v4/core/index.ts", + "./v4/locales": "./src/v4/locales/index.ts", + "./v4/locales/*": "./src/v4/locales/*" + }, + "sourceDialects": ["@zod/source"], + "exclude": ["./src/**/*.test.ts"] + }, + "repository": { + "type": "git", + "url": "git+https://github.com/colinhacks/zod.git" + }, + "bugs": { + "url": "https://github.com/colinhacks/zod/issues" + }, + "support": { + "backing": { + "npm-funding": true + } + }, + "scripts": { + "clean": "rm -rf dist", + "build": "tsx ./build.mts", + "postbuild": "pnpm biome check --write .", + "test:watch": "pnpm vitest", + "test": "pnpm vitest run", + "bump:beta": "pnpm version \"v$(pnpm pkg get version | jq -r)-beta.$(date +%Y%m%dT%H%M%S)\"", + "pub:beta": "pnpm bump:beta && pnpm publish --tag next --publish-branch v4 --no-git-checks --dry-run" + } +} diff --git a/tests/fixtures/npm/cases/zod3-adoption/package.json b/tests/fixtures/npm/cases/zod3-adoption/package.json new file mode 100644 index 000000000..ff368ad87 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/package.json @@ -0,0 +1 @@ +{ "name": "zod3-adoption-case", "private": true, "type": "module" } diff --git a/tests/fixtures/npm/cases/zod3-boundary/lying.ts b/tests/fixtures/npm/cases/zod3-boundary/lying.ts new file mode 100644 index 000000000..107dda7d8 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/lying.ts @@ -0,0 +1,19 @@ +// A declaration that lies about a value: the boundary check throws a +// TypeError naming the path (native only; Node does not check). Run by +// tests/harness/decl-boundary.test.ts. +import { z } from "zod"; + +const Liar = z.custom<{ name: string; n: number }>(() => true); +try { + const v = Liar.parse({ name: "x", n: "not a number" }); + console.log("liar", v.n + 1); +} catch (e) { + console.log("liar threw", e instanceof TypeError, (e as Error).message); +} +const Frozen = z.custom<{ tags: string[] }>(); +try { + const f = Frozen.parse(Object.freeze(JSON.parse('{"tags":["a"]}'))); + console.log("frozen", f.tags.length); +} catch (e) { + console.log("frozen threw", (e as Error).message); +} diff --git a/tests/fixtures/npm/cases/zod3-boundary/main.ts b/tests/fixtures/npm/cases/zod3-boundary/main.ts new file mode 100644 index 000000000..8e8cedee7 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/main.ts @@ -0,0 +1,83 @@ +// Values that cross from zod 3.25 (compiled from its JavaScript) into code +// typed by zod's declarations: identity, aliasing and mutation after parse, +// values zod hands back unchanged, callbacks zod calls, typed array work, +// and failing checks. +import { z } from "zod"; + +const show = (label: string, value: unknown): void => { + console.log(label + " " + JSON.stringify(value)); +}; + +// Typed records: field reads, writes and array work on parsed data. +const Item = z.object({ + sku: z.string(), + price: z.number().nonnegative(), + qty: z.number().int(), + tags: z.array(z.string()).default([]), +}); +type Item = z.infer; +const items: Item[] = z.array(Item).parse([ + { sku: "a", price: 2.5, qty: 4, tags: ["x"] }, + { sku: "b", price: 10, qty: 1 }, + { sku: "c", price: 0.5, qty: 10, tags: ["x", "y"] }, +]); +const total = items.reduce((sum, it) => sum + it.price * it.qty, 0); +console.log("total", total); +const byTag = new Map(); +for (const it of items) for (const t of it.tags) byTag.set(t, (byTag.get(t) ?? 0) + it.qty); +console.log("byTag", [...byTag].map(([k, v]) => `${k}=${v}`).join(",")); +const sorted = [...items].sort((a, b) => b.price - a.price).map((it) => it.sku); +console.log("sorted", sorted.join(",")); + +// Mutation after parse is visible through every reference. +const first = items[0]!; +first.qty += 1; +first.tags.push("z"); +console.log("mutated", items[0]!.qty, items[0]!.tags.join("|"), first === items[0]); +show("item", items[0]); + +// A value zod hands back unchanged keeps its identity and its mutations. +const Point = z.custom<{ x: number; y: number }>(); +const original = { x: 1, y: 2 }; +const same = Point.parse(original); +same.x = 10; +console.log("custom", original.x, same === original); +original.y = 20; +console.log("custom back", same.y); + +// A shared object returned by a transform: every parse sees the same one. +const shared = { hits: 0 }; +const Counter = z.string().transform(() => shared); +const c1 = Counter.parse("a"); +const c2 = Counter.parse("b"); +c1.hits += 1; +console.log("shared", c2.hits, shared.hits, c1 === c2); + +// A refine callback reads typed fields of the value zod checks. +const Range = z + .object({ lo: z.number(), hi: z.number() }) + .refine((r) => r.lo <= r.hi, { message: "lo must not exceed hi" }); +console.log("range ok", JSON.stringify(Range.parse({ lo: 1, hi: 3 }))); +const bad = Range.safeParse({ lo: 5, hi: 3 }); +console.log("range bad", bad.success ? "?" : bad.error.issues[0]?.message); + +// safeParse data, a discriminated union, and passthrough keys. +const Event = z.discriminatedUnion("type", [ + z.object({ type: z.literal("click"), x: z.number(), y: z.number() }), + z.object({ type: z.literal("key"), key: z.string() }), +]); +type Event = z.infer; +const describe = (e: Event): string => (e.type === "click" ? `click@${e.x},${e.y}` : `key:${e.key}`); +const r = Event.safeParse({ type: "key", key: "Enter" }); +if (r.success) console.log("event", describe(r.data)); +const Loose = z.object({ id: z.number() }).passthrough(); +const loose = Loose.parse({ extra: true, id: 3, more: "m" }); +console.log("loose", loose.id, Object.keys(loose).join(","), JSON.stringify(loose)); + +// async parse +async function main(): Promise { + const it = await Item.parseAsync({ sku: "d", price: 1, qty: 2 }); + console.log("async", it.sku, it.price * it.qty, it.tags.length); +} + +void main(); diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/LICENSE b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/LICENSE new file mode 100644 index 000000000..c06579627 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Colin McDonnell + +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/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/index.js b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/index.js new file mode 100644 index 000000000..8352ab713 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/index.js @@ -0,0 +1,3 @@ +import z3 from "./v3/index.js"; +export * from "./v3/index.js"; +export default z3; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/package.json b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/package.json new file mode 100644 index 000000000..aead43de3 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} \ No newline at end of file diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/ZodError.js b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/ZodError.js new file mode 100644 index 000000000..c72ce5288 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/ZodError.js @@ -0,0 +1,132 @@ +import { util } from "./helpers/util.js"; +export const ZodIssueCode = util.arrayToEnum([ + "invalid_type", + "invalid_literal", + "custom", + "invalid_union", + "invalid_union_discriminator", + "invalid_enum_value", + "unrecognized_keys", + "invalid_arguments", + "invalid_return_type", + "invalid_date", + "invalid_string", + "too_small", + "too_big", + "invalid_intersection_types", + "not_multiple_of", + "not_finite", +]); +export const quotelessJson = (obj) => { + const json = JSON.stringify(obj, null, 2); + return json.replace(/"([^"]+)":/g, "$1:"); +}; +export class ZodError extends Error { + get errors() { + return this.issues; + } + constructor(issues) { + super(); + this.issues = []; + this.addIssue = (sub) => { + this.issues = [...this.issues, sub]; + }; + this.addIssues = (subs = []) => { + this.issues = [...this.issues, ...subs]; + }; + const actualProto = new.target.prototype; + if (Object.setPrototypeOf) { + // eslint-disable-next-line ban/ban + Object.setPrototypeOf(this, actualProto); + } + else { + this.__proto__ = actualProto; + } + this.name = "ZodError"; + this.issues = issues; + } + format(_mapper) { + const mapper = _mapper || + function (issue) { + return issue.message; + }; + const fieldErrors = { _errors: [] }; + const processError = (error) => { + for (const issue of error.issues) { + if (issue.code === "invalid_union") { + issue.unionErrors.map(processError); + } + else if (issue.code === "invalid_return_type") { + processError(issue.returnTypeError); + } + else if (issue.code === "invalid_arguments") { + processError(issue.argumentsError); + } + else if (issue.path.length === 0) { + fieldErrors._errors.push(mapper(issue)); + } + else { + let curr = fieldErrors; + let i = 0; + while (i < issue.path.length) { + const el = issue.path[i]; + const terminal = i === issue.path.length - 1; + if (!terminal) { + curr[el] = curr[el] || { _errors: [] }; + // if (typeof el === "string") { + // curr[el] = curr[el] || { _errors: [] }; + // } else if (typeof el === "number") { + // const errorArray: any = []; + // errorArray._errors = []; + // curr[el] = curr[el] || errorArray; + // } + } + else { + curr[el] = curr[el] || { _errors: [] }; + curr[el]._errors.push(mapper(issue)); + } + curr = curr[el]; + i++; + } + } + } + }; + processError(this); + return fieldErrors; + } + static assert(value) { + if (!(value instanceof ZodError)) { + throw new Error(`Not a ZodError: ${value}`); + } + } + toString() { + return this.message; + } + get message() { + return JSON.stringify(this.issues, util.jsonStringifyReplacer, 2); + } + get isEmpty() { + return this.issues.length === 0; + } + flatten(mapper = (issue) => issue.message) { + const fieldErrors = {}; + const formErrors = []; + for (const sub of this.issues) { + if (sub.path.length > 0) { + fieldErrors[sub.path[0]] = fieldErrors[sub.path[0]] || []; + fieldErrors[sub.path[0]].push(mapper(sub)); + } + else { + formErrors.push(mapper(sub)); + } + } + return { formErrors, fieldErrors }; + } + get formErrors() { + return this.flatten(); + } +} +ZodError.create = (issues) => { + const error = new ZodError(issues); + return error; +}; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/errors.js b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/errors.js new file mode 100644 index 000000000..26fdf628a --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/errors.js @@ -0,0 +1,9 @@ +import defaultErrorMap from "./locales/en.js"; +let overrideErrorMap = defaultErrorMap; +export { defaultErrorMap }; +export function setErrorMap(map) { + overrideErrorMap = map; +} +export function getErrorMap() { + return overrideErrorMap; +} diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/external.js b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/external.js new file mode 100644 index 000000000..f0a4be4c7 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/external.js @@ -0,0 +1,6 @@ +export * from "./errors.js"; +export * from "./helpers/parseUtil.js"; +export * from "./helpers/typeAliases.js"; +export * from "./helpers/util.js"; +export * from "./types.js"; +export * from "./ZodError.js"; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/enumUtil.js b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/enumUtil.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/enumUtil.js @@ -0,0 +1 @@ +export {}; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/errorUtil.js b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/errorUtil.js new file mode 100644 index 000000000..e1640d1a4 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/errorUtil.js @@ -0,0 +1,6 @@ +export var errorUtil; +(function (errorUtil) { + errorUtil.errToObj = (message) => typeof message === "string" ? { message } : message || {}; + // biome-ignore lint: + errorUtil.toString = (message) => typeof message === "string" ? message : message?.message; +})(errorUtil || (errorUtil = {})); diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/parseUtil.js b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/parseUtil.js new file mode 100644 index 000000000..91b2afb81 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/parseUtil.js @@ -0,0 +1,109 @@ +import { getErrorMap } from "../errors.js"; +import defaultErrorMap from "../locales/en.js"; +export const makeIssue = (params) => { + const { data, path, errorMaps, issueData } = params; + const fullPath = [...path, ...(issueData.path || [])]; + const fullIssue = { + ...issueData, + path: fullPath, + }; + if (issueData.message !== undefined) { + return { + ...issueData, + path: fullPath, + message: issueData.message, + }; + } + let errorMessage = ""; + const maps = errorMaps + .filter((m) => !!m) + .slice() + .reverse(); + for (const map of maps) { + errorMessage = map(fullIssue, { data, defaultError: errorMessage }).message; + } + return { + ...issueData, + path: fullPath, + message: errorMessage, + }; +}; +export const EMPTY_PATH = []; +export function addIssueToContext(ctx, issueData) { + const overrideMap = getErrorMap(); + const issue = makeIssue({ + issueData: issueData, + data: ctx.data, + path: ctx.path, + errorMaps: [ + ctx.common.contextualErrorMap, // contextual error map is first priority + ctx.schemaErrorMap, // then schema-bound map if available + overrideMap, // then global override map + overrideMap === defaultErrorMap ? undefined : defaultErrorMap, // then global default map + ].filter((x) => !!x), + }); + ctx.common.issues.push(issue); +} +export class ParseStatus { + constructor() { + this.value = "valid"; + } + dirty() { + if (this.value === "valid") + this.value = "dirty"; + } + abort() { + if (this.value !== "aborted") + this.value = "aborted"; + } + static mergeArray(status, results) { + const arrayValue = []; + for (const s of results) { + if (s.status === "aborted") + return INVALID; + if (s.status === "dirty") + status.dirty(); + arrayValue.push(s.value); + } + return { status: status.value, value: arrayValue }; + } + static async mergeObjectAsync(status, pairs) { + const syncPairs = []; + for (const pair of pairs) { + const key = await pair.key; + const value = await pair.value; + syncPairs.push({ + key, + value, + }); + } + return ParseStatus.mergeObjectSync(status, syncPairs); + } + static mergeObjectSync(status, pairs) { + const finalObject = {}; + for (const pair of pairs) { + const { key, value } = pair; + if (key.status === "aborted") + return INVALID; + if (value.status === "aborted") + return INVALID; + if (key.status === "dirty") + status.dirty(); + if (value.status === "dirty") + status.dirty(); + if (key.value !== "__proto__" && (typeof value.value !== "undefined" || pair.alwaysSet)) { + finalObject[key.value] = value.value; + } + } + return { status: status.value, value: finalObject }; + } +} +export const INVALID = Object.freeze({ + status: "aborted", +}); +export const DIRTY = (value) => ({ status: "dirty", value }); +export const OK = (value) => ({ status: "valid", value }); +export const isAborted = (x) => x.status === "aborted"; +export const isDirty = (x) => x.status === "dirty"; +export const isValid = (x) => x.status === "valid"; +export const isAsync = (x) => typeof Promise !== "undefined" && x instanceof Promise; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/partialUtil.js b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/partialUtil.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/partialUtil.js @@ -0,0 +1 @@ +export {}; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/typeAliases.js b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/typeAliases.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/typeAliases.js @@ -0,0 +1 @@ +export {}; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/util.js b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/util.js new file mode 100644 index 000000000..b9641f29b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/util.js @@ -0,0 +1,133 @@ +export var util; +(function (util) { + util.assertEqual = (_) => { }; + function assertIs(_arg) { } + util.assertIs = assertIs; + function assertNever(_x) { + throw new Error(); + } + util.assertNever = assertNever; + util.arrayToEnum = (items) => { + const obj = {}; + for (const item of items) { + obj[item] = item; + } + return obj; + }; + util.getValidEnumValues = (obj) => { + const validKeys = util.objectKeys(obj).filter((k) => typeof obj[obj[k]] !== "number"); + const filtered = {}; + for (const k of validKeys) { + filtered[k] = obj[k]; + } + return util.objectValues(filtered); + }; + util.objectValues = (obj) => { + return util.objectKeys(obj).map(function (e) { + return obj[e]; + }); + }; + util.objectKeys = typeof Object.keys === "function" // eslint-disable-line ban/ban + ? (obj) => Object.keys(obj) // eslint-disable-line ban/ban + : (object) => { + const keys = []; + for (const key in object) { + if (Object.prototype.hasOwnProperty.call(object, key)) { + keys.push(key); + } + } + return keys; + }; + util.find = (arr, checker) => { + for (const item of arr) { + if (checker(item)) + return item; + } + return undefined; + }; + util.isInteger = typeof Number.isInteger === "function" + ? (val) => Number.isInteger(val) // eslint-disable-line ban/ban + : (val) => typeof val === "number" && Number.isFinite(val) && Math.floor(val) === val; + function joinValues(array, separator = " | ") { + return array.map((val) => (typeof val === "string" ? `'${val}'` : val)).join(separator); + } + util.joinValues = joinValues; + util.jsonStringifyReplacer = (_, value) => { + if (typeof value === "bigint") { + return value.toString(); + } + return value; + }; +})(util || (util = {})); +export var objectUtil; +(function (objectUtil) { + objectUtil.mergeShapes = (first, second) => { + return { + ...first, + ...second, // second overwrites first + }; + }; +})(objectUtil || (objectUtil = {})); +export const ZodParsedType = util.arrayToEnum([ + "string", + "nan", + "number", + "integer", + "float", + "boolean", + "date", + "bigint", + "symbol", + "function", + "undefined", + "null", + "array", + "object", + "unknown", + "promise", + "void", + "never", + "map", + "set", +]); +export const getParsedType = (data) => { + const t = typeof data; + switch (t) { + case "undefined": + return ZodParsedType.undefined; + case "string": + return ZodParsedType.string; + case "number": + return Number.isNaN(data) ? ZodParsedType.nan : ZodParsedType.number; + case "boolean": + return ZodParsedType.boolean; + case "function": + return ZodParsedType.function; + case "bigint": + return ZodParsedType.bigint; + case "symbol": + return ZodParsedType.symbol; + case "object": + if (Array.isArray(data)) { + return ZodParsedType.array; + } + if (data === null) { + return ZodParsedType.null; + } + if (data.then && typeof data.then === "function" && data.catch && typeof data.catch === "function") { + return ZodParsedType.promise; + } + if (typeof Map !== "undefined" && data instanceof Map) { + return ZodParsedType.map; + } + if (typeof Set !== "undefined" && data instanceof Set) { + return ZodParsedType.set; + } + if (typeof Date !== "undefined" && data instanceof Date) { + return ZodParsedType.date; + } + return ZodParsedType.object; + default: + return ZodParsedType.unknown; + } +}; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/index.js b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/index.js new file mode 100644 index 000000000..a9dab5738 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/index.js @@ -0,0 +1,4 @@ +import * as z from "./external.js"; +export * from "./external.js"; +export { z }; +export default z; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/locales/en.js b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/locales/en.js new file mode 100644 index 000000000..3e97ad51a --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/locales/en.js @@ -0,0 +1,107 @@ +import { ZodIssueCode } from "../ZodError.js"; +import { util, ZodParsedType } from "../helpers/util.js"; +const errorMap = (issue, _ctx) => { + let message; + switch (issue.code) { + case ZodIssueCode.invalid_type: + if (issue.received === ZodParsedType.undefined) { + message = "Required"; + } + else { + message = `Expected ${issue.expected}, received ${issue.received}`; + } + break; + case ZodIssueCode.invalid_literal: + message = `Invalid literal value, expected ${JSON.stringify(issue.expected, util.jsonStringifyReplacer)}`; + break; + case ZodIssueCode.unrecognized_keys: + message = `Unrecognized key(s) in object: ${util.joinValues(issue.keys, ", ")}`; + break; + case ZodIssueCode.invalid_union: + message = `Invalid input`; + break; + case ZodIssueCode.invalid_union_discriminator: + message = `Invalid discriminator value. Expected ${util.joinValues(issue.options)}`; + break; + case ZodIssueCode.invalid_enum_value: + message = `Invalid enum value. Expected ${util.joinValues(issue.options)}, received '${issue.received}'`; + break; + case ZodIssueCode.invalid_arguments: + message = `Invalid function arguments`; + break; + case ZodIssueCode.invalid_return_type: + message = `Invalid function return type`; + break; + case ZodIssueCode.invalid_date: + message = `Invalid date`; + break; + case ZodIssueCode.invalid_string: + if (typeof issue.validation === "object") { + if ("includes" in issue.validation) { + message = `Invalid input: must include "${issue.validation.includes}"`; + if (typeof issue.validation.position === "number") { + message = `${message} at one or more positions greater than or equal to ${issue.validation.position}`; + } + } + else if ("startsWith" in issue.validation) { + message = `Invalid input: must start with "${issue.validation.startsWith}"`; + } + else if ("endsWith" in issue.validation) { + message = `Invalid input: must end with "${issue.validation.endsWith}"`; + } + else { + util.assertNever(issue.validation); + } + } + else if (issue.validation !== "regex") { + message = `Invalid ${issue.validation}`; + } + else { + message = "Invalid"; + } + break; + case ZodIssueCode.too_small: + if (issue.type === "array") + message = `Array must contain ${issue.exact ? "exactly" : issue.inclusive ? `at least` : `more than`} ${issue.minimum} element(s)`; + else if (issue.type === "string") + message = `String must contain ${issue.exact ? "exactly" : issue.inclusive ? `at least` : `over`} ${issue.minimum} character(s)`; + else if (issue.type === "number") + message = `Number must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${issue.minimum}`; + else if (issue.type === "date") + message = `Date must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${new Date(Number(issue.minimum))}`; + else + message = "Invalid input"; + break; + case ZodIssueCode.too_big: + if (issue.type === "array") + message = `Array must contain ${issue.exact ? `exactly` : issue.inclusive ? `at most` : `less than`} ${issue.maximum} element(s)`; + else if (issue.type === "string") + message = `String must contain ${issue.exact ? `exactly` : issue.inclusive ? `at most` : `under`} ${issue.maximum} character(s)`; + else if (issue.type === "number") + message = `Number must be ${issue.exact ? `exactly` : issue.inclusive ? `less than or equal to` : `less than`} ${issue.maximum}`; + else if (issue.type === "bigint") + message = `BigInt must be ${issue.exact ? `exactly` : issue.inclusive ? `less than or equal to` : `less than`} ${issue.maximum}`; + else if (issue.type === "date") + message = `Date must be ${issue.exact ? `exactly` : issue.inclusive ? `smaller than or equal to` : `smaller than`} ${new Date(Number(issue.maximum))}`; + else + message = "Invalid input"; + break; + case ZodIssueCode.custom: + message = `Invalid input`; + break; + case ZodIssueCode.invalid_intersection_types: + message = `Intersection results could not be merged`; + break; + case ZodIssueCode.not_multiple_of: + message = `Number must be a multiple of ${issue.multipleOf}`; + break; + case ZodIssueCode.not_finite: + message = "Number must be finite"; + break; + default: + message = _ctx.defaultError; + util.assertNever(issue); + } + return { message }; +}; +export default errorMap; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/standard-schema.js b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/standard-schema.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/standard-schema.js @@ -0,0 +1 @@ +export {}; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/types.js b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/types.js new file mode 100644 index 000000000..de1e6213e --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/types.js @@ -0,0 +1,3691 @@ +import { ZodError, ZodIssueCode, } from "./ZodError.js"; +import { defaultErrorMap, getErrorMap } from "./errors.js"; +import { errorUtil } from "./helpers/errorUtil.js"; +import { DIRTY, INVALID, OK, ParseStatus, addIssueToContext, isAborted, isAsync, isDirty, isValid, makeIssue, } from "./helpers/parseUtil.js"; +import { util, ZodParsedType, getParsedType } from "./helpers/util.js"; +class ParseInputLazyPath { + constructor(parent, value, path, key) { + this._cachedPath = []; + this.parent = parent; + this.data = value; + this._path = path; + this._key = key; + } + get path() { + if (!this._cachedPath.length) { + if (Array.isArray(this._key)) { + this._cachedPath.push(...this._path, ...this._key); + } + else { + this._cachedPath.push(...this._path, this._key); + } + } + return this._cachedPath; + } +} +const handleResult = (ctx, result) => { + if (isValid(result)) { + return { success: true, data: result.value }; + } + else { + if (!ctx.common.issues.length) { + throw new Error("Validation failed but no issues detected."); + } + return { + success: false, + get error() { + if (this._error) + return this._error; + const error = new ZodError(ctx.common.issues); + this._error = error; + return this._error; + }, + }; + } +}; +function processCreateParams(params) { + if (!params) + return {}; + const { errorMap, invalid_type_error, required_error, description } = params; + if (errorMap && (invalid_type_error || required_error)) { + throw new Error(`Can't use "invalid_type_error" or "required_error" in conjunction with custom error map.`); + } + if (errorMap) + return { errorMap: errorMap, description }; + const customMap = (iss, ctx) => { + const { message } = params; + if (iss.code === "invalid_enum_value") { + return { message: message ?? ctx.defaultError }; + } + if (typeof ctx.data === "undefined") { + return { message: message ?? required_error ?? ctx.defaultError }; + } + if (iss.code !== "invalid_type") + return { message: ctx.defaultError }; + return { message: message ?? invalid_type_error ?? ctx.defaultError }; + }; + return { errorMap: customMap, description }; +} +export class ZodType { + get description() { + return this._def.description; + } + _getType(input) { + return getParsedType(input.data); + } + _getOrReturnCtx(input, ctx) { + return (ctx || { + common: input.parent.common, + data: input.data, + parsedType: getParsedType(input.data), + schemaErrorMap: this._def.errorMap, + path: input.path, + parent: input.parent, + }); + } + _processInputParams(input) { + return { + status: new ParseStatus(), + ctx: { + common: input.parent.common, + data: input.data, + parsedType: getParsedType(input.data), + schemaErrorMap: this._def.errorMap, + path: input.path, + parent: input.parent, + }, + }; + } + _parseSync(input) { + const result = this._parse(input); + if (isAsync(result)) { + throw new Error("Synchronous parse encountered promise."); + } + return result; + } + _parseAsync(input) { + const result = this._parse(input); + return Promise.resolve(result); + } + parse(data, params) { + const result = this.safeParse(data, params); + if (result.success) + return result.data; + throw result.error; + } + safeParse(data, params) { + const ctx = { + common: { + issues: [], + async: params?.async ?? false, + contextualErrorMap: params?.errorMap, + }, + path: params?.path || [], + schemaErrorMap: this._def.errorMap, + parent: null, + data, + parsedType: getParsedType(data), + }; + const result = this._parseSync({ data, path: ctx.path, parent: ctx }); + return handleResult(ctx, result); + } + "~validate"(data) { + const ctx = { + common: { + issues: [], + async: !!this["~standard"].async, + }, + path: [], + schemaErrorMap: this._def.errorMap, + parent: null, + data, + parsedType: getParsedType(data), + }; + if (!this["~standard"].async) { + try { + const result = this._parseSync({ data, path: [], parent: ctx }); + return isValid(result) + ? { + value: result.value, + } + : { + issues: ctx.common.issues, + }; + } + catch (err) { + if (err?.message?.toLowerCase()?.includes("encountered")) { + this["~standard"].async = true; + } + ctx.common = { + issues: [], + async: true, + }; + } + } + return this._parseAsync({ data, path: [], parent: ctx }).then((result) => isValid(result) + ? { + value: result.value, + } + : { + issues: ctx.common.issues, + }); + } + async parseAsync(data, params) { + const result = await this.safeParseAsync(data, params); + if (result.success) + return result.data; + throw result.error; + } + async safeParseAsync(data, params) { + const ctx = { + common: { + issues: [], + contextualErrorMap: params?.errorMap, + async: true, + }, + path: params?.path || [], + schemaErrorMap: this._def.errorMap, + parent: null, + data, + parsedType: getParsedType(data), + }; + const maybeAsyncResult = this._parse({ data, path: ctx.path, parent: ctx }); + const result = await (isAsync(maybeAsyncResult) ? maybeAsyncResult : Promise.resolve(maybeAsyncResult)); + return handleResult(ctx, result); + } + refine(check, message) { + const getIssueProperties = (val) => { + if (typeof message === "string" || typeof message === "undefined") { + return { message }; + } + else if (typeof message === "function") { + return message(val); + } + else { + return message; + } + }; + return this._refinement((val, ctx) => { + const result = check(val); + const setError = () => ctx.addIssue({ + code: ZodIssueCode.custom, + ...getIssueProperties(val), + }); + if (typeof Promise !== "undefined" && result instanceof Promise) { + return result.then((data) => { + if (!data) { + setError(); + return false; + } + else { + return true; + } + }); + } + if (!result) { + setError(); + return false; + } + else { + return true; + } + }); + } + refinement(check, refinementData) { + return this._refinement((val, ctx) => { + if (!check(val)) { + ctx.addIssue(typeof refinementData === "function" ? refinementData(val, ctx) : refinementData); + return false; + } + else { + return true; + } + }); + } + _refinement(refinement) { + return new ZodEffects({ + schema: this, + typeName: ZodFirstPartyTypeKind.ZodEffects, + effect: { type: "refinement", refinement }, + }); + } + superRefine(refinement) { + return this._refinement(refinement); + } + constructor(def) { + /** Alias of safeParseAsync */ + this.spa = this.safeParseAsync; + this._def = def; + this.parse = this.parse.bind(this); + this.safeParse = this.safeParse.bind(this); + this.parseAsync = this.parseAsync.bind(this); + this.safeParseAsync = this.safeParseAsync.bind(this); + this.spa = this.spa.bind(this); + this.refine = this.refine.bind(this); + this.refinement = this.refinement.bind(this); + this.superRefine = this.superRefine.bind(this); + this.optional = this.optional.bind(this); + this.nullable = this.nullable.bind(this); + this.nullish = this.nullish.bind(this); + this.array = this.array.bind(this); + this.promise = this.promise.bind(this); + this.or = this.or.bind(this); + this.and = this.and.bind(this); + this.transform = this.transform.bind(this); + this.brand = this.brand.bind(this); + this.default = this.default.bind(this); + this.catch = this.catch.bind(this); + this.describe = this.describe.bind(this); + this.pipe = this.pipe.bind(this); + this.readonly = this.readonly.bind(this); + this.isNullable = this.isNullable.bind(this); + this.isOptional = this.isOptional.bind(this); + this["~standard"] = { + version: 1, + vendor: "zod", + validate: (data) => this["~validate"](data), + }; + } + optional() { + return ZodOptional.create(this, this._def); + } + nullable() { + return ZodNullable.create(this, this._def); + } + nullish() { + return this.nullable().optional(); + } + array() { + return ZodArray.create(this); + } + promise() { + return ZodPromise.create(this, this._def); + } + or(option) { + return ZodUnion.create([this, option], this._def); + } + and(incoming) { + return ZodIntersection.create(this, incoming, this._def); + } + transform(transform) { + return new ZodEffects({ + ...processCreateParams(this._def), + schema: this, + typeName: ZodFirstPartyTypeKind.ZodEffects, + effect: { type: "transform", transform }, + }); + } + default(def) { + const defaultValueFunc = typeof def === "function" ? def : () => def; + return new ZodDefault({ + ...processCreateParams(this._def), + innerType: this, + defaultValue: defaultValueFunc, + typeName: ZodFirstPartyTypeKind.ZodDefault, + }); + } + brand() { + return new ZodBranded({ + typeName: ZodFirstPartyTypeKind.ZodBranded, + type: this, + ...processCreateParams(this._def), + }); + } + catch(def) { + const catchValueFunc = typeof def === "function" ? def : () => def; + return new ZodCatch({ + ...processCreateParams(this._def), + innerType: this, + catchValue: catchValueFunc, + typeName: ZodFirstPartyTypeKind.ZodCatch, + }); + } + describe(description) { + const This = this.constructor; + return new This({ + ...this._def, + description, + }); + } + pipe(target) { + return ZodPipeline.create(this, target); + } + readonly() { + return ZodReadonly.create(this); + } + isOptional() { + return this.safeParse(undefined).success; + } + isNullable() { + return this.safeParse(null).success; + } +} +const cuidRegex = /^c[^\s-]{8,}$/i; +const cuid2Regex = /^[0-9a-z]+$/; +const ulidRegex = /^[0-9A-HJKMNP-TV-Z]{26}$/i; +// const uuidRegex = +// /^([a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[a-f0-9]{4}-[a-f0-9]{12}|00000000-0000-0000-0000-000000000000)$/i; +const uuidRegex = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/i; +const nanoidRegex = /^[a-z0-9_-]{21}$/i; +const jwtRegex = /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$/; +const durationRegex = /^[-+]?P(?!$)(?:(?:[-+]?\d+Y)|(?:[-+]?\d+[.,]\d+Y$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:(?:[-+]?\d+W)|(?:[-+]?\d+[.,]\d+W$))?(?:(?:[-+]?\d+D)|(?:[-+]?\d+[.,]\d+D$))?(?:T(?=[\d+-])(?:(?:[-+]?\d+H)|(?:[-+]?\d+[.,]\d+H$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:[-+]?\d+(?:[.,]\d+)?S)?)??$/; +// from https://stackoverflow.com/a/46181/1550155 +// old version: too slow, didn't support unicode +// const emailRegex = /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i; +//old email regex +// const emailRegex = /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@((?!-)([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{1,})[^-<>()[\].,;:\s@"]$/i; +// eslint-disable-next-line +// const emailRegex = +// /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\])|(\[IPv6:(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))\])|([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])*(\.[A-Za-z]{2,})+))$/; +// const emailRegex = +// /^[a-zA-Z0-9\.\!\#\$\%\&\'\*\+\/\=\?\^\_\`\{\|\}\~\-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; +// const emailRegex = +// /^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$/i; +const emailRegex = /^(?!\.)(?!.*\.\.)([A-Z0-9_'+\-\.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i; +// const emailRegex = +// /^[a-z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-z0-9-]+(?:\.[a-z0-9\-]+)*$/i; +// from https://thekevinscott.com/emojis-in-javascript/#writing-a-regular-expression +const _emojiRegex = `^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$`; +let emojiRegex; +// faster, simpler, safer +const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/; +const ipv4CidrRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/(3[0-2]|[12]?[0-9])$/; +// const ipv6Regex = +// /^(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))$/; +const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/; +const ipv6CidrRegex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/; +// https://stackoverflow.com/questions/7860392/determine-if-string-is-in-base64-using-javascript +const base64Regex = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/; +// https://base64.guru/standards/base64url +const base64urlRegex = /^([0-9a-zA-Z-_]{4})*(([0-9a-zA-Z-_]{2}(==)?)|([0-9a-zA-Z-_]{3}(=)?))?$/; +// simple +// const dateRegexSource = `\\d{4}-\\d{2}-\\d{2}`; +// no leap year validation +// const dateRegexSource = `\\d{4}-((0[13578]|10|12)-31|(0[13-9]|1[0-2])-30|(0[1-9]|1[0-2])-(0[1-9]|1\\d|2\\d))`; +// with leap year validation +const dateRegexSource = `((\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\\d|3[01])|(0[469]|11)-(0[1-9]|[12]\\d|30)|(02)-(0[1-9]|1\\d|2[0-8])))`; +const dateRegex = new RegExp(`^${dateRegexSource}$`); +function timeRegexSource(args) { + let secondsRegexSource = `[0-5]\\d`; + if (args.precision) { + secondsRegexSource = `${secondsRegexSource}\\.\\d{${args.precision}}`; + } + else if (args.precision == null) { + secondsRegexSource = `${secondsRegexSource}(\\.\\d+)?`; + } + const secondsQuantifier = args.precision ? "+" : "?"; // require seconds if precision is nonzero + return `([01]\\d|2[0-3]):[0-5]\\d(:${secondsRegexSource})${secondsQuantifier}`; +} +function timeRegex(args) { + return new RegExp(`^${timeRegexSource(args)}$`); +} +// Adapted from https://stackoverflow.com/a/3143231 +export function datetimeRegex(args) { + let regex = `${dateRegexSource}T${timeRegexSource(args)}`; + const opts = []; + opts.push(args.local ? `Z?` : `Z`); + if (args.offset) + opts.push(`([+-]\\d{2}:?\\d{2})`); + regex = `${regex}(${opts.join("|")})`; + return new RegExp(`^${regex}$`); +} +function isValidIP(ip, version) { + if ((version === "v4" || !version) && ipv4Regex.test(ip)) { + return true; + } + if ((version === "v6" || !version) && ipv6Regex.test(ip)) { + return true; + } + return false; +} +function isValidJWT(jwt, alg) { + if (!jwtRegex.test(jwt)) + return false; + try { + const [header] = jwt.split("."); + // Convert base64url to base64 + const base64 = header + .replace(/-/g, "+") + .replace(/_/g, "/") + .padEnd(header.length + ((4 - (header.length % 4)) % 4), "="); + const decoded = JSON.parse(atob(base64)); + if (typeof decoded !== "object" || decoded === null) + return false; + if ("typ" in decoded && decoded?.typ !== "JWT") + return false; + if (!decoded.alg) + return false; + if (alg && decoded.alg !== alg) + return false; + return true; + } + catch { + return false; + } +} +function isValidCidr(ip, version) { + if ((version === "v4" || !version) && ipv4CidrRegex.test(ip)) { + return true; + } + if ((version === "v6" || !version) && ipv6CidrRegex.test(ip)) { + return true; + } + return false; +} +export class ZodString extends ZodType { + _parse(input) { + if (this._def.coerce) { + input.data = String(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.string) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.string, + received: ctx.parsedType, + }); + return INVALID; + } + const status = new ParseStatus(); + let ctx = undefined; + for (const check of this._def.checks) { + if (check.kind === "min") { + if (input.data.length < check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: check.value, + type: "string", + inclusive: true, + exact: false, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "max") { + if (input.data.length > check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: check.value, + type: "string", + inclusive: true, + exact: false, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "length") { + const tooBig = input.data.length > check.value; + const tooSmall = input.data.length < check.value; + if (tooBig || tooSmall) { + ctx = this._getOrReturnCtx(input, ctx); + if (tooBig) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: check.value, + type: "string", + inclusive: true, + exact: true, + message: check.message, + }); + } + else if (tooSmall) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: check.value, + type: "string", + inclusive: true, + exact: true, + message: check.message, + }); + } + status.dirty(); + } + } + else if (check.kind === "email") { + if (!emailRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "email", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "emoji") { + if (!emojiRegex) { + emojiRegex = new RegExp(_emojiRegex, "u"); + } + if (!emojiRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "emoji", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "uuid") { + if (!uuidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "uuid", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "nanoid") { + if (!nanoidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "nanoid", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "cuid") { + if (!cuidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cuid", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "cuid2") { + if (!cuid2Regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cuid2", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "ulid") { + if (!ulidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "ulid", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "url") { + try { + new URL(input.data); + } + catch { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "url", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "regex") { + check.regex.lastIndex = 0; + const testResult = check.regex.test(input.data); + if (!testResult) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "regex", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "trim") { + input.data = input.data.trim(); + } + else if (check.kind === "includes") { + if (!input.data.includes(check.value, check.position)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: { includes: check.value, position: check.position }, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "toLowerCase") { + input.data = input.data.toLowerCase(); + } + else if (check.kind === "toUpperCase") { + input.data = input.data.toUpperCase(); + } + else if (check.kind === "startsWith") { + if (!input.data.startsWith(check.value)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: { startsWith: check.value }, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "endsWith") { + if (!input.data.endsWith(check.value)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: { endsWith: check.value }, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "datetime") { + const regex = datetimeRegex(check); + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: "datetime", + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "date") { + const regex = dateRegex; + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: "date", + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "time") { + const regex = timeRegex(check); + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: "time", + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "duration") { + if (!durationRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "duration", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "ip") { + if (!isValidIP(input.data, check.version)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "ip", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "jwt") { + if (!isValidJWT(input.data, check.alg)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "jwt", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "cidr") { + if (!isValidCidr(input.data, check.version)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cidr", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "base64") { + if (!base64Regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "base64", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "base64url") { + if (!base64urlRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "base64url", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else { + util.assertNever(check); + } + } + return { status: status.value, value: input.data }; + } + _regex(regex, validation, message) { + return this.refinement((data) => regex.test(data), { + validation, + code: ZodIssueCode.invalid_string, + ...errorUtil.errToObj(message), + }); + } + _addCheck(check) { + return new ZodString({ + ...this._def, + checks: [...this._def.checks, check], + }); + } + email(message) { + return this._addCheck({ kind: "email", ...errorUtil.errToObj(message) }); + } + url(message) { + return this._addCheck({ kind: "url", ...errorUtil.errToObj(message) }); + } + emoji(message) { + return this._addCheck({ kind: "emoji", ...errorUtil.errToObj(message) }); + } + uuid(message) { + return this._addCheck({ kind: "uuid", ...errorUtil.errToObj(message) }); + } + nanoid(message) { + return this._addCheck({ kind: "nanoid", ...errorUtil.errToObj(message) }); + } + cuid(message) { + return this._addCheck({ kind: "cuid", ...errorUtil.errToObj(message) }); + } + cuid2(message) { + return this._addCheck({ kind: "cuid2", ...errorUtil.errToObj(message) }); + } + ulid(message) { + return this._addCheck({ kind: "ulid", ...errorUtil.errToObj(message) }); + } + base64(message) { + return this._addCheck({ kind: "base64", ...errorUtil.errToObj(message) }); + } + base64url(message) { + // base64url encoding is a modification of base64 that can safely be used in URLs and filenames + return this._addCheck({ + kind: "base64url", + ...errorUtil.errToObj(message), + }); + } + jwt(options) { + return this._addCheck({ kind: "jwt", ...errorUtil.errToObj(options) }); + } + ip(options) { + return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) }); + } + cidr(options) { + return this._addCheck({ kind: "cidr", ...errorUtil.errToObj(options) }); + } + datetime(options) { + if (typeof options === "string") { + return this._addCheck({ + kind: "datetime", + precision: null, + offset: false, + local: false, + message: options, + }); + } + return this._addCheck({ + kind: "datetime", + precision: typeof options?.precision === "undefined" ? null : options?.precision, + offset: options?.offset ?? false, + local: options?.local ?? false, + ...errorUtil.errToObj(options?.message), + }); + } + date(message) { + return this._addCheck({ kind: "date", message }); + } + time(options) { + if (typeof options === "string") { + return this._addCheck({ + kind: "time", + precision: null, + message: options, + }); + } + return this._addCheck({ + kind: "time", + precision: typeof options?.precision === "undefined" ? null : options?.precision, + ...errorUtil.errToObj(options?.message), + }); + } + duration(message) { + return this._addCheck({ kind: "duration", ...errorUtil.errToObj(message) }); + } + regex(regex, message) { + return this._addCheck({ + kind: "regex", + regex: regex, + ...errorUtil.errToObj(message), + }); + } + includes(value, options) { + return this._addCheck({ + kind: "includes", + value: value, + position: options?.position, + ...errorUtil.errToObj(options?.message), + }); + } + startsWith(value, message) { + return this._addCheck({ + kind: "startsWith", + value: value, + ...errorUtil.errToObj(message), + }); + } + endsWith(value, message) { + return this._addCheck({ + kind: "endsWith", + value: value, + ...errorUtil.errToObj(message), + }); + } + min(minLength, message) { + return this._addCheck({ + kind: "min", + value: minLength, + ...errorUtil.errToObj(message), + }); + } + max(maxLength, message) { + return this._addCheck({ + kind: "max", + value: maxLength, + ...errorUtil.errToObj(message), + }); + } + length(len, message) { + return this._addCheck({ + kind: "length", + value: len, + ...errorUtil.errToObj(message), + }); + } + /** + * Equivalent to `.min(1)` + */ + nonempty(message) { + return this.min(1, errorUtil.errToObj(message)); + } + trim() { + return new ZodString({ + ...this._def, + checks: [...this._def.checks, { kind: "trim" }], + }); + } + toLowerCase() { + return new ZodString({ + ...this._def, + checks: [...this._def.checks, { kind: "toLowerCase" }], + }); + } + toUpperCase() { + return new ZodString({ + ...this._def, + checks: [...this._def.checks, { kind: "toUpperCase" }], + }); + } + get isDatetime() { + return !!this._def.checks.find((ch) => ch.kind === "datetime"); + } + get isDate() { + return !!this._def.checks.find((ch) => ch.kind === "date"); + } + get isTime() { + return !!this._def.checks.find((ch) => ch.kind === "time"); + } + get isDuration() { + return !!this._def.checks.find((ch) => ch.kind === "duration"); + } + get isEmail() { + return !!this._def.checks.find((ch) => ch.kind === "email"); + } + get isURL() { + return !!this._def.checks.find((ch) => ch.kind === "url"); + } + get isEmoji() { + return !!this._def.checks.find((ch) => ch.kind === "emoji"); + } + get isUUID() { + return !!this._def.checks.find((ch) => ch.kind === "uuid"); + } + get isNANOID() { + return !!this._def.checks.find((ch) => ch.kind === "nanoid"); + } + get isCUID() { + return !!this._def.checks.find((ch) => ch.kind === "cuid"); + } + get isCUID2() { + return !!this._def.checks.find((ch) => ch.kind === "cuid2"); + } + get isULID() { + return !!this._def.checks.find((ch) => ch.kind === "ulid"); + } + get isIP() { + return !!this._def.checks.find((ch) => ch.kind === "ip"); + } + get isCIDR() { + return !!this._def.checks.find((ch) => ch.kind === "cidr"); + } + get isBase64() { + return !!this._def.checks.find((ch) => ch.kind === "base64"); + } + get isBase64url() { + // base64url encoding is a modification of base64 that can safely be used in URLs and filenames + return !!this._def.checks.find((ch) => ch.kind === "base64url"); + } + get minLength() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min; + } + get maxLength() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max; + } +} +ZodString.create = (params) => { + return new ZodString({ + checks: [], + typeName: ZodFirstPartyTypeKind.ZodString, + coerce: params?.coerce ?? false, + ...processCreateParams(params), + }); +}; +// https://stackoverflow.com/questions/3966484/why-does-modulus-operator-return-fractional-number-in-javascript/31711034#31711034 +function floatSafeRemainder(val, step) { + const valDecCount = (val.toString().split(".")[1] || "").length; + const stepDecCount = (step.toString().split(".")[1] || "").length; + const decCount = valDecCount > stepDecCount ? valDecCount : stepDecCount; + const valInt = Number.parseInt(val.toFixed(decCount).replace(".", "")); + const stepInt = Number.parseInt(step.toFixed(decCount).replace(".", "")); + return (valInt % stepInt) / 10 ** decCount; +} +export class ZodNumber extends ZodType { + constructor() { + super(...arguments); + this.min = this.gte; + this.max = this.lte; + this.step = this.multipleOf; + } + _parse(input) { + if (this._def.coerce) { + input.data = Number(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.number) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.number, + received: ctx.parsedType, + }); + return INVALID; + } + let ctx = undefined; + const status = new ParseStatus(); + for (const check of this._def.checks) { + if (check.kind === "int") { + if (!util.isInteger(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: "integer", + received: "float", + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "min") { + const tooSmall = check.inclusive ? input.data < check.value : input.data <= check.value; + if (tooSmall) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: check.value, + type: "number", + inclusive: check.inclusive, + exact: false, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "max") { + const tooBig = check.inclusive ? input.data > check.value : input.data >= check.value; + if (tooBig) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: check.value, + type: "number", + inclusive: check.inclusive, + exact: false, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "multipleOf") { + if (floatSafeRemainder(input.data, check.value) !== 0) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.not_multiple_of, + multipleOf: check.value, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "finite") { + if (!Number.isFinite(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.not_finite, + message: check.message, + }); + status.dirty(); + } + } + else { + util.assertNever(check); + } + } + return { status: status.value, value: input.data }; + } + gte(value, message) { + return this.setLimit("min", value, true, errorUtil.toString(message)); + } + gt(value, message) { + return this.setLimit("min", value, false, errorUtil.toString(message)); + } + lte(value, message) { + return this.setLimit("max", value, true, errorUtil.toString(message)); + } + lt(value, message) { + return this.setLimit("max", value, false, errorUtil.toString(message)); + } + setLimit(kind, value, inclusive, message) { + return new ZodNumber({ + ...this._def, + checks: [ + ...this._def.checks, + { + kind, + value, + inclusive, + message: errorUtil.toString(message), + }, + ], + }); + } + _addCheck(check) { + return new ZodNumber({ + ...this._def, + checks: [...this._def.checks, check], + }); + } + int(message) { + return this._addCheck({ + kind: "int", + message: errorUtil.toString(message), + }); + } + positive(message) { + return this._addCheck({ + kind: "min", + value: 0, + inclusive: false, + message: errorUtil.toString(message), + }); + } + negative(message) { + return this._addCheck({ + kind: "max", + value: 0, + inclusive: false, + message: errorUtil.toString(message), + }); + } + nonpositive(message) { + return this._addCheck({ + kind: "max", + value: 0, + inclusive: true, + message: errorUtil.toString(message), + }); + } + nonnegative(message) { + return this._addCheck({ + kind: "min", + value: 0, + inclusive: true, + message: errorUtil.toString(message), + }); + } + multipleOf(value, message) { + return this._addCheck({ + kind: "multipleOf", + value: value, + message: errorUtil.toString(message), + }); + } + finite(message) { + return this._addCheck({ + kind: "finite", + message: errorUtil.toString(message), + }); + } + safe(message) { + return this._addCheck({ + kind: "min", + inclusive: true, + value: Number.MIN_SAFE_INTEGER, + message: errorUtil.toString(message), + })._addCheck({ + kind: "max", + inclusive: true, + value: Number.MAX_SAFE_INTEGER, + message: errorUtil.toString(message), + }); + } + get minValue() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min; + } + get maxValue() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max; + } + get isInt() { + return !!this._def.checks.find((ch) => ch.kind === "int" || (ch.kind === "multipleOf" && util.isInteger(ch.value))); + } + get isFinite() { + let max = null; + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "finite" || ch.kind === "int" || ch.kind === "multipleOf") { + return true; + } + else if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + else if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return Number.isFinite(min) && Number.isFinite(max); + } +} +ZodNumber.create = (params) => { + return new ZodNumber({ + checks: [], + typeName: ZodFirstPartyTypeKind.ZodNumber, + coerce: params?.coerce || false, + ...processCreateParams(params), + }); +}; +export class ZodBigInt extends ZodType { + constructor() { + super(...arguments); + this.min = this.gte; + this.max = this.lte; + } + _parse(input) { + if (this._def.coerce) { + try { + input.data = BigInt(input.data); + } + catch { + return this._getInvalidInput(input); + } + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.bigint) { + return this._getInvalidInput(input); + } + let ctx = undefined; + const status = new ParseStatus(); + for (const check of this._def.checks) { + if (check.kind === "min") { + const tooSmall = check.inclusive ? input.data < check.value : input.data <= check.value; + if (tooSmall) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + type: "bigint", + minimum: check.value, + inclusive: check.inclusive, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "max") { + const tooBig = check.inclusive ? input.data > check.value : input.data >= check.value; + if (tooBig) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + type: "bigint", + maximum: check.value, + inclusive: check.inclusive, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "multipleOf") { + if (input.data % check.value !== BigInt(0)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.not_multiple_of, + multipleOf: check.value, + message: check.message, + }); + status.dirty(); + } + } + else { + util.assertNever(check); + } + } + return { status: status.value, value: input.data }; + } + _getInvalidInput(input) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.bigint, + received: ctx.parsedType, + }); + return INVALID; + } + gte(value, message) { + return this.setLimit("min", value, true, errorUtil.toString(message)); + } + gt(value, message) { + return this.setLimit("min", value, false, errorUtil.toString(message)); + } + lte(value, message) { + return this.setLimit("max", value, true, errorUtil.toString(message)); + } + lt(value, message) { + return this.setLimit("max", value, false, errorUtil.toString(message)); + } + setLimit(kind, value, inclusive, message) { + return new ZodBigInt({ + ...this._def, + checks: [ + ...this._def.checks, + { + kind, + value, + inclusive, + message: errorUtil.toString(message), + }, + ], + }); + } + _addCheck(check) { + return new ZodBigInt({ + ...this._def, + checks: [...this._def.checks, check], + }); + } + positive(message) { + return this._addCheck({ + kind: "min", + value: BigInt(0), + inclusive: false, + message: errorUtil.toString(message), + }); + } + negative(message) { + return this._addCheck({ + kind: "max", + value: BigInt(0), + inclusive: false, + message: errorUtil.toString(message), + }); + } + nonpositive(message) { + return this._addCheck({ + kind: "max", + value: BigInt(0), + inclusive: true, + message: errorUtil.toString(message), + }); + } + nonnegative(message) { + return this._addCheck({ + kind: "min", + value: BigInt(0), + inclusive: true, + message: errorUtil.toString(message), + }); + } + multipleOf(value, message) { + return this._addCheck({ + kind: "multipleOf", + value, + message: errorUtil.toString(message), + }); + } + get minValue() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min; + } + get maxValue() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max; + } +} +ZodBigInt.create = (params) => { + return new ZodBigInt({ + checks: [], + typeName: ZodFirstPartyTypeKind.ZodBigInt, + coerce: params?.coerce ?? false, + ...processCreateParams(params), + }); +}; +export class ZodBoolean extends ZodType { + _parse(input) { + if (this._def.coerce) { + input.data = Boolean(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.boolean) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.boolean, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodBoolean.create = (params) => { + return new ZodBoolean({ + typeName: ZodFirstPartyTypeKind.ZodBoolean, + coerce: params?.coerce || false, + ...processCreateParams(params), + }); +}; +export class ZodDate extends ZodType { + _parse(input) { + if (this._def.coerce) { + input.data = new Date(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.date) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.date, + received: ctx.parsedType, + }); + return INVALID; + } + if (Number.isNaN(input.data.getTime())) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_date, + }); + return INVALID; + } + const status = new ParseStatus(); + let ctx = undefined; + for (const check of this._def.checks) { + if (check.kind === "min") { + if (input.data.getTime() < check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + message: check.message, + inclusive: true, + exact: false, + minimum: check.value, + type: "date", + }); + status.dirty(); + } + } + else if (check.kind === "max") { + if (input.data.getTime() > check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + message: check.message, + inclusive: true, + exact: false, + maximum: check.value, + type: "date", + }); + status.dirty(); + } + } + else { + util.assertNever(check); + } + } + return { + status: status.value, + value: new Date(input.data.getTime()), + }; + } + _addCheck(check) { + return new ZodDate({ + ...this._def, + checks: [...this._def.checks, check], + }); + } + min(minDate, message) { + return this._addCheck({ + kind: "min", + value: minDate.getTime(), + message: errorUtil.toString(message), + }); + } + max(maxDate, message) { + return this._addCheck({ + kind: "max", + value: maxDate.getTime(), + message: errorUtil.toString(message), + }); + } + get minDate() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min != null ? new Date(min) : null; + } + get maxDate() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max != null ? new Date(max) : null; + } +} +ZodDate.create = (params) => { + return new ZodDate({ + checks: [], + coerce: params?.coerce || false, + typeName: ZodFirstPartyTypeKind.ZodDate, + ...processCreateParams(params), + }); +}; +export class ZodSymbol extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.symbol) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.symbol, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodSymbol.create = (params) => { + return new ZodSymbol({ + typeName: ZodFirstPartyTypeKind.ZodSymbol, + ...processCreateParams(params), + }); +}; +export class ZodUndefined extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.undefined) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.undefined, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodUndefined.create = (params) => { + return new ZodUndefined({ + typeName: ZodFirstPartyTypeKind.ZodUndefined, + ...processCreateParams(params), + }); +}; +export class ZodNull extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.null) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.null, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodNull.create = (params) => { + return new ZodNull({ + typeName: ZodFirstPartyTypeKind.ZodNull, + ...processCreateParams(params), + }); +}; +export class ZodAny extends ZodType { + constructor() { + super(...arguments); + // to prevent instances of other classes from extending ZodAny. this causes issues with catchall in ZodObject. + this._any = true; + } + _parse(input) { + return OK(input.data); + } +} +ZodAny.create = (params) => { + return new ZodAny({ + typeName: ZodFirstPartyTypeKind.ZodAny, + ...processCreateParams(params), + }); +}; +export class ZodUnknown extends ZodType { + constructor() { + super(...arguments); + // required + this._unknown = true; + } + _parse(input) { + return OK(input.data); + } +} +ZodUnknown.create = (params) => { + return new ZodUnknown({ + typeName: ZodFirstPartyTypeKind.ZodUnknown, + ...processCreateParams(params), + }); +}; +export class ZodNever extends ZodType { + _parse(input) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.never, + received: ctx.parsedType, + }); + return INVALID; + } +} +ZodNever.create = (params) => { + return new ZodNever({ + typeName: ZodFirstPartyTypeKind.ZodNever, + ...processCreateParams(params), + }); +}; +export class ZodVoid extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.undefined) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.void, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodVoid.create = (params) => { + return new ZodVoid({ + typeName: ZodFirstPartyTypeKind.ZodVoid, + ...processCreateParams(params), + }); +}; +export class ZodArray extends ZodType { + _parse(input) { + const { ctx, status } = this._processInputParams(input); + const def = this._def; + if (ctx.parsedType !== ZodParsedType.array) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.array, + received: ctx.parsedType, + }); + return INVALID; + } + if (def.exactLength !== null) { + const tooBig = ctx.data.length > def.exactLength.value; + const tooSmall = ctx.data.length < def.exactLength.value; + if (tooBig || tooSmall) { + addIssueToContext(ctx, { + code: tooBig ? ZodIssueCode.too_big : ZodIssueCode.too_small, + minimum: (tooSmall ? def.exactLength.value : undefined), + maximum: (tooBig ? def.exactLength.value : undefined), + type: "array", + inclusive: true, + exact: true, + message: def.exactLength.message, + }); + status.dirty(); + } + } + if (def.minLength !== null) { + if (ctx.data.length < def.minLength.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: def.minLength.value, + type: "array", + inclusive: true, + exact: false, + message: def.minLength.message, + }); + status.dirty(); + } + } + if (def.maxLength !== null) { + if (ctx.data.length > def.maxLength.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: def.maxLength.value, + type: "array", + inclusive: true, + exact: false, + message: def.maxLength.message, + }); + status.dirty(); + } + } + if (ctx.common.async) { + return Promise.all([...ctx.data].map((item, i) => { + return def.type._parseAsync(new ParseInputLazyPath(ctx, item, ctx.path, i)); + })).then((result) => { + return ParseStatus.mergeArray(status, result); + }); + } + const result = [...ctx.data].map((item, i) => { + return def.type._parseSync(new ParseInputLazyPath(ctx, item, ctx.path, i)); + }); + return ParseStatus.mergeArray(status, result); + } + get element() { + return this._def.type; + } + min(minLength, message) { + return new ZodArray({ + ...this._def, + minLength: { value: minLength, message: errorUtil.toString(message) }, + }); + } + max(maxLength, message) { + return new ZodArray({ + ...this._def, + maxLength: { value: maxLength, message: errorUtil.toString(message) }, + }); + } + length(len, message) { + return new ZodArray({ + ...this._def, + exactLength: { value: len, message: errorUtil.toString(message) }, + }); + } + nonempty(message) { + return this.min(1, message); + } +} +ZodArray.create = (schema, params) => { + return new ZodArray({ + type: schema, + minLength: null, + maxLength: null, + exactLength: null, + typeName: ZodFirstPartyTypeKind.ZodArray, + ...processCreateParams(params), + }); +}; +function deepPartialify(schema) { + if (schema instanceof ZodObject) { + const newShape = {}; + for (const key in schema.shape) { + const fieldSchema = schema.shape[key]; + newShape[key] = ZodOptional.create(deepPartialify(fieldSchema)); + } + return new ZodObject({ + ...schema._def, + shape: () => newShape, + }); + } + else if (schema instanceof ZodArray) { + return new ZodArray({ + ...schema._def, + type: deepPartialify(schema.element), + }); + } + else if (schema instanceof ZodOptional) { + return ZodOptional.create(deepPartialify(schema.unwrap())); + } + else if (schema instanceof ZodNullable) { + return ZodNullable.create(deepPartialify(schema.unwrap())); + } + else if (schema instanceof ZodTuple) { + return ZodTuple.create(schema.items.map((item) => deepPartialify(item))); + } + else { + return schema; + } +} +export class ZodObject extends ZodType { + constructor() { + super(...arguments); + this._cached = null; + /** + * @deprecated In most cases, this is no longer needed - unknown properties are now silently stripped. + * If you want to pass through unknown properties, use `.passthrough()` instead. + */ + this.nonstrict = this.passthrough; + // extend< + // Augmentation extends ZodRawShape, + // NewOutput extends util.flatten<{ + // [k in keyof Augmentation | keyof Output]: k extends keyof Augmentation + // ? Augmentation[k]["_output"] + // : k extends keyof Output + // ? Output[k] + // : never; + // }>, + // NewInput extends util.flatten<{ + // [k in keyof Augmentation | keyof Input]: k extends keyof Augmentation + // ? Augmentation[k]["_input"] + // : k extends keyof Input + // ? Input[k] + // : never; + // }> + // >( + // augmentation: Augmentation + // ): ZodObject< + // extendShape, + // UnknownKeys, + // Catchall, + // NewOutput, + // NewInput + // > { + // return new ZodObject({ + // ...this._def, + // shape: () => ({ + // ...this._def.shape(), + // ...augmentation, + // }), + // }) as any; + // } + /** + * @deprecated Use `.extend` instead + * */ + this.augment = this.extend; + } + _getCached() { + if (this._cached !== null) + return this._cached; + const shape = this._def.shape(); + const keys = util.objectKeys(shape); + this._cached = { shape, keys }; + return this._cached; + } + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.object) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.object, + received: ctx.parsedType, + }); + return INVALID; + } + const { status, ctx } = this._processInputParams(input); + const { shape, keys: shapeKeys } = this._getCached(); + const extraKeys = []; + if (!(this._def.catchall instanceof ZodNever && this._def.unknownKeys === "strip")) { + for (const key in ctx.data) { + if (!shapeKeys.includes(key)) { + extraKeys.push(key); + } + } + } + const pairs = []; + for (const key of shapeKeys) { + const keyValidator = shape[key]; + const value = ctx.data[key]; + pairs.push({ + key: { status: "valid", value: key }, + value: keyValidator._parse(new ParseInputLazyPath(ctx, value, ctx.path, key)), + alwaysSet: key in ctx.data, + }); + } + if (this._def.catchall instanceof ZodNever) { + const unknownKeys = this._def.unknownKeys; + if (unknownKeys === "passthrough") { + for (const key of extraKeys) { + pairs.push({ + key: { status: "valid", value: key }, + value: { status: "valid", value: ctx.data[key] }, + }); + } + } + else if (unknownKeys === "strict") { + if (extraKeys.length > 0) { + addIssueToContext(ctx, { + code: ZodIssueCode.unrecognized_keys, + keys: extraKeys, + }); + status.dirty(); + } + } + else if (unknownKeys === "strip") { + } + else { + throw new Error(`Internal ZodObject error: invalid unknownKeys value.`); + } + } + else { + // run catchall validation + const catchall = this._def.catchall; + for (const key of extraKeys) { + const value = ctx.data[key]; + pairs.push({ + key: { status: "valid", value: key }, + value: catchall._parse(new ParseInputLazyPath(ctx, value, ctx.path, key) //, ctx.child(key), value, getParsedType(value) + ), + alwaysSet: key in ctx.data, + }); + } + } + if (ctx.common.async) { + return Promise.resolve() + .then(async () => { + const syncPairs = []; + for (const pair of pairs) { + const key = await pair.key; + const value = await pair.value; + syncPairs.push({ + key, + value, + alwaysSet: pair.alwaysSet, + }); + } + return syncPairs; + }) + .then((syncPairs) => { + return ParseStatus.mergeObjectSync(status, syncPairs); + }); + } + else { + return ParseStatus.mergeObjectSync(status, pairs); + } + } + get shape() { + return this._def.shape(); + } + strict(message) { + errorUtil.errToObj; + return new ZodObject({ + ...this._def, + unknownKeys: "strict", + ...(message !== undefined + ? { + errorMap: (issue, ctx) => { + const defaultError = this._def.errorMap?.(issue, ctx).message ?? ctx.defaultError; + if (issue.code === "unrecognized_keys") + return { + message: errorUtil.errToObj(message).message ?? defaultError, + }; + return { + message: defaultError, + }; + }, + } + : {}), + }); + } + strip() { + return new ZodObject({ + ...this._def, + unknownKeys: "strip", + }); + } + passthrough() { + return new ZodObject({ + ...this._def, + unknownKeys: "passthrough", + }); + } + // const AugmentFactory = + // (def: Def) => + // ( + // augmentation: Augmentation + // ): ZodObject< + // extendShape, Augmentation>, + // Def["unknownKeys"], + // Def["catchall"] + // > => { + // return new ZodObject({ + // ...def, + // shape: () => ({ + // ...def.shape(), + // ...augmentation, + // }), + // }) as any; + // }; + extend(augmentation) { + return new ZodObject({ + ...this._def, + shape: () => ({ + ...this._def.shape(), + ...augmentation, + }), + }); + } + /** + * Prior to zod@1.0.12 there was a bug in the + * inferred type of merged objects. Please + * upgrade if you are experiencing issues. + */ + merge(merging) { + const merged = new ZodObject({ + unknownKeys: merging._def.unknownKeys, + catchall: merging._def.catchall, + shape: () => ({ + ...this._def.shape(), + ...merging._def.shape(), + }), + typeName: ZodFirstPartyTypeKind.ZodObject, + }); + return merged; + } + // merge< + // Incoming extends AnyZodObject, + // Augmentation extends Incoming["shape"], + // NewOutput extends { + // [k in keyof Augmentation | keyof Output]: k extends keyof Augmentation + // ? Augmentation[k]["_output"] + // : k extends keyof Output + // ? Output[k] + // : never; + // }, + // NewInput extends { + // [k in keyof Augmentation | keyof Input]: k extends keyof Augmentation + // ? Augmentation[k]["_input"] + // : k extends keyof Input + // ? Input[k] + // : never; + // } + // >( + // merging: Incoming + // ): ZodObject< + // extendShape>, + // Incoming["_def"]["unknownKeys"], + // Incoming["_def"]["catchall"], + // NewOutput, + // NewInput + // > { + // const merged: any = new ZodObject({ + // unknownKeys: merging._def.unknownKeys, + // catchall: merging._def.catchall, + // shape: () => + // objectUtil.mergeShapes(this._def.shape(), merging._def.shape()), + // typeName: ZodFirstPartyTypeKind.ZodObject, + // }) as any; + // return merged; + // } + setKey(key, schema) { + return this.augment({ [key]: schema }); + } + // merge( + // merging: Incoming + // ): //ZodObject = (merging) => { + // ZodObject< + // extendShape>, + // Incoming["_def"]["unknownKeys"], + // Incoming["_def"]["catchall"] + // > { + // // const mergedShape = objectUtil.mergeShapes( + // // this._def.shape(), + // // merging._def.shape() + // // ); + // const merged: any = new ZodObject({ + // unknownKeys: merging._def.unknownKeys, + // catchall: merging._def.catchall, + // shape: () => + // objectUtil.mergeShapes(this._def.shape(), merging._def.shape()), + // typeName: ZodFirstPartyTypeKind.ZodObject, + // }) as any; + // return merged; + // } + catchall(index) { + return new ZodObject({ + ...this._def, + catchall: index, + }); + } + pick(mask) { + const shape = {}; + for (const key of util.objectKeys(mask)) { + if (mask[key] && this.shape[key]) { + shape[key] = this.shape[key]; + } + } + return new ZodObject({ + ...this._def, + shape: () => shape, + }); + } + omit(mask) { + const shape = {}; + for (const key of util.objectKeys(this.shape)) { + if (!mask[key]) { + shape[key] = this.shape[key]; + } + } + return new ZodObject({ + ...this._def, + shape: () => shape, + }); + } + /** + * @deprecated + */ + deepPartial() { + return deepPartialify(this); + } + partial(mask) { + const newShape = {}; + for (const key of util.objectKeys(this.shape)) { + const fieldSchema = this.shape[key]; + if (mask && !mask[key]) { + newShape[key] = fieldSchema; + } + else { + newShape[key] = fieldSchema.optional(); + } + } + return new ZodObject({ + ...this._def, + shape: () => newShape, + }); + } + required(mask) { + const newShape = {}; + for (const key of util.objectKeys(this.shape)) { + if (mask && !mask[key]) { + newShape[key] = this.shape[key]; + } + else { + const fieldSchema = this.shape[key]; + let newField = fieldSchema; + while (newField instanceof ZodOptional) { + newField = newField._def.innerType; + } + newShape[key] = newField; + } + } + return new ZodObject({ + ...this._def, + shape: () => newShape, + }); + } + keyof() { + return createZodEnum(util.objectKeys(this.shape)); + } +} +ZodObject.create = (shape, params) => { + return new ZodObject({ + shape: () => shape, + unknownKeys: "strip", + catchall: ZodNever.create(), + typeName: ZodFirstPartyTypeKind.ZodObject, + ...processCreateParams(params), + }); +}; +ZodObject.strictCreate = (shape, params) => { + return new ZodObject({ + shape: () => shape, + unknownKeys: "strict", + catchall: ZodNever.create(), + typeName: ZodFirstPartyTypeKind.ZodObject, + ...processCreateParams(params), + }); +}; +ZodObject.lazycreate = (shape, params) => { + return new ZodObject({ + shape, + unknownKeys: "strip", + catchall: ZodNever.create(), + typeName: ZodFirstPartyTypeKind.ZodObject, + ...processCreateParams(params), + }); +}; +export class ZodUnion extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + const options = this._def.options; + function handleResults(results) { + // return first issue-free validation if it exists + for (const result of results) { + if (result.result.status === "valid") { + return result.result; + } + } + for (const result of results) { + if (result.result.status === "dirty") { + // add issues from dirty option + ctx.common.issues.push(...result.ctx.common.issues); + return result.result; + } + } + // return invalid + const unionErrors = results.map((result) => new ZodError(result.ctx.common.issues)); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_union, + unionErrors, + }); + return INVALID; + } + if (ctx.common.async) { + return Promise.all(options.map(async (option) => { + const childCtx = { + ...ctx, + common: { + ...ctx.common, + issues: [], + }, + parent: null, + }; + return { + result: await option._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: childCtx, + }), + ctx: childCtx, + }; + })).then(handleResults); + } + else { + let dirty = undefined; + const issues = []; + for (const option of options) { + const childCtx = { + ...ctx, + common: { + ...ctx.common, + issues: [], + }, + parent: null, + }; + const result = option._parseSync({ + data: ctx.data, + path: ctx.path, + parent: childCtx, + }); + if (result.status === "valid") { + return result; + } + else if (result.status === "dirty" && !dirty) { + dirty = { result, ctx: childCtx }; + } + if (childCtx.common.issues.length) { + issues.push(childCtx.common.issues); + } + } + if (dirty) { + ctx.common.issues.push(...dirty.ctx.common.issues); + return dirty.result; + } + const unionErrors = issues.map((issues) => new ZodError(issues)); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_union, + unionErrors, + }); + return INVALID; + } + } + get options() { + return this._def.options; + } +} +ZodUnion.create = (types, params) => { + return new ZodUnion({ + options: types, + typeName: ZodFirstPartyTypeKind.ZodUnion, + ...processCreateParams(params), + }); +}; +///////////////////////////////////////////////////// +///////////////////////////////////////////////////// +////////// ////////// +////////// ZodDiscriminatedUnion ////////// +////////// ////////// +///////////////////////////////////////////////////// +///////////////////////////////////////////////////// +const getDiscriminator = (type) => { + if (type instanceof ZodLazy) { + return getDiscriminator(type.schema); + } + else if (type instanceof ZodEffects) { + return getDiscriminator(type.innerType()); + } + else if (type instanceof ZodLiteral) { + return [type.value]; + } + else if (type instanceof ZodEnum) { + return type.options; + } + else if (type instanceof ZodNativeEnum) { + // eslint-disable-next-line ban/ban + return util.objectValues(type.enum); + } + else if (type instanceof ZodDefault) { + return getDiscriminator(type._def.innerType); + } + else if (type instanceof ZodUndefined) { + return [undefined]; + } + else if (type instanceof ZodNull) { + return [null]; + } + else if (type instanceof ZodOptional) { + return [undefined, ...getDiscriminator(type.unwrap())]; + } + else if (type instanceof ZodNullable) { + return [null, ...getDiscriminator(type.unwrap())]; + } + else if (type instanceof ZodBranded) { + return getDiscriminator(type.unwrap()); + } + else if (type instanceof ZodReadonly) { + return getDiscriminator(type.unwrap()); + } + else if (type instanceof ZodCatch) { + return getDiscriminator(type._def.innerType); + } + else { + return []; + } +}; +export class ZodDiscriminatedUnion extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.object) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.object, + received: ctx.parsedType, + }); + return INVALID; + } + const discriminator = this.discriminator; + const discriminatorValue = ctx.data[discriminator]; + const option = this.optionsMap.get(discriminatorValue); + if (!option) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_union_discriminator, + options: Array.from(this.optionsMap.keys()), + path: [discriminator], + }); + return INVALID; + } + if (ctx.common.async) { + return option._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + } + else { + return option._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + } + } + get discriminator() { + return this._def.discriminator; + } + get options() { + return this._def.options; + } + get optionsMap() { + return this._def.optionsMap; + } + /** + * The constructor of the discriminated union schema. Its behaviour is very similar to that of the normal z.union() constructor. + * However, it only allows a union of objects, all of which need to share a discriminator property. This property must + * have a different value for each object in the union. + * @param discriminator the name of the discriminator property + * @param types an array of object schemas + * @param params + */ + static create(discriminator, options, params) { + // Get all the valid discriminator values + const optionsMap = new Map(); + // try { + for (const type of options) { + const discriminatorValues = getDiscriminator(type.shape[discriminator]); + if (!discriminatorValues.length) { + throw new Error(`A discriminator value for key \`${discriminator}\` could not be extracted from all schema options`); + } + for (const value of discriminatorValues) { + if (optionsMap.has(value)) { + throw new Error(`Discriminator property ${String(discriminator)} has duplicate value ${String(value)}`); + } + optionsMap.set(value, type); + } + } + return new ZodDiscriminatedUnion({ + typeName: ZodFirstPartyTypeKind.ZodDiscriminatedUnion, + discriminator, + options, + optionsMap, + ...processCreateParams(params), + }); + } +} +function mergeValues(a, b) { + const aType = getParsedType(a); + const bType = getParsedType(b); + if (a === b) { + return { valid: true, data: a }; + } + else if (aType === ZodParsedType.object && bType === ZodParsedType.object) { + const bKeys = util.objectKeys(b); + const sharedKeys = util.objectKeys(a).filter((key) => bKeys.indexOf(key) !== -1); + const newObj = { ...a, ...b }; + for (const key of sharedKeys) { + const sharedValue = mergeValues(a[key], b[key]); + if (!sharedValue.valid) { + return { valid: false }; + } + newObj[key] = sharedValue.data; + } + return { valid: true, data: newObj }; + } + else if (aType === ZodParsedType.array && bType === ZodParsedType.array) { + if (a.length !== b.length) { + return { valid: false }; + } + const newArray = []; + for (let index = 0; index < a.length; index++) { + const itemA = a[index]; + const itemB = b[index]; + const sharedValue = mergeValues(itemA, itemB); + if (!sharedValue.valid) { + return { valid: false }; + } + newArray.push(sharedValue.data); + } + return { valid: true, data: newArray }; + } + else if (aType === ZodParsedType.date && bType === ZodParsedType.date && +a === +b) { + return { valid: true, data: a }; + } + else { + return { valid: false }; + } +} +export class ZodIntersection extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + const handleParsed = (parsedLeft, parsedRight) => { + if (isAborted(parsedLeft) || isAborted(parsedRight)) { + return INVALID; + } + const merged = mergeValues(parsedLeft.value, parsedRight.value); + if (!merged.valid) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_intersection_types, + }); + return INVALID; + } + if (isDirty(parsedLeft) || isDirty(parsedRight)) { + status.dirty(); + } + return { status: status.value, value: merged.data }; + }; + if (ctx.common.async) { + return Promise.all([ + this._def.left._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }), + this._def.right._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }), + ]).then(([left, right]) => handleParsed(left, right)); + } + else { + return handleParsed(this._def.left._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }), this._def.right._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + })); + } + } +} +ZodIntersection.create = (left, right, params) => { + return new ZodIntersection({ + left: left, + right: right, + typeName: ZodFirstPartyTypeKind.ZodIntersection, + ...processCreateParams(params), + }); +}; +// type ZodTupleItems = [ZodTypeAny, ...ZodTypeAny[]]; +export class ZodTuple extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.array) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.array, + received: ctx.parsedType, + }); + return INVALID; + } + if (ctx.data.length < this._def.items.length) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: this._def.items.length, + inclusive: true, + exact: false, + type: "array", + }); + return INVALID; + } + const rest = this._def.rest; + if (!rest && ctx.data.length > this._def.items.length) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: this._def.items.length, + inclusive: true, + exact: false, + type: "array", + }); + status.dirty(); + } + const items = [...ctx.data] + .map((item, itemIndex) => { + const schema = this._def.items[itemIndex] || this._def.rest; + if (!schema) + return null; + return schema._parse(new ParseInputLazyPath(ctx, item, ctx.path, itemIndex)); + }) + .filter((x) => !!x); // filter nulls + if (ctx.common.async) { + return Promise.all(items).then((results) => { + return ParseStatus.mergeArray(status, results); + }); + } + else { + return ParseStatus.mergeArray(status, items); + } + } + get items() { + return this._def.items; + } + rest(rest) { + return new ZodTuple({ + ...this._def, + rest, + }); + } +} +ZodTuple.create = (schemas, params) => { + if (!Array.isArray(schemas)) { + throw new Error("You must pass an array of schemas to z.tuple([ ... ])"); + } + return new ZodTuple({ + items: schemas, + typeName: ZodFirstPartyTypeKind.ZodTuple, + rest: null, + ...processCreateParams(params), + }); +}; +export class ZodRecord extends ZodType { + get keySchema() { + return this._def.keyType; + } + get valueSchema() { + return this._def.valueType; + } + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.object) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.object, + received: ctx.parsedType, + }); + return INVALID; + } + const pairs = []; + const keyType = this._def.keyType; + const valueType = this._def.valueType; + for (const key in ctx.data) { + pairs.push({ + key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, key)), + value: valueType._parse(new ParseInputLazyPath(ctx, ctx.data[key], ctx.path, key)), + alwaysSet: key in ctx.data, + }); + } + if (ctx.common.async) { + return ParseStatus.mergeObjectAsync(status, pairs); + } + else { + return ParseStatus.mergeObjectSync(status, pairs); + } + } + get element() { + return this._def.valueType; + } + static create(first, second, third) { + if (second instanceof ZodType) { + return new ZodRecord({ + keyType: first, + valueType: second, + typeName: ZodFirstPartyTypeKind.ZodRecord, + ...processCreateParams(third), + }); + } + return new ZodRecord({ + keyType: ZodString.create(), + valueType: first, + typeName: ZodFirstPartyTypeKind.ZodRecord, + ...processCreateParams(second), + }); + } +} +export class ZodMap extends ZodType { + get keySchema() { + return this._def.keyType; + } + get valueSchema() { + return this._def.valueType; + } + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.map) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.map, + received: ctx.parsedType, + }); + return INVALID; + } + const keyType = this._def.keyType; + const valueType = this._def.valueType; + const pairs = [...ctx.data.entries()].map(([key, value], index) => { + return { + key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, [index, "key"])), + value: valueType._parse(new ParseInputLazyPath(ctx, value, ctx.path, [index, "value"])), + }; + }); + if (ctx.common.async) { + const finalMap = new Map(); + return Promise.resolve().then(async () => { + for (const pair of pairs) { + const key = await pair.key; + const value = await pair.value; + if (key.status === "aborted" || value.status === "aborted") { + return INVALID; + } + if (key.status === "dirty" || value.status === "dirty") { + status.dirty(); + } + finalMap.set(key.value, value.value); + } + return { status: status.value, value: finalMap }; + }); + } + else { + const finalMap = new Map(); + for (const pair of pairs) { + const key = pair.key; + const value = pair.value; + if (key.status === "aborted" || value.status === "aborted") { + return INVALID; + } + if (key.status === "dirty" || value.status === "dirty") { + status.dirty(); + } + finalMap.set(key.value, value.value); + } + return { status: status.value, value: finalMap }; + } + } +} +ZodMap.create = (keyType, valueType, params) => { + return new ZodMap({ + valueType, + keyType, + typeName: ZodFirstPartyTypeKind.ZodMap, + ...processCreateParams(params), + }); +}; +export class ZodSet extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.set) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.set, + received: ctx.parsedType, + }); + return INVALID; + } + const def = this._def; + if (def.minSize !== null) { + if (ctx.data.size < def.minSize.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: def.minSize.value, + type: "set", + inclusive: true, + exact: false, + message: def.minSize.message, + }); + status.dirty(); + } + } + if (def.maxSize !== null) { + if (ctx.data.size > def.maxSize.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: def.maxSize.value, + type: "set", + inclusive: true, + exact: false, + message: def.maxSize.message, + }); + status.dirty(); + } + } + const valueType = this._def.valueType; + function finalizeSet(elements) { + const parsedSet = new Set(); + for (const element of elements) { + if (element.status === "aborted") + return INVALID; + if (element.status === "dirty") + status.dirty(); + parsedSet.add(element.value); + } + return { status: status.value, value: parsedSet }; + } + const elements = [...ctx.data.values()].map((item, i) => valueType._parse(new ParseInputLazyPath(ctx, item, ctx.path, i))); + if (ctx.common.async) { + return Promise.all(elements).then((elements) => finalizeSet(elements)); + } + else { + return finalizeSet(elements); + } + } + min(minSize, message) { + return new ZodSet({ + ...this._def, + minSize: { value: minSize, message: errorUtil.toString(message) }, + }); + } + max(maxSize, message) { + return new ZodSet({ + ...this._def, + maxSize: { value: maxSize, message: errorUtil.toString(message) }, + }); + } + size(size, message) { + return this.min(size, message).max(size, message); + } + nonempty(message) { + return this.min(1, message); + } +} +ZodSet.create = (valueType, params) => { + return new ZodSet({ + valueType, + minSize: null, + maxSize: null, + typeName: ZodFirstPartyTypeKind.ZodSet, + ...processCreateParams(params), + }); +}; +export class ZodFunction extends ZodType { + constructor() { + super(...arguments); + this.validate = this.implement; + } + _parse(input) { + const { ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.function) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.function, + received: ctx.parsedType, + }); + return INVALID; + } + function makeArgsIssue(args, error) { + return makeIssue({ + data: args, + path: ctx.path, + errorMaps: [ctx.common.contextualErrorMap, ctx.schemaErrorMap, getErrorMap(), defaultErrorMap].filter((x) => !!x), + issueData: { + code: ZodIssueCode.invalid_arguments, + argumentsError: error, + }, + }); + } + function makeReturnsIssue(returns, error) { + return makeIssue({ + data: returns, + path: ctx.path, + errorMaps: [ctx.common.contextualErrorMap, ctx.schemaErrorMap, getErrorMap(), defaultErrorMap].filter((x) => !!x), + issueData: { + code: ZodIssueCode.invalid_return_type, + returnTypeError: error, + }, + }); + } + const params = { errorMap: ctx.common.contextualErrorMap }; + const fn = ctx.data; + if (this._def.returns instanceof ZodPromise) { + // Would love a way to avoid disabling this rule, but we need + // an alias (using an arrow function was what caused 2651). + // eslint-disable-next-line @typescript-eslint/no-this-alias + const me = this; + return OK(async function (...args) { + const error = new ZodError([]); + const parsedArgs = await me._def.args.parseAsync(args, params).catch((e) => { + error.addIssue(makeArgsIssue(args, e)); + throw error; + }); + const result = await Reflect.apply(fn, this, parsedArgs); + const parsedReturns = await me._def.returns._def.type + .parseAsync(result, params) + .catch((e) => { + error.addIssue(makeReturnsIssue(result, e)); + throw error; + }); + return parsedReturns; + }); + } + else { + // Would love a way to avoid disabling this rule, but we need + // an alias (using an arrow function was what caused 2651). + // eslint-disable-next-line @typescript-eslint/no-this-alias + const me = this; + return OK(function (...args) { + const parsedArgs = me._def.args.safeParse(args, params); + if (!parsedArgs.success) { + throw new ZodError([makeArgsIssue(args, parsedArgs.error)]); + } + const result = Reflect.apply(fn, this, parsedArgs.data); + const parsedReturns = me._def.returns.safeParse(result, params); + if (!parsedReturns.success) { + throw new ZodError([makeReturnsIssue(result, parsedReturns.error)]); + } + return parsedReturns.data; + }); + } + } + parameters() { + return this._def.args; + } + returnType() { + return this._def.returns; + } + args(...items) { + return new ZodFunction({ + ...this._def, + args: ZodTuple.create(items).rest(ZodUnknown.create()), + }); + } + returns(returnType) { + return new ZodFunction({ + ...this._def, + returns: returnType, + }); + } + implement(func) { + const validatedFunc = this.parse(func); + return validatedFunc; + } + strictImplement(func) { + const validatedFunc = this.parse(func); + return validatedFunc; + } + static create(args, returns, params) { + return new ZodFunction({ + args: (args ? args : ZodTuple.create([]).rest(ZodUnknown.create())), + returns: returns || ZodUnknown.create(), + typeName: ZodFirstPartyTypeKind.ZodFunction, + ...processCreateParams(params), + }); + } +} +export class ZodLazy extends ZodType { + get schema() { + return this._def.getter(); + } + _parse(input) { + const { ctx } = this._processInputParams(input); + const lazySchema = this._def.getter(); + return lazySchema._parse({ data: ctx.data, path: ctx.path, parent: ctx }); + } +} +ZodLazy.create = (getter, params) => { + return new ZodLazy({ + getter: getter, + typeName: ZodFirstPartyTypeKind.ZodLazy, + ...processCreateParams(params), + }); +}; +export class ZodLiteral extends ZodType { + _parse(input) { + if (input.data !== this._def.value) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + received: ctx.data, + code: ZodIssueCode.invalid_literal, + expected: this._def.value, + }); + return INVALID; + } + return { status: "valid", value: input.data }; + } + get value() { + return this._def.value; + } +} +ZodLiteral.create = (value, params) => { + return new ZodLiteral({ + value: value, + typeName: ZodFirstPartyTypeKind.ZodLiteral, + ...processCreateParams(params), + }); +}; +function createZodEnum(values, params) { + return new ZodEnum({ + values, + typeName: ZodFirstPartyTypeKind.ZodEnum, + ...processCreateParams(params), + }); +} +export class ZodEnum extends ZodType { + _parse(input) { + if (typeof input.data !== "string") { + const ctx = this._getOrReturnCtx(input); + const expectedValues = this._def.values; + addIssueToContext(ctx, { + expected: util.joinValues(expectedValues), + received: ctx.parsedType, + code: ZodIssueCode.invalid_type, + }); + return INVALID; + } + if (!this._cache) { + this._cache = new Set(this._def.values); + } + if (!this._cache.has(input.data)) { + const ctx = this._getOrReturnCtx(input); + const expectedValues = this._def.values; + addIssueToContext(ctx, { + received: ctx.data, + code: ZodIssueCode.invalid_enum_value, + options: expectedValues, + }); + return INVALID; + } + return OK(input.data); + } + get options() { + return this._def.values; + } + get enum() { + const enumValues = {}; + for (const val of this._def.values) { + enumValues[val] = val; + } + return enumValues; + } + get Values() { + const enumValues = {}; + for (const val of this._def.values) { + enumValues[val] = val; + } + return enumValues; + } + get Enum() { + const enumValues = {}; + for (const val of this._def.values) { + enumValues[val] = val; + } + return enumValues; + } + extract(values, newDef = this._def) { + return ZodEnum.create(values, { + ...this._def, + ...newDef, + }); + } + exclude(values, newDef = this._def) { + return ZodEnum.create(this.options.filter((opt) => !values.includes(opt)), { + ...this._def, + ...newDef, + }); + } +} +ZodEnum.create = createZodEnum; +export class ZodNativeEnum extends ZodType { + _parse(input) { + const nativeEnumValues = util.getValidEnumValues(this._def.values); + const ctx = this._getOrReturnCtx(input); + if (ctx.parsedType !== ZodParsedType.string && ctx.parsedType !== ZodParsedType.number) { + const expectedValues = util.objectValues(nativeEnumValues); + addIssueToContext(ctx, { + expected: util.joinValues(expectedValues), + received: ctx.parsedType, + code: ZodIssueCode.invalid_type, + }); + return INVALID; + } + if (!this._cache) { + this._cache = new Set(util.getValidEnumValues(this._def.values)); + } + if (!this._cache.has(input.data)) { + const expectedValues = util.objectValues(nativeEnumValues); + addIssueToContext(ctx, { + received: ctx.data, + code: ZodIssueCode.invalid_enum_value, + options: expectedValues, + }); + return INVALID; + } + return OK(input.data); + } + get enum() { + return this._def.values; + } +} +ZodNativeEnum.create = (values, params) => { + return new ZodNativeEnum({ + values: values, + typeName: ZodFirstPartyTypeKind.ZodNativeEnum, + ...processCreateParams(params), + }); +}; +export class ZodPromise extends ZodType { + unwrap() { + return this._def.type; + } + _parse(input) { + const { ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.promise && ctx.common.async === false) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.promise, + received: ctx.parsedType, + }); + return INVALID; + } + const promisified = ctx.parsedType === ZodParsedType.promise ? ctx.data : Promise.resolve(ctx.data); + return OK(promisified.then((data) => { + return this._def.type.parseAsync(data, { + path: ctx.path, + errorMap: ctx.common.contextualErrorMap, + }); + })); + } +} +ZodPromise.create = (schema, params) => { + return new ZodPromise({ + type: schema, + typeName: ZodFirstPartyTypeKind.ZodPromise, + ...processCreateParams(params), + }); +}; +export class ZodEffects extends ZodType { + innerType() { + return this._def.schema; + } + sourceType() { + return this._def.schema._def.typeName === ZodFirstPartyTypeKind.ZodEffects + ? this._def.schema.sourceType() + : this._def.schema; + } + _parse(input) { + const { status, ctx } = this._processInputParams(input); + const effect = this._def.effect || null; + const checkCtx = { + addIssue: (arg) => { + addIssueToContext(ctx, arg); + if (arg.fatal) { + status.abort(); + } + else { + status.dirty(); + } + }, + get path() { + return ctx.path; + }, + }; + checkCtx.addIssue = checkCtx.addIssue.bind(checkCtx); + if (effect.type === "preprocess") { + const processed = effect.transform(ctx.data, checkCtx); + if (ctx.common.async) { + return Promise.resolve(processed).then(async (processed) => { + if (status.value === "aborted") + return INVALID; + const result = await this._def.schema._parseAsync({ + data: processed, + path: ctx.path, + parent: ctx, + }); + if (result.status === "aborted") + return INVALID; + if (result.status === "dirty") + return DIRTY(result.value); + if (status.value === "dirty") + return DIRTY(result.value); + return result; + }); + } + else { + if (status.value === "aborted") + return INVALID; + const result = this._def.schema._parseSync({ + data: processed, + path: ctx.path, + parent: ctx, + }); + if (result.status === "aborted") + return INVALID; + if (result.status === "dirty") + return DIRTY(result.value); + if (status.value === "dirty") + return DIRTY(result.value); + return result; + } + } + if (effect.type === "refinement") { + const executeRefinement = (acc) => { + const result = effect.refinement(acc, checkCtx); + if (ctx.common.async) { + return Promise.resolve(result); + } + if (result instanceof Promise) { + throw new Error("Async refinement encountered during synchronous parse operation. Use .parseAsync instead."); + } + return acc; + }; + if (ctx.common.async === false) { + const inner = this._def.schema._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + if (inner.status === "aborted") + return INVALID; + if (inner.status === "dirty") + status.dirty(); + // return value is ignored + executeRefinement(inner.value); + return { status: status.value, value: inner.value }; + } + else { + return this._def.schema._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }).then((inner) => { + if (inner.status === "aborted") + return INVALID; + if (inner.status === "dirty") + status.dirty(); + return executeRefinement(inner.value).then(() => { + return { status: status.value, value: inner.value }; + }); + }); + } + } + if (effect.type === "transform") { + if (ctx.common.async === false) { + const base = this._def.schema._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + if (!isValid(base)) + return INVALID; + const result = effect.transform(base.value, checkCtx); + if (result instanceof Promise) { + throw new Error(`Asynchronous transform encountered during synchronous parse operation. Use .parseAsync instead.`); + } + return { status: status.value, value: result }; + } + else { + return this._def.schema._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }).then((base) => { + if (!isValid(base)) + return INVALID; + return Promise.resolve(effect.transform(base.value, checkCtx)).then((result) => ({ + status: status.value, + value: result, + })); + }); + } + } + util.assertNever(effect); + } +} +ZodEffects.create = (schema, effect, params) => { + return new ZodEffects({ + schema, + typeName: ZodFirstPartyTypeKind.ZodEffects, + effect, + ...processCreateParams(params), + }); +}; +ZodEffects.createWithPreprocess = (preprocess, schema, params) => { + return new ZodEffects({ + schema, + effect: { type: "preprocess", transform: preprocess }, + typeName: ZodFirstPartyTypeKind.ZodEffects, + ...processCreateParams(params), + }); +}; +export { ZodEffects as ZodTransformer }; +export class ZodOptional extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType === ZodParsedType.undefined) { + return OK(undefined); + } + return this._def.innerType._parse(input); + } + unwrap() { + return this._def.innerType; + } +} +ZodOptional.create = (type, params) => { + return new ZodOptional({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodOptional, + ...processCreateParams(params), + }); +}; +export class ZodNullable extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType === ZodParsedType.null) { + return OK(null); + } + return this._def.innerType._parse(input); + } + unwrap() { + return this._def.innerType; + } +} +ZodNullable.create = (type, params) => { + return new ZodNullable({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodNullable, + ...processCreateParams(params), + }); +}; +export class ZodDefault extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + let data = ctx.data; + if (ctx.parsedType === ZodParsedType.undefined) { + data = this._def.defaultValue(); + } + return this._def.innerType._parse({ + data, + path: ctx.path, + parent: ctx, + }); + } + removeDefault() { + return this._def.innerType; + } +} +ZodDefault.create = (type, params) => { + return new ZodDefault({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodDefault, + defaultValue: typeof params.default === "function" ? params.default : () => params.default, + ...processCreateParams(params), + }); +}; +export class ZodCatch extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + // newCtx is used to not collect issues from inner types in ctx + const newCtx = { + ...ctx, + common: { + ...ctx.common, + issues: [], + }, + }; + const result = this._def.innerType._parse({ + data: newCtx.data, + path: newCtx.path, + parent: { + ...newCtx, + }, + }); + if (isAsync(result)) { + return result.then((result) => { + return { + status: "valid", + value: result.status === "valid" + ? result.value + : this._def.catchValue({ + get error() { + return new ZodError(newCtx.common.issues); + }, + input: newCtx.data, + }), + }; + }); + } + else { + return { + status: "valid", + value: result.status === "valid" + ? result.value + : this._def.catchValue({ + get error() { + return new ZodError(newCtx.common.issues); + }, + input: newCtx.data, + }), + }; + } + } + removeCatch() { + return this._def.innerType; + } +} +ZodCatch.create = (type, params) => { + return new ZodCatch({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodCatch, + catchValue: typeof params.catch === "function" ? params.catch : () => params.catch, + ...processCreateParams(params), + }); +}; +export class ZodNaN extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.nan) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.nan, + received: ctx.parsedType, + }); + return INVALID; + } + return { status: "valid", value: input.data }; + } +} +ZodNaN.create = (params) => { + return new ZodNaN({ + typeName: ZodFirstPartyTypeKind.ZodNaN, + ...processCreateParams(params), + }); +}; +export const BRAND = Symbol("zod_brand"); +export class ZodBranded extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + const data = ctx.data; + return this._def.type._parse({ + data, + path: ctx.path, + parent: ctx, + }); + } + unwrap() { + return this._def.type; + } +} +export class ZodPipeline extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.common.async) { + const handleAsync = async () => { + const inResult = await this._def.in._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + if (inResult.status === "aborted") + return INVALID; + if (inResult.status === "dirty") { + status.dirty(); + return DIRTY(inResult.value); + } + else { + return this._def.out._parseAsync({ + data: inResult.value, + path: ctx.path, + parent: ctx, + }); + } + }; + return handleAsync(); + } + else { + const inResult = this._def.in._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + if (inResult.status === "aborted") + return INVALID; + if (inResult.status === "dirty") { + status.dirty(); + return { + status: "dirty", + value: inResult.value, + }; + } + else { + return this._def.out._parseSync({ + data: inResult.value, + path: ctx.path, + parent: ctx, + }); + } + } + } + static create(a, b) { + return new ZodPipeline({ + in: a, + out: b, + typeName: ZodFirstPartyTypeKind.ZodPipeline, + }); + } +} +export class ZodReadonly extends ZodType { + _parse(input) { + const result = this._def.innerType._parse(input); + const freeze = (data) => { + if (isValid(data)) { + data.value = Object.freeze(data.value); + } + return data; + }; + return isAsync(result) ? result.then((data) => freeze(data)) : freeze(result); + } + unwrap() { + return this._def.innerType; + } +} +ZodReadonly.create = (type, params) => { + return new ZodReadonly({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodReadonly, + ...processCreateParams(params), + }); +}; +//////////////////////////////////////// +//////////////////////////////////////// +////////// ////////// +////////// z.custom ////////// +////////// ////////// +//////////////////////////////////////// +//////////////////////////////////////// +function cleanParams(params, data) { + const p = typeof params === "function" ? params(data) : typeof params === "string" ? { message: params } : params; + const p2 = typeof p === "string" ? { message: p } : p; + return p2; +} +export function custom(check, _params = {}, +/** + * @deprecated + * + * Pass `fatal` into the params object instead: + * + * ```ts + * z.string().custom((val) => val.length > 5, { fatal: false }) + * ``` + * + */ +fatal) { + if (check) + return ZodAny.create().superRefine((data, ctx) => { + const r = check(data); + if (r instanceof Promise) { + return r.then((r) => { + if (!r) { + const params = cleanParams(_params, data); + const _fatal = params.fatal ?? fatal ?? true; + ctx.addIssue({ code: "custom", ...params, fatal: _fatal }); + } + }); + } + if (!r) { + const params = cleanParams(_params, data); + const _fatal = params.fatal ?? fatal ?? true; + ctx.addIssue({ code: "custom", ...params, fatal: _fatal }); + } + return; + }); + return ZodAny.create(); +} +export { ZodType as Schema, ZodType as ZodSchema }; +export const late = { + object: ZodObject.lazycreate, +}; +export var ZodFirstPartyTypeKind; +(function (ZodFirstPartyTypeKind) { + ZodFirstPartyTypeKind["ZodString"] = "ZodString"; + ZodFirstPartyTypeKind["ZodNumber"] = "ZodNumber"; + ZodFirstPartyTypeKind["ZodNaN"] = "ZodNaN"; + ZodFirstPartyTypeKind["ZodBigInt"] = "ZodBigInt"; + ZodFirstPartyTypeKind["ZodBoolean"] = "ZodBoolean"; + ZodFirstPartyTypeKind["ZodDate"] = "ZodDate"; + ZodFirstPartyTypeKind["ZodSymbol"] = "ZodSymbol"; + ZodFirstPartyTypeKind["ZodUndefined"] = "ZodUndefined"; + ZodFirstPartyTypeKind["ZodNull"] = "ZodNull"; + ZodFirstPartyTypeKind["ZodAny"] = "ZodAny"; + ZodFirstPartyTypeKind["ZodUnknown"] = "ZodUnknown"; + ZodFirstPartyTypeKind["ZodNever"] = "ZodNever"; + ZodFirstPartyTypeKind["ZodVoid"] = "ZodVoid"; + ZodFirstPartyTypeKind["ZodArray"] = "ZodArray"; + ZodFirstPartyTypeKind["ZodObject"] = "ZodObject"; + ZodFirstPartyTypeKind["ZodUnion"] = "ZodUnion"; + ZodFirstPartyTypeKind["ZodDiscriminatedUnion"] = "ZodDiscriminatedUnion"; + ZodFirstPartyTypeKind["ZodIntersection"] = "ZodIntersection"; + ZodFirstPartyTypeKind["ZodTuple"] = "ZodTuple"; + ZodFirstPartyTypeKind["ZodRecord"] = "ZodRecord"; + ZodFirstPartyTypeKind["ZodMap"] = "ZodMap"; + ZodFirstPartyTypeKind["ZodSet"] = "ZodSet"; + ZodFirstPartyTypeKind["ZodFunction"] = "ZodFunction"; + ZodFirstPartyTypeKind["ZodLazy"] = "ZodLazy"; + ZodFirstPartyTypeKind["ZodLiteral"] = "ZodLiteral"; + ZodFirstPartyTypeKind["ZodEnum"] = "ZodEnum"; + ZodFirstPartyTypeKind["ZodEffects"] = "ZodEffects"; + ZodFirstPartyTypeKind["ZodNativeEnum"] = "ZodNativeEnum"; + ZodFirstPartyTypeKind["ZodOptional"] = "ZodOptional"; + ZodFirstPartyTypeKind["ZodNullable"] = "ZodNullable"; + ZodFirstPartyTypeKind["ZodDefault"] = "ZodDefault"; + ZodFirstPartyTypeKind["ZodCatch"] = "ZodCatch"; + ZodFirstPartyTypeKind["ZodPromise"] = "ZodPromise"; + ZodFirstPartyTypeKind["ZodBranded"] = "ZodBranded"; + ZodFirstPartyTypeKind["ZodPipeline"] = "ZodPipeline"; + ZodFirstPartyTypeKind["ZodReadonly"] = "ZodReadonly"; +})(ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {})); +// requires TS 4.4+ +class Class { + constructor(..._) { } +} +const instanceOfType = ( +// const instanceOfType = any>( +cls, params = { + message: `Input not instance of ${cls.name}`, +}) => custom((data) => data instanceof cls, params); +const stringType = ZodString.create; +const numberType = ZodNumber.create; +const nanType = ZodNaN.create; +const bigIntType = ZodBigInt.create; +const booleanType = ZodBoolean.create; +const dateType = ZodDate.create; +const symbolType = ZodSymbol.create; +const undefinedType = ZodUndefined.create; +const nullType = ZodNull.create; +const anyType = ZodAny.create; +const unknownType = ZodUnknown.create; +const neverType = ZodNever.create; +const voidType = ZodVoid.create; +const arrayType = ZodArray.create; +const objectType = ZodObject.create; +const strictObjectType = ZodObject.strictCreate; +const unionType = ZodUnion.create; +const discriminatedUnionType = ZodDiscriminatedUnion.create; +const intersectionType = ZodIntersection.create; +const tupleType = ZodTuple.create; +const recordType = ZodRecord.create; +const mapType = ZodMap.create; +const setType = ZodSet.create; +const functionType = ZodFunction.create; +const lazyType = ZodLazy.create; +const literalType = ZodLiteral.create; +const enumType = ZodEnum.create; +const nativeEnumType = ZodNativeEnum.create; +const promiseType = ZodPromise.create; +const effectsType = ZodEffects.create; +const optionalType = ZodOptional.create; +const nullableType = ZodNullable.create; +const preprocessType = ZodEffects.createWithPreprocess; +const pipelineType = ZodPipeline.create; +const ostring = () => stringType().optional(); +const onumber = () => numberType().optional(); +const oboolean = () => booleanType().optional(); +export const coerce = { + string: ((arg) => ZodString.create({ ...arg, coerce: true })), + number: ((arg) => ZodNumber.create({ ...arg, coerce: true })), + boolean: ((arg) => ZodBoolean.create({ + ...arg, + coerce: true, + })), + bigint: ((arg) => ZodBigInt.create({ ...arg, coerce: true })), + date: ((arg) => ZodDate.create({ ...arg, coerce: true })), +}; +export { anyType as any, arrayType as array, bigIntType as bigint, booleanType as boolean, dateType as date, discriminatedUnionType as discriminatedUnion, effectsType as effect, enumType as enum, functionType as function, instanceOfType as instanceof, intersectionType as intersection, lazyType as lazy, literalType as literal, mapType as map, nanType as nan, nativeEnumType as nativeEnum, neverType as never, nullType as null, nullableType as nullable, numberType as number, objectType as object, oboolean, onumber, optionalType as optional, ostring, pipelineType as pipeline, preprocessType as preprocess, promiseType as promise, recordType as record, setType as set, strictObjectType as strictObject, stringType as string, symbolType as symbol, effectsType as transformer, tupleType as tuple, undefinedType as undefined, unionType as union, unknownType as unknown, voidType as void, }; +export const NEVER = INVALID; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/index.d.ts b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/index.d.ts new file mode 100644 index 000000000..8352ab713 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/index.d.ts @@ -0,0 +1,3 @@ +import z3 from "./v3/index.js"; +export * from "./v3/index.js"; +export default z3; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/ZodError.d.ts b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/ZodError.d.ts new file mode 100644 index 000000000..4e71355a3 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/ZodError.d.ts @@ -0,0 +1,164 @@ +import type { Primitive } from "./helpers/typeAliases.js"; +import { util, type ZodParsedType } from "./helpers/util.js"; +import type { TypeOf, ZodType } from "./index.js"; +type allKeys = T extends any ? keyof T : never; +export type inferFlattenedErrors, U = string> = typeToFlattenedError, U>; +export type typeToFlattenedError = { + formErrors: U[]; + fieldErrors: { + [P in allKeys]?: U[]; + }; +}; +export declare const ZodIssueCode: { + invalid_type: "invalid_type"; + invalid_literal: "invalid_literal"; + custom: "custom"; + invalid_union: "invalid_union"; + invalid_union_discriminator: "invalid_union_discriminator"; + invalid_enum_value: "invalid_enum_value"; + unrecognized_keys: "unrecognized_keys"; + invalid_arguments: "invalid_arguments"; + invalid_return_type: "invalid_return_type"; + invalid_date: "invalid_date"; + invalid_string: "invalid_string"; + too_small: "too_small"; + too_big: "too_big"; + invalid_intersection_types: "invalid_intersection_types"; + not_multiple_of: "not_multiple_of"; + not_finite: "not_finite"; +}; +export type ZodIssueCode = keyof typeof ZodIssueCode; +export type ZodIssueBase = { + path: (string | number)[]; + message?: string | undefined; +}; +export interface ZodInvalidTypeIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_type; + expected: ZodParsedType; + received: ZodParsedType; +} +export interface ZodInvalidLiteralIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_literal; + expected: unknown; + received: unknown; +} +export interface ZodUnrecognizedKeysIssue extends ZodIssueBase { + code: typeof ZodIssueCode.unrecognized_keys; + keys: string[]; +} +export interface ZodInvalidUnionIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_union; + unionErrors: ZodError[]; +} +export interface ZodInvalidUnionDiscriminatorIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_union_discriminator; + options: Primitive[]; +} +export interface ZodInvalidEnumValueIssue extends ZodIssueBase { + received: string | number; + code: typeof ZodIssueCode.invalid_enum_value; + options: (string | number)[]; +} +export interface ZodInvalidArgumentsIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_arguments; + argumentsError: ZodError; +} +export interface ZodInvalidReturnTypeIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_return_type; + returnTypeError: ZodError; +} +export interface ZodInvalidDateIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_date; +} +export type StringValidation = "email" | "url" | "emoji" | "uuid" | "nanoid" | "regex" | "cuid" | "cuid2" | "ulid" | "datetime" | "date" | "time" | "duration" | "ip" | "cidr" | "base64" | "jwt" | "base64url" | { + includes: string; + position?: number | undefined; +} | { + startsWith: string; +} | { + endsWith: string; +}; +export interface ZodInvalidStringIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_string; + validation: StringValidation; +} +export interface ZodTooSmallIssue extends ZodIssueBase { + code: typeof ZodIssueCode.too_small; + minimum: number | bigint; + inclusive: boolean; + exact?: boolean; + type: "array" | "string" | "number" | "set" | "date" | "bigint"; +} +export interface ZodTooBigIssue extends ZodIssueBase { + code: typeof ZodIssueCode.too_big; + maximum: number | bigint; + inclusive: boolean; + exact?: boolean; + type: "array" | "string" | "number" | "set" | "date" | "bigint"; +} +export interface ZodInvalidIntersectionTypesIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_intersection_types; +} +export interface ZodNotMultipleOfIssue extends ZodIssueBase { + code: typeof ZodIssueCode.not_multiple_of; + multipleOf: number | bigint; +} +export interface ZodNotFiniteIssue extends ZodIssueBase { + code: typeof ZodIssueCode.not_finite; +} +export interface ZodCustomIssue extends ZodIssueBase { + code: typeof ZodIssueCode.custom; + params?: { + [k: string]: any; + }; +} +export type DenormalizedError = { + [k: string]: DenormalizedError | string[]; +}; +export type ZodIssueOptionalMessage = ZodInvalidTypeIssue | ZodInvalidLiteralIssue | ZodUnrecognizedKeysIssue | ZodInvalidUnionIssue | ZodInvalidUnionDiscriminatorIssue | ZodInvalidEnumValueIssue | ZodInvalidArgumentsIssue | ZodInvalidReturnTypeIssue | ZodInvalidDateIssue | ZodInvalidStringIssue | ZodTooSmallIssue | ZodTooBigIssue | ZodInvalidIntersectionTypesIssue | ZodNotMultipleOfIssue | ZodNotFiniteIssue | ZodCustomIssue; +export type ZodIssue = ZodIssueOptionalMessage & { + fatal?: boolean | undefined; + message: string; +}; +export declare const quotelessJson: (obj: any) => string; +type recursiveZodFormattedError = T extends [any, ...any[]] ? { + [K in keyof T]?: ZodFormattedError; +} : T extends any[] ? { + [k: number]: ZodFormattedError; +} : T extends object ? { + [K in keyof T]?: ZodFormattedError; +} : unknown; +export type ZodFormattedError = { + _errors: U[]; +} & recursiveZodFormattedError>; +export type inferFormattedError, U = string> = ZodFormattedError, U>; +export declare class ZodError extends Error { + issues: ZodIssue[]; + get errors(): ZodIssue[]; + constructor(issues: ZodIssue[]); + format(): ZodFormattedError; + format(mapper: (issue: ZodIssue) => U): ZodFormattedError; + static create: (issues: ZodIssue[]) => ZodError; + static assert(value: unknown): asserts value is ZodError; + toString(): string; + get message(): string; + get isEmpty(): boolean; + addIssue: (sub: ZodIssue) => void; + addIssues: (subs?: ZodIssue[]) => void; + flatten(): typeToFlattenedError; + flatten(mapper?: (issue: ZodIssue) => U): typeToFlattenedError; + get formErrors(): typeToFlattenedError; +} +type stripPath = T extends any ? util.OmitKeys : never; +export type IssueData = stripPath & { + path?: (string | number)[]; + fatal?: boolean | undefined; +}; +export type ErrorMapCtx = { + defaultError: string; + data: any; +}; +export type ZodErrorMap = (issue: ZodIssueOptionalMessage, _ctx: ErrorMapCtx) => { + message: string; +}; +export {}; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/errors.d.ts b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/errors.d.ts new file mode 100644 index 000000000..52eacf72e --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/errors.d.ts @@ -0,0 +1,5 @@ +import type { ZodErrorMap } from "./ZodError.js"; +import defaultErrorMap from "./locales/en.js"; +export { defaultErrorMap }; +export declare function setErrorMap(map: ZodErrorMap): void; +export declare function getErrorMap(): ZodErrorMap; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/external.d.ts b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/external.d.ts new file mode 100644 index 000000000..f0a4be4c7 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/external.d.ts @@ -0,0 +1,6 @@ +export * from "./errors.js"; +export * from "./helpers/parseUtil.js"; +export * from "./helpers/typeAliases.js"; +export * from "./helpers/util.js"; +export * from "./types.js"; +export * from "./ZodError.js"; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/enumUtil.d.ts b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/enumUtil.d.ts new file mode 100644 index 000000000..fad28a711 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/enumUtil.d.ts @@ -0,0 +1,8 @@ +export declare namespace enumUtil { + type UnionToIntersectionFn = (T extends unknown ? (k: () => T) => void : never) extends (k: infer Intersection) => void ? Intersection : never; + type GetUnionLast = UnionToIntersectionFn extends () => infer Last ? Last : never; + type UnionToTuple = [T] extends [never] ? Tuple : UnionToTuple>, [GetUnionLast, ...Tuple]>; + type CastToStringTuple = T extends [string, ...string[]] ? T : never; + export type UnionToTupleString = CastToStringTuple>; + export {}; +} diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/errorUtil.d.ts b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/errorUtil.d.ts new file mode 100644 index 000000000..bc9b033ff --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/errorUtil.d.ts @@ -0,0 +1,9 @@ +export declare namespace errorUtil { + type ErrMessage = string | { + message?: string | undefined; + }; + const errToObj: (message?: ErrMessage) => { + message?: string | undefined; + }; + const toString: (message?: ErrMessage) => string | undefined; +} diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/parseUtil.d.ts b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/parseUtil.d.ts new file mode 100644 index 000000000..e4dc1d7b3 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/parseUtil.d.ts @@ -0,0 +1,78 @@ +import type { IssueData, ZodErrorMap, ZodIssue } from "../ZodError.js"; +import type { ZodParsedType } from "./util.js"; +export declare const makeIssue: (params: { + data: any; + path: (string | number)[]; + errorMaps: ZodErrorMap[]; + issueData: IssueData; +}) => ZodIssue; +export type ParseParams = { + path: (string | number)[]; + errorMap: ZodErrorMap; + async: boolean; +}; +export type ParsePathComponent = string | number; +export type ParsePath = ParsePathComponent[]; +export declare const EMPTY_PATH: ParsePath; +export interface ParseContext { + readonly common: { + readonly issues: ZodIssue[]; + readonly contextualErrorMap?: ZodErrorMap | undefined; + readonly async: boolean; + }; + readonly path: ParsePath; + readonly schemaErrorMap?: ZodErrorMap | undefined; + readonly parent: ParseContext | null; + readonly data: any; + readonly parsedType: ZodParsedType; +} +export type ParseInput = { + data: any; + path: (string | number)[]; + parent: ParseContext; +}; +export declare function addIssueToContext(ctx: ParseContext, issueData: IssueData): void; +export type ObjectPair = { + key: SyncParseReturnType; + value: SyncParseReturnType; +}; +export declare class ParseStatus { + value: "aborted" | "dirty" | "valid"; + dirty(): void; + abort(): void; + static mergeArray(status: ParseStatus, results: SyncParseReturnType[]): SyncParseReturnType; + static mergeObjectAsync(status: ParseStatus, pairs: { + key: ParseReturnType; + value: ParseReturnType; + }[]): Promise>; + static mergeObjectSync(status: ParseStatus, pairs: { + key: SyncParseReturnType; + value: SyncParseReturnType; + alwaysSet?: boolean; + }[]): SyncParseReturnType; +} +export interface ParseResult { + status: "aborted" | "dirty" | "valid"; + data: any; +} +export type INVALID = { + status: "aborted"; +}; +export declare const INVALID: INVALID; +export type DIRTY = { + status: "dirty"; + value: T; +}; +export declare const DIRTY: (value: T) => DIRTY; +export type OK = { + status: "valid"; + value: T; +}; +export declare const OK: (value: T) => OK; +export type SyncParseReturnType = OK | DIRTY | INVALID; +export type AsyncParseReturnType = Promise>; +export type ParseReturnType = SyncParseReturnType | AsyncParseReturnType; +export declare const isAborted: (x: ParseReturnType) => x is INVALID; +export declare const isDirty: (x: ParseReturnType) => x is OK | DIRTY; +export declare const isValid: (x: ParseReturnType) => x is OK; +export declare const isAsync: (x: ParseReturnType) => x is AsyncParseReturnType; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/partialUtil.d.ts b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/partialUtil.d.ts new file mode 100644 index 000000000..d07657a25 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/partialUtil.d.ts @@ -0,0 +1,8 @@ +import type { ZodArray, ZodNullable, ZodObject, ZodOptional, ZodRawShape, ZodTuple, ZodTupleItems, ZodTypeAny } from "../types.js"; +export declare namespace partialUtil { + type DeepPartial = T extends ZodObject ? ZodObject<{ + [k in keyof T["shape"]]: ZodOptional>; + }, T["_def"]["unknownKeys"], T["_def"]["catchall"]> : T extends ZodArray ? ZodArray, Card> : T extends ZodOptional ? ZodOptional> : T extends ZodNullable ? ZodNullable> : T extends ZodTuple ? { + [k in keyof Items]: Items[k] extends ZodTypeAny ? DeepPartial : never; + } extends infer PI ? PI extends ZodTupleItems ? ZodTuple : never : never : T; +} diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/typeAliases.d.ts b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/typeAliases.d.ts new file mode 100644 index 000000000..32df022ca --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/typeAliases.d.ts @@ -0,0 +1,2 @@ +export type Primitive = string | number | symbol | bigint | boolean | null | undefined; +export type Scalars = Primitive | Primitive[]; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/util.d.ts b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/util.d.ts new file mode 100644 index 000000000..eeaaeee1c --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/util.d.ts @@ -0,0 +1,85 @@ +export declare namespace util { + type AssertEqual = (() => V extends T ? 1 : 2) extends () => V extends U ? 1 : 2 ? true : false; + export type isAny = 0 extends 1 & T ? true : false; + export const assertEqual: (_: AssertEqual) => void; + export function assertIs(_arg: T): void; + export function assertNever(_x: never): never; + export type Omit = Pick>; + export type OmitKeys = Pick>; + export type MakePartial = Omit & Partial>; + export type Exactly = T & Record, never>; + export type InexactPartial = { + [k in keyof T]?: T[k] | undefined; + }; + export const arrayToEnum: (items: U) => { [k in U[number]]: k; }; + export const getValidEnumValues: (obj: any) => any[]; + export const objectValues: (obj: any) => any[]; + export const objectKeys: ObjectConstructor["keys"]; + export const find: (arr: T[], checker: (arg: T) => any) => T | undefined; + export type identity = objectUtil.identity; + export type flatten = objectUtil.flatten; + export type noUndefined = T extends undefined ? never : T; + export const isInteger: NumberConstructor["isInteger"]; + export function joinValues(array: T, separator?: string): string; + export const jsonStringifyReplacer: (_: string, value: any) => any; + export {}; +} +export declare namespace objectUtil { + export type MergeShapes = keyof U & keyof V extends never ? U & V : { + [k in Exclude]: U[k]; + } & V; + type optionalKeys = { + [k in keyof T]: undefined extends T[k] ? k : never; + }[keyof T]; + type requiredKeys = { + [k in keyof T]: undefined extends T[k] ? never : k; + }[keyof T]; + export type addQuestionMarks = { + [K in requiredKeys]: T[K]; + } & { + [K in optionalKeys]?: T[K]; + } & { + [k in keyof T]?: unknown; + }; + export type identity = T; + export type flatten = identity<{ + [k in keyof T]: T[k]; + }>; + export type noNeverKeys = { + [k in keyof T]: [T[k]] extends [never] ? never : k; + }[keyof T]; + export type noNever = identity<{ + [k in noNeverKeys]: k extends keyof T ? T[k] : never; + }>; + export const mergeShapes: (first: U, second: T) => T & U; + export type extendShape = keyof A & keyof B extends never ? A & B : { + [K in keyof A as K extends keyof B ? never : K]: A[K]; + } & { + [K in keyof B]: B[K]; + }; + export {}; +} +export declare const ZodParsedType: { + string: "string"; + nan: "nan"; + number: "number"; + integer: "integer"; + float: "float"; + boolean: "boolean"; + date: "date"; + bigint: "bigint"; + symbol: "symbol"; + function: "function"; + undefined: "undefined"; + null: "null"; + array: "array"; + object: "object"; + unknown: "unknown"; + promise: "promise"; + void: "void"; + never: "never"; + map: "map"; + set: "set"; +}; +export type ZodParsedType = keyof typeof ZodParsedType; +export declare const getParsedType: (data: any) => ZodParsedType; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/index.d.ts b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/index.d.ts new file mode 100644 index 000000000..a9dab5738 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/index.d.ts @@ -0,0 +1,4 @@ +import * as z from "./external.js"; +export * from "./external.js"; +export { z }; +export default z; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/locales/en.d.ts b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/locales/en.d.ts new file mode 100644 index 000000000..b2bea1ec4 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/locales/en.d.ts @@ -0,0 +1,3 @@ +import { type ZodErrorMap } from "../ZodError.js"; +declare const errorMap: ZodErrorMap; +export default errorMap; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/standard-schema.d.ts b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/standard-schema.d.ts new file mode 100644 index 000000000..7ddff3b7f --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/standard-schema.d.ts @@ -0,0 +1,102 @@ +/** + * The Standard Schema interface. + */ +export type StandardSchemaV1 = { + /** + * The Standard Schema properties. + */ + readonly "~standard": StandardSchemaV1.Props; +}; +export declare namespace StandardSchemaV1 { + /** + * The Standard Schema properties interface. + */ + export interface Props { + /** + * The version number of the standard. + */ + readonly version: 1; + /** + * The vendor name of the schema library. + */ + readonly vendor: string; + /** + * Validates unknown input values. + */ + readonly validate: (value: unknown) => Result | Promise>; + /** + * Inferred types associated with the schema. + */ + readonly types?: Types | undefined; + } + /** + * The result interface of the validate function. + */ + export type Result = SuccessResult | FailureResult; + /** + * The result interface if validation succeeds. + */ + export interface SuccessResult { + /** + * The typed output value. + */ + readonly value: Output; + /** + * The non-existent issues. + */ + readonly issues?: undefined; + } + /** + * The result interface if validation fails. + */ + export interface FailureResult { + /** + * The issues of failed validation. + */ + readonly issues: ReadonlyArray; + } + /** + * The issue interface of the failure output. + */ + export interface Issue { + /** + * The error message of the issue. + */ + readonly message: string; + /** + * The path of the issue, if any. + */ + readonly path?: ReadonlyArray | undefined; + } + /** + * The path segment interface of the issue. + */ + export interface PathSegment { + /** + * The key representing a path segment. + */ + readonly key: PropertyKey; + } + /** + * The Standard Schema types interface. + */ + export interface Types { + /** + * The input type of the schema. + */ + readonly input: Input; + /** + * The output type of the schema. + */ + readonly output: Output; + } + /** + * Infers the input type of a Standard Schema. + */ + export type InferInput = NonNullable["input"]; + /** + * Infers the output type of a Standard Schema. + */ + export type InferOutput = NonNullable["output"]; + export {}; +} diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/types.d.ts b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/types.d.ts new file mode 100644 index 000000000..301e09128 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/types.d.ts @@ -0,0 +1,1031 @@ +import { type IssueData, type StringValidation, type ZodCustomIssue, ZodError, type ZodErrorMap } from "./ZodError.js"; +import type { enumUtil } from "./helpers/enumUtil.js"; +import { errorUtil } from "./helpers/errorUtil.js"; +import { type AsyncParseReturnType, INVALID, type ParseContext, type ParseInput, type ParseParams, type ParseReturnType, ParseStatus, type SyncParseReturnType } from "./helpers/parseUtil.js"; +import type { partialUtil } from "./helpers/partialUtil.js"; +import type { Primitive } from "./helpers/typeAliases.js"; +import { util, type objectUtil } from "./helpers/util.js"; +import type { StandardSchemaV1 } from "./standard-schema.js"; +export interface RefinementCtx { + addIssue: (arg: IssueData) => void; + path: (string | number)[]; +} +export type ZodRawShape = { + [k: string]: ZodTypeAny; +}; +export type ZodTypeAny = ZodType; +export type TypeOf> = T["_output"]; +export type input> = T["_input"]; +export type output> = T["_output"]; +export type { TypeOf as infer }; +export type CustomErrorParams = Partial>; +export interface ZodTypeDef { + errorMap?: ZodErrorMap | undefined; + description?: string | undefined; +} +export type RawCreateParams = { + errorMap?: ZodErrorMap | undefined; + invalid_type_error?: string | undefined; + required_error?: string | undefined; + message?: string | undefined; + description?: string | undefined; +} | undefined; +export type ProcessedCreateParams = { + errorMap?: ZodErrorMap | undefined; + description?: string | undefined; +}; +export type SafeParseSuccess = { + success: true; + data: Output; + error?: never; +}; +export type SafeParseError = { + success: false; + error: ZodError; + data?: never; +}; +export type SafeParseReturnType = SafeParseSuccess | SafeParseError; +export declare abstract class ZodType { + readonly _type: Output; + readonly _output: Output; + readonly _input: Input; + readonly _def: Def; + get description(): string | undefined; + "~standard": StandardSchemaV1.Props; + abstract _parse(input: ParseInput): ParseReturnType; + _getType(input: ParseInput): string; + _getOrReturnCtx(input: ParseInput, ctx?: ParseContext | undefined): ParseContext; + _processInputParams(input: ParseInput): { + status: ParseStatus; + ctx: ParseContext; + }; + _parseSync(input: ParseInput): SyncParseReturnType; + _parseAsync(input: ParseInput): AsyncParseReturnType; + parse(data: unknown, params?: util.InexactPartial): Output; + safeParse(data: unknown, params?: util.InexactPartial): SafeParseReturnType; + "~validate"(data: unknown): StandardSchemaV1.Result | Promise>; + parseAsync(data: unknown, params?: util.InexactPartial): Promise; + safeParseAsync(data: unknown, params?: util.InexactPartial): Promise>; + /** Alias of safeParseAsync */ + spa: (data: unknown, params?: util.InexactPartial) => Promise>; + refine(check: (arg: Output) => arg is RefinedOutput, message?: string | CustomErrorParams | ((arg: Output) => CustomErrorParams)): ZodEffects; + refine(check: (arg: Output) => unknown | Promise, message?: string | CustomErrorParams | ((arg: Output) => CustomErrorParams)): ZodEffects; + refinement(check: (arg: Output) => arg is RefinedOutput, refinementData: IssueData | ((arg: Output, ctx: RefinementCtx) => IssueData)): ZodEffects; + refinement(check: (arg: Output) => boolean, refinementData: IssueData | ((arg: Output, ctx: RefinementCtx) => IssueData)): ZodEffects; + _refinement(refinement: RefinementEffect["refinement"]): ZodEffects; + superRefine(refinement: (arg: Output, ctx: RefinementCtx) => arg is RefinedOutput): ZodEffects; + superRefine(refinement: (arg: Output, ctx: RefinementCtx) => void): ZodEffects; + superRefine(refinement: (arg: Output, ctx: RefinementCtx) => Promise): ZodEffects; + constructor(def: Def); + optional(): ZodOptional; + nullable(): ZodNullable; + nullish(): ZodOptional>; + array(): ZodArray; + promise(): ZodPromise; + or(option: T): ZodUnion<[this, T]>; + and(incoming: T): ZodIntersection; + transform(transform: (arg: Output, ctx: RefinementCtx) => NewOut | Promise): ZodEffects; + default(def: util.noUndefined): ZodDefault; + default(def: () => util.noUndefined): ZodDefault; + brand(brand?: B): ZodBranded; + catch(def: Output): ZodCatch; + catch(def: (ctx: { + error: ZodError; + input: Input; + }) => Output): ZodCatch; + describe(description: string): this; + pipe(target: T): ZodPipeline; + readonly(): ZodReadonly; + isOptional(): boolean; + isNullable(): boolean; +} +export type IpVersion = "v4" | "v6"; +export type ZodStringCheck = { + kind: "min"; + value: number; + message?: string | undefined; +} | { + kind: "max"; + value: number; + message?: string | undefined; +} | { + kind: "length"; + value: number; + message?: string | undefined; +} | { + kind: "email"; + message?: string | undefined; +} | { + kind: "url"; + message?: string | undefined; +} | { + kind: "emoji"; + message?: string | undefined; +} | { + kind: "uuid"; + message?: string | undefined; +} | { + kind: "nanoid"; + message?: string | undefined; +} | { + kind: "cuid"; + message?: string | undefined; +} | { + kind: "includes"; + value: string; + position?: number | undefined; + message?: string | undefined; +} | { + kind: "cuid2"; + message?: string | undefined; +} | { + kind: "ulid"; + message?: string | undefined; +} | { + kind: "startsWith"; + value: string; + message?: string | undefined; +} | { + kind: "endsWith"; + value: string; + message?: string | undefined; +} | { + kind: "regex"; + regex: RegExp; + message?: string | undefined; +} | { + kind: "trim"; + message?: string | undefined; +} | { + kind: "toLowerCase"; + message?: string | undefined; +} | { + kind: "toUpperCase"; + message?: string | undefined; +} | { + kind: "jwt"; + alg?: string; + message?: string | undefined; +} | { + kind: "datetime"; + offset: boolean; + local: boolean; + precision: number | null; + message?: string | undefined; +} | { + kind: "date"; + message?: string | undefined; +} | { + kind: "time"; + precision: number | null; + message?: string | undefined; +} | { + kind: "duration"; + message?: string | undefined; +} | { + kind: "ip"; + version?: IpVersion | undefined; + message?: string | undefined; +} | { + kind: "cidr"; + version?: IpVersion | undefined; + message?: string | undefined; +} | { + kind: "base64"; + message?: string | undefined; +} | { + kind: "base64url"; + message?: string | undefined; +}; +export interface ZodStringDef extends ZodTypeDef { + checks: ZodStringCheck[]; + typeName: ZodFirstPartyTypeKind.ZodString; + coerce: boolean; +} +export declare function datetimeRegex(args: { + precision?: number | null; + offset?: boolean; + local?: boolean; +}): RegExp; +export declare class ZodString extends ZodType { + _parse(input: ParseInput): ParseReturnType; + protected _regex(regex: RegExp, validation: StringValidation, message?: errorUtil.ErrMessage): ZodEffects; + _addCheck(check: ZodStringCheck): ZodString; + email(message?: errorUtil.ErrMessage): ZodString; + url(message?: errorUtil.ErrMessage): ZodString; + emoji(message?: errorUtil.ErrMessage): ZodString; + uuid(message?: errorUtil.ErrMessage): ZodString; + nanoid(message?: errorUtil.ErrMessage): ZodString; + cuid(message?: errorUtil.ErrMessage): ZodString; + cuid2(message?: errorUtil.ErrMessage): ZodString; + ulid(message?: errorUtil.ErrMessage): ZodString; + base64(message?: errorUtil.ErrMessage): ZodString; + base64url(message?: errorUtil.ErrMessage): ZodString; + jwt(options?: { + alg?: string; + message?: string | undefined; + }): ZodString; + ip(options?: string | { + version?: IpVersion; + message?: string | undefined; + }): ZodString; + cidr(options?: string | { + version?: IpVersion; + message?: string | undefined; + }): ZodString; + datetime(options?: string | { + message?: string | undefined; + precision?: number | null; + offset?: boolean; + local?: boolean; + }): ZodString; + date(message?: string): ZodString; + time(options?: string | { + message?: string | undefined; + precision?: number | null; + }): ZodString; + duration(message?: errorUtil.ErrMessage): ZodString; + regex(regex: RegExp, message?: errorUtil.ErrMessage): ZodString; + includes(value: string, options?: { + message?: string; + position?: number; + }): ZodString; + startsWith(value: string, message?: errorUtil.ErrMessage): ZodString; + endsWith(value: string, message?: errorUtil.ErrMessage): ZodString; + min(minLength: number, message?: errorUtil.ErrMessage): ZodString; + max(maxLength: number, message?: errorUtil.ErrMessage): ZodString; + length(len: number, message?: errorUtil.ErrMessage): ZodString; + /** + * Equivalent to `.min(1)` + */ + nonempty(message?: errorUtil.ErrMessage): ZodString; + trim(): ZodString; + toLowerCase(): ZodString; + toUpperCase(): ZodString; + get isDatetime(): boolean; + get isDate(): boolean; + get isTime(): boolean; + get isDuration(): boolean; + get isEmail(): boolean; + get isURL(): boolean; + get isEmoji(): boolean; + get isUUID(): boolean; + get isNANOID(): boolean; + get isCUID(): boolean; + get isCUID2(): boolean; + get isULID(): boolean; + get isIP(): boolean; + get isCIDR(): boolean; + get isBase64(): boolean; + get isBase64url(): boolean; + get minLength(): number | null; + get maxLength(): number | null; + static create: (params?: RawCreateParams & { + coerce?: true; + }) => ZodString; +} +export type ZodNumberCheck = { + kind: "min"; + value: number; + inclusive: boolean; + message?: string | undefined; +} | { + kind: "max"; + value: number; + inclusive: boolean; + message?: string | undefined; +} | { + kind: "int"; + message?: string | undefined; +} | { + kind: "multipleOf"; + value: number; + message?: string | undefined; +} | { + kind: "finite"; + message?: string | undefined; +}; +export interface ZodNumberDef extends ZodTypeDef { + checks: ZodNumberCheck[]; + typeName: ZodFirstPartyTypeKind.ZodNumber; + coerce: boolean; +} +export declare class ZodNumber extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams & { + coerce?: boolean; + }) => ZodNumber; + gte(value: number, message?: errorUtil.ErrMessage): ZodNumber; + min: (value: number, message?: errorUtil.ErrMessage) => ZodNumber; + gt(value: number, message?: errorUtil.ErrMessage): ZodNumber; + lte(value: number, message?: errorUtil.ErrMessage): ZodNumber; + max: (value: number, message?: errorUtil.ErrMessage) => ZodNumber; + lt(value: number, message?: errorUtil.ErrMessage): ZodNumber; + protected setLimit(kind: "min" | "max", value: number, inclusive: boolean, message?: string): ZodNumber; + _addCheck(check: ZodNumberCheck): ZodNumber; + int(message?: errorUtil.ErrMessage): ZodNumber; + positive(message?: errorUtil.ErrMessage): ZodNumber; + negative(message?: errorUtil.ErrMessage): ZodNumber; + nonpositive(message?: errorUtil.ErrMessage): ZodNumber; + nonnegative(message?: errorUtil.ErrMessage): ZodNumber; + multipleOf(value: number, message?: errorUtil.ErrMessage): ZodNumber; + step: (value: number, message?: errorUtil.ErrMessage) => ZodNumber; + finite(message?: errorUtil.ErrMessage): ZodNumber; + safe(message?: errorUtil.ErrMessage): ZodNumber; + get minValue(): number | null; + get maxValue(): number | null; + get isInt(): boolean; + get isFinite(): boolean; +} +export type ZodBigIntCheck = { + kind: "min"; + value: bigint; + inclusive: boolean; + message?: string | undefined; +} | { + kind: "max"; + value: bigint; + inclusive: boolean; + message?: string | undefined; +} | { + kind: "multipleOf"; + value: bigint; + message?: string | undefined; +}; +export interface ZodBigIntDef extends ZodTypeDef { + checks: ZodBigIntCheck[]; + typeName: ZodFirstPartyTypeKind.ZodBigInt; + coerce: boolean; +} +export declare class ZodBigInt extends ZodType { + _parse(input: ParseInput): ParseReturnType; + _getInvalidInput(input: ParseInput): INVALID; + static create: (params?: RawCreateParams & { + coerce?: boolean; + }) => ZodBigInt; + gte(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + min: (value: bigint, message?: errorUtil.ErrMessage) => ZodBigInt; + gt(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + lte(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + max: (value: bigint, message?: errorUtil.ErrMessage) => ZodBigInt; + lt(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + protected setLimit(kind: "min" | "max", value: bigint, inclusive: boolean, message?: string): ZodBigInt; + _addCheck(check: ZodBigIntCheck): ZodBigInt; + positive(message?: errorUtil.ErrMessage): ZodBigInt; + negative(message?: errorUtil.ErrMessage): ZodBigInt; + nonpositive(message?: errorUtil.ErrMessage): ZodBigInt; + nonnegative(message?: errorUtil.ErrMessage): ZodBigInt; + multipleOf(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + get minValue(): bigint | null; + get maxValue(): bigint | null; +} +export interface ZodBooleanDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodBoolean; + coerce: boolean; +} +export declare class ZodBoolean extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams & { + coerce?: boolean; + }) => ZodBoolean; +} +export type ZodDateCheck = { + kind: "min"; + value: number; + message?: string | undefined; +} | { + kind: "max"; + value: number; + message?: string | undefined; +}; +export interface ZodDateDef extends ZodTypeDef { + checks: ZodDateCheck[]; + coerce: boolean; + typeName: ZodFirstPartyTypeKind.ZodDate; +} +export declare class ZodDate extends ZodType { + _parse(input: ParseInput): ParseReturnType; + _addCheck(check: ZodDateCheck): ZodDate; + min(minDate: Date, message?: errorUtil.ErrMessage): ZodDate; + max(maxDate: Date, message?: errorUtil.ErrMessage): ZodDate; + get minDate(): Date | null; + get maxDate(): Date | null; + static create: (params?: RawCreateParams & { + coerce?: boolean; + }) => ZodDate; +} +export interface ZodSymbolDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodSymbol; +} +export declare class ZodSymbol extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodSymbol; +} +export interface ZodUndefinedDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodUndefined; +} +export declare class ZodUndefined extends ZodType { + _parse(input: ParseInput): ParseReturnType; + params?: RawCreateParams; + static create: (params?: RawCreateParams) => ZodUndefined; +} +export interface ZodNullDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodNull; +} +export declare class ZodNull extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodNull; +} +export interface ZodAnyDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodAny; +} +export declare class ZodAny extends ZodType { + _any: true; + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodAny; +} +export interface ZodUnknownDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodUnknown; +} +export declare class ZodUnknown extends ZodType { + _unknown: true; + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodUnknown; +} +export interface ZodNeverDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodNever; +} +export declare class ZodNever extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodNever; +} +export interface ZodVoidDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodVoid; +} +export declare class ZodVoid extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodVoid; +} +export interface ZodArrayDef extends ZodTypeDef { + type: T; + typeName: ZodFirstPartyTypeKind.ZodArray; + exactLength: { + value: number; + message?: string | undefined; + } | null; + minLength: { + value: number; + message?: string | undefined; + } | null; + maxLength: { + value: number; + message?: string | undefined; + } | null; +} +export type ArrayCardinality = "many" | "atleastone"; +export type arrayOutputType = Cardinality extends "atleastone" ? [T["_output"], ...T["_output"][]] : T["_output"][]; +export declare class ZodArray extends ZodType, ZodArrayDef, Cardinality extends "atleastone" ? [T["_input"], ...T["_input"][]] : T["_input"][]> { + _parse(input: ParseInput): ParseReturnType; + get element(): T; + min(minLength: number, message?: errorUtil.ErrMessage): this; + max(maxLength: number, message?: errorUtil.ErrMessage): this; + length(len: number, message?: errorUtil.ErrMessage): this; + nonempty(message?: errorUtil.ErrMessage): ZodArray; + static create: (schema: El, params?: RawCreateParams) => ZodArray; +} +export type ZodNonEmptyArray = ZodArray; +export type UnknownKeysParam = "passthrough" | "strict" | "strip"; +export interface ZodObjectDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodObject; + shape: () => T; + catchall: Catchall; + unknownKeys: UnknownKeys; +} +export type mergeTypes = { + [k in keyof A | keyof B]: k extends keyof B ? B[k] : k extends keyof A ? A[k] : never; +}; +export type objectOutputType = objectUtil.flatten>> & CatchallOutput & PassthroughType; +export type baseObjectOutputType = { + [k in keyof Shape]: Shape[k]["_output"]; +}; +export type objectInputType = objectUtil.flatten> & CatchallInput & PassthroughType; +export type baseObjectInputType = objectUtil.addQuestionMarks<{ + [k in keyof Shape]: Shape[k]["_input"]; +}>; +export type CatchallOutput = ZodType extends T ? unknown : { + [k: string]: T["_output"]; +}; +export type CatchallInput = ZodType extends T ? unknown : { + [k: string]: T["_input"]; +}; +export type PassthroughType = T extends "passthrough" ? { + [k: string]: unknown; +} : unknown; +export type deoptional = T extends ZodOptional ? deoptional : T extends ZodNullable ? ZodNullable> : T; +export type SomeZodObject = ZodObject; +export type noUnrecognized = { + [k in keyof Obj]: k extends keyof Shape ? Obj[k] : never; +}; +export declare class ZodObject, Input = objectInputType> extends ZodType, Input> { + private _cached; + _getCached(): { + shape: T; + keys: string[]; + }; + _parse(input: ParseInput): ParseReturnType; + get shape(): T; + strict(message?: errorUtil.ErrMessage): ZodObject; + strip(): ZodObject; + passthrough(): ZodObject; + /** + * @deprecated In most cases, this is no longer needed - unknown properties are now silently stripped. + * If you want to pass through unknown properties, use `.passthrough()` instead. + */ + nonstrict: () => ZodObject; + extend(augmentation: Augmentation): ZodObject, UnknownKeys, Catchall>; + /** + * @deprecated Use `.extend` instead + * */ + augment: (augmentation: Augmentation) => ZodObject, UnknownKeys, Catchall>; + /** + * Prior to zod@1.0.12 there was a bug in the + * inferred type of merged objects. Please + * upgrade if you are experiencing issues. + */ + merge(merging: Incoming): ZodObject, Incoming["_def"]["unknownKeys"], Incoming["_def"]["catchall"]>; + setKey(key: Key, schema: Schema): ZodObject; + catchall(index: Index): ZodObject; + pick>(mask: Mask): ZodObject>, UnknownKeys, Catchall>; + omit>(mask: Mask): ZodObject, UnknownKeys, Catchall>; + /** + * @deprecated + */ + deepPartial(): partialUtil.DeepPartial; + partial(): ZodObject<{ + [k in keyof T]: ZodOptional; + }, UnknownKeys, Catchall>; + partial>(mask: Mask): ZodObject : T[k]; + }>, UnknownKeys, Catchall>; + required(): ZodObject<{ + [k in keyof T]: deoptional; + }, UnknownKeys, Catchall>; + required>(mask: Mask): ZodObject : T[k]; + }>, UnknownKeys, Catchall>; + keyof(): ZodEnum>; + static create: (shape: Shape, params?: RawCreateParams) => ZodObject, objectInputType>; + static strictCreate: (shape: Shape, params?: RawCreateParams) => ZodObject; + static lazycreate: (shape: () => Shape, params?: RawCreateParams) => ZodObject; +} +export type AnyZodObject = ZodObject; +export type ZodUnionOptions = Readonly<[ZodTypeAny, ...ZodTypeAny[]]>; +export interface ZodUnionDef> extends ZodTypeDef { + options: T; + typeName: ZodFirstPartyTypeKind.ZodUnion; +} +export declare class ZodUnion extends ZodType, T[number]["_input"]> { + _parse(input: ParseInput): ParseReturnType; + get options(): T; + static create: >(types: Options, params?: RawCreateParams) => ZodUnion; +} +export type ZodDiscriminatedUnionOption = ZodObject<{ + [key in Discriminator]: ZodTypeAny; +} & ZodRawShape, UnknownKeysParam, ZodTypeAny>; +export interface ZodDiscriminatedUnionDef[] = ZodDiscriminatedUnionOption[]> extends ZodTypeDef { + discriminator: Discriminator; + options: Options; + optionsMap: Map>; + typeName: ZodFirstPartyTypeKind.ZodDiscriminatedUnion; +} +export declare class ZodDiscriminatedUnion[]> extends ZodType, ZodDiscriminatedUnionDef, input> { + _parse(input: ParseInput): ParseReturnType; + get discriminator(): Discriminator; + get options(): Options; + get optionsMap(): Map>; + /** + * The constructor of the discriminated union schema. Its behaviour is very similar to that of the normal z.union() constructor. + * However, it only allows a union of objects, all of which need to share a discriminator property. This property must + * have a different value for each object in the union. + * @param discriminator the name of the discriminator property + * @param types an array of object schemas + * @param params + */ + static create, + ...ZodDiscriminatedUnionOption[] + ]>(discriminator: Discriminator, options: Types, params?: RawCreateParams): ZodDiscriminatedUnion; +} +export interface ZodIntersectionDef extends ZodTypeDef { + left: T; + right: U; + typeName: ZodFirstPartyTypeKind.ZodIntersection; +} +export declare class ZodIntersection extends ZodType, T["_input"] & U["_input"]> { + _parse(input: ParseInput): ParseReturnType; + static create: (left: TSchema, right: USchema, params?: RawCreateParams) => ZodIntersection; +} +export type ZodTupleItems = [ZodTypeAny, ...ZodTypeAny[]]; +export type AssertArray = T extends any[] ? T : never; +export type OutputTypeOfTuple = AssertArray<{ + [k in keyof T]: T[k] extends ZodType ? T[k]["_output"] : never; +}>; +export type OutputTypeOfTupleWithRest = Rest extends ZodTypeAny ? [...OutputTypeOfTuple, ...Rest["_output"][]] : OutputTypeOfTuple; +export type InputTypeOfTuple = AssertArray<{ + [k in keyof T]: T[k] extends ZodType ? T[k]["_input"] : never; +}>; +export type InputTypeOfTupleWithRest = Rest extends ZodTypeAny ? [...InputTypeOfTuple, ...Rest["_input"][]] : InputTypeOfTuple; +export interface ZodTupleDef extends ZodTypeDef { + items: T; + rest: Rest; + typeName: ZodFirstPartyTypeKind.ZodTuple; +} +export type AnyZodTuple = ZodTuple<[ZodTypeAny, ...ZodTypeAny[]] | [], ZodTypeAny | null>; +export declare class ZodTuple extends ZodType, ZodTupleDef, InputTypeOfTupleWithRest> { + _parse(input: ParseInput): ParseReturnType; + get items(): T; + rest(rest: RestSchema): ZodTuple; + static create: (schemas: Items, params?: RawCreateParams) => ZodTuple; +} +export interface ZodRecordDef extends ZodTypeDef { + valueType: Value; + keyType: Key; + typeName: ZodFirstPartyTypeKind.ZodRecord; +} +export type KeySchema = ZodType; +export type RecordType = [string] extends [K] ? Record : [number] extends [K] ? Record : [symbol] extends [K] ? Record : [BRAND] extends [K] ? Record : Partial>; +export declare class ZodRecord extends ZodType, ZodRecordDef, RecordType> { + get keySchema(): Key; + get valueSchema(): Value; + _parse(input: ParseInput): ParseReturnType; + get element(): Value; + static create(valueType: Value, params?: RawCreateParams): ZodRecord; + static create(keySchema: Keys, valueType: Value, params?: RawCreateParams): ZodRecord; +} +export interface ZodMapDef extends ZodTypeDef { + valueType: Value; + keyType: Key; + typeName: ZodFirstPartyTypeKind.ZodMap; +} +export declare class ZodMap extends ZodType, ZodMapDef, Map> { + get keySchema(): Key; + get valueSchema(): Value; + _parse(input: ParseInput): ParseReturnType; + static create: (keyType: KeySchema, valueType: ValueSchema, params?: RawCreateParams) => ZodMap; +} +export interface ZodSetDef extends ZodTypeDef { + valueType: Value; + typeName: ZodFirstPartyTypeKind.ZodSet; + minSize: { + value: number; + message?: string | undefined; + } | null; + maxSize: { + value: number; + message?: string | undefined; + } | null; +} +export declare class ZodSet extends ZodType, ZodSetDef, Set> { + _parse(input: ParseInput): ParseReturnType; + min(minSize: number, message?: errorUtil.ErrMessage): this; + max(maxSize: number, message?: errorUtil.ErrMessage): this; + size(size: number, message?: errorUtil.ErrMessage): this; + nonempty(message?: errorUtil.ErrMessage): ZodSet; + static create: (valueType: ValueSchema, params?: RawCreateParams) => ZodSet; +} +export interface ZodFunctionDef = ZodTuple, Returns extends ZodTypeAny = ZodTypeAny> extends ZodTypeDef { + args: Args; + returns: Returns; + typeName: ZodFirstPartyTypeKind.ZodFunction; +} +export type OuterTypeOfFunction, Returns extends ZodTypeAny> = Args["_input"] extends Array ? (...args: Args["_input"]) => Returns["_output"] : never; +export type InnerTypeOfFunction, Returns extends ZodTypeAny> = Args["_output"] extends Array ? (...args: Args["_output"]) => Returns["_input"] : never; +export declare class ZodFunction, Returns extends ZodTypeAny> extends ZodType, ZodFunctionDef, InnerTypeOfFunction> { + _parse(input: ParseInput): ParseReturnType; + parameters(): Args; + returnType(): Returns; + args[0]>(...items: Items): ZodFunction, Returns>; + returns>(returnType: NewReturnType): ZodFunction; + implement>(func: F): ReturnType extends Returns["_output"] ? (...args: Args["_input"]) => ReturnType : OuterTypeOfFunction; + strictImplement(func: InnerTypeOfFunction): InnerTypeOfFunction; + validate: >(func: F) => ReturnType extends Returns["_output"] ? (...args: Args["_input"]) => ReturnType : OuterTypeOfFunction; + static create(): ZodFunction, ZodUnknown>; + static create>(args: T): ZodFunction; + static create(args: T, returns: U): ZodFunction; + static create, U extends ZodTypeAny = ZodUnknown>(args: T, returns: U, params?: RawCreateParams): ZodFunction; +} +export interface ZodLazyDef extends ZodTypeDef { + getter: () => T; + typeName: ZodFirstPartyTypeKind.ZodLazy; +} +export declare class ZodLazy extends ZodType, ZodLazyDef, input> { + get schema(): T; + _parse(input: ParseInput): ParseReturnType; + static create: (getter: () => Inner, params?: RawCreateParams) => ZodLazy; +} +export interface ZodLiteralDef extends ZodTypeDef { + value: T; + typeName: ZodFirstPartyTypeKind.ZodLiteral; +} +export declare class ZodLiteral extends ZodType, T> { + _parse(input: ParseInput): ParseReturnType; + get value(): T; + static create: (value: Value, params?: RawCreateParams) => ZodLiteral; +} +export type ArrayKeys = keyof any[]; +export type Indices = Exclude; +export type EnumValues = readonly [T, ...T[]]; +export type Values = { + [k in T[number]]: k; +}; +export interface ZodEnumDef extends ZodTypeDef { + values: T; + typeName: ZodFirstPartyTypeKind.ZodEnum; +} +export type Writeable = { + -readonly [P in keyof T]: T[P]; +}; +export type FilterEnum = Values extends [] ? [] : Values extends [infer Head, ...infer Rest] ? Head extends ToExclude ? FilterEnum : [Head, ...FilterEnum] : never; +export type typecast = A extends T ? A : never; +declare function createZodEnum>(values: T, params?: RawCreateParams): ZodEnum>; +declare function createZodEnum(values: T, params?: RawCreateParams): ZodEnum; +export declare class ZodEnum extends ZodType, T[number]> { + _cache: Set | undefined; + _parse(input: ParseInput): ParseReturnType; + get options(): T; + get enum(): Values; + get Values(): Values; + get Enum(): Values; + extract(values: ToExtract, newDef?: RawCreateParams): ZodEnum>; + exclude(values: ToExclude, newDef?: RawCreateParams): ZodEnum>, [string, ...string[]]>>; + static create: typeof createZodEnum; +} +export interface ZodNativeEnumDef extends ZodTypeDef { + values: T; + typeName: ZodFirstPartyTypeKind.ZodNativeEnum; +} +export type EnumLike = { + [k: string]: string | number; + [nu: number]: string; +}; +export declare class ZodNativeEnum extends ZodType, T[keyof T]> { + _cache: Set | undefined; + _parse(input: ParseInput): ParseReturnType; + get enum(): T; + static create: (values: Elements, params?: RawCreateParams) => ZodNativeEnum; +} +export interface ZodPromiseDef extends ZodTypeDef { + type: T; + typeName: ZodFirstPartyTypeKind.ZodPromise; +} +export declare class ZodPromise extends ZodType, ZodPromiseDef, Promise> { + unwrap(): T; + _parse(input: ParseInput): ParseReturnType; + static create: (schema: Inner, params?: RawCreateParams) => ZodPromise; +} +export type Refinement = (arg: T, ctx: RefinementCtx) => any; +export type SuperRefinement = (arg: T, ctx: RefinementCtx) => void | Promise; +export type RefinementEffect = { + type: "refinement"; + refinement: (arg: T, ctx: RefinementCtx) => any; +}; +export type TransformEffect = { + type: "transform"; + transform: (arg: T, ctx: RefinementCtx) => any; +}; +export type PreprocessEffect = { + type: "preprocess"; + transform: (arg: T, ctx: RefinementCtx) => any; +}; +export type Effect = RefinementEffect | TransformEffect | PreprocessEffect; +export interface ZodEffectsDef extends ZodTypeDef { + schema: T; + typeName: ZodFirstPartyTypeKind.ZodEffects; + effect: Effect; +} +export declare class ZodEffects, Input = input> extends ZodType, Input> { + innerType(): T; + sourceType(): T; + _parse(input: ParseInput): ParseReturnType; + static create: (schema: I, effect: Effect, params?: RawCreateParams) => ZodEffects; + static createWithPreprocess: (preprocess: (arg: unknown, ctx: RefinementCtx) => unknown, schema: I, params?: RawCreateParams) => ZodEffects; +} +export { ZodEffects as ZodTransformer }; +export interface ZodOptionalDef extends ZodTypeDef { + innerType: T; + typeName: ZodFirstPartyTypeKind.ZodOptional; +} +export type ZodOptionalType = ZodOptional; +export declare class ZodOptional extends ZodType, T["_input"] | undefined> { + _parse(input: ParseInput): ParseReturnType; + unwrap(): T; + static create: (type: Inner, params?: RawCreateParams) => ZodOptional; +} +export interface ZodNullableDef extends ZodTypeDef { + innerType: T; + typeName: ZodFirstPartyTypeKind.ZodNullable; +} +export type ZodNullableType = ZodNullable; +export declare class ZodNullable extends ZodType, T["_input"] | null> { + _parse(input: ParseInput): ParseReturnType; + unwrap(): T; + static create: (type: Inner, params?: RawCreateParams) => ZodNullable; +} +export interface ZodDefaultDef extends ZodTypeDef { + innerType: T; + defaultValue: () => util.noUndefined; + typeName: ZodFirstPartyTypeKind.ZodDefault; +} +export declare class ZodDefault extends ZodType, ZodDefaultDef, T["_input"] | undefined> { + _parse(input: ParseInput): ParseReturnType; + removeDefault(): T; + static create: (type: Inner, params: RawCreateParams & { + default: Inner["_input"] | (() => util.noUndefined); + }) => ZodDefault; +} +export interface ZodCatchDef extends ZodTypeDef { + innerType: T; + catchValue: (ctx: { + error: ZodError; + input: unknown; + }) => T["_input"]; + typeName: ZodFirstPartyTypeKind.ZodCatch; +} +export declare class ZodCatch extends ZodType, unknown> { + _parse(input: ParseInput): ParseReturnType; + removeCatch(): T; + static create: (type: Inner, params: RawCreateParams & { + catch: Inner["_output"] | (() => Inner["_output"]); + }) => ZodCatch; +} +export interface ZodNaNDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodNaN; +} +export declare class ZodNaN extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodNaN; +} +export interface ZodBrandedDef extends ZodTypeDef { + type: T; + typeName: ZodFirstPartyTypeKind.ZodBranded; +} +export declare const BRAND: unique symbol; +export type BRAND = { + [BRAND]: { + [k in T]: true; + }; +}; +export declare class ZodBranded extends ZodType, ZodBrandedDef, T["_input"]> { + _parse(input: ParseInput): ParseReturnType; + unwrap(): T; +} +export interface ZodPipelineDef extends ZodTypeDef { + in: A; + out: B; + typeName: ZodFirstPartyTypeKind.ZodPipeline; +} +export declare class ZodPipeline extends ZodType, A["_input"]> { + _parse(input: ParseInput): ParseReturnType; + static create(a: ASchema, b: BSchema): ZodPipeline; +} +type BuiltIn = (((...args: any[]) => any) | (new (...args: any[]) => any)) | { + readonly [Symbol.toStringTag]: string; +} | Date | Error | Generator | Promise | RegExp; +type MakeReadonly = T extends Map ? ReadonlyMap : T extends Set ? ReadonlySet : T extends [infer Head, ...infer Tail] ? readonly [Head, ...Tail] : T extends Array ? ReadonlyArray : T extends BuiltIn ? T : Readonly; +export interface ZodReadonlyDef extends ZodTypeDef { + innerType: T; + typeName: ZodFirstPartyTypeKind.ZodReadonly; +} +export declare class ZodReadonly extends ZodType, ZodReadonlyDef, MakeReadonly> { + _parse(input: ParseInput): ParseReturnType; + static create: (type: Inner, params?: RawCreateParams) => ZodReadonly; + unwrap(): T; +} +type CustomParams = CustomErrorParams & { + fatal?: boolean; +}; +export declare function custom(check?: (data: any) => any, _params?: string | CustomParams | ((input: any) => CustomParams), +/** + * @deprecated + * + * Pass `fatal` into the params object instead: + * + * ```ts + * z.string().custom((val) => val.length > 5, { fatal: false }) + * ``` + * + */ +fatal?: boolean): ZodType; +export { ZodType as Schema, ZodType as ZodSchema }; +export declare const late: { + object: (shape: () => Shape, params?: RawCreateParams) => ZodObject; +}; +export declare enum ZodFirstPartyTypeKind { + ZodString = "ZodString", + ZodNumber = "ZodNumber", + ZodNaN = "ZodNaN", + ZodBigInt = "ZodBigInt", + ZodBoolean = "ZodBoolean", + ZodDate = "ZodDate", + ZodSymbol = "ZodSymbol", + ZodUndefined = "ZodUndefined", + ZodNull = "ZodNull", + ZodAny = "ZodAny", + ZodUnknown = "ZodUnknown", + ZodNever = "ZodNever", + ZodVoid = "ZodVoid", + ZodArray = "ZodArray", + ZodObject = "ZodObject", + ZodUnion = "ZodUnion", + ZodDiscriminatedUnion = "ZodDiscriminatedUnion", + ZodIntersection = "ZodIntersection", + ZodTuple = "ZodTuple", + ZodRecord = "ZodRecord", + ZodMap = "ZodMap", + ZodSet = "ZodSet", + ZodFunction = "ZodFunction", + ZodLazy = "ZodLazy", + ZodLiteral = "ZodLiteral", + ZodEnum = "ZodEnum", + ZodEffects = "ZodEffects", + ZodNativeEnum = "ZodNativeEnum", + ZodOptional = "ZodOptional", + ZodNullable = "ZodNullable", + ZodDefault = "ZodDefault", + ZodCatch = "ZodCatch", + ZodPromise = "ZodPromise", + ZodBranded = "ZodBranded", + ZodPipeline = "ZodPipeline", + ZodReadonly = "ZodReadonly" +} +export type ZodFirstPartySchemaTypes = ZodString | ZodNumber | ZodNaN | ZodBigInt | ZodBoolean | ZodDate | ZodUndefined | ZodNull | ZodAny | ZodUnknown | ZodNever | ZodVoid | ZodArray | ZodObject | ZodUnion | ZodDiscriminatedUnion | ZodIntersection | ZodTuple | ZodRecord | ZodMap | ZodSet | ZodFunction | ZodLazy | ZodLiteral | ZodEnum | ZodEffects | ZodNativeEnum | ZodOptional | ZodNullable | ZodDefault | ZodCatch | ZodPromise | ZodBranded | ZodPipeline | ZodReadonly | ZodSymbol; +declare abstract class Class { + constructor(..._: any[]); +} +declare const instanceOfType: (cls: T, params?: CustomParams) => ZodType, ZodTypeDef, InstanceType>; +declare const stringType: (params?: RawCreateParams & { + coerce?: true; +}) => ZodString; +declare const numberType: (params?: RawCreateParams & { + coerce?: boolean; +}) => ZodNumber; +declare const nanType: (params?: RawCreateParams) => ZodNaN; +declare const bigIntType: (params?: RawCreateParams & { + coerce?: boolean; +}) => ZodBigInt; +declare const booleanType: (params?: RawCreateParams & { + coerce?: boolean; +}) => ZodBoolean; +declare const dateType: (params?: RawCreateParams & { + coerce?: boolean; +}) => ZodDate; +declare const symbolType: (params?: RawCreateParams) => ZodSymbol; +declare const undefinedType: (params?: RawCreateParams) => ZodUndefined; +declare const nullType: (params?: RawCreateParams) => ZodNull; +declare const anyType: (params?: RawCreateParams) => ZodAny; +declare const unknownType: (params?: RawCreateParams) => ZodUnknown; +declare const neverType: (params?: RawCreateParams) => ZodNever; +declare const voidType: (params?: RawCreateParams) => ZodVoid; +declare const arrayType: (schema: El, params?: RawCreateParams) => ZodArray; +declare const objectType: (shape: Shape, params?: RawCreateParams) => ZodObject, objectInputType>; +declare const strictObjectType: (shape: Shape, params?: RawCreateParams) => ZodObject; +declare const unionType: >(types: Options, params?: RawCreateParams) => ZodUnion; +declare const discriminatedUnionType: typeof ZodDiscriminatedUnion.create; +declare const intersectionType: (left: TSchema, right: USchema, params?: RawCreateParams) => ZodIntersection; +declare const tupleType: (schemas: Items, params?: RawCreateParams) => ZodTuple; +declare const recordType: typeof ZodRecord.create; +declare const mapType: (keyType: KeySchema, valueType: ValueSchema, params?: RawCreateParams) => ZodMap; +declare const setType: (valueType: ValueSchema, params?: RawCreateParams) => ZodSet; +declare const functionType: typeof ZodFunction.create; +declare const lazyType: (getter: () => Inner, params?: RawCreateParams) => ZodLazy; +declare const literalType: (value: Value, params?: RawCreateParams) => ZodLiteral; +declare const enumType: typeof createZodEnum; +declare const nativeEnumType: (values: Elements, params?: RawCreateParams) => ZodNativeEnum; +declare const promiseType: (schema: Inner, params?: RawCreateParams) => ZodPromise; +declare const effectsType: (schema: I, effect: Effect, params?: RawCreateParams) => ZodEffects; +declare const optionalType: (type: Inner, params?: RawCreateParams) => ZodOptional; +declare const nullableType: (type: Inner, params?: RawCreateParams) => ZodNullable; +declare const preprocessType: (preprocess: (arg: unknown, ctx: RefinementCtx) => unknown, schema: I, params?: RawCreateParams) => ZodEffects; +declare const pipelineType: typeof ZodPipeline.create; +declare const ostring: () => ZodOptional; +declare const onumber: () => ZodOptional; +declare const oboolean: () => ZodOptional; +export declare const coerce: { + string: (typeof ZodString)["create"]; + number: (typeof ZodNumber)["create"]; + boolean: (typeof ZodBoolean)["create"]; + bigint: (typeof ZodBigInt)["create"]; + date: (typeof ZodDate)["create"]; +}; +export { anyType as any, arrayType as array, bigIntType as bigint, booleanType as boolean, dateType as date, discriminatedUnionType as discriminatedUnion, effectsType as effect, enumType as enum, functionType as function, instanceOfType as instanceof, intersectionType as intersection, lazyType as lazy, literalType as literal, mapType as map, nanType as nan, nativeEnumType as nativeEnum, neverType as never, nullType as null, nullableType as nullable, numberType as number, objectType as object, oboolean, onumber, optionalType as optional, ostring, pipelineType as pipeline, preprocessType as preprocess, promiseType as promise, recordType as record, setType as set, strictObjectType as strictObject, stringType as string, symbolType as symbol, effectsType as transformer, tupleType as tuple, undefinedType as undefined, unionType as union, unknownType as unknown, voidType as void, }; +export declare const NEVER: never; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/package.json b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/package.json new file mode 100644 index 000000000..e02b87614 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/package.json @@ -0,0 +1,120 @@ +{ + "name": "zod", + "version": "3.25.67", + "type": "module", + "author": "Colin McDonnell ", + "description": "TypeScript-first schema declaration and validation library with static type inference", + "files": ["dist", "v3", "v4", "v4-mini"], + "funding": "https://github.com/sponsors/colinhacks", + "homepage": "https://zod.dev", + "keywords": ["typescript", "schema", "validation", "type", "inference"], + "license": "MIT", + "sideEffects": false, + "main": "./dist/cjs/index.js", + "types": "./dist/types/index.d.ts", + "module": "./dist/esm/index.js", + "exports": { + ".": { + "import": { + "@zod/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "@zod/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "default": "./dist/cjs/index.js" + } + }, + "./v3": { + "import": { + "@zod/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "@zod/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "default": "./dist/cjs/index.js" + } + }, + "./v4": { + "import": { + "@zod/source": "./src/v4/index.ts", + "types": "./dist/types/v4/index.d.ts", + "default": "./dist/esm/v4/index.js" + }, + "require": { + "@zod/source": "./src/v4/index.ts", + "types": "./dist/types/v4/index.d.ts", + "default": "./dist/cjs/v4/index.js" + } + }, + "./v4-mini": { + "@zod/source": "./src/v4/mini/index.ts", + "types": "./dist/types/v4/mini/index.d.ts", + "import": "./dist/esm/v4/mini/index.js", + "require": "./dist/cjs/v4/mini/index.js" + }, + "./v4/mini": { + "@zod/source": "./src/v4/mini/index.ts", + "types": "./dist/types/v4/mini/index.d.ts", + "import": "./dist/esm/v4/mini/index.js", + "require": "./dist/cjs/v4/mini/index.js" + }, + "./v4/core": { + "@zod/source": "./src/v4/core/index.ts", + "types": "./dist/types/v4/core/index.d.ts", + "import": "./dist/esm/v4/core/index.js", + "require": "./dist/cjs/v4/core/index.js" + }, + "./v4/locales": { + "@zod/source": "./src/v4/locales/index.ts", + "types": "./dist/types/v4/locales/index.d.ts", + "import": "./dist/esm/v4/locales/index.js", + "require": "./dist/cjs/v4/locales/index.js" + }, + "./v4/locales/*": { + "@zod/source": "./src/v4/locales/*", + "types": "./dist/types/v4/locales/*", + "import": "./dist/esm/v4/locales/*", + "require": "./dist/cjs/v4/locales/*" + }, + "./package.json": "./package.json" + }, + "tshy": { + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./v3": "./src/v3/index.ts", + "./v4": "./src/v4/index.ts", + "./v4-mini": "./src/v4/mini/index.ts", + "./v4/core": "./src/v4/core/index.ts", + "./v4/locales": "./src/v4/locales/index.ts", + "./v4/locales/*": "./src/v4/locales/*" + }, + "sourceDialects": ["@zod/source"], + "exclude": ["./src/**/*.test.ts"] + }, + "repository": { + "type": "git", + "url": "git+https://github.com/colinhacks/zod.git" + }, + "bugs": { + "url": "https://github.com/colinhacks/zod/issues" + }, + "support": { + "backing": { + "npm-funding": true + } + }, + "scripts": { + "clean": "rm -rf dist", + "build": "tsx ./build.mts", + "postbuild": "pnpm biome check --write .", + "test:watch": "pnpm vitest", + "test": "pnpm vitest run", + "bump:beta": "pnpm version \"v$(pnpm pkg get version | jq -r)-beta.$(date +%Y%m%dT%H%M%S)\"", + "pub:beta": "pnpm bump:beta && pnpm publish --tag next --publish-branch v4 --no-git-checks --dry-run" + } +} diff --git a/tests/fixtures/npm/cases/zod3-boundary/package.json b/tests/fixtures/npm/cases/zod3-boundary/package.json new file mode 100644 index 000000000..be0bf47db --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/package.json @@ -0,0 +1 @@ +{ "name": "zod3-boundary-case", "private": true, "type": "module" } diff --git a/tests/fixtures/npm/cases/zod3-boundary/rewrites.ts b/tests/fixtures/npm/cases/zod3-boundary/rewrites.ts new file mode 100644 index 000000000..7a98cccf0 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/rewrites.ts @@ -0,0 +1,21 @@ +// Rewrites the declaration-typed boundary makes in user code: a typed +// parameter of a bare `async` arrow zod calls, and a cast at the start of +// an unbraced loop body. +import { z } from "zod"; + +const Box = z.object({ n: z.number() }); +const raw: unknown = { n: 2 }; + +// The cast opens the loop body's statement; the loop runs it each time. +let seen = 0; +for (let i = 0; i < 3; i++) Box.parse(raw).n > 0 && seen++; +let left = 2; +while (left-- > 0) Box.parse(raw).n > 1 && seen++; +console.log("loops", seen); + +// zod calls the callback with the string it checked. +const Length = z.string().transform(async s => s.length + 1); +async function main(): Promise { + console.log("async arrow", await Length.parseAsync("abc")); +} +void main(); diff --git a/tests/fixtures/npm/cases/zod3-infer/main.ts b/tests/fixtures/npm/cases/zod3-infer/main.ts new file mode 100644 index 000000000..49029c38e --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/main.ts @@ -0,0 +1,118 @@ +// Types derived from zod 3.25 schemas (z.infer, z.input, z.output) used as +// ordinary typed data: field reads, spreads, interfaces extending inferred +// types, unions and discriminated unions narrowed by their tags, arrays, +// optional and nullable fields, transforms and defaults, and .extend. +import { z } from "zod"; + +const show = (label: string, value: unknown): void => { + console.log(label + " " + JSON.stringify(value)); +}; + +// Objects, and interfaces that extend an inferred type. +const ProductInput = z.object({ + name: z.string().min(1), + priceCents: z.number().int().nonnegative(), + tags: z.array(z.string()).default([]), +}); +type ProductInput = z.infer; +interface Product extends ProductInput { + id: string; +} + +function makeProduct(id: string, input: ProductInput): Product { + return { id, ...input }; +} +const parsed = ProductInput.parse({ name: "Keyboard", priceCents: 12900, tags: ["input"] }); +const product = makeProduct("p1", parsed); +show("product", product); +console.log("name", product.name, "price", product.priceCents, "tags", product.tags.join("|")); +const products: Product[] = [product, makeProduct("p2", ProductInput.parse({ name: "Cable", priceCents: 900 }))]; +console.log("tagged", products.filter((p) => p.tags.includes("input")).map((p) => p.name).join(",")); +let total = 0; +for (const p of products) total += p.priceCents; +console.log("total", total); + +// safeParse data spread into a record with more fields. +const OrderInput = z.object({ + productId: z.string(), + quantity: z.coerce.number().int().min(1), + email: z.string().email(), +}); +type OrderInput = z.infer; +interface Order extends OrderInput { + id: string; + totalCents: number; +} +function placeOrder(raw: unknown): Order | string { + const result = OrderInput.safeParse(raw); + if (!result.success) return result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; "); + return { id: "o1", ...result.data, totalCents: result.data.quantity * 4900 }; +} +show("order", placeOrder({ productId: "p2", quantity: "3", email: "a@b.co" })); +show("bad order", placeOrder({ productId: "p2", quantity: "0", email: "nope" })); + +// Optional and nullable fields. +const Profile = z.object({ + handle: z.string(), + bio: z.string().optional(), + avatar: z.string().nullable(), + age: z.number().optional().nullable(), +}); +type Profile = z.infer; +function describe(p: Profile): string { + const bio = p.bio ?? "(no bio)"; + const avatar = p.avatar === null ? "(no avatar)" : p.avatar; + const age = p.age == null ? "?" : String(p.age); + return `${p.handle}: ${bio}, ${avatar}, ${age}`; +} +console.log(describe(Profile.parse({ handle: "ada", avatar: null }))); +console.log(describe(Profile.parse({ handle: "bob", bio: "hi", avatar: "b.png", age: 42 }))); + +// Unions and discriminated unions narrowed by their tag. +const Shape = z.discriminatedUnion("kind", [ + z.object({ kind: z.literal("circle"), radius: z.number() }), + z.object({ kind: z.literal("rect"), width: z.number(), height: z.number() }), +]); +type Shape = z.infer; +function area(s: Shape): number { + switch (s.kind) { + case "circle": + return Math.round(Math.PI * s.radius * s.radius); + case "rect": + return s.width * s.height; + } +} +const shapes: Shape[] = z.array(Shape).parse([ + { kind: "circle", radius: 2 }, + { kind: "rect", width: 3, height: 4 }, +]); +console.log("areas", shapes.map(area).join(",")); + +const IdOrName = z.union([z.number(), z.string()]); +type IdOrName = z.infer; +const label = (v: IdOrName): string => (typeof v === "number" ? `#${v}` : v.toUpperCase()); +console.log("labels", [IdOrName.parse(7), IdOrName.parse("x")].map(label).join(",")); + +// Transforms and defaults: input and output types differ. +const Settings = z.object({ + port: z.string().transform((s) => Number(s)), + mode: z.enum(["dev", "prod"]).default("dev"), +}); +type SettingsIn = z.input; +type SettingsOut = z.output; +const rawSettings: SettingsIn = { port: "8080" }; +const settings: SettingsOut = Settings.parse(rawSettings); +console.log("settings", settings.port + 1, settings.mode, typeof settings.port, rawSettings.port.length); + +// .extend and arrays of inferred records. +const Base = z.object({ id: z.string(), createdAt: z.string() }); +const User = Base.extend({ email: z.string().email(), roles: z.array(z.enum(["admin", "user"])) }); +type User = z.infer; +const users: User[] = z.array(User).parse([ + { id: "u1", createdAt: "2026-01-01", email: "a@b.co", roles: ["admin", "user"] }, + { id: "u2", createdAt: "2026-01-02", email: "c@d.co", roles: ["user"] }, +]); +const admins = users.filter((u) => u.roles.includes("admin")).map((u) => `${u.id}<${u.email}>`); +console.log("admins", admins.join(",")); +const renamed = users.map((u) => ({ ...u, id: u.id.toUpperCase() })); +show("renamed", renamed); diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/LICENSE b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/LICENSE new file mode 100644 index 000000000..c06579627 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Colin McDonnell + +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/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/index.js b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/index.js new file mode 100644 index 000000000..8352ab713 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/index.js @@ -0,0 +1,3 @@ +import z3 from "./v3/index.js"; +export * from "./v3/index.js"; +export default z3; diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/package.json b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/package.json new file mode 100644 index 000000000..aead43de3 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} \ No newline at end of file diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/ZodError.js b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/ZodError.js new file mode 100644 index 000000000..c72ce5288 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/ZodError.js @@ -0,0 +1,132 @@ +import { util } from "./helpers/util.js"; +export const ZodIssueCode = util.arrayToEnum([ + "invalid_type", + "invalid_literal", + "custom", + "invalid_union", + "invalid_union_discriminator", + "invalid_enum_value", + "unrecognized_keys", + "invalid_arguments", + "invalid_return_type", + "invalid_date", + "invalid_string", + "too_small", + "too_big", + "invalid_intersection_types", + "not_multiple_of", + "not_finite", +]); +export const quotelessJson = (obj) => { + const json = JSON.stringify(obj, null, 2); + return json.replace(/"([^"]+)":/g, "$1:"); +}; +export class ZodError extends Error { + get errors() { + return this.issues; + } + constructor(issues) { + super(); + this.issues = []; + this.addIssue = (sub) => { + this.issues = [...this.issues, sub]; + }; + this.addIssues = (subs = []) => { + this.issues = [...this.issues, ...subs]; + }; + const actualProto = new.target.prototype; + if (Object.setPrototypeOf) { + // eslint-disable-next-line ban/ban + Object.setPrototypeOf(this, actualProto); + } + else { + this.__proto__ = actualProto; + } + this.name = "ZodError"; + this.issues = issues; + } + format(_mapper) { + const mapper = _mapper || + function (issue) { + return issue.message; + }; + const fieldErrors = { _errors: [] }; + const processError = (error) => { + for (const issue of error.issues) { + if (issue.code === "invalid_union") { + issue.unionErrors.map(processError); + } + else if (issue.code === "invalid_return_type") { + processError(issue.returnTypeError); + } + else if (issue.code === "invalid_arguments") { + processError(issue.argumentsError); + } + else if (issue.path.length === 0) { + fieldErrors._errors.push(mapper(issue)); + } + else { + let curr = fieldErrors; + let i = 0; + while (i < issue.path.length) { + const el = issue.path[i]; + const terminal = i === issue.path.length - 1; + if (!terminal) { + curr[el] = curr[el] || { _errors: [] }; + // if (typeof el === "string") { + // curr[el] = curr[el] || { _errors: [] }; + // } else if (typeof el === "number") { + // const errorArray: any = []; + // errorArray._errors = []; + // curr[el] = curr[el] || errorArray; + // } + } + else { + curr[el] = curr[el] || { _errors: [] }; + curr[el]._errors.push(mapper(issue)); + } + curr = curr[el]; + i++; + } + } + } + }; + processError(this); + return fieldErrors; + } + static assert(value) { + if (!(value instanceof ZodError)) { + throw new Error(`Not a ZodError: ${value}`); + } + } + toString() { + return this.message; + } + get message() { + return JSON.stringify(this.issues, util.jsonStringifyReplacer, 2); + } + get isEmpty() { + return this.issues.length === 0; + } + flatten(mapper = (issue) => issue.message) { + const fieldErrors = {}; + const formErrors = []; + for (const sub of this.issues) { + if (sub.path.length > 0) { + fieldErrors[sub.path[0]] = fieldErrors[sub.path[0]] || []; + fieldErrors[sub.path[0]].push(mapper(sub)); + } + else { + formErrors.push(mapper(sub)); + } + } + return { formErrors, fieldErrors }; + } + get formErrors() { + return this.flatten(); + } +} +ZodError.create = (issues) => { + const error = new ZodError(issues); + return error; +}; diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/errors.js b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/errors.js new file mode 100644 index 000000000..26fdf628a --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/errors.js @@ -0,0 +1,9 @@ +import defaultErrorMap from "./locales/en.js"; +let overrideErrorMap = defaultErrorMap; +export { defaultErrorMap }; +export function setErrorMap(map) { + overrideErrorMap = map; +} +export function getErrorMap() { + return overrideErrorMap; +} diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/external.js b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/external.js new file mode 100644 index 000000000..f0a4be4c7 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/external.js @@ -0,0 +1,6 @@ +export * from "./errors.js"; +export * from "./helpers/parseUtil.js"; +export * from "./helpers/typeAliases.js"; +export * from "./helpers/util.js"; +export * from "./types.js"; +export * from "./ZodError.js"; diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/helpers/enumUtil.js b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/helpers/enumUtil.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/helpers/enumUtil.js @@ -0,0 +1 @@ +export {}; diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/helpers/errorUtil.js b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/helpers/errorUtil.js new file mode 100644 index 000000000..e1640d1a4 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/helpers/errorUtil.js @@ -0,0 +1,6 @@ +export var errorUtil; +(function (errorUtil) { + errorUtil.errToObj = (message) => typeof message === "string" ? { message } : message || {}; + // biome-ignore lint: + errorUtil.toString = (message) => typeof message === "string" ? message : message?.message; +})(errorUtil || (errorUtil = {})); diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/helpers/parseUtil.js b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/helpers/parseUtil.js new file mode 100644 index 000000000..91b2afb81 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/helpers/parseUtil.js @@ -0,0 +1,109 @@ +import { getErrorMap } from "../errors.js"; +import defaultErrorMap from "../locales/en.js"; +export const makeIssue = (params) => { + const { data, path, errorMaps, issueData } = params; + const fullPath = [...path, ...(issueData.path || [])]; + const fullIssue = { + ...issueData, + path: fullPath, + }; + if (issueData.message !== undefined) { + return { + ...issueData, + path: fullPath, + message: issueData.message, + }; + } + let errorMessage = ""; + const maps = errorMaps + .filter((m) => !!m) + .slice() + .reverse(); + for (const map of maps) { + errorMessage = map(fullIssue, { data, defaultError: errorMessage }).message; + } + return { + ...issueData, + path: fullPath, + message: errorMessage, + }; +}; +export const EMPTY_PATH = []; +export function addIssueToContext(ctx, issueData) { + const overrideMap = getErrorMap(); + const issue = makeIssue({ + issueData: issueData, + data: ctx.data, + path: ctx.path, + errorMaps: [ + ctx.common.contextualErrorMap, // contextual error map is first priority + ctx.schemaErrorMap, // then schema-bound map if available + overrideMap, // then global override map + overrideMap === defaultErrorMap ? undefined : defaultErrorMap, // then global default map + ].filter((x) => !!x), + }); + ctx.common.issues.push(issue); +} +export class ParseStatus { + constructor() { + this.value = "valid"; + } + dirty() { + if (this.value === "valid") + this.value = "dirty"; + } + abort() { + if (this.value !== "aborted") + this.value = "aborted"; + } + static mergeArray(status, results) { + const arrayValue = []; + for (const s of results) { + if (s.status === "aborted") + return INVALID; + if (s.status === "dirty") + status.dirty(); + arrayValue.push(s.value); + } + return { status: status.value, value: arrayValue }; + } + static async mergeObjectAsync(status, pairs) { + const syncPairs = []; + for (const pair of pairs) { + const key = await pair.key; + const value = await pair.value; + syncPairs.push({ + key, + value, + }); + } + return ParseStatus.mergeObjectSync(status, syncPairs); + } + static mergeObjectSync(status, pairs) { + const finalObject = {}; + for (const pair of pairs) { + const { key, value } = pair; + if (key.status === "aborted") + return INVALID; + if (value.status === "aborted") + return INVALID; + if (key.status === "dirty") + status.dirty(); + if (value.status === "dirty") + status.dirty(); + if (key.value !== "__proto__" && (typeof value.value !== "undefined" || pair.alwaysSet)) { + finalObject[key.value] = value.value; + } + } + return { status: status.value, value: finalObject }; + } +} +export const INVALID = Object.freeze({ + status: "aborted", +}); +export const DIRTY = (value) => ({ status: "dirty", value }); +export const OK = (value) => ({ status: "valid", value }); +export const isAborted = (x) => x.status === "aborted"; +export const isDirty = (x) => x.status === "dirty"; +export const isValid = (x) => x.status === "valid"; +export const isAsync = (x) => typeof Promise !== "undefined" && x instanceof Promise; diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/helpers/partialUtil.js b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/helpers/partialUtil.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/helpers/partialUtil.js @@ -0,0 +1 @@ +export {}; diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/helpers/typeAliases.js b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/helpers/typeAliases.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/helpers/typeAliases.js @@ -0,0 +1 @@ +export {}; diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/helpers/util.js b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/helpers/util.js new file mode 100644 index 000000000..b9641f29b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/helpers/util.js @@ -0,0 +1,133 @@ +export var util; +(function (util) { + util.assertEqual = (_) => { }; + function assertIs(_arg) { } + util.assertIs = assertIs; + function assertNever(_x) { + throw new Error(); + } + util.assertNever = assertNever; + util.arrayToEnum = (items) => { + const obj = {}; + for (const item of items) { + obj[item] = item; + } + return obj; + }; + util.getValidEnumValues = (obj) => { + const validKeys = util.objectKeys(obj).filter((k) => typeof obj[obj[k]] !== "number"); + const filtered = {}; + for (const k of validKeys) { + filtered[k] = obj[k]; + } + return util.objectValues(filtered); + }; + util.objectValues = (obj) => { + return util.objectKeys(obj).map(function (e) { + return obj[e]; + }); + }; + util.objectKeys = typeof Object.keys === "function" // eslint-disable-line ban/ban + ? (obj) => Object.keys(obj) // eslint-disable-line ban/ban + : (object) => { + const keys = []; + for (const key in object) { + if (Object.prototype.hasOwnProperty.call(object, key)) { + keys.push(key); + } + } + return keys; + }; + util.find = (arr, checker) => { + for (const item of arr) { + if (checker(item)) + return item; + } + return undefined; + }; + util.isInteger = typeof Number.isInteger === "function" + ? (val) => Number.isInteger(val) // eslint-disable-line ban/ban + : (val) => typeof val === "number" && Number.isFinite(val) && Math.floor(val) === val; + function joinValues(array, separator = " | ") { + return array.map((val) => (typeof val === "string" ? `'${val}'` : val)).join(separator); + } + util.joinValues = joinValues; + util.jsonStringifyReplacer = (_, value) => { + if (typeof value === "bigint") { + return value.toString(); + } + return value; + }; +})(util || (util = {})); +export var objectUtil; +(function (objectUtil) { + objectUtil.mergeShapes = (first, second) => { + return { + ...first, + ...second, // second overwrites first + }; + }; +})(objectUtil || (objectUtil = {})); +export const ZodParsedType = util.arrayToEnum([ + "string", + "nan", + "number", + "integer", + "float", + "boolean", + "date", + "bigint", + "symbol", + "function", + "undefined", + "null", + "array", + "object", + "unknown", + "promise", + "void", + "never", + "map", + "set", +]); +export const getParsedType = (data) => { + const t = typeof data; + switch (t) { + case "undefined": + return ZodParsedType.undefined; + case "string": + return ZodParsedType.string; + case "number": + return Number.isNaN(data) ? ZodParsedType.nan : ZodParsedType.number; + case "boolean": + return ZodParsedType.boolean; + case "function": + return ZodParsedType.function; + case "bigint": + return ZodParsedType.bigint; + case "symbol": + return ZodParsedType.symbol; + case "object": + if (Array.isArray(data)) { + return ZodParsedType.array; + } + if (data === null) { + return ZodParsedType.null; + } + if (data.then && typeof data.then === "function" && data.catch && typeof data.catch === "function") { + return ZodParsedType.promise; + } + if (typeof Map !== "undefined" && data instanceof Map) { + return ZodParsedType.map; + } + if (typeof Set !== "undefined" && data instanceof Set) { + return ZodParsedType.set; + } + if (typeof Date !== "undefined" && data instanceof Date) { + return ZodParsedType.date; + } + return ZodParsedType.object; + default: + return ZodParsedType.unknown; + } +}; diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/index.js b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/index.js new file mode 100644 index 000000000..a9dab5738 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/index.js @@ -0,0 +1,4 @@ +import * as z from "./external.js"; +export * from "./external.js"; +export { z }; +export default z; diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/locales/en.js b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/locales/en.js new file mode 100644 index 000000000..3e97ad51a --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/locales/en.js @@ -0,0 +1,107 @@ +import { ZodIssueCode } from "../ZodError.js"; +import { util, ZodParsedType } from "../helpers/util.js"; +const errorMap = (issue, _ctx) => { + let message; + switch (issue.code) { + case ZodIssueCode.invalid_type: + if (issue.received === ZodParsedType.undefined) { + message = "Required"; + } + else { + message = `Expected ${issue.expected}, received ${issue.received}`; + } + break; + case ZodIssueCode.invalid_literal: + message = `Invalid literal value, expected ${JSON.stringify(issue.expected, util.jsonStringifyReplacer)}`; + break; + case ZodIssueCode.unrecognized_keys: + message = `Unrecognized key(s) in object: ${util.joinValues(issue.keys, ", ")}`; + break; + case ZodIssueCode.invalid_union: + message = `Invalid input`; + break; + case ZodIssueCode.invalid_union_discriminator: + message = `Invalid discriminator value. Expected ${util.joinValues(issue.options)}`; + break; + case ZodIssueCode.invalid_enum_value: + message = `Invalid enum value. Expected ${util.joinValues(issue.options)}, received '${issue.received}'`; + break; + case ZodIssueCode.invalid_arguments: + message = `Invalid function arguments`; + break; + case ZodIssueCode.invalid_return_type: + message = `Invalid function return type`; + break; + case ZodIssueCode.invalid_date: + message = `Invalid date`; + break; + case ZodIssueCode.invalid_string: + if (typeof issue.validation === "object") { + if ("includes" in issue.validation) { + message = `Invalid input: must include "${issue.validation.includes}"`; + if (typeof issue.validation.position === "number") { + message = `${message} at one or more positions greater than or equal to ${issue.validation.position}`; + } + } + else if ("startsWith" in issue.validation) { + message = `Invalid input: must start with "${issue.validation.startsWith}"`; + } + else if ("endsWith" in issue.validation) { + message = `Invalid input: must end with "${issue.validation.endsWith}"`; + } + else { + util.assertNever(issue.validation); + } + } + else if (issue.validation !== "regex") { + message = `Invalid ${issue.validation}`; + } + else { + message = "Invalid"; + } + break; + case ZodIssueCode.too_small: + if (issue.type === "array") + message = `Array must contain ${issue.exact ? "exactly" : issue.inclusive ? `at least` : `more than`} ${issue.minimum} element(s)`; + else if (issue.type === "string") + message = `String must contain ${issue.exact ? "exactly" : issue.inclusive ? `at least` : `over`} ${issue.minimum} character(s)`; + else if (issue.type === "number") + message = `Number must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${issue.minimum}`; + else if (issue.type === "date") + message = `Date must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${new Date(Number(issue.minimum))}`; + else + message = "Invalid input"; + break; + case ZodIssueCode.too_big: + if (issue.type === "array") + message = `Array must contain ${issue.exact ? `exactly` : issue.inclusive ? `at most` : `less than`} ${issue.maximum} element(s)`; + else if (issue.type === "string") + message = `String must contain ${issue.exact ? `exactly` : issue.inclusive ? `at most` : `under`} ${issue.maximum} character(s)`; + else if (issue.type === "number") + message = `Number must be ${issue.exact ? `exactly` : issue.inclusive ? `less than or equal to` : `less than`} ${issue.maximum}`; + else if (issue.type === "bigint") + message = `BigInt must be ${issue.exact ? `exactly` : issue.inclusive ? `less than or equal to` : `less than`} ${issue.maximum}`; + else if (issue.type === "date") + message = `Date must be ${issue.exact ? `exactly` : issue.inclusive ? `smaller than or equal to` : `smaller than`} ${new Date(Number(issue.maximum))}`; + else + message = "Invalid input"; + break; + case ZodIssueCode.custom: + message = `Invalid input`; + break; + case ZodIssueCode.invalid_intersection_types: + message = `Intersection results could not be merged`; + break; + case ZodIssueCode.not_multiple_of: + message = `Number must be a multiple of ${issue.multipleOf}`; + break; + case ZodIssueCode.not_finite: + message = "Number must be finite"; + break; + default: + message = _ctx.defaultError; + util.assertNever(issue); + } + return { message }; +}; +export default errorMap; diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/standard-schema.js b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/standard-schema.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/standard-schema.js @@ -0,0 +1 @@ +export {}; diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/types.js b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/types.js new file mode 100644 index 000000000..de1e6213e --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/types.js @@ -0,0 +1,3691 @@ +import { ZodError, ZodIssueCode, } from "./ZodError.js"; +import { defaultErrorMap, getErrorMap } from "./errors.js"; +import { errorUtil } from "./helpers/errorUtil.js"; +import { DIRTY, INVALID, OK, ParseStatus, addIssueToContext, isAborted, isAsync, isDirty, isValid, makeIssue, } from "./helpers/parseUtil.js"; +import { util, ZodParsedType, getParsedType } from "./helpers/util.js"; +class ParseInputLazyPath { + constructor(parent, value, path, key) { + this._cachedPath = []; + this.parent = parent; + this.data = value; + this._path = path; + this._key = key; + } + get path() { + if (!this._cachedPath.length) { + if (Array.isArray(this._key)) { + this._cachedPath.push(...this._path, ...this._key); + } + else { + this._cachedPath.push(...this._path, this._key); + } + } + return this._cachedPath; + } +} +const handleResult = (ctx, result) => { + if (isValid(result)) { + return { success: true, data: result.value }; + } + else { + if (!ctx.common.issues.length) { + throw new Error("Validation failed but no issues detected."); + } + return { + success: false, + get error() { + if (this._error) + return this._error; + const error = new ZodError(ctx.common.issues); + this._error = error; + return this._error; + }, + }; + } +}; +function processCreateParams(params) { + if (!params) + return {}; + const { errorMap, invalid_type_error, required_error, description } = params; + if (errorMap && (invalid_type_error || required_error)) { + throw new Error(`Can't use "invalid_type_error" or "required_error" in conjunction with custom error map.`); + } + if (errorMap) + return { errorMap: errorMap, description }; + const customMap = (iss, ctx) => { + const { message } = params; + if (iss.code === "invalid_enum_value") { + return { message: message ?? ctx.defaultError }; + } + if (typeof ctx.data === "undefined") { + return { message: message ?? required_error ?? ctx.defaultError }; + } + if (iss.code !== "invalid_type") + return { message: ctx.defaultError }; + return { message: message ?? invalid_type_error ?? ctx.defaultError }; + }; + return { errorMap: customMap, description }; +} +export class ZodType { + get description() { + return this._def.description; + } + _getType(input) { + return getParsedType(input.data); + } + _getOrReturnCtx(input, ctx) { + return (ctx || { + common: input.parent.common, + data: input.data, + parsedType: getParsedType(input.data), + schemaErrorMap: this._def.errorMap, + path: input.path, + parent: input.parent, + }); + } + _processInputParams(input) { + return { + status: new ParseStatus(), + ctx: { + common: input.parent.common, + data: input.data, + parsedType: getParsedType(input.data), + schemaErrorMap: this._def.errorMap, + path: input.path, + parent: input.parent, + }, + }; + } + _parseSync(input) { + const result = this._parse(input); + if (isAsync(result)) { + throw new Error("Synchronous parse encountered promise."); + } + return result; + } + _parseAsync(input) { + const result = this._parse(input); + return Promise.resolve(result); + } + parse(data, params) { + const result = this.safeParse(data, params); + if (result.success) + return result.data; + throw result.error; + } + safeParse(data, params) { + const ctx = { + common: { + issues: [], + async: params?.async ?? false, + contextualErrorMap: params?.errorMap, + }, + path: params?.path || [], + schemaErrorMap: this._def.errorMap, + parent: null, + data, + parsedType: getParsedType(data), + }; + const result = this._parseSync({ data, path: ctx.path, parent: ctx }); + return handleResult(ctx, result); + } + "~validate"(data) { + const ctx = { + common: { + issues: [], + async: !!this["~standard"].async, + }, + path: [], + schemaErrorMap: this._def.errorMap, + parent: null, + data, + parsedType: getParsedType(data), + }; + if (!this["~standard"].async) { + try { + const result = this._parseSync({ data, path: [], parent: ctx }); + return isValid(result) + ? { + value: result.value, + } + : { + issues: ctx.common.issues, + }; + } + catch (err) { + if (err?.message?.toLowerCase()?.includes("encountered")) { + this["~standard"].async = true; + } + ctx.common = { + issues: [], + async: true, + }; + } + } + return this._parseAsync({ data, path: [], parent: ctx }).then((result) => isValid(result) + ? { + value: result.value, + } + : { + issues: ctx.common.issues, + }); + } + async parseAsync(data, params) { + const result = await this.safeParseAsync(data, params); + if (result.success) + return result.data; + throw result.error; + } + async safeParseAsync(data, params) { + const ctx = { + common: { + issues: [], + contextualErrorMap: params?.errorMap, + async: true, + }, + path: params?.path || [], + schemaErrorMap: this._def.errorMap, + parent: null, + data, + parsedType: getParsedType(data), + }; + const maybeAsyncResult = this._parse({ data, path: ctx.path, parent: ctx }); + const result = await (isAsync(maybeAsyncResult) ? maybeAsyncResult : Promise.resolve(maybeAsyncResult)); + return handleResult(ctx, result); + } + refine(check, message) { + const getIssueProperties = (val) => { + if (typeof message === "string" || typeof message === "undefined") { + return { message }; + } + else if (typeof message === "function") { + return message(val); + } + else { + return message; + } + }; + return this._refinement((val, ctx) => { + const result = check(val); + const setError = () => ctx.addIssue({ + code: ZodIssueCode.custom, + ...getIssueProperties(val), + }); + if (typeof Promise !== "undefined" && result instanceof Promise) { + return result.then((data) => { + if (!data) { + setError(); + return false; + } + else { + return true; + } + }); + } + if (!result) { + setError(); + return false; + } + else { + return true; + } + }); + } + refinement(check, refinementData) { + return this._refinement((val, ctx) => { + if (!check(val)) { + ctx.addIssue(typeof refinementData === "function" ? refinementData(val, ctx) : refinementData); + return false; + } + else { + return true; + } + }); + } + _refinement(refinement) { + return new ZodEffects({ + schema: this, + typeName: ZodFirstPartyTypeKind.ZodEffects, + effect: { type: "refinement", refinement }, + }); + } + superRefine(refinement) { + return this._refinement(refinement); + } + constructor(def) { + /** Alias of safeParseAsync */ + this.spa = this.safeParseAsync; + this._def = def; + this.parse = this.parse.bind(this); + this.safeParse = this.safeParse.bind(this); + this.parseAsync = this.parseAsync.bind(this); + this.safeParseAsync = this.safeParseAsync.bind(this); + this.spa = this.spa.bind(this); + this.refine = this.refine.bind(this); + this.refinement = this.refinement.bind(this); + this.superRefine = this.superRefine.bind(this); + this.optional = this.optional.bind(this); + this.nullable = this.nullable.bind(this); + this.nullish = this.nullish.bind(this); + this.array = this.array.bind(this); + this.promise = this.promise.bind(this); + this.or = this.or.bind(this); + this.and = this.and.bind(this); + this.transform = this.transform.bind(this); + this.brand = this.brand.bind(this); + this.default = this.default.bind(this); + this.catch = this.catch.bind(this); + this.describe = this.describe.bind(this); + this.pipe = this.pipe.bind(this); + this.readonly = this.readonly.bind(this); + this.isNullable = this.isNullable.bind(this); + this.isOptional = this.isOptional.bind(this); + this["~standard"] = { + version: 1, + vendor: "zod", + validate: (data) => this["~validate"](data), + }; + } + optional() { + return ZodOptional.create(this, this._def); + } + nullable() { + return ZodNullable.create(this, this._def); + } + nullish() { + return this.nullable().optional(); + } + array() { + return ZodArray.create(this); + } + promise() { + return ZodPromise.create(this, this._def); + } + or(option) { + return ZodUnion.create([this, option], this._def); + } + and(incoming) { + return ZodIntersection.create(this, incoming, this._def); + } + transform(transform) { + return new ZodEffects({ + ...processCreateParams(this._def), + schema: this, + typeName: ZodFirstPartyTypeKind.ZodEffects, + effect: { type: "transform", transform }, + }); + } + default(def) { + const defaultValueFunc = typeof def === "function" ? def : () => def; + return new ZodDefault({ + ...processCreateParams(this._def), + innerType: this, + defaultValue: defaultValueFunc, + typeName: ZodFirstPartyTypeKind.ZodDefault, + }); + } + brand() { + return new ZodBranded({ + typeName: ZodFirstPartyTypeKind.ZodBranded, + type: this, + ...processCreateParams(this._def), + }); + } + catch(def) { + const catchValueFunc = typeof def === "function" ? def : () => def; + return new ZodCatch({ + ...processCreateParams(this._def), + innerType: this, + catchValue: catchValueFunc, + typeName: ZodFirstPartyTypeKind.ZodCatch, + }); + } + describe(description) { + const This = this.constructor; + return new This({ + ...this._def, + description, + }); + } + pipe(target) { + return ZodPipeline.create(this, target); + } + readonly() { + return ZodReadonly.create(this); + } + isOptional() { + return this.safeParse(undefined).success; + } + isNullable() { + return this.safeParse(null).success; + } +} +const cuidRegex = /^c[^\s-]{8,}$/i; +const cuid2Regex = /^[0-9a-z]+$/; +const ulidRegex = /^[0-9A-HJKMNP-TV-Z]{26}$/i; +// const uuidRegex = +// /^([a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[a-f0-9]{4}-[a-f0-9]{12}|00000000-0000-0000-0000-000000000000)$/i; +const uuidRegex = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/i; +const nanoidRegex = /^[a-z0-9_-]{21}$/i; +const jwtRegex = /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$/; +const durationRegex = /^[-+]?P(?!$)(?:(?:[-+]?\d+Y)|(?:[-+]?\d+[.,]\d+Y$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:(?:[-+]?\d+W)|(?:[-+]?\d+[.,]\d+W$))?(?:(?:[-+]?\d+D)|(?:[-+]?\d+[.,]\d+D$))?(?:T(?=[\d+-])(?:(?:[-+]?\d+H)|(?:[-+]?\d+[.,]\d+H$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:[-+]?\d+(?:[.,]\d+)?S)?)??$/; +// from https://stackoverflow.com/a/46181/1550155 +// old version: too slow, didn't support unicode +// const emailRegex = /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i; +//old email regex +// const emailRegex = /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@((?!-)([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{1,})[^-<>()[\].,;:\s@"]$/i; +// eslint-disable-next-line +// const emailRegex = +// /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\])|(\[IPv6:(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))\])|([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])*(\.[A-Za-z]{2,})+))$/; +// const emailRegex = +// /^[a-zA-Z0-9\.\!\#\$\%\&\'\*\+\/\=\?\^\_\`\{\|\}\~\-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; +// const emailRegex = +// /^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$/i; +const emailRegex = /^(?!\.)(?!.*\.\.)([A-Z0-9_'+\-\.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i; +// const emailRegex = +// /^[a-z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-z0-9-]+(?:\.[a-z0-9\-]+)*$/i; +// from https://thekevinscott.com/emojis-in-javascript/#writing-a-regular-expression +const _emojiRegex = `^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$`; +let emojiRegex; +// faster, simpler, safer +const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/; +const ipv4CidrRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/(3[0-2]|[12]?[0-9])$/; +// const ipv6Regex = +// /^(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))$/; +const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/; +const ipv6CidrRegex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/; +// https://stackoverflow.com/questions/7860392/determine-if-string-is-in-base64-using-javascript +const base64Regex = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/; +// https://base64.guru/standards/base64url +const base64urlRegex = /^([0-9a-zA-Z-_]{4})*(([0-9a-zA-Z-_]{2}(==)?)|([0-9a-zA-Z-_]{3}(=)?))?$/; +// simple +// const dateRegexSource = `\\d{4}-\\d{2}-\\d{2}`; +// no leap year validation +// const dateRegexSource = `\\d{4}-((0[13578]|10|12)-31|(0[13-9]|1[0-2])-30|(0[1-9]|1[0-2])-(0[1-9]|1\\d|2\\d))`; +// with leap year validation +const dateRegexSource = `((\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\\d|3[01])|(0[469]|11)-(0[1-9]|[12]\\d|30)|(02)-(0[1-9]|1\\d|2[0-8])))`; +const dateRegex = new RegExp(`^${dateRegexSource}$`); +function timeRegexSource(args) { + let secondsRegexSource = `[0-5]\\d`; + if (args.precision) { + secondsRegexSource = `${secondsRegexSource}\\.\\d{${args.precision}}`; + } + else if (args.precision == null) { + secondsRegexSource = `${secondsRegexSource}(\\.\\d+)?`; + } + const secondsQuantifier = args.precision ? "+" : "?"; // require seconds if precision is nonzero + return `([01]\\d|2[0-3]):[0-5]\\d(:${secondsRegexSource})${secondsQuantifier}`; +} +function timeRegex(args) { + return new RegExp(`^${timeRegexSource(args)}$`); +} +// Adapted from https://stackoverflow.com/a/3143231 +export function datetimeRegex(args) { + let regex = `${dateRegexSource}T${timeRegexSource(args)}`; + const opts = []; + opts.push(args.local ? `Z?` : `Z`); + if (args.offset) + opts.push(`([+-]\\d{2}:?\\d{2})`); + regex = `${regex}(${opts.join("|")})`; + return new RegExp(`^${regex}$`); +} +function isValidIP(ip, version) { + if ((version === "v4" || !version) && ipv4Regex.test(ip)) { + return true; + } + if ((version === "v6" || !version) && ipv6Regex.test(ip)) { + return true; + } + return false; +} +function isValidJWT(jwt, alg) { + if (!jwtRegex.test(jwt)) + return false; + try { + const [header] = jwt.split("."); + // Convert base64url to base64 + const base64 = header + .replace(/-/g, "+") + .replace(/_/g, "/") + .padEnd(header.length + ((4 - (header.length % 4)) % 4), "="); + const decoded = JSON.parse(atob(base64)); + if (typeof decoded !== "object" || decoded === null) + return false; + if ("typ" in decoded && decoded?.typ !== "JWT") + return false; + if (!decoded.alg) + return false; + if (alg && decoded.alg !== alg) + return false; + return true; + } + catch { + return false; + } +} +function isValidCidr(ip, version) { + if ((version === "v4" || !version) && ipv4CidrRegex.test(ip)) { + return true; + } + if ((version === "v6" || !version) && ipv6CidrRegex.test(ip)) { + return true; + } + return false; +} +export class ZodString extends ZodType { + _parse(input) { + if (this._def.coerce) { + input.data = String(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.string) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.string, + received: ctx.parsedType, + }); + return INVALID; + } + const status = new ParseStatus(); + let ctx = undefined; + for (const check of this._def.checks) { + if (check.kind === "min") { + if (input.data.length < check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: check.value, + type: "string", + inclusive: true, + exact: false, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "max") { + if (input.data.length > check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: check.value, + type: "string", + inclusive: true, + exact: false, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "length") { + const tooBig = input.data.length > check.value; + const tooSmall = input.data.length < check.value; + if (tooBig || tooSmall) { + ctx = this._getOrReturnCtx(input, ctx); + if (tooBig) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: check.value, + type: "string", + inclusive: true, + exact: true, + message: check.message, + }); + } + else if (tooSmall) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: check.value, + type: "string", + inclusive: true, + exact: true, + message: check.message, + }); + } + status.dirty(); + } + } + else if (check.kind === "email") { + if (!emailRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "email", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "emoji") { + if (!emojiRegex) { + emojiRegex = new RegExp(_emojiRegex, "u"); + } + if (!emojiRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "emoji", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "uuid") { + if (!uuidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "uuid", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "nanoid") { + if (!nanoidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "nanoid", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "cuid") { + if (!cuidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cuid", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "cuid2") { + if (!cuid2Regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cuid2", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "ulid") { + if (!ulidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "ulid", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "url") { + try { + new URL(input.data); + } + catch { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "url", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "regex") { + check.regex.lastIndex = 0; + const testResult = check.regex.test(input.data); + if (!testResult) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "regex", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "trim") { + input.data = input.data.trim(); + } + else if (check.kind === "includes") { + if (!input.data.includes(check.value, check.position)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: { includes: check.value, position: check.position }, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "toLowerCase") { + input.data = input.data.toLowerCase(); + } + else if (check.kind === "toUpperCase") { + input.data = input.data.toUpperCase(); + } + else if (check.kind === "startsWith") { + if (!input.data.startsWith(check.value)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: { startsWith: check.value }, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "endsWith") { + if (!input.data.endsWith(check.value)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: { endsWith: check.value }, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "datetime") { + const regex = datetimeRegex(check); + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: "datetime", + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "date") { + const regex = dateRegex; + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: "date", + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "time") { + const regex = timeRegex(check); + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: "time", + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "duration") { + if (!durationRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "duration", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "ip") { + if (!isValidIP(input.data, check.version)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "ip", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "jwt") { + if (!isValidJWT(input.data, check.alg)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "jwt", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "cidr") { + if (!isValidCidr(input.data, check.version)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cidr", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "base64") { + if (!base64Regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "base64", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "base64url") { + if (!base64urlRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "base64url", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else { + util.assertNever(check); + } + } + return { status: status.value, value: input.data }; + } + _regex(regex, validation, message) { + return this.refinement((data) => regex.test(data), { + validation, + code: ZodIssueCode.invalid_string, + ...errorUtil.errToObj(message), + }); + } + _addCheck(check) { + return new ZodString({ + ...this._def, + checks: [...this._def.checks, check], + }); + } + email(message) { + return this._addCheck({ kind: "email", ...errorUtil.errToObj(message) }); + } + url(message) { + return this._addCheck({ kind: "url", ...errorUtil.errToObj(message) }); + } + emoji(message) { + return this._addCheck({ kind: "emoji", ...errorUtil.errToObj(message) }); + } + uuid(message) { + return this._addCheck({ kind: "uuid", ...errorUtil.errToObj(message) }); + } + nanoid(message) { + return this._addCheck({ kind: "nanoid", ...errorUtil.errToObj(message) }); + } + cuid(message) { + return this._addCheck({ kind: "cuid", ...errorUtil.errToObj(message) }); + } + cuid2(message) { + return this._addCheck({ kind: "cuid2", ...errorUtil.errToObj(message) }); + } + ulid(message) { + return this._addCheck({ kind: "ulid", ...errorUtil.errToObj(message) }); + } + base64(message) { + return this._addCheck({ kind: "base64", ...errorUtil.errToObj(message) }); + } + base64url(message) { + // base64url encoding is a modification of base64 that can safely be used in URLs and filenames + return this._addCheck({ + kind: "base64url", + ...errorUtil.errToObj(message), + }); + } + jwt(options) { + return this._addCheck({ kind: "jwt", ...errorUtil.errToObj(options) }); + } + ip(options) { + return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) }); + } + cidr(options) { + return this._addCheck({ kind: "cidr", ...errorUtil.errToObj(options) }); + } + datetime(options) { + if (typeof options === "string") { + return this._addCheck({ + kind: "datetime", + precision: null, + offset: false, + local: false, + message: options, + }); + } + return this._addCheck({ + kind: "datetime", + precision: typeof options?.precision === "undefined" ? null : options?.precision, + offset: options?.offset ?? false, + local: options?.local ?? false, + ...errorUtil.errToObj(options?.message), + }); + } + date(message) { + return this._addCheck({ kind: "date", message }); + } + time(options) { + if (typeof options === "string") { + return this._addCheck({ + kind: "time", + precision: null, + message: options, + }); + } + return this._addCheck({ + kind: "time", + precision: typeof options?.precision === "undefined" ? null : options?.precision, + ...errorUtil.errToObj(options?.message), + }); + } + duration(message) { + return this._addCheck({ kind: "duration", ...errorUtil.errToObj(message) }); + } + regex(regex, message) { + return this._addCheck({ + kind: "regex", + regex: regex, + ...errorUtil.errToObj(message), + }); + } + includes(value, options) { + return this._addCheck({ + kind: "includes", + value: value, + position: options?.position, + ...errorUtil.errToObj(options?.message), + }); + } + startsWith(value, message) { + return this._addCheck({ + kind: "startsWith", + value: value, + ...errorUtil.errToObj(message), + }); + } + endsWith(value, message) { + return this._addCheck({ + kind: "endsWith", + value: value, + ...errorUtil.errToObj(message), + }); + } + min(minLength, message) { + return this._addCheck({ + kind: "min", + value: minLength, + ...errorUtil.errToObj(message), + }); + } + max(maxLength, message) { + return this._addCheck({ + kind: "max", + value: maxLength, + ...errorUtil.errToObj(message), + }); + } + length(len, message) { + return this._addCheck({ + kind: "length", + value: len, + ...errorUtil.errToObj(message), + }); + } + /** + * Equivalent to `.min(1)` + */ + nonempty(message) { + return this.min(1, errorUtil.errToObj(message)); + } + trim() { + return new ZodString({ + ...this._def, + checks: [...this._def.checks, { kind: "trim" }], + }); + } + toLowerCase() { + return new ZodString({ + ...this._def, + checks: [...this._def.checks, { kind: "toLowerCase" }], + }); + } + toUpperCase() { + return new ZodString({ + ...this._def, + checks: [...this._def.checks, { kind: "toUpperCase" }], + }); + } + get isDatetime() { + return !!this._def.checks.find((ch) => ch.kind === "datetime"); + } + get isDate() { + return !!this._def.checks.find((ch) => ch.kind === "date"); + } + get isTime() { + return !!this._def.checks.find((ch) => ch.kind === "time"); + } + get isDuration() { + return !!this._def.checks.find((ch) => ch.kind === "duration"); + } + get isEmail() { + return !!this._def.checks.find((ch) => ch.kind === "email"); + } + get isURL() { + return !!this._def.checks.find((ch) => ch.kind === "url"); + } + get isEmoji() { + return !!this._def.checks.find((ch) => ch.kind === "emoji"); + } + get isUUID() { + return !!this._def.checks.find((ch) => ch.kind === "uuid"); + } + get isNANOID() { + return !!this._def.checks.find((ch) => ch.kind === "nanoid"); + } + get isCUID() { + return !!this._def.checks.find((ch) => ch.kind === "cuid"); + } + get isCUID2() { + return !!this._def.checks.find((ch) => ch.kind === "cuid2"); + } + get isULID() { + return !!this._def.checks.find((ch) => ch.kind === "ulid"); + } + get isIP() { + return !!this._def.checks.find((ch) => ch.kind === "ip"); + } + get isCIDR() { + return !!this._def.checks.find((ch) => ch.kind === "cidr"); + } + get isBase64() { + return !!this._def.checks.find((ch) => ch.kind === "base64"); + } + get isBase64url() { + // base64url encoding is a modification of base64 that can safely be used in URLs and filenames + return !!this._def.checks.find((ch) => ch.kind === "base64url"); + } + get minLength() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min; + } + get maxLength() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max; + } +} +ZodString.create = (params) => { + return new ZodString({ + checks: [], + typeName: ZodFirstPartyTypeKind.ZodString, + coerce: params?.coerce ?? false, + ...processCreateParams(params), + }); +}; +// https://stackoverflow.com/questions/3966484/why-does-modulus-operator-return-fractional-number-in-javascript/31711034#31711034 +function floatSafeRemainder(val, step) { + const valDecCount = (val.toString().split(".")[1] || "").length; + const stepDecCount = (step.toString().split(".")[1] || "").length; + const decCount = valDecCount > stepDecCount ? valDecCount : stepDecCount; + const valInt = Number.parseInt(val.toFixed(decCount).replace(".", "")); + const stepInt = Number.parseInt(step.toFixed(decCount).replace(".", "")); + return (valInt % stepInt) / 10 ** decCount; +} +export class ZodNumber extends ZodType { + constructor() { + super(...arguments); + this.min = this.gte; + this.max = this.lte; + this.step = this.multipleOf; + } + _parse(input) { + if (this._def.coerce) { + input.data = Number(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.number) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.number, + received: ctx.parsedType, + }); + return INVALID; + } + let ctx = undefined; + const status = new ParseStatus(); + for (const check of this._def.checks) { + if (check.kind === "int") { + if (!util.isInteger(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: "integer", + received: "float", + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "min") { + const tooSmall = check.inclusive ? input.data < check.value : input.data <= check.value; + if (tooSmall) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: check.value, + type: "number", + inclusive: check.inclusive, + exact: false, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "max") { + const tooBig = check.inclusive ? input.data > check.value : input.data >= check.value; + if (tooBig) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: check.value, + type: "number", + inclusive: check.inclusive, + exact: false, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "multipleOf") { + if (floatSafeRemainder(input.data, check.value) !== 0) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.not_multiple_of, + multipleOf: check.value, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "finite") { + if (!Number.isFinite(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.not_finite, + message: check.message, + }); + status.dirty(); + } + } + else { + util.assertNever(check); + } + } + return { status: status.value, value: input.data }; + } + gte(value, message) { + return this.setLimit("min", value, true, errorUtil.toString(message)); + } + gt(value, message) { + return this.setLimit("min", value, false, errorUtil.toString(message)); + } + lte(value, message) { + return this.setLimit("max", value, true, errorUtil.toString(message)); + } + lt(value, message) { + return this.setLimit("max", value, false, errorUtil.toString(message)); + } + setLimit(kind, value, inclusive, message) { + return new ZodNumber({ + ...this._def, + checks: [ + ...this._def.checks, + { + kind, + value, + inclusive, + message: errorUtil.toString(message), + }, + ], + }); + } + _addCheck(check) { + return new ZodNumber({ + ...this._def, + checks: [...this._def.checks, check], + }); + } + int(message) { + return this._addCheck({ + kind: "int", + message: errorUtil.toString(message), + }); + } + positive(message) { + return this._addCheck({ + kind: "min", + value: 0, + inclusive: false, + message: errorUtil.toString(message), + }); + } + negative(message) { + return this._addCheck({ + kind: "max", + value: 0, + inclusive: false, + message: errorUtil.toString(message), + }); + } + nonpositive(message) { + return this._addCheck({ + kind: "max", + value: 0, + inclusive: true, + message: errorUtil.toString(message), + }); + } + nonnegative(message) { + return this._addCheck({ + kind: "min", + value: 0, + inclusive: true, + message: errorUtil.toString(message), + }); + } + multipleOf(value, message) { + return this._addCheck({ + kind: "multipleOf", + value: value, + message: errorUtil.toString(message), + }); + } + finite(message) { + return this._addCheck({ + kind: "finite", + message: errorUtil.toString(message), + }); + } + safe(message) { + return this._addCheck({ + kind: "min", + inclusive: true, + value: Number.MIN_SAFE_INTEGER, + message: errorUtil.toString(message), + })._addCheck({ + kind: "max", + inclusive: true, + value: Number.MAX_SAFE_INTEGER, + message: errorUtil.toString(message), + }); + } + get minValue() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min; + } + get maxValue() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max; + } + get isInt() { + return !!this._def.checks.find((ch) => ch.kind === "int" || (ch.kind === "multipleOf" && util.isInteger(ch.value))); + } + get isFinite() { + let max = null; + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "finite" || ch.kind === "int" || ch.kind === "multipleOf") { + return true; + } + else if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + else if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return Number.isFinite(min) && Number.isFinite(max); + } +} +ZodNumber.create = (params) => { + return new ZodNumber({ + checks: [], + typeName: ZodFirstPartyTypeKind.ZodNumber, + coerce: params?.coerce || false, + ...processCreateParams(params), + }); +}; +export class ZodBigInt extends ZodType { + constructor() { + super(...arguments); + this.min = this.gte; + this.max = this.lte; + } + _parse(input) { + if (this._def.coerce) { + try { + input.data = BigInt(input.data); + } + catch { + return this._getInvalidInput(input); + } + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.bigint) { + return this._getInvalidInput(input); + } + let ctx = undefined; + const status = new ParseStatus(); + for (const check of this._def.checks) { + if (check.kind === "min") { + const tooSmall = check.inclusive ? input.data < check.value : input.data <= check.value; + if (tooSmall) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + type: "bigint", + minimum: check.value, + inclusive: check.inclusive, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "max") { + const tooBig = check.inclusive ? input.data > check.value : input.data >= check.value; + if (tooBig) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + type: "bigint", + maximum: check.value, + inclusive: check.inclusive, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "multipleOf") { + if (input.data % check.value !== BigInt(0)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.not_multiple_of, + multipleOf: check.value, + message: check.message, + }); + status.dirty(); + } + } + else { + util.assertNever(check); + } + } + return { status: status.value, value: input.data }; + } + _getInvalidInput(input) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.bigint, + received: ctx.parsedType, + }); + return INVALID; + } + gte(value, message) { + return this.setLimit("min", value, true, errorUtil.toString(message)); + } + gt(value, message) { + return this.setLimit("min", value, false, errorUtil.toString(message)); + } + lte(value, message) { + return this.setLimit("max", value, true, errorUtil.toString(message)); + } + lt(value, message) { + return this.setLimit("max", value, false, errorUtil.toString(message)); + } + setLimit(kind, value, inclusive, message) { + return new ZodBigInt({ + ...this._def, + checks: [ + ...this._def.checks, + { + kind, + value, + inclusive, + message: errorUtil.toString(message), + }, + ], + }); + } + _addCheck(check) { + return new ZodBigInt({ + ...this._def, + checks: [...this._def.checks, check], + }); + } + positive(message) { + return this._addCheck({ + kind: "min", + value: BigInt(0), + inclusive: false, + message: errorUtil.toString(message), + }); + } + negative(message) { + return this._addCheck({ + kind: "max", + value: BigInt(0), + inclusive: false, + message: errorUtil.toString(message), + }); + } + nonpositive(message) { + return this._addCheck({ + kind: "max", + value: BigInt(0), + inclusive: true, + message: errorUtil.toString(message), + }); + } + nonnegative(message) { + return this._addCheck({ + kind: "min", + value: BigInt(0), + inclusive: true, + message: errorUtil.toString(message), + }); + } + multipleOf(value, message) { + return this._addCheck({ + kind: "multipleOf", + value, + message: errorUtil.toString(message), + }); + } + get minValue() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min; + } + get maxValue() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max; + } +} +ZodBigInt.create = (params) => { + return new ZodBigInt({ + checks: [], + typeName: ZodFirstPartyTypeKind.ZodBigInt, + coerce: params?.coerce ?? false, + ...processCreateParams(params), + }); +}; +export class ZodBoolean extends ZodType { + _parse(input) { + if (this._def.coerce) { + input.data = Boolean(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.boolean) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.boolean, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodBoolean.create = (params) => { + return new ZodBoolean({ + typeName: ZodFirstPartyTypeKind.ZodBoolean, + coerce: params?.coerce || false, + ...processCreateParams(params), + }); +}; +export class ZodDate extends ZodType { + _parse(input) { + if (this._def.coerce) { + input.data = new Date(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.date) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.date, + received: ctx.parsedType, + }); + return INVALID; + } + if (Number.isNaN(input.data.getTime())) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_date, + }); + return INVALID; + } + const status = new ParseStatus(); + let ctx = undefined; + for (const check of this._def.checks) { + if (check.kind === "min") { + if (input.data.getTime() < check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + message: check.message, + inclusive: true, + exact: false, + minimum: check.value, + type: "date", + }); + status.dirty(); + } + } + else if (check.kind === "max") { + if (input.data.getTime() > check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + message: check.message, + inclusive: true, + exact: false, + maximum: check.value, + type: "date", + }); + status.dirty(); + } + } + else { + util.assertNever(check); + } + } + return { + status: status.value, + value: new Date(input.data.getTime()), + }; + } + _addCheck(check) { + return new ZodDate({ + ...this._def, + checks: [...this._def.checks, check], + }); + } + min(minDate, message) { + return this._addCheck({ + kind: "min", + value: minDate.getTime(), + message: errorUtil.toString(message), + }); + } + max(maxDate, message) { + return this._addCheck({ + kind: "max", + value: maxDate.getTime(), + message: errorUtil.toString(message), + }); + } + get minDate() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min != null ? new Date(min) : null; + } + get maxDate() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max != null ? new Date(max) : null; + } +} +ZodDate.create = (params) => { + return new ZodDate({ + checks: [], + coerce: params?.coerce || false, + typeName: ZodFirstPartyTypeKind.ZodDate, + ...processCreateParams(params), + }); +}; +export class ZodSymbol extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.symbol) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.symbol, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodSymbol.create = (params) => { + return new ZodSymbol({ + typeName: ZodFirstPartyTypeKind.ZodSymbol, + ...processCreateParams(params), + }); +}; +export class ZodUndefined extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.undefined) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.undefined, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodUndefined.create = (params) => { + return new ZodUndefined({ + typeName: ZodFirstPartyTypeKind.ZodUndefined, + ...processCreateParams(params), + }); +}; +export class ZodNull extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.null) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.null, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodNull.create = (params) => { + return new ZodNull({ + typeName: ZodFirstPartyTypeKind.ZodNull, + ...processCreateParams(params), + }); +}; +export class ZodAny extends ZodType { + constructor() { + super(...arguments); + // to prevent instances of other classes from extending ZodAny. this causes issues with catchall in ZodObject. + this._any = true; + } + _parse(input) { + return OK(input.data); + } +} +ZodAny.create = (params) => { + return new ZodAny({ + typeName: ZodFirstPartyTypeKind.ZodAny, + ...processCreateParams(params), + }); +}; +export class ZodUnknown extends ZodType { + constructor() { + super(...arguments); + // required + this._unknown = true; + } + _parse(input) { + return OK(input.data); + } +} +ZodUnknown.create = (params) => { + return new ZodUnknown({ + typeName: ZodFirstPartyTypeKind.ZodUnknown, + ...processCreateParams(params), + }); +}; +export class ZodNever extends ZodType { + _parse(input) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.never, + received: ctx.parsedType, + }); + return INVALID; + } +} +ZodNever.create = (params) => { + return new ZodNever({ + typeName: ZodFirstPartyTypeKind.ZodNever, + ...processCreateParams(params), + }); +}; +export class ZodVoid extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.undefined) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.void, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodVoid.create = (params) => { + return new ZodVoid({ + typeName: ZodFirstPartyTypeKind.ZodVoid, + ...processCreateParams(params), + }); +}; +export class ZodArray extends ZodType { + _parse(input) { + const { ctx, status } = this._processInputParams(input); + const def = this._def; + if (ctx.parsedType !== ZodParsedType.array) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.array, + received: ctx.parsedType, + }); + return INVALID; + } + if (def.exactLength !== null) { + const tooBig = ctx.data.length > def.exactLength.value; + const tooSmall = ctx.data.length < def.exactLength.value; + if (tooBig || tooSmall) { + addIssueToContext(ctx, { + code: tooBig ? ZodIssueCode.too_big : ZodIssueCode.too_small, + minimum: (tooSmall ? def.exactLength.value : undefined), + maximum: (tooBig ? def.exactLength.value : undefined), + type: "array", + inclusive: true, + exact: true, + message: def.exactLength.message, + }); + status.dirty(); + } + } + if (def.minLength !== null) { + if (ctx.data.length < def.minLength.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: def.minLength.value, + type: "array", + inclusive: true, + exact: false, + message: def.minLength.message, + }); + status.dirty(); + } + } + if (def.maxLength !== null) { + if (ctx.data.length > def.maxLength.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: def.maxLength.value, + type: "array", + inclusive: true, + exact: false, + message: def.maxLength.message, + }); + status.dirty(); + } + } + if (ctx.common.async) { + return Promise.all([...ctx.data].map((item, i) => { + return def.type._parseAsync(new ParseInputLazyPath(ctx, item, ctx.path, i)); + })).then((result) => { + return ParseStatus.mergeArray(status, result); + }); + } + const result = [...ctx.data].map((item, i) => { + return def.type._parseSync(new ParseInputLazyPath(ctx, item, ctx.path, i)); + }); + return ParseStatus.mergeArray(status, result); + } + get element() { + return this._def.type; + } + min(minLength, message) { + return new ZodArray({ + ...this._def, + minLength: { value: minLength, message: errorUtil.toString(message) }, + }); + } + max(maxLength, message) { + return new ZodArray({ + ...this._def, + maxLength: { value: maxLength, message: errorUtil.toString(message) }, + }); + } + length(len, message) { + return new ZodArray({ + ...this._def, + exactLength: { value: len, message: errorUtil.toString(message) }, + }); + } + nonempty(message) { + return this.min(1, message); + } +} +ZodArray.create = (schema, params) => { + return new ZodArray({ + type: schema, + minLength: null, + maxLength: null, + exactLength: null, + typeName: ZodFirstPartyTypeKind.ZodArray, + ...processCreateParams(params), + }); +}; +function deepPartialify(schema) { + if (schema instanceof ZodObject) { + const newShape = {}; + for (const key in schema.shape) { + const fieldSchema = schema.shape[key]; + newShape[key] = ZodOptional.create(deepPartialify(fieldSchema)); + } + return new ZodObject({ + ...schema._def, + shape: () => newShape, + }); + } + else if (schema instanceof ZodArray) { + return new ZodArray({ + ...schema._def, + type: deepPartialify(schema.element), + }); + } + else if (schema instanceof ZodOptional) { + return ZodOptional.create(deepPartialify(schema.unwrap())); + } + else if (schema instanceof ZodNullable) { + return ZodNullable.create(deepPartialify(schema.unwrap())); + } + else if (schema instanceof ZodTuple) { + return ZodTuple.create(schema.items.map((item) => deepPartialify(item))); + } + else { + return schema; + } +} +export class ZodObject extends ZodType { + constructor() { + super(...arguments); + this._cached = null; + /** + * @deprecated In most cases, this is no longer needed - unknown properties are now silently stripped. + * If you want to pass through unknown properties, use `.passthrough()` instead. + */ + this.nonstrict = this.passthrough; + // extend< + // Augmentation extends ZodRawShape, + // NewOutput extends util.flatten<{ + // [k in keyof Augmentation | keyof Output]: k extends keyof Augmentation + // ? Augmentation[k]["_output"] + // : k extends keyof Output + // ? Output[k] + // : never; + // }>, + // NewInput extends util.flatten<{ + // [k in keyof Augmentation | keyof Input]: k extends keyof Augmentation + // ? Augmentation[k]["_input"] + // : k extends keyof Input + // ? Input[k] + // : never; + // }> + // >( + // augmentation: Augmentation + // ): ZodObject< + // extendShape, + // UnknownKeys, + // Catchall, + // NewOutput, + // NewInput + // > { + // return new ZodObject({ + // ...this._def, + // shape: () => ({ + // ...this._def.shape(), + // ...augmentation, + // }), + // }) as any; + // } + /** + * @deprecated Use `.extend` instead + * */ + this.augment = this.extend; + } + _getCached() { + if (this._cached !== null) + return this._cached; + const shape = this._def.shape(); + const keys = util.objectKeys(shape); + this._cached = { shape, keys }; + return this._cached; + } + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.object) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.object, + received: ctx.parsedType, + }); + return INVALID; + } + const { status, ctx } = this._processInputParams(input); + const { shape, keys: shapeKeys } = this._getCached(); + const extraKeys = []; + if (!(this._def.catchall instanceof ZodNever && this._def.unknownKeys === "strip")) { + for (const key in ctx.data) { + if (!shapeKeys.includes(key)) { + extraKeys.push(key); + } + } + } + const pairs = []; + for (const key of shapeKeys) { + const keyValidator = shape[key]; + const value = ctx.data[key]; + pairs.push({ + key: { status: "valid", value: key }, + value: keyValidator._parse(new ParseInputLazyPath(ctx, value, ctx.path, key)), + alwaysSet: key in ctx.data, + }); + } + if (this._def.catchall instanceof ZodNever) { + const unknownKeys = this._def.unknownKeys; + if (unknownKeys === "passthrough") { + for (const key of extraKeys) { + pairs.push({ + key: { status: "valid", value: key }, + value: { status: "valid", value: ctx.data[key] }, + }); + } + } + else if (unknownKeys === "strict") { + if (extraKeys.length > 0) { + addIssueToContext(ctx, { + code: ZodIssueCode.unrecognized_keys, + keys: extraKeys, + }); + status.dirty(); + } + } + else if (unknownKeys === "strip") { + } + else { + throw new Error(`Internal ZodObject error: invalid unknownKeys value.`); + } + } + else { + // run catchall validation + const catchall = this._def.catchall; + for (const key of extraKeys) { + const value = ctx.data[key]; + pairs.push({ + key: { status: "valid", value: key }, + value: catchall._parse(new ParseInputLazyPath(ctx, value, ctx.path, key) //, ctx.child(key), value, getParsedType(value) + ), + alwaysSet: key in ctx.data, + }); + } + } + if (ctx.common.async) { + return Promise.resolve() + .then(async () => { + const syncPairs = []; + for (const pair of pairs) { + const key = await pair.key; + const value = await pair.value; + syncPairs.push({ + key, + value, + alwaysSet: pair.alwaysSet, + }); + } + return syncPairs; + }) + .then((syncPairs) => { + return ParseStatus.mergeObjectSync(status, syncPairs); + }); + } + else { + return ParseStatus.mergeObjectSync(status, pairs); + } + } + get shape() { + return this._def.shape(); + } + strict(message) { + errorUtil.errToObj; + return new ZodObject({ + ...this._def, + unknownKeys: "strict", + ...(message !== undefined + ? { + errorMap: (issue, ctx) => { + const defaultError = this._def.errorMap?.(issue, ctx).message ?? ctx.defaultError; + if (issue.code === "unrecognized_keys") + return { + message: errorUtil.errToObj(message).message ?? defaultError, + }; + return { + message: defaultError, + }; + }, + } + : {}), + }); + } + strip() { + return new ZodObject({ + ...this._def, + unknownKeys: "strip", + }); + } + passthrough() { + return new ZodObject({ + ...this._def, + unknownKeys: "passthrough", + }); + } + // const AugmentFactory = + // (def: Def) => + // ( + // augmentation: Augmentation + // ): ZodObject< + // extendShape, Augmentation>, + // Def["unknownKeys"], + // Def["catchall"] + // > => { + // return new ZodObject({ + // ...def, + // shape: () => ({ + // ...def.shape(), + // ...augmentation, + // }), + // }) as any; + // }; + extend(augmentation) { + return new ZodObject({ + ...this._def, + shape: () => ({ + ...this._def.shape(), + ...augmentation, + }), + }); + } + /** + * Prior to zod@1.0.12 there was a bug in the + * inferred type of merged objects. Please + * upgrade if you are experiencing issues. + */ + merge(merging) { + const merged = new ZodObject({ + unknownKeys: merging._def.unknownKeys, + catchall: merging._def.catchall, + shape: () => ({ + ...this._def.shape(), + ...merging._def.shape(), + }), + typeName: ZodFirstPartyTypeKind.ZodObject, + }); + return merged; + } + // merge< + // Incoming extends AnyZodObject, + // Augmentation extends Incoming["shape"], + // NewOutput extends { + // [k in keyof Augmentation | keyof Output]: k extends keyof Augmentation + // ? Augmentation[k]["_output"] + // : k extends keyof Output + // ? Output[k] + // : never; + // }, + // NewInput extends { + // [k in keyof Augmentation | keyof Input]: k extends keyof Augmentation + // ? Augmentation[k]["_input"] + // : k extends keyof Input + // ? Input[k] + // : never; + // } + // >( + // merging: Incoming + // ): ZodObject< + // extendShape>, + // Incoming["_def"]["unknownKeys"], + // Incoming["_def"]["catchall"], + // NewOutput, + // NewInput + // > { + // const merged: any = new ZodObject({ + // unknownKeys: merging._def.unknownKeys, + // catchall: merging._def.catchall, + // shape: () => + // objectUtil.mergeShapes(this._def.shape(), merging._def.shape()), + // typeName: ZodFirstPartyTypeKind.ZodObject, + // }) as any; + // return merged; + // } + setKey(key, schema) { + return this.augment({ [key]: schema }); + } + // merge( + // merging: Incoming + // ): //ZodObject = (merging) => { + // ZodObject< + // extendShape>, + // Incoming["_def"]["unknownKeys"], + // Incoming["_def"]["catchall"] + // > { + // // const mergedShape = objectUtil.mergeShapes( + // // this._def.shape(), + // // merging._def.shape() + // // ); + // const merged: any = new ZodObject({ + // unknownKeys: merging._def.unknownKeys, + // catchall: merging._def.catchall, + // shape: () => + // objectUtil.mergeShapes(this._def.shape(), merging._def.shape()), + // typeName: ZodFirstPartyTypeKind.ZodObject, + // }) as any; + // return merged; + // } + catchall(index) { + return new ZodObject({ + ...this._def, + catchall: index, + }); + } + pick(mask) { + const shape = {}; + for (const key of util.objectKeys(mask)) { + if (mask[key] && this.shape[key]) { + shape[key] = this.shape[key]; + } + } + return new ZodObject({ + ...this._def, + shape: () => shape, + }); + } + omit(mask) { + const shape = {}; + for (const key of util.objectKeys(this.shape)) { + if (!mask[key]) { + shape[key] = this.shape[key]; + } + } + return new ZodObject({ + ...this._def, + shape: () => shape, + }); + } + /** + * @deprecated + */ + deepPartial() { + return deepPartialify(this); + } + partial(mask) { + const newShape = {}; + for (const key of util.objectKeys(this.shape)) { + const fieldSchema = this.shape[key]; + if (mask && !mask[key]) { + newShape[key] = fieldSchema; + } + else { + newShape[key] = fieldSchema.optional(); + } + } + return new ZodObject({ + ...this._def, + shape: () => newShape, + }); + } + required(mask) { + const newShape = {}; + for (const key of util.objectKeys(this.shape)) { + if (mask && !mask[key]) { + newShape[key] = this.shape[key]; + } + else { + const fieldSchema = this.shape[key]; + let newField = fieldSchema; + while (newField instanceof ZodOptional) { + newField = newField._def.innerType; + } + newShape[key] = newField; + } + } + return new ZodObject({ + ...this._def, + shape: () => newShape, + }); + } + keyof() { + return createZodEnum(util.objectKeys(this.shape)); + } +} +ZodObject.create = (shape, params) => { + return new ZodObject({ + shape: () => shape, + unknownKeys: "strip", + catchall: ZodNever.create(), + typeName: ZodFirstPartyTypeKind.ZodObject, + ...processCreateParams(params), + }); +}; +ZodObject.strictCreate = (shape, params) => { + return new ZodObject({ + shape: () => shape, + unknownKeys: "strict", + catchall: ZodNever.create(), + typeName: ZodFirstPartyTypeKind.ZodObject, + ...processCreateParams(params), + }); +}; +ZodObject.lazycreate = (shape, params) => { + return new ZodObject({ + shape, + unknownKeys: "strip", + catchall: ZodNever.create(), + typeName: ZodFirstPartyTypeKind.ZodObject, + ...processCreateParams(params), + }); +}; +export class ZodUnion extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + const options = this._def.options; + function handleResults(results) { + // return first issue-free validation if it exists + for (const result of results) { + if (result.result.status === "valid") { + return result.result; + } + } + for (const result of results) { + if (result.result.status === "dirty") { + // add issues from dirty option + ctx.common.issues.push(...result.ctx.common.issues); + return result.result; + } + } + // return invalid + const unionErrors = results.map((result) => new ZodError(result.ctx.common.issues)); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_union, + unionErrors, + }); + return INVALID; + } + if (ctx.common.async) { + return Promise.all(options.map(async (option) => { + const childCtx = { + ...ctx, + common: { + ...ctx.common, + issues: [], + }, + parent: null, + }; + return { + result: await option._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: childCtx, + }), + ctx: childCtx, + }; + })).then(handleResults); + } + else { + let dirty = undefined; + const issues = []; + for (const option of options) { + const childCtx = { + ...ctx, + common: { + ...ctx.common, + issues: [], + }, + parent: null, + }; + const result = option._parseSync({ + data: ctx.data, + path: ctx.path, + parent: childCtx, + }); + if (result.status === "valid") { + return result; + } + else if (result.status === "dirty" && !dirty) { + dirty = { result, ctx: childCtx }; + } + if (childCtx.common.issues.length) { + issues.push(childCtx.common.issues); + } + } + if (dirty) { + ctx.common.issues.push(...dirty.ctx.common.issues); + return dirty.result; + } + const unionErrors = issues.map((issues) => new ZodError(issues)); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_union, + unionErrors, + }); + return INVALID; + } + } + get options() { + return this._def.options; + } +} +ZodUnion.create = (types, params) => { + return new ZodUnion({ + options: types, + typeName: ZodFirstPartyTypeKind.ZodUnion, + ...processCreateParams(params), + }); +}; +///////////////////////////////////////////////////// +///////////////////////////////////////////////////// +////////// ////////// +////////// ZodDiscriminatedUnion ////////// +////////// ////////// +///////////////////////////////////////////////////// +///////////////////////////////////////////////////// +const getDiscriminator = (type) => { + if (type instanceof ZodLazy) { + return getDiscriminator(type.schema); + } + else if (type instanceof ZodEffects) { + return getDiscriminator(type.innerType()); + } + else if (type instanceof ZodLiteral) { + return [type.value]; + } + else if (type instanceof ZodEnum) { + return type.options; + } + else if (type instanceof ZodNativeEnum) { + // eslint-disable-next-line ban/ban + return util.objectValues(type.enum); + } + else if (type instanceof ZodDefault) { + return getDiscriminator(type._def.innerType); + } + else if (type instanceof ZodUndefined) { + return [undefined]; + } + else if (type instanceof ZodNull) { + return [null]; + } + else if (type instanceof ZodOptional) { + return [undefined, ...getDiscriminator(type.unwrap())]; + } + else if (type instanceof ZodNullable) { + return [null, ...getDiscriminator(type.unwrap())]; + } + else if (type instanceof ZodBranded) { + return getDiscriminator(type.unwrap()); + } + else if (type instanceof ZodReadonly) { + return getDiscriminator(type.unwrap()); + } + else if (type instanceof ZodCatch) { + return getDiscriminator(type._def.innerType); + } + else { + return []; + } +}; +export class ZodDiscriminatedUnion extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.object) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.object, + received: ctx.parsedType, + }); + return INVALID; + } + const discriminator = this.discriminator; + const discriminatorValue = ctx.data[discriminator]; + const option = this.optionsMap.get(discriminatorValue); + if (!option) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_union_discriminator, + options: Array.from(this.optionsMap.keys()), + path: [discriminator], + }); + return INVALID; + } + if (ctx.common.async) { + return option._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + } + else { + return option._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + } + } + get discriminator() { + return this._def.discriminator; + } + get options() { + return this._def.options; + } + get optionsMap() { + return this._def.optionsMap; + } + /** + * The constructor of the discriminated union schema. Its behaviour is very similar to that of the normal z.union() constructor. + * However, it only allows a union of objects, all of which need to share a discriminator property. This property must + * have a different value for each object in the union. + * @param discriminator the name of the discriminator property + * @param types an array of object schemas + * @param params + */ + static create(discriminator, options, params) { + // Get all the valid discriminator values + const optionsMap = new Map(); + // try { + for (const type of options) { + const discriminatorValues = getDiscriminator(type.shape[discriminator]); + if (!discriminatorValues.length) { + throw new Error(`A discriminator value for key \`${discriminator}\` could not be extracted from all schema options`); + } + for (const value of discriminatorValues) { + if (optionsMap.has(value)) { + throw new Error(`Discriminator property ${String(discriminator)} has duplicate value ${String(value)}`); + } + optionsMap.set(value, type); + } + } + return new ZodDiscriminatedUnion({ + typeName: ZodFirstPartyTypeKind.ZodDiscriminatedUnion, + discriminator, + options, + optionsMap, + ...processCreateParams(params), + }); + } +} +function mergeValues(a, b) { + const aType = getParsedType(a); + const bType = getParsedType(b); + if (a === b) { + return { valid: true, data: a }; + } + else if (aType === ZodParsedType.object && bType === ZodParsedType.object) { + const bKeys = util.objectKeys(b); + const sharedKeys = util.objectKeys(a).filter((key) => bKeys.indexOf(key) !== -1); + const newObj = { ...a, ...b }; + for (const key of sharedKeys) { + const sharedValue = mergeValues(a[key], b[key]); + if (!sharedValue.valid) { + return { valid: false }; + } + newObj[key] = sharedValue.data; + } + return { valid: true, data: newObj }; + } + else if (aType === ZodParsedType.array && bType === ZodParsedType.array) { + if (a.length !== b.length) { + return { valid: false }; + } + const newArray = []; + for (let index = 0; index < a.length; index++) { + const itemA = a[index]; + const itemB = b[index]; + const sharedValue = mergeValues(itemA, itemB); + if (!sharedValue.valid) { + return { valid: false }; + } + newArray.push(sharedValue.data); + } + return { valid: true, data: newArray }; + } + else if (aType === ZodParsedType.date && bType === ZodParsedType.date && +a === +b) { + return { valid: true, data: a }; + } + else { + return { valid: false }; + } +} +export class ZodIntersection extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + const handleParsed = (parsedLeft, parsedRight) => { + if (isAborted(parsedLeft) || isAborted(parsedRight)) { + return INVALID; + } + const merged = mergeValues(parsedLeft.value, parsedRight.value); + if (!merged.valid) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_intersection_types, + }); + return INVALID; + } + if (isDirty(parsedLeft) || isDirty(parsedRight)) { + status.dirty(); + } + return { status: status.value, value: merged.data }; + }; + if (ctx.common.async) { + return Promise.all([ + this._def.left._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }), + this._def.right._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }), + ]).then(([left, right]) => handleParsed(left, right)); + } + else { + return handleParsed(this._def.left._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }), this._def.right._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + })); + } + } +} +ZodIntersection.create = (left, right, params) => { + return new ZodIntersection({ + left: left, + right: right, + typeName: ZodFirstPartyTypeKind.ZodIntersection, + ...processCreateParams(params), + }); +}; +// type ZodTupleItems = [ZodTypeAny, ...ZodTypeAny[]]; +export class ZodTuple extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.array) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.array, + received: ctx.parsedType, + }); + return INVALID; + } + if (ctx.data.length < this._def.items.length) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: this._def.items.length, + inclusive: true, + exact: false, + type: "array", + }); + return INVALID; + } + const rest = this._def.rest; + if (!rest && ctx.data.length > this._def.items.length) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: this._def.items.length, + inclusive: true, + exact: false, + type: "array", + }); + status.dirty(); + } + const items = [...ctx.data] + .map((item, itemIndex) => { + const schema = this._def.items[itemIndex] || this._def.rest; + if (!schema) + return null; + return schema._parse(new ParseInputLazyPath(ctx, item, ctx.path, itemIndex)); + }) + .filter((x) => !!x); // filter nulls + if (ctx.common.async) { + return Promise.all(items).then((results) => { + return ParseStatus.mergeArray(status, results); + }); + } + else { + return ParseStatus.mergeArray(status, items); + } + } + get items() { + return this._def.items; + } + rest(rest) { + return new ZodTuple({ + ...this._def, + rest, + }); + } +} +ZodTuple.create = (schemas, params) => { + if (!Array.isArray(schemas)) { + throw new Error("You must pass an array of schemas to z.tuple([ ... ])"); + } + return new ZodTuple({ + items: schemas, + typeName: ZodFirstPartyTypeKind.ZodTuple, + rest: null, + ...processCreateParams(params), + }); +}; +export class ZodRecord extends ZodType { + get keySchema() { + return this._def.keyType; + } + get valueSchema() { + return this._def.valueType; + } + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.object) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.object, + received: ctx.parsedType, + }); + return INVALID; + } + const pairs = []; + const keyType = this._def.keyType; + const valueType = this._def.valueType; + for (const key in ctx.data) { + pairs.push({ + key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, key)), + value: valueType._parse(new ParseInputLazyPath(ctx, ctx.data[key], ctx.path, key)), + alwaysSet: key in ctx.data, + }); + } + if (ctx.common.async) { + return ParseStatus.mergeObjectAsync(status, pairs); + } + else { + return ParseStatus.mergeObjectSync(status, pairs); + } + } + get element() { + return this._def.valueType; + } + static create(first, second, third) { + if (second instanceof ZodType) { + return new ZodRecord({ + keyType: first, + valueType: second, + typeName: ZodFirstPartyTypeKind.ZodRecord, + ...processCreateParams(third), + }); + } + return new ZodRecord({ + keyType: ZodString.create(), + valueType: first, + typeName: ZodFirstPartyTypeKind.ZodRecord, + ...processCreateParams(second), + }); + } +} +export class ZodMap extends ZodType { + get keySchema() { + return this._def.keyType; + } + get valueSchema() { + return this._def.valueType; + } + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.map) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.map, + received: ctx.parsedType, + }); + return INVALID; + } + const keyType = this._def.keyType; + const valueType = this._def.valueType; + const pairs = [...ctx.data.entries()].map(([key, value], index) => { + return { + key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, [index, "key"])), + value: valueType._parse(new ParseInputLazyPath(ctx, value, ctx.path, [index, "value"])), + }; + }); + if (ctx.common.async) { + const finalMap = new Map(); + return Promise.resolve().then(async () => { + for (const pair of pairs) { + const key = await pair.key; + const value = await pair.value; + if (key.status === "aborted" || value.status === "aborted") { + return INVALID; + } + if (key.status === "dirty" || value.status === "dirty") { + status.dirty(); + } + finalMap.set(key.value, value.value); + } + return { status: status.value, value: finalMap }; + }); + } + else { + const finalMap = new Map(); + for (const pair of pairs) { + const key = pair.key; + const value = pair.value; + if (key.status === "aborted" || value.status === "aborted") { + return INVALID; + } + if (key.status === "dirty" || value.status === "dirty") { + status.dirty(); + } + finalMap.set(key.value, value.value); + } + return { status: status.value, value: finalMap }; + } + } +} +ZodMap.create = (keyType, valueType, params) => { + return new ZodMap({ + valueType, + keyType, + typeName: ZodFirstPartyTypeKind.ZodMap, + ...processCreateParams(params), + }); +}; +export class ZodSet extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.set) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.set, + received: ctx.parsedType, + }); + return INVALID; + } + const def = this._def; + if (def.minSize !== null) { + if (ctx.data.size < def.minSize.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: def.minSize.value, + type: "set", + inclusive: true, + exact: false, + message: def.minSize.message, + }); + status.dirty(); + } + } + if (def.maxSize !== null) { + if (ctx.data.size > def.maxSize.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: def.maxSize.value, + type: "set", + inclusive: true, + exact: false, + message: def.maxSize.message, + }); + status.dirty(); + } + } + const valueType = this._def.valueType; + function finalizeSet(elements) { + const parsedSet = new Set(); + for (const element of elements) { + if (element.status === "aborted") + return INVALID; + if (element.status === "dirty") + status.dirty(); + parsedSet.add(element.value); + } + return { status: status.value, value: parsedSet }; + } + const elements = [...ctx.data.values()].map((item, i) => valueType._parse(new ParseInputLazyPath(ctx, item, ctx.path, i))); + if (ctx.common.async) { + return Promise.all(elements).then((elements) => finalizeSet(elements)); + } + else { + return finalizeSet(elements); + } + } + min(minSize, message) { + return new ZodSet({ + ...this._def, + minSize: { value: minSize, message: errorUtil.toString(message) }, + }); + } + max(maxSize, message) { + return new ZodSet({ + ...this._def, + maxSize: { value: maxSize, message: errorUtil.toString(message) }, + }); + } + size(size, message) { + return this.min(size, message).max(size, message); + } + nonempty(message) { + return this.min(1, message); + } +} +ZodSet.create = (valueType, params) => { + return new ZodSet({ + valueType, + minSize: null, + maxSize: null, + typeName: ZodFirstPartyTypeKind.ZodSet, + ...processCreateParams(params), + }); +}; +export class ZodFunction extends ZodType { + constructor() { + super(...arguments); + this.validate = this.implement; + } + _parse(input) { + const { ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.function) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.function, + received: ctx.parsedType, + }); + return INVALID; + } + function makeArgsIssue(args, error) { + return makeIssue({ + data: args, + path: ctx.path, + errorMaps: [ctx.common.contextualErrorMap, ctx.schemaErrorMap, getErrorMap(), defaultErrorMap].filter((x) => !!x), + issueData: { + code: ZodIssueCode.invalid_arguments, + argumentsError: error, + }, + }); + } + function makeReturnsIssue(returns, error) { + return makeIssue({ + data: returns, + path: ctx.path, + errorMaps: [ctx.common.contextualErrorMap, ctx.schemaErrorMap, getErrorMap(), defaultErrorMap].filter((x) => !!x), + issueData: { + code: ZodIssueCode.invalid_return_type, + returnTypeError: error, + }, + }); + } + const params = { errorMap: ctx.common.contextualErrorMap }; + const fn = ctx.data; + if (this._def.returns instanceof ZodPromise) { + // Would love a way to avoid disabling this rule, but we need + // an alias (using an arrow function was what caused 2651). + // eslint-disable-next-line @typescript-eslint/no-this-alias + const me = this; + return OK(async function (...args) { + const error = new ZodError([]); + const parsedArgs = await me._def.args.parseAsync(args, params).catch((e) => { + error.addIssue(makeArgsIssue(args, e)); + throw error; + }); + const result = await Reflect.apply(fn, this, parsedArgs); + const parsedReturns = await me._def.returns._def.type + .parseAsync(result, params) + .catch((e) => { + error.addIssue(makeReturnsIssue(result, e)); + throw error; + }); + return parsedReturns; + }); + } + else { + // Would love a way to avoid disabling this rule, but we need + // an alias (using an arrow function was what caused 2651). + // eslint-disable-next-line @typescript-eslint/no-this-alias + const me = this; + return OK(function (...args) { + const parsedArgs = me._def.args.safeParse(args, params); + if (!parsedArgs.success) { + throw new ZodError([makeArgsIssue(args, parsedArgs.error)]); + } + const result = Reflect.apply(fn, this, parsedArgs.data); + const parsedReturns = me._def.returns.safeParse(result, params); + if (!parsedReturns.success) { + throw new ZodError([makeReturnsIssue(result, parsedReturns.error)]); + } + return parsedReturns.data; + }); + } + } + parameters() { + return this._def.args; + } + returnType() { + return this._def.returns; + } + args(...items) { + return new ZodFunction({ + ...this._def, + args: ZodTuple.create(items).rest(ZodUnknown.create()), + }); + } + returns(returnType) { + return new ZodFunction({ + ...this._def, + returns: returnType, + }); + } + implement(func) { + const validatedFunc = this.parse(func); + return validatedFunc; + } + strictImplement(func) { + const validatedFunc = this.parse(func); + return validatedFunc; + } + static create(args, returns, params) { + return new ZodFunction({ + args: (args ? args : ZodTuple.create([]).rest(ZodUnknown.create())), + returns: returns || ZodUnknown.create(), + typeName: ZodFirstPartyTypeKind.ZodFunction, + ...processCreateParams(params), + }); + } +} +export class ZodLazy extends ZodType { + get schema() { + return this._def.getter(); + } + _parse(input) { + const { ctx } = this._processInputParams(input); + const lazySchema = this._def.getter(); + return lazySchema._parse({ data: ctx.data, path: ctx.path, parent: ctx }); + } +} +ZodLazy.create = (getter, params) => { + return new ZodLazy({ + getter: getter, + typeName: ZodFirstPartyTypeKind.ZodLazy, + ...processCreateParams(params), + }); +}; +export class ZodLiteral extends ZodType { + _parse(input) { + if (input.data !== this._def.value) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + received: ctx.data, + code: ZodIssueCode.invalid_literal, + expected: this._def.value, + }); + return INVALID; + } + return { status: "valid", value: input.data }; + } + get value() { + return this._def.value; + } +} +ZodLiteral.create = (value, params) => { + return new ZodLiteral({ + value: value, + typeName: ZodFirstPartyTypeKind.ZodLiteral, + ...processCreateParams(params), + }); +}; +function createZodEnum(values, params) { + return new ZodEnum({ + values, + typeName: ZodFirstPartyTypeKind.ZodEnum, + ...processCreateParams(params), + }); +} +export class ZodEnum extends ZodType { + _parse(input) { + if (typeof input.data !== "string") { + const ctx = this._getOrReturnCtx(input); + const expectedValues = this._def.values; + addIssueToContext(ctx, { + expected: util.joinValues(expectedValues), + received: ctx.parsedType, + code: ZodIssueCode.invalid_type, + }); + return INVALID; + } + if (!this._cache) { + this._cache = new Set(this._def.values); + } + if (!this._cache.has(input.data)) { + const ctx = this._getOrReturnCtx(input); + const expectedValues = this._def.values; + addIssueToContext(ctx, { + received: ctx.data, + code: ZodIssueCode.invalid_enum_value, + options: expectedValues, + }); + return INVALID; + } + return OK(input.data); + } + get options() { + return this._def.values; + } + get enum() { + const enumValues = {}; + for (const val of this._def.values) { + enumValues[val] = val; + } + return enumValues; + } + get Values() { + const enumValues = {}; + for (const val of this._def.values) { + enumValues[val] = val; + } + return enumValues; + } + get Enum() { + const enumValues = {}; + for (const val of this._def.values) { + enumValues[val] = val; + } + return enumValues; + } + extract(values, newDef = this._def) { + return ZodEnum.create(values, { + ...this._def, + ...newDef, + }); + } + exclude(values, newDef = this._def) { + return ZodEnum.create(this.options.filter((opt) => !values.includes(opt)), { + ...this._def, + ...newDef, + }); + } +} +ZodEnum.create = createZodEnum; +export class ZodNativeEnum extends ZodType { + _parse(input) { + const nativeEnumValues = util.getValidEnumValues(this._def.values); + const ctx = this._getOrReturnCtx(input); + if (ctx.parsedType !== ZodParsedType.string && ctx.parsedType !== ZodParsedType.number) { + const expectedValues = util.objectValues(nativeEnumValues); + addIssueToContext(ctx, { + expected: util.joinValues(expectedValues), + received: ctx.parsedType, + code: ZodIssueCode.invalid_type, + }); + return INVALID; + } + if (!this._cache) { + this._cache = new Set(util.getValidEnumValues(this._def.values)); + } + if (!this._cache.has(input.data)) { + const expectedValues = util.objectValues(nativeEnumValues); + addIssueToContext(ctx, { + received: ctx.data, + code: ZodIssueCode.invalid_enum_value, + options: expectedValues, + }); + return INVALID; + } + return OK(input.data); + } + get enum() { + return this._def.values; + } +} +ZodNativeEnum.create = (values, params) => { + return new ZodNativeEnum({ + values: values, + typeName: ZodFirstPartyTypeKind.ZodNativeEnum, + ...processCreateParams(params), + }); +}; +export class ZodPromise extends ZodType { + unwrap() { + return this._def.type; + } + _parse(input) { + const { ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.promise && ctx.common.async === false) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.promise, + received: ctx.parsedType, + }); + return INVALID; + } + const promisified = ctx.parsedType === ZodParsedType.promise ? ctx.data : Promise.resolve(ctx.data); + return OK(promisified.then((data) => { + return this._def.type.parseAsync(data, { + path: ctx.path, + errorMap: ctx.common.contextualErrorMap, + }); + })); + } +} +ZodPromise.create = (schema, params) => { + return new ZodPromise({ + type: schema, + typeName: ZodFirstPartyTypeKind.ZodPromise, + ...processCreateParams(params), + }); +}; +export class ZodEffects extends ZodType { + innerType() { + return this._def.schema; + } + sourceType() { + return this._def.schema._def.typeName === ZodFirstPartyTypeKind.ZodEffects + ? this._def.schema.sourceType() + : this._def.schema; + } + _parse(input) { + const { status, ctx } = this._processInputParams(input); + const effect = this._def.effect || null; + const checkCtx = { + addIssue: (arg) => { + addIssueToContext(ctx, arg); + if (arg.fatal) { + status.abort(); + } + else { + status.dirty(); + } + }, + get path() { + return ctx.path; + }, + }; + checkCtx.addIssue = checkCtx.addIssue.bind(checkCtx); + if (effect.type === "preprocess") { + const processed = effect.transform(ctx.data, checkCtx); + if (ctx.common.async) { + return Promise.resolve(processed).then(async (processed) => { + if (status.value === "aborted") + return INVALID; + const result = await this._def.schema._parseAsync({ + data: processed, + path: ctx.path, + parent: ctx, + }); + if (result.status === "aborted") + return INVALID; + if (result.status === "dirty") + return DIRTY(result.value); + if (status.value === "dirty") + return DIRTY(result.value); + return result; + }); + } + else { + if (status.value === "aborted") + return INVALID; + const result = this._def.schema._parseSync({ + data: processed, + path: ctx.path, + parent: ctx, + }); + if (result.status === "aborted") + return INVALID; + if (result.status === "dirty") + return DIRTY(result.value); + if (status.value === "dirty") + return DIRTY(result.value); + return result; + } + } + if (effect.type === "refinement") { + const executeRefinement = (acc) => { + const result = effect.refinement(acc, checkCtx); + if (ctx.common.async) { + return Promise.resolve(result); + } + if (result instanceof Promise) { + throw new Error("Async refinement encountered during synchronous parse operation. Use .parseAsync instead."); + } + return acc; + }; + if (ctx.common.async === false) { + const inner = this._def.schema._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + if (inner.status === "aborted") + return INVALID; + if (inner.status === "dirty") + status.dirty(); + // return value is ignored + executeRefinement(inner.value); + return { status: status.value, value: inner.value }; + } + else { + return this._def.schema._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }).then((inner) => { + if (inner.status === "aborted") + return INVALID; + if (inner.status === "dirty") + status.dirty(); + return executeRefinement(inner.value).then(() => { + return { status: status.value, value: inner.value }; + }); + }); + } + } + if (effect.type === "transform") { + if (ctx.common.async === false) { + const base = this._def.schema._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + if (!isValid(base)) + return INVALID; + const result = effect.transform(base.value, checkCtx); + if (result instanceof Promise) { + throw new Error(`Asynchronous transform encountered during synchronous parse operation. Use .parseAsync instead.`); + } + return { status: status.value, value: result }; + } + else { + return this._def.schema._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }).then((base) => { + if (!isValid(base)) + return INVALID; + return Promise.resolve(effect.transform(base.value, checkCtx)).then((result) => ({ + status: status.value, + value: result, + })); + }); + } + } + util.assertNever(effect); + } +} +ZodEffects.create = (schema, effect, params) => { + return new ZodEffects({ + schema, + typeName: ZodFirstPartyTypeKind.ZodEffects, + effect, + ...processCreateParams(params), + }); +}; +ZodEffects.createWithPreprocess = (preprocess, schema, params) => { + return new ZodEffects({ + schema, + effect: { type: "preprocess", transform: preprocess }, + typeName: ZodFirstPartyTypeKind.ZodEffects, + ...processCreateParams(params), + }); +}; +export { ZodEffects as ZodTransformer }; +export class ZodOptional extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType === ZodParsedType.undefined) { + return OK(undefined); + } + return this._def.innerType._parse(input); + } + unwrap() { + return this._def.innerType; + } +} +ZodOptional.create = (type, params) => { + return new ZodOptional({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodOptional, + ...processCreateParams(params), + }); +}; +export class ZodNullable extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType === ZodParsedType.null) { + return OK(null); + } + return this._def.innerType._parse(input); + } + unwrap() { + return this._def.innerType; + } +} +ZodNullable.create = (type, params) => { + return new ZodNullable({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodNullable, + ...processCreateParams(params), + }); +}; +export class ZodDefault extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + let data = ctx.data; + if (ctx.parsedType === ZodParsedType.undefined) { + data = this._def.defaultValue(); + } + return this._def.innerType._parse({ + data, + path: ctx.path, + parent: ctx, + }); + } + removeDefault() { + return this._def.innerType; + } +} +ZodDefault.create = (type, params) => { + return new ZodDefault({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodDefault, + defaultValue: typeof params.default === "function" ? params.default : () => params.default, + ...processCreateParams(params), + }); +}; +export class ZodCatch extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + // newCtx is used to not collect issues from inner types in ctx + const newCtx = { + ...ctx, + common: { + ...ctx.common, + issues: [], + }, + }; + const result = this._def.innerType._parse({ + data: newCtx.data, + path: newCtx.path, + parent: { + ...newCtx, + }, + }); + if (isAsync(result)) { + return result.then((result) => { + return { + status: "valid", + value: result.status === "valid" + ? result.value + : this._def.catchValue({ + get error() { + return new ZodError(newCtx.common.issues); + }, + input: newCtx.data, + }), + }; + }); + } + else { + return { + status: "valid", + value: result.status === "valid" + ? result.value + : this._def.catchValue({ + get error() { + return new ZodError(newCtx.common.issues); + }, + input: newCtx.data, + }), + }; + } + } + removeCatch() { + return this._def.innerType; + } +} +ZodCatch.create = (type, params) => { + return new ZodCatch({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodCatch, + catchValue: typeof params.catch === "function" ? params.catch : () => params.catch, + ...processCreateParams(params), + }); +}; +export class ZodNaN extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.nan) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.nan, + received: ctx.parsedType, + }); + return INVALID; + } + return { status: "valid", value: input.data }; + } +} +ZodNaN.create = (params) => { + return new ZodNaN({ + typeName: ZodFirstPartyTypeKind.ZodNaN, + ...processCreateParams(params), + }); +}; +export const BRAND = Symbol("zod_brand"); +export class ZodBranded extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + const data = ctx.data; + return this._def.type._parse({ + data, + path: ctx.path, + parent: ctx, + }); + } + unwrap() { + return this._def.type; + } +} +export class ZodPipeline extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.common.async) { + const handleAsync = async () => { + const inResult = await this._def.in._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + if (inResult.status === "aborted") + return INVALID; + if (inResult.status === "dirty") { + status.dirty(); + return DIRTY(inResult.value); + } + else { + return this._def.out._parseAsync({ + data: inResult.value, + path: ctx.path, + parent: ctx, + }); + } + }; + return handleAsync(); + } + else { + const inResult = this._def.in._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + if (inResult.status === "aborted") + return INVALID; + if (inResult.status === "dirty") { + status.dirty(); + return { + status: "dirty", + value: inResult.value, + }; + } + else { + return this._def.out._parseSync({ + data: inResult.value, + path: ctx.path, + parent: ctx, + }); + } + } + } + static create(a, b) { + return new ZodPipeline({ + in: a, + out: b, + typeName: ZodFirstPartyTypeKind.ZodPipeline, + }); + } +} +export class ZodReadonly extends ZodType { + _parse(input) { + const result = this._def.innerType._parse(input); + const freeze = (data) => { + if (isValid(data)) { + data.value = Object.freeze(data.value); + } + return data; + }; + return isAsync(result) ? result.then((data) => freeze(data)) : freeze(result); + } + unwrap() { + return this._def.innerType; + } +} +ZodReadonly.create = (type, params) => { + return new ZodReadonly({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodReadonly, + ...processCreateParams(params), + }); +}; +//////////////////////////////////////// +//////////////////////////////////////// +////////// ////////// +////////// z.custom ////////// +////////// ////////// +//////////////////////////////////////// +//////////////////////////////////////// +function cleanParams(params, data) { + const p = typeof params === "function" ? params(data) : typeof params === "string" ? { message: params } : params; + const p2 = typeof p === "string" ? { message: p } : p; + return p2; +} +export function custom(check, _params = {}, +/** + * @deprecated + * + * Pass `fatal` into the params object instead: + * + * ```ts + * z.string().custom((val) => val.length > 5, { fatal: false }) + * ``` + * + */ +fatal) { + if (check) + return ZodAny.create().superRefine((data, ctx) => { + const r = check(data); + if (r instanceof Promise) { + return r.then((r) => { + if (!r) { + const params = cleanParams(_params, data); + const _fatal = params.fatal ?? fatal ?? true; + ctx.addIssue({ code: "custom", ...params, fatal: _fatal }); + } + }); + } + if (!r) { + const params = cleanParams(_params, data); + const _fatal = params.fatal ?? fatal ?? true; + ctx.addIssue({ code: "custom", ...params, fatal: _fatal }); + } + return; + }); + return ZodAny.create(); +} +export { ZodType as Schema, ZodType as ZodSchema }; +export const late = { + object: ZodObject.lazycreate, +}; +export var ZodFirstPartyTypeKind; +(function (ZodFirstPartyTypeKind) { + ZodFirstPartyTypeKind["ZodString"] = "ZodString"; + ZodFirstPartyTypeKind["ZodNumber"] = "ZodNumber"; + ZodFirstPartyTypeKind["ZodNaN"] = "ZodNaN"; + ZodFirstPartyTypeKind["ZodBigInt"] = "ZodBigInt"; + ZodFirstPartyTypeKind["ZodBoolean"] = "ZodBoolean"; + ZodFirstPartyTypeKind["ZodDate"] = "ZodDate"; + ZodFirstPartyTypeKind["ZodSymbol"] = "ZodSymbol"; + ZodFirstPartyTypeKind["ZodUndefined"] = "ZodUndefined"; + ZodFirstPartyTypeKind["ZodNull"] = "ZodNull"; + ZodFirstPartyTypeKind["ZodAny"] = "ZodAny"; + ZodFirstPartyTypeKind["ZodUnknown"] = "ZodUnknown"; + ZodFirstPartyTypeKind["ZodNever"] = "ZodNever"; + ZodFirstPartyTypeKind["ZodVoid"] = "ZodVoid"; + ZodFirstPartyTypeKind["ZodArray"] = "ZodArray"; + ZodFirstPartyTypeKind["ZodObject"] = "ZodObject"; + ZodFirstPartyTypeKind["ZodUnion"] = "ZodUnion"; + ZodFirstPartyTypeKind["ZodDiscriminatedUnion"] = "ZodDiscriminatedUnion"; + ZodFirstPartyTypeKind["ZodIntersection"] = "ZodIntersection"; + ZodFirstPartyTypeKind["ZodTuple"] = "ZodTuple"; + ZodFirstPartyTypeKind["ZodRecord"] = "ZodRecord"; + ZodFirstPartyTypeKind["ZodMap"] = "ZodMap"; + ZodFirstPartyTypeKind["ZodSet"] = "ZodSet"; + ZodFirstPartyTypeKind["ZodFunction"] = "ZodFunction"; + ZodFirstPartyTypeKind["ZodLazy"] = "ZodLazy"; + ZodFirstPartyTypeKind["ZodLiteral"] = "ZodLiteral"; + ZodFirstPartyTypeKind["ZodEnum"] = "ZodEnum"; + ZodFirstPartyTypeKind["ZodEffects"] = "ZodEffects"; + ZodFirstPartyTypeKind["ZodNativeEnum"] = "ZodNativeEnum"; + ZodFirstPartyTypeKind["ZodOptional"] = "ZodOptional"; + ZodFirstPartyTypeKind["ZodNullable"] = "ZodNullable"; + ZodFirstPartyTypeKind["ZodDefault"] = "ZodDefault"; + ZodFirstPartyTypeKind["ZodCatch"] = "ZodCatch"; + ZodFirstPartyTypeKind["ZodPromise"] = "ZodPromise"; + ZodFirstPartyTypeKind["ZodBranded"] = "ZodBranded"; + ZodFirstPartyTypeKind["ZodPipeline"] = "ZodPipeline"; + ZodFirstPartyTypeKind["ZodReadonly"] = "ZodReadonly"; +})(ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {})); +// requires TS 4.4+ +class Class { + constructor(..._) { } +} +const instanceOfType = ( +// const instanceOfType = any>( +cls, params = { + message: `Input not instance of ${cls.name}`, +}) => custom((data) => data instanceof cls, params); +const stringType = ZodString.create; +const numberType = ZodNumber.create; +const nanType = ZodNaN.create; +const bigIntType = ZodBigInt.create; +const booleanType = ZodBoolean.create; +const dateType = ZodDate.create; +const symbolType = ZodSymbol.create; +const undefinedType = ZodUndefined.create; +const nullType = ZodNull.create; +const anyType = ZodAny.create; +const unknownType = ZodUnknown.create; +const neverType = ZodNever.create; +const voidType = ZodVoid.create; +const arrayType = ZodArray.create; +const objectType = ZodObject.create; +const strictObjectType = ZodObject.strictCreate; +const unionType = ZodUnion.create; +const discriminatedUnionType = ZodDiscriminatedUnion.create; +const intersectionType = ZodIntersection.create; +const tupleType = ZodTuple.create; +const recordType = ZodRecord.create; +const mapType = ZodMap.create; +const setType = ZodSet.create; +const functionType = ZodFunction.create; +const lazyType = ZodLazy.create; +const literalType = ZodLiteral.create; +const enumType = ZodEnum.create; +const nativeEnumType = ZodNativeEnum.create; +const promiseType = ZodPromise.create; +const effectsType = ZodEffects.create; +const optionalType = ZodOptional.create; +const nullableType = ZodNullable.create; +const preprocessType = ZodEffects.createWithPreprocess; +const pipelineType = ZodPipeline.create; +const ostring = () => stringType().optional(); +const onumber = () => numberType().optional(); +const oboolean = () => booleanType().optional(); +export const coerce = { + string: ((arg) => ZodString.create({ ...arg, coerce: true })), + number: ((arg) => ZodNumber.create({ ...arg, coerce: true })), + boolean: ((arg) => ZodBoolean.create({ + ...arg, + coerce: true, + })), + bigint: ((arg) => ZodBigInt.create({ ...arg, coerce: true })), + date: ((arg) => ZodDate.create({ ...arg, coerce: true })), +}; +export { anyType as any, arrayType as array, bigIntType as bigint, booleanType as boolean, dateType as date, discriminatedUnionType as discriminatedUnion, effectsType as effect, enumType as enum, functionType as function, instanceOfType as instanceof, intersectionType as intersection, lazyType as lazy, literalType as literal, mapType as map, nanType as nan, nativeEnumType as nativeEnum, neverType as never, nullType as null, nullableType as nullable, numberType as number, objectType as object, oboolean, onumber, optionalType as optional, ostring, pipelineType as pipeline, preprocessType as preprocess, promiseType as promise, recordType as record, setType as set, strictObjectType as strictObject, stringType as string, symbolType as symbol, effectsType as transformer, tupleType as tuple, undefinedType as undefined, unionType as union, unknownType as unknown, voidType as void, }; +export const NEVER = INVALID; diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/index.d.ts b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/index.d.ts new file mode 100644 index 000000000..8352ab713 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/index.d.ts @@ -0,0 +1,3 @@ +import z3 from "./v3/index.js"; +export * from "./v3/index.js"; +export default z3; diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/ZodError.d.ts b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/ZodError.d.ts new file mode 100644 index 000000000..4e71355a3 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/ZodError.d.ts @@ -0,0 +1,164 @@ +import type { Primitive } from "./helpers/typeAliases.js"; +import { util, type ZodParsedType } from "./helpers/util.js"; +import type { TypeOf, ZodType } from "./index.js"; +type allKeys = T extends any ? keyof T : never; +export type inferFlattenedErrors, U = string> = typeToFlattenedError, U>; +export type typeToFlattenedError = { + formErrors: U[]; + fieldErrors: { + [P in allKeys]?: U[]; + }; +}; +export declare const ZodIssueCode: { + invalid_type: "invalid_type"; + invalid_literal: "invalid_literal"; + custom: "custom"; + invalid_union: "invalid_union"; + invalid_union_discriminator: "invalid_union_discriminator"; + invalid_enum_value: "invalid_enum_value"; + unrecognized_keys: "unrecognized_keys"; + invalid_arguments: "invalid_arguments"; + invalid_return_type: "invalid_return_type"; + invalid_date: "invalid_date"; + invalid_string: "invalid_string"; + too_small: "too_small"; + too_big: "too_big"; + invalid_intersection_types: "invalid_intersection_types"; + not_multiple_of: "not_multiple_of"; + not_finite: "not_finite"; +}; +export type ZodIssueCode = keyof typeof ZodIssueCode; +export type ZodIssueBase = { + path: (string | number)[]; + message?: string | undefined; +}; +export interface ZodInvalidTypeIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_type; + expected: ZodParsedType; + received: ZodParsedType; +} +export interface ZodInvalidLiteralIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_literal; + expected: unknown; + received: unknown; +} +export interface ZodUnrecognizedKeysIssue extends ZodIssueBase { + code: typeof ZodIssueCode.unrecognized_keys; + keys: string[]; +} +export interface ZodInvalidUnionIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_union; + unionErrors: ZodError[]; +} +export interface ZodInvalidUnionDiscriminatorIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_union_discriminator; + options: Primitive[]; +} +export interface ZodInvalidEnumValueIssue extends ZodIssueBase { + received: string | number; + code: typeof ZodIssueCode.invalid_enum_value; + options: (string | number)[]; +} +export interface ZodInvalidArgumentsIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_arguments; + argumentsError: ZodError; +} +export interface ZodInvalidReturnTypeIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_return_type; + returnTypeError: ZodError; +} +export interface ZodInvalidDateIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_date; +} +export type StringValidation = "email" | "url" | "emoji" | "uuid" | "nanoid" | "regex" | "cuid" | "cuid2" | "ulid" | "datetime" | "date" | "time" | "duration" | "ip" | "cidr" | "base64" | "jwt" | "base64url" | { + includes: string; + position?: number | undefined; +} | { + startsWith: string; +} | { + endsWith: string; +}; +export interface ZodInvalidStringIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_string; + validation: StringValidation; +} +export interface ZodTooSmallIssue extends ZodIssueBase { + code: typeof ZodIssueCode.too_small; + minimum: number | bigint; + inclusive: boolean; + exact?: boolean; + type: "array" | "string" | "number" | "set" | "date" | "bigint"; +} +export interface ZodTooBigIssue extends ZodIssueBase { + code: typeof ZodIssueCode.too_big; + maximum: number | bigint; + inclusive: boolean; + exact?: boolean; + type: "array" | "string" | "number" | "set" | "date" | "bigint"; +} +export interface ZodInvalidIntersectionTypesIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_intersection_types; +} +export interface ZodNotMultipleOfIssue extends ZodIssueBase { + code: typeof ZodIssueCode.not_multiple_of; + multipleOf: number | bigint; +} +export interface ZodNotFiniteIssue extends ZodIssueBase { + code: typeof ZodIssueCode.not_finite; +} +export interface ZodCustomIssue extends ZodIssueBase { + code: typeof ZodIssueCode.custom; + params?: { + [k: string]: any; + }; +} +export type DenormalizedError = { + [k: string]: DenormalizedError | string[]; +}; +export type ZodIssueOptionalMessage = ZodInvalidTypeIssue | ZodInvalidLiteralIssue | ZodUnrecognizedKeysIssue | ZodInvalidUnionIssue | ZodInvalidUnionDiscriminatorIssue | ZodInvalidEnumValueIssue | ZodInvalidArgumentsIssue | ZodInvalidReturnTypeIssue | ZodInvalidDateIssue | ZodInvalidStringIssue | ZodTooSmallIssue | ZodTooBigIssue | ZodInvalidIntersectionTypesIssue | ZodNotMultipleOfIssue | ZodNotFiniteIssue | ZodCustomIssue; +export type ZodIssue = ZodIssueOptionalMessage & { + fatal?: boolean | undefined; + message: string; +}; +export declare const quotelessJson: (obj: any) => string; +type recursiveZodFormattedError = T extends [any, ...any[]] ? { + [K in keyof T]?: ZodFormattedError; +} : T extends any[] ? { + [k: number]: ZodFormattedError; +} : T extends object ? { + [K in keyof T]?: ZodFormattedError; +} : unknown; +export type ZodFormattedError = { + _errors: U[]; +} & recursiveZodFormattedError>; +export type inferFormattedError, U = string> = ZodFormattedError, U>; +export declare class ZodError extends Error { + issues: ZodIssue[]; + get errors(): ZodIssue[]; + constructor(issues: ZodIssue[]); + format(): ZodFormattedError; + format(mapper: (issue: ZodIssue) => U): ZodFormattedError; + static create: (issues: ZodIssue[]) => ZodError; + static assert(value: unknown): asserts value is ZodError; + toString(): string; + get message(): string; + get isEmpty(): boolean; + addIssue: (sub: ZodIssue) => void; + addIssues: (subs?: ZodIssue[]) => void; + flatten(): typeToFlattenedError; + flatten(mapper?: (issue: ZodIssue) => U): typeToFlattenedError; + get formErrors(): typeToFlattenedError; +} +type stripPath = T extends any ? util.OmitKeys : never; +export type IssueData = stripPath & { + path?: (string | number)[]; + fatal?: boolean | undefined; +}; +export type ErrorMapCtx = { + defaultError: string; + data: any; +}; +export type ZodErrorMap = (issue: ZodIssueOptionalMessage, _ctx: ErrorMapCtx) => { + message: string; +}; +export {}; diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/errors.d.ts b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/errors.d.ts new file mode 100644 index 000000000..52eacf72e --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/errors.d.ts @@ -0,0 +1,5 @@ +import type { ZodErrorMap } from "./ZodError.js"; +import defaultErrorMap from "./locales/en.js"; +export { defaultErrorMap }; +export declare function setErrorMap(map: ZodErrorMap): void; +export declare function getErrorMap(): ZodErrorMap; diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/external.d.ts b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/external.d.ts new file mode 100644 index 000000000..f0a4be4c7 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/external.d.ts @@ -0,0 +1,6 @@ +export * from "./errors.js"; +export * from "./helpers/parseUtil.js"; +export * from "./helpers/typeAliases.js"; +export * from "./helpers/util.js"; +export * from "./types.js"; +export * from "./ZodError.js"; diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/helpers/enumUtil.d.ts b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/helpers/enumUtil.d.ts new file mode 100644 index 000000000..fad28a711 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/helpers/enumUtil.d.ts @@ -0,0 +1,8 @@ +export declare namespace enumUtil { + type UnionToIntersectionFn = (T extends unknown ? (k: () => T) => void : never) extends (k: infer Intersection) => void ? Intersection : never; + type GetUnionLast = UnionToIntersectionFn extends () => infer Last ? Last : never; + type UnionToTuple = [T] extends [never] ? Tuple : UnionToTuple>, [GetUnionLast, ...Tuple]>; + type CastToStringTuple = T extends [string, ...string[]] ? T : never; + export type UnionToTupleString = CastToStringTuple>; + export {}; +} diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/helpers/errorUtil.d.ts b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/helpers/errorUtil.d.ts new file mode 100644 index 000000000..bc9b033ff --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/helpers/errorUtil.d.ts @@ -0,0 +1,9 @@ +export declare namespace errorUtil { + type ErrMessage = string | { + message?: string | undefined; + }; + const errToObj: (message?: ErrMessage) => { + message?: string | undefined; + }; + const toString: (message?: ErrMessage) => string | undefined; +} diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/helpers/parseUtil.d.ts b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/helpers/parseUtil.d.ts new file mode 100644 index 000000000..e4dc1d7b3 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/helpers/parseUtil.d.ts @@ -0,0 +1,78 @@ +import type { IssueData, ZodErrorMap, ZodIssue } from "../ZodError.js"; +import type { ZodParsedType } from "./util.js"; +export declare const makeIssue: (params: { + data: any; + path: (string | number)[]; + errorMaps: ZodErrorMap[]; + issueData: IssueData; +}) => ZodIssue; +export type ParseParams = { + path: (string | number)[]; + errorMap: ZodErrorMap; + async: boolean; +}; +export type ParsePathComponent = string | number; +export type ParsePath = ParsePathComponent[]; +export declare const EMPTY_PATH: ParsePath; +export interface ParseContext { + readonly common: { + readonly issues: ZodIssue[]; + readonly contextualErrorMap?: ZodErrorMap | undefined; + readonly async: boolean; + }; + readonly path: ParsePath; + readonly schemaErrorMap?: ZodErrorMap | undefined; + readonly parent: ParseContext | null; + readonly data: any; + readonly parsedType: ZodParsedType; +} +export type ParseInput = { + data: any; + path: (string | number)[]; + parent: ParseContext; +}; +export declare function addIssueToContext(ctx: ParseContext, issueData: IssueData): void; +export type ObjectPair = { + key: SyncParseReturnType; + value: SyncParseReturnType; +}; +export declare class ParseStatus { + value: "aborted" | "dirty" | "valid"; + dirty(): void; + abort(): void; + static mergeArray(status: ParseStatus, results: SyncParseReturnType[]): SyncParseReturnType; + static mergeObjectAsync(status: ParseStatus, pairs: { + key: ParseReturnType; + value: ParseReturnType; + }[]): Promise>; + static mergeObjectSync(status: ParseStatus, pairs: { + key: SyncParseReturnType; + value: SyncParseReturnType; + alwaysSet?: boolean; + }[]): SyncParseReturnType; +} +export interface ParseResult { + status: "aborted" | "dirty" | "valid"; + data: any; +} +export type INVALID = { + status: "aborted"; +}; +export declare const INVALID: INVALID; +export type DIRTY = { + status: "dirty"; + value: T; +}; +export declare const DIRTY: (value: T) => DIRTY; +export type OK = { + status: "valid"; + value: T; +}; +export declare const OK: (value: T) => OK; +export type SyncParseReturnType = OK | DIRTY | INVALID; +export type AsyncParseReturnType = Promise>; +export type ParseReturnType = SyncParseReturnType | AsyncParseReturnType; +export declare const isAborted: (x: ParseReturnType) => x is INVALID; +export declare const isDirty: (x: ParseReturnType) => x is OK | DIRTY; +export declare const isValid: (x: ParseReturnType) => x is OK; +export declare const isAsync: (x: ParseReturnType) => x is AsyncParseReturnType; diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/helpers/partialUtil.d.ts b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/helpers/partialUtil.d.ts new file mode 100644 index 000000000..d07657a25 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/helpers/partialUtil.d.ts @@ -0,0 +1,8 @@ +import type { ZodArray, ZodNullable, ZodObject, ZodOptional, ZodRawShape, ZodTuple, ZodTupleItems, ZodTypeAny } from "../types.js"; +export declare namespace partialUtil { + type DeepPartial = T extends ZodObject ? ZodObject<{ + [k in keyof T["shape"]]: ZodOptional>; + }, T["_def"]["unknownKeys"], T["_def"]["catchall"]> : T extends ZodArray ? ZodArray, Card> : T extends ZodOptional ? ZodOptional> : T extends ZodNullable ? ZodNullable> : T extends ZodTuple ? { + [k in keyof Items]: Items[k] extends ZodTypeAny ? DeepPartial : never; + } extends infer PI ? PI extends ZodTupleItems ? ZodTuple : never : never : T; +} diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/helpers/typeAliases.d.ts b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/helpers/typeAliases.d.ts new file mode 100644 index 000000000..32df022ca --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/helpers/typeAliases.d.ts @@ -0,0 +1,2 @@ +export type Primitive = string | number | symbol | bigint | boolean | null | undefined; +export type Scalars = Primitive | Primitive[]; diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/helpers/util.d.ts b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/helpers/util.d.ts new file mode 100644 index 000000000..eeaaeee1c --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/helpers/util.d.ts @@ -0,0 +1,85 @@ +export declare namespace util { + type AssertEqual = (() => V extends T ? 1 : 2) extends () => V extends U ? 1 : 2 ? true : false; + export type isAny = 0 extends 1 & T ? true : false; + export const assertEqual: (_: AssertEqual) => void; + export function assertIs(_arg: T): void; + export function assertNever(_x: never): never; + export type Omit = Pick>; + export type OmitKeys = Pick>; + export type MakePartial = Omit & Partial>; + export type Exactly = T & Record, never>; + export type InexactPartial = { + [k in keyof T]?: T[k] | undefined; + }; + export const arrayToEnum: (items: U) => { [k in U[number]]: k; }; + export const getValidEnumValues: (obj: any) => any[]; + export const objectValues: (obj: any) => any[]; + export const objectKeys: ObjectConstructor["keys"]; + export const find: (arr: T[], checker: (arg: T) => any) => T | undefined; + export type identity = objectUtil.identity; + export type flatten = objectUtil.flatten; + export type noUndefined = T extends undefined ? never : T; + export const isInteger: NumberConstructor["isInteger"]; + export function joinValues(array: T, separator?: string): string; + export const jsonStringifyReplacer: (_: string, value: any) => any; + export {}; +} +export declare namespace objectUtil { + export type MergeShapes = keyof U & keyof V extends never ? U & V : { + [k in Exclude]: U[k]; + } & V; + type optionalKeys = { + [k in keyof T]: undefined extends T[k] ? k : never; + }[keyof T]; + type requiredKeys = { + [k in keyof T]: undefined extends T[k] ? never : k; + }[keyof T]; + export type addQuestionMarks = { + [K in requiredKeys]: T[K]; + } & { + [K in optionalKeys]?: T[K]; + } & { + [k in keyof T]?: unknown; + }; + export type identity = T; + export type flatten = identity<{ + [k in keyof T]: T[k]; + }>; + export type noNeverKeys = { + [k in keyof T]: [T[k]] extends [never] ? never : k; + }[keyof T]; + export type noNever = identity<{ + [k in noNeverKeys]: k extends keyof T ? T[k] : never; + }>; + export const mergeShapes: (first: U, second: T) => T & U; + export type extendShape = keyof A & keyof B extends never ? A & B : { + [K in keyof A as K extends keyof B ? never : K]: A[K]; + } & { + [K in keyof B]: B[K]; + }; + export {}; +} +export declare const ZodParsedType: { + string: "string"; + nan: "nan"; + number: "number"; + integer: "integer"; + float: "float"; + boolean: "boolean"; + date: "date"; + bigint: "bigint"; + symbol: "symbol"; + function: "function"; + undefined: "undefined"; + null: "null"; + array: "array"; + object: "object"; + unknown: "unknown"; + promise: "promise"; + void: "void"; + never: "never"; + map: "map"; + set: "set"; +}; +export type ZodParsedType = keyof typeof ZodParsedType; +export declare const getParsedType: (data: any) => ZodParsedType; diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/index.d.ts b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/index.d.ts new file mode 100644 index 000000000..a9dab5738 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/index.d.ts @@ -0,0 +1,4 @@ +import * as z from "./external.js"; +export * from "./external.js"; +export { z }; +export default z; diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/locales/en.d.ts b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/locales/en.d.ts new file mode 100644 index 000000000..b2bea1ec4 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/locales/en.d.ts @@ -0,0 +1,3 @@ +import { type ZodErrorMap } from "../ZodError.js"; +declare const errorMap: ZodErrorMap; +export default errorMap; diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/standard-schema.d.ts b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/standard-schema.d.ts new file mode 100644 index 000000000..7ddff3b7f --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/standard-schema.d.ts @@ -0,0 +1,102 @@ +/** + * The Standard Schema interface. + */ +export type StandardSchemaV1 = { + /** + * The Standard Schema properties. + */ + readonly "~standard": StandardSchemaV1.Props; +}; +export declare namespace StandardSchemaV1 { + /** + * The Standard Schema properties interface. + */ + export interface Props { + /** + * The version number of the standard. + */ + readonly version: 1; + /** + * The vendor name of the schema library. + */ + readonly vendor: string; + /** + * Validates unknown input values. + */ + readonly validate: (value: unknown) => Result | Promise>; + /** + * Inferred types associated with the schema. + */ + readonly types?: Types | undefined; + } + /** + * The result interface of the validate function. + */ + export type Result = SuccessResult | FailureResult; + /** + * The result interface if validation succeeds. + */ + export interface SuccessResult { + /** + * The typed output value. + */ + readonly value: Output; + /** + * The non-existent issues. + */ + readonly issues?: undefined; + } + /** + * The result interface if validation fails. + */ + export interface FailureResult { + /** + * The issues of failed validation. + */ + readonly issues: ReadonlyArray; + } + /** + * The issue interface of the failure output. + */ + export interface Issue { + /** + * The error message of the issue. + */ + readonly message: string; + /** + * The path of the issue, if any. + */ + readonly path?: ReadonlyArray | undefined; + } + /** + * The path segment interface of the issue. + */ + export interface PathSegment { + /** + * The key representing a path segment. + */ + readonly key: PropertyKey; + } + /** + * The Standard Schema types interface. + */ + export interface Types { + /** + * The input type of the schema. + */ + readonly input: Input; + /** + * The output type of the schema. + */ + readonly output: Output; + } + /** + * Infers the input type of a Standard Schema. + */ + export type InferInput = NonNullable["input"]; + /** + * Infers the output type of a Standard Schema. + */ + export type InferOutput = NonNullable["output"]; + export {}; +} diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/types.d.ts b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/types.d.ts new file mode 100644 index 000000000..301e09128 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/types.d.ts @@ -0,0 +1,1031 @@ +import { type IssueData, type StringValidation, type ZodCustomIssue, ZodError, type ZodErrorMap } from "./ZodError.js"; +import type { enumUtil } from "./helpers/enumUtil.js"; +import { errorUtil } from "./helpers/errorUtil.js"; +import { type AsyncParseReturnType, INVALID, type ParseContext, type ParseInput, type ParseParams, type ParseReturnType, ParseStatus, type SyncParseReturnType } from "./helpers/parseUtil.js"; +import type { partialUtil } from "./helpers/partialUtil.js"; +import type { Primitive } from "./helpers/typeAliases.js"; +import { util, type objectUtil } from "./helpers/util.js"; +import type { StandardSchemaV1 } from "./standard-schema.js"; +export interface RefinementCtx { + addIssue: (arg: IssueData) => void; + path: (string | number)[]; +} +export type ZodRawShape = { + [k: string]: ZodTypeAny; +}; +export type ZodTypeAny = ZodType; +export type TypeOf> = T["_output"]; +export type input> = T["_input"]; +export type output> = T["_output"]; +export type { TypeOf as infer }; +export type CustomErrorParams = Partial>; +export interface ZodTypeDef { + errorMap?: ZodErrorMap | undefined; + description?: string | undefined; +} +export type RawCreateParams = { + errorMap?: ZodErrorMap | undefined; + invalid_type_error?: string | undefined; + required_error?: string | undefined; + message?: string | undefined; + description?: string | undefined; +} | undefined; +export type ProcessedCreateParams = { + errorMap?: ZodErrorMap | undefined; + description?: string | undefined; +}; +export type SafeParseSuccess = { + success: true; + data: Output; + error?: never; +}; +export type SafeParseError = { + success: false; + error: ZodError; + data?: never; +}; +export type SafeParseReturnType = SafeParseSuccess | SafeParseError; +export declare abstract class ZodType { + readonly _type: Output; + readonly _output: Output; + readonly _input: Input; + readonly _def: Def; + get description(): string | undefined; + "~standard": StandardSchemaV1.Props; + abstract _parse(input: ParseInput): ParseReturnType; + _getType(input: ParseInput): string; + _getOrReturnCtx(input: ParseInput, ctx?: ParseContext | undefined): ParseContext; + _processInputParams(input: ParseInput): { + status: ParseStatus; + ctx: ParseContext; + }; + _parseSync(input: ParseInput): SyncParseReturnType; + _parseAsync(input: ParseInput): AsyncParseReturnType; + parse(data: unknown, params?: util.InexactPartial): Output; + safeParse(data: unknown, params?: util.InexactPartial): SafeParseReturnType; + "~validate"(data: unknown): StandardSchemaV1.Result | Promise>; + parseAsync(data: unknown, params?: util.InexactPartial): Promise; + safeParseAsync(data: unknown, params?: util.InexactPartial): Promise>; + /** Alias of safeParseAsync */ + spa: (data: unknown, params?: util.InexactPartial) => Promise>; + refine(check: (arg: Output) => arg is RefinedOutput, message?: string | CustomErrorParams | ((arg: Output) => CustomErrorParams)): ZodEffects; + refine(check: (arg: Output) => unknown | Promise, message?: string | CustomErrorParams | ((arg: Output) => CustomErrorParams)): ZodEffects; + refinement(check: (arg: Output) => arg is RefinedOutput, refinementData: IssueData | ((arg: Output, ctx: RefinementCtx) => IssueData)): ZodEffects; + refinement(check: (arg: Output) => boolean, refinementData: IssueData | ((arg: Output, ctx: RefinementCtx) => IssueData)): ZodEffects; + _refinement(refinement: RefinementEffect["refinement"]): ZodEffects; + superRefine(refinement: (arg: Output, ctx: RefinementCtx) => arg is RefinedOutput): ZodEffects; + superRefine(refinement: (arg: Output, ctx: RefinementCtx) => void): ZodEffects; + superRefine(refinement: (arg: Output, ctx: RefinementCtx) => Promise): ZodEffects; + constructor(def: Def); + optional(): ZodOptional; + nullable(): ZodNullable; + nullish(): ZodOptional>; + array(): ZodArray; + promise(): ZodPromise; + or(option: T): ZodUnion<[this, T]>; + and(incoming: T): ZodIntersection; + transform(transform: (arg: Output, ctx: RefinementCtx) => NewOut | Promise): ZodEffects; + default(def: util.noUndefined): ZodDefault; + default(def: () => util.noUndefined): ZodDefault; + brand(brand?: B): ZodBranded; + catch(def: Output): ZodCatch; + catch(def: (ctx: { + error: ZodError; + input: Input; + }) => Output): ZodCatch; + describe(description: string): this; + pipe(target: T): ZodPipeline; + readonly(): ZodReadonly; + isOptional(): boolean; + isNullable(): boolean; +} +export type IpVersion = "v4" | "v6"; +export type ZodStringCheck = { + kind: "min"; + value: number; + message?: string | undefined; +} | { + kind: "max"; + value: number; + message?: string | undefined; +} | { + kind: "length"; + value: number; + message?: string | undefined; +} | { + kind: "email"; + message?: string | undefined; +} | { + kind: "url"; + message?: string | undefined; +} | { + kind: "emoji"; + message?: string | undefined; +} | { + kind: "uuid"; + message?: string | undefined; +} | { + kind: "nanoid"; + message?: string | undefined; +} | { + kind: "cuid"; + message?: string | undefined; +} | { + kind: "includes"; + value: string; + position?: number | undefined; + message?: string | undefined; +} | { + kind: "cuid2"; + message?: string | undefined; +} | { + kind: "ulid"; + message?: string | undefined; +} | { + kind: "startsWith"; + value: string; + message?: string | undefined; +} | { + kind: "endsWith"; + value: string; + message?: string | undefined; +} | { + kind: "regex"; + regex: RegExp; + message?: string | undefined; +} | { + kind: "trim"; + message?: string | undefined; +} | { + kind: "toLowerCase"; + message?: string | undefined; +} | { + kind: "toUpperCase"; + message?: string | undefined; +} | { + kind: "jwt"; + alg?: string; + message?: string | undefined; +} | { + kind: "datetime"; + offset: boolean; + local: boolean; + precision: number | null; + message?: string | undefined; +} | { + kind: "date"; + message?: string | undefined; +} | { + kind: "time"; + precision: number | null; + message?: string | undefined; +} | { + kind: "duration"; + message?: string | undefined; +} | { + kind: "ip"; + version?: IpVersion | undefined; + message?: string | undefined; +} | { + kind: "cidr"; + version?: IpVersion | undefined; + message?: string | undefined; +} | { + kind: "base64"; + message?: string | undefined; +} | { + kind: "base64url"; + message?: string | undefined; +}; +export interface ZodStringDef extends ZodTypeDef { + checks: ZodStringCheck[]; + typeName: ZodFirstPartyTypeKind.ZodString; + coerce: boolean; +} +export declare function datetimeRegex(args: { + precision?: number | null; + offset?: boolean; + local?: boolean; +}): RegExp; +export declare class ZodString extends ZodType { + _parse(input: ParseInput): ParseReturnType; + protected _regex(regex: RegExp, validation: StringValidation, message?: errorUtil.ErrMessage): ZodEffects; + _addCheck(check: ZodStringCheck): ZodString; + email(message?: errorUtil.ErrMessage): ZodString; + url(message?: errorUtil.ErrMessage): ZodString; + emoji(message?: errorUtil.ErrMessage): ZodString; + uuid(message?: errorUtil.ErrMessage): ZodString; + nanoid(message?: errorUtil.ErrMessage): ZodString; + cuid(message?: errorUtil.ErrMessage): ZodString; + cuid2(message?: errorUtil.ErrMessage): ZodString; + ulid(message?: errorUtil.ErrMessage): ZodString; + base64(message?: errorUtil.ErrMessage): ZodString; + base64url(message?: errorUtil.ErrMessage): ZodString; + jwt(options?: { + alg?: string; + message?: string | undefined; + }): ZodString; + ip(options?: string | { + version?: IpVersion; + message?: string | undefined; + }): ZodString; + cidr(options?: string | { + version?: IpVersion; + message?: string | undefined; + }): ZodString; + datetime(options?: string | { + message?: string | undefined; + precision?: number | null; + offset?: boolean; + local?: boolean; + }): ZodString; + date(message?: string): ZodString; + time(options?: string | { + message?: string | undefined; + precision?: number | null; + }): ZodString; + duration(message?: errorUtil.ErrMessage): ZodString; + regex(regex: RegExp, message?: errorUtil.ErrMessage): ZodString; + includes(value: string, options?: { + message?: string; + position?: number; + }): ZodString; + startsWith(value: string, message?: errorUtil.ErrMessage): ZodString; + endsWith(value: string, message?: errorUtil.ErrMessage): ZodString; + min(minLength: number, message?: errorUtil.ErrMessage): ZodString; + max(maxLength: number, message?: errorUtil.ErrMessage): ZodString; + length(len: number, message?: errorUtil.ErrMessage): ZodString; + /** + * Equivalent to `.min(1)` + */ + nonempty(message?: errorUtil.ErrMessage): ZodString; + trim(): ZodString; + toLowerCase(): ZodString; + toUpperCase(): ZodString; + get isDatetime(): boolean; + get isDate(): boolean; + get isTime(): boolean; + get isDuration(): boolean; + get isEmail(): boolean; + get isURL(): boolean; + get isEmoji(): boolean; + get isUUID(): boolean; + get isNANOID(): boolean; + get isCUID(): boolean; + get isCUID2(): boolean; + get isULID(): boolean; + get isIP(): boolean; + get isCIDR(): boolean; + get isBase64(): boolean; + get isBase64url(): boolean; + get minLength(): number | null; + get maxLength(): number | null; + static create: (params?: RawCreateParams & { + coerce?: true; + }) => ZodString; +} +export type ZodNumberCheck = { + kind: "min"; + value: number; + inclusive: boolean; + message?: string | undefined; +} | { + kind: "max"; + value: number; + inclusive: boolean; + message?: string | undefined; +} | { + kind: "int"; + message?: string | undefined; +} | { + kind: "multipleOf"; + value: number; + message?: string | undefined; +} | { + kind: "finite"; + message?: string | undefined; +}; +export interface ZodNumberDef extends ZodTypeDef { + checks: ZodNumberCheck[]; + typeName: ZodFirstPartyTypeKind.ZodNumber; + coerce: boolean; +} +export declare class ZodNumber extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams & { + coerce?: boolean; + }) => ZodNumber; + gte(value: number, message?: errorUtil.ErrMessage): ZodNumber; + min: (value: number, message?: errorUtil.ErrMessage) => ZodNumber; + gt(value: number, message?: errorUtil.ErrMessage): ZodNumber; + lte(value: number, message?: errorUtil.ErrMessage): ZodNumber; + max: (value: number, message?: errorUtil.ErrMessage) => ZodNumber; + lt(value: number, message?: errorUtil.ErrMessage): ZodNumber; + protected setLimit(kind: "min" | "max", value: number, inclusive: boolean, message?: string): ZodNumber; + _addCheck(check: ZodNumberCheck): ZodNumber; + int(message?: errorUtil.ErrMessage): ZodNumber; + positive(message?: errorUtil.ErrMessage): ZodNumber; + negative(message?: errorUtil.ErrMessage): ZodNumber; + nonpositive(message?: errorUtil.ErrMessage): ZodNumber; + nonnegative(message?: errorUtil.ErrMessage): ZodNumber; + multipleOf(value: number, message?: errorUtil.ErrMessage): ZodNumber; + step: (value: number, message?: errorUtil.ErrMessage) => ZodNumber; + finite(message?: errorUtil.ErrMessage): ZodNumber; + safe(message?: errorUtil.ErrMessage): ZodNumber; + get minValue(): number | null; + get maxValue(): number | null; + get isInt(): boolean; + get isFinite(): boolean; +} +export type ZodBigIntCheck = { + kind: "min"; + value: bigint; + inclusive: boolean; + message?: string | undefined; +} | { + kind: "max"; + value: bigint; + inclusive: boolean; + message?: string | undefined; +} | { + kind: "multipleOf"; + value: bigint; + message?: string | undefined; +}; +export interface ZodBigIntDef extends ZodTypeDef { + checks: ZodBigIntCheck[]; + typeName: ZodFirstPartyTypeKind.ZodBigInt; + coerce: boolean; +} +export declare class ZodBigInt extends ZodType { + _parse(input: ParseInput): ParseReturnType; + _getInvalidInput(input: ParseInput): INVALID; + static create: (params?: RawCreateParams & { + coerce?: boolean; + }) => ZodBigInt; + gte(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + min: (value: bigint, message?: errorUtil.ErrMessage) => ZodBigInt; + gt(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + lte(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + max: (value: bigint, message?: errorUtil.ErrMessage) => ZodBigInt; + lt(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + protected setLimit(kind: "min" | "max", value: bigint, inclusive: boolean, message?: string): ZodBigInt; + _addCheck(check: ZodBigIntCheck): ZodBigInt; + positive(message?: errorUtil.ErrMessage): ZodBigInt; + negative(message?: errorUtil.ErrMessage): ZodBigInt; + nonpositive(message?: errorUtil.ErrMessage): ZodBigInt; + nonnegative(message?: errorUtil.ErrMessage): ZodBigInt; + multipleOf(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + get minValue(): bigint | null; + get maxValue(): bigint | null; +} +export interface ZodBooleanDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodBoolean; + coerce: boolean; +} +export declare class ZodBoolean extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams & { + coerce?: boolean; + }) => ZodBoolean; +} +export type ZodDateCheck = { + kind: "min"; + value: number; + message?: string | undefined; +} | { + kind: "max"; + value: number; + message?: string | undefined; +}; +export interface ZodDateDef extends ZodTypeDef { + checks: ZodDateCheck[]; + coerce: boolean; + typeName: ZodFirstPartyTypeKind.ZodDate; +} +export declare class ZodDate extends ZodType { + _parse(input: ParseInput): ParseReturnType; + _addCheck(check: ZodDateCheck): ZodDate; + min(minDate: Date, message?: errorUtil.ErrMessage): ZodDate; + max(maxDate: Date, message?: errorUtil.ErrMessage): ZodDate; + get minDate(): Date | null; + get maxDate(): Date | null; + static create: (params?: RawCreateParams & { + coerce?: boolean; + }) => ZodDate; +} +export interface ZodSymbolDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodSymbol; +} +export declare class ZodSymbol extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodSymbol; +} +export interface ZodUndefinedDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodUndefined; +} +export declare class ZodUndefined extends ZodType { + _parse(input: ParseInput): ParseReturnType; + params?: RawCreateParams; + static create: (params?: RawCreateParams) => ZodUndefined; +} +export interface ZodNullDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodNull; +} +export declare class ZodNull extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodNull; +} +export interface ZodAnyDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodAny; +} +export declare class ZodAny extends ZodType { + _any: true; + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodAny; +} +export interface ZodUnknownDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodUnknown; +} +export declare class ZodUnknown extends ZodType { + _unknown: true; + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodUnknown; +} +export interface ZodNeverDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodNever; +} +export declare class ZodNever extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodNever; +} +export interface ZodVoidDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodVoid; +} +export declare class ZodVoid extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodVoid; +} +export interface ZodArrayDef extends ZodTypeDef { + type: T; + typeName: ZodFirstPartyTypeKind.ZodArray; + exactLength: { + value: number; + message?: string | undefined; + } | null; + minLength: { + value: number; + message?: string | undefined; + } | null; + maxLength: { + value: number; + message?: string | undefined; + } | null; +} +export type ArrayCardinality = "many" | "atleastone"; +export type arrayOutputType = Cardinality extends "atleastone" ? [T["_output"], ...T["_output"][]] : T["_output"][]; +export declare class ZodArray extends ZodType, ZodArrayDef, Cardinality extends "atleastone" ? [T["_input"], ...T["_input"][]] : T["_input"][]> { + _parse(input: ParseInput): ParseReturnType; + get element(): T; + min(minLength: number, message?: errorUtil.ErrMessage): this; + max(maxLength: number, message?: errorUtil.ErrMessage): this; + length(len: number, message?: errorUtil.ErrMessage): this; + nonempty(message?: errorUtil.ErrMessage): ZodArray; + static create: (schema: El, params?: RawCreateParams) => ZodArray; +} +export type ZodNonEmptyArray = ZodArray; +export type UnknownKeysParam = "passthrough" | "strict" | "strip"; +export interface ZodObjectDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodObject; + shape: () => T; + catchall: Catchall; + unknownKeys: UnknownKeys; +} +export type mergeTypes = { + [k in keyof A | keyof B]: k extends keyof B ? B[k] : k extends keyof A ? A[k] : never; +}; +export type objectOutputType = objectUtil.flatten>> & CatchallOutput & PassthroughType; +export type baseObjectOutputType = { + [k in keyof Shape]: Shape[k]["_output"]; +}; +export type objectInputType = objectUtil.flatten> & CatchallInput & PassthroughType; +export type baseObjectInputType = objectUtil.addQuestionMarks<{ + [k in keyof Shape]: Shape[k]["_input"]; +}>; +export type CatchallOutput = ZodType extends T ? unknown : { + [k: string]: T["_output"]; +}; +export type CatchallInput = ZodType extends T ? unknown : { + [k: string]: T["_input"]; +}; +export type PassthroughType = T extends "passthrough" ? { + [k: string]: unknown; +} : unknown; +export type deoptional = T extends ZodOptional ? deoptional : T extends ZodNullable ? ZodNullable> : T; +export type SomeZodObject = ZodObject; +export type noUnrecognized = { + [k in keyof Obj]: k extends keyof Shape ? Obj[k] : never; +}; +export declare class ZodObject, Input = objectInputType> extends ZodType, Input> { + private _cached; + _getCached(): { + shape: T; + keys: string[]; + }; + _parse(input: ParseInput): ParseReturnType; + get shape(): T; + strict(message?: errorUtil.ErrMessage): ZodObject; + strip(): ZodObject; + passthrough(): ZodObject; + /** + * @deprecated In most cases, this is no longer needed - unknown properties are now silently stripped. + * If you want to pass through unknown properties, use `.passthrough()` instead. + */ + nonstrict: () => ZodObject; + extend(augmentation: Augmentation): ZodObject, UnknownKeys, Catchall>; + /** + * @deprecated Use `.extend` instead + * */ + augment: (augmentation: Augmentation) => ZodObject, UnknownKeys, Catchall>; + /** + * Prior to zod@1.0.12 there was a bug in the + * inferred type of merged objects. Please + * upgrade if you are experiencing issues. + */ + merge(merging: Incoming): ZodObject, Incoming["_def"]["unknownKeys"], Incoming["_def"]["catchall"]>; + setKey(key: Key, schema: Schema): ZodObject; + catchall(index: Index): ZodObject; + pick>(mask: Mask): ZodObject>, UnknownKeys, Catchall>; + omit>(mask: Mask): ZodObject, UnknownKeys, Catchall>; + /** + * @deprecated + */ + deepPartial(): partialUtil.DeepPartial; + partial(): ZodObject<{ + [k in keyof T]: ZodOptional; + }, UnknownKeys, Catchall>; + partial>(mask: Mask): ZodObject : T[k]; + }>, UnknownKeys, Catchall>; + required(): ZodObject<{ + [k in keyof T]: deoptional; + }, UnknownKeys, Catchall>; + required>(mask: Mask): ZodObject : T[k]; + }>, UnknownKeys, Catchall>; + keyof(): ZodEnum>; + static create: (shape: Shape, params?: RawCreateParams) => ZodObject, objectInputType>; + static strictCreate: (shape: Shape, params?: RawCreateParams) => ZodObject; + static lazycreate: (shape: () => Shape, params?: RawCreateParams) => ZodObject; +} +export type AnyZodObject = ZodObject; +export type ZodUnionOptions = Readonly<[ZodTypeAny, ...ZodTypeAny[]]>; +export interface ZodUnionDef> extends ZodTypeDef { + options: T; + typeName: ZodFirstPartyTypeKind.ZodUnion; +} +export declare class ZodUnion extends ZodType, T[number]["_input"]> { + _parse(input: ParseInput): ParseReturnType; + get options(): T; + static create: >(types: Options, params?: RawCreateParams) => ZodUnion; +} +export type ZodDiscriminatedUnionOption = ZodObject<{ + [key in Discriminator]: ZodTypeAny; +} & ZodRawShape, UnknownKeysParam, ZodTypeAny>; +export interface ZodDiscriminatedUnionDef[] = ZodDiscriminatedUnionOption[]> extends ZodTypeDef { + discriminator: Discriminator; + options: Options; + optionsMap: Map>; + typeName: ZodFirstPartyTypeKind.ZodDiscriminatedUnion; +} +export declare class ZodDiscriminatedUnion[]> extends ZodType, ZodDiscriminatedUnionDef, input> { + _parse(input: ParseInput): ParseReturnType; + get discriminator(): Discriminator; + get options(): Options; + get optionsMap(): Map>; + /** + * The constructor of the discriminated union schema. Its behaviour is very similar to that of the normal z.union() constructor. + * However, it only allows a union of objects, all of which need to share a discriminator property. This property must + * have a different value for each object in the union. + * @param discriminator the name of the discriminator property + * @param types an array of object schemas + * @param params + */ + static create, + ...ZodDiscriminatedUnionOption[] + ]>(discriminator: Discriminator, options: Types, params?: RawCreateParams): ZodDiscriminatedUnion; +} +export interface ZodIntersectionDef extends ZodTypeDef { + left: T; + right: U; + typeName: ZodFirstPartyTypeKind.ZodIntersection; +} +export declare class ZodIntersection extends ZodType, T["_input"] & U["_input"]> { + _parse(input: ParseInput): ParseReturnType; + static create: (left: TSchema, right: USchema, params?: RawCreateParams) => ZodIntersection; +} +export type ZodTupleItems = [ZodTypeAny, ...ZodTypeAny[]]; +export type AssertArray = T extends any[] ? T : never; +export type OutputTypeOfTuple = AssertArray<{ + [k in keyof T]: T[k] extends ZodType ? T[k]["_output"] : never; +}>; +export type OutputTypeOfTupleWithRest = Rest extends ZodTypeAny ? [...OutputTypeOfTuple, ...Rest["_output"][]] : OutputTypeOfTuple; +export type InputTypeOfTuple = AssertArray<{ + [k in keyof T]: T[k] extends ZodType ? T[k]["_input"] : never; +}>; +export type InputTypeOfTupleWithRest = Rest extends ZodTypeAny ? [...InputTypeOfTuple, ...Rest["_input"][]] : InputTypeOfTuple; +export interface ZodTupleDef extends ZodTypeDef { + items: T; + rest: Rest; + typeName: ZodFirstPartyTypeKind.ZodTuple; +} +export type AnyZodTuple = ZodTuple<[ZodTypeAny, ...ZodTypeAny[]] | [], ZodTypeAny | null>; +export declare class ZodTuple extends ZodType, ZodTupleDef, InputTypeOfTupleWithRest> { + _parse(input: ParseInput): ParseReturnType; + get items(): T; + rest(rest: RestSchema): ZodTuple; + static create: (schemas: Items, params?: RawCreateParams) => ZodTuple; +} +export interface ZodRecordDef extends ZodTypeDef { + valueType: Value; + keyType: Key; + typeName: ZodFirstPartyTypeKind.ZodRecord; +} +export type KeySchema = ZodType; +export type RecordType = [string] extends [K] ? Record : [number] extends [K] ? Record : [symbol] extends [K] ? Record : [BRAND] extends [K] ? Record : Partial>; +export declare class ZodRecord extends ZodType, ZodRecordDef, RecordType> { + get keySchema(): Key; + get valueSchema(): Value; + _parse(input: ParseInput): ParseReturnType; + get element(): Value; + static create(valueType: Value, params?: RawCreateParams): ZodRecord; + static create(keySchema: Keys, valueType: Value, params?: RawCreateParams): ZodRecord; +} +export interface ZodMapDef extends ZodTypeDef { + valueType: Value; + keyType: Key; + typeName: ZodFirstPartyTypeKind.ZodMap; +} +export declare class ZodMap extends ZodType, ZodMapDef, Map> { + get keySchema(): Key; + get valueSchema(): Value; + _parse(input: ParseInput): ParseReturnType; + static create: (keyType: KeySchema, valueType: ValueSchema, params?: RawCreateParams) => ZodMap; +} +export interface ZodSetDef extends ZodTypeDef { + valueType: Value; + typeName: ZodFirstPartyTypeKind.ZodSet; + minSize: { + value: number; + message?: string | undefined; + } | null; + maxSize: { + value: number; + message?: string | undefined; + } | null; +} +export declare class ZodSet extends ZodType, ZodSetDef, Set> { + _parse(input: ParseInput): ParseReturnType; + min(minSize: number, message?: errorUtil.ErrMessage): this; + max(maxSize: number, message?: errorUtil.ErrMessage): this; + size(size: number, message?: errorUtil.ErrMessage): this; + nonempty(message?: errorUtil.ErrMessage): ZodSet; + static create: (valueType: ValueSchema, params?: RawCreateParams) => ZodSet; +} +export interface ZodFunctionDef = ZodTuple, Returns extends ZodTypeAny = ZodTypeAny> extends ZodTypeDef { + args: Args; + returns: Returns; + typeName: ZodFirstPartyTypeKind.ZodFunction; +} +export type OuterTypeOfFunction, Returns extends ZodTypeAny> = Args["_input"] extends Array ? (...args: Args["_input"]) => Returns["_output"] : never; +export type InnerTypeOfFunction, Returns extends ZodTypeAny> = Args["_output"] extends Array ? (...args: Args["_output"]) => Returns["_input"] : never; +export declare class ZodFunction, Returns extends ZodTypeAny> extends ZodType, ZodFunctionDef, InnerTypeOfFunction> { + _parse(input: ParseInput): ParseReturnType; + parameters(): Args; + returnType(): Returns; + args[0]>(...items: Items): ZodFunction, Returns>; + returns>(returnType: NewReturnType): ZodFunction; + implement>(func: F): ReturnType extends Returns["_output"] ? (...args: Args["_input"]) => ReturnType : OuterTypeOfFunction; + strictImplement(func: InnerTypeOfFunction): InnerTypeOfFunction; + validate: >(func: F) => ReturnType extends Returns["_output"] ? (...args: Args["_input"]) => ReturnType : OuterTypeOfFunction; + static create(): ZodFunction, ZodUnknown>; + static create>(args: T): ZodFunction; + static create(args: T, returns: U): ZodFunction; + static create, U extends ZodTypeAny = ZodUnknown>(args: T, returns: U, params?: RawCreateParams): ZodFunction; +} +export interface ZodLazyDef extends ZodTypeDef { + getter: () => T; + typeName: ZodFirstPartyTypeKind.ZodLazy; +} +export declare class ZodLazy extends ZodType, ZodLazyDef, input> { + get schema(): T; + _parse(input: ParseInput): ParseReturnType; + static create: (getter: () => Inner, params?: RawCreateParams) => ZodLazy; +} +export interface ZodLiteralDef extends ZodTypeDef { + value: T; + typeName: ZodFirstPartyTypeKind.ZodLiteral; +} +export declare class ZodLiteral extends ZodType, T> { + _parse(input: ParseInput): ParseReturnType; + get value(): T; + static create: (value: Value, params?: RawCreateParams) => ZodLiteral; +} +export type ArrayKeys = keyof any[]; +export type Indices = Exclude; +export type EnumValues = readonly [T, ...T[]]; +export type Values = { + [k in T[number]]: k; +}; +export interface ZodEnumDef extends ZodTypeDef { + values: T; + typeName: ZodFirstPartyTypeKind.ZodEnum; +} +export type Writeable = { + -readonly [P in keyof T]: T[P]; +}; +export type FilterEnum = Values extends [] ? [] : Values extends [infer Head, ...infer Rest] ? Head extends ToExclude ? FilterEnum : [Head, ...FilterEnum] : never; +export type typecast = A extends T ? A : never; +declare function createZodEnum>(values: T, params?: RawCreateParams): ZodEnum>; +declare function createZodEnum(values: T, params?: RawCreateParams): ZodEnum; +export declare class ZodEnum extends ZodType, T[number]> { + _cache: Set | undefined; + _parse(input: ParseInput): ParseReturnType; + get options(): T; + get enum(): Values; + get Values(): Values; + get Enum(): Values; + extract(values: ToExtract, newDef?: RawCreateParams): ZodEnum>; + exclude(values: ToExclude, newDef?: RawCreateParams): ZodEnum>, [string, ...string[]]>>; + static create: typeof createZodEnum; +} +export interface ZodNativeEnumDef extends ZodTypeDef { + values: T; + typeName: ZodFirstPartyTypeKind.ZodNativeEnum; +} +export type EnumLike = { + [k: string]: string | number; + [nu: number]: string; +}; +export declare class ZodNativeEnum extends ZodType, T[keyof T]> { + _cache: Set | undefined; + _parse(input: ParseInput): ParseReturnType; + get enum(): T; + static create: (values: Elements, params?: RawCreateParams) => ZodNativeEnum; +} +export interface ZodPromiseDef extends ZodTypeDef { + type: T; + typeName: ZodFirstPartyTypeKind.ZodPromise; +} +export declare class ZodPromise extends ZodType, ZodPromiseDef, Promise> { + unwrap(): T; + _parse(input: ParseInput): ParseReturnType; + static create: (schema: Inner, params?: RawCreateParams) => ZodPromise; +} +export type Refinement = (arg: T, ctx: RefinementCtx) => any; +export type SuperRefinement = (arg: T, ctx: RefinementCtx) => void | Promise; +export type RefinementEffect = { + type: "refinement"; + refinement: (arg: T, ctx: RefinementCtx) => any; +}; +export type TransformEffect = { + type: "transform"; + transform: (arg: T, ctx: RefinementCtx) => any; +}; +export type PreprocessEffect = { + type: "preprocess"; + transform: (arg: T, ctx: RefinementCtx) => any; +}; +export type Effect = RefinementEffect | TransformEffect | PreprocessEffect; +export interface ZodEffectsDef extends ZodTypeDef { + schema: T; + typeName: ZodFirstPartyTypeKind.ZodEffects; + effect: Effect; +} +export declare class ZodEffects, Input = input> extends ZodType, Input> { + innerType(): T; + sourceType(): T; + _parse(input: ParseInput): ParseReturnType; + static create: (schema: I, effect: Effect, params?: RawCreateParams) => ZodEffects; + static createWithPreprocess: (preprocess: (arg: unknown, ctx: RefinementCtx) => unknown, schema: I, params?: RawCreateParams) => ZodEffects; +} +export { ZodEffects as ZodTransformer }; +export interface ZodOptionalDef extends ZodTypeDef { + innerType: T; + typeName: ZodFirstPartyTypeKind.ZodOptional; +} +export type ZodOptionalType = ZodOptional; +export declare class ZodOptional extends ZodType, T["_input"] | undefined> { + _parse(input: ParseInput): ParseReturnType; + unwrap(): T; + static create: (type: Inner, params?: RawCreateParams) => ZodOptional; +} +export interface ZodNullableDef extends ZodTypeDef { + innerType: T; + typeName: ZodFirstPartyTypeKind.ZodNullable; +} +export type ZodNullableType = ZodNullable; +export declare class ZodNullable extends ZodType, T["_input"] | null> { + _parse(input: ParseInput): ParseReturnType; + unwrap(): T; + static create: (type: Inner, params?: RawCreateParams) => ZodNullable; +} +export interface ZodDefaultDef extends ZodTypeDef { + innerType: T; + defaultValue: () => util.noUndefined; + typeName: ZodFirstPartyTypeKind.ZodDefault; +} +export declare class ZodDefault extends ZodType, ZodDefaultDef, T["_input"] | undefined> { + _parse(input: ParseInput): ParseReturnType; + removeDefault(): T; + static create: (type: Inner, params: RawCreateParams & { + default: Inner["_input"] | (() => util.noUndefined); + }) => ZodDefault; +} +export interface ZodCatchDef extends ZodTypeDef { + innerType: T; + catchValue: (ctx: { + error: ZodError; + input: unknown; + }) => T["_input"]; + typeName: ZodFirstPartyTypeKind.ZodCatch; +} +export declare class ZodCatch extends ZodType, unknown> { + _parse(input: ParseInput): ParseReturnType; + removeCatch(): T; + static create: (type: Inner, params: RawCreateParams & { + catch: Inner["_output"] | (() => Inner["_output"]); + }) => ZodCatch; +} +export interface ZodNaNDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodNaN; +} +export declare class ZodNaN extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodNaN; +} +export interface ZodBrandedDef extends ZodTypeDef { + type: T; + typeName: ZodFirstPartyTypeKind.ZodBranded; +} +export declare const BRAND: unique symbol; +export type BRAND = { + [BRAND]: { + [k in T]: true; + }; +}; +export declare class ZodBranded extends ZodType, ZodBrandedDef, T["_input"]> { + _parse(input: ParseInput): ParseReturnType; + unwrap(): T; +} +export interface ZodPipelineDef extends ZodTypeDef { + in: A; + out: B; + typeName: ZodFirstPartyTypeKind.ZodPipeline; +} +export declare class ZodPipeline extends ZodType, A["_input"]> { + _parse(input: ParseInput): ParseReturnType; + static create(a: ASchema, b: BSchema): ZodPipeline; +} +type BuiltIn = (((...args: any[]) => any) | (new (...args: any[]) => any)) | { + readonly [Symbol.toStringTag]: string; +} | Date | Error | Generator | Promise | RegExp; +type MakeReadonly = T extends Map ? ReadonlyMap : T extends Set ? ReadonlySet : T extends [infer Head, ...infer Tail] ? readonly [Head, ...Tail] : T extends Array ? ReadonlyArray : T extends BuiltIn ? T : Readonly; +export interface ZodReadonlyDef extends ZodTypeDef { + innerType: T; + typeName: ZodFirstPartyTypeKind.ZodReadonly; +} +export declare class ZodReadonly extends ZodType, ZodReadonlyDef, MakeReadonly> { + _parse(input: ParseInput): ParseReturnType; + static create: (type: Inner, params?: RawCreateParams) => ZodReadonly; + unwrap(): T; +} +type CustomParams = CustomErrorParams & { + fatal?: boolean; +}; +export declare function custom(check?: (data: any) => any, _params?: string | CustomParams | ((input: any) => CustomParams), +/** + * @deprecated + * + * Pass `fatal` into the params object instead: + * + * ```ts + * z.string().custom((val) => val.length > 5, { fatal: false }) + * ``` + * + */ +fatal?: boolean): ZodType; +export { ZodType as Schema, ZodType as ZodSchema }; +export declare const late: { + object: (shape: () => Shape, params?: RawCreateParams) => ZodObject; +}; +export declare enum ZodFirstPartyTypeKind { + ZodString = "ZodString", + ZodNumber = "ZodNumber", + ZodNaN = "ZodNaN", + ZodBigInt = "ZodBigInt", + ZodBoolean = "ZodBoolean", + ZodDate = "ZodDate", + ZodSymbol = "ZodSymbol", + ZodUndefined = "ZodUndefined", + ZodNull = "ZodNull", + ZodAny = "ZodAny", + ZodUnknown = "ZodUnknown", + ZodNever = "ZodNever", + ZodVoid = "ZodVoid", + ZodArray = "ZodArray", + ZodObject = "ZodObject", + ZodUnion = "ZodUnion", + ZodDiscriminatedUnion = "ZodDiscriminatedUnion", + ZodIntersection = "ZodIntersection", + ZodTuple = "ZodTuple", + ZodRecord = "ZodRecord", + ZodMap = "ZodMap", + ZodSet = "ZodSet", + ZodFunction = "ZodFunction", + ZodLazy = "ZodLazy", + ZodLiteral = "ZodLiteral", + ZodEnum = "ZodEnum", + ZodEffects = "ZodEffects", + ZodNativeEnum = "ZodNativeEnum", + ZodOptional = "ZodOptional", + ZodNullable = "ZodNullable", + ZodDefault = "ZodDefault", + ZodCatch = "ZodCatch", + ZodPromise = "ZodPromise", + ZodBranded = "ZodBranded", + ZodPipeline = "ZodPipeline", + ZodReadonly = "ZodReadonly" +} +export type ZodFirstPartySchemaTypes = ZodString | ZodNumber | ZodNaN | ZodBigInt | ZodBoolean | ZodDate | ZodUndefined | ZodNull | ZodAny | ZodUnknown | ZodNever | ZodVoid | ZodArray | ZodObject | ZodUnion | ZodDiscriminatedUnion | ZodIntersection | ZodTuple | ZodRecord | ZodMap | ZodSet | ZodFunction | ZodLazy | ZodLiteral | ZodEnum | ZodEffects | ZodNativeEnum | ZodOptional | ZodNullable | ZodDefault | ZodCatch | ZodPromise | ZodBranded | ZodPipeline | ZodReadonly | ZodSymbol; +declare abstract class Class { + constructor(..._: any[]); +} +declare const instanceOfType: (cls: T, params?: CustomParams) => ZodType, ZodTypeDef, InstanceType>; +declare const stringType: (params?: RawCreateParams & { + coerce?: true; +}) => ZodString; +declare const numberType: (params?: RawCreateParams & { + coerce?: boolean; +}) => ZodNumber; +declare const nanType: (params?: RawCreateParams) => ZodNaN; +declare const bigIntType: (params?: RawCreateParams & { + coerce?: boolean; +}) => ZodBigInt; +declare const booleanType: (params?: RawCreateParams & { + coerce?: boolean; +}) => ZodBoolean; +declare const dateType: (params?: RawCreateParams & { + coerce?: boolean; +}) => ZodDate; +declare const symbolType: (params?: RawCreateParams) => ZodSymbol; +declare const undefinedType: (params?: RawCreateParams) => ZodUndefined; +declare const nullType: (params?: RawCreateParams) => ZodNull; +declare const anyType: (params?: RawCreateParams) => ZodAny; +declare const unknownType: (params?: RawCreateParams) => ZodUnknown; +declare const neverType: (params?: RawCreateParams) => ZodNever; +declare const voidType: (params?: RawCreateParams) => ZodVoid; +declare const arrayType: (schema: El, params?: RawCreateParams) => ZodArray; +declare const objectType: (shape: Shape, params?: RawCreateParams) => ZodObject, objectInputType>; +declare const strictObjectType: (shape: Shape, params?: RawCreateParams) => ZodObject; +declare const unionType: >(types: Options, params?: RawCreateParams) => ZodUnion; +declare const discriminatedUnionType: typeof ZodDiscriminatedUnion.create; +declare const intersectionType: (left: TSchema, right: USchema, params?: RawCreateParams) => ZodIntersection; +declare const tupleType: (schemas: Items, params?: RawCreateParams) => ZodTuple; +declare const recordType: typeof ZodRecord.create; +declare const mapType: (keyType: KeySchema, valueType: ValueSchema, params?: RawCreateParams) => ZodMap; +declare const setType: (valueType: ValueSchema, params?: RawCreateParams) => ZodSet; +declare const functionType: typeof ZodFunction.create; +declare const lazyType: (getter: () => Inner, params?: RawCreateParams) => ZodLazy; +declare const literalType: (value: Value, params?: RawCreateParams) => ZodLiteral; +declare const enumType: typeof createZodEnum; +declare const nativeEnumType: (values: Elements, params?: RawCreateParams) => ZodNativeEnum; +declare const promiseType: (schema: Inner, params?: RawCreateParams) => ZodPromise; +declare const effectsType: (schema: I, effect: Effect, params?: RawCreateParams) => ZodEffects; +declare const optionalType: (type: Inner, params?: RawCreateParams) => ZodOptional; +declare const nullableType: (type: Inner, params?: RawCreateParams) => ZodNullable; +declare const preprocessType: (preprocess: (arg: unknown, ctx: RefinementCtx) => unknown, schema: I, params?: RawCreateParams) => ZodEffects; +declare const pipelineType: typeof ZodPipeline.create; +declare const ostring: () => ZodOptional; +declare const onumber: () => ZodOptional; +declare const oboolean: () => ZodOptional; +export declare const coerce: { + string: (typeof ZodString)["create"]; + number: (typeof ZodNumber)["create"]; + boolean: (typeof ZodBoolean)["create"]; + bigint: (typeof ZodBigInt)["create"]; + date: (typeof ZodDate)["create"]; +}; +export { anyType as any, arrayType as array, bigIntType as bigint, booleanType as boolean, dateType as date, discriminatedUnionType as discriminatedUnion, effectsType as effect, enumType as enum, functionType as function, instanceOfType as instanceof, intersectionType as intersection, lazyType as lazy, literalType as literal, mapType as map, nanType as nan, nativeEnumType as nativeEnum, neverType as never, nullType as null, nullableType as nullable, numberType as number, objectType as object, oboolean, onumber, optionalType as optional, ostring, pipelineType as pipeline, preprocessType as preprocess, promiseType as promise, recordType as record, setType as set, strictObjectType as strictObject, stringType as string, symbolType as symbol, effectsType as transformer, tupleType as tuple, undefinedType as undefined, unionType as union, unknownType as unknown, voidType as void, }; +export declare const NEVER: never; diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/package.json b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/package.json new file mode 100644 index 000000000..e02b87614 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/package.json @@ -0,0 +1,120 @@ +{ + "name": "zod", + "version": "3.25.67", + "type": "module", + "author": "Colin McDonnell ", + "description": "TypeScript-first schema declaration and validation library with static type inference", + "files": ["dist", "v3", "v4", "v4-mini"], + "funding": "https://github.com/sponsors/colinhacks", + "homepage": "https://zod.dev", + "keywords": ["typescript", "schema", "validation", "type", "inference"], + "license": "MIT", + "sideEffects": false, + "main": "./dist/cjs/index.js", + "types": "./dist/types/index.d.ts", + "module": "./dist/esm/index.js", + "exports": { + ".": { + "import": { + "@zod/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "@zod/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "default": "./dist/cjs/index.js" + } + }, + "./v3": { + "import": { + "@zod/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "@zod/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "default": "./dist/cjs/index.js" + } + }, + "./v4": { + "import": { + "@zod/source": "./src/v4/index.ts", + "types": "./dist/types/v4/index.d.ts", + "default": "./dist/esm/v4/index.js" + }, + "require": { + "@zod/source": "./src/v4/index.ts", + "types": "./dist/types/v4/index.d.ts", + "default": "./dist/cjs/v4/index.js" + } + }, + "./v4-mini": { + "@zod/source": "./src/v4/mini/index.ts", + "types": "./dist/types/v4/mini/index.d.ts", + "import": "./dist/esm/v4/mini/index.js", + "require": "./dist/cjs/v4/mini/index.js" + }, + "./v4/mini": { + "@zod/source": "./src/v4/mini/index.ts", + "types": "./dist/types/v4/mini/index.d.ts", + "import": "./dist/esm/v4/mini/index.js", + "require": "./dist/cjs/v4/mini/index.js" + }, + "./v4/core": { + "@zod/source": "./src/v4/core/index.ts", + "types": "./dist/types/v4/core/index.d.ts", + "import": "./dist/esm/v4/core/index.js", + "require": "./dist/cjs/v4/core/index.js" + }, + "./v4/locales": { + "@zod/source": "./src/v4/locales/index.ts", + "types": "./dist/types/v4/locales/index.d.ts", + "import": "./dist/esm/v4/locales/index.js", + "require": "./dist/cjs/v4/locales/index.js" + }, + "./v4/locales/*": { + "@zod/source": "./src/v4/locales/*", + "types": "./dist/types/v4/locales/*", + "import": "./dist/esm/v4/locales/*", + "require": "./dist/cjs/v4/locales/*" + }, + "./package.json": "./package.json" + }, + "tshy": { + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./v3": "./src/v3/index.ts", + "./v4": "./src/v4/index.ts", + "./v4-mini": "./src/v4/mini/index.ts", + "./v4/core": "./src/v4/core/index.ts", + "./v4/locales": "./src/v4/locales/index.ts", + "./v4/locales/*": "./src/v4/locales/*" + }, + "sourceDialects": ["@zod/source"], + "exclude": ["./src/**/*.test.ts"] + }, + "repository": { + "type": "git", + "url": "git+https://github.com/colinhacks/zod.git" + }, + "bugs": { + "url": "https://github.com/colinhacks/zod/issues" + }, + "support": { + "backing": { + "npm-funding": true + } + }, + "scripts": { + "clean": "rm -rf dist", + "build": "tsx ./build.mts", + "postbuild": "pnpm biome check --write .", + "test:watch": "pnpm vitest", + "test": "pnpm vitest run", + "bump:beta": "pnpm version \"v$(pnpm pkg get version | jq -r)-beta.$(date +%Y%m%dT%H%M%S)\"", + "pub:beta": "pnpm bump:beta && pnpm publish --tag next --publish-branch v4 --no-git-checks --dry-run" + } +} diff --git a/tests/fixtures/npm/cases/zod3-infer/package.json b/tests/fixtures/npm/cases/zod3-infer/package.json new file mode 100644 index 000000000..ccfb8101b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/package.json @@ -0,0 +1 @@ +{ "name": "zod3-infer-case", "private": true, "type": "module" } diff --git a/tests/fixtures/npm/cases/zod3/main.ts b/tests/fixtures/npm/cases/zod3/main.ts new file mode 100644 index 000000000..d7f1a433a --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/main.ts @@ -0,0 +1,115 @@ +// zod 3.25 (the real package, trimmed to its v3 ESM build and types) compiled +// natively: schemas built at module top level, parse and safeParse across the +// common schema kinds, number checks, issue paths, codes and messages, and +// async refinements through parseAsync/safeParseAsync. +import { z } from "zod"; + +const show = (label: string, value: unknown): void => { + console.log(label + " " + JSON.stringify(value)); +}; +const issues = (result: z.SafeParseReturnType): string => + result.success + ? "ok" + : result.error.issues.map((i) => `${i.path.join(".")}:${i.code}:${i.message}`).join(" | "); + +const Region = z.enum(["east", "west", "north"]); +const Job = z.object({ + id: z.string().min(3).max(32), + region: Region, + attempts: z.number().int().nonnegative().default(0), + tags: z.record(z.string()).optional(), + files: z.array(z.object({ path: z.string(), mode: z.number() })), + kind: z.union([z.literal("static"), z.literal("worker")]), + meta: z.object({ owner: z.string().nullable() }).partial(), +}); +type Job = z.infer; + +const job: Job = Job.parse({ + id: "job_1", + region: "east", + files: [{ path: "a.js", mode: 420 }], + kind: "worker", + meta: { owner: null }, +}); +show("job", job); +show("bad job", issues(Job.safeParse({ id: "x", region: "south", files: [{ path: 1 }], kind: "edge" }))); + +// The common schema kinds. +show("number", z.number().parse(3)); +show("default", z.number().default(0).parse(undefined)); +show("optional", z.string().optional().parse(undefined) ?? "undefined"); +show("nullable", z.string().nullable().parse(null)); +show("record", z.record(z.string()).parse({ a: "b" })); +show("array", z.array(z.string()).parse(["a"])); +show("union", z.union([z.literal("a"), z.literal("b")]).parse("b")); +show("partial", z.object({ a: z.string() }).partial().parse({})); +show("discriminated", z.discriminatedUnion("t", [z.object({ t: z.literal("x") })]).parse({ t: "x" })); +show("transform", z.string().transform((s) => s.length).parse("abc")); +show("wrong type", issues(z.object({ a: z.number() }).safeParse({ a: "x" }))); +show("email", issues(z.string().email().safeParse("nope"))); +show("boolean", z.boolean().parse(true)); +show("strict", issues(z.object({ a: z.string() }).strict().safeParse({ a: "x", b: 1 }))); +show("regex", z.string().regex(/^a/).parse("abc")); +show("coerce", z.coerce.number().parse("4")); +show("extend", z.object({ a: z.string() }).extend({ b: z.number() }).parse({ a: "x", b: 1 })); +show("refine", issues(z.string().refine((s) => s.length > 1, "short").safeParse("a"))); +show("enum", z.enum(["a", "b"]).parse("a")); +show("literal", z.literal("q").parse("q")); + +// Number checks, passing and failing, with and without messages. +show("int", z.number().int().parse(3)); +show("int fail", issues(z.number().int().safeParse(1.5))); +show("int message", issues(z.number().int("whole numbers only").safeParse(2.5))); +show("min", z.number().min(1).parse(3)); +show("min fail", issues(z.number().min(1, "too low").safeParse(0))); +show("max fail", issues(z.number().max(10).safeParse(11))); +show("gt lt", issues(z.number().gt(0).lt(5).safeParse(5))); +show("positive", issues(z.number().positive().safeParse(0))); +show("nonnegative", issues(z.number().nonnegative().safeParse(-1))); +show("multipleOf", z.number().multipleOf(2).parse(4)); +show("multipleOf fail", issues(z.number().multipleOf(0.1).safeParse(0.35))); +show("multipleOf float", z.number().multipleOf(0.01).parse(1.23)); +show("step", issues(z.number().step(3).safeParse(10))); +show("finite", issues(z.number().finite().safeParse(Infinity))); +show("safe", issues(z.number().safe().safeParse(2 ** 60))); +show("bounds", [z.number().min(2).max(9).minValue, z.number().min(2).max(9).maxValue, z.number().int().isInt]); + +try { + Job.parse({}); +} catch (error) { + show("thrown", error instanceof z.ZodError ? error.issues.length : "other"); +} + +// Async refinements. +const Unique = z.string().refine(async (name) => name !== "taken", { message: "name is taken" }); +const Account = z.object({ + name: Unique, + age: z.number().int().min(18), +}); + +async function main(): Promise { + show("async ok", await Account.parseAsync({ name: "free", age: 30 })); + show("async fail", issues(await Account.safeParseAsync({ name: "taken", age: 12.5 }))); + try { + await Unique.parseAsync("taken"); + } catch (error) { + show("async thrown", error instanceof z.ZodError ? error.issues[0]!.message : "other"); + } + try { + Unique.parse("free"); + } catch (error) { + show("sync on async", (error as Error).message); + } + const Slow = z.number().superRefine(async (n, ctx) => { + await new Promise((resolve) => setTimeout(resolve, 1)); + if (n % 2 !== 0) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "odd" }); + }); + show("superRefine", issues(await Slow.safeParseAsync(3))); + show("transform async", await z.string().transform(async (s) => s.toUpperCase()).parseAsync("up")); + show("array async", issues(await z.array(Unique).safeParseAsync(["a", "taken", "b"]))); + show("array async ok", await z.array(Unique).parseAsync(["x", "y"])); + show("tuple async", issues(await z.tuple([z.string(), Unique]).safeParseAsync(["a", "taken"]))); + show("tuple async ok", await z.tuple([z.number(), Unique]).parseAsync([1, "z"])); +} + +void main(); diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/LICENSE b/tests/fixtures/npm/cases/zod3/node_modules/zod/LICENSE new file mode 100644 index 000000000..c06579627 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Colin McDonnell + +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/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/index.js b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/index.js new file mode 100644 index 000000000..8352ab713 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/index.js @@ -0,0 +1,3 @@ +import z3 from "./v3/index.js"; +export * from "./v3/index.js"; +export default z3; diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/package.json b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/package.json new file mode 100644 index 000000000..aead43de3 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} \ No newline at end of file diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/ZodError.js b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/ZodError.js new file mode 100644 index 000000000..c72ce5288 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/ZodError.js @@ -0,0 +1,132 @@ +import { util } from "./helpers/util.js"; +export const ZodIssueCode = util.arrayToEnum([ + "invalid_type", + "invalid_literal", + "custom", + "invalid_union", + "invalid_union_discriminator", + "invalid_enum_value", + "unrecognized_keys", + "invalid_arguments", + "invalid_return_type", + "invalid_date", + "invalid_string", + "too_small", + "too_big", + "invalid_intersection_types", + "not_multiple_of", + "not_finite", +]); +export const quotelessJson = (obj) => { + const json = JSON.stringify(obj, null, 2); + return json.replace(/"([^"]+)":/g, "$1:"); +}; +export class ZodError extends Error { + get errors() { + return this.issues; + } + constructor(issues) { + super(); + this.issues = []; + this.addIssue = (sub) => { + this.issues = [...this.issues, sub]; + }; + this.addIssues = (subs = []) => { + this.issues = [...this.issues, ...subs]; + }; + const actualProto = new.target.prototype; + if (Object.setPrototypeOf) { + // eslint-disable-next-line ban/ban + Object.setPrototypeOf(this, actualProto); + } + else { + this.__proto__ = actualProto; + } + this.name = "ZodError"; + this.issues = issues; + } + format(_mapper) { + const mapper = _mapper || + function (issue) { + return issue.message; + }; + const fieldErrors = { _errors: [] }; + const processError = (error) => { + for (const issue of error.issues) { + if (issue.code === "invalid_union") { + issue.unionErrors.map(processError); + } + else if (issue.code === "invalid_return_type") { + processError(issue.returnTypeError); + } + else if (issue.code === "invalid_arguments") { + processError(issue.argumentsError); + } + else if (issue.path.length === 0) { + fieldErrors._errors.push(mapper(issue)); + } + else { + let curr = fieldErrors; + let i = 0; + while (i < issue.path.length) { + const el = issue.path[i]; + const terminal = i === issue.path.length - 1; + if (!terminal) { + curr[el] = curr[el] || { _errors: [] }; + // if (typeof el === "string") { + // curr[el] = curr[el] || { _errors: [] }; + // } else if (typeof el === "number") { + // const errorArray: any = []; + // errorArray._errors = []; + // curr[el] = curr[el] || errorArray; + // } + } + else { + curr[el] = curr[el] || { _errors: [] }; + curr[el]._errors.push(mapper(issue)); + } + curr = curr[el]; + i++; + } + } + } + }; + processError(this); + return fieldErrors; + } + static assert(value) { + if (!(value instanceof ZodError)) { + throw new Error(`Not a ZodError: ${value}`); + } + } + toString() { + return this.message; + } + get message() { + return JSON.stringify(this.issues, util.jsonStringifyReplacer, 2); + } + get isEmpty() { + return this.issues.length === 0; + } + flatten(mapper = (issue) => issue.message) { + const fieldErrors = {}; + const formErrors = []; + for (const sub of this.issues) { + if (sub.path.length > 0) { + fieldErrors[sub.path[0]] = fieldErrors[sub.path[0]] || []; + fieldErrors[sub.path[0]].push(mapper(sub)); + } + else { + formErrors.push(mapper(sub)); + } + } + return { formErrors, fieldErrors }; + } + get formErrors() { + return this.flatten(); + } +} +ZodError.create = (issues) => { + const error = new ZodError(issues); + return error; +}; diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/errors.js b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/errors.js new file mode 100644 index 000000000..26fdf628a --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/errors.js @@ -0,0 +1,9 @@ +import defaultErrorMap from "./locales/en.js"; +let overrideErrorMap = defaultErrorMap; +export { defaultErrorMap }; +export function setErrorMap(map) { + overrideErrorMap = map; +} +export function getErrorMap() { + return overrideErrorMap; +} diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/external.js b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/external.js new file mode 100644 index 000000000..f0a4be4c7 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/external.js @@ -0,0 +1,6 @@ +export * from "./errors.js"; +export * from "./helpers/parseUtil.js"; +export * from "./helpers/typeAliases.js"; +export * from "./helpers/util.js"; +export * from "./types.js"; +export * from "./ZodError.js"; diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/helpers/enumUtil.js b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/helpers/enumUtil.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/helpers/enumUtil.js @@ -0,0 +1 @@ +export {}; diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/helpers/errorUtil.js b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/helpers/errorUtil.js new file mode 100644 index 000000000..e1640d1a4 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/helpers/errorUtil.js @@ -0,0 +1,6 @@ +export var errorUtil; +(function (errorUtil) { + errorUtil.errToObj = (message) => typeof message === "string" ? { message } : message || {}; + // biome-ignore lint: + errorUtil.toString = (message) => typeof message === "string" ? message : message?.message; +})(errorUtil || (errorUtil = {})); diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/helpers/parseUtil.js b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/helpers/parseUtil.js new file mode 100644 index 000000000..91b2afb81 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/helpers/parseUtil.js @@ -0,0 +1,109 @@ +import { getErrorMap } from "../errors.js"; +import defaultErrorMap from "../locales/en.js"; +export const makeIssue = (params) => { + const { data, path, errorMaps, issueData } = params; + const fullPath = [...path, ...(issueData.path || [])]; + const fullIssue = { + ...issueData, + path: fullPath, + }; + if (issueData.message !== undefined) { + return { + ...issueData, + path: fullPath, + message: issueData.message, + }; + } + let errorMessage = ""; + const maps = errorMaps + .filter((m) => !!m) + .slice() + .reverse(); + for (const map of maps) { + errorMessage = map(fullIssue, { data, defaultError: errorMessage }).message; + } + return { + ...issueData, + path: fullPath, + message: errorMessage, + }; +}; +export const EMPTY_PATH = []; +export function addIssueToContext(ctx, issueData) { + const overrideMap = getErrorMap(); + const issue = makeIssue({ + issueData: issueData, + data: ctx.data, + path: ctx.path, + errorMaps: [ + ctx.common.contextualErrorMap, // contextual error map is first priority + ctx.schemaErrorMap, // then schema-bound map if available + overrideMap, // then global override map + overrideMap === defaultErrorMap ? undefined : defaultErrorMap, // then global default map + ].filter((x) => !!x), + }); + ctx.common.issues.push(issue); +} +export class ParseStatus { + constructor() { + this.value = "valid"; + } + dirty() { + if (this.value === "valid") + this.value = "dirty"; + } + abort() { + if (this.value !== "aborted") + this.value = "aborted"; + } + static mergeArray(status, results) { + const arrayValue = []; + for (const s of results) { + if (s.status === "aborted") + return INVALID; + if (s.status === "dirty") + status.dirty(); + arrayValue.push(s.value); + } + return { status: status.value, value: arrayValue }; + } + static async mergeObjectAsync(status, pairs) { + const syncPairs = []; + for (const pair of pairs) { + const key = await pair.key; + const value = await pair.value; + syncPairs.push({ + key, + value, + }); + } + return ParseStatus.mergeObjectSync(status, syncPairs); + } + static mergeObjectSync(status, pairs) { + const finalObject = {}; + for (const pair of pairs) { + const { key, value } = pair; + if (key.status === "aborted") + return INVALID; + if (value.status === "aborted") + return INVALID; + if (key.status === "dirty") + status.dirty(); + if (value.status === "dirty") + status.dirty(); + if (key.value !== "__proto__" && (typeof value.value !== "undefined" || pair.alwaysSet)) { + finalObject[key.value] = value.value; + } + } + return { status: status.value, value: finalObject }; + } +} +export const INVALID = Object.freeze({ + status: "aborted", +}); +export const DIRTY = (value) => ({ status: "dirty", value }); +export const OK = (value) => ({ status: "valid", value }); +export const isAborted = (x) => x.status === "aborted"; +export const isDirty = (x) => x.status === "dirty"; +export const isValid = (x) => x.status === "valid"; +export const isAsync = (x) => typeof Promise !== "undefined" && x instanceof Promise; diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/helpers/partialUtil.js b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/helpers/partialUtil.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/helpers/partialUtil.js @@ -0,0 +1 @@ +export {}; diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/helpers/typeAliases.js b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/helpers/typeAliases.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/helpers/typeAliases.js @@ -0,0 +1 @@ +export {}; diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/helpers/util.js b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/helpers/util.js new file mode 100644 index 000000000..b9641f29b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/helpers/util.js @@ -0,0 +1,133 @@ +export var util; +(function (util) { + util.assertEqual = (_) => { }; + function assertIs(_arg) { } + util.assertIs = assertIs; + function assertNever(_x) { + throw new Error(); + } + util.assertNever = assertNever; + util.arrayToEnum = (items) => { + const obj = {}; + for (const item of items) { + obj[item] = item; + } + return obj; + }; + util.getValidEnumValues = (obj) => { + const validKeys = util.objectKeys(obj).filter((k) => typeof obj[obj[k]] !== "number"); + const filtered = {}; + for (const k of validKeys) { + filtered[k] = obj[k]; + } + return util.objectValues(filtered); + }; + util.objectValues = (obj) => { + return util.objectKeys(obj).map(function (e) { + return obj[e]; + }); + }; + util.objectKeys = typeof Object.keys === "function" // eslint-disable-line ban/ban + ? (obj) => Object.keys(obj) // eslint-disable-line ban/ban + : (object) => { + const keys = []; + for (const key in object) { + if (Object.prototype.hasOwnProperty.call(object, key)) { + keys.push(key); + } + } + return keys; + }; + util.find = (arr, checker) => { + for (const item of arr) { + if (checker(item)) + return item; + } + return undefined; + }; + util.isInteger = typeof Number.isInteger === "function" + ? (val) => Number.isInteger(val) // eslint-disable-line ban/ban + : (val) => typeof val === "number" && Number.isFinite(val) && Math.floor(val) === val; + function joinValues(array, separator = " | ") { + return array.map((val) => (typeof val === "string" ? `'${val}'` : val)).join(separator); + } + util.joinValues = joinValues; + util.jsonStringifyReplacer = (_, value) => { + if (typeof value === "bigint") { + return value.toString(); + } + return value; + }; +})(util || (util = {})); +export var objectUtil; +(function (objectUtil) { + objectUtil.mergeShapes = (first, second) => { + return { + ...first, + ...second, // second overwrites first + }; + }; +})(objectUtil || (objectUtil = {})); +export const ZodParsedType = util.arrayToEnum([ + "string", + "nan", + "number", + "integer", + "float", + "boolean", + "date", + "bigint", + "symbol", + "function", + "undefined", + "null", + "array", + "object", + "unknown", + "promise", + "void", + "never", + "map", + "set", +]); +export const getParsedType = (data) => { + const t = typeof data; + switch (t) { + case "undefined": + return ZodParsedType.undefined; + case "string": + return ZodParsedType.string; + case "number": + return Number.isNaN(data) ? ZodParsedType.nan : ZodParsedType.number; + case "boolean": + return ZodParsedType.boolean; + case "function": + return ZodParsedType.function; + case "bigint": + return ZodParsedType.bigint; + case "symbol": + return ZodParsedType.symbol; + case "object": + if (Array.isArray(data)) { + return ZodParsedType.array; + } + if (data === null) { + return ZodParsedType.null; + } + if (data.then && typeof data.then === "function" && data.catch && typeof data.catch === "function") { + return ZodParsedType.promise; + } + if (typeof Map !== "undefined" && data instanceof Map) { + return ZodParsedType.map; + } + if (typeof Set !== "undefined" && data instanceof Set) { + return ZodParsedType.set; + } + if (typeof Date !== "undefined" && data instanceof Date) { + return ZodParsedType.date; + } + return ZodParsedType.object; + default: + return ZodParsedType.unknown; + } +}; diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/index.js b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/index.js new file mode 100644 index 000000000..a9dab5738 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/index.js @@ -0,0 +1,4 @@ +import * as z from "./external.js"; +export * from "./external.js"; +export { z }; +export default z; diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/locales/en.js b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/locales/en.js new file mode 100644 index 000000000..3e97ad51a --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/locales/en.js @@ -0,0 +1,107 @@ +import { ZodIssueCode } from "../ZodError.js"; +import { util, ZodParsedType } from "../helpers/util.js"; +const errorMap = (issue, _ctx) => { + let message; + switch (issue.code) { + case ZodIssueCode.invalid_type: + if (issue.received === ZodParsedType.undefined) { + message = "Required"; + } + else { + message = `Expected ${issue.expected}, received ${issue.received}`; + } + break; + case ZodIssueCode.invalid_literal: + message = `Invalid literal value, expected ${JSON.stringify(issue.expected, util.jsonStringifyReplacer)}`; + break; + case ZodIssueCode.unrecognized_keys: + message = `Unrecognized key(s) in object: ${util.joinValues(issue.keys, ", ")}`; + break; + case ZodIssueCode.invalid_union: + message = `Invalid input`; + break; + case ZodIssueCode.invalid_union_discriminator: + message = `Invalid discriminator value. Expected ${util.joinValues(issue.options)}`; + break; + case ZodIssueCode.invalid_enum_value: + message = `Invalid enum value. Expected ${util.joinValues(issue.options)}, received '${issue.received}'`; + break; + case ZodIssueCode.invalid_arguments: + message = `Invalid function arguments`; + break; + case ZodIssueCode.invalid_return_type: + message = `Invalid function return type`; + break; + case ZodIssueCode.invalid_date: + message = `Invalid date`; + break; + case ZodIssueCode.invalid_string: + if (typeof issue.validation === "object") { + if ("includes" in issue.validation) { + message = `Invalid input: must include "${issue.validation.includes}"`; + if (typeof issue.validation.position === "number") { + message = `${message} at one or more positions greater than or equal to ${issue.validation.position}`; + } + } + else if ("startsWith" in issue.validation) { + message = `Invalid input: must start with "${issue.validation.startsWith}"`; + } + else if ("endsWith" in issue.validation) { + message = `Invalid input: must end with "${issue.validation.endsWith}"`; + } + else { + util.assertNever(issue.validation); + } + } + else if (issue.validation !== "regex") { + message = `Invalid ${issue.validation}`; + } + else { + message = "Invalid"; + } + break; + case ZodIssueCode.too_small: + if (issue.type === "array") + message = `Array must contain ${issue.exact ? "exactly" : issue.inclusive ? `at least` : `more than`} ${issue.minimum} element(s)`; + else if (issue.type === "string") + message = `String must contain ${issue.exact ? "exactly" : issue.inclusive ? `at least` : `over`} ${issue.minimum} character(s)`; + else if (issue.type === "number") + message = `Number must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${issue.minimum}`; + else if (issue.type === "date") + message = `Date must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${new Date(Number(issue.minimum))}`; + else + message = "Invalid input"; + break; + case ZodIssueCode.too_big: + if (issue.type === "array") + message = `Array must contain ${issue.exact ? `exactly` : issue.inclusive ? `at most` : `less than`} ${issue.maximum} element(s)`; + else if (issue.type === "string") + message = `String must contain ${issue.exact ? `exactly` : issue.inclusive ? `at most` : `under`} ${issue.maximum} character(s)`; + else if (issue.type === "number") + message = `Number must be ${issue.exact ? `exactly` : issue.inclusive ? `less than or equal to` : `less than`} ${issue.maximum}`; + else if (issue.type === "bigint") + message = `BigInt must be ${issue.exact ? `exactly` : issue.inclusive ? `less than or equal to` : `less than`} ${issue.maximum}`; + else if (issue.type === "date") + message = `Date must be ${issue.exact ? `exactly` : issue.inclusive ? `smaller than or equal to` : `smaller than`} ${new Date(Number(issue.maximum))}`; + else + message = "Invalid input"; + break; + case ZodIssueCode.custom: + message = `Invalid input`; + break; + case ZodIssueCode.invalid_intersection_types: + message = `Intersection results could not be merged`; + break; + case ZodIssueCode.not_multiple_of: + message = `Number must be a multiple of ${issue.multipleOf}`; + break; + case ZodIssueCode.not_finite: + message = "Number must be finite"; + break; + default: + message = _ctx.defaultError; + util.assertNever(issue); + } + return { message }; +}; +export default errorMap; diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/standard-schema.js b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/standard-schema.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/standard-schema.js @@ -0,0 +1 @@ +export {}; diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/types.js b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/types.js new file mode 100644 index 000000000..de1e6213e --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/types.js @@ -0,0 +1,3691 @@ +import { ZodError, ZodIssueCode, } from "./ZodError.js"; +import { defaultErrorMap, getErrorMap } from "./errors.js"; +import { errorUtil } from "./helpers/errorUtil.js"; +import { DIRTY, INVALID, OK, ParseStatus, addIssueToContext, isAborted, isAsync, isDirty, isValid, makeIssue, } from "./helpers/parseUtil.js"; +import { util, ZodParsedType, getParsedType } from "./helpers/util.js"; +class ParseInputLazyPath { + constructor(parent, value, path, key) { + this._cachedPath = []; + this.parent = parent; + this.data = value; + this._path = path; + this._key = key; + } + get path() { + if (!this._cachedPath.length) { + if (Array.isArray(this._key)) { + this._cachedPath.push(...this._path, ...this._key); + } + else { + this._cachedPath.push(...this._path, this._key); + } + } + return this._cachedPath; + } +} +const handleResult = (ctx, result) => { + if (isValid(result)) { + return { success: true, data: result.value }; + } + else { + if (!ctx.common.issues.length) { + throw new Error("Validation failed but no issues detected."); + } + return { + success: false, + get error() { + if (this._error) + return this._error; + const error = new ZodError(ctx.common.issues); + this._error = error; + return this._error; + }, + }; + } +}; +function processCreateParams(params) { + if (!params) + return {}; + const { errorMap, invalid_type_error, required_error, description } = params; + if (errorMap && (invalid_type_error || required_error)) { + throw new Error(`Can't use "invalid_type_error" or "required_error" in conjunction with custom error map.`); + } + if (errorMap) + return { errorMap: errorMap, description }; + const customMap = (iss, ctx) => { + const { message } = params; + if (iss.code === "invalid_enum_value") { + return { message: message ?? ctx.defaultError }; + } + if (typeof ctx.data === "undefined") { + return { message: message ?? required_error ?? ctx.defaultError }; + } + if (iss.code !== "invalid_type") + return { message: ctx.defaultError }; + return { message: message ?? invalid_type_error ?? ctx.defaultError }; + }; + return { errorMap: customMap, description }; +} +export class ZodType { + get description() { + return this._def.description; + } + _getType(input) { + return getParsedType(input.data); + } + _getOrReturnCtx(input, ctx) { + return (ctx || { + common: input.parent.common, + data: input.data, + parsedType: getParsedType(input.data), + schemaErrorMap: this._def.errorMap, + path: input.path, + parent: input.parent, + }); + } + _processInputParams(input) { + return { + status: new ParseStatus(), + ctx: { + common: input.parent.common, + data: input.data, + parsedType: getParsedType(input.data), + schemaErrorMap: this._def.errorMap, + path: input.path, + parent: input.parent, + }, + }; + } + _parseSync(input) { + const result = this._parse(input); + if (isAsync(result)) { + throw new Error("Synchronous parse encountered promise."); + } + return result; + } + _parseAsync(input) { + const result = this._parse(input); + return Promise.resolve(result); + } + parse(data, params) { + const result = this.safeParse(data, params); + if (result.success) + return result.data; + throw result.error; + } + safeParse(data, params) { + const ctx = { + common: { + issues: [], + async: params?.async ?? false, + contextualErrorMap: params?.errorMap, + }, + path: params?.path || [], + schemaErrorMap: this._def.errorMap, + parent: null, + data, + parsedType: getParsedType(data), + }; + const result = this._parseSync({ data, path: ctx.path, parent: ctx }); + return handleResult(ctx, result); + } + "~validate"(data) { + const ctx = { + common: { + issues: [], + async: !!this["~standard"].async, + }, + path: [], + schemaErrorMap: this._def.errorMap, + parent: null, + data, + parsedType: getParsedType(data), + }; + if (!this["~standard"].async) { + try { + const result = this._parseSync({ data, path: [], parent: ctx }); + return isValid(result) + ? { + value: result.value, + } + : { + issues: ctx.common.issues, + }; + } + catch (err) { + if (err?.message?.toLowerCase()?.includes("encountered")) { + this["~standard"].async = true; + } + ctx.common = { + issues: [], + async: true, + }; + } + } + return this._parseAsync({ data, path: [], parent: ctx }).then((result) => isValid(result) + ? { + value: result.value, + } + : { + issues: ctx.common.issues, + }); + } + async parseAsync(data, params) { + const result = await this.safeParseAsync(data, params); + if (result.success) + return result.data; + throw result.error; + } + async safeParseAsync(data, params) { + const ctx = { + common: { + issues: [], + contextualErrorMap: params?.errorMap, + async: true, + }, + path: params?.path || [], + schemaErrorMap: this._def.errorMap, + parent: null, + data, + parsedType: getParsedType(data), + }; + const maybeAsyncResult = this._parse({ data, path: ctx.path, parent: ctx }); + const result = await (isAsync(maybeAsyncResult) ? maybeAsyncResult : Promise.resolve(maybeAsyncResult)); + return handleResult(ctx, result); + } + refine(check, message) { + const getIssueProperties = (val) => { + if (typeof message === "string" || typeof message === "undefined") { + return { message }; + } + else if (typeof message === "function") { + return message(val); + } + else { + return message; + } + }; + return this._refinement((val, ctx) => { + const result = check(val); + const setError = () => ctx.addIssue({ + code: ZodIssueCode.custom, + ...getIssueProperties(val), + }); + if (typeof Promise !== "undefined" && result instanceof Promise) { + return result.then((data) => { + if (!data) { + setError(); + return false; + } + else { + return true; + } + }); + } + if (!result) { + setError(); + return false; + } + else { + return true; + } + }); + } + refinement(check, refinementData) { + return this._refinement((val, ctx) => { + if (!check(val)) { + ctx.addIssue(typeof refinementData === "function" ? refinementData(val, ctx) : refinementData); + return false; + } + else { + return true; + } + }); + } + _refinement(refinement) { + return new ZodEffects({ + schema: this, + typeName: ZodFirstPartyTypeKind.ZodEffects, + effect: { type: "refinement", refinement }, + }); + } + superRefine(refinement) { + return this._refinement(refinement); + } + constructor(def) { + /** Alias of safeParseAsync */ + this.spa = this.safeParseAsync; + this._def = def; + this.parse = this.parse.bind(this); + this.safeParse = this.safeParse.bind(this); + this.parseAsync = this.parseAsync.bind(this); + this.safeParseAsync = this.safeParseAsync.bind(this); + this.spa = this.spa.bind(this); + this.refine = this.refine.bind(this); + this.refinement = this.refinement.bind(this); + this.superRefine = this.superRefine.bind(this); + this.optional = this.optional.bind(this); + this.nullable = this.nullable.bind(this); + this.nullish = this.nullish.bind(this); + this.array = this.array.bind(this); + this.promise = this.promise.bind(this); + this.or = this.or.bind(this); + this.and = this.and.bind(this); + this.transform = this.transform.bind(this); + this.brand = this.brand.bind(this); + this.default = this.default.bind(this); + this.catch = this.catch.bind(this); + this.describe = this.describe.bind(this); + this.pipe = this.pipe.bind(this); + this.readonly = this.readonly.bind(this); + this.isNullable = this.isNullable.bind(this); + this.isOptional = this.isOptional.bind(this); + this["~standard"] = { + version: 1, + vendor: "zod", + validate: (data) => this["~validate"](data), + }; + } + optional() { + return ZodOptional.create(this, this._def); + } + nullable() { + return ZodNullable.create(this, this._def); + } + nullish() { + return this.nullable().optional(); + } + array() { + return ZodArray.create(this); + } + promise() { + return ZodPromise.create(this, this._def); + } + or(option) { + return ZodUnion.create([this, option], this._def); + } + and(incoming) { + return ZodIntersection.create(this, incoming, this._def); + } + transform(transform) { + return new ZodEffects({ + ...processCreateParams(this._def), + schema: this, + typeName: ZodFirstPartyTypeKind.ZodEffects, + effect: { type: "transform", transform }, + }); + } + default(def) { + const defaultValueFunc = typeof def === "function" ? def : () => def; + return new ZodDefault({ + ...processCreateParams(this._def), + innerType: this, + defaultValue: defaultValueFunc, + typeName: ZodFirstPartyTypeKind.ZodDefault, + }); + } + brand() { + return new ZodBranded({ + typeName: ZodFirstPartyTypeKind.ZodBranded, + type: this, + ...processCreateParams(this._def), + }); + } + catch(def) { + const catchValueFunc = typeof def === "function" ? def : () => def; + return new ZodCatch({ + ...processCreateParams(this._def), + innerType: this, + catchValue: catchValueFunc, + typeName: ZodFirstPartyTypeKind.ZodCatch, + }); + } + describe(description) { + const This = this.constructor; + return new This({ + ...this._def, + description, + }); + } + pipe(target) { + return ZodPipeline.create(this, target); + } + readonly() { + return ZodReadonly.create(this); + } + isOptional() { + return this.safeParse(undefined).success; + } + isNullable() { + return this.safeParse(null).success; + } +} +const cuidRegex = /^c[^\s-]{8,}$/i; +const cuid2Regex = /^[0-9a-z]+$/; +const ulidRegex = /^[0-9A-HJKMNP-TV-Z]{26}$/i; +// const uuidRegex = +// /^([a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[a-f0-9]{4}-[a-f0-9]{12}|00000000-0000-0000-0000-000000000000)$/i; +const uuidRegex = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/i; +const nanoidRegex = /^[a-z0-9_-]{21}$/i; +const jwtRegex = /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$/; +const durationRegex = /^[-+]?P(?!$)(?:(?:[-+]?\d+Y)|(?:[-+]?\d+[.,]\d+Y$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:(?:[-+]?\d+W)|(?:[-+]?\d+[.,]\d+W$))?(?:(?:[-+]?\d+D)|(?:[-+]?\d+[.,]\d+D$))?(?:T(?=[\d+-])(?:(?:[-+]?\d+H)|(?:[-+]?\d+[.,]\d+H$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:[-+]?\d+(?:[.,]\d+)?S)?)??$/; +// from https://stackoverflow.com/a/46181/1550155 +// old version: too slow, didn't support unicode +// const emailRegex = /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i; +//old email regex +// const emailRegex = /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@((?!-)([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{1,})[^-<>()[\].,;:\s@"]$/i; +// eslint-disable-next-line +// const emailRegex = +// /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\])|(\[IPv6:(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))\])|([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])*(\.[A-Za-z]{2,})+))$/; +// const emailRegex = +// /^[a-zA-Z0-9\.\!\#\$\%\&\'\*\+\/\=\?\^\_\`\{\|\}\~\-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; +// const emailRegex = +// /^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$/i; +const emailRegex = /^(?!\.)(?!.*\.\.)([A-Z0-9_'+\-\.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i; +// const emailRegex = +// /^[a-z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-z0-9-]+(?:\.[a-z0-9\-]+)*$/i; +// from https://thekevinscott.com/emojis-in-javascript/#writing-a-regular-expression +const _emojiRegex = `^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$`; +let emojiRegex; +// faster, simpler, safer +const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/; +const ipv4CidrRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/(3[0-2]|[12]?[0-9])$/; +// const ipv6Regex = +// /^(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))$/; +const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/; +const ipv6CidrRegex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/; +// https://stackoverflow.com/questions/7860392/determine-if-string-is-in-base64-using-javascript +const base64Regex = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/; +// https://base64.guru/standards/base64url +const base64urlRegex = /^([0-9a-zA-Z-_]{4})*(([0-9a-zA-Z-_]{2}(==)?)|([0-9a-zA-Z-_]{3}(=)?))?$/; +// simple +// const dateRegexSource = `\\d{4}-\\d{2}-\\d{2}`; +// no leap year validation +// const dateRegexSource = `\\d{4}-((0[13578]|10|12)-31|(0[13-9]|1[0-2])-30|(0[1-9]|1[0-2])-(0[1-9]|1\\d|2\\d))`; +// with leap year validation +const dateRegexSource = `((\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\\d|3[01])|(0[469]|11)-(0[1-9]|[12]\\d|30)|(02)-(0[1-9]|1\\d|2[0-8])))`; +const dateRegex = new RegExp(`^${dateRegexSource}$`); +function timeRegexSource(args) { + let secondsRegexSource = `[0-5]\\d`; + if (args.precision) { + secondsRegexSource = `${secondsRegexSource}\\.\\d{${args.precision}}`; + } + else if (args.precision == null) { + secondsRegexSource = `${secondsRegexSource}(\\.\\d+)?`; + } + const secondsQuantifier = args.precision ? "+" : "?"; // require seconds if precision is nonzero + return `([01]\\d|2[0-3]):[0-5]\\d(:${secondsRegexSource})${secondsQuantifier}`; +} +function timeRegex(args) { + return new RegExp(`^${timeRegexSource(args)}$`); +} +// Adapted from https://stackoverflow.com/a/3143231 +export function datetimeRegex(args) { + let regex = `${dateRegexSource}T${timeRegexSource(args)}`; + const opts = []; + opts.push(args.local ? `Z?` : `Z`); + if (args.offset) + opts.push(`([+-]\\d{2}:?\\d{2})`); + regex = `${regex}(${opts.join("|")})`; + return new RegExp(`^${regex}$`); +} +function isValidIP(ip, version) { + if ((version === "v4" || !version) && ipv4Regex.test(ip)) { + return true; + } + if ((version === "v6" || !version) && ipv6Regex.test(ip)) { + return true; + } + return false; +} +function isValidJWT(jwt, alg) { + if (!jwtRegex.test(jwt)) + return false; + try { + const [header] = jwt.split("."); + // Convert base64url to base64 + const base64 = header + .replace(/-/g, "+") + .replace(/_/g, "/") + .padEnd(header.length + ((4 - (header.length % 4)) % 4), "="); + const decoded = JSON.parse(atob(base64)); + if (typeof decoded !== "object" || decoded === null) + return false; + if ("typ" in decoded && decoded?.typ !== "JWT") + return false; + if (!decoded.alg) + return false; + if (alg && decoded.alg !== alg) + return false; + return true; + } + catch { + return false; + } +} +function isValidCidr(ip, version) { + if ((version === "v4" || !version) && ipv4CidrRegex.test(ip)) { + return true; + } + if ((version === "v6" || !version) && ipv6CidrRegex.test(ip)) { + return true; + } + return false; +} +export class ZodString extends ZodType { + _parse(input) { + if (this._def.coerce) { + input.data = String(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.string) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.string, + received: ctx.parsedType, + }); + return INVALID; + } + const status = new ParseStatus(); + let ctx = undefined; + for (const check of this._def.checks) { + if (check.kind === "min") { + if (input.data.length < check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: check.value, + type: "string", + inclusive: true, + exact: false, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "max") { + if (input.data.length > check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: check.value, + type: "string", + inclusive: true, + exact: false, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "length") { + const tooBig = input.data.length > check.value; + const tooSmall = input.data.length < check.value; + if (tooBig || tooSmall) { + ctx = this._getOrReturnCtx(input, ctx); + if (tooBig) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: check.value, + type: "string", + inclusive: true, + exact: true, + message: check.message, + }); + } + else if (tooSmall) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: check.value, + type: "string", + inclusive: true, + exact: true, + message: check.message, + }); + } + status.dirty(); + } + } + else if (check.kind === "email") { + if (!emailRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "email", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "emoji") { + if (!emojiRegex) { + emojiRegex = new RegExp(_emojiRegex, "u"); + } + if (!emojiRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "emoji", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "uuid") { + if (!uuidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "uuid", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "nanoid") { + if (!nanoidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "nanoid", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "cuid") { + if (!cuidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cuid", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "cuid2") { + if (!cuid2Regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cuid2", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "ulid") { + if (!ulidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "ulid", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "url") { + try { + new URL(input.data); + } + catch { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "url", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "regex") { + check.regex.lastIndex = 0; + const testResult = check.regex.test(input.data); + if (!testResult) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "regex", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "trim") { + input.data = input.data.trim(); + } + else if (check.kind === "includes") { + if (!input.data.includes(check.value, check.position)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: { includes: check.value, position: check.position }, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "toLowerCase") { + input.data = input.data.toLowerCase(); + } + else if (check.kind === "toUpperCase") { + input.data = input.data.toUpperCase(); + } + else if (check.kind === "startsWith") { + if (!input.data.startsWith(check.value)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: { startsWith: check.value }, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "endsWith") { + if (!input.data.endsWith(check.value)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: { endsWith: check.value }, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "datetime") { + const regex = datetimeRegex(check); + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: "datetime", + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "date") { + const regex = dateRegex; + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: "date", + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "time") { + const regex = timeRegex(check); + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: "time", + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "duration") { + if (!durationRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "duration", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "ip") { + if (!isValidIP(input.data, check.version)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "ip", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "jwt") { + if (!isValidJWT(input.data, check.alg)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "jwt", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "cidr") { + if (!isValidCidr(input.data, check.version)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cidr", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "base64") { + if (!base64Regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "base64", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "base64url") { + if (!base64urlRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "base64url", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else { + util.assertNever(check); + } + } + return { status: status.value, value: input.data }; + } + _regex(regex, validation, message) { + return this.refinement((data) => regex.test(data), { + validation, + code: ZodIssueCode.invalid_string, + ...errorUtil.errToObj(message), + }); + } + _addCheck(check) { + return new ZodString({ + ...this._def, + checks: [...this._def.checks, check], + }); + } + email(message) { + return this._addCheck({ kind: "email", ...errorUtil.errToObj(message) }); + } + url(message) { + return this._addCheck({ kind: "url", ...errorUtil.errToObj(message) }); + } + emoji(message) { + return this._addCheck({ kind: "emoji", ...errorUtil.errToObj(message) }); + } + uuid(message) { + return this._addCheck({ kind: "uuid", ...errorUtil.errToObj(message) }); + } + nanoid(message) { + return this._addCheck({ kind: "nanoid", ...errorUtil.errToObj(message) }); + } + cuid(message) { + return this._addCheck({ kind: "cuid", ...errorUtil.errToObj(message) }); + } + cuid2(message) { + return this._addCheck({ kind: "cuid2", ...errorUtil.errToObj(message) }); + } + ulid(message) { + return this._addCheck({ kind: "ulid", ...errorUtil.errToObj(message) }); + } + base64(message) { + return this._addCheck({ kind: "base64", ...errorUtil.errToObj(message) }); + } + base64url(message) { + // base64url encoding is a modification of base64 that can safely be used in URLs and filenames + return this._addCheck({ + kind: "base64url", + ...errorUtil.errToObj(message), + }); + } + jwt(options) { + return this._addCheck({ kind: "jwt", ...errorUtil.errToObj(options) }); + } + ip(options) { + return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) }); + } + cidr(options) { + return this._addCheck({ kind: "cidr", ...errorUtil.errToObj(options) }); + } + datetime(options) { + if (typeof options === "string") { + return this._addCheck({ + kind: "datetime", + precision: null, + offset: false, + local: false, + message: options, + }); + } + return this._addCheck({ + kind: "datetime", + precision: typeof options?.precision === "undefined" ? null : options?.precision, + offset: options?.offset ?? false, + local: options?.local ?? false, + ...errorUtil.errToObj(options?.message), + }); + } + date(message) { + return this._addCheck({ kind: "date", message }); + } + time(options) { + if (typeof options === "string") { + return this._addCheck({ + kind: "time", + precision: null, + message: options, + }); + } + return this._addCheck({ + kind: "time", + precision: typeof options?.precision === "undefined" ? null : options?.precision, + ...errorUtil.errToObj(options?.message), + }); + } + duration(message) { + return this._addCheck({ kind: "duration", ...errorUtil.errToObj(message) }); + } + regex(regex, message) { + return this._addCheck({ + kind: "regex", + regex: regex, + ...errorUtil.errToObj(message), + }); + } + includes(value, options) { + return this._addCheck({ + kind: "includes", + value: value, + position: options?.position, + ...errorUtil.errToObj(options?.message), + }); + } + startsWith(value, message) { + return this._addCheck({ + kind: "startsWith", + value: value, + ...errorUtil.errToObj(message), + }); + } + endsWith(value, message) { + return this._addCheck({ + kind: "endsWith", + value: value, + ...errorUtil.errToObj(message), + }); + } + min(minLength, message) { + return this._addCheck({ + kind: "min", + value: minLength, + ...errorUtil.errToObj(message), + }); + } + max(maxLength, message) { + return this._addCheck({ + kind: "max", + value: maxLength, + ...errorUtil.errToObj(message), + }); + } + length(len, message) { + return this._addCheck({ + kind: "length", + value: len, + ...errorUtil.errToObj(message), + }); + } + /** + * Equivalent to `.min(1)` + */ + nonempty(message) { + return this.min(1, errorUtil.errToObj(message)); + } + trim() { + return new ZodString({ + ...this._def, + checks: [...this._def.checks, { kind: "trim" }], + }); + } + toLowerCase() { + return new ZodString({ + ...this._def, + checks: [...this._def.checks, { kind: "toLowerCase" }], + }); + } + toUpperCase() { + return new ZodString({ + ...this._def, + checks: [...this._def.checks, { kind: "toUpperCase" }], + }); + } + get isDatetime() { + return !!this._def.checks.find((ch) => ch.kind === "datetime"); + } + get isDate() { + return !!this._def.checks.find((ch) => ch.kind === "date"); + } + get isTime() { + return !!this._def.checks.find((ch) => ch.kind === "time"); + } + get isDuration() { + return !!this._def.checks.find((ch) => ch.kind === "duration"); + } + get isEmail() { + return !!this._def.checks.find((ch) => ch.kind === "email"); + } + get isURL() { + return !!this._def.checks.find((ch) => ch.kind === "url"); + } + get isEmoji() { + return !!this._def.checks.find((ch) => ch.kind === "emoji"); + } + get isUUID() { + return !!this._def.checks.find((ch) => ch.kind === "uuid"); + } + get isNANOID() { + return !!this._def.checks.find((ch) => ch.kind === "nanoid"); + } + get isCUID() { + return !!this._def.checks.find((ch) => ch.kind === "cuid"); + } + get isCUID2() { + return !!this._def.checks.find((ch) => ch.kind === "cuid2"); + } + get isULID() { + return !!this._def.checks.find((ch) => ch.kind === "ulid"); + } + get isIP() { + return !!this._def.checks.find((ch) => ch.kind === "ip"); + } + get isCIDR() { + return !!this._def.checks.find((ch) => ch.kind === "cidr"); + } + get isBase64() { + return !!this._def.checks.find((ch) => ch.kind === "base64"); + } + get isBase64url() { + // base64url encoding is a modification of base64 that can safely be used in URLs and filenames + return !!this._def.checks.find((ch) => ch.kind === "base64url"); + } + get minLength() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min; + } + get maxLength() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max; + } +} +ZodString.create = (params) => { + return new ZodString({ + checks: [], + typeName: ZodFirstPartyTypeKind.ZodString, + coerce: params?.coerce ?? false, + ...processCreateParams(params), + }); +}; +// https://stackoverflow.com/questions/3966484/why-does-modulus-operator-return-fractional-number-in-javascript/31711034#31711034 +function floatSafeRemainder(val, step) { + const valDecCount = (val.toString().split(".")[1] || "").length; + const stepDecCount = (step.toString().split(".")[1] || "").length; + const decCount = valDecCount > stepDecCount ? valDecCount : stepDecCount; + const valInt = Number.parseInt(val.toFixed(decCount).replace(".", "")); + const stepInt = Number.parseInt(step.toFixed(decCount).replace(".", "")); + return (valInt % stepInt) / 10 ** decCount; +} +export class ZodNumber extends ZodType { + constructor() { + super(...arguments); + this.min = this.gte; + this.max = this.lte; + this.step = this.multipleOf; + } + _parse(input) { + if (this._def.coerce) { + input.data = Number(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.number) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.number, + received: ctx.parsedType, + }); + return INVALID; + } + let ctx = undefined; + const status = new ParseStatus(); + for (const check of this._def.checks) { + if (check.kind === "int") { + if (!util.isInteger(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: "integer", + received: "float", + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "min") { + const tooSmall = check.inclusive ? input.data < check.value : input.data <= check.value; + if (tooSmall) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: check.value, + type: "number", + inclusive: check.inclusive, + exact: false, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "max") { + const tooBig = check.inclusive ? input.data > check.value : input.data >= check.value; + if (tooBig) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: check.value, + type: "number", + inclusive: check.inclusive, + exact: false, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "multipleOf") { + if (floatSafeRemainder(input.data, check.value) !== 0) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.not_multiple_of, + multipleOf: check.value, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "finite") { + if (!Number.isFinite(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.not_finite, + message: check.message, + }); + status.dirty(); + } + } + else { + util.assertNever(check); + } + } + return { status: status.value, value: input.data }; + } + gte(value, message) { + return this.setLimit("min", value, true, errorUtil.toString(message)); + } + gt(value, message) { + return this.setLimit("min", value, false, errorUtil.toString(message)); + } + lte(value, message) { + return this.setLimit("max", value, true, errorUtil.toString(message)); + } + lt(value, message) { + return this.setLimit("max", value, false, errorUtil.toString(message)); + } + setLimit(kind, value, inclusive, message) { + return new ZodNumber({ + ...this._def, + checks: [ + ...this._def.checks, + { + kind, + value, + inclusive, + message: errorUtil.toString(message), + }, + ], + }); + } + _addCheck(check) { + return new ZodNumber({ + ...this._def, + checks: [...this._def.checks, check], + }); + } + int(message) { + return this._addCheck({ + kind: "int", + message: errorUtil.toString(message), + }); + } + positive(message) { + return this._addCheck({ + kind: "min", + value: 0, + inclusive: false, + message: errorUtil.toString(message), + }); + } + negative(message) { + return this._addCheck({ + kind: "max", + value: 0, + inclusive: false, + message: errorUtil.toString(message), + }); + } + nonpositive(message) { + return this._addCheck({ + kind: "max", + value: 0, + inclusive: true, + message: errorUtil.toString(message), + }); + } + nonnegative(message) { + return this._addCheck({ + kind: "min", + value: 0, + inclusive: true, + message: errorUtil.toString(message), + }); + } + multipleOf(value, message) { + return this._addCheck({ + kind: "multipleOf", + value: value, + message: errorUtil.toString(message), + }); + } + finite(message) { + return this._addCheck({ + kind: "finite", + message: errorUtil.toString(message), + }); + } + safe(message) { + return this._addCheck({ + kind: "min", + inclusive: true, + value: Number.MIN_SAFE_INTEGER, + message: errorUtil.toString(message), + })._addCheck({ + kind: "max", + inclusive: true, + value: Number.MAX_SAFE_INTEGER, + message: errorUtil.toString(message), + }); + } + get minValue() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min; + } + get maxValue() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max; + } + get isInt() { + return !!this._def.checks.find((ch) => ch.kind === "int" || (ch.kind === "multipleOf" && util.isInteger(ch.value))); + } + get isFinite() { + let max = null; + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "finite" || ch.kind === "int" || ch.kind === "multipleOf") { + return true; + } + else if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + else if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return Number.isFinite(min) && Number.isFinite(max); + } +} +ZodNumber.create = (params) => { + return new ZodNumber({ + checks: [], + typeName: ZodFirstPartyTypeKind.ZodNumber, + coerce: params?.coerce || false, + ...processCreateParams(params), + }); +}; +export class ZodBigInt extends ZodType { + constructor() { + super(...arguments); + this.min = this.gte; + this.max = this.lte; + } + _parse(input) { + if (this._def.coerce) { + try { + input.data = BigInt(input.data); + } + catch { + return this._getInvalidInput(input); + } + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.bigint) { + return this._getInvalidInput(input); + } + let ctx = undefined; + const status = new ParseStatus(); + for (const check of this._def.checks) { + if (check.kind === "min") { + const tooSmall = check.inclusive ? input.data < check.value : input.data <= check.value; + if (tooSmall) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + type: "bigint", + minimum: check.value, + inclusive: check.inclusive, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "max") { + const tooBig = check.inclusive ? input.data > check.value : input.data >= check.value; + if (tooBig) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + type: "bigint", + maximum: check.value, + inclusive: check.inclusive, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "multipleOf") { + if (input.data % check.value !== BigInt(0)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.not_multiple_of, + multipleOf: check.value, + message: check.message, + }); + status.dirty(); + } + } + else { + util.assertNever(check); + } + } + return { status: status.value, value: input.data }; + } + _getInvalidInput(input) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.bigint, + received: ctx.parsedType, + }); + return INVALID; + } + gte(value, message) { + return this.setLimit("min", value, true, errorUtil.toString(message)); + } + gt(value, message) { + return this.setLimit("min", value, false, errorUtil.toString(message)); + } + lte(value, message) { + return this.setLimit("max", value, true, errorUtil.toString(message)); + } + lt(value, message) { + return this.setLimit("max", value, false, errorUtil.toString(message)); + } + setLimit(kind, value, inclusive, message) { + return new ZodBigInt({ + ...this._def, + checks: [ + ...this._def.checks, + { + kind, + value, + inclusive, + message: errorUtil.toString(message), + }, + ], + }); + } + _addCheck(check) { + return new ZodBigInt({ + ...this._def, + checks: [...this._def.checks, check], + }); + } + positive(message) { + return this._addCheck({ + kind: "min", + value: BigInt(0), + inclusive: false, + message: errorUtil.toString(message), + }); + } + negative(message) { + return this._addCheck({ + kind: "max", + value: BigInt(0), + inclusive: false, + message: errorUtil.toString(message), + }); + } + nonpositive(message) { + return this._addCheck({ + kind: "max", + value: BigInt(0), + inclusive: true, + message: errorUtil.toString(message), + }); + } + nonnegative(message) { + return this._addCheck({ + kind: "min", + value: BigInt(0), + inclusive: true, + message: errorUtil.toString(message), + }); + } + multipleOf(value, message) { + return this._addCheck({ + kind: "multipleOf", + value, + message: errorUtil.toString(message), + }); + } + get minValue() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min; + } + get maxValue() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max; + } +} +ZodBigInt.create = (params) => { + return new ZodBigInt({ + checks: [], + typeName: ZodFirstPartyTypeKind.ZodBigInt, + coerce: params?.coerce ?? false, + ...processCreateParams(params), + }); +}; +export class ZodBoolean extends ZodType { + _parse(input) { + if (this._def.coerce) { + input.data = Boolean(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.boolean) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.boolean, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodBoolean.create = (params) => { + return new ZodBoolean({ + typeName: ZodFirstPartyTypeKind.ZodBoolean, + coerce: params?.coerce || false, + ...processCreateParams(params), + }); +}; +export class ZodDate extends ZodType { + _parse(input) { + if (this._def.coerce) { + input.data = new Date(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.date) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.date, + received: ctx.parsedType, + }); + return INVALID; + } + if (Number.isNaN(input.data.getTime())) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_date, + }); + return INVALID; + } + const status = new ParseStatus(); + let ctx = undefined; + for (const check of this._def.checks) { + if (check.kind === "min") { + if (input.data.getTime() < check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + message: check.message, + inclusive: true, + exact: false, + minimum: check.value, + type: "date", + }); + status.dirty(); + } + } + else if (check.kind === "max") { + if (input.data.getTime() > check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + message: check.message, + inclusive: true, + exact: false, + maximum: check.value, + type: "date", + }); + status.dirty(); + } + } + else { + util.assertNever(check); + } + } + return { + status: status.value, + value: new Date(input.data.getTime()), + }; + } + _addCheck(check) { + return new ZodDate({ + ...this._def, + checks: [...this._def.checks, check], + }); + } + min(minDate, message) { + return this._addCheck({ + kind: "min", + value: minDate.getTime(), + message: errorUtil.toString(message), + }); + } + max(maxDate, message) { + return this._addCheck({ + kind: "max", + value: maxDate.getTime(), + message: errorUtil.toString(message), + }); + } + get minDate() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min != null ? new Date(min) : null; + } + get maxDate() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max != null ? new Date(max) : null; + } +} +ZodDate.create = (params) => { + return new ZodDate({ + checks: [], + coerce: params?.coerce || false, + typeName: ZodFirstPartyTypeKind.ZodDate, + ...processCreateParams(params), + }); +}; +export class ZodSymbol extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.symbol) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.symbol, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodSymbol.create = (params) => { + return new ZodSymbol({ + typeName: ZodFirstPartyTypeKind.ZodSymbol, + ...processCreateParams(params), + }); +}; +export class ZodUndefined extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.undefined) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.undefined, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodUndefined.create = (params) => { + return new ZodUndefined({ + typeName: ZodFirstPartyTypeKind.ZodUndefined, + ...processCreateParams(params), + }); +}; +export class ZodNull extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.null) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.null, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodNull.create = (params) => { + return new ZodNull({ + typeName: ZodFirstPartyTypeKind.ZodNull, + ...processCreateParams(params), + }); +}; +export class ZodAny extends ZodType { + constructor() { + super(...arguments); + // to prevent instances of other classes from extending ZodAny. this causes issues with catchall in ZodObject. + this._any = true; + } + _parse(input) { + return OK(input.data); + } +} +ZodAny.create = (params) => { + return new ZodAny({ + typeName: ZodFirstPartyTypeKind.ZodAny, + ...processCreateParams(params), + }); +}; +export class ZodUnknown extends ZodType { + constructor() { + super(...arguments); + // required + this._unknown = true; + } + _parse(input) { + return OK(input.data); + } +} +ZodUnknown.create = (params) => { + return new ZodUnknown({ + typeName: ZodFirstPartyTypeKind.ZodUnknown, + ...processCreateParams(params), + }); +}; +export class ZodNever extends ZodType { + _parse(input) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.never, + received: ctx.parsedType, + }); + return INVALID; + } +} +ZodNever.create = (params) => { + return new ZodNever({ + typeName: ZodFirstPartyTypeKind.ZodNever, + ...processCreateParams(params), + }); +}; +export class ZodVoid extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.undefined) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.void, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodVoid.create = (params) => { + return new ZodVoid({ + typeName: ZodFirstPartyTypeKind.ZodVoid, + ...processCreateParams(params), + }); +}; +export class ZodArray extends ZodType { + _parse(input) { + const { ctx, status } = this._processInputParams(input); + const def = this._def; + if (ctx.parsedType !== ZodParsedType.array) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.array, + received: ctx.parsedType, + }); + return INVALID; + } + if (def.exactLength !== null) { + const tooBig = ctx.data.length > def.exactLength.value; + const tooSmall = ctx.data.length < def.exactLength.value; + if (tooBig || tooSmall) { + addIssueToContext(ctx, { + code: tooBig ? ZodIssueCode.too_big : ZodIssueCode.too_small, + minimum: (tooSmall ? def.exactLength.value : undefined), + maximum: (tooBig ? def.exactLength.value : undefined), + type: "array", + inclusive: true, + exact: true, + message: def.exactLength.message, + }); + status.dirty(); + } + } + if (def.minLength !== null) { + if (ctx.data.length < def.minLength.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: def.minLength.value, + type: "array", + inclusive: true, + exact: false, + message: def.minLength.message, + }); + status.dirty(); + } + } + if (def.maxLength !== null) { + if (ctx.data.length > def.maxLength.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: def.maxLength.value, + type: "array", + inclusive: true, + exact: false, + message: def.maxLength.message, + }); + status.dirty(); + } + } + if (ctx.common.async) { + return Promise.all([...ctx.data].map((item, i) => { + return def.type._parseAsync(new ParseInputLazyPath(ctx, item, ctx.path, i)); + })).then((result) => { + return ParseStatus.mergeArray(status, result); + }); + } + const result = [...ctx.data].map((item, i) => { + return def.type._parseSync(new ParseInputLazyPath(ctx, item, ctx.path, i)); + }); + return ParseStatus.mergeArray(status, result); + } + get element() { + return this._def.type; + } + min(minLength, message) { + return new ZodArray({ + ...this._def, + minLength: { value: minLength, message: errorUtil.toString(message) }, + }); + } + max(maxLength, message) { + return new ZodArray({ + ...this._def, + maxLength: { value: maxLength, message: errorUtil.toString(message) }, + }); + } + length(len, message) { + return new ZodArray({ + ...this._def, + exactLength: { value: len, message: errorUtil.toString(message) }, + }); + } + nonempty(message) { + return this.min(1, message); + } +} +ZodArray.create = (schema, params) => { + return new ZodArray({ + type: schema, + minLength: null, + maxLength: null, + exactLength: null, + typeName: ZodFirstPartyTypeKind.ZodArray, + ...processCreateParams(params), + }); +}; +function deepPartialify(schema) { + if (schema instanceof ZodObject) { + const newShape = {}; + for (const key in schema.shape) { + const fieldSchema = schema.shape[key]; + newShape[key] = ZodOptional.create(deepPartialify(fieldSchema)); + } + return new ZodObject({ + ...schema._def, + shape: () => newShape, + }); + } + else if (schema instanceof ZodArray) { + return new ZodArray({ + ...schema._def, + type: deepPartialify(schema.element), + }); + } + else if (schema instanceof ZodOptional) { + return ZodOptional.create(deepPartialify(schema.unwrap())); + } + else if (schema instanceof ZodNullable) { + return ZodNullable.create(deepPartialify(schema.unwrap())); + } + else if (schema instanceof ZodTuple) { + return ZodTuple.create(schema.items.map((item) => deepPartialify(item))); + } + else { + return schema; + } +} +export class ZodObject extends ZodType { + constructor() { + super(...arguments); + this._cached = null; + /** + * @deprecated In most cases, this is no longer needed - unknown properties are now silently stripped. + * If you want to pass through unknown properties, use `.passthrough()` instead. + */ + this.nonstrict = this.passthrough; + // extend< + // Augmentation extends ZodRawShape, + // NewOutput extends util.flatten<{ + // [k in keyof Augmentation | keyof Output]: k extends keyof Augmentation + // ? Augmentation[k]["_output"] + // : k extends keyof Output + // ? Output[k] + // : never; + // }>, + // NewInput extends util.flatten<{ + // [k in keyof Augmentation | keyof Input]: k extends keyof Augmentation + // ? Augmentation[k]["_input"] + // : k extends keyof Input + // ? Input[k] + // : never; + // }> + // >( + // augmentation: Augmentation + // ): ZodObject< + // extendShape, + // UnknownKeys, + // Catchall, + // NewOutput, + // NewInput + // > { + // return new ZodObject({ + // ...this._def, + // shape: () => ({ + // ...this._def.shape(), + // ...augmentation, + // }), + // }) as any; + // } + /** + * @deprecated Use `.extend` instead + * */ + this.augment = this.extend; + } + _getCached() { + if (this._cached !== null) + return this._cached; + const shape = this._def.shape(); + const keys = util.objectKeys(shape); + this._cached = { shape, keys }; + return this._cached; + } + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.object) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.object, + received: ctx.parsedType, + }); + return INVALID; + } + const { status, ctx } = this._processInputParams(input); + const { shape, keys: shapeKeys } = this._getCached(); + const extraKeys = []; + if (!(this._def.catchall instanceof ZodNever && this._def.unknownKeys === "strip")) { + for (const key in ctx.data) { + if (!shapeKeys.includes(key)) { + extraKeys.push(key); + } + } + } + const pairs = []; + for (const key of shapeKeys) { + const keyValidator = shape[key]; + const value = ctx.data[key]; + pairs.push({ + key: { status: "valid", value: key }, + value: keyValidator._parse(new ParseInputLazyPath(ctx, value, ctx.path, key)), + alwaysSet: key in ctx.data, + }); + } + if (this._def.catchall instanceof ZodNever) { + const unknownKeys = this._def.unknownKeys; + if (unknownKeys === "passthrough") { + for (const key of extraKeys) { + pairs.push({ + key: { status: "valid", value: key }, + value: { status: "valid", value: ctx.data[key] }, + }); + } + } + else if (unknownKeys === "strict") { + if (extraKeys.length > 0) { + addIssueToContext(ctx, { + code: ZodIssueCode.unrecognized_keys, + keys: extraKeys, + }); + status.dirty(); + } + } + else if (unknownKeys === "strip") { + } + else { + throw new Error(`Internal ZodObject error: invalid unknownKeys value.`); + } + } + else { + // run catchall validation + const catchall = this._def.catchall; + for (const key of extraKeys) { + const value = ctx.data[key]; + pairs.push({ + key: { status: "valid", value: key }, + value: catchall._parse(new ParseInputLazyPath(ctx, value, ctx.path, key) //, ctx.child(key), value, getParsedType(value) + ), + alwaysSet: key in ctx.data, + }); + } + } + if (ctx.common.async) { + return Promise.resolve() + .then(async () => { + const syncPairs = []; + for (const pair of pairs) { + const key = await pair.key; + const value = await pair.value; + syncPairs.push({ + key, + value, + alwaysSet: pair.alwaysSet, + }); + } + return syncPairs; + }) + .then((syncPairs) => { + return ParseStatus.mergeObjectSync(status, syncPairs); + }); + } + else { + return ParseStatus.mergeObjectSync(status, pairs); + } + } + get shape() { + return this._def.shape(); + } + strict(message) { + errorUtil.errToObj; + return new ZodObject({ + ...this._def, + unknownKeys: "strict", + ...(message !== undefined + ? { + errorMap: (issue, ctx) => { + const defaultError = this._def.errorMap?.(issue, ctx).message ?? ctx.defaultError; + if (issue.code === "unrecognized_keys") + return { + message: errorUtil.errToObj(message).message ?? defaultError, + }; + return { + message: defaultError, + }; + }, + } + : {}), + }); + } + strip() { + return new ZodObject({ + ...this._def, + unknownKeys: "strip", + }); + } + passthrough() { + return new ZodObject({ + ...this._def, + unknownKeys: "passthrough", + }); + } + // const AugmentFactory = + // (def: Def) => + // ( + // augmentation: Augmentation + // ): ZodObject< + // extendShape, Augmentation>, + // Def["unknownKeys"], + // Def["catchall"] + // > => { + // return new ZodObject({ + // ...def, + // shape: () => ({ + // ...def.shape(), + // ...augmentation, + // }), + // }) as any; + // }; + extend(augmentation) { + return new ZodObject({ + ...this._def, + shape: () => ({ + ...this._def.shape(), + ...augmentation, + }), + }); + } + /** + * Prior to zod@1.0.12 there was a bug in the + * inferred type of merged objects. Please + * upgrade if you are experiencing issues. + */ + merge(merging) { + const merged = new ZodObject({ + unknownKeys: merging._def.unknownKeys, + catchall: merging._def.catchall, + shape: () => ({ + ...this._def.shape(), + ...merging._def.shape(), + }), + typeName: ZodFirstPartyTypeKind.ZodObject, + }); + return merged; + } + // merge< + // Incoming extends AnyZodObject, + // Augmentation extends Incoming["shape"], + // NewOutput extends { + // [k in keyof Augmentation | keyof Output]: k extends keyof Augmentation + // ? Augmentation[k]["_output"] + // : k extends keyof Output + // ? Output[k] + // : never; + // }, + // NewInput extends { + // [k in keyof Augmentation | keyof Input]: k extends keyof Augmentation + // ? Augmentation[k]["_input"] + // : k extends keyof Input + // ? Input[k] + // : never; + // } + // >( + // merging: Incoming + // ): ZodObject< + // extendShape>, + // Incoming["_def"]["unknownKeys"], + // Incoming["_def"]["catchall"], + // NewOutput, + // NewInput + // > { + // const merged: any = new ZodObject({ + // unknownKeys: merging._def.unknownKeys, + // catchall: merging._def.catchall, + // shape: () => + // objectUtil.mergeShapes(this._def.shape(), merging._def.shape()), + // typeName: ZodFirstPartyTypeKind.ZodObject, + // }) as any; + // return merged; + // } + setKey(key, schema) { + return this.augment({ [key]: schema }); + } + // merge( + // merging: Incoming + // ): //ZodObject = (merging) => { + // ZodObject< + // extendShape>, + // Incoming["_def"]["unknownKeys"], + // Incoming["_def"]["catchall"] + // > { + // // const mergedShape = objectUtil.mergeShapes( + // // this._def.shape(), + // // merging._def.shape() + // // ); + // const merged: any = new ZodObject({ + // unknownKeys: merging._def.unknownKeys, + // catchall: merging._def.catchall, + // shape: () => + // objectUtil.mergeShapes(this._def.shape(), merging._def.shape()), + // typeName: ZodFirstPartyTypeKind.ZodObject, + // }) as any; + // return merged; + // } + catchall(index) { + return new ZodObject({ + ...this._def, + catchall: index, + }); + } + pick(mask) { + const shape = {}; + for (const key of util.objectKeys(mask)) { + if (mask[key] && this.shape[key]) { + shape[key] = this.shape[key]; + } + } + return new ZodObject({ + ...this._def, + shape: () => shape, + }); + } + omit(mask) { + const shape = {}; + for (const key of util.objectKeys(this.shape)) { + if (!mask[key]) { + shape[key] = this.shape[key]; + } + } + return new ZodObject({ + ...this._def, + shape: () => shape, + }); + } + /** + * @deprecated + */ + deepPartial() { + return deepPartialify(this); + } + partial(mask) { + const newShape = {}; + for (const key of util.objectKeys(this.shape)) { + const fieldSchema = this.shape[key]; + if (mask && !mask[key]) { + newShape[key] = fieldSchema; + } + else { + newShape[key] = fieldSchema.optional(); + } + } + return new ZodObject({ + ...this._def, + shape: () => newShape, + }); + } + required(mask) { + const newShape = {}; + for (const key of util.objectKeys(this.shape)) { + if (mask && !mask[key]) { + newShape[key] = this.shape[key]; + } + else { + const fieldSchema = this.shape[key]; + let newField = fieldSchema; + while (newField instanceof ZodOptional) { + newField = newField._def.innerType; + } + newShape[key] = newField; + } + } + return new ZodObject({ + ...this._def, + shape: () => newShape, + }); + } + keyof() { + return createZodEnum(util.objectKeys(this.shape)); + } +} +ZodObject.create = (shape, params) => { + return new ZodObject({ + shape: () => shape, + unknownKeys: "strip", + catchall: ZodNever.create(), + typeName: ZodFirstPartyTypeKind.ZodObject, + ...processCreateParams(params), + }); +}; +ZodObject.strictCreate = (shape, params) => { + return new ZodObject({ + shape: () => shape, + unknownKeys: "strict", + catchall: ZodNever.create(), + typeName: ZodFirstPartyTypeKind.ZodObject, + ...processCreateParams(params), + }); +}; +ZodObject.lazycreate = (shape, params) => { + return new ZodObject({ + shape, + unknownKeys: "strip", + catchall: ZodNever.create(), + typeName: ZodFirstPartyTypeKind.ZodObject, + ...processCreateParams(params), + }); +}; +export class ZodUnion extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + const options = this._def.options; + function handleResults(results) { + // return first issue-free validation if it exists + for (const result of results) { + if (result.result.status === "valid") { + return result.result; + } + } + for (const result of results) { + if (result.result.status === "dirty") { + // add issues from dirty option + ctx.common.issues.push(...result.ctx.common.issues); + return result.result; + } + } + // return invalid + const unionErrors = results.map((result) => new ZodError(result.ctx.common.issues)); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_union, + unionErrors, + }); + return INVALID; + } + if (ctx.common.async) { + return Promise.all(options.map(async (option) => { + const childCtx = { + ...ctx, + common: { + ...ctx.common, + issues: [], + }, + parent: null, + }; + return { + result: await option._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: childCtx, + }), + ctx: childCtx, + }; + })).then(handleResults); + } + else { + let dirty = undefined; + const issues = []; + for (const option of options) { + const childCtx = { + ...ctx, + common: { + ...ctx.common, + issues: [], + }, + parent: null, + }; + const result = option._parseSync({ + data: ctx.data, + path: ctx.path, + parent: childCtx, + }); + if (result.status === "valid") { + return result; + } + else if (result.status === "dirty" && !dirty) { + dirty = { result, ctx: childCtx }; + } + if (childCtx.common.issues.length) { + issues.push(childCtx.common.issues); + } + } + if (dirty) { + ctx.common.issues.push(...dirty.ctx.common.issues); + return dirty.result; + } + const unionErrors = issues.map((issues) => new ZodError(issues)); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_union, + unionErrors, + }); + return INVALID; + } + } + get options() { + return this._def.options; + } +} +ZodUnion.create = (types, params) => { + return new ZodUnion({ + options: types, + typeName: ZodFirstPartyTypeKind.ZodUnion, + ...processCreateParams(params), + }); +}; +///////////////////////////////////////////////////// +///////////////////////////////////////////////////// +////////// ////////// +////////// ZodDiscriminatedUnion ////////// +////////// ////////// +///////////////////////////////////////////////////// +///////////////////////////////////////////////////// +const getDiscriminator = (type) => { + if (type instanceof ZodLazy) { + return getDiscriminator(type.schema); + } + else if (type instanceof ZodEffects) { + return getDiscriminator(type.innerType()); + } + else if (type instanceof ZodLiteral) { + return [type.value]; + } + else if (type instanceof ZodEnum) { + return type.options; + } + else if (type instanceof ZodNativeEnum) { + // eslint-disable-next-line ban/ban + return util.objectValues(type.enum); + } + else if (type instanceof ZodDefault) { + return getDiscriminator(type._def.innerType); + } + else if (type instanceof ZodUndefined) { + return [undefined]; + } + else if (type instanceof ZodNull) { + return [null]; + } + else if (type instanceof ZodOptional) { + return [undefined, ...getDiscriminator(type.unwrap())]; + } + else if (type instanceof ZodNullable) { + return [null, ...getDiscriminator(type.unwrap())]; + } + else if (type instanceof ZodBranded) { + return getDiscriminator(type.unwrap()); + } + else if (type instanceof ZodReadonly) { + return getDiscriminator(type.unwrap()); + } + else if (type instanceof ZodCatch) { + return getDiscriminator(type._def.innerType); + } + else { + return []; + } +}; +export class ZodDiscriminatedUnion extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.object) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.object, + received: ctx.parsedType, + }); + return INVALID; + } + const discriminator = this.discriminator; + const discriminatorValue = ctx.data[discriminator]; + const option = this.optionsMap.get(discriminatorValue); + if (!option) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_union_discriminator, + options: Array.from(this.optionsMap.keys()), + path: [discriminator], + }); + return INVALID; + } + if (ctx.common.async) { + return option._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + } + else { + return option._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + } + } + get discriminator() { + return this._def.discriminator; + } + get options() { + return this._def.options; + } + get optionsMap() { + return this._def.optionsMap; + } + /** + * The constructor of the discriminated union schema. Its behaviour is very similar to that of the normal z.union() constructor. + * However, it only allows a union of objects, all of which need to share a discriminator property. This property must + * have a different value for each object in the union. + * @param discriminator the name of the discriminator property + * @param types an array of object schemas + * @param params + */ + static create(discriminator, options, params) { + // Get all the valid discriminator values + const optionsMap = new Map(); + // try { + for (const type of options) { + const discriminatorValues = getDiscriminator(type.shape[discriminator]); + if (!discriminatorValues.length) { + throw new Error(`A discriminator value for key \`${discriminator}\` could not be extracted from all schema options`); + } + for (const value of discriminatorValues) { + if (optionsMap.has(value)) { + throw new Error(`Discriminator property ${String(discriminator)} has duplicate value ${String(value)}`); + } + optionsMap.set(value, type); + } + } + return new ZodDiscriminatedUnion({ + typeName: ZodFirstPartyTypeKind.ZodDiscriminatedUnion, + discriminator, + options, + optionsMap, + ...processCreateParams(params), + }); + } +} +function mergeValues(a, b) { + const aType = getParsedType(a); + const bType = getParsedType(b); + if (a === b) { + return { valid: true, data: a }; + } + else if (aType === ZodParsedType.object && bType === ZodParsedType.object) { + const bKeys = util.objectKeys(b); + const sharedKeys = util.objectKeys(a).filter((key) => bKeys.indexOf(key) !== -1); + const newObj = { ...a, ...b }; + for (const key of sharedKeys) { + const sharedValue = mergeValues(a[key], b[key]); + if (!sharedValue.valid) { + return { valid: false }; + } + newObj[key] = sharedValue.data; + } + return { valid: true, data: newObj }; + } + else if (aType === ZodParsedType.array && bType === ZodParsedType.array) { + if (a.length !== b.length) { + return { valid: false }; + } + const newArray = []; + for (let index = 0; index < a.length; index++) { + const itemA = a[index]; + const itemB = b[index]; + const sharedValue = mergeValues(itemA, itemB); + if (!sharedValue.valid) { + return { valid: false }; + } + newArray.push(sharedValue.data); + } + return { valid: true, data: newArray }; + } + else if (aType === ZodParsedType.date && bType === ZodParsedType.date && +a === +b) { + return { valid: true, data: a }; + } + else { + return { valid: false }; + } +} +export class ZodIntersection extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + const handleParsed = (parsedLeft, parsedRight) => { + if (isAborted(parsedLeft) || isAborted(parsedRight)) { + return INVALID; + } + const merged = mergeValues(parsedLeft.value, parsedRight.value); + if (!merged.valid) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_intersection_types, + }); + return INVALID; + } + if (isDirty(parsedLeft) || isDirty(parsedRight)) { + status.dirty(); + } + return { status: status.value, value: merged.data }; + }; + if (ctx.common.async) { + return Promise.all([ + this._def.left._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }), + this._def.right._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }), + ]).then(([left, right]) => handleParsed(left, right)); + } + else { + return handleParsed(this._def.left._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }), this._def.right._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + })); + } + } +} +ZodIntersection.create = (left, right, params) => { + return new ZodIntersection({ + left: left, + right: right, + typeName: ZodFirstPartyTypeKind.ZodIntersection, + ...processCreateParams(params), + }); +}; +// type ZodTupleItems = [ZodTypeAny, ...ZodTypeAny[]]; +export class ZodTuple extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.array) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.array, + received: ctx.parsedType, + }); + return INVALID; + } + if (ctx.data.length < this._def.items.length) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: this._def.items.length, + inclusive: true, + exact: false, + type: "array", + }); + return INVALID; + } + const rest = this._def.rest; + if (!rest && ctx.data.length > this._def.items.length) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: this._def.items.length, + inclusive: true, + exact: false, + type: "array", + }); + status.dirty(); + } + const items = [...ctx.data] + .map((item, itemIndex) => { + const schema = this._def.items[itemIndex] || this._def.rest; + if (!schema) + return null; + return schema._parse(new ParseInputLazyPath(ctx, item, ctx.path, itemIndex)); + }) + .filter((x) => !!x); // filter nulls + if (ctx.common.async) { + return Promise.all(items).then((results) => { + return ParseStatus.mergeArray(status, results); + }); + } + else { + return ParseStatus.mergeArray(status, items); + } + } + get items() { + return this._def.items; + } + rest(rest) { + return new ZodTuple({ + ...this._def, + rest, + }); + } +} +ZodTuple.create = (schemas, params) => { + if (!Array.isArray(schemas)) { + throw new Error("You must pass an array of schemas to z.tuple([ ... ])"); + } + return new ZodTuple({ + items: schemas, + typeName: ZodFirstPartyTypeKind.ZodTuple, + rest: null, + ...processCreateParams(params), + }); +}; +export class ZodRecord extends ZodType { + get keySchema() { + return this._def.keyType; + } + get valueSchema() { + return this._def.valueType; + } + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.object) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.object, + received: ctx.parsedType, + }); + return INVALID; + } + const pairs = []; + const keyType = this._def.keyType; + const valueType = this._def.valueType; + for (const key in ctx.data) { + pairs.push({ + key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, key)), + value: valueType._parse(new ParseInputLazyPath(ctx, ctx.data[key], ctx.path, key)), + alwaysSet: key in ctx.data, + }); + } + if (ctx.common.async) { + return ParseStatus.mergeObjectAsync(status, pairs); + } + else { + return ParseStatus.mergeObjectSync(status, pairs); + } + } + get element() { + return this._def.valueType; + } + static create(first, second, third) { + if (second instanceof ZodType) { + return new ZodRecord({ + keyType: first, + valueType: second, + typeName: ZodFirstPartyTypeKind.ZodRecord, + ...processCreateParams(third), + }); + } + return new ZodRecord({ + keyType: ZodString.create(), + valueType: first, + typeName: ZodFirstPartyTypeKind.ZodRecord, + ...processCreateParams(second), + }); + } +} +export class ZodMap extends ZodType { + get keySchema() { + return this._def.keyType; + } + get valueSchema() { + return this._def.valueType; + } + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.map) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.map, + received: ctx.parsedType, + }); + return INVALID; + } + const keyType = this._def.keyType; + const valueType = this._def.valueType; + const pairs = [...ctx.data.entries()].map(([key, value], index) => { + return { + key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, [index, "key"])), + value: valueType._parse(new ParseInputLazyPath(ctx, value, ctx.path, [index, "value"])), + }; + }); + if (ctx.common.async) { + const finalMap = new Map(); + return Promise.resolve().then(async () => { + for (const pair of pairs) { + const key = await pair.key; + const value = await pair.value; + if (key.status === "aborted" || value.status === "aborted") { + return INVALID; + } + if (key.status === "dirty" || value.status === "dirty") { + status.dirty(); + } + finalMap.set(key.value, value.value); + } + return { status: status.value, value: finalMap }; + }); + } + else { + const finalMap = new Map(); + for (const pair of pairs) { + const key = pair.key; + const value = pair.value; + if (key.status === "aborted" || value.status === "aborted") { + return INVALID; + } + if (key.status === "dirty" || value.status === "dirty") { + status.dirty(); + } + finalMap.set(key.value, value.value); + } + return { status: status.value, value: finalMap }; + } + } +} +ZodMap.create = (keyType, valueType, params) => { + return new ZodMap({ + valueType, + keyType, + typeName: ZodFirstPartyTypeKind.ZodMap, + ...processCreateParams(params), + }); +}; +export class ZodSet extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.set) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.set, + received: ctx.parsedType, + }); + return INVALID; + } + const def = this._def; + if (def.minSize !== null) { + if (ctx.data.size < def.minSize.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: def.minSize.value, + type: "set", + inclusive: true, + exact: false, + message: def.minSize.message, + }); + status.dirty(); + } + } + if (def.maxSize !== null) { + if (ctx.data.size > def.maxSize.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: def.maxSize.value, + type: "set", + inclusive: true, + exact: false, + message: def.maxSize.message, + }); + status.dirty(); + } + } + const valueType = this._def.valueType; + function finalizeSet(elements) { + const parsedSet = new Set(); + for (const element of elements) { + if (element.status === "aborted") + return INVALID; + if (element.status === "dirty") + status.dirty(); + parsedSet.add(element.value); + } + return { status: status.value, value: parsedSet }; + } + const elements = [...ctx.data.values()].map((item, i) => valueType._parse(new ParseInputLazyPath(ctx, item, ctx.path, i))); + if (ctx.common.async) { + return Promise.all(elements).then((elements) => finalizeSet(elements)); + } + else { + return finalizeSet(elements); + } + } + min(minSize, message) { + return new ZodSet({ + ...this._def, + minSize: { value: minSize, message: errorUtil.toString(message) }, + }); + } + max(maxSize, message) { + return new ZodSet({ + ...this._def, + maxSize: { value: maxSize, message: errorUtil.toString(message) }, + }); + } + size(size, message) { + return this.min(size, message).max(size, message); + } + nonempty(message) { + return this.min(1, message); + } +} +ZodSet.create = (valueType, params) => { + return new ZodSet({ + valueType, + minSize: null, + maxSize: null, + typeName: ZodFirstPartyTypeKind.ZodSet, + ...processCreateParams(params), + }); +}; +export class ZodFunction extends ZodType { + constructor() { + super(...arguments); + this.validate = this.implement; + } + _parse(input) { + const { ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.function) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.function, + received: ctx.parsedType, + }); + return INVALID; + } + function makeArgsIssue(args, error) { + return makeIssue({ + data: args, + path: ctx.path, + errorMaps: [ctx.common.contextualErrorMap, ctx.schemaErrorMap, getErrorMap(), defaultErrorMap].filter((x) => !!x), + issueData: { + code: ZodIssueCode.invalid_arguments, + argumentsError: error, + }, + }); + } + function makeReturnsIssue(returns, error) { + return makeIssue({ + data: returns, + path: ctx.path, + errorMaps: [ctx.common.contextualErrorMap, ctx.schemaErrorMap, getErrorMap(), defaultErrorMap].filter((x) => !!x), + issueData: { + code: ZodIssueCode.invalid_return_type, + returnTypeError: error, + }, + }); + } + const params = { errorMap: ctx.common.contextualErrorMap }; + const fn = ctx.data; + if (this._def.returns instanceof ZodPromise) { + // Would love a way to avoid disabling this rule, but we need + // an alias (using an arrow function was what caused 2651). + // eslint-disable-next-line @typescript-eslint/no-this-alias + const me = this; + return OK(async function (...args) { + const error = new ZodError([]); + const parsedArgs = await me._def.args.parseAsync(args, params).catch((e) => { + error.addIssue(makeArgsIssue(args, e)); + throw error; + }); + const result = await Reflect.apply(fn, this, parsedArgs); + const parsedReturns = await me._def.returns._def.type + .parseAsync(result, params) + .catch((e) => { + error.addIssue(makeReturnsIssue(result, e)); + throw error; + }); + return parsedReturns; + }); + } + else { + // Would love a way to avoid disabling this rule, but we need + // an alias (using an arrow function was what caused 2651). + // eslint-disable-next-line @typescript-eslint/no-this-alias + const me = this; + return OK(function (...args) { + const parsedArgs = me._def.args.safeParse(args, params); + if (!parsedArgs.success) { + throw new ZodError([makeArgsIssue(args, parsedArgs.error)]); + } + const result = Reflect.apply(fn, this, parsedArgs.data); + const parsedReturns = me._def.returns.safeParse(result, params); + if (!parsedReturns.success) { + throw new ZodError([makeReturnsIssue(result, parsedReturns.error)]); + } + return parsedReturns.data; + }); + } + } + parameters() { + return this._def.args; + } + returnType() { + return this._def.returns; + } + args(...items) { + return new ZodFunction({ + ...this._def, + args: ZodTuple.create(items).rest(ZodUnknown.create()), + }); + } + returns(returnType) { + return new ZodFunction({ + ...this._def, + returns: returnType, + }); + } + implement(func) { + const validatedFunc = this.parse(func); + return validatedFunc; + } + strictImplement(func) { + const validatedFunc = this.parse(func); + return validatedFunc; + } + static create(args, returns, params) { + return new ZodFunction({ + args: (args ? args : ZodTuple.create([]).rest(ZodUnknown.create())), + returns: returns || ZodUnknown.create(), + typeName: ZodFirstPartyTypeKind.ZodFunction, + ...processCreateParams(params), + }); + } +} +export class ZodLazy extends ZodType { + get schema() { + return this._def.getter(); + } + _parse(input) { + const { ctx } = this._processInputParams(input); + const lazySchema = this._def.getter(); + return lazySchema._parse({ data: ctx.data, path: ctx.path, parent: ctx }); + } +} +ZodLazy.create = (getter, params) => { + return new ZodLazy({ + getter: getter, + typeName: ZodFirstPartyTypeKind.ZodLazy, + ...processCreateParams(params), + }); +}; +export class ZodLiteral extends ZodType { + _parse(input) { + if (input.data !== this._def.value) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + received: ctx.data, + code: ZodIssueCode.invalid_literal, + expected: this._def.value, + }); + return INVALID; + } + return { status: "valid", value: input.data }; + } + get value() { + return this._def.value; + } +} +ZodLiteral.create = (value, params) => { + return new ZodLiteral({ + value: value, + typeName: ZodFirstPartyTypeKind.ZodLiteral, + ...processCreateParams(params), + }); +}; +function createZodEnum(values, params) { + return new ZodEnum({ + values, + typeName: ZodFirstPartyTypeKind.ZodEnum, + ...processCreateParams(params), + }); +} +export class ZodEnum extends ZodType { + _parse(input) { + if (typeof input.data !== "string") { + const ctx = this._getOrReturnCtx(input); + const expectedValues = this._def.values; + addIssueToContext(ctx, { + expected: util.joinValues(expectedValues), + received: ctx.parsedType, + code: ZodIssueCode.invalid_type, + }); + return INVALID; + } + if (!this._cache) { + this._cache = new Set(this._def.values); + } + if (!this._cache.has(input.data)) { + const ctx = this._getOrReturnCtx(input); + const expectedValues = this._def.values; + addIssueToContext(ctx, { + received: ctx.data, + code: ZodIssueCode.invalid_enum_value, + options: expectedValues, + }); + return INVALID; + } + return OK(input.data); + } + get options() { + return this._def.values; + } + get enum() { + const enumValues = {}; + for (const val of this._def.values) { + enumValues[val] = val; + } + return enumValues; + } + get Values() { + const enumValues = {}; + for (const val of this._def.values) { + enumValues[val] = val; + } + return enumValues; + } + get Enum() { + const enumValues = {}; + for (const val of this._def.values) { + enumValues[val] = val; + } + return enumValues; + } + extract(values, newDef = this._def) { + return ZodEnum.create(values, { + ...this._def, + ...newDef, + }); + } + exclude(values, newDef = this._def) { + return ZodEnum.create(this.options.filter((opt) => !values.includes(opt)), { + ...this._def, + ...newDef, + }); + } +} +ZodEnum.create = createZodEnum; +export class ZodNativeEnum extends ZodType { + _parse(input) { + const nativeEnumValues = util.getValidEnumValues(this._def.values); + const ctx = this._getOrReturnCtx(input); + if (ctx.parsedType !== ZodParsedType.string && ctx.parsedType !== ZodParsedType.number) { + const expectedValues = util.objectValues(nativeEnumValues); + addIssueToContext(ctx, { + expected: util.joinValues(expectedValues), + received: ctx.parsedType, + code: ZodIssueCode.invalid_type, + }); + return INVALID; + } + if (!this._cache) { + this._cache = new Set(util.getValidEnumValues(this._def.values)); + } + if (!this._cache.has(input.data)) { + const expectedValues = util.objectValues(nativeEnumValues); + addIssueToContext(ctx, { + received: ctx.data, + code: ZodIssueCode.invalid_enum_value, + options: expectedValues, + }); + return INVALID; + } + return OK(input.data); + } + get enum() { + return this._def.values; + } +} +ZodNativeEnum.create = (values, params) => { + return new ZodNativeEnum({ + values: values, + typeName: ZodFirstPartyTypeKind.ZodNativeEnum, + ...processCreateParams(params), + }); +}; +export class ZodPromise extends ZodType { + unwrap() { + return this._def.type; + } + _parse(input) { + const { ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.promise && ctx.common.async === false) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.promise, + received: ctx.parsedType, + }); + return INVALID; + } + const promisified = ctx.parsedType === ZodParsedType.promise ? ctx.data : Promise.resolve(ctx.data); + return OK(promisified.then((data) => { + return this._def.type.parseAsync(data, { + path: ctx.path, + errorMap: ctx.common.contextualErrorMap, + }); + })); + } +} +ZodPromise.create = (schema, params) => { + return new ZodPromise({ + type: schema, + typeName: ZodFirstPartyTypeKind.ZodPromise, + ...processCreateParams(params), + }); +}; +export class ZodEffects extends ZodType { + innerType() { + return this._def.schema; + } + sourceType() { + return this._def.schema._def.typeName === ZodFirstPartyTypeKind.ZodEffects + ? this._def.schema.sourceType() + : this._def.schema; + } + _parse(input) { + const { status, ctx } = this._processInputParams(input); + const effect = this._def.effect || null; + const checkCtx = { + addIssue: (arg) => { + addIssueToContext(ctx, arg); + if (arg.fatal) { + status.abort(); + } + else { + status.dirty(); + } + }, + get path() { + return ctx.path; + }, + }; + checkCtx.addIssue = checkCtx.addIssue.bind(checkCtx); + if (effect.type === "preprocess") { + const processed = effect.transform(ctx.data, checkCtx); + if (ctx.common.async) { + return Promise.resolve(processed).then(async (processed) => { + if (status.value === "aborted") + return INVALID; + const result = await this._def.schema._parseAsync({ + data: processed, + path: ctx.path, + parent: ctx, + }); + if (result.status === "aborted") + return INVALID; + if (result.status === "dirty") + return DIRTY(result.value); + if (status.value === "dirty") + return DIRTY(result.value); + return result; + }); + } + else { + if (status.value === "aborted") + return INVALID; + const result = this._def.schema._parseSync({ + data: processed, + path: ctx.path, + parent: ctx, + }); + if (result.status === "aborted") + return INVALID; + if (result.status === "dirty") + return DIRTY(result.value); + if (status.value === "dirty") + return DIRTY(result.value); + return result; + } + } + if (effect.type === "refinement") { + const executeRefinement = (acc) => { + const result = effect.refinement(acc, checkCtx); + if (ctx.common.async) { + return Promise.resolve(result); + } + if (result instanceof Promise) { + throw new Error("Async refinement encountered during synchronous parse operation. Use .parseAsync instead."); + } + return acc; + }; + if (ctx.common.async === false) { + const inner = this._def.schema._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + if (inner.status === "aborted") + return INVALID; + if (inner.status === "dirty") + status.dirty(); + // return value is ignored + executeRefinement(inner.value); + return { status: status.value, value: inner.value }; + } + else { + return this._def.schema._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }).then((inner) => { + if (inner.status === "aborted") + return INVALID; + if (inner.status === "dirty") + status.dirty(); + return executeRefinement(inner.value).then(() => { + return { status: status.value, value: inner.value }; + }); + }); + } + } + if (effect.type === "transform") { + if (ctx.common.async === false) { + const base = this._def.schema._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + if (!isValid(base)) + return INVALID; + const result = effect.transform(base.value, checkCtx); + if (result instanceof Promise) { + throw new Error(`Asynchronous transform encountered during synchronous parse operation. Use .parseAsync instead.`); + } + return { status: status.value, value: result }; + } + else { + return this._def.schema._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }).then((base) => { + if (!isValid(base)) + return INVALID; + return Promise.resolve(effect.transform(base.value, checkCtx)).then((result) => ({ + status: status.value, + value: result, + })); + }); + } + } + util.assertNever(effect); + } +} +ZodEffects.create = (schema, effect, params) => { + return new ZodEffects({ + schema, + typeName: ZodFirstPartyTypeKind.ZodEffects, + effect, + ...processCreateParams(params), + }); +}; +ZodEffects.createWithPreprocess = (preprocess, schema, params) => { + return new ZodEffects({ + schema, + effect: { type: "preprocess", transform: preprocess }, + typeName: ZodFirstPartyTypeKind.ZodEffects, + ...processCreateParams(params), + }); +}; +export { ZodEffects as ZodTransformer }; +export class ZodOptional extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType === ZodParsedType.undefined) { + return OK(undefined); + } + return this._def.innerType._parse(input); + } + unwrap() { + return this._def.innerType; + } +} +ZodOptional.create = (type, params) => { + return new ZodOptional({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodOptional, + ...processCreateParams(params), + }); +}; +export class ZodNullable extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType === ZodParsedType.null) { + return OK(null); + } + return this._def.innerType._parse(input); + } + unwrap() { + return this._def.innerType; + } +} +ZodNullable.create = (type, params) => { + return new ZodNullable({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodNullable, + ...processCreateParams(params), + }); +}; +export class ZodDefault extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + let data = ctx.data; + if (ctx.parsedType === ZodParsedType.undefined) { + data = this._def.defaultValue(); + } + return this._def.innerType._parse({ + data, + path: ctx.path, + parent: ctx, + }); + } + removeDefault() { + return this._def.innerType; + } +} +ZodDefault.create = (type, params) => { + return new ZodDefault({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodDefault, + defaultValue: typeof params.default === "function" ? params.default : () => params.default, + ...processCreateParams(params), + }); +}; +export class ZodCatch extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + // newCtx is used to not collect issues from inner types in ctx + const newCtx = { + ...ctx, + common: { + ...ctx.common, + issues: [], + }, + }; + const result = this._def.innerType._parse({ + data: newCtx.data, + path: newCtx.path, + parent: { + ...newCtx, + }, + }); + if (isAsync(result)) { + return result.then((result) => { + return { + status: "valid", + value: result.status === "valid" + ? result.value + : this._def.catchValue({ + get error() { + return new ZodError(newCtx.common.issues); + }, + input: newCtx.data, + }), + }; + }); + } + else { + return { + status: "valid", + value: result.status === "valid" + ? result.value + : this._def.catchValue({ + get error() { + return new ZodError(newCtx.common.issues); + }, + input: newCtx.data, + }), + }; + } + } + removeCatch() { + return this._def.innerType; + } +} +ZodCatch.create = (type, params) => { + return new ZodCatch({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodCatch, + catchValue: typeof params.catch === "function" ? params.catch : () => params.catch, + ...processCreateParams(params), + }); +}; +export class ZodNaN extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.nan) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.nan, + received: ctx.parsedType, + }); + return INVALID; + } + return { status: "valid", value: input.data }; + } +} +ZodNaN.create = (params) => { + return new ZodNaN({ + typeName: ZodFirstPartyTypeKind.ZodNaN, + ...processCreateParams(params), + }); +}; +export const BRAND = Symbol("zod_brand"); +export class ZodBranded extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + const data = ctx.data; + return this._def.type._parse({ + data, + path: ctx.path, + parent: ctx, + }); + } + unwrap() { + return this._def.type; + } +} +export class ZodPipeline extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.common.async) { + const handleAsync = async () => { + const inResult = await this._def.in._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + if (inResult.status === "aborted") + return INVALID; + if (inResult.status === "dirty") { + status.dirty(); + return DIRTY(inResult.value); + } + else { + return this._def.out._parseAsync({ + data: inResult.value, + path: ctx.path, + parent: ctx, + }); + } + }; + return handleAsync(); + } + else { + const inResult = this._def.in._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + if (inResult.status === "aborted") + return INVALID; + if (inResult.status === "dirty") { + status.dirty(); + return { + status: "dirty", + value: inResult.value, + }; + } + else { + return this._def.out._parseSync({ + data: inResult.value, + path: ctx.path, + parent: ctx, + }); + } + } + } + static create(a, b) { + return new ZodPipeline({ + in: a, + out: b, + typeName: ZodFirstPartyTypeKind.ZodPipeline, + }); + } +} +export class ZodReadonly extends ZodType { + _parse(input) { + const result = this._def.innerType._parse(input); + const freeze = (data) => { + if (isValid(data)) { + data.value = Object.freeze(data.value); + } + return data; + }; + return isAsync(result) ? result.then((data) => freeze(data)) : freeze(result); + } + unwrap() { + return this._def.innerType; + } +} +ZodReadonly.create = (type, params) => { + return new ZodReadonly({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodReadonly, + ...processCreateParams(params), + }); +}; +//////////////////////////////////////// +//////////////////////////////////////// +////////// ////////// +////////// z.custom ////////// +////////// ////////// +//////////////////////////////////////// +//////////////////////////////////////// +function cleanParams(params, data) { + const p = typeof params === "function" ? params(data) : typeof params === "string" ? { message: params } : params; + const p2 = typeof p === "string" ? { message: p } : p; + return p2; +} +export function custom(check, _params = {}, +/** + * @deprecated + * + * Pass `fatal` into the params object instead: + * + * ```ts + * z.string().custom((val) => val.length > 5, { fatal: false }) + * ``` + * + */ +fatal) { + if (check) + return ZodAny.create().superRefine((data, ctx) => { + const r = check(data); + if (r instanceof Promise) { + return r.then((r) => { + if (!r) { + const params = cleanParams(_params, data); + const _fatal = params.fatal ?? fatal ?? true; + ctx.addIssue({ code: "custom", ...params, fatal: _fatal }); + } + }); + } + if (!r) { + const params = cleanParams(_params, data); + const _fatal = params.fatal ?? fatal ?? true; + ctx.addIssue({ code: "custom", ...params, fatal: _fatal }); + } + return; + }); + return ZodAny.create(); +} +export { ZodType as Schema, ZodType as ZodSchema }; +export const late = { + object: ZodObject.lazycreate, +}; +export var ZodFirstPartyTypeKind; +(function (ZodFirstPartyTypeKind) { + ZodFirstPartyTypeKind["ZodString"] = "ZodString"; + ZodFirstPartyTypeKind["ZodNumber"] = "ZodNumber"; + ZodFirstPartyTypeKind["ZodNaN"] = "ZodNaN"; + ZodFirstPartyTypeKind["ZodBigInt"] = "ZodBigInt"; + ZodFirstPartyTypeKind["ZodBoolean"] = "ZodBoolean"; + ZodFirstPartyTypeKind["ZodDate"] = "ZodDate"; + ZodFirstPartyTypeKind["ZodSymbol"] = "ZodSymbol"; + ZodFirstPartyTypeKind["ZodUndefined"] = "ZodUndefined"; + ZodFirstPartyTypeKind["ZodNull"] = "ZodNull"; + ZodFirstPartyTypeKind["ZodAny"] = "ZodAny"; + ZodFirstPartyTypeKind["ZodUnknown"] = "ZodUnknown"; + ZodFirstPartyTypeKind["ZodNever"] = "ZodNever"; + ZodFirstPartyTypeKind["ZodVoid"] = "ZodVoid"; + ZodFirstPartyTypeKind["ZodArray"] = "ZodArray"; + ZodFirstPartyTypeKind["ZodObject"] = "ZodObject"; + ZodFirstPartyTypeKind["ZodUnion"] = "ZodUnion"; + ZodFirstPartyTypeKind["ZodDiscriminatedUnion"] = "ZodDiscriminatedUnion"; + ZodFirstPartyTypeKind["ZodIntersection"] = "ZodIntersection"; + ZodFirstPartyTypeKind["ZodTuple"] = "ZodTuple"; + ZodFirstPartyTypeKind["ZodRecord"] = "ZodRecord"; + ZodFirstPartyTypeKind["ZodMap"] = "ZodMap"; + ZodFirstPartyTypeKind["ZodSet"] = "ZodSet"; + ZodFirstPartyTypeKind["ZodFunction"] = "ZodFunction"; + ZodFirstPartyTypeKind["ZodLazy"] = "ZodLazy"; + ZodFirstPartyTypeKind["ZodLiteral"] = "ZodLiteral"; + ZodFirstPartyTypeKind["ZodEnum"] = "ZodEnum"; + ZodFirstPartyTypeKind["ZodEffects"] = "ZodEffects"; + ZodFirstPartyTypeKind["ZodNativeEnum"] = "ZodNativeEnum"; + ZodFirstPartyTypeKind["ZodOptional"] = "ZodOptional"; + ZodFirstPartyTypeKind["ZodNullable"] = "ZodNullable"; + ZodFirstPartyTypeKind["ZodDefault"] = "ZodDefault"; + ZodFirstPartyTypeKind["ZodCatch"] = "ZodCatch"; + ZodFirstPartyTypeKind["ZodPromise"] = "ZodPromise"; + ZodFirstPartyTypeKind["ZodBranded"] = "ZodBranded"; + ZodFirstPartyTypeKind["ZodPipeline"] = "ZodPipeline"; + ZodFirstPartyTypeKind["ZodReadonly"] = "ZodReadonly"; +})(ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {})); +// requires TS 4.4+ +class Class { + constructor(..._) { } +} +const instanceOfType = ( +// const instanceOfType = any>( +cls, params = { + message: `Input not instance of ${cls.name}`, +}) => custom((data) => data instanceof cls, params); +const stringType = ZodString.create; +const numberType = ZodNumber.create; +const nanType = ZodNaN.create; +const bigIntType = ZodBigInt.create; +const booleanType = ZodBoolean.create; +const dateType = ZodDate.create; +const symbolType = ZodSymbol.create; +const undefinedType = ZodUndefined.create; +const nullType = ZodNull.create; +const anyType = ZodAny.create; +const unknownType = ZodUnknown.create; +const neverType = ZodNever.create; +const voidType = ZodVoid.create; +const arrayType = ZodArray.create; +const objectType = ZodObject.create; +const strictObjectType = ZodObject.strictCreate; +const unionType = ZodUnion.create; +const discriminatedUnionType = ZodDiscriminatedUnion.create; +const intersectionType = ZodIntersection.create; +const tupleType = ZodTuple.create; +const recordType = ZodRecord.create; +const mapType = ZodMap.create; +const setType = ZodSet.create; +const functionType = ZodFunction.create; +const lazyType = ZodLazy.create; +const literalType = ZodLiteral.create; +const enumType = ZodEnum.create; +const nativeEnumType = ZodNativeEnum.create; +const promiseType = ZodPromise.create; +const effectsType = ZodEffects.create; +const optionalType = ZodOptional.create; +const nullableType = ZodNullable.create; +const preprocessType = ZodEffects.createWithPreprocess; +const pipelineType = ZodPipeline.create; +const ostring = () => stringType().optional(); +const onumber = () => numberType().optional(); +const oboolean = () => booleanType().optional(); +export const coerce = { + string: ((arg) => ZodString.create({ ...arg, coerce: true })), + number: ((arg) => ZodNumber.create({ ...arg, coerce: true })), + boolean: ((arg) => ZodBoolean.create({ + ...arg, + coerce: true, + })), + bigint: ((arg) => ZodBigInt.create({ ...arg, coerce: true })), + date: ((arg) => ZodDate.create({ ...arg, coerce: true })), +}; +export { anyType as any, arrayType as array, bigIntType as bigint, booleanType as boolean, dateType as date, discriminatedUnionType as discriminatedUnion, effectsType as effect, enumType as enum, functionType as function, instanceOfType as instanceof, intersectionType as intersection, lazyType as lazy, literalType as literal, mapType as map, nanType as nan, nativeEnumType as nativeEnum, neverType as never, nullType as null, nullableType as nullable, numberType as number, objectType as object, oboolean, onumber, optionalType as optional, ostring, pipelineType as pipeline, preprocessType as preprocess, promiseType as promise, recordType as record, setType as set, strictObjectType as strictObject, stringType as string, symbolType as symbol, effectsType as transformer, tupleType as tuple, undefinedType as undefined, unionType as union, unknownType as unknown, voidType as void, }; +export const NEVER = INVALID; diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/index.d.ts b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/index.d.ts new file mode 100644 index 000000000..8352ab713 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/index.d.ts @@ -0,0 +1,3 @@ +import z3 from "./v3/index.js"; +export * from "./v3/index.js"; +export default z3; diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/ZodError.d.ts b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/ZodError.d.ts new file mode 100644 index 000000000..4e71355a3 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/ZodError.d.ts @@ -0,0 +1,164 @@ +import type { Primitive } from "./helpers/typeAliases.js"; +import { util, type ZodParsedType } from "./helpers/util.js"; +import type { TypeOf, ZodType } from "./index.js"; +type allKeys = T extends any ? keyof T : never; +export type inferFlattenedErrors, U = string> = typeToFlattenedError, U>; +export type typeToFlattenedError = { + formErrors: U[]; + fieldErrors: { + [P in allKeys]?: U[]; + }; +}; +export declare const ZodIssueCode: { + invalid_type: "invalid_type"; + invalid_literal: "invalid_literal"; + custom: "custom"; + invalid_union: "invalid_union"; + invalid_union_discriminator: "invalid_union_discriminator"; + invalid_enum_value: "invalid_enum_value"; + unrecognized_keys: "unrecognized_keys"; + invalid_arguments: "invalid_arguments"; + invalid_return_type: "invalid_return_type"; + invalid_date: "invalid_date"; + invalid_string: "invalid_string"; + too_small: "too_small"; + too_big: "too_big"; + invalid_intersection_types: "invalid_intersection_types"; + not_multiple_of: "not_multiple_of"; + not_finite: "not_finite"; +}; +export type ZodIssueCode = keyof typeof ZodIssueCode; +export type ZodIssueBase = { + path: (string | number)[]; + message?: string | undefined; +}; +export interface ZodInvalidTypeIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_type; + expected: ZodParsedType; + received: ZodParsedType; +} +export interface ZodInvalidLiteralIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_literal; + expected: unknown; + received: unknown; +} +export interface ZodUnrecognizedKeysIssue extends ZodIssueBase { + code: typeof ZodIssueCode.unrecognized_keys; + keys: string[]; +} +export interface ZodInvalidUnionIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_union; + unionErrors: ZodError[]; +} +export interface ZodInvalidUnionDiscriminatorIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_union_discriminator; + options: Primitive[]; +} +export interface ZodInvalidEnumValueIssue extends ZodIssueBase { + received: string | number; + code: typeof ZodIssueCode.invalid_enum_value; + options: (string | number)[]; +} +export interface ZodInvalidArgumentsIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_arguments; + argumentsError: ZodError; +} +export interface ZodInvalidReturnTypeIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_return_type; + returnTypeError: ZodError; +} +export interface ZodInvalidDateIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_date; +} +export type StringValidation = "email" | "url" | "emoji" | "uuid" | "nanoid" | "regex" | "cuid" | "cuid2" | "ulid" | "datetime" | "date" | "time" | "duration" | "ip" | "cidr" | "base64" | "jwt" | "base64url" | { + includes: string; + position?: number | undefined; +} | { + startsWith: string; +} | { + endsWith: string; +}; +export interface ZodInvalidStringIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_string; + validation: StringValidation; +} +export interface ZodTooSmallIssue extends ZodIssueBase { + code: typeof ZodIssueCode.too_small; + minimum: number | bigint; + inclusive: boolean; + exact?: boolean; + type: "array" | "string" | "number" | "set" | "date" | "bigint"; +} +export interface ZodTooBigIssue extends ZodIssueBase { + code: typeof ZodIssueCode.too_big; + maximum: number | bigint; + inclusive: boolean; + exact?: boolean; + type: "array" | "string" | "number" | "set" | "date" | "bigint"; +} +export interface ZodInvalidIntersectionTypesIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_intersection_types; +} +export interface ZodNotMultipleOfIssue extends ZodIssueBase { + code: typeof ZodIssueCode.not_multiple_of; + multipleOf: number | bigint; +} +export interface ZodNotFiniteIssue extends ZodIssueBase { + code: typeof ZodIssueCode.not_finite; +} +export interface ZodCustomIssue extends ZodIssueBase { + code: typeof ZodIssueCode.custom; + params?: { + [k: string]: any; + }; +} +export type DenormalizedError = { + [k: string]: DenormalizedError | string[]; +}; +export type ZodIssueOptionalMessage = ZodInvalidTypeIssue | ZodInvalidLiteralIssue | ZodUnrecognizedKeysIssue | ZodInvalidUnionIssue | ZodInvalidUnionDiscriminatorIssue | ZodInvalidEnumValueIssue | ZodInvalidArgumentsIssue | ZodInvalidReturnTypeIssue | ZodInvalidDateIssue | ZodInvalidStringIssue | ZodTooSmallIssue | ZodTooBigIssue | ZodInvalidIntersectionTypesIssue | ZodNotMultipleOfIssue | ZodNotFiniteIssue | ZodCustomIssue; +export type ZodIssue = ZodIssueOptionalMessage & { + fatal?: boolean | undefined; + message: string; +}; +export declare const quotelessJson: (obj: any) => string; +type recursiveZodFormattedError = T extends [any, ...any[]] ? { + [K in keyof T]?: ZodFormattedError; +} : T extends any[] ? { + [k: number]: ZodFormattedError; +} : T extends object ? { + [K in keyof T]?: ZodFormattedError; +} : unknown; +export type ZodFormattedError = { + _errors: U[]; +} & recursiveZodFormattedError>; +export type inferFormattedError, U = string> = ZodFormattedError, U>; +export declare class ZodError extends Error { + issues: ZodIssue[]; + get errors(): ZodIssue[]; + constructor(issues: ZodIssue[]); + format(): ZodFormattedError; + format(mapper: (issue: ZodIssue) => U): ZodFormattedError; + static create: (issues: ZodIssue[]) => ZodError; + static assert(value: unknown): asserts value is ZodError; + toString(): string; + get message(): string; + get isEmpty(): boolean; + addIssue: (sub: ZodIssue) => void; + addIssues: (subs?: ZodIssue[]) => void; + flatten(): typeToFlattenedError; + flatten(mapper?: (issue: ZodIssue) => U): typeToFlattenedError; + get formErrors(): typeToFlattenedError; +} +type stripPath = T extends any ? util.OmitKeys : never; +export type IssueData = stripPath & { + path?: (string | number)[]; + fatal?: boolean | undefined; +}; +export type ErrorMapCtx = { + defaultError: string; + data: any; +}; +export type ZodErrorMap = (issue: ZodIssueOptionalMessage, _ctx: ErrorMapCtx) => { + message: string; +}; +export {}; diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/errors.d.ts b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/errors.d.ts new file mode 100644 index 000000000..52eacf72e --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/errors.d.ts @@ -0,0 +1,5 @@ +import type { ZodErrorMap } from "./ZodError.js"; +import defaultErrorMap from "./locales/en.js"; +export { defaultErrorMap }; +export declare function setErrorMap(map: ZodErrorMap): void; +export declare function getErrorMap(): ZodErrorMap; diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/external.d.ts b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/external.d.ts new file mode 100644 index 000000000..f0a4be4c7 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/external.d.ts @@ -0,0 +1,6 @@ +export * from "./errors.js"; +export * from "./helpers/parseUtil.js"; +export * from "./helpers/typeAliases.js"; +export * from "./helpers/util.js"; +export * from "./types.js"; +export * from "./ZodError.js"; diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/helpers/enumUtil.d.ts b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/helpers/enumUtil.d.ts new file mode 100644 index 000000000..fad28a711 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/helpers/enumUtil.d.ts @@ -0,0 +1,8 @@ +export declare namespace enumUtil { + type UnionToIntersectionFn = (T extends unknown ? (k: () => T) => void : never) extends (k: infer Intersection) => void ? Intersection : never; + type GetUnionLast = UnionToIntersectionFn extends () => infer Last ? Last : never; + type UnionToTuple = [T] extends [never] ? Tuple : UnionToTuple>, [GetUnionLast, ...Tuple]>; + type CastToStringTuple = T extends [string, ...string[]] ? T : never; + export type UnionToTupleString = CastToStringTuple>; + export {}; +} diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/helpers/errorUtil.d.ts b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/helpers/errorUtil.d.ts new file mode 100644 index 000000000..bc9b033ff --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/helpers/errorUtil.d.ts @@ -0,0 +1,9 @@ +export declare namespace errorUtil { + type ErrMessage = string | { + message?: string | undefined; + }; + const errToObj: (message?: ErrMessage) => { + message?: string | undefined; + }; + const toString: (message?: ErrMessage) => string | undefined; +} diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/helpers/parseUtil.d.ts b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/helpers/parseUtil.d.ts new file mode 100644 index 000000000..e4dc1d7b3 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/helpers/parseUtil.d.ts @@ -0,0 +1,78 @@ +import type { IssueData, ZodErrorMap, ZodIssue } from "../ZodError.js"; +import type { ZodParsedType } from "./util.js"; +export declare const makeIssue: (params: { + data: any; + path: (string | number)[]; + errorMaps: ZodErrorMap[]; + issueData: IssueData; +}) => ZodIssue; +export type ParseParams = { + path: (string | number)[]; + errorMap: ZodErrorMap; + async: boolean; +}; +export type ParsePathComponent = string | number; +export type ParsePath = ParsePathComponent[]; +export declare const EMPTY_PATH: ParsePath; +export interface ParseContext { + readonly common: { + readonly issues: ZodIssue[]; + readonly contextualErrorMap?: ZodErrorMap | undefined; + readonly async: boolean; + }; + readonly path: ParsePath; + readonly schemaErrorMap?: ZodErrorMap | undefined; + readonly parent: ParseContext | null; + readonly data: any; + readonly parsedType: ZodParsedType; +} +export type ParseInput = { + data: any; + path: (string | number)[]; + parent: ParseContext; +}; +export declare function addIssueToContext(ctx: ParseContext, issueData: IssueData): void; +export type ObjectPair = { + key: SyncParseReturnType; + value: SyncParseReturnType; +}; +export declare class ParseStatus { + value: "aborted" | "dirty" | "valid"; + dirty(): void; + abort(): void; + static mergeArray(status: ParseStatus, results: SyncParseReturnType[]): SyncParseReturnType; + static mergeObjectAsync(status: ParseStatus, pairs: { + key: ParseReturnType; + value: ParseReturnType; + }[]): Promise>; + static mergeObjectSync(status: ParseStatus, pairs: { + key: SyncParseReturnType; + value: SyncParseReturnType; + alwaysSet?: boolean; + }[]): SyncParseReturnType; +} +export interface ParseResult { + status: "aborted" | "dirty" | "valid"; + data: any; +} +export type INVALID = { + status: "aborted"; +}; +export declare const INVALID: INVALID; +export type DIRTY = { + status: "dirty"; + value: T; +}; +export declare const DIRTY: (value: T) => DIRTY; +export type OK = { + status: "valid"; + value: T; +}; +export declare const OK: (value: T) => OK; +export type SyncParseReturnType = OK | DIRTY | INVALID; +export type AsyncParseReturnType = Promise>; +export type ParseReturnType = SyncParseReturnType | AsyncParseReturnType; +export declare const isAborted: (x: ParseReturnType) => x is INVALID; +export declare const isDirty: (x: ParseReturnType) => x is OK | DIRTY; +export declare const isValid: (x: ParseReturnType) => x is OK; +export declare const isAsync: (x: ParseReturnType) => x is AsyncParseReturnType; diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/helpers/partialUtil.d.ts b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/helpers/partialUtil.d.ts new file mode 100644 index 000000000..d07657a25 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/helpers/partialUtil.d.ts @@ -0,0 +1,8 @@ +import type { ZodArray, ZodNullable, ZodObject, ZodOptional, ZodRawShape, ZodTuple, ZodTupleItems, ZodTypeAny } from "../types.js"; +export declare namespace partialUtil { + type DeepPartial = T extends ZodObject ? ZodObject<{ + [k in keyof T["shape"]]: ZodOptional>; + }, T["_def"]["unknownKeys"], T["_def"]["catchall"]> : T extends ZodArray ? ZodArray, Card> : T extends ZodOptional ? ZodOptional> : T extends ZodNullable ? ZodNullable> : T extends ZodTuple ? { + [k in keyof Items]: Items[k] extends ZodTypeAny ? DeepPartial : never; + } extends infer PI ? PI extends ZodTupleItems ? ZodTuple : never : never : T; +} diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/helpers/typeAliases.d.ts b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/helpers/typeAliases.d.ts new file mode 100644 index 000000000..32df022ca --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/helpers/typeAliases.d.ts @@ -0,0 +1,2 @@ +export type Primitive = string | number | symbol | bigint | boolean | null | undefined; +export type Scalars = Primitive | Primitive[]; diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/helpers/util.d.ts b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/helpers/util.d.ts new file mode 100644 index 000000000..eeaaeee1c --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/helpers/util.d.ts @@ -0,0 +1,85 @@ +export declare namespace util { + type AssertEqual = (() => V extends T ? 1 : 2) extends () => V extends U ? 1 : 2 ? true : false; + export type isAny = 0 extends 1 & T ? true : false; + export const assertEqual: (_: AssertEqual) => void; + export function assertIs(_arg: T): void; + export function assertNever(_x: never): never; + export type Omit = Pick>; + export type OmitKeys = Pick>; + export type MakePartial = Omit & Partial>; + export type Exactly = T & Record, never>; + export type InexactPartial = { + [k in keyof T]?: T[k] | undefined; + }; + export const arrayToEnum: (items: U) => { [k in U[number]]: k; }; + export const getValidEnumValues: (obj: any) => any[]; + export const objectValues: (obj: any) => any[]; + export const objectKeys: ObjectConstructor["keys"]; + export const find: (arr: T[], checker: (arg: T) => any) => T | undefined; + export type identity = objectUtil.identity; + export type flatten = objectUtil.flatten; + export type noUndefined = T extends undefined ? never : T; + export const isInteger: NumberConstructor["isInteger"]; + export function joinValues(array: T, separator?: string): string; + export const jsonStringifyReplacer: (_: string, value: any) => any; + export {}; +} +export declare namespace objectUtil { + export type MergeShapes = keyof U & keyof V extends never ? U & V : { + [k in Exclude]: U[k]; + } & V; + type optionalKeys = { + [k in keyof T]: undefined extends T[k] ? k : never; + }[keyof T]; + type requiredKeys = { + [k in keyof T]: undefined extends T[k] ? never : k; + }[keyof T]; + export type addQuestionMarks = { + [K in requiredKeys]: T[K]; + } & { + [K in optionalKeys]?: T[K]; + } & { + [k in keyof T]?: unknown; + }; + export type identity = T; + export type flatten = identity<{ + [k in keyof T]: T[k]; + }>; + export type noNeverKeys = { + [k in keyof T]: [T[k]] extends [never] ? never : k; + }[keyof T]; + export type noNever = identity<{ + [k in noNeverKeys]: k extends keyof T ? T[k] : never; + }>; + export const mergeShapes: (first: U, second: T) => T & U; + export type extendShape = keyof A & keyof B extends never ? A & B : { + [K in keyof A as K extends keyof B ? never : K]: A[K]; + } & { + [K in keyof B]: B[K]; + }; + export {}; +} +export declare const ZodParsedType: { + string: "string"; + nan: "nan"; + number: "number"; + integer: "integer"; + float: "float"; + boolean: "boolean"; + date: "date"; + bigint: "bigint"; + symbol: "symbol"; + function: "function"; + undefined: "undefined"; + null: "null"; + array: "array"; + object: "object"; + unknown: "unknown"; + promise: "promise"; + void: "void"; + never: "never"; + map: "map"; + set: "set"; +}; +export type ZodParsedType = keyof typeof ZodParsedType; +export declare const getParsedType: (data: any) => ZodParsedType; diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/index.d.ts b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/index.d.ts new file mode 100644 index 000000000..a9dab5738 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/index.d.ts @@ -0,0 +1,4 @@ +import * as z from "./external.js"; +export * from "./external.js"; +export { z }; +export default z; diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/locales/en.d.ts b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/locales/en.d.ts new file mode 100644 index 000000000..b2bea1ec4 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/locales/en.d.ts @@ -0,0 +1,3 @@ +import { type ZodErrorMap } from "../ZodError.js"; +declare const errorMap: ZodErrorMap; +export default errorMap; diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/standard-schema.d.ts b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/standard-schema.d.ts new file mode 100644 index 000000000..7ddff3b7f --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/standard-schema.d.ts @@ -0,0 +1,102 @@ +/** + * The Standard Schema interface. + */ +export type StandardSchemaV1 = { + /** + * The Standard Schema properties. + */ + readonly "~standard": StandardSchemaV1.Props; +}; +export declare namespace StandardSchemaV1 { + /** + * The Standard Schema properties interface. + */ + export interface Props { + /** + * The version number of the standard. + */ + readonly version: 1; + /** + * The vendor name of the schema library. + */ + readonly vendor: string; + /** + * Validates unknown input values. + */ + readonly validate: (value: unknown) => Result | Promise>; + /** + * Inferred types associated with the schema. + */ + readonly types?: Types | undefined; + } + /** + * The result interface of the validate function. + */ + export type Result = SuccessResult | FailureResult; + /** + * The result interface if validation succeeds. + */ + export interface SuccessResult { + /** + * The typed output value. + */ + readonly value: Output; + /** + * The non-existent issues. + */ + readonly issues?: undefined; + } + /** + * The result interface if validation fails. + */ + export interface FailureResult { + /** + * The issues of failed validation. + */ + readonly issues: ReadonlyArray; + } + /** + * The issue interface of the failure output. + */ + export interface Issue { + /** + * The error message of the issue. + */ + readonly message: string; + /** + * The path of the issue, if any. + */ + readonly path?: ReadonlyArray | undefined; + } + /** + * The path segment interface of the issue. + */ + export interface PathSegment { + /** + * The key representing a path segment. + */ + readonly key: PropertyKey; + } + /** + * The Standard Schema types interface. + */ + export interface Types { + /** + * The input type of the schema. + */ + readonly input: Input; + /** + * The output type of the schema. + */ + readonly output: Output; + } + /** + * Infers the input type of a Standard Schema. + */ + export type InferInput = NonNullable["input"]; + /** + * Infers the output type of a Standard Schema. + */ + export type InferOutput = NonNullable["output"]; + export {}; +} diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/types.d.ts b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/types.d.ts new file mode 100644 index 000000000..301e09128 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/types.d.ts @@ -0,0 +1,1031 @@ +import { type IssueData, type StringValidation, type ZodCustomIssue, ZodError, type ZodErrorMap } from "./ZodError.js"; +import type { enumUtil } from "./helpers/enumUtil.js"; +import { errorUtil } from "./helpers/errorUtil.js"; +import { type AsyncParseReturnType, INVALID, type ParseContext, type ParseInput, type ParseParams, type ParseReturnType, ParseStatus, type SyncParseReturnType } from "./helpers/parseUtil.js"; +import type { partialUtil } from "./helpers/partialUtil.js"; +import type { Primitive } from "./helpers/typeAliases.js"; +import { util, type objectUtil } from "./helpers/util.js"; +import type { StandardSchemaV1 } from "./standard-schema.js"; +export interface RefinementCtx { + addIssue: (arg: IssueData) => void; + path: (string | number)[]; +} +export type ZodRawShape = { + [k: string]: ZodTypeAny; +}; +export type ZodTypeAny = ZodType; +export type TypeOf> = T["_output"]; +export type input> = T["_input"]; +export type output> = T["_output"]; +export type { TypeOf as infer }; +export type CustomErrorParams = Partial>; +export interface ZodTypeDef { + errorMap?: ZodErrorMap | undefined; + description?: string | undefined; +} +export type RawCreateParams = { + errorMap?: ZodErrorMap | undefined; + invalid_type_error?: string | undefined; + required_error?: string | undefined; + message?: string | undefined; + description?: string | undefined; +} | undefined; +export type ProcessedCreateParams = { + errorMap?: ZodErrorMap | undefined; + description?: string | undefined; +}; +export type SafeParseSuccess = { + success: true; + data: Output; + error?: never; +}; +export type SafeParseError = { + success: false; + error: ZodError; + data?: never; +}; +export type SafeParseReturnType = SafeParseSuccess | SafeParseError; +export declare abstract class ZodType { + readonly _type: Output; + readonly _output: Output; + readonly _input: Input; + readonly _def: Def; + get description(): string | undefined; + "~standard": StandardSchemaV1.Props; + abstract _parse(input: ParseInput): ParseReturnType; + _getType(input: ParseInput): string; + _getOrReturnCtx(input: ParseInput, ctx?: ParseContext | undefined): ParseContext; + _processInputParams(input: ParseInput): { + status: ParseStatus; + ctx: ParseContext; + }; + _parseSync(input: ParseInput): SyncParseReturnType; + _parseAsync(input: ParseInput): AsyncParseReturnType; + parse(data: unknown, params?: util.InexactPartial): Output; + safeParse(data: unknown, params?: util.InexactPartial): SafeParseReturnType; + "~validate"(data: unknown): StandardSchemaV1.Result | Promise>; + parseAsync(data: unknown, params?: util.InexactPartial): Promise; + safeParseAsync(data: unknown, params?: util.InexactPartial): Promise>; + /** Alias of safeParseAsync */ + spa: (data: unknown, params?: util.InexactPartial) => Promise>; + refine(check: (arg: Output) => arg is RefinedOutput, message?: string | CustomErrorParams | ((arg: Output) => CustomErrorParams)): ZodEffects; + refine(check: (arg: Output) => unknown | Promise, message?: string | CustomErrorParams | ((arg: Output) => CustomErrorParams)): ZodEffects; + refinement(check: (arg: Output) => arg is RefinedOutput, refinementData: IssueData | ((arg: Output, ctx: RefinementCtx) => IssueData)): ZodEffects; + refinement(check: (arg: Output) => boolean, refinementData: IssueData | ((arg: Output, ctx: RefinementCtx) => IssueData)): ZodEffects; + _refinement(refinement: RefinementEffect["refinement"]): ZodEffects; + superRefine(refinement: (arg: Output, ctx: RefinementCtx) => arg is RefinedOutput): ZodEffects; + superRefine(refinement: (arg: Output, ctx: RefinementCtx) => void): ZodEffects; + superRefine(refinement: (arg: Output, ctx: RefinementCtx) => Promise): ZodEffects; + constructor(def: Def); + optional(): ZodOptional; + nullable(): ZodNullable; + nullish(): ZodOptional>; + array(): ZodArray; + promise(): ZodPromise; + or(option: T): ZodUnion<[this, T]>; + and(incoming: T): ZodIntersection; + transform(transform: (arg: Output, ctx: RefinementCtx) => NewOut | Promise): ZodEffects; + default(def: util.noUndefined): ZodDefault; + default(def: () => util.noUndefined): ZodDefault; + brand(brand?: B): ZodBranded; + catch(def: Output): ZodCatch; + catch(def: (ctx: { + error: ZodError; + input: Input; + }) => Output): ZodCatch; + describe(description: string): this; + pipe(target: T): ZodPipeline; + readonly(): ZodReadonly; + isOptional(): boolean; + isNullable(): boolean; +} +export type IpVersion = "v4" | "v6"; +export type ZodStringCheck = { + kind: "min"; + value: number; + message?: string | undefined; +} | { + kind: "max"; + value: number; + message?: string | undefined; +} | { + kind: "length"; + value: number; + message?: string | undefined; +} | { + kind: "email"; + message?: string | undefined; +} | { + kind: "url"; + message?: string | undefined; +} | { + kind: "emoji"; + message?: string | undefined; +} | { + kind: "uuid"; + message?: string | undefined; +} | { + kind: "nanoid"; + message?: string | undefined; +} | { + kind: "cuid"; + message?: string | undefined; +} | { + kind: "includes"; + value: string; + position?: number | undefined; + message?: string | undefined; +} | { + kind: "cuid2"; + message?: string | undefined; +} | { + kind: "ulid"; + message?: string | undefined; +} | { + kind: "startsWith"; + value: string; + message?: string | undefined; +} | { + kind: "endsWith"; + value: string; + message?: string | undefined; +} | { + kind: "regex"; + regex: RegExp; + message?: string | undefined; +} | { + kind: "trim"; + message?: string | undefined; +} | { + kind: "toLowerCase"; + message?: string | undefined; +} | { + kind: "toUpperCase"; + message?: string | undefined; +} | { + kind: "jwt"; + alg?: string; + message?: string | undefined; +} | { + kind: "datetime"; + offset: boolean; + local: boolean; + precision: number | null; + message?: string | undefined; +} | { + kind: "date"; + message?: string | undefined; +} | { + kind: "time"; + precision: number | null; + message?: string | undefined; +} | { + kind: "duration"; + message?: string | undefined; +} | { + kind: "ip"; + version?: IpVersion | undefined; + message?: string | undefined; +} | { + kind: "cidr"; + version?: IpVersion | undefined; + message?: string | undefined; +} | { + kind: "base64"; + message?: string | undefined; +} | { + kind: "base64url"; + message?: string | undefined; +}; +export interface ZodStringDef extends ZodTypeDef { + checks: ZodStringCheck[]; + typeName: ZodFirstPartyTypeKind.ZodString; + coerce: boolean; +} +export declare function datetimeRegex(args: { + precision?: number | null; + offset?: boolean; + local?: boolean; +}): RegExp; +export declare class ZodString extends ZodType { + _parse(input: ParseInput): ParseReturnType; + protected _regex(regex: RegExp, validation: StringValidation, message?: errorUtil.ErrMessage): ZodEffects; + _addCheck(check: ZodStringCheck): ZodString; + email(message?: errorUtil.ErrMessage): ZodString; + url(message?: errorUtil.ErrMessage): ZodString; + emoji(message?: errorUtil.ErrMessage): ZodString; + uuid(message?: errorUtil.ErrMessage): ZodString; + nanoid(message?: errorUtil.ErrMessage): ZodString; + cuid(message?: errorUtil.ErrMessage): ZodString; + cuid2(message?: errorUtil.ErrMessage): ZodString; + ulid(message?: errorUtil.ErrMessage): ZodString; + base64(message?: errorUtil.ErrMessage): ZodString; + base64url(message?: errorUtil.ErrMessage): ZodString; + jwt(options?: { + alg?: string; + message?: string | undefined; + }): ZodString; + ip(options?: string | { + version?: IpVersion; + message?: string | undefined; + }): ZodString; + cidr(options?: string | { + version?: IpVersion; + message?: string | undefined; + }): ZodString; + datetime(options?: string | { + message?: string | undefined; + precision?: number | null; + offset?: boolean; + local?: boolean; + }): ZodString; + date(message?: string): ZodString; + time(options?: string | { + message?: string | undefined; + precision?: number | null; + }): ZodString; + duration(message?: errorUtil.ErrMessage): ZodString; + regex(regex: RegExp, message?: errorUtil.ErrMessage): ZodString; + includes(value: string, options?: { + message?: string; + position?: number; + }): ZodString; + startsWith(value: string, message?: errorUtil.ErrMessage): ZodString; + endsWith(value: string, message?: errorUtil.ErrMessage): ZodString; + min(minLength: number, message?: errorUtil.ErrMessage): ZodString; + max(maxLength: number, message?: errorUtil.ErrMessage): ZodString; + length(len: number, message?: errorUtil.ErrMessage): ZodString; + /** + * Equivalent to `.min(1)` + */ + nonempty(message?: errorUtil.ErrMessage): ZodString; + trim(): ZodString; + toLowerCase(): ZodString; + toUpperCase(): ZodString; + get isDatetime(): boolean; + get isDate(): boolean; + get isTime(): boolean; + get isDuration(): boolean; + get isEmail(): boolean; + get isURL(): boolean; + get isEmoji(): boolean; + get isUUID(): boolean; + get isNANOID(): boolean; + get isCUID(): boolean; + get isCUID2(): boolean; + get isULID(): boolean; + get isIP(): boolean; + get isCIDR(): boolean; + get isBase64(): boolean; + get isBase64url(): boolean; + get minLength(): number | null; + get maxLength(): number | null; + static create: (params?: RawCreateParams & { + coerce?: true; + }) => ZodString; +} +export type ZodNumberCheck = { + kind: "min"; + value: number; + inclusive: boolean; + message?: string | undefined; +} | { + kind: "max"; + value: number; + inclusive: boolean; + message?: string | undefined; +} | { + kind: "int"; + message?: string | undefined; +} | { + kind: "multipleOf"; + value: number; + message?: string | undefined; +} | { + kind: "finite"; + message?: string | undefined; +}; +export interface ZodNumberDef extends ZodTypeDef { + checks: ZodNumberCheck[]; + typeName: ZodFirstPartyTypeKind.ZodNumber; + coerce: boolean; +} +export declare class ZodNumber extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams & { + coerce?: boolean; + }) => ZodNumber; + gte(value: number, message?: errorUtil.ErrMessage): ZodNumber; + min: (value: number, message?: errorUtil.ErrMessage) => ZodNumber; + gt(value: number, message?: errorUtil.ErrMessage): ZodNumber; + lte(value: number, message?: errorUtil.ErrMessage): ZodNumber; + max: (value: number, message?: errorUtil.ErrMessage) => ZodNumber; + lt(value: number, message?: errorUtil.ErrMessage): ZodNumber; + protected setLimit(kind: "min" | "max", value: number, inclusive: boolean, message?: string): ZodNumber; + _addCheck(check: ZodNumberCheck): ZodNumber; + int(message?: errorUtil.ErrMessage): ZodNumber; + positive(message?: errorUtil.ErrMessage): ZodNumber; + negative(message?: errorUtil.ErrMessage): ZodNumber; + nonpositive(message?: errorUtil.ErrMessage): ZodNumber; + nonnegative(message?: errorUtil.ErrMessage): ZodNumber; + multipleOf(value: number, message?: errorUtil.ErrMessage): ZodNumber; + step: (value: number, message?: errorUtil.ErrMessage) => ZodNumber; + finite(message?: errorUtil.ErrMessage): ZodNumber; + safe(message?: errorUtil.ErrMessage): ZodNumber; + get minValue(): number | null; + get maxValue(): number | null; + get isInt(): boolean; + get isFinite(): boolean; +} +export type ZodBigIntCheck = { + kind: "min"; + value: bigint; + inclusive: boolean; + message?: string | undefined; +} | { + kind: "max"; + value: bigint; + inclusive: boolean; + message?: string | undefined; +} | { + kind: "multipleOf"; + value: bigint; + message?: string | undefined; +}; +export interface ZodBigIntDef extends ZodTypeDef { + checks: ZodBigIntCheck[]; + typeName: ZodFirstPartyTypeKind.ZodBigInt; + coerce: boolean; +} +export declare class ZodBigInt extends ZodType { + _parse(input: ParseInput): ParseReturnType; + _getInvalidInput(input: ParseInput): INVALID; + static create: (params?: RawCreateParams & { + coerce?: boolean; + }) => ZodBigInt; + gte(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + min: (value: bigint, message?: errorUtil.ErrMessage) => ZodBigInt; + gt(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + lte(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + max: (value: bigint, message?: errorUtil.ErrMessage) => ZodBigInt; + lt(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + protected setLimit(kind: "min" | "max", value: bigint, inclusive: boolean, message?: string): ZodBigInt; + _addCheck(check: ZodBigIntCheck): ZodBigInt; + positive(message?: errorUtil.ErrMessage): ZodBigInt; + negative(message?: errorUtil.ErrMessage): ZodBigInt; + nonpositive(message?: errorUtil.ErrMessage): ZodBigInt; + nonnegative(message?: errorUtil.ErrMessage): ZodBigInt; + multipleOf(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + get minValue(): bigint | null; + get maxValue(): bigint | null; +} +export interface ZodBooleanDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodBoolean; + coerce: boolean; +} +export declare class ZodBoolean extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams & { + coerce?: boolean; + }) => ZodBoolean; +} +export type ZodDateCheck = { + kind: "min"; + value: number; + message?: string | undefined; +} | { + kind: "max"; + value: number; + message?: string | undefined; +}; +export interface ZodDateDef extends ZodTypeDef { + checks: ZodDateCheck[]; + coerce: boolean; + typeName: ZodFirstPartyTypeKind.ZodDate; +} +export declare class ZodDate extends ZodType { + _parse(input: ParseInput): ParseReturnType; + _addCheck(check: ZodDateCheck): ZodDate; + min(minDate: Date, message?: errorUtil.ErrMessage): ZodDate; + max(maxDate: Date, message?: errorUtil.ErrMessage): ZodDate; + get minDate(): Date | null; + get maxDate(): Date | null; + static create: (params?: RawCreateParams & { + coerce?: boolean; + }) => ZodDate; +} +export interface ZodSymbolDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodSymbol; +} +export declare class ZodSymbol extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodSymbol; +} +export interface ZodUndefinedDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodUndefined; +} +export declare class ZodUndefined extends ZodType { + _parse(input: ParseInput): ParseReturnType; + params?: RawCreateParams; + static create: (params?: RawCreateParams) => ZodUndefined; +} +export interface ZodNullDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodNull; +} +export declare class ZodNull extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodNull; +} +export interface ZodAnyDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodAny; +} +export declare class ZodAny extends ZodType { + _any: true; + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodAny; +} +export interface ZodUnknownDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodUnknown; +} +export declare class ZodUnknown extends ZodType { + _unknown: true; + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodUnknown; +} +export interface ZodNeverDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodNever; +} +export declare class ZodNever extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodNever; +} +export interface ZodVoidDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodVoid; +} +export declare class ZodVoid extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodVoid; +} +export interface ZodArrayDef extends ZodTypeDef { + type: T; + typeName: ZodFirstPartyTypeKind.ZodArray; + exactLength: { + value: number; + message?: string | undefined; + } | null; + minLength: { + value: number; + message?: string | undefined; + } | null; + maxLength: { + value: number; + message?: string | undefined; + } | null; +} +export type ArrayCardinality = "many" | "atleastone"; +export type arrayOutputType = Cardinality extends "atleastone" ? [T["_output"], ...T["_output"][]] : T["_output"][]; +export declare class ZodArray extends ZodType, ZodArrayDef, Cardinality extends "atleastone" ? [T["_input"], ...T["_input"][]] : T["_input"][]> { + _parse(input: ParseInput): ParseReturnType; + get element(): T; + min(minLength: number, message?: errorUtil.ErrMessage): this; + max(maxLength: number, message?: errorUtil.ErrMessage): this; + length(len: number, message?: errorUtil.ErrMessage): this; + nonempty(message?: errorUtil.ErrMessage): ZodArray; + static create: (schema: El, params?: RawCreateParams) => ZodArray; +} +export type ZodNonEmptyArray = ZodArray; +export type UnknownKeysParam = "passthrough" | "strict" | "strip"; +export interface ZodObjectDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodObject; + shape: () => T; + catchall: Catchall; + unknownKeys: UnknownKeys; +} +export type mergeTypes = { + [k in keyof A | keyof B]: k extends keyof B ? B[k] : k extends keyof A ? A[k] : never; +}; +export type objectOutputType = objectUtil.flatten>> & CatchallOutput & PassthroughType; +export type baseObjectOutputType = { + [k in keyof Shape]: Shape[k]["_output"]; +}; +export type objectInputType = objectUtil.flatten> & CatchallInput & PassthroughType; +export type baseObjectInputType = objectUtil.addQuestionMarks<{ + [k in keyof Shape]: Shape[k]["_input"]; +}>; +export type CatchallOutput = ZodType extends T ? unknown : { + [k: string]: T["_output"]; +}; +export type CatchallInput = ZodType extends T ? unknown : { + [k: string]: T["_input"]; +}; +export type PassthroughType = T extends "passthrough" ? { + [k: string]: unknown; +} : unknown; +export type deoptional = T extends ZodOptional ? deoptional : T extends ZodNullable ? ZodNullable> : T; +export type SomeZodObject = ZodObject; +export type noUnrecognized = { + [k in keyof Obj]: k extends keyof Shape ? Obj[k] : never; +}; +export declare class ZodObject, Input = objectInputType> extends ZodType, Input> { + private _cached; + _getCached(): { + shape: T; + keys: string[]; + }; + _parse(input: ParseInput): ParseReturnType; + get shape(): T; + strict(message?: errorUtil.ErrMessage): ZodObject; + strip(): ZodObject; + passthrough(): ZodObject; + /** + * @deprecated In most cases, this is no longer needed - unknown properties are now silently stripped. + * If you want to pass through unknown properties, use `.passthrough()` instead. + */ + nonstrict: () => ZodObject; + extend(augmentation: Augmentation): ZodObject, UnknownKeys, Catchall>; + /** + * @deprecated Use `.extend` instead + * */ + augment: (augmentation: Augmentation) => ZodObject, UnknownKeys, Catchall>; + /** + * Prior to zod@1.0.12 there was a bug in the + * inferred type of merged objects. Please + * upgrade if you are experiencing issues. + */ + merge(merging: Incoming): ZodObject, Incoming["_def"]["unknownKeys"], Incoming["_def"]["catchall"]>; + setKey(key: Key, schema: Schema): ZodObject; + catchall(index: Index): ZodObject; + pick>(mask: Mask): ZodObject>, UnknownKeys, Catchall>; + omit>(mask: Mask): ZodObject, UnknownKeys, Catchall>; + /** + * @deprecated + */ + deepPartial(): partialUtil.DeepPartial; + partial(): ZodObject<{ + [k in keyof T]: ZodOptional; + }, UnknownKeys, Catchall>; + partial>(mask: Mask): ZodObject : T[k]; + }>, UnknownKeys, Catchall>; + required(): ZodObject<{ + [k in keyof T]: deoptional; + }, UnknownKeys, Catchall>; + required>(mask: Mask): ZodObject : T[k]; + }>, UnknownKeys, Catchall>; + keyof(): ZodEnum>; + static create: (shape: Shape, params?: RawCreateParams) => ZodObject, objectInputType>; + static strictCreate: (shape: Shape, params?: RawCreateParams) => ZodObject; + static lazycreate: (shape: () => Shape, params?: RawCreateParams) => ZodObject; +} +export type AnyZodObject = ZodObject; +export type ZodUnionOptions = Readonly<[ZodTypeAny, ...ZodTypeAny[]]>; +export interface ZodUnionDef> extends ZodTypeDef { + options: T; + typeName: ZodFirstPartyTypeKind.ZodUnion; +} +export declare class ZodUnion extends ZodType, T[number]["_input"]> { + _parse(input: ParseInput): ParseReturnType; + get options(): T; + static create: >(types: Options, params?: RawCreateParams) => ZodUnion; +} +export type ZodDiscriminatedUnionOption = ZodObject<{ + [key in Discriminator]: ZodTypeAny; +} & ZodRawShape, UnknownKeysParam, ZodTypeAny>; +export interface ZodDiscriminatedUnionDef[] = ZodDiscriminatedUnionOption[]> extends ZodTypeDef { + discriminator: Discriminator; + options: Options; + optionsMap: Map>; + typeName: ZodFirstPartyTypeKind.ZodDiscriminatedUnion; +} +export declare class ZodDiscriminatedUnion[]> extends ZodType, ZodDiscriminatedUnionDef, input> { + _parse(input: ParseInput): ParseReturnType; + get discriminator(): Discriminator; + get options(): Options; + get optionsMap(): Map>; + /** + * The constructor of the discriminated union schema. Its behaviour is very similar to that of the normal z.union() constructor. + * However, it only allows a union of objects, all of which need to share a discriminator property. This property must + * have a different value for each object in the union. + * @param discriminator the name of the discriminator property + * @param types an array of object schemas + * @param params + */ + static create, + ...ZodDiscriminatedUnionOption[] + ]>(discriminator: Discriminator, options: Types, params?: RawCreateParams): ZodDiscriminatedUnion; +} +export interface ZodIntersectionDef extends ZodTypeDef { + left: T; + right: U; + typeName: ZodFirstPartyTypeKind.ZodIntersection; +} +export declare class ZodIntersection extends ZodType, T["_input"] & U["_input"]> { + _parse(input: ParseInput): ParseReturnType; + static create: (left: TSchema, right: USchema, params?: RawCreateParams) => ZodIntersection; +} +export type ZodTupleItems = [ZodTypeAny, ...ZodTypeAny[]]; +export type AssertArray = T extends any[] ? T : never; +export type OutputTypeOfTuple = AssertArray<{ + [k in keyof T]: T[k] extends ZodType ? T[k]["_output"] : never; +}>; +export type OutputTypeOfTupleWithRest = Rest extends ZodTypeAny ? [...OutputTypeOfTuple, ...Rest["_output"][]] : OutputTypeOfTuple; +export type InputTypeOfTuple = AssertArray<{ + [k in keyof T]: T[k] extends ZodType ? T[k]["_input"] : never; +}>; +export type InputTypeOfTupleWithRest = Rest extends ZodTypeAny ? [...InputTypeOfTuple, ...Rest["_input"][]] : InputTypeOfTuple; +export interface ZodTupleDef extends ZodTypeDef { + items: T; + rest: Rest; + typeName: ZodFirstPartyTypeKind.ZodTuple; +} +export type AnyZodTuple = ZodTuple<[ZodTypeAny, ...ZodTypeAny[]] | [], ZodTypeAny | null>; +export declare class ZodTuple extends ZodType, ZodTupleDef, InputTypeOfTupleWithRest> { + _parse(input: ParseInput): ParseReturnType; + get items(): T; + rest(rest: RestSchema): ZodTuple; + static create: (schemas: Items, params?: RawCreateParams) => ZodTuple; +} +export interface ZodRecordDef extends ZodTypeDef { + valueType: Value; + keyType: Key; + typeName: ZodFirstPartyTypeKind.ZodRecord; +} +export type KeySchema = ZodType; +export type RecordType = [string] extends [K] ? Record : [number] extends [K] ? Record : [symbol] extends [K] ? Record : [BRAND] extends [K] ? Record : Partial>; +export declare class ZodRecord extends ZodType, ZodRecordDef, RecordType> { + get keySchema(): Key; + get valueSchema(): Value; + _parse(input: ParseInput): ParseReturnType; + get element(): Value; + static create(valueType: Value, params?: RawCreateParams): ZodRecord; + static create(keySchema: Keys, valueType: Value, params?: RawCreateParams): ZodRecord; +} +export interface ZodMapDef extends ZodTypeDef { + valueType: Value; + keyType: Key; + typeName: ZodFirstPartyTypeKind.ZodMap; +} +export declare class ZodMap extends ZodType, ZodMapDef, Map> { + get keySchema(): Key; + get valueSchema(): Value; + _parse(input: ParseInput): ParseReturnType; + static create: (keyType: KeySchema, valueType: ValueSchema, params?: RawCreateParams) => ZodMap; +} +export interface ZodSetDef extends ZodTypeDef { + valueType: Value; + typeName: ZodFirstPartyTypeKind.ZodSet; + minSize: { + value: number; + message?: string | undefined; + } | null; + maxSize: { + value: number; + message?: string | undefined; + } | null; +} +export declare class ZodSet extends ZodType, ZodSetDef, Set> { + _parse(input: ParseInput): ParseReturnType; + min(minSize: number, message?: errorUtil.ErrMessage): this; + max(maxSize: number, message?: errorUtil.ErrMessage): this; + size(size: number, message?: errorUtil.ErrMessage): this; + nonempty(message?: errorUtil.ErrMessage): ZodSet; + static create: (valueType: ValueSchema, params?: RawCreateParams) => ZodSet; +} +export interface ZodFunctionDef = ZodTuple, Returns extends ZodTypeAny = ZodTypeAny> extends ZodTypeDef { + args: Args; + returns: Returns; + typeName: ZodFirstPartyTypeKind.ZodFunction; +} +export type OuterTypeOfFunction, Returns extends ZodTypeAny> = Args["_input"] extends Array ? (...args: Args["_input"]) => Returns["_output"] : never; +export type InnerTypeOfFunction, Returns extends ZodTypeAny> = Args["_output"] extends Array ? (...args: Args["_output"]) => Returns["_input"] : never; +export declare class ZodFunction, Returns extends ZodTypeAny> extends ZodType, ZodFunctionDef, InnerTypeOfFunction> { + _parse(input: ParseInput): ParseReturnType; + parameters(): Args; + returnType(): Returns; + args[0]>(...items: Items): ZodFunction, Returns>; + returns>(returnType: NewReturnType): ZodFunction; + implement>(func: F): ReturnType extends Returns["_output"] ? (...args: Args["_input"]) => ReturnType : OuterTypeOfFunction; + strictImplement(func: InnerTypeOfFunction): InnerTypeOfFunction; + validate: >(func: F) => ReturnType extends Returns["_output"] ? (...args: Args["_input"]) => ReturnType : OuterTypeOfFunction; + static create(): ZodFunction, ZodUnknown>; + static create>(args: T): ZodFunction; + static create(args: T, returns: U): ZodFunction; + static create, U extends ZodTypeAny = ZodUnknown>(args: T, returns: U, params?: RawCreateParams): ZodFunction; +} +export interface ZodLazyDef extends ZodTypeDef { + getter: () => T; + typeName: ZodFirstPartyTypeKind.ZodLazy; +} +export declare class ZodLazy extends ZodType, ZodLazyDef, input> { + get schema(): T; + _parse(input: ParseInput): ParseReturnType; + static create: (getter: () => Inner, params?: RawCreateParams) => ZodLazy; +} +export interface ZodLiteralDef extends ZodTypeDef { + value: T; + typeName: ZodFirstPartyTypeKind.ZodLiteral; +} +export declare class ZodLiteral extends ZodType, T> { + _parse(input: ParseInput): ParseReturnType; + get value(): T; + static create: (value: Value, params?: RawCreateParams) => ZodLiteral; +} +export type ArrayKeys = keyof any[]; +export type Indices = Exclude; +export type EnumValues = readonly [T, ...T[]]; +export type Values = { + [k in T[number]]: k; +}; +export interface ZodEnumDef extends ZodTypeDef { + values: T; + typeName: ZodFirstPartyTypeKind.ZodEnum; +} +export type Writeable = { + -readonly [P in keyof T]: T[P]; +}; +export type FilterEnum = Values extends [] ? [] : Values extends [infer Head, ...infer Rest] ? Head extends ToExclude ? FilterEnum : [Head, ...FilterEnum] : never; +export type typecast = A extends T ? A : never; +declare function createZodEnum>(values: T, params?: RawCreateParams): ZodEnum>; +declare function createZodEnum(values: T, params?: RawCreateParams): ZodEnum; +export declare class ZodEnum extends ZodType, T[number]> { + _cache: Set | undefined; + _parse(input: ParseInput): ParseReturnType; + get options(): T; + get enum(): Values; + get Values(): Values; + get Enum(): Values; + extract(values: ToExtract, newDef?: RawCreateParams): ZodEnum>; + exclude(values: ToExclude, newDef?: RawCreateParams): ZodEnum>, [string, ...string[]]>>; + static create: typeof createZodEnum; +} +export interface ZodNativeEnumDef extends ZodTypeDef { + values: T; + typeName: ZodFirstPartyTypeKind.ZodNativeEnum; +} +export type EnumLike = { + [k: string]: string | number; + [nu: number]: string; +}; +export declare class ZodNativeEnum extends ZodType, T[keyof T]> { + _cache: Set | undefined; + _parse(input: ParseInput): ParseReturnType; + get enum(): T; + static create: (values: Elements, params?: RawCreateParams) => ZodNativeEnum; +} +export interface ZodPromiseDef extends ZodTypeDef { + type: T; + typeName: ZodFirstPartyTypeKind.ZodPromise; +} +export declare class ZodPromise extends ZodType, ZodPromiseDef, Promise> { + unwrap(): T; + _parse(input: ParseInput): ParseReturnType; + static create: (schema: Inner, params?: RawCreateParams) => ZodPromise; +} +export type Refinement = (arg: T, ctx: RefinementCtx) => any; +export type SuperRefinement = (arg: T, ctx: RefinementCtx) => void | Promise; +export type RefinementEffect = { + type: "refinement"; + refinement: (arg: T, ctx: RefinementCtx) => any; +}; +export type TransformEffect = { + type: "transform"; + transform: (arg: T, ctx: RefinementCtx) => any; +}; +export type PreprocessEffect = { + type: "preprocess"; + transform: (arg: T, ctx: RefinementCtx) => any; +}; +export type Effect = RefinementEffect | TransformEffect | PreprocessEffect; +export interface ZodEffectsDef extends ZodTypeDef { + schema: T; + typeName: ZodFirstPartyTypeKind.ZodEffects; + effect: Effect; +} +export declare class ZodEffects, Input = input> extends ZodType, Input> { + innerType(): T; + sourceType(): T; + _parse(input: ParseInput): ParseReturnType; + static create: (schema: I, effect: Effect, params?: RawCreateParams) => ZodEffects; + static createWithPreprocess: (preprocess: (arg: unknown, ctx: RefinementCtx) => unknown, schema: I, params?: RawCreateParams) => ZodEffects; +} +export { ZodEffects as ZodTransformer }; +export interface ZodOptionalDef extends ZodTypeDef { + innerType: T; + typeName: ZodFirstPartyTypeKind.ZodOptional; +} +export type ZodOptionalType = ZodOptional; +export declare class ZodOptional extends ZodType, T["_input"] | undefined> { + _parse(input: ParseInput): ParseReturnType; + unwrap(): T; + static create: (type: Inner, params?: RawCreateParams) => ZodOptional; +} +export interface ZodNullableDef extends ZodTypeDef { + innerType: T; + typeName: ZodFirstPartyTypeKind.ZodNullable; +} +export type ZodNullableType = ZodNullable; +export declare class ZodNullable extends ZodType, T["_input"] | null> { + _parse(input: ParseInput): ParseReturnType; + unwrap(): T; + static create: (type: Inner, params?: RawCreateParams) => ZodNullable; +} +export interface ZodDefaultDef extends ZodTypeDef { + innerType: T; + defaultValue: () => util.noUndefined; + typeName: ZodFirstPartyTypeKind.ZodDefault; +} +export declare class ZodDefault extends ZodType, ZodDefaultDef, T["_input"] | undefined> { + _parse(input: ParseInput): ParseReturnType; + removeDefault(): T; + static create: (type: Inner, params: RawCreateParams & { + default: Inner["_input"] | (() => util.noUndefined); + }) => ZodDefault; +} +export interface ZodCatchDef extends ZodTypeDef { + innerType: T; + catchValue: (ctx: { + error: ZodError; + input: unknown; + }) => T["_input"]; + typeName: ZodFirstPartyTypeKind.ZodCatch; +} +export declare class ZodCatch extends ZodType, unknown> { + _parse(input: ParseInput): ParseReturnType; + removeCatch(): T; + static create: (type: Inner, params: RawCreateParams & { + catch: Inner["_output"] | (() => Inner["_output"]); + }) => ZodCatch; +} +export interface ZodNaNDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodNaN; +} +export declare class ZodNaN extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodNaN; +} +export interface ZodBrandedDef extends ZodTypeDef { + type: T; + typeName: ZodFirstPartyTypeKind.ZodBranded; +} +export declare const BRAND: unique symbol; +export type BRAND = { + [BRAND]: { + [k in T]: true; + }; +}; +export declare class ZodBranded extends ZodType, ZodBrandedDef, T["_input"]> { + _parse(input: ParseInput): ParseReturnType; + unwrap(): T; +} +export interface ZodPipelineDef extends ZodTypeDef { + in: A; + out: B; + typeName: ZodFirstPartyTypeKind.ZodPipeline; +} +export declare class ZodPipeline extends ZodType, A["_input"]> { + _parse(input: ParseInput): ParseReturnType; + static create(a: ASchema, b: BSchema): ZodPipeline; +} +type BuiltIn = (((...args: any[]) => any) | (new (...args: any[]) => any)) | { + readonly [Symbol.toStringTag]: string; +} | Date | Error | Generator | Promise | RegExp; +type MakeReadonly = T extends Map ? ReadonlyMap : T extends Set ? ReadonlySet : T extends [infer Head, ...infer Tail] ? readonly [Head, ...Tail] : T extends Array ? ReadonlyArray : T extends BuiltIn ? T : Readonly; +export interface ZodReadonlyDef extends ZodTypeDef { + innerType: T; + typeName: ZodFirstPartyTypeKind.ZodReadonly; +} +export declare class ZodReadonly extends ZodType, ZodReadonlyDef, MakeReadonly> { + _parse(input: ParseInput): ParseReturnType; + static create: (type: Inner, params?: RawCreateParams) => ZodReadonly; + unwrap(): T; +} +type CustomParams = CustomErrorParams & { + fatal?: boolean; +}; +export declare function custom(check?: (data: any) => any, _params?: string | CustomParams | ((input: any) => CustomParams), +/** + * @deprecated + * + * Pass `fatal` into the params object instead: + * + * ```ts + * z.string().custom((val) => val.length > 5, { fatal: false }) + * ``` + * + */ +fatal?: boolean): ZodType; +export { ZodType as Schema, ZodType as ZodSchema }; +export declare const late: { + object: (shape: () => Shape, params?: RawCreateParams) => ZodObject; +}; +export declare enum ZodFirstPartyTypeKind { + ZodString = "ZodString", + ZodNumber = "ZodNumber", + ZodNaN = "ZodNaN", + ZodBigInt = "ZodBigInt", + ZodBoolean = "ZodBoolean", + ZodDate = "ZodDate", + ZodSymbol = "ZodSymbol", + ZodUndefined = "ZodUndefined", + ZodNull = "ZodNull", + ZodAny = "ZodAny", + ZodUnknown = "ZodUnknown", + ZodNever = "ZodNever", + ZodVoid = "ZodVoid", + ZodArray = "ZodArray", + ZodObject = "ZodObject", + ZodUnion = "ZodUnion", + ZodDiscriminatedUnion = "ZodDiscriminatedUnion", + ZodIntersection = "ZodIntersection", + ZodTuple = "ZodTuple", + ZodRecord = "ZodRecord", + ZodMap = "ZodMap", + ZodSet = "ZodSet", + ZodFunction = "ZodFunction", + ZodLazy = "ZodLazy", + ZodLiteral = "ZodLiteral", + ZodEnum = "ZodEnum", + ZodEffects = "ZodEffects", + ZodNativeEnum = "ZodNativeEnum", + ZodOptional = "ZodOptional", + ZodNullable = "ZodNullable", + ZodDefault = "ZodDefault", + ZodCatch = "ZodCatch", + ZodPromise = "ZodPromise", + ZodBranded = "ZodBranded", + ZodPipeline = "ZodPipeline", + ZodReadonly = "ZodReadonly" +} +export type ZodFirstPartySchemaTypes = ZodString | ZodNumber | ZodNaN | ZodBigInt | ZodBoolean | ZodDate | ZodUndefined | ZodNull | ZodAny | ZodUnknown | ZodNever | ZodVoid | ZodArray | ZodObject | ZodUnion | ZodDiscriminatedUnion | ZodIntersection | ZodTuple | ZodRecord | ZodMap | ZodSet | ZodFunction | ZodLazy | ZodLiteral | ZodEnum | ZodEffects | ZodNativeEnum | ZodOptional | ZodNullable | ZodDefault | ZodCatch | ZodPromise | ZodBranded | ZodPipeline | ZodReadonly | ZodSymbol; +declare abstract class Class { + constructor(..._: any[]); +} +declare const instanceOfType: (cls: T, params?: CustomParams) => ZodType, ZodTypeDef, InstanceType>; +declare const stringType: (params?: RawCreateParams & { + coerce?: true; +}) => ZodString; +declare const numberType: (params?: RawCreateParams & { + coerce?: boolean; +}) => ZodNumber; +declare const nanType: (params?: RawCreateParams) => ZodNaN; +declare const bigIntType: (params?: RawCreateParams & { + coerce?: boolean; +}) => ZodBigInt; +declare const booleanType: (params?: RawCreateParams & { + coerce?: boolean; +}) => ZodBoolean; +declare const dateType: (params?: RawCreateParams & { + coerce?: boolean; +}) => ZodDate; +declare const symbolType: (params?: RawCreateParams) => ZodSymbol; +declare const undefinedType: (params?: RawCreateParams) => ZodUndefined; +declare const nullType: (params?: RawCreateParams) => ZodNull; +declare const anyType: (params?: RawCreateParams) => ZodAny; +declare const unknownType: (params?: RawCreateParams) => ZodUnknown; +declare const neverType: (params?: RawCreateParams) => ZodNever; +declare const voidType: (params?: RawCreateParams) => ZodVoid; +declare const arrayType: (schema: El, params?: RawCreateParams) => ZodArray; +declare const objectType: (shape: Shape, params?: RawCreateParams) => ZodObject, objectInputType>; +declare const strictObjectType: (shape: Shape, params?: RawCreateParams) => ZodObject; +declare const unionType: >(types: Options, params?: RawCreateParams) => ZodUnion; +declare const discriminatedUnionType: typeof ZodDiscriminatedUnion.create; +declare const intersectionType: (left: TSchema, right: USchema, params?: RawCreateParams) => ZodIntersection; +declare const tupleType: (schemas: Items, params?: RawCreateParams) => ZodTuple; +declare const recordType: typeof ZodRecord.create; +declare const mapType: (keyType: KeySchema, valueType: ValueSchema, params?: RawCreateParams) => ZodMap; +declare const setType: (valueType: ValueSchema, params?: RawCreateParams) => ZodSet; +declare const functionType: typeof ZodFunction.create; +declare const lazyType: (getter: () => Inner, params?: RawCreateParams) => ZodLazy; +declare const literalType: (value: Value, params?: RawCreateParams) => ZodLiteral; +declare const enumType: typeof createZodEnum; +declare const nativeEnumType: (values: Elements, params?: RawCreateParams) => ZodNativeEnum; +declare const promiseType: (schema: Inner, params?: RawCreateParams) => ZodPromise; +declare const effectsType: (schema: I, effect: Effect, params?: RawCreateParams) => ZodEffects; +declare const optionalType: (type: Inner, params?: RawCreateParams) => ZodOptional; +declare const nullableType: (type: Inner, params?: RawCreateParams) => ZodNullable; +declare const preprocessType: (preprocess: (arg: unknown, ctx: RefinementCtx) => unknown, schema: I, params?: RawCreateParams) => ZodEffects; +declare const pipelineType: typeof ZodPipeline.create; +declare const ostring: () => ZodOptional; +declare const onumber: () => ZodOptional; +declare const oboolean: () => ZodOptional; +export declare const coerce: { + string: (typeof ZodString)["create"]; + number: (typeof ZodNumber)["create"]; + boolean: (typeof ZodBoolean)["create"]; + bigint: (typeof ZodBigInt)["create"]; + date: (typeof ZodDate)["create"]; +}; +export { anyType as any, arrayType as array, bigIntType as bigint, booleanType as boolean, dateType as date, discriminatedUnionType as discriminatedUnion, effectsType as effect, enumType as enum, functionType as function, instanceOfType as instanceof, intersectionType as intersection, lazyType as lazy, literalType as literal, mapType as map, nanType as nan, nativeEnumType as nativeEnum, neverType as never, nullType as null, nullableType as nullable, numberType as number, objectType as object, oboolean, onumber, optionalType as optional, ostring, pipelineType as pipeline, preprocessType as preprocess, promiseType as promise, recordType as record, setType as set, strictObjectType as strictObject, stringType as string, symbolType as symbol, effectsType as transformer, tupleType as tuple, undefinedType as undefined, unionType as union, unknownType as unknown, voidType as void, }; +export declare const NEVER: never; diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/package.json b/tests/fixtures/npm/cases/zod3/node_modules/zod/package.json new file mode 100644 index 000000000..e02b87614 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/package.json @@ -0,0 +1,120 @@ +{ + "name": "zod", + "version": "3.25.67", + "type": "module", + "author": "Colin McDonnell ", + "description": "TypeScript-first schema declaration and validation library with static type inference", + "files": ["dist", "v3", "v4", "v4-mini"], + "funding": "https://github.com/sponsors/colinhacks", + "homepage": "https://zod.dev", + "keywords": ["typescript", "schema", "validation", "type", "inference"], + "license": "MIT", + "sideEffects": false, + "main": "./dist/cjs/index.js", + "types": "./dist/types/index.d.ts", + "module": "./dist/esm/index.js", + "exports": { + ".": { + "import": { + "@zod/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "@zod/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "default": "./dist/cjs/index.js" + } + }, + "./v3": { + "import": { + "@zod/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "@zod/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "default": "./dist/cjs/index.js" + } + }, + "./v4": { + "import": { + "@zod/source": "./src/v4/index.ts", + "types": "./dist/types/v4/index.d.ts", + "default": "./dist/esm/v4/index.js" + }, + "require": { + "@zod/source": "./src/v4/index.ts", + "types": "./dist/types/v4/index.d.ts", + "default": "./dist/cjs/v4/index.js" + } + }, + "./v4-mini": { + "@zod/source": "./src/v4/mini/index.ts", + "types": "./dist/types/v4/mini/index.d.ts", + "import": "./dist/esm/v4/mini/index.js", + "require": "./dist/cjs/v4/mini/index.js" + }, + "./v4/mini": { + "@zod/source": "./src/v4/mini/index.ts", + "types": "./dist/types/v4/mini/index.d.ts", + "import": "./dist/esm/v4/mini/index.js", + "require": "./dist/cjs/v4/mini/index.js" + }, + "./v4/core": { + "@zod/source": "./src/v4/core/index.ts", + "types": "./dist/types/v4/core/index.d.ts", + "import": "./dist/esm/v4/core/index.js", + "require": "./dist/cjs/v4/core/index.js" + }, + "./v4/locales": { + "@zod/source": "./src/v4/locales/index.ts", + "types": "./dist/types/v4/locales/index.d.ts", + "import": "./dist/esm/v4/locales/index.js", + "require": "./dist/cjs/v4/locales/index.js" + }, + "./v4/locales/*": { + "@zod/source": "./src/v4/locales/*", + "types": "./dist/types/v4/locales/*", + "import": "./dist/esm/v4/locales/*", + "require": "./dist/cjs/v4/locales/*" + }, + "./package.json": "./package.json" + }, + "tshy": { + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./v3": "./src/v3/index.ts", + "./v4": "./src/v4/index.ts", + "./v4-mini": "./src/v4/mini/index.ts", + "./v4/core": "./src/v4/core/index.ts", + "./v4/locales": "./src/v4/locales/index.ts", + "./v4/locales/*": "./src/v4/locales/*" + }, + "sourceDialects": ["@zod/source"], + "exclude": ["./src/**/*.test.ts"] + }, + "repository": { + "type": "git", + "url": "git+https://github.com/colinhacks/zod.git" + }, + "bugs": { + "url": "https://github.com/colinhacks/zod/issues" + }, + "support": { + "backing": { + "npm-funding": true + } + }, + "scripts": { + "clean": "rm -rf dist", + "build": "tsx ./build.mts", + "postbuild": "pnpm biome check --write .", + "test:watch": "pnpm vitest", + "test": "pnpm vitest run", + "bump:beta": "pnpm version \"v$(pnpm pkg get version | jq -r)-beta.$(date +%Y%m%dT%H%M%S)\"", + "pub:beta": "pnpm bump:beta && pnpm publish --tag next --publish-branch v4 --no-git-checks --dry-run" + } +} diff --git a/tests/fixtures/npm/cases/zod3/package.json b/tests/fixtures/npm/cases/zod3/package.json new file mode 100644 index 000000000..cda6a6168 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/package.json @@ -0,0 +1 @@ +{ "name": "zod3-case", "private": true, "type": "module" } diff --git a/tests/fixtures/npm/node_modules/jsdocmw/index.d.ts b/tests/fixtures/npm/node_modules/jsdocmw/index.d.ts new file mode 100644 index 000000000..1c93f3348 --- /dev/null +++ b/tests/fixtures/npm/node_modules/jsdocmw/index.d.ts @@ -0,0 +1,18 @@ +export interface Env { + Variables?: Record; +} +export interface Ctx { + path: string; + token: string; + log: string[]; + env?: E; +} +export type Middleware = (ctx: Ctx, next: () => Promise) => Promise; +export interface GuardOptions { + token: string; +} +export declare const guard: (options: GuardOptions) => Middleware; +export declare class Router { + on(path: string, ...handlers: Middleware[]): this; + dispatch(path: string, token: string): Promise; +} diff --git a/tests/fixtures/npm/node_modules/jsdocmw/index.js b/tests/fixtures/npm/node_modules/jsdocmw/index.js new file mode 100644 index 000000000..f1e38383b --- /dev/null +++ b/tests/fixtures/npm/node_modules/jsdocmw/index.js @@ -0,0 +1,43 @@ +/** + * Guard middleware. + * + * @template E - The environment type. + * @param {GuardOptions} options - The options. + * @returns {Middleware} The middleware. + */ +export const guard = (options) => { + return async function guard(ctx, next) { + if (ctx.token !== options.token) { + ctx.log.push(`denied ${ctx.path}`); + return; + } + ctx.log.push(`allowed ${ctx.path}`); + await next(); + }; +}; + +export class Router { + constructor() { + this.routes = []; + } + + on(path, ...handlers) { + this.routes.push({ path, handlers }); + return this; + } + + async dispatch(path, token) { + const log = []; + for (const route of this.routes) { + if (route.path !== path) continue; + const ctx = { path, token, log }; + let index = 0; + const next = async () => { + const handler = route.handlers[index++]; + if (handler) await handler(ctx, next); + }; + await next(); + } + return log; + } +} diff --git a/tests/fixtures/npm/node_modules/jsdocmw/package.json b/tests/fixtures/npm/node_modules/jsdocmw/package.json new file mode 100644 index 000000000..a3283642f --- /dev/null +++ b/tests/fixtures/npm/node_modules/jsdocmw/package.json @@ -0,0 +1,8 @@ +{ + "name": "jsdocmw", + "version": "1.0.0", + "description": "a bundled middleware factory whose JSDoc @template result names a type only its .d.ts declares (hono's bearerAuth shape)", + "type": "module", + "main": "index.js", + "types": "index.d.ts" +} diff --git a/tests/fixtures/split-tools/symlink/outside/linked.ts b/tests/fixtures/split-tools/symlink/outside/linked.ts new file mode 100644 index 000000000..7e551bf2e --- /dev/null +++ b/tests/fixtures/split-tools/symlink/outside/linked.ts @@ -0,0 +1,2 @@ +// A file outside the checkout that the split tools must not read or write. +export const outside = 1; diff --git a/tests/fixtures/split-tools/symlink/repo/src/host.ts b/tests/fixtures/split-tools/symlink/repo/src/host.ts new file mode 100644 index 000000000..4bf39c941 --- /dev/null +++ b/tests/fixtures/split-tools/symlink/repo/src/host.ts @@ -0,0 +1,6 @@ +export class Host { + count = 0; + moved(): number { + return this.count + 1; + } +} diff --git a/tests/fixtures/split-tools/symlink/repo/src/linked.ts b/tests/fixtures/split-tools/symlink/repo/src/linked.ts new file mode 120000 index 000000000..3a9e39fbe --- /dev/null +++ b/tests/fixtures/split-tools/symlink/repo/src/linked.ts @@ -0,0 +1 @@ +../../outside/linked.ts \ No newline at end of file diff --git a/tests/harness/__snapshots__/arguments-length-self-call.ts.txt b/tests/harness/__snapshots__/arguments-length-self-call.ts.txt new file mode 100644 index 000000000..7e94b56c6 --- /dev/null +++ b/tests/harness/__snapshots__/arguments-length-self-call.ts.txt @@ -0,0 +1,6 @@ +arguments-length-self-call.ts:6:32 - error SC1090: 'arguments.length' in a function expression that is called both through its own name and through a function type with more parameters (the count varies per call) are not supported yet + + 5 | const count: (a: number, b: number) => number = function self(a: number): number { + 6 | return a > 0 ? self(a - 1) + arguments.length : arguments.length; + | ^~~~~~~~~~~~~~~~ + 7 | }; \ No newline at end of file diff --git a/tests/harness/__snapshots__/child-process.ts.txt b/tests/harness/__snapshots__/child-process.ts.txt index f5f9f8f86..a1f2999b7 100644 --- a/tests/harness/__snapshots__/child-process.ts.txt +++ b/tests/harness/__snapshots__/child-process.ts.txt @@ -14,7 +14,7 @@ child-process.ts:27:14 - error SC1090: listeners returning a value (make the cal | ^~~~~~~~~~~~~~~ 28 | // Methods have no bound-value form — call on directly. -child-process.ts:29:7 - error SC2007: values of type '{ (event: "spawn", listener: () => void): void; (event: "exit", listener: (code: number | null, signal: string | null) => void): void; (event: "close", listener: (code: number | null, signal: string | null) => void): void; (event: "error", listener: (err: Error) => void): void; (event: "message", listener: (message:...' cannot be compiled: the type declares multiple call signatures (overloads), and a compiled function value is always one concrete signature +child-process.ts:29:7 - error SC2007: values of type '{ (event: "spawn", listener: () => void): ChildProcess; (event: "exit", listener: (code: number | null, signal: string | null) => void): ChildProcess; (event: "close", listener: (code: number | null, signal: string | null) => void): ChildProcess; (event: "error", listener: (err: Error) => void): ChildProcess; (event...' cannot be compiled: the type declares multiple call signatures (overloads), and a compiled function value is always one concrete signature 28 | // Methods have no bound-value form — call on directly. 29 | const f = c.on; diff --git a/tests/harness/__snapshots__/class-call-shadowed-static.ts.txt b/tests/harness/__snapshots__/class-call-shadowed-static.ts.txt new file mode 100644 index 000000000..e04c8423a --- /dev/null +++ b/tests/harness/__snapshots__/class-call-shadowed-static.ts.txt @@ -0,0 +1,8 @@ +class-call-shadowed-static.ts:13:10 - error SC2020: 'typeof Base.call' is part of the standard library types but has no scriptc lowering yet + + 12 | function callThrough(C: typeof Base): unknown { + 13 | return C.call(new Base()); + | ^~~~~~ + 14 | } + + 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__/coverage-divergences.txt b/tests/harness/__snapshots__/coverage-divergences.txt index 66373655f..8ab286873 100644 --- a/tests/harness/__snapshots__/coverage-divergences.txt +++ b/tests/harness/__snapshots__/coverage-divergences.txt @@ -1,7 +1,7 @@ scriptc coverage tests/coverage-fixtures/divergences.ts - statements analyzed 18 - compile statically 18 (100%) + statements analyzed 20 + compile statically 20 (100%) in dynamic tier 1 (operate on values typed only at runtime; --explain-dynamic lists them) fully static — all analyzed statements compile to native code. @@ -14,11 +14,11 @@ scriptc coverage tests/coverage-fixtures/divergences.ts at tests/coverage-fixtures/divergences.ts:20:23 hint: declare the parameter or binding with the value's full type ('Tagged'), or make the shape a class — class instances keep their identity when passed as a base class ×1 SC6002 this object's keys enumerate as 'x, y' natively (the declaration order of 'Point') but as 'y, x' under Node (insertion order), and the program observes the order (JSON.stringify, Object.keys, for-in … - at tests/coverage-fixtures/divergences.ts:29:22 + at tests/coverage-fixtures/divergences.ts:31:22 hint: write the properties in the declaration order of 'Point' (or reorder the declaration to match how objects are built) ×1 SC6003 localeCompare over 'яблоко' compares characters outside the built-in collation (Latin letters, digits, punctuation and symbols) by code point natively, while Node applies ICU's full collation - at tests/coverage-fixtures/divergences.ts:33:70 + at tests/coverage-fixtures/divergences.ts:37:70 hint: results agree with Node for Latin-script text; for other scripts compare explicitly (for example by code point with < and >) so both runtimes agree ×1 SC6004 this 'Circle' reaches a cast to the unrelated class 'Square' in the callee: natively the checked cast throws a TypeError when it executes, while Node reads the members structurally - at tests/coverage-fixtures/divergences.ts:48:20 + at tests/coverage-fixtures/divergences.ts:52:20 hint: narrow with 'instanceof Square' before casting, or cast to a base class or interface both classes share \ No newline at end of file diff --git a/tests/harness/__snapshots__/coverage-js-builtin-callable-alias.txt b/tests/harness/__snapshots__/coverage-js-builtin-callable-alias.txt index 53eac6c3c..6291c291f 100644 --- a/tests/harness/__snapshots__/coverage-js-builtin-callable-alias.txt +++ b/tests/harness/__snapshots__/coverage-js-builtin-callable-alias.txt @@ -3,7 +3,7 @@ scriptc coverage tests/coverage-fixtures/js-builtin-callable-alias.js statements analyzed 3 compile statically 2 (66%) - deferred to runtime 1 site (JS statements that throw their fence if executed) + runs at startup 1 site (JS statements in module top-level code, which runs when the module loads) ×1 SC1090 a builtin function alias as an escaping value (call 'ext' directly) at tests/coverage-fixtures/js-builtin-callable-alias.js:8:13 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) diff --git a/tests/harness/__snapshots__/destructuring-fences.ts.txt b/tests/harness/__snapshots__/destructuring-fences.ts.txt index 743bb5657..41026a7c8 100644 --- a/tests/harness/__snapshots__/destructuring-fences.ts.txt +++ b/tests/harness/__snapshots__/destructuring-fences.ts.txt @@ -19,18 +19,18 @@ destructuring-fences.ts:31:9 - error SC1090: reading a property that has only a | ^~~~~~ 32 | console.log(onlyIn); -destructuring-fences.ts:45:9 - error SC1031: object destructuring of non-record values (the source is union-typed) are not supported yet +destructuring-fences.ts:45:11 - error SC1031: destructuring 'b' from a union whose arms type it differently (every arm must be a record or class with a same-typed field 'b' — narrow first: check a discriminant field, or compare with '!== undefined'/'!== null' for unit arms) is not supported yet 44 | function fromUnion(u: E1 | E2): string { - 45 | const { b } = u; // union source: narrow first - | ^~~~~ - 46 | return b; + 45 | const { b } = u; // union source whose arms type the field differently: narrow first + | ^ + 46 | return String(b); -destructuring-fences.ts:46:10 - error SC2004: uses of 'b' inherit the blocker on its declaration +destructuring-fences.ts:46:17 - error SC2004: uses of 'b' inherit the blocker on its declaration - 45 | const { b } = u; // union source: narrow first - 46 | return b; - | ^ + 45 | const { b } = u; // union source whose arms type the field differently: narrow first + 46 | return String(b); + | ^ 47 | } hint: the declaration of 'b' did not compile — fix the diagnostic reported there and these sites clear with it diff --git a/tests/harness/__snapshots__/filter-narrow.ts.txt b/tests/harness/__snapshots__/filter-narrow.ts.txt deleted file mode 100644 index bc5180219..000000000 --- a/tests/harness/__snapshots__/filter-narrow.ts.txt +++ /dev/null @@ -1,20 +0,0 @@ -filter-narrow.ts:9:17 - error SC1090: '.filter' narrowing 'number | string | undefined' elements to the multi-arm 'number | string' (only a SINGLE arm re-tags — keep the receiver's element type instead — annotate the callback's return ': boolean' (the checker then skips the predicate) or annotate the result with the receiver's own element type — and narrow the elements after) is not supported yet - - 8 | // Multi-arm narrowing: string | number is not a single arm. - 9 | const defined = xs.filter((x) => x !== undefined); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 10 | console.log(defined.length); - -filter-narrow.ts:14:27 - error SC1090: narrowing '.filter' with a hand-written type predicate (a written 'x is T' is an unchecked assertion nothing validates at runtime — keep the receiver's element type instead — annotate the callback's return ': boolean' (the checker then skips the predicate) or annotate the result with the receiver's own element type — and narrow the elements after) is not supported yet - - 13 | const ys: (string | undefined)[] = ["a", undefined]; - 14 | const strings = ys.filter((x): x is string => typeof x === "string"); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 15 | console.log(strings.length); - -filter-narrow.ts:21:28 - error SC1090: narrowing '.filter' through a callback VALUE (only an inline callback whose predicate the checker inferred can re-tag — keep the receiver's element type instead — annotate the callback's return ': boolean' (the checker then skips the predicate) or annotate the result with the receiver's own element type — and narrow the elements after) is not supported yet - - 20 | } - 21 | const viaValue = ys.filter(isString); - | ^~~~~~~~ - 22 | console.log(viaValue.length); \ No newline at end of file diff --git a/tests/harness/__snapshots__/fromentries-aliased-entries.ts.txt b/tests/harness/__snapshots__/fromentries-aliased-entries.ts.txt deleted file mode 100644 index 7b0f02266..000000000 --- a/tests/harness/__snapshots__/fromentries-aliased-entries.ts.txt +++ /dev/null @@ -1,8 +0,0 @@ -fromentries-aliased-entries.ts:8:16 - error SC2002: record shapes must match exactly or width-coerce: expected '{ a: number[]; b: number[] }', got '{ [key: string]: number[] }' — 'a' is not a declared field of the source, and the source's index signature could hold it at runtime (a completed undefined would drop that value) - - 7 | const shared = [10]; - 8 | const scores = Object.fromEntries([ - | ^~~~~~~~~~~~~~~~~~~~ - 9 | ["a", shared], - - hint: width subtyping compiles as a copy (each expected field copied off the source; extra fields drop) — this pair is outside the copy's rules; build a literal with exactly the expected fields instead \ No newline at end of file diff --git a/tests/harness/__snapshots__/iterable-sources.ts.txt b/tests/harness/__snapshots__/iterable-sources.ts.txt new file mode 100644 index 000000000..cac17a49e --- /dev/null +++ b/tests/harness/__snapshots__/iterable-sources.ts.txt @@ -0,0 +1,13 @@ +iterable-sources.ts:15:17 - error SC1090: an Iterable holding a 'Countdown' (an Iterable value holds an array, Set, Map, string, or iterator) is not supported yet + + 14 | } + 15 | console.log(sum(new Countdown())); + | ^~~~~~~~~~~~~~~ + 16 | console.log(sum(new Uint8Array([1, 2]))); + +iterable-sources.ts:16:17 - error SC1090: an Iterable holding a 'Uint8Array' (an Iterable value holds an array, Set, Map, string, or iterator) is not supported yet + + 15 | console.log(sum(new Countdown())); + 16 | console.log(sum(new Uint8Array([1, 2]))); + | ^~~~~~~~~~~~~~~~~~~~~~ + 17 | export const marker: number = 1; \ No newline at end of file diff --git a/tests/harness/__snapshots__/iterator-slot-fences.ts.txt b/tests/harness/__snapshots__/iterator-slot-fences.ts.txt index fd244e8fd..a29b4b186 100644 --- a/tests/harness/__snapshots__/iterator-slot-fences.ts.txt +++ b/tests/harness/__snapshots__/iterator-slot-fences.ts.txt @@ -3,22 +3,4 @@ iterator-slot-fences.ts:14:9 - error SC1090: 'Countdown' values in an iterator-t 13 | } 14 | firstOf(new Countdown()); | ^~~~~~~~~~~~~~~ - 15 | - -iterator-slot-fences.ts:16:16 - error SC2020: 'Iterable' is part of the standard library types but has no scriptc lowering yet - - 15 | - 16 | function total(values: Iterable): number { - | ^~~~~~ - 17 | let sum = 0; - - hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) - -iterator-slot-fences.ts:21:13 - error SC2004: uses of 'total' inherit the blocker on its declaration - - 20 | } - 21 | console.log(total([1, 2])); - | ^~~~~ - 22 | - - hint: the declaration of 'total' did not compile — fix the diagnostic reported there and these sites clear with it \ No newline at end of file + 15 | \ No newline at end of file diff --git a/tests/harness/__snapshots__/local-class-boundaries.ts.txt b/tests/harness/__snapshots__/local-class-boundaries.ts.txt index 6706a1278..697b1b8ed 100644 --- a/tests/harness/__snapshots__/local-class-boundaries.ts.txt +++ b/tests/harness/__snapshots__/local-class-boundaries.ts.txt @@ -2,18 +2,11 @@ local-class-boundaries.ts:1:34 - error SC1090: function-local classes with priva 1 | function privateBrand() { return class { #value = 1; has(other: object): boolean { return #value in other; } }; } | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 2 | function selfName() { return class Local { copy() { return new Local(); } }; } + 2 | class Base {} -local-class-boundaries.ts:2:64 - error SC1090: named self references inside function-local classes are not supported yet +local-class-boundaries.ts:4:29 - error SC1090: function-local classes with statics, decorators, or generic members are not supported yet - 1 | function privateBrand() { return class { #value = 1; has(other: object): boolean { return #value in other; } }; } - 2 | function selfName() { return class Local { copy() { return new Local(); } }; } - | ^~~~~ - 3 | class Base {} - -local-class-boundaries.ts:5:29 - error SC1090: function-local classes with statics, decorators, or generic members are not supported yet - - 4 | function derived() { return class extends Base {}; } - 5 | function generic() { return class { read(value: T): T { return value; } }; } + 3 | function derived() { return class extends Base {}; } + 4 | function generic() { return class { read(value: T): T { return value; } }; } | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 6 | privateBrand(); \ No newline at end of file + 5 | privateBrand(); \ No newline at end of file diff --git a/tests/harness/__snapshots__/new-target-late-local-subclass.ts.txt b/tests/harness/__snapshots__/new-target-late-local-subclass.ts.txt new file mode 100644 index 000000000..feb663813 --- /dev/null +++ b/tests/harness/__snapshots__/new-target-late-local-subclass.ts.txt @@ -0,0 +1,6 @@ +new-target-late-local-subclass.ts:7:18 - error SC1090: 'new.target' where 'Local' is a function-local class (each evaluation creates a new class) is not supported yet + + 6 | constructor() { + 7 | this.label = new.target.name; + | ^~~~~~~~~~ + 8 | } \ No newline at end of file diff --git a/tests/harness/__snapshots__/new-target-local-subclass.ts.txt b/tests/harness/__snapshots__/new-target-local-subclass.ts.txt new file mode 100644 index 000000000..0f1d465b1 --- /dev/null +++ b/tests/harness/__snapshots__/new-target-local-subclass.ts.txt @@ -0,0 +1,6 @@ +new-target-local-subclass.ts:3:39 - error SC1090: 'new.target' where 'Local' is a function-local class (each evaluation creates a new class) is not supported yet + + 2 | // base constructor's new.target has no static dispatch. + 3 | class A { constructor() { console.log(new.target.name); } } + | ^~~~~~~~~~ + 4 | function make() { class Local extends A {} return new Local(); } \ No newline at end of file diff --git a/tests/harness/__snapshots__/new-target-plain-function.ts.txt b/tests/harness/__snapshots__/new-target-plain-function.ts.txt new file mode 100644 index 000000000..5005f5915 --- /dev/null +++ b/tests/harness/__snapshots__/new-target-plain-function.ts.txt @@ -0,0 +1,6 @@ +new-target-plain-function.ts:2:41 - error SC1090: 'new.target' in a plain function (a constructible function's invocation record does not exist in compiled code; use a class constructor) is not supported yet + + 1 | // new.target in a plain function has no invocation record to read. + 2 | function F(this: unknown) { console.log(new.target === undefined); } + | ^~~~~~~~~~ + 3 | F.call(null); \ No newline at end of file diff --git a/tests/harness/__snapshots__/new-target-value-generic-subclass.ts.txt b/tests/harness/__snapshots__/new-target-value-generic-subclass.ts.txt new file mode 100644 index 000000000..0e5cf2e52 --- /dev/null +++ b/tests/harness/__snapshots__/new-target-value-generic-subclass.ts.txt @@ -0,0 +1,6 @@ +new-target-value-generic-subclass.ts:3:58 - error SC1090: 'new.target' as a value where 'B' is a generic subclass (its instantiations have no common class value; read 'new.target.name' or compare 'new.target' with a class instead) is not supported yet + + 2 | // instantiations have no common class value. + 3 | class A { readonly t: typeof A; constructor() { this.t = new.target; } } + | ^~~~~~~~~~ + 4 | class B extends A { readonly v: T | undefined = undefined; } \ No newline at end of file diff --git a/tests/harness/__snapshots__/new-target-value-signatures.ts.txt b/tests/harness/__snapshots__/new-target-value-signatures.ts.txt new file mode 100644 index 000000000..9c38e4d51 --- /dev/null +++ b/tests/harness/__snapshots__/new-target-value-signatures.ts.txt @@ -0,0 +1,6 @@ +new-target-value-signatures.ts:3:58 - error SC1090: 'new.target' as a value where 'B' constructs with a different signature than 'A' (read 'new.target.name' or compare 'new.target' with a class instead) is not supported yet + + 2 | // base's signature; B's constructor takes a parameter A's does not. + 3 | class A { readonly t: typeof A; constructor() { this.t = new.target; } } + | ^~~~~~~~~~ + 4 | class B extends A { constructor(readonly n: number) { super(); } } \ 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 133978b5e..cabce49c4 100644 --- a/tests/harness/__snapshots__/node-types-fenced.txt +++ b/tests/harness/__snapshots__/node-types-fenced.txt @@ -117,7 +117,7 @@ fenced.ts:69:71 - error SC2020: 'createSecureServer option 'streamResetBurst' wi fenced.ts:76:1 - error SC2020: 'crypto.publicEncrypt' is typed by @types/node but has no scriptc lowering yet - 75 | import { checkPrimeSync, createCipheriv, publicEncrypt, setFips } from "node:crypto"; + 75 | import { createCipheriv, publicEncrypt, secureHeapUsed, setFips } from "node:crypto"; 76 | publicEncrypt("key", Buffer.alloc(1)); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 77 | createCipheriv("aes-128-ccm", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); @@ -129,22 +129,22 @@ fenced.ts:77:16 - error SC2020: 'crypto.createCipheriv with cipher 'aes-128-ccm' 76 | publicEncrypt("key", Buffer.alloc(1)); 77 | createCipheriv("aes-128-ccm", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); | ^~~~~~~~~~~~~ - 78 | checkPrimeSync(7n); + 78 | secureHeapUsed(); - hint: the supported ciphers are aes-128-cbc, aes-192-cbc, aes-256-cbc, aes-128-ctr, aes-192-ctr, aes-256-ctr, aes-128-gcm, aes-192-gcm, aes-256-gcm, and chacha20-poly1305 + hint: the supported ciphers are aes-128, aes-192 and aes-256 in cbc, ctr, gcm, ecb, cfb, cfb8, cfb1 and ofb modes, des-ede3 in cbc, ecb, cfb, cfb8, cfb1 and ofb modes, des-ede in cbc, ecb, cfb and ofb modes, chacha20, and chacha20-poly1305 -fenced.ts:78:1 - error SC2020: 'crypto.checkPrimeSync' is typed by @types/node but has no scriptc lowering yet +fenced.ts:78:1 - error SC2020: 'crypto.secureHeapUsed' is typed by @types/node but has no scriptc lowering yet 77 | createCipheriv("aes-128-ccm", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); - 78 | checkPrimeSync(7n); - | ^~~~~~~~~~~~~~~~~~ + 78 | secureHeapUsed(); + | ^~~~~~~~~~~~~~~~ 79 | setFips(false); hint: the type checker sees everything @types/node declares, but only the supported surface compiles (https://scriptc.dev/limitations) fenced.ts:79:1 - error SC2020: 'crypto.setFips' is typed by @types/node but has no scriptc lowering yet - 78 | checkPrimeSync(7n); + 78 | secureHeapUsed(); 79 | setFips(false); | ^~~~~~~~~~~~~~ 80 | fetch("https://example.invalid/", { diff --git a/tests/harness/__snapshots__/retired-engine-1123-any-conditional-spread.ts.txt b/tests/harness/__snapshots__/retired-engine-1123-any-conditional-spread.ts.txt deleted file mode 100644 index 707d1dfd9..000000000 --- a/tests/harness/__snapshots__/retired-engine-1123-any-conditional-spread.ts.txt +++ /dev/null @@ -1,6 +0,0 @@ -retired-engine-1123-any-conditional-spread.ts:21:21 - error SC1090: conditional spreads beyond `...(c ? { field: v } : {})` (exactly one property against an empty arm — spell other shapes as optional fields) is not supported yet - - 20 | function multi(on: boolean): any { - 21 | return { base: 1, ...(on ? { x: "X", y: "Y" } : {}) }; - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 22 | } \ No newline at end of file diff --git a/tests/harness/__snapshots__/spread-fences.ts.txt b/tests/harness/__snapshots__/spread-fences.ts.txt index 56f8b81ee..eaced4979 100644 --- a/tests/harness/__snapshots__/spread-fences.ts.txt +++ b/tests/harness/__snapshots__/spread-fences.ts.txt @@ -1,4 +1,4 @@ -spread-fences.ts:14:21 - error SC1090: conditional spreads beyond `...(c ? { field: v } : {})` (exactly one property against an empty arm — spell other shapes as optional fields) is not supported yet +spread-fences.ts:14:21 - error SC1090: conditional spreads beyond `...(c ? { field: v } : {})` and `...(c ? record : {})` (one arm must be empty — spell other shapes as optional fields) is not supported yet 13 | const cond = true; 14 | const s1: Shape = { ...(cond ? { x: 1, y: 2 } : { x: 3, y: 4 }) }; diff --git a/tests/harness/atomics-emission.test.ts b/tests/harness/atomics-emission.test.ts index 9ae06d904..2d708e382 100644 --- a/tests/harness/atomics-emission.test.ts +++ b/tests/harness/atomics-emission.test.ts @@ -61,3 +61,32 @@ console.log(work(new Int32Array(buffer), new Uint8Array(buffer), new Int16Array( expect(calls).toHaveLength(9); for (const call of calls) expect(call).toMatch(/\) cold$/); }); + +test("an unchanged local array is borrowed across Atomics operations", async () => { + const llvm = await emittedLlvm( + "borrow", + `function count(buffer: SharedArrayBuffer, n: number): number { + const cells = new Int32Array(buffer); + for (let i = 0; i < n; i++) { + Atomics.add(cells, 0, 1); + Atomics.compareExchange(cells, 1, Atomics.load(cells, 1), i); + } + return Atomics.load(cells, 0); +} +function swap(buffer: SharedArrayBuffer, other: SharedArrayBuffer): number { + let cells = new Int32Array(buffer); + return Atomics.add(cells, ((cells = new Int32Array(other)), 0), 1); +} +const buffer = new SharedArrayBuffer(16); +console.log(count(buffer, 3), swap(buffer, new SharedArrayBuffer(16))); +`, + ); + // The local owns the array across each operation: no reference count + // operations around the atomic instructions. + const counted = body(llvm, "sc_bf_count"); + expect(counted).toMatch(/atomicrmw add ptr/); + expect(counted).not.toMatch(/call ptr @scr_bytes_retain_v/); + // An operand that reassigns the binding releases the old array, so the + // operation keeps its own reference. + expect(body(llvm, "sc_bf_swap")).toMatch(/call ptr @scr_bytes_retain_v/); +}); diff --git a/tests/harness/bootstrap-seed.test.ts b/tests/harness/bootstrap-seed.test.ts index 2e8b3bad9..76b1f6de5 100644 --- a/tests/harness/bootstrap-seed.test.ts +++ b/tests/harness/bootstrap-seed.test.ts @@ -78,6 +78,10 @@ test("CI restores each seed only into its matching instrumented bootstrap lane", const execute = checks.steps.find((step) => step.run?.split("\n").includes("node scripts/ci-native-bootstrap.mjs"), )!; + // The sanitized seed's build needs more memory than the runner has: swap backs it. + const swap = seed.steps.findIndex((step) => step.run?.includes("swapon /mnt/swapfile")); + expect(seed.steps[swap]?.if).toBe("matrix.flavor == 'san'"); + expect(swap).toBeLessThan(seed.steps.indexOf(prepare)); expect(prepare.env?.SCRIPTC_BOOTSTRAP_SEED_DIRECTORY).toBe("${{ runner.temp }}/native-bootstrap"); expect(execute.env?.SCRIPTC_BOOTSTRAP_SEED_DIRECTORY).toBe( prepare.env?.SCRIPTC_BOOTSTRAP_SEED_DIRECTORY, diff --git a/tests/harness/corpus-costs/4638-contract-program-lifetime-arrays.ts.json b/tests/harness/corpus-costs/4638-contract-program-lifetime-arrays.ts.json new file mode 100644 index 000000000..8512b117c --- /dev/null +++ b/tests/harness/corpus-costs/4638-contract-program-lifetime-arrays.ts.json @@ -0,0 +1 @@ +{ "plain": 2800, "san": 4600 } diff --git a/tests/harness/corpus-costs/4639-contract-program-lifetime-maps.ts.json b/tests/harness/corpus-costs/4639-contract-program-lifetime-maps.ts.json new file mode 100644 index 000000000..d12d0c3a4 --- /dev/null +++ b/tests/harness/corpus-costs/4639-contract-program-lifetime-maps.ts.json @@ -0,0 +1 @@ +{ "plain": 2500, "san": 3600 } diff --git a/tests/harness/corpus-costs/4814-integer-or-nan-indexes.ts.json b/tests/harness/corpus-costs/4814-integer-or-nan-indexes.ts.json new file mode 100644 index 000000000..23f4b5b65 --- /dev/null +++ b/tests/harness/corpus-costs/4814-integer-or-nan-indexes.ts.json @@ -0,0 +1 @@ +{"plain":3473,"san":4706} diff --git a/tests/harness/corpus-costs/4815-touint32-wide-magnitudes.ts.json b/tests/harness/corpus-costs/4815-touint32-wide-magnitudes.ts.json new file mode 100644 index 000000000..ee076af6b --- /dev/null +++ b/tests/harness/corpus-costs/4815-touint32-wide-magnitudes.ts.json @@ -0,0 +1 @@ +{"plain":2871,"san":4039} diff --git a/tests/harness/corpus-costs/6349-instanceof-subclass-capsules/main.ts.json b/tests/harness/corpus-costs/6349-instanceof-subclass-capsules/main.ts.json new file mode 100644 index 000000000..6474582a0 --- /dev/null +++ b/tests/harness/corpus-costs/6349-instanceof-subclass-capsules/main.ts.json @@ -0,0 +1 @@ +{ "plain": 1600, "san": 2000 } diff --git a/tests/harness/corpus-costs/6913-builtin-constructor-shorthand.ts.json b/tests/harness/corpus-costs/6913-builtin-constructor-shorthand.ts.json new file mode 100644 index 000000000..3c9170116 --- /dev/null +++ b/tests/harness/corpus-costs/6913-builtin-constructor-shorthand.ts.json @@ -0,0 +1 @@ +{ "plain": 3202, "san": 4630 } diff --git a/tests/harness/corpus-costs/6914-dynamic-builtin-construct.ts.json b/tests/harness/corpus-costs/6914-dynamic-builtin-construct.ts.json new file mode 100644 index 000000000..a3f53b09c --- /dev/null +++ b/tests/harness/corpus-costs/6914-dynamic-builtin-construct.ts.json @@ -0,0 +1 @@ +{ "plain": 3064, "san": 5586 } diff --git a/tests/harness/corpus-costs/6915-dynamic-builtin-construct.mjs.json b/tests/harness/corpus-costs/6915-dynamic-builtin-construct.mjs.json new file mode 100644 index 000000000..0c65d046c --- /dev/null +++ b/tests/harness/corpus-costs/6915-dynamic-builtin-construct.mjs.json @@ -0,0 +1 @@ +{ "plain": 2821, "san": 4440 } diff --git a/tests/harness/corpus-costs/6915-key-order-narrowed-throw.ts.json b/tests/harness/corpus-costs/6915-key-order-narrowed-throw.ts.json new file mode 100644 index 000000000..fae2adeb4 --- /dev/null +++ b/tests/harness/corpus-costs/6915-key-order-narrowed-throw.ts.json @@ -0,0 +1 @@ +{ "plain": 1500, "san": 9000 } diff --git a/tests/harness/corpus-costs/6916-index-record-spread-overrides.ts.json b/tests/harness/corpus-costs/6916-index-record-spread-overrides.ts.json new file mode 100644 index 000000000..fae2adeb4 --- /dev/null +++ b/tests/harness/corpus-costs/6916-index-record-spread-overrides.ts.json @@ -0,0 +1 @@ +{ "plain": 1500, "san": 9000 } diff --git a/tests/harness/corpus-costs/6916-local-class-static-self-reference.mjs.json b/tests/harness/corpus-costs/6916-local-class-static-self-reference.mjs.json new file mode 100644 index 000000000..0d9aa6b13 --- /dev/null +++ b/tests/harness/corpus-costs/6916-local-class-static-self-reference.mjs.json @@ -0,0 +1 @@ +{ "plain": 3135, "san": 4365 } diff --git a/tests/harness/corpus-costs/6917-local-class-expression-self-reference.ts.json b/tests/harness/corpus-costs/6917-local-class-expression-self-reference.ts.json new file mode 100644 index 000000000..2769f5b72 --- /dev/null +++ b/tests/harness/corpus-costs/6917-local-class-expression-self-reference.ts.json @@ -0,0 +1 @@ +{ "plain": 3074, "san": 4624 } diff --git a/tests/harness/corpus-costs/6918-local-class-expression-self-reference.mjs.json b/tests/harness/corpus-costs/6918-local-class-expression-self-reference.mjs.json new file mode 100644 index 000000000..04f903d8a --- /dev/null +++ b/tests/harness/corpus-costs/6918-local-class-expression-self-reference.mjs.json @@ -0,0 +1 @@ +{ "plain": 3050, "san": 4393 } diff --git a/tests/harness/corpus-costs/6919-local-class-inspect-name.mjs.json b/tests/harness/corpus-costs/6919-local-class-inspect-name.mjs.json new file mode 100644 index 000000000..78aea9163 --- /dev/null +++ b/tests/harness/corpus-costs/6919-local-class-inspect-name.mjs.json @@ -0,0 +1 @@ +{ "plain": 3058, "san": 4846 } diff --git a/tests/harness/corpus-costs/6921-local-subclass-override-dispatch.ts.json b/tests/harness/corpus-costs/6921-local-subclass-override-dispatch.ts.json new file mode 100644 index 000000000..0dd7e966b --- /dev/null +++ b/tests/harness/corpus-costs/6921-local-subclass-override-dispatch.ts.json @@ -0,0 +1 @@ +{ "plain": 2459, "san": 4630 } diff --git a/tests/harness/corpus-costs/6922-local-subclass-override-dispatch.mjs.json b/tests/harness/corpus-costs/6922-local-subclass-override-dispatch.mjs.json new file mode 100644 index 000000000..f2ba6c139 --- /dev/null +++ b/tests/harness/corpus-costs/6922-local-subclass-override-dispatch.mjs.json @@ -0,0 +1 @@ +{ "plain": 3216, "san": 5042 } diff --git a/tests/harness/corpus-costs/6923-instanceof-builtin-constructor-values.mjs.json b/tests/harness/corpus-costs/6923-instanceof-builtin-constructor-values.mjs.json new file mode 100644 index 000000000..53f54c1dd --- /dev/null +++ b/tests/harness/corpus-costs/6923-instanceof-builtin-constructor-values.mjs.json @@ -0,0 +1 @@ +{ "plain": 3810, "san": 5921 } diff --git a/tests/harness/corpus-costs/6924-instanceof-class-values-native.mjs.json b/tests/harness/corpus-costs/6924-instanceof-class-values-native.mjs.json new file mode 100644 index 000000000..8de22d657 --- /dev/null +++ b/tests/harness/corpus-costs/6924-instanceof-class-values-native.mjs.json @@ -0,0 +1 @@ +{ "plain": 3033, "san": 4212 } diff --git a/tests/harness/corpus-costs/6925-instanceof-dynamic-constructors.ts.json b/tests/harness/corpus-costs/6925-instanceof-dynamic-constructors.ts.json new file mode 100644 index 000000000..e2d372e0c --- /dev/null +++ b/tests/harness/corpus-costs/6925-instanceof-dynamic-constructors.ts.json @@ -0,0 +1 @@ +{ "plain": 3097, "san": 4707 } diff --git a/tests/harness/corpus-costs/6926-dynamic-collection-seeds.mjs.json b/tests/harness/corpus-costs/6926-dynamic-collection-seeds.mjs.json new file mode 100644 index 000000000..b4a56f5c5 --- /dev/null +++ b/tests/harness/corpus-costs/6926-dynamic-collection-seeds.mjs.json @@ -0,0 +1 @@ +{ "plain": 3813, "san": 3374 } diff --git a/tests/harness/corpus-costs/6927-dynamic-collection-seeds.ts.json b/tests/harness/corpus-costs/6927-dynamic-collection-seeds.ts.json new file mode 100644 index 000000000..93da087d2 --- /dev/null +++ b/tests/harness/corpus-costs/6927-dynamic-collection-seeds.ts.json @@ -0,0 +1 @@ +{ "plain": 2436, "san": 2273 } diff --git a/tests/harness/corpus-costs/6998-typed-array-while-loop-versions.ts.json b/tests/harness/corpus-costs/6998-typed-array-while-loop-versions.ts.json new file mode 100644 index 000000000..204a01a83 --- /dev/null +++ b/tests/harness/corpus-costs/6998-typed-array-while-loop-versions.ts.json @@ -0,0 +1 @@ +{ "plain": 3700, "san": 5000 } diff --git a/tests/harness/corpus-costs/7002-empty-array-contextual-slots/main.ts.json b/tests/harness/corpus-costs/7002-empty-array-contextual-slots/main.ts.json new file mode 100644 index 000000000..9495bb65c --- /dev/null +++ b/tests/harness/corpus-costs/7002-empty-array-contextual-slots/main.ts.json @@ -0,0 +1 @@ +{ "plain": 2282, "san": 1173 } diff --git a/tests/harness/corpus-costs/7005-dyn-constructor-instanceof/main.ts.json b/tests/harness/corpus-costs/7005-dyn-constructor-instanceof/main.ts.json new file mode 100644 index 000000000..f84e21437 --- /dev/null +++ b/tests/harness/corpus-costs/7005-dyn-constructor-instanceof/main.ts.json @@ -0,0 +1 @@ +{ "plain": 2308, "san": 955 } diff --git a/tests/harness/corpus-costs/7006-evolving-array-aliases.ts.json b/tests/harness/corpus-costs/7006-evolving-array-aliases.ts.json new file mode 100644 index 000000000..f34efd332 --- /dev/null +++ b/tests/harness/corpus-costs/7006-evolving-array-aliases.ts.json @@ -0,0 +1 @@ +{ "plain": 2292, "san": 3445 } diff --git a/tests/harness/corpus-costs/7007-filter-written-type-predicates.ts.json b/tests/harness/corpus-costs/7007-filter-written-type-predicates.ts.json new file mode 100644 index 000000000..188fa7fab --- /dev/null +++ b/tests/harness/corpus-costs/7007-filter-written-type-predicates.ts.json @@ -0,0 +1 @@ +{ "plain": 3510, "san": 2873 } diff --git a/tests/harness/corpus-costs/7008-js-unit-initialized-properties/main.ts.json b/tests/harness/corpus-costs/7008-js-unit-initialized-properties/main.ts.json new file mode 100644 index 000000000..3a6df58d2 --- /dev/null +++ b/tests/harness/corpus-costs/7008-js-unit-initialized-properties/main.ts.json @@ -0,0 +1 @@ +{ "plain": 3065, "san": 2623 } diff --git a/tests/harness/corpus-costs/7009-finalization-registry-wasm-loader/main.ts.json b/tests/harness/corpus-costs/7009-finalization-registry-wasm-loader/main.ts.json new file mode 100644 index 000000000..db9fc4168 --- /dev/null +++ b/tests/harness/corpus-costs/7009-finalization-registry-wasm-loader/main.ts.json @@ -0,0 +1 @@ +{ "plain": 4351, "san": 2333 } diff --git a/tests/harness/corpus-costs/7011-for-in-dictionary-deletes.ts.json b/tests/harness/corpus-costs/7011-for-in-dictionary-deletes.ts.json new file mode 100644 index 000000000..e172eed8a --- /dev/null +++ b/tests/harness/corpus-costs/7011-for-in-dictionary-deletes.ts.json @@ -0,0 +1 @@ +{ "plain": 1527, "san": 2135 } diff --git a/tests/harness/corpus-costs/7014-js-never-array-literal-fields/main.ts.json b/tests/harness/corpus-costs/7014-js-never-array-literal-fields/main.ts.json new file mode 100644 index 000000000..c5bf12c38 --- /dev/null +++ b/tests/harness/corpus-costs/7014-js-never-array-literal-fields/main.ts.json @@ -0,0 +1 @@ +{ "plain": 3004, "san": 3359 } diff --git a/tests/harness/corpus-costs/7020-conditional-spread-fields.ts.json b/tests/harness/corpus-costs/7020-conditional-spread-fields.ts.json new file mode 100644 index 000000000..ee564e79f --- /dev/null +++ b/tests/harness/corpus-costs/7020-conditional-spread-fields.ts.json @@ -0,0 +1 @@ +{ "plain": 3640, "san": 5580 } diff --git a/tests/harness/corpus-costs/7021-promise-all-uniform-tuple.ts.json b/tests/harness/corpus-costs/7021-promise-all-uniform-tuple.ts.json new file mode 100644 index 000000000..fba6fd530 --- /dev/null +++ b/tests/harness/corpus-costs/7021-promise-all-uniform-tuple.ts.json @@ -0,0 +1 @@ +{ "plain": 2510, "san": 3630 } diff --git a/tests/harness/corpus-costs/7022-promise-all-dynamic-arrays.ts.json b/tests/harness/corpus-costs/7022-promise-all-dynamic-arrays.ts.json new file mode 100644 index 000000000..c59bb483b --- /dev/null +++ b/tests/harness/corpus-costs/7022-promise-all-dynamic-arrays.ts.json @@ -0,0 +1 @@ +{ "plain": 3330, "san": 4690 } diff --git a/tests/harness/corpus-costs/7023-union-object-patterns.ts.json b/tests/harness/corpus-costs/7023-union-object-patterns.ts.json new file mode 100644 index 000000000..3f0a047e7 --- /dev/null +++ b/tests/harness/corpus-costs/7023-union-object-patterns.ts.json @@ -0,0 +1 @@ +{ "plain": 2440, "san": 3670 } diff --git a/tests/harness/corpus-costs/7024-async-return-fresh-collections.ts.json b/tests/harness/corpus-costs/7024-async-return-fresh-collections.ts.json new file mode 100644 index 000000000..6285a25c5 --- /dev/null +++ b/tests/harness/corpus-costs/7024-async-return-fresh-collections.ts.json @@ -0,0 +1 @@ +{ "plain": 2570, "san": 4360 } diff --git a/tests/harness/corpus-costs/7025-child-listener-chaining.ts.json b/tests/harness/corpus-costs/7025-child-listener-chaining.ts.json new file mode 100644 index 000000000..44843dc06 --- /dev/null +++ b/tests/harness/corpus-costs/7025-child-listener-chaining.ts.json @@ -0,0 +1 @@ +{ "plain": 2510, "san": 3950 } diff --git a/tests/harness/corpus-costs/7026-url-object-patterns.ts.json b/tests/harness/corpus-costs/7026-url-object-patterns.ts.json new file mode 100644 index 000000000..9d3f28ab1 --- /dev/null +++ b/tests/harness/corpus-costs/7026-url-object-patterns.ts.json @@ -0,0 +1 @@ +{ "plain": 2520, "san": 4000 } diff --git a/tests/harness/corpus-costs/7027-search-params-conditional-init.ts.json b/tests/harness/corpus-costs/7027-search-params-conditional-init.ts.json new file mode 100644 index 000000000..56c46a74b --- /dev/null +++ b/tests/harness/corpus-costs/7027-search-params-conditional-init.ts.json @@ -0,0 +1 @@ +{ "plain": 2240, "san": 3440 } diff --git a/tests/harness/corpus-costs/7028-non-empty-rest-tuples.ts.json b/tests/harness/corpus-costs/7028-non-empty-rest-tuples.ts.json new file mode 100644 index 000000000..147f70b9a --- /dev/null +++ b/tests/harness/corpus-costs/7028-non-empty-rest-tuples.ts.json @@ -0,0 +1 @@ +{ "plain": 2520, "san": 4030 } diff --git a/tests/harness/corpus-costs/7029-flatmap-union-results.ts.json b/tests/harness/corpus-costs/7029-flatmap-union-results.ts.json new file mode 100644 index 000000000..7efdd91bf --- /dev/null +++ b/tests/harness/corpus-costs/7029-flatmap-union-results.ts.json @@ -0,0 +1 @@ +{ "plain": 2640, "san": 4710 } diff --git a/tests/harness/corpus-costs/7030-spawn-sync-env.ts.json b/tests/harness/corpus-costs/7030-spawn-sync-env.ts.json new file mode 100644 index 000000000..cf8ef4d26 --- /dev/null +++ b/tests/harness/corpus-costs/7030-spawn-sync-env.ts.json @@ -0,0 +1 @@ +{ "plain": 2460, "san": 4060 } diff --git a/tests/harness/corpus-costs/7033-any-fields-into-declared-records.ts.json b/tests/harness/corpus-costs/7033-any-fields-into-declared-records.ts.json new file mode 100644 index 000000000..1fd3ee053 --- /dev/null +++ b/tests/harness/corpus-costs/7033-any-fields-into-declared-records.ts.json @@ -0,0 +1 @@ +{ "plain": 2640, "san": 3950 } diff --git a/tests/harness/corpus-costs/7034-optional-field-union-routes.ts.json b/tests/harness/corpus-costs/7034-optional-field-union-routes.ts.json new file mode 100644 index 000000000..7e342cfd0 --- /dev/null +++ b/tests/harness/corpus-costs/7034-optional-field-union-routes.ts.json @@ -0,0 +1 @@ +{ "plain": 2420, "san": 3640 } diff --git a/tests/harness/corpus-costs/7036-child-env-path-lookup.ts.json b/tests/harness/corpus-costs/7036-child-env-path-lookup.ts.json new file mode 100644 index 000000000..f9bfce7d9 --- /dev/null +++ b/tests/harness/corpus-costs/7036-child-env-path-lookup.ts.json @@ -0,0 +1 @@ +{ "plain": 2570, "san": 4110 } diff --git a/tests/harness/corpus-costs/7037-js-var-open-writes/main.ts.json b/tests/harness/corpus-costs/7037-js-var-open-writes/main.ts.json new file mode 100644 index 000000000..2e747487d --- /dev/null +++ b/tests/harness/corpus-costs/7037-js-var-open-writes/main.ts.json @@ -0,0 +1 @@ +{ "plain": 1970, "san": 2150 } diff --git a/tests/harness/corpus-costs/7038-js-var-later-writes.mjs.json b/tests/harness/corpus-costs/7038-js-var-later-writes.mjs.json new file mode 100644 index 000000000..29130736b --- /dev/null +++ b/tests/harness/corpus-costs/7038-js-var-later-writes.mjs.json @@ -0,0 +1 @@ +{ "plain": 1570, "san": 2390 } diff --git a/tests/harness/corpus-costs/7060-array-numeric-keys.cjs.json b/tests/harness/corpus-costs/7060-array-numeric-keys.cjs.json new file mode 100644 index 000000000..184d2ea9f --- /dev/null +++ b/tests/harness/corpus-costs/7060-array-numeric-keys.cjs.json @@ -0,0 +1 @@ +{ "plain": 4400, "san": 6000 } diff --git a/tests/harness/corpus-costs/7061-array-object-keys.ts.json b/tests/harness/corpus-costs/7061-array-object-keys.ts.json new file mode 100644 index 000000000..b39c9d973 --- /dev/null +++ b/tests/harness/corpus-costs/7061-array-object-keys.ts.json @@ -0,0 +1 @@ +{ "plain": 4000, "san": 5400 } diff --git a/tests/harness/corpus-costs/7062-inspect-array-properties.ts.json b/tests/harness/corpus-costs/7062-inspect-array-properties.ts.json new file mode 100644 index 000000000..dcd73aea7 --- /dev/null +++ b/tests/harness/corpus-costs/7062-inspect-array-properties.ts.json @@ -0,0 +1 @@ +{ "plain": 4300, "san": 6000 } diff --git a/tests/harness/corpus-costs/7143-optional-read-integer-index.ts.json b/tests/harness/corpus-costs/7143-optional-read-integer-index.ts.json new file mode 100644 index 000000000..16148d8ce --- /dev/null +++ b/tests/harness/corpus-costs/7143-optional-read-integer-index.ts.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 5400 } diff --git a/tests/harness/corpus-costs/7144-paired-while-counters.ts.json b/tests/harness/corpus-costs/7144-paired-while-counters.ts.json new file mode 100644 index 000000000..935249de9 --- /dev/null +++ b/tests/harness/corpus-costs/7144-paired-while-counters.ts.json @@ -0,0 +1 @@ +{ "plain": 3300, "san": 4600 } diff --git a/tests/harness/corpus-costs/7150-cipher-legacy-modes.ts.json b/tests/harness/corpus-costs/7150-cipher-legacy-modes.ts.json new file mode 100644 index 000000000..40196decf --- /dev/null +++ b/tests/harness/corpus-costs/7150-cipher-legacy-modes.ts.json @@ -0,0 +1 @@ +{ "plain": 6099, "san": 8251 } diff --git a/tests/harness/corpus-costs/7151-cipher-legacy-errors.cjs.json b/tests/harness/corpus-costs/7151-cipher-legacy-errors.cjs.json new file mode 100644 index 000000000..6a82dd081 --- /dev/null +++ b/tests/harness/corpus-costs/7151-cipher-legacy-errors.cjs.json @@ -0,0 +1 @@ +{ "plain": 7845, "san": 9581 } diff --git a/tests/harness/corpus-costs/7152-crypto-prime.ts.json b/tests/harness/corpus-costs/7152-crypto-prime.ts.json new file mode 100644 index 000000000..4df2be436 --- /dev/null +++ b/tests/harness/corpus-costs/7152-crypto-prime.ts.json @@ -0,0 +1 @@ +{ "plain": 7273, "san": 10284 } diff --git a/tests/harness/corpus-costs/7153-crypto-prime-errors.cjs.json b/tests/harness/corpus-costs/7153-crypto-prime-errors.cjs.json new file mode 100644 index 000000000..d20dc198a --- /dev/null +++ b/tests/harness/corpus-costs/7153-crypto-prime-errors.cjs.json @@ -0,0 +1 @@ +{ "plain": 5893, "san": 5691 } diff --git a/tests/harness/corpus-costs/7184-reduce-runtime-accumulator.ts.json b/tests/harness/corpus-costs/7184-reduce-runtime-accumulator.ts.json new file mode 100644 index 000000000..c480bea3f --- /dev/null +++ b/tests/harness/corpus-costs/7184-reduce-runtime-accumulator.ts.json @@ -0,0 +1 @@ +{ "plain": 2541, "san": 3260 } diff --git a/tests/harness/corpus-costs/7185-array-callback-arguments-length.ts.json b/tests/harness/corpus-costs/7185-array-callback-arguments-length.ts.json new file mode 100644 index 000000000..b86a9e70a --- /dev/null +++ b/tests/harness/corpus-costs/7185-array-callback-arguments-length.ts.json @@ -0,0 +1 @@ +{ "plain": 2528, "san": 3201 } diff --git a/tests/harness/corpus-costs/7246-stream-construct.ts.json b/tests/harness/corpus-costs/7246-stream-construct.ts.json new file mode 100644 index 000000000..d94320c7e --- /dev/null +++ b/tests/harness/corpus-costs/7246-stream-construct.ts.json @@ -0,0 +1 @@ +{ "plain": 2327, "san": 3064 } diff --git a/tests/harness/corpus-costs/7247-stream-construct.mjs.json b/tests/harness/corpus-costs/7247-stream-construct.mjs.json new file mode 100644 index 000000000..a74da86c8 --- /dev/null +++ b/tests/harness/corpus-costs/7247-stream-construct.mjs.json @@ -0,0 +1 @@ +{ "plain": 2834, "san": 3538 } diff --git a/tests/harness/corpus-costs/7248-stream-writev.ts.json b/tests/harness/corpus-costs/7248-stream-writev.ts.json new file mode 100644 index 000000000..83ccdb03d --- /dev/null +++ b/tests/harness/corpus-costs/7248-stream-writev.ts.json @@ -0,0 +1 @@ +{ "plain": 2294, "san": 3026 } diff --git a/tests/harness/corpus-costs/7249-buffer-index-of-candidates.ts.json b/tests/harness/corpus-costs/7249-buffer-index-of-candidates.ts.json new file mode 100644 index 000000000..5c265ea01 --- /dev/null +++ b/tests/harness/corpus-costs/7249-buffer-index-of-candidates.ts.json @@ -0,0 +1 @@ +{ "plain": 2870, "san": 3656 } diff --git a/tests/harness/corpus-costs/7249-stream-writev.cjs.json b/tests/harness/corpus-costs/7249-stream-writev.cjs.json new file mode 100644 index 000000000..2fc75720e --- /dev/null +++ b/tests/harness/corpus-costs/7249-stream-writev.cjs.json @@ -0,0 +1 @@ +{ "plain": 2640, "san": 2765 } diff --git a/tests/harness/corpus-costs/7250-stream-decode-strings.ts.json b/tests/harness/corpus-costs/7250-stream-decode-strings.ts.json new file mode 100644 index 000000000..b6c0a39a0 --- /dev/null +++ b/tests/harness/corpus-costs/7250-stream-decode-strings.ts.json @@ -0,0 +1 @@ +{ "plain": 2016, "san": 2519 } diff --git a/tests/harness/corpus-costs/7251-stream-write-encodings.mjs.json b/tests/harness/corpus-costs/7251-stream-write-encodings.mjs.json new file mode 100644 index 000000000..7e459d978 --- /dev/null +++ b/tests/harness/corpus-costs/7251-stream-write-encodings.mjs.json @@ -0,0 +1 @@ +{ "plain": 2161, "san": 2661 } diff --git a/tests/harness/corpus-costs/7252-duplex-from-generator.ts.json b/tests/harness/corpus-costs/7252-duplex-from-generator.ts.json new file mode 100644 index 000000000..47f7b9ed9 --- /dev/null +++ b/tests/harness/corpus-costs/7252-duplex-from-generator.ts.json @@ -0,0 +1 @@ +{ "plain": 2522, "san": 3330 } diff --git a/tests/harness/corpus-costs/7253-duplex-from.mjs.json b/tests/harness/corpus-costs/7253-duplex-from.mjs.json new file mode 100644 index 000000000..7743ca7c8 --- /dev/null +++ b/tests/harness/corpus-costs/7253-duplex-from.mjs.json @@ -0,0 +1 @@ +{ "plain": 1849, "san": 2440 } diff --git a/tests/harness/corpus-costs/7254-compose-functions.ts.json b/tests/harness/corpus-costs/7254-compose-functions.ts.json new file mode 100644 index 000000000..19d896024 --- /dev/null +++ b/tests/harness/corpus-costs/7254-compose-functions.ts.json @@ -0,0 +1 @@ +{ "plain": 1782, "san": 2350 } diff --git a/tests/harness/corpus-costs/7255-compose-stages.mjs.json b/tests/harness/corpus-costs/7255-compose-stages.mjs.json new file mode 100644 index 000000000..e8e0ab3d1 --- /dev/null +++ b/tests/harness/corpus-costs/7255-compose-stages.mjs.json @@ -0,0 +1 @@ +{ "plain": 2033, "san": 2680 } diff --git a/tests/harness/corpus-costs/7256-readable-compose.mjs.json b/tests/harness/corpus-costs/7256-readable-compose.mjs.json new file mode 100644 index 000000000..fe1307a1f --- /dev/null +++ b/tests/harness/corpus-costs/7256-readable-compose.mjs.json @@ -0,0 +1 @@ +{ "plain": 2146, "san": 2830 } diff --git a/tests/harness/corpus-costs/7257-pipeline-generator-destination.mjs.json b/tests/harness/corpus-costs/7257-pipeline-generator-destination.mjs.json new file mode 100644 index 000000000..ebaa9214c --- /dev/null +++ b/tests/harness/corpus-costs/7257-pipeline-generator-destination.mjs.json @@ -0,0 +1 @@ +{ "plain": 2008, "san": 2731 } diff --git a/tests/harness/corpus-costs/7258-pipeline-generator-destination.ts.json b/tests/harness/corpus-costs/7258-pipeline-generator-destination.ts.json new file mode 100644 index 000000000..15ff27f21 --- /dev/null +++ b/tests/harness/corpus-costs/7258-pipeline-generator-destination.ts.json @@ -0,0 +1 @@ +{ "plain": 1751, "san": 2442 } diff --git a/tests/harness/corpus-costs/7259-promises-pipeline-stage-options.cjs.json b/tests/harness/corpus-costs/7259-promises-pipeline-stage-options.cjs.json new file mode 100644 index 000000000..6bda6d818 --- /dev/null +++ b/tests/harness/corpus-costs/7259-promises-pipeline-stage-options.cjs.json @@ -0,0 +1 @@ +{ "plain": 2504, "san": 3707 } diff --git a/tests/harness/corpus-costs/7261-acyclic-result-builders.ts.json b/tests/harness/corpus-costs/7261-acyclic-result-builders.ts.json new file mode 100644 index 000000000..30b7c116f --- /dev/null +++ b/tests/harness/corpus-costs/7261-acyclic-result-builders.ts.json @@ -0,0 +1 @@ +{ "plain": 3336, "san": 4712 } diff --git a/tests/harness/corpus-costs/7262-fresh-literal-untyped-param/main.ts.json b/tests/harness/corpus-costs/7262-fresh-literal-untyped-param/main.ts.json new file mode 100644 index 000000000..62c164e5c --- /dev/null +++ b/tests/harness/corpus-costs/7262-fresh-literal-untyped-param/main.ts.json @@ -0,0 +1 @@ +{ "plain": 2800, "san": 3200 } diff --git a/tests/harness/corpus-costs/7263-error-accessor-object-helper.ts.json b/tests/harness/corpus-costs/7263-error-accessor-object-helper.ts.json new file mode 100644 index 000000000..b56ad904d --- /dev/null +++ b/tests/harness/corpus-costs/7263-error-accessor-object-helper.ts.json @@ -0,0 +1 @@ +{ "plain": 2600, "san": 4000 } diff --git a/tests/harness/corpus-costs/7301-running-extrema-branches.ts.json b/tests/harness/corpus-costs/7301-running-extrema-branches.ts.json new file mode 100644 index 000000000..74df88f0c --- /dev/null +++ b/tests/harness/corpus-costs/7301-running-extrema-branches.ts.json @@ -0,0 +1 @@ +{ "plain": 5300, "san": 5800 } diff --git a/tests/harness/corpus-costs/7302-string-scan-loop-versions.ts.json b/tests/harness/corpus-costs/7302-string-scan-loop-versions.ts.json new file mode 100644 index 000000000..793171eba --- /dev/null +++ b/tests/harness/corpus-costs/7302-string-scan-loop-versions.ts.json @@ -0,0 +1 @@ +{ "plain": 4000, "san": 6300 } diff --git a/tests/harness/corpus-costs/7303-string-char-loop-versions.ts.json b/tests/harness/corpus-costs/7303-string-char-loop-versions.ts.json new file mode 100644 index 000000000..0c31a05f7 --- /dev/null +++ b/tests/harness/corpus-costs/7303-string-char-loop-versions.ts.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 4000 } diff --git a/tests/harness/corpus-costs/7322-union-scalar-equality.ts.json b/tests/harness/corpus-costs/7322-union-scalar-equality.ts.json new file mode 100644 index 000000000..2df20a0ed --- /dev/null +++ b/tests/harness/corpus-costs/7322-union-scalar-equality.ts.json @@ -0,0 +1 @@ +{ "plain": 2100, "san": 2800 } diff --git a/tests/harness/corpus-costs/7323-loop-bound-parameter-versions.ts.json b/tests/harness/corpus-costs/7323-loop-bound-parameter-versions.ts.json new file mode 100644 index 000000000..ee18f5f11 --- /dev/null +++ b/tests/harness/corpus-costs/7323-loop-bound-parameter-versions.ts.json @@ -0,0 +1 @@ +{ "plain": 2000, "san": 2800 } diff --git a/tests/harness/corpus-costs/7324-loop-bound-parameter-window-sums.ts.json b/tests/harness/corpus-costs/7324-loop-bound-parameter-window-sums.ts.json new file mode 100644 index 000000000..9622c5beb --- /dev/null +++ b/tests/harness/corpus-costs/7324-loop-bound-parameter-window-sums.ts.json @@ -0,0 +1 @@ +{ "plain": 2000, "san": 3000 } diff --git a/tests/harness/corpus-costs/7325-array-index-products-near-2-53.ts.json b/tests/harness/corpus-costs/7325-array-index-products-near-2-53.ts.json new file mode 100644 index 000000000..f71f3eb98 --- /dev/null +++ b/tests/harness/corpus-costs/7325-array-index-products-near-2-53.ts.json @@ -0,0 +1 @@ +{ "plain": 1800, "san": 2400 } diff --git a/tests/harness/corpus-costs/7326-int32-element-arrays.ts.json b/tests/harness/corpus-costs/7326-int32-element-arrays.ts.json new file mode 100644 index 000000000..168ae57fb --- /dev/null +++ b/tests/harness/corpus-costs/7326-int32-element-arrays.ts.json @@ -0,0 +1 @@ +{ "plain": 3700, "san": 4100 } diff --git a/tests/harness/corpus-costs/7327-int32-array-widening.ts.json b/tests/harness/corpus-costs/7327-int32-array-widening.ts.json new file mode 100644 index 000000000..ac0e8497d --- /dev/null +++ b/tests/harness/corpus-costs/7327-int32-array-widening.ts.json @@ -0,0 +1 @@ +{ "plain": 4500, "san": 6100 } diff --git a/tests/harness/corpus-costs/7362-globalthis-intersection-singleton.ts.json b/tests/harness/corpus-costs/7362-globalthis-intersection-singleton.ts.json new file mode 100644 index 000000000..27b641ffe --- /dev/null +++ b/tests/harness/corpus-costs/7362-globalthis-intersection-singleton.ts.json @@ -0,0 +1 @@ +{ "plain": 2900, "san": 3200 } diff --git a/tests/harness/corpus-costs/7363-json-stringify-live-capsules.ts.json b/tests/harness/corpus-costs/7363-json-stringify-live-capsules.ts.json new file mode 100644 index 000000000..27b641ffe --- /dev/null +++ b/tests/harness/corpus-costs/7363-json-stringify-live-capsules.ts.json @@ -0,0 +1 @@ +{ "plain": 2900, "san": 3200 } diff --git a/tests/harness/corpus-costs/7364-new-target-late-instantiations.ts.json b/tests/harness/corpus-costs/7364-new-target-late-instantiations.ts.json new file mode 100644 index 000000000..725cf769c --- /dev/null +++ b/tests/harness/corpus-costs/7364-new-target-late-instantiations.ts.json @@ -0,0 +1 @@ +{ "plain": 4300, "san": 17500 } diff --git a/tests/harness/corpus-costs/7365-class-methods-untyped-computed-writes/main.ts.json b/tests/harness/corpus-costs/7365-class-methods-untyped-computed-writes/main.ts.json new file mode 100644 index 000000000..0347bbd22 --- /dev/null +++ b/tests/harness/corpus-costs/7365-class-methods-untyped-computed-writes/main.ts.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 3400 } diff --git a/tests/harness/corpus-costs/7366-typed-global-view-arrays.ts.json b/tests/harness/corpus-costs/7366-typed-global-view-arrays.ts.json new file mode 100644 index 000000000..27b641ffe --- /dev/null +++ b/tests/harness/corpus-costs/7366-typed-global-view-arrays.ts.json @@ -0,0 +1 @@ +{ "plain": 2900, "san": 3200 } diff --git a/tests/harness/corpus-costs/7380-iterable-values.ts.json b/tests/harness/corpus-costs/7380-iterable-values.ts.json new file mode 100644 index 000000000..87569618d --- /dev/null +++ b/tests/harness/corpus-costs/7380-iterable-values.ts.json @@ -0,0 +1 @@ +{ "plain": 4000, "san": 6000 } diff --git a/tests/harness/corpus-costs/7381-iterable-forof-semantics.ts.json b/tests/harness/corpus-costs/7381-iterable-forof-semantics.ts.json new file mode 100644 index 000000000..87569618d --- /dev/null +++ b/tests/harness/corpus-costs/7381-iterable-forof-semantics.ts.json @@ -0,0 +1 @@ +{ "plain": 4000, "san": 6000 } diff --git a/tests/harness/corpus-costs/7382-iterable-generic-repository.ts.json b/tests/harness/corpus-costs/7382-iterable-generic-repository.ts.json new file mode 100644 index 000000000..87569618d --- /dev/null +++ b/tests/harness/corpus-costs/7382-iterable-generic-repository.ts.json @@ -0,0 +1 @@ +{ "plain": 4000, "san": 6000 } diff --git a/tests/harness/corpus-costs/7383-iterable-union-sources.ts.json b/tests/harness/corpus-costs/7383-iterable-union-sources.ts.json new file mode 100644 index 000000000..87569618d --- /dev/null +++ b/tests/harness/corpus-costs/7383-iterable-union-sources.ts.json @@ -0,0 +1 @@ +{ "plain": 4000, "san": 6000 } diff --git a/tests/harness/corpus-costs/7400-record-keyof-union-read.ts.json b/tests/harness/corpus-costs/7400-record-keyof-union-read.ts.json new file mode 100644 index 000000000..9b72eaf8d --- /dev/null +++ b/tests/harness/corpus-costs/7400-record-keyof-union-read.ts.json @@ -0,0 +1 @@ +{ "plain": 2600, "san": 20500 } diff --git a/tests/harness/corpus-costs/7401-any-spread-into-typed-literal.ts.json b/tests/harness/corpus-costs/7401-any-spread-into-typed-literal.ts.json new file mode 100644 index 000000000..9eb118813 --- /dev/null +++ b/tests/harness/corpus-costs/7401-any-spread-into-typed-literal.ts.json @@ -0,0 +1 @@ +{ "plain": 2600, "san": 20000 } diff --git a/tests/harness/corpus-costs/7401-program-lifetime-array-teardown.ts.json b/tests/harness/corpus-costs/7401-program-lifetime-array-teardown.ts.json new file mode 100644 index 000000000..2741c4d83 --- /dev/null +++ b/tests/harness/corpus-costs/7401-program-lifetime-array-teardown.ts.json @@ -0,0 +1 @@ +{ "plain": 3103, "san": 4147 } diff --git a/tests/harness/corpus-costs/7402-unknown-array-methods.ts.json b/tests/harness/corpus-costs/7402-unknown-array-methods.ts.json new file mode 100644 index 000000000..9eb118813 --- /dev/null +++ b/tests/harness/corpus-costs/7402-unknown-array-methods.ts.json @@ -0,0 +1 @@ +{ "plain": 2600, "san": 20000 } diff --git a/tests/harness/corpus-costs/7403-bigint-of-typed-arrays.mjs.json b/tests/harness/corpus-costs/7403-bigint-of-typed-arrays.mjs.json new file mode 100644 index 000000000..754441b3e --- /dev/null +++ b/tests/harness/corpus-costs/7403-bigint-of-typed-arrays.mjs.json @@ -0,0 +1 @@ +{ "plain": 1738, "san": 3053 } diff --git a/tests/harness/corpus-costs/7403-dyn-string-at.mjs.json b/tests/harness/corpus-costs/7403-dyn-string-at.mjs.json new file mode 100644 index 000000000..cd29a3461 --- /dev/null +++ b/tests/harness/corpus-costs/7403-dyn-string-at.mjs.json @@ -0,0 +1 @@ +{ "plain": 2600, "san": 15000 } diff --git a/tests/harness/corpus-costs/7420-for-await-close.ts.json b/tests/harness/corpus-costs/7420-for-await-close.ts.json new file mode 100644 index 000000000..2df20a0ed --- /dev/null +++ b/tests/harness/corpus-costs/7420-for-await-close.ts.json @@ -0,0 +1 @@ +{ "plain": 2100, "san": 2800 } diff --git a/tests/harness/corpus-costs/7421-for-await-close.mjs.json b/tests/harness/corpus-costs/7421-for-await-close.mjs.json new file mode 100644 index 000000000..cb701dc93 --- /dev/null +++ b/tests/harness/corpus-costs/7421-for-await-close.mjs.json @@ -0,0 +1 @@ +{ "plain": 1900, "san": 2600 } diff --git a/tests/harness/corpus-costs/7422-for-await-untyped-streams.mjs.json b/tests/harness/corpus-costs/7422-for-await-untyped-streams.mjs.json new file mode 100644 index 000000000..cb701dc93 --- /dev/null +++ b/tests/harness/corpus-costs/7422-for-await-untyped-streams.mjs.json @@ -0,0 +1 @@ +{ "plain": 1900, "san": 2600 } diff --git a/tests/harness/corpus-costs/7423-stream-untyped-bindings.mjs.json b/tests/harness/corpus-costs/7423-stream-untyped-bindings.mjs.json new file mode 100644 index 000000000..d2ac5db99 --- /dev/null +++ b/tests/harness/corpus-costs/7423-stream-untyped-bindings.mjs.json @@ -0,0 +1 @@ +{ "plain": 4900, "san": 6454 } diff --git a/tests/harness/corpus-costs/7424-duplex-from-streams.mjs.json b/tests/harness/corpus-costs/7424-duplex-from-streams.mjs.json new file mode 100644 index 000000000..14a2052b9 --- /dev/null +++ b/tests/harness/corpus-costs/7424-duplex-from-streams.mjs.json @@ -0,0 +1 @@ +{ "plain": 2607, "san": 3440 } diff --git a/tests/harness/corpus-costs/7425-duplex-from-streams.ts.json b/tests/harness/corpus-costs/7425-duplex-from-streams.ts.json new file mode 100644 index 000000000..9dde8d70e --- /dev/null +++ b/tests/harness/corpus-costs/7425-duplex-from-streams.ts.json @@ -0,0 +1 @@ +{ "plain": 2009, "san": 2743 } diff --git a/tests/harness/corpus-costs/7426-duplex-from-promise.mjs.json b/tests/harness/corpus-costs/7426-duplex-from-promise.mjs.json new file mode 100644 index 000000000..9195268ab --- /dev/null +++ b/tests/harness/corpus-costs/7426-duplex-from-promise.mjs.json @@ -0,0 +1 @@ +{ "plain": 1877, "san": 2357 } diff --git a/tests/harness/corpus-costs/7427-duplex-from-async-function.mjs.json b/tests/harness/corpus-costs/7427-duplex-from-async-function.mjs.json new file mode 100644 index 000000000..c572d9930 --- /dev/null +++ b/tests/harness/corpus-costs/7427-duplex-from-async-function.mjs.json @@ -0,0 +1 @@ +{ "plain": 1811, "san": 2417 } diff --git a/tests/harness/corpus-costs/7428-web-stream-sources.mjs.json b/tests/harness/corpus-costs/7428-web-stream-sources.mjs.json new file mode 100644 index 000000000..e95faaa64 --- /dev/null +++ b/tests/harness/corpus-costs/7428-web-stream-sources.mjs.json @@ -0,0 +1 @@ +{ "plain": 1829, "san": 2414 } diff --git a/tests/harness/corpus-costs/7429-web-stream-sources.ts.json b/tests/harness/corpus-costs/7429-web-stream-sources.ts.json new file mode 100644 index 000000000..6b14e36cc --- /dev/null +++ b/tests/harness/corpus-costs/7429-web-stream-sources.ts.json @@ -0,0 +1 @@ +{ "plain": 1250, "san": 3013 } diff --git a/tests/harness/corpus-costs/7430-readable-from-web.mjs.json b/tests/harness/corpus-costs/7430-readable-from-web.mjs.json new file mode 100644 index 000000000..2266382b4 --- /dev/null +++ b/tests/harness/corpus-costs/7430-readable-from-web.mjs.json @@ -0,0 +1 @@ +{ "plain": 3342, "san": 4411 } diff --git a/tests/harness/corpus-costs/7431-readable-from-web.ts.json b/tests/harness/corpus-costs/7431-readable-from-web.ts.json new file mode 100644 index 000000000..05fab8e59 --- /dev/null +++ b/tests/harness/corpus-costs/7431-readable-from-web.ts.json @@ -0,0 +1 @@ +{ "plain": 1956, "san": 2929 } diff --git a/tests/harness/corpus-costs/7432-readable-push-views.mjs.json b/tests/harness/corpus-costs/7432-readable-push-views.mjs.json new file mode 100644 index 000000000..8a2f5280e --- /dev/null +++ b/tests/harness/corpus-costs/7432-readable-push-views.mjs.json @@ -0,0 +1 @@ +{ "plain": 1582, "san": 2088 } diff --git a/tests/harness/corpus-costs/7433-readable-operator-signal.mjs.json b/tests/harness/corpus-costs/7433-readable-operator-signal.mjs.json new file mode 100644 index 000000000..3ef40f751 --- /dev/null +++ b/tests/harness/corpus-costs/7433-readable-operator-signal.mjs.json @@ -0,0 +1 @@ +{ "plain": 3885, "san": 5128 } diff --git a/tests/harness/corpus-costs/7434-readable-operator-values.ts.json b/tests/harness/corpus-costs/7434-readable-operator-values.ts.json new file mode 100644 index 000000000..fdbfd8cde --- /dev/null +++ b/tests/harness/corpus-costs/7434-readable-operator-values.ts.json @@ -0,0 +1 @@ +{ "plain": 2465, "san": 2933 } diff --git a/tests/harness/corpus-costs/7435-readable-flatmap-iterables.mjs.json b/tests/harness/corpus-costs/7435-readable-flatmap-iterables.mjs.json new file mode 100644 index 000000000..8c31cc905 --- /dev/null +++ b/tests/harness/corpus-costs/7435-readable-flatmap-iterables.mjs.json @@ -0,0 +1 @@ +{ "plain": 2265, "san": 2990 } diff --git a/tests/harness/corpus-costs/7436-readable-read-any-value.mjs.json b/tests/harness/corpus-costs/7436-readable-read-any-value.mjs.json new file mode 100644 index 000000000..c4a7cbc84 --- /dev/null +++ b/tests/harness/corpus-costs/7436-readable-read-any-value.mjs.json @@ -0,0 +1 @@ +{ "plain": 1675, "san": 2211 } diff --git a/tests/harness/corpus-costs/7437-duplex-from-signal.mjs.json b/tests/harness/corpus-costs/7437-duplex-from-signal.mjs.json new file mode 100644 index 000000000..dfbf933f4 --- /dev/null +++ b/tests/harness/corpus-costs/7437-duplex-from-signal.mjs.json @@ -0,0 +1 @@ +{ "plain": 2383, "san": 3146 } diff --git a/tests/harness/corpus-costs/7438-duplex-from-signal.ts.json b/tests/harness/corpus-costs/7438-duplex-from-signal.ts.json new file mode 100644 index 000000000..13b6f4240 --- /dev/null +++ b/tests/harness/corpus-costs/7438-duplex-from-signal.ts.json @@ -0,0 +1 @@ +{ "plain": 1777, "san": 2722 } diff --git a/tests/harness/corpus-costs/7439-readable-read-string-chunks.ts.json b/tests/harness/corpus-costs/7439-readable-read-string-chunks.ts.json new file mode 100644 index 000000000..a93aa2e95 --- /dev/null +++ b/tests/harness/corpus-costs/7439-readable-read-string-chunks.ts.json @@ -0,0 +1 @@ +{ "plain": 2578, "san": 3403 } diff --git a/tests/harness/corpus-costs/7440-fromentries-aliased-entries.ts.json b/tests/harness/corpus-costs/7440-fromentries-aliased-entries.ts.json new file mode 100644 index 000000000..87569618d --- /dev/null +++ b/tests/harness/corpus-costs/7440-fromentries-aliased-entries.ts.json @@ -0,0 +1 @@ +{ "plain": 4000, "san": 6000 } diff --git a/tests/harness/corpus-costs/7441-untyped-receiver-own-string-method-names.ts.json b/tests/harness/corpus-costs/7441-untyped-receiver-own-string-method-names.ts.json new file mode 100644 index 000000000..615c617ae --- /dev/null +++ b/tests/harness/corpus-costs/7441-untyped-receiver-own-string-method-names.ts.json @@ -0,0 +1 @@ +{ "plain": 5800 } diff --git a/tests/harness/corpus-costs/7445-crypto-key-pair-options.mjs.json b/tests/harness/corpus-costs/7445-crypto-key-pair-options.mjs.json new file mode 100644 index 000000000..273d0c784 --- /dev/null +++ b/tests/harness/corpus-costs/7445-crypto-key-pair-options.mjs.json @@ -0,0 +1 @@ +{ "plain": 3596, "san": 5499 } diff --git a/tests/harness/corpus-costs/7446-crypto-rsa-key-pair-errors.ts.json b/tests/harness/corpus-costs/7446-crypto-rsa-key-pair-errors.ts.json new file mode 100644 index 000000000..ed5d25bcc --- /dev/null +++ b/tests/harness/corpus-costs/7446-crypto-rsa-key-pair-errors.ts.json @@ -0,0 +1 @@ +{ "plain": 7539, "san": 19613 } diff --git a/tests/harness/corpus-costs/7480-builtin-constructor-in.js.json b/tests/harness/corpus-costs/7480-builtin-constructor-in.js.json new file mode 100644 index 000000000..20168ce9a --- /dev/null +++ b/tests/harness/corpus-costs/7480-builtin-constructor-in.js.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 3800 } diff --git a/tests/harness/corpus-costs/7480-class-call-without-new.ts.json b/tests/harness/corpus-costs/7480-class-call-without-new.ts.json new file mode 100644 index 000000000..f14d666a4 --- /dev/null +++ b/tests/harness/corpus-costs/7480-class-call-without-new.ts.json @@ -0,0 +1 @@ +{ "plain": 3114, "san": 2571 } diff --git a/tests/harness/corpus-costs/7481-class-call-without-new.mjs.json b/tests/harness/corpus-costs/7481-class-call-without-new.mjs.json new file mode 100644 index 000000000..cecf59f78 --- /dev/null +++ b/tests/harness/corpus-costs/7481-class-call-without-new.mjs.json @@ -0,0 +1 @@ +{ "plain": 2315, "san": 2145 } diff --git a/tests/harness/corpus-costs/7481-class-runtime-heritage-object-error.js.json b/tests/harness/corpus-costs/7481-class-runtime-heritage-object-error.js.json new file mode 100644 index 000000000..20168ce9a --- /dev/null +++ b/tests/harness/corpus-costs/7481-class-runtime-heritage-object-error.js.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 3800 } diff --git a/tests/harness/corpus-costs/7482-define-property-function-class-values.js.json b/tests/harness/corpus-costs/7482-define-property-function-class-values.js.json new file mode 100644 index 000000000..20168ce9a --- /dev/null +++ b/tests/harness/corpus-costs/7482-define-property-function-class-values.js.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 3800 } diff --git a/tests/harness/corpus-costs/7483-constructor-factory-traits.js.json b/tests/harness/corpus-costs/7483-constructor-factory-traits.js.json new file mode 100644 index 000000000..20168ce9a --- /dev/null +++ b/tests/harness/corpus-costs/7483-constructor-factory-traits.js.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 3800 } diff --git a/tests/harness/corpus-costs/7484-hoisted-function-captures-later-class.js.json b/tests/harness/corpus-costs/7484-hoisted-function-captures-later-class.js.json new file mode 100644 index 000000000..20168ce9a --- /dev/null +++ b/tests/harness/corpus-costs/7484-hoisted-function-captures-later-class.js.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 3800 } diff --git a/tests/harness/corpus-costs/7485-global-object-own-properties.js.json b/tests/harness/corpus-costs/7485-global-object-own-properties.js.json new file mode 100644 index 000000000..20168ce9a --- /dev/null +++ b/tests/harness/corpus-costs/7485-global-object-own-properties.js.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 3800 } diff --git a/tests/harness/corpus-costs/7486-prototype-accessor-string-method-names.js.json b/tests/harness/corpus-costs/7486-prototype-accessor-string-method-names.js.json new file mode 100644 index 000000000..20168ce9a --- /dev/null +++ b/tests/harness/corpus-costs/7486-prototype-accessor-string-method-names.js.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 3800 } diff --git a/tests/harness/corpus-costs/7487-namespace-spread-untyped/main.js.json b/tests/harness/corpus-costs/7487-namespace-spread-untyped/main.js.json new file mode 100644 index 000000000..20168ce9a --- /dev/null +++ b/tests/harness/corpus-costs/7487-namespace-spread-untyped/main.js.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 3800 } diff --git a/tests/harness/corpus-costs/7488-math-spread-untyped.js.json b/tests/harness/corpus-costs/7488-math-spread-untyped.js.json new file mode 100644 index 000000000..20168ce9a --- /dev/null +++ b/tests/harness/corpus-costs/7488-math-spread-untyped.js.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 3800 } diff --git a/tests/harness/corpus-costs/7489-math-spread-generator-only.js.json b/tests/harness/corpus-costs/7489-math-spread-generator-only.js.json new file mode 100644 index 000000000..2aa0f8038 --- /dev/null +++ b/tests/harness/corpus-costs/7489-math-spread-generator-only.js.json @@ -0,0 +1 @@ +{ "plain": 3384, "san": 4732 } diff --git a/tests/harness/corpus-costs/7540-undefined-element-index-keys.ts.json b/tests/harness/corpus-costs/7540-undefined-element-index-keys.ts.json new file mode 100644 index 000000000..1da1b737f --- /dev/null +++ b/tests/harness/corpus-costs/7540-undefined-element-index-keys.ts.json @@ -0,0 +1 @@ +{ "plain": 3700, "san": 5300 } diff --git a/tests/harness/corpus-costs/7541-prototype-identity-stand-ins.ts.json b/tests/harness/corpus-costs/7541-prototype-identity-stand-ins.ts.json new file mode 100644 index 000000000..046e0461c --- /dev/null +++ b/tests/harness/corpus-costs/7541-prototype-identity-stand-ins.ts.json @@ -0,0 +1 @@ +{ "plain": 4700, "san": 6500 } diff --git a/tests/harness/corpus-costs/7542-prototype-identity-without-data.ts.json b/tests/harness/corpus-costs/7542-prototype-identity-without-data.ts.json new file mode 100644 index 000000000..d2c83f967 --- /dev/null +++ b/tests/harness/corpus-costs/7542-prototype-identity-without-data.ts.json @@ -0,0 +1 @@ +{ "plain": 2500, "san": 3300 } diff --git a/tests/harness/corpus-costs/7544-local-class-prototype-identity.ts.json b/tests/harness/corpus-costs/7544-local-class-prototype-identity.ts.json new file mode 100644 index 000000000..87dec518c --- /dev/null +++ b/tests/harness/corpus-costs/7544-local-class-prototype-identity.ts.json @@ -0,0 +1 @@ +{ "plain": 3900, "san": 5100 } diff --git a/tests/harness/corpus-costs/7560-new-target-constructors.ts.json b/tests/harness/corpus-costs/7560-new-target-constructors.ts.json new file mode 100644 index 000000000..2c7339a24 --- /dev/null +++ b/tests/harness/corpus-costs/7560-new-target-constructors.ts.json @@ -0,0 +1 @@ +{ "plain": 6000, "san": 26000 } diff --git a/tests/harness/corpus-costs/7561-new-target-generic-registry.ts.json b/tests/harness/corpus-costs/7561-new-target-generic-registry.ts.json new file mode 100644 index 000000000..eceea988d --- /dev/null +++ b/tests/harness/corpus-costs/7561-new-target-generic-registry.ts.json @@ -0,0 +1 @@ +{ "plain": 4500, "san": 23500 } diff --git a/tests/harness/corpus-costs/7565-iterable-replaceable-method-results.ts.json b/tests/harness/corpus-costs/7565-iterable-replaceable-method-results.ts.json new file mode 100644 index 000000000..6d81df217 --- /dev/null +++ b/tests/harness/corpus-costs/7565-iterable-replaceable-method-results.ts.json @@ -0,0 +1 @@ +{ "plain": 8000, "san": 14000 } diff --git a/tests/harness/corpus-costs/7600-navigator-optional-chain.js.json b/tests/harness/corpus-costs/7600-navigator-optional-chain.js.json new file mode 100644 index 000000000..e350cf2da --- /dev/null +++ b/tests/harness/corpus-costs/7600-navigator-optional-chain.js.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 22000 } diff --git a/tests/harness/corpus-costs/7602-error-stack-limit-value.js.json b/tests/harness/corpus-costs/7602-error-stack-limit-value.js.json new file mode 100644 index 000000000..bb380e241 --- /dev/null +++ b/tests/harness/corpus-costs/7602-error-stack-limit-value.js.json @@ -0,0 +1 @@ +{ "plain": 2500, "san": 3000 } diff --git a/tests/harness/corpus-costs/7603-error-stack-limit-own.js.json b/tests/harness/corpus-costs/7603-error-stack-limit-own.js.json new file mode 100644 index 000000000..bb380e241 --- /dev/null +++ b/tests/harness/corpus-costs/7603-error-stack-limit-own.js.json @@ -0,0 +1 @@ +{ "plain": 2500, "san": 3000 } diff --git a/tests/harness/corpus-costs/7604-error-stack-limit-frozen.js.json b/tests/harness/corpus-costs/7604-error-stack-limit-frozen.js.json new file mode 100644 index 000000000..bb380e241 --- /dev/null +++ b/tests/harness/corpus-costs/7604-error-stack-limit-frozen.js.json @@ -0,0 +1 @@ +{ "plain": 2500, "san": 3000 } diff --git a/tests/harness/corpus-costs/7610-key-order-json.ts.json b/tests/harness/corpus-costs/7610-key-order-json.ts.json new file mode 100644 index 000000000..aa8658007 --- /dev/null +++ b/tests/harness/corpus-costs/7610-key-order-json.ts.json @@ -0,0 +1 @@ +{ "plain": 2200, "san": 3600 } diff --git a/tests/harness/corpus-costs/7611-error-message-accessor.ts.json b/tests/harness/corpus-costs/7611-error-message-accessor.ts.json new file mode 100644 index 000000000..8c97e80f9 --- /dev/null +++ b/tests/harness/corpus-costs/7611-error-message-accessor.ts.json @@ -0,0 +1 @@ +{ "plain": 2962, "san": 3600 } diff --git a/tests/harness/corpus-costs/7611-key-order-keys-values-entries.ts.json b/tests/harness/corpus-costs/7611-key-order-keys-values-entries.ts.json new file mode 100644 index 000000000..58d7b29f8 --- /dev/null +++ b/tests/harness/corpus-costs/7611-key-order-keys-values-entries.ts.json @@ -0,0 +1 @@ +{ "plain": 1500, "san": 2800 } diff --git a/tests/harness/corpus-costs/7612-error-message-accessor-lazy.js.json b/tests/harness/corpus-costs/7612-error-message-accessor-lazy.js.json new file mode 100644 index 000000000..bbc81f519 --- /dev/null +++ b/tests/harness/corpus-costs/7612-error-message-accessor-lazy.js.json @@ -0,0 +1 @@ +{ "plain": 2217, "san": 2721 } diff --git a/tests/harness/corpus-costs/7612-key-order-for-in.ts.json b/tests/harness/corpus-costs/7612-key-order-for-in.ts.json new file mode 100644 index 000000000..e0b1e4dd1 --- /dev/null +++ b/tests/harness/corpus-costs/7612-key-order-for-in.ts.json @@ -0,0 +1 @@ +{ "plain": 1600, "san": 2100 } diff --git a/tests/harness/corpus-costs/7613-key-order-inspect.ts.json b/tests/harness/corpus-costs/7613-key-order-inspect.ts.json new file mode 100644 index 000000000..932ea4809 --- /dev/null +++ b/tests/harness/corpus-costs/7613-key-order-inspect.ts.json @@ -0,0 +1 @@ +{ "plain": 1600, "san": 2500 } diff --git a/tests/harness/corpus-costs/7613-local-class-prototype-reflection.js.json b/tests/harness/corpus-costs/7613-local-class-prototype-reflection.js.json new file mode 100644 index 000000000..9c4d9369a --- /dev/null +++ b/tests/harness/corpus-costs/7613-local-class-prototype-reflection.js.json @@ -0,0 +1 @@ +{ "plain": 1967, "san": 2510 } diff --git a/tests/harness/corpus-costs/7614-key-order-copies.ts.json b/tests/harness/corpus-costs/7614-key-order-copies.ts.json new file mode 100644 index 000000000..9d76a548c --- /dev/null +++ b/tests/harness/corpus-costs/7614-key-order-copies.ts.json @@ -0,0 +1 @@ +{ "plain": 1900, "san": 3500 } diff --git a/tests/harness/corpus-costs/7614-npm-local-class-per-instance/main.ts.json b/tests/harness/corpus-costs/7614-npm-local-class-per-instance/main.ts.json new file mode 100644 index 000000000..582f2a9d2 --- /dev/null +++ b/tests/harness/corpus-costs/7614-npm-local-class-per-instance/main.ts.json @@ -0,0 +1 @@ +{ "plain": 3502, "san": 4012 } diff --git a/tests/harness/corpus-costs/7615-key-order-dynamic.ts.json b/tests/harness/corpus-costs/7615-key-order-dynamic.ts.json new file mode 100644 index 000000000..12680c6af --- /dev/null +++ b/tests/harness/corpus-costs/7615-key-order-dynamic.ts.json @@ -0,0 +1 @@ +{ "plain": 1800, "san": 2700 } diff --git a/tests/harness/corpus-costs/7615-local-class-prototype-reflection-ts.ts.json b/tests/harness/corpus-costs/7615-local-class-prototype-reflection-ts.ts.json new file mode 100644 index 000000000..2e0a455c2 --- /dev/null +++ b/tests/harness/corpus-costs/7615-local-class-prototype-reflection-ts.ts.json @@ -0,0 +1 @@ +{ "plain": 1466, "san": 2143 } diff --git a/tests/harness/corpus-costs/7616-key-order-late-writes.ts.json b/tests/harness/corpus-costs/7616-key-order-late-writes.ts.json new file mode 100644 index 000000000..2b1aad28d --- /dev/null +++ b/tests/harness/corpus-costs/7616-key-order-late-writes.ts.json @@ -0,0 +1 @@ +{ "plain": 1700, "san": 2800 } diff --git a/tests/harness/corpus-costs/7621-js-class-field-methods.js.json b/tests/harness/corpus-costs/7621-js-class-field-methods.js.json new file mode 100644 index 000000000..ef439d096 --- /dev/null +++ b/tests/harness/corpus-costs/7621-js-class-field-methods.js.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 4500 } diff --git a/tests/harness/corpus-costs/7731-npm-any-string-index/main.ts.json b/tests/harness/corpus-costs/7731-npm-any-string-index/main.ts.json new file mode 100644 index 000000000..9fe9ad101 --- /dev/null +++ b/tests/harness/corpus-costs/7731-npm-any-string-index/main.ts.json @@ -0,0 +1 @@ +{ "plain": 6200, "san": 5800 } diff --git a/tests/harness/corpus-costs/7732-untyped-match-pattern.js.json b/tests/harness/corpus-costs/7732-untyped-match-pattern.js.json new file mode 100644 index 000000000..2de3f6b94 --- /dev/null +++ b/tests/harness/corpus-costs/7732-untyped-match-pattern.js.json @@ -0,0 +1 @@ +{ "plain": 3600, "san": 3300 } diff --git a/tests/harness/corpus-costs/7733-property-bag-late-prototype.js.json b/tests/harness/corpus-costs/7733-property-bag-late-prototype.js.json new file mode 100644 index 000000000..706d1a2a5 --- /dev/null +++ b/tests/harness/corpus-costs/7733-property-bag-late-prototype.js.json @@ -0,0 +1 @@ +{ "plain": 4400, "san": 3900 } diff --git a/tests/harness/corpus-costs/7734-caught-instanceof-class-binding.js.json b/tests/harness/corpus-costs/7734-caught-instanceof-class-binding.js.json new file mode 100644 index 000000000..71aa92bd3 --- /dev/null +++ b/tests/harness/corpus-costs/7734-caught-instanceof-class-binding.js.json @@ -0,0 +1 @@ +{ "plain": 3300, "san": 3600 } diff --git a/tests/harness/corpus-costs/7735-untyped-headers-iterators.js.json b/tests/harness/corpus-costs/7735-untyped-headers-iterators.js.json new file mode 100644 index 000000000..8836ebdf7 --- /dev/null +++ b/tests/harness/corpus-costs/7735-untyped-headers-iterators.js.json @@ -0,0 +1 @@ +{ "plain": 4000, "san": 4000 } diff --git a/tests/harness/corpus-costs/7738-private-logical-assignment-untyped.js.json b/tests/harness/corpus-costs/7738-private-logical-assignment-untyped.js.json new file mode 100644 index 000000000..d61b17a5d --- /dev/null +++ b/tests/harness/corpus-costs/7738-private-logical-assignment-untyped.js.json @@ -0,0 +1 @@ +{ "plain": 5500, "san": 8800 } diff --git a/tests/harness/coverage.test.ts b/tests/harness/coverage.test.ts index 86d202b46..e1831c609 100644 --- a/tests/harness/coverage.test.ts +++ b/tests/harness/coverage.test.ts @@ -182,6 +182,57 @@ test("statements of a signature-blocked function count as not static", () => { expect(coverage.stats.statementsFailed + (un?.statementsFailed ?? 0)).toBe(5); }); +test("the body of a signature-blocked function reports its diagnostics", () => { + const { coverage } = analyze(fixture("blocked-signature-body.ts")); + const sites = (diags: readonly { code: string; message: string }[]) => + diags.map((d) => `${d.code} ${d.message}`).sort(); + // measure is called: its body's diagnostics join the reached ones. + expect(sites(coverage.diagnostics)).toEqual([ + "SC2004 uses of 'extra' inherit the blocker on its declaration", + "SC2004 uses of 'measure' inherit the blocker on its declaration", + "SC2004 uses of 'target' inherit the blocker on its declaration", + "SC2020 'WeakRef' is part of the standard library types but has no scriptc lowering yet", + "SC2020 'new WeakRef' is part of the standard library types but has no scriptc lowering yet", + ]); + // Nothing calls unused or described: their signatures and bodies report + // as unreached. described's declared `this` is its receiver, so + // `this.label` reports nothing. + expect(sites(coverage.unreached?.diagnostics ?? [])).toEqual([ + "SC2004 uses of 'extra' inherit the blocker on its declaration", + "SC2004 uses of 'target' inherit the blocker on its declaration", + "SC2020 'WeakRef' is part of the standard library types but has no scriptc lowering yet", + "SC2020 'WeakRef' is part of the standard library types but has no scriptc lowering yet", + "SC2020 'new WeakRef' is part of the standard library types but has no scriptc lowering yet", + ]); + // The bodies' statements still count once each, as not static. + const un = coverage.unreached?.stats; + expect(coverage.stats.functionsSkipped + (un?.functionsSkipped ?? 0)).toBe(3); + expect(coverage.stats.statementsTotal + (un?.statementsTotal ?? 0)).toBe(7); + expect(coverage.stats.statementsFailed + (un?.statementsFailed ?? 0)).toBe(7); +}); + +test("deferred fences in a module's top-level code report as running at startup", () => { + const { coverage, sourceTexts } = analyze(fixture("startup-fences.js")); + expect( + (coverage.runtimeFences ?? []).map((d) => [d.code, d.loc.start, d.startup === true]), + ).toEqual([ + ["SC1090", expect.any(Number), true], + ["SC1090", expect.any(Number), false], + ]); + const out = report(fixture("startup-fences.js")); + expect(out).toMatch( + /runs at startup {3}1 site .*\n.*instanceof against this native constructor\n\s+at tests\/coverage-fixtures\/startup-fences\.js:5:13/, + ); + expect(out).toMatch( + /deferred to runtime {3}1 site .*\n.*instanceof against this native constructor\n\s+at tests\/coverage-fixtures\/startup-fences\.js:7:10/, + ); + const envelope = coverageEnvelope(coverage, { compilerVersion: "test", sourceTexts }); + expect(envelope.diagnostics.map((d) => [d.scope, d.line, d.startup])).toEqual([ + ["deferred", 5, true], + ["deferred", 7, undefined], + ]); +}); + test("a failed unreached JavaScript statement is always explained", () => { const { coverage } = analyze(fixture("unreached-js-fence.js")); const failed = @@ -510,13 +561,14 @@ test("types only a package's .d.ts declares stay static through re-exports and . // shape-kinds re-exports types its .d.ts entry imports; row-module's // runtime entry is an .mjs file. Without their declared types the // program's uses of Form, Kind and Row run in the dynamic tier (12 - // statements); with them only the packages' own JavaScript and one - // arithmetic on a JavaScript return value do (4). + // statements); with them only the packages' own JavaScript does (3). + // readRows's result keeps its declared number type, so `count - 1` is + // native arithmetic too. const { coverage } = analyze( join(repoRoot, "tests/corpus/7004-package-declared-type-reexports/main.ts"), ); expect(coverage.diagnostics).toEqual([]); - expect(coverage.stats.statementsDynamic).toBe(4); + expect(coverage.stats.statementsDynamic).toBe(3); }); test("any-typed checked-dynamic operators compile natively", () => { diff --git a/tests/harness/decl-boundary.test.ts b/tests/harness/decl-boundary.test.ts new file mode 100644 index 000000000..931208836 --- /dev/null +++ b/tests/harness/decl-boundary.test.ts @@ -0,0 +1,68 @@ +/* The declaration-typed boundary's checks (decl-projection.ts): a value + * zod's declarations type wrongly fails the boundary cast with a TypeError + * naming the path, and a value the native record cannot represent (a + * frozen object) fails with the reason. Node does not check, so this is a + * native-only test; npm.test.ts runs the differential cases + * (zod3-boundary, zod3-adoption, zod3-infer). The rewrite test checks that + * a file keeps its projection (no SC6102 note) where the rewrite has to + * parenthesize a bare async arrow's parameter and cast the start of an + * unbraced loop body. */ +import { execFile } from "node:child_process"; +import { mkdirSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { promisify } from "node:util"; +import { expect, test } from "vitest"; +import { compile } from "@scriptc/compiler"; + +const execFileAsync = promisify(execFile); +const repoRoot = join(import.meta.dirname, "../.."); +const entry = join(repoRoot, "tests/fixtures/npm/cases/zod3-boundary/lying.ts"); +const outDir = join(repoRoot, "node_modules/.cache/scriptc-tests/decl-boundary"); +const sanitize = process.env["SCRIPTC_SAN"] === "1"; + +test( + "boundary casts throw a path TypeError for values the declarations misdescribe", + { timeout: 600_000 }, + async () => { + mkdirSync(outDir, { recursive: true }); + const result = await compile(entry, { outPath: join(outDir, "lying"), outDir, sanitize }); + if (!result.ok) { + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + } + const pending = execFileAsync(result.binaryPath, [], { cwd: dirname(entry), encoding: "utf8" }); + pending.child.stdin?.end(); + const { stdout } = await pending; + expect(stdout).toBe( + "liar threw true expected number at $.n, got string\n" + + "frozen threw expected a plain object (this value is frozen, sealed or not extensible) at $, got object\n", + ); + }, +); + +test( + "boundary rewrites keep bare async arrows and unbraced loop bodies valid", + { timeout: 600_000 }, + async () => { + const rewrites = join(repoRoot, "tests/fixtures/npm/cases/zod3-boundary/rewrites.ts"); + mkdirSync(outDir, { recursive: true }); + const result = await compile(rewrites, { + outPath: join(outDir, "rewrites"), + outDir, + sanitize, + }); + if (!result.ok) { + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + } + // The file keeps its projection: no SC6102 note drops it (a build + // that lowered nothing carries no warnings at all). + expect((result.warnings ?? []).map((w) => w.code)).not.toContain("SC6102"); + const pending = execFileAsync(result.binaryPath, [], { + cwd: dirname(rewrites), + encoding: "utf8", + }); + pending.child.stdin?.end(); + const { stdout } = await pending; + // Node's output: every loop iteration runs the cast statement. + expect(stdout).toBe("loops 5\nasync arrow 4\n"); + }, +); diff --git a/tests/harness/error-accessor-reflection.test.ts b/tests/harness/error-accessor-reflection.test.ts new file mode 100644 index 000000000..5f50d8a66 --- /dev/null +++ b/tests/harness/error-accessor-reflection.test.ts @@ -0,0 +1,77 @@ +/* Reflection that can install an accessor over an error's `name` or + * `message`: a defineProperty call over a target that may hold an Error + * turns on the accessor checks of typed Error reads and writes + * (error-methods.ts), whatever the target parameter's declared type. */ +import { createHash } from "node:crypto"; +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile } from "@scriptc/compiler"; + +const repoRoot = join(import.meta.dirname, "../.."); +const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests"); + +async function emittedIr(source: string): Promise { + const key = createHash("sha256").update(source).digest("hex").slice(0, 16); + const outDir = join(cacheDir, `error-accessor-${key}`); + mkdirSync(outDir, { recursive: true }); + const file = join(outDir, "main.ts"); + writeFileSync(file, source); + const result = await compile(file, { + outPath: join(outDir, "main.ir"), + outDir, + outputKind: "ir", + }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + return readFileSync(result.artifact.path, "utf8"); +} + +const program = ( + param: string, + define = "Object.defineProperty", +): string => `class E extends Error {} +function install${param} { + ${define}(target, "message", { get: () => "lazy", configurable: true }); +} +const e = new E("orig"); +install(e); +console.log(e.message); +`; + +const checksAccessors = async (source: string): Promise => + (await emittedIr(source)).includes("error.ownAccessor"); + +test.each([ + ["object", "(target: object)"], + ["{}", "(target: {})"], + ["Object", "(target: Object)"], + ["a weak type", "(target: { message?: string })"], + ["{ message: string }", "(target: { message: string })"], + ["a type parameter", "(target: T)"], + ["a constrained type parameter", "(target: T)"], + ["object | undefined", "(target: object | undefined)"], + ["Error", "(target: Error)"], + ["unknown", "(target: unknown)"], +])("a definition over %s checks for accessors", async (_, param) => { + expect(await checksAccessors(program(param))).toBe(true); +}); + +test("Reflect.defineProperty over object checks for accessors", async () => { + expect(await checksAccessors(program("(target: object)", "Reflect.defineProperty"))).toBe(true); +}); + +test("a definition over a type that cannot hold an Error leaves typed reads plain", async () => { + const source = `class E extends Error {} +class Box { + id = 0; +} +function install(target: Box) { + Object.defineProperty(target, "extra", { get: () => 1, configurable: true }); +} +install(new Box()); +const e = new E("orig"); +console.log(e.message); +`; + expect(await checksAccessors(source)).toBe(false); +}); diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index b5fb028f9..319e7329f 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -305,6 +305,26 @@ describe("checked catch-binding casts", () => { ); }); + test("a typed-array write through an undefined index is a named runtime fence", async () => { + // Node stores an ordinary property named "undefined" on the typed + // array, which a compiled typed array cannot hold; defined indexes + // before it store as usual. + const r = await compileAndRun( + "typed-array-undefined-index", + `const holey: number[] = [1]; +holey[3] = 3; +const cells = new Float64Array(4); +for (const i of holey) cells[i] = 9; +console.log(cells.join(",")); +`, + ); + expect(r.exitCode).toBe(1); + expect(r.stdout).toBe(""); + expect(r.stderr).toMatch( + /^Uncaught Error: typed array properties named 'undefined' \(an element index that is undefined\) are not supported yet \[SC1090 at .*typed-array-undefined-index\.ts:4\]\n$/, + ); + }); + test("an EFFECTFUL surplus argument is a named deferred fence (JS lane)", async () => { // Effect-free surplus args drop at compile time, JS-exact (corpus // 1760 — the test-string-decoder writeSequences shape). An effectful @@ -847,8 +867,9 @@ try { expect(r.exitCode).toBe(0); expect(r.stdout).toBe( "Error SC2020 The cipher 'aes-128-ccm' has no native implementation; the supported ciphers are " + - "aes-128-cbc, aes-192-cbc, aes-256-cbc, aes-128-ctr, aes-192-ctr, aes-256-ctr, aes-128-gcm, " + - "aes-192-gcm, aes-256-gcm and chacha20-poly1305\n" + + "aes-128, aes-192 and aes-256 in cbc, ctr, gcm, ecb, cfb, cfb8, cfb1 and ofb modes, des-ede3 in " + + "cbc, ecb, cfb, cfb8, cfb1 and ofb modes, des-ede in cbc, ecb, cfb and ofb modes, chacha20 and " + + "chacha20-poly1305\n" + "Error ERR_CRYPTO_UNKNOWN_CIPHER Unknown cipher\n", ); expect(r.stderr).toBe(""); @@ -1710,9 +1731,10 @@ describe("builtin constructor values (JS lane)", () => { test("members without a lowering stay refused instead of reading undefined", async () => { // Builtin constructors read as interned function values that model only // name, length and constructor. Every other static member or prototype - // read (direct, bracketed, `in`, through an alias or globalThis), and + // read (direct, bracketed, through an alias or globalThis), and // reflection on or redefinition of their members, must stay a located // SC2020/SC1090 refusal, never an undefined or otherwise wrong value. + // `key in Ctor` answers from the constructor's fixed keys. // Test262 lost 769 refusals to undefined reads before this was pinned. const reads = [ ["Map.prototype", "Map.prototype"], @@ -1730,7 +1752,6 @@ describe("builtin constructor values (JS lane)", () => { ], ["isExtensible(Date.prototype)", "Object.isExtensible(Date.prototype)"], ["Map['prototype']", 'Map["prototype"]'], - ["'prototype' in Map", '"prototype" in Map'], ["globalThis.Map.prototype", "globalThis.Map.prototype"], ["Map alias prototype", "MapAlias.prototype"], ["Date alias prototype", "DateAlias.prototype"], @@ -1753,6 +1774,7 @@ console.log(Date.length, RegExp.length, Promise.length, Object.length, Map.lengt console.log(Date.name, Map.name, Map.constructor === Function, Object.constructor === Function); const promisePrototype = Promise.prototype; console.log(typeof promisePrototype, typeof promisePrototype.then, promisePrototype.constructor === Promise); +console.log("prototype" in Map, "groupBy" in Map, "captureStackTrace" in Error, "nope" in Map); `; const r = await compileAndRun("builtin-constructor-members", source, "cjs"); expect(r.stderr).toBe(""); @@ -1767,11 +1789,13 @@ console.log(typeof promisePrototype, typeof promisePrototype.then, promiseProtot ), ), ); - // Promise.prototype reads as an object holding the runtime's methods. + // Promise.prototype reads as an object holding the runtime's methods, + // and `in` on a builtin constructor answers from its fixed keys. expect(lines.slice(reads.length)).toEqual([ "7 2 1 1 0 0", "Date Map true true", "object function true", + "true true true false", ]); }); }); @@ -1984,3 +2008,45 @@ console.log("unreachable"); ); }); }); + +describe("numeric properties of native arrays", () => { + // A native array keeps its numeric properties ("-1", "1.5") in a property + // table that holds values without attributes, so defining one with a + // descriptor refuses instead of dropping the attributes. + test("Object.defineProperty on a numeric key is a named fence", async () => { + const r = await compileAndRun( + "array-numeric-define-property", + `const words: string[] = ["a"]; +words[-1] = "x"; +console.log(words[-1]); +Object.defineProperty(words as unknown as Record, "-3", { value: "kept" }); +console.log("unreachable"); +`, + ); + expect(r.exitCode).toBe(1); + expect(r.stdout).toBe("x\n"); + expect(r.stderr).toMatch( + /^Uncaught Error: Object\.defineProperty on a numeric property of a native array is not supported yet\n$/, + ); + }); + + test("a number key that is not an array index takes the same fence", async () => { + const r = await compileAndRun( + "array-numeric-define-property-number", + `const words: string[] = ["a"]; +for (const key of [-3, 1.5]) { + try { + Object.defineProperty(words as unknown as Record, key, { value: "kept" }); + } catch (e) { + console.log((e as Error).message); + } +} +console.log(words.length); +`, + ); + const fence = + "Object.defineProperty on a numeric property of a native array is not supported yet\n"; + expect(r.exitCode).toBe(0); + expect(r.stdout).toBe(`${fence}${fence}1\n`); + }); +}); diff --git a/tests/harness/library-mode.test.ts b/tests/harness/library-mode.test.ts index ffd42bd8e..0d4052952 100644 --- a/tests/harness/library-mode.test.ts +++ b/tests/harness/library-mode.test.ts @@ -351,6 +351,7 @@ bump: 1 2 note: 1 2 indexed: 72100 recall: a,b +globals: undefined fenced `; test("K4: three init sessions are byte-identical", async () => { diff --git a/tests/harness/npm.test.ts b/tests/harness/npm.test.ts index 2df0d87ef..461b7fbcd 100644 --- a/tests/harness/npm.test.ts +++ b/tests/harness/npm.test.ts @@ -102,7 +102,7 @@ const KNOWN: Record = { "package-imports": { build: "SC2013" }, "promise-bridge": { build: "SC1090" }, "self-name": { build: "SC2013" }, - "stream-shims": { run: "SC1090" }, + "stream-shims": { run: "SC2020" }, "util-shims": { run: "SC2020" }, "web-streams": { run: "SC1090" }, "workspace-copied": { build: "SC2013" }, @@ -112,9 +112,18 @@ const KNOWN: Record = { const cases = shardSelect(npmCases(fixturesRoot), (c) => c.name); /** Per-case timeouts past the default 120 s: the undici cases compile the - * built-in undici implementation's module graph, which a sanitized build - * on a CI runner takes minutes to compile. */ -const TIMEOUTS: Record = { undici5: 600_000, undici7: 600_000 }; + * built-in undici implementation's module graph, and the zod 3 cases + * (zod3, zod3-adoption, zod3-boundary, zod3-infer) compile zod 3's + * 3,700-line types module. A sanitized build of either takes minutes on a + * CI runner. */ +const TIMEOUTS: Record = { + undici5: 600_000, + undici7: 600_000, + zod3: 600_000, + "zod3-adoption": 600_000, + "zod3-boundary": 600_000, + "zod3-infer": 600_000, +}; interface RunResult { stdout: Buffer; diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index c0176eb93..091d3f0b5 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -108,7 +108,7 @@ for (const backend of ["llvm"] as const) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" }, + runtime_abi: { version: 36, marker: "scr_runtime_abi_v36" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: [], sanitizer: "external-toolchain-required" }, flavors: { diff --git a/tests/harness/split-tools.test.ts b/tests/harness/split-tools.test.ts new file mode 100644 index 000000000..8bdefdf0e --- /dev/null +++ b/tests/harness/split-tools.test.ts @@ -0,0 +1,296 @@ +import { execFileSync, spawnSync } from "node:child_process"; +import { + cpSync, + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + symlinkSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import ts from "typescript"; +import { afterEach, expect, test } from "vitest"; + +// The repository root supplies typescript and oxfmt to the split tools. +const root = fileURLToPath(new URL("../..", import.meta.url)); +const tool = (name: string): string => join(root, "scripts/split", name); +const dirs: string[] = []; + +afterEach(() => { + for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true }); +}); + +const compilerOptions = { + strict: true, + noEmit: true, + target: "ES2022", + module: "ESNext", + moduleResolution: "Bundler", +}; + +/** A checkout holding `files` (paths relative to its root) and a tsconfig. */ +function checkout(files: Record): string { + const dir = mkdtempSync(join(tmpdir(), "split-tools-")); + dirs.push(dir); + const all = { ...files, "tsconfig.json": JSON.stringify({ compilerOptions }) }; + for (const [path, text] of Object.entries(all)) { + mkdirSync(dirname(join(dir, path)), { recursive: true }); + writeFileSync(join(dir, path), text); + } + return dir; +} + +function run(script: string, args: string[], repo: string): void { + execFileSync(process.execPath, [tool(script), ...args, "--repo", repo, "--tools", root], { + cwd: repo, + stdio: "pipe", + }); +} + +/** Type errors in the checkout's files. */ +function diagnostics(dir: string): string[] { + const parsed = ts.getParsedCommandLineOfConfigFile( + join(dir, "tsconfig.json"), + {}, + { + ...ts.sys, + onUnRecoverableConfigFileDiagnostic: () => {}, + }, + ); + const program = ts.createProgram(parsed!.fileNames, parsed!.options); + return ts + .getPreEmitDiagnostics(program) + .map((d) => ts.flattenDiagnosticMessageText(d.messageText, "\n")); +} + +test("split-methods keeps an import a staying method uses as a shorthand property", () => { + const dir = checkout({ + "src/helper.ts": "export const helper = 2;\n", + "src/host.ts": [ + 'import { helper } from "./helper.js";', + "", + "export class Host {", + " value = 1;", + " keep(): { helper: number } {", + " return { helper };", + " }", + " moved(): number {", + " return this.value + 1;", + " }", + "}", + "", + ].join("\n"), + "split.json": JSON.stringify({ + module: "src/host.ts", + class: "Host", + methods: { "moved.ts": ["moved"] }, + }), + }); + run("split-methods.mjs", ["apply", join(dir, "split.json")], dir); + expect(readFileSync(join(dir, "src/host.ts"), "utf8")).toContain( + 'import { helper } from "./helper.js";', + ); + expect(diagnostics(dir)).toEqual([]); +}); + +/** An oxfmt.config.ts that records it ran by writing `marker`. */ +const recordingConfig = (marker: string): string => + [ + 'import { writeFileSync } from "node:fs";', + `writeFileSync(${JSON.stringify(marker)}, "ran");`, + "export default {};", + "", + ].join("\n"); + +test("split-methods formats with the tools checkout's configuration, not the branch's", () => { + const dir = checkout({ + "src/host.ts": [ + "export class Host {", + " value = 1;", + " moved(): number {", + " return this.value + 1;", + " }", + "}", + "", + ].join("\n"), + "split.json": JSON.stringify({ + module: "src/host.ts", + class: "Host", + methods: { "moved.ts": ["moved"] }, + }), + }); + const markers = [join(dir, "root-config-ran"), join(dir, "nested-config-ran")]; + writeFileSync(join(dir, "oxfmt.config.ts"), recordingConfig(markers[0]!)); + writeFileSync(join(dir, "src/oxfmt.config.ts"), recordingConfig(markers[1]!)); + run("split-methods.mjs", ["apply", join(dir, "split.json")], dir); + expect(readFileSync(join(dir, "src/moved.ts"), "utf8")).toContain("export function moved"); + for (const marker of markers) expect(existsSync(marker)).toBe(false); +}); + +test("move-state formats with the tools checkout's configuration, not the branch's", () => { + const dir = checkout({ + "src/host.ts": + "export class Host {\n count = 0;\n moved(): number {\n return this.count + 1;\n }\n}\n", + "state.template": "", + "split.json": JSON.stringify({ + module: "src/host.ts", + class: "Host", + dir: "src", + create: { "host-state.ts": "state.template" }, + groups: [ + { file: "host-state.ts", key: "COUNTS", interfaceName: "Counts", fields: ["count"] }, + ], + }), + }); + const markers = [join(dir, "root-config-ran"), join(dir, "nested-config-ran")]; + writeFileSync(join(dir, "oxfmt.config.ts"), recordingConfig(markers[0]!)); + writeFileSync(join(dir, "src/oxfmt.config.ts"), recordingConfig(markers[1]!)); + run("move-state.mjs", ["apply", join(dir, "split.json")], dir); + expect(readFileSync(join(dir, "src/host.ts"), "utf8")).toContain("COUNTS.of(this).count"); + for (const marker of markers) expect(existsSync(marker)).toBe(false); +}); + +test("split-module imports a moved class's local base class as a value", () => { + const dir = checkout({ + "src/shapes.ts": [ + "export class Base {", + " x = 1;", + "}", + "", + "export class Derived extends Base {", + " y = 2;", + "}", + "", + "export function base(): Base {", + " return new Base();", + "}", + "", + ].join("\n"), + "split.json": JSON.stringify({ + tsconfig: "tsconfig.json", + module: "src/shapes.ts", + seeds: { "derived.ts": ["Derived"] }, + manifest: "manifest.json", + }), + }); + const config = join(dir, "split.json"); + run("split-module.mjs", ["plan", config], dir); + run("split-module.mjs", ["apply", config], dir); + expect(readFileSync(join(dir, "src/derived.ts"), "utf8")).toContain( + 'import { Base } from "./shapes.js";', + ); + expect(diagnostics(dir)).toEqual([]); +}); + +/* A copy of tests/fixtures/split-tools/symlink: `repo/` is the checkout the + * tools rewrite, and its committed `src/linked.ts` links to `outside/linked.ts` + * next to it. */ +const symlinkFixture = fileURLToPath(new URL("../fixtures/split-tools/symlink", import.meta.url)); +const outsideText = readFileSync(join(symlinkFixture, "outside/linked.ts"), "utf8"); + +function symlinkCheckout(): { dir: string; repo: string } { + const dir = mkdtempSync(join(tmpdir(), "split-tools-")); + dirs.push(dir); + cpSync(symlinkFixture, dir, { recursive: true, verbatimSymlinks: true }); + return { dir, repo: join(dir, "repo") }; +} + +/** Runs a tool that must refuse; answers its stderr. */ +function refused(script: string, config: object, dir: string, repo: string): string { + const path = join(dir, "split.json"); + writeFileSync(path, JSON.stringify(config)); + const result = spawnSync( + process.execPath, + [tool(script), "apply", path, "--repo", repo, "--tools", root], + { cwd: repo, encoding: "utf8" }, + ); + expect(result.status).not.toBe(0); + return result.stderr; +} + +test("split-methods refuses to write a new file through a symlink that leaves the checkout", () => { + const { dir, repo } = symlinkCheckout(); + // A link to a file that doesn't exist yet: writing through it would create the file. + symlinkSync("../../outside/created.ts", join(repo, "src/moved.ts")); + const stderr = refused( + "split-methods.mjs", + { module: "src/host.ts", class: "Host", methods: { "moved.ts": ["moved"] } }, + dir, + repo, + ); + expect(stderr).toContain("refusing"); + expect(existsSync(join(dir, "outside/created.ts"))).toBe(false); +}); + +test("split-methods refuses a module that links outside the checkout", () => { + const { dir, repo } = symlinkCheckout(); + const stderr = refused( + "split-methods.mjs", + { module: "src/linked.ts", class: "Host", methods: { "moved.ts": ["moved"] } }, + dir, + repo, + ); + expect(stderr).toContain("refusing"); +}); + +test("split-module refuses a module that links outside the checkout", () => { + const { dir, repo } = symlinkCheckout(); + writeFileSync(join(repo, "tsconfig.json"), JSON.stringify({ compilerOptions })); + writeFileSync(join(dir, "manifest.json"), JSON.stringify({ "moved.ts": ["outside"] })); + const stderr = refused( + "split-module.mjs", + { tsconfig: "tsconfig.json", module: "src/linked.ts", manifest: "manifest.json" }, + dir, + repo, + ); + expect(stderr).toContain("refusing"); +}); + +test("move-state refuses a state file that a branch commits as a link outside the checkout", () => { + const { dir, repo } = symlinkCheckout(); + // The group file is listed under `create` and already exists as a link, so + // creation skips it and the move would append the state through the link. + symlinkSync("../../outside/linked.ts", join(repo, "src/host-state.ts")); + writeFileSync(join(dir, "state.template"), ""); + const stderr = refused( + "move-state.mjs", + { + module: "src/host.ts", + class: "Host", + dir: "src", + create: { "host-state.ts": "state.template" }, + groups: [ + { file: "host-state.ts", key: "COUNTS", interfaceName: "Counts", fields: ["count"] }, + ], + }, + dir, + repo, + ); + expect(stderr).toContain("refusing"); + expect(readFileSync(join(dir, "outside/linked.ts"), "utf8")).toBe(outsideText); +}); + +test("move-state refuses to rewrite a file that links outside the checkout", () => { + const { dir, repo } = symlinkCheckout(); + writeFileSync(join(repo, "src/host-state.ts"), ""); + const stderr = refused( + "move-state.mjs", + { + module: "src/host.ts", + class: "Host", + dir: "src", + groups: [ + { file: "host-state.ts", key: "COUNTS", interfaceName: "Counts", fields: ["count"] }, + ], + replacements: [{ file: "linked.ts", find: "export const outside = 1;", replace: "" }], + }, + dir, + repo, + ); + expect(stderr).toContain("refusing"); + expect(readFileSync(join(dir, "outside/linked.ts"), "utf8")).toBe(outsideText); +}); diff --git a/tests/harness/stream-entry-kinds.test.ts b/tests/harness/stream-entry-kinds.test.ts index ec89c5914..b0aa52294 100644 --- a/tests/harness/stream-entry-kinds.test.ts +++ b/tests/harness/stream-entry-kinds.test.ts @@ -1,14 +1,9 @@ /* Stream chunk kinds Node cannot oracle — scriptc-only. * - * A typed read() answers `Buffer | null`. When the read itself makes the - * stream hold strings (the first string a lazy Readable.from pull over a - * dynamic array produces, or setEncoding() inside _read), Node returns - * the string, which the typed result cannot hold, so the read throws the - * encoding fence instead (caught here, so the program exits normally). - * These pins keep that refusal exact and, under - * SCRIPTC_SAN=1 (ASan and the RC audit, like every harness lane), prove - * no string reaches Buffer code. The Node-observable cases are corpus - * programs (stream-dynamic-entry-kinds.*). + * Under SCRIPTC_SAN=1 (ASan and the RC audit, like every harness lane) + * these pins prove no string reaches Buffer code. The Node-observable + * cases are corpus programs (stream-dynamic-entry-kinds.*, and + * 7439-readable-read-string-chunks for read() of strings). */ import { execFile } from "node:child_process"; import { createHash } from "node:crypto"; @@ -73,71 +68,7 @@ function stripAsanFiberWarning(s: string): string { ); } -const READ_FENCE_MESSAGE = - "read() on a stream with an encoding set is not supported yet (consume 'data' events, which deliver strings)"; - describe(`typed stream reads of string chunks (scriptc-only${sanitize ? ", sanitized" : ""})`, () => { - test("a typed read() whose lazy pull yields the first string throws the encoding fence", async () => { - const r = await compileAndRun( - "typed-read-json-string", - `import { Readable } from "node:stream"; -const r = Readable.from(JSON.parse('["j1", "j2"]')); -try { - const chunk = r.read(); - console.log("hex", chunk === null ? "null" : chunk.toString("hex")); -} catch (err) { - console.log("caught", (err as Error).message); -} -`, - ); - expect(r.exitCode).toBe(0); - expect(r.stdout).toBe(`caught ${READ_FENCE_MESSAGE}\n`); - }); - - test("a typed read() whose _read calls setEncoding() throws the encoding fence", async () => { - const r = await compileAndRun( - "typed-read-encoding-in-read", - `import { Readable } from "node:stream"; -const r = new Readable({ - read() { - this.setEncoding("utf8"); - this.push(Buffer.from("ab")); - this.push(null); - }, -}); -try { - const chunk = r.read(); - console.log("hex", chunk === null ? "null" : chunk.toString("hex")); -} catch (err) { - console.log("caught", (err as Error).message); -} -`, - ); - expect(r.exitCode).toBe(0); - expect(r.stdout).toBe(`caught ${READ_FENCE_MESSAGE}\n`); - }); - - test("the JavaScript lane's read() on a JSON source throws the same fence", async () => { - const r = await compileAndRun( - "js-read-json-string", - `'use strict'; -const { Readable } = require('node:stream'); -const r = Readable.from(JSON.parse('["j1"]')); -r.once('readable', () => { - try { - const chunk = r.read(); - console.log('got', chunk === null ? 'null' : chunk.length); - } catch (err) { - console.log('caught', err.message); - } -}); -`, - "cjs", - ); - expect(r.exitCode).toBe(0); - expect(r.stdout).toBe(`caught ${READ_FENCE_MESSAGE}\n`); - }); - test("a dynamic source that mixes Buffers and strings yields each as it is, as Node does", async () => { const r = await compileAndRun( "mixed-dynamic-source", diff --git a/tests/library-mode/reinit/lib.ts b/tests/library-mode/reinit/lib.ts index dad54090a..73e466f70 100644 --- a/tests/library-mode/reinit/lib.ts +++ b/tests/library-mode/reinit/lib.ts @@ -29,4 +29,22 @@ export function indexedUnicode(): number { return s.length + s.charCodeAt(n) + s.slice(n - 1, n + 2).length; } +// A symbol-keyed global access creates the global object before any read of +// `globalThis` in a session: the builtin names must still be fenced on the +// stored global object, in every session. +const probeKey = Symbol.for("scriptc.reinit"); +function stored(global: typeof globalThis): any { + return global; +} +export function globalFences(): string { + // @ts-expect-error: globalThis has no symbol index signature + const before: unknown = globalThis[probeKey]; + const view = stored(globalThis); + try { + return `${typeof before} ${typeof view.Reflect}`; + } catch { + return `${typeof before} fenced`; + } +} + console.log(`session start counter=${counter}`); diff --git a/tests/library-mode/reinit/probe.c b/tests/library-mode/reinit/probe.c index 8cd17be1e..409766d0d 100644 --- a/tests/library-mode/reinit/probe.c +++ b/tests/library-mode/reinit/probe.c @@ -12,6 +12,7 @@ extern double kr_bump(void); extern double kr_note(const uint8_t *p, size_t len); extern double kr_indexed_unicode(void); extern void kr_recall(const uint8_t **out, size_t *out_len); +extern void kr_global_fences(const uint8_t **out, size_t *out_len); static void sink(void *ctx, const uint8_t *msg, size_t len, uint64_t addr) { (void)ctx; (void)addr; @@ -30,6 +31,8 @@ static void session(void) { const uint8_t *s; size_t n; kr_recall(&s, &n); printf("recall: %.*s\n", (int)n, s); + kr_global_fences(&s, &n); + printf("globals: %.*s\n", (int)n, s); } int main(void) { diff --git a/tests/library-mode/reinit/profile.json b/tests/library-mode/reinit/profile.json index 620150db1..cfcfebc9a 100644 --- a/tests/library-mode/reinit/profile.json +++ b/tests/library-mode/reinit/profile.json @@ -14,6 +14,7 @@ { "export": "bump", "symbol": "kr_bump", "params": [], "returns": "f64" }, { "export": "note", "symbol": "kr_note", "params": ["string"], "returns": "f64" }, { "export": "recall", "symbol": "kr_recall", "params": [], "returns": "string" }, - { "export": "indexedUnicode", "symbol": "kr_indexed_unicode", "params": [], "returns": "f64" } + { "export": "indexedUnicode", "symbol": "kr_indexed_unicode", "params": [], "returns": "f64" }, + { "export": "globalFences", "symbol": "kr_global_fences", "params": [], "returns": "string" } ] }