diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f131747fa..b3bf0e770 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -524,7 +524,7 @@ jobs: if: ${{ needs.validation_scope.outputs.full == 'true' && needs.lanes.outputs.linux == 'true' }} name: test (tsgo and tsc-ts give the same IR) runs-on: ubuntu-24.04 - timeout-minutes: 30 + timeout-minutes: 60 steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 diff --git a/.gitignore b/.gitignore index cd44fe11d..b271af8e1 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ output/ /packages/runtime-*/runtime-pack.json /packages/runtime-*/.runtime-pack-* !tests/fixtures/npm/node_modules/*/dist/ +!tests/fixtures/npm/node_modules/@*/*/dist/ !tests/fixtures/npm/workspace/*/dist/ !tests/fixtures/npm/cases/workspace-copied/members/*/dist/ !tests/fixtures/npm/cases/workspace-copied/node_modules/*/dist/ diff --git a/benchmarks/runtime/README.md b/benchmarks/runtime/README.md index e177e36c0..4f152e4bb 100644 --- a/benchmarks/runtime/README.md +++ b/benchmarks/runtime/README.md @@ -12,6 +12,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea | `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/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/benchmarks/runtime/workloads/index-cursors.json b/benchmarks/runtime/workloads/index-cursors.json new file mode 100644 index 000000000..24f2a263c --- /dev/null +++ b/benchmarks/runtime/workloads/index-cursors.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/index-cursors.ts", + "args": [ + "20000" + ] +} diff --git a/benchmarks/runtime/workloads/index-cursors.ts b/benchmarks/runtime/workloads/index-cursors.ts new file mode 100644 index 000000000..b65ded46b --- /dev/null +++ b/benchmarks/runtime/workloads/index-cursors.ts @@ -0,0 +1,25 @@ +// Arrays of indices walked by loop-carried cursors: each step reads the +// next index from the array and relinks the visited entries, so the +// cursor and every stored value are small integers read back from the +// array itself. +const N = 4096; + +function relink(rounds: number): number { + const next: number[] = []; + for (let i = 0; i < N; i++) next.push(i + 1 < N ? i + 1 : 0); + let at = next[0]!; + let total = 0; + for (let r = 0; r < rounds; r++) { + for (let s = 0; s < N; s++) { + const t = next[at]!; + next[at] = next[t]!; + next[t] = at; + at = t; + total = (total + at) | 0; + } + } + return total; +} + +const rounds = Number(process.argv[2] ?? "1"); +console.log("relink", relink(rounds)); diff --git a/docs/content/docs/coverage.mdx b/docs/content/docs/coverage.mdx index 954b040db..db1193b98 100644 --- a/docs/content/docs/coverage.mdx +++ b/docs/content/docs/coverage.mdx @@ -97,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. diff --git a/docs/content/docs/dependencies.mdx b/docs/content/docs/dependencies.mdx index 409b9cb30..a9c5ae138 100644 --- a/docs/content/docs/dependencies.mdx +++ b/docs/content/docs/dependencies.mdx @@ -53,7 +53,7 @@ Type-only imports follow Node.js's type stripping. `import type` and `export typ scriptc compiles a package automatically when it: - ships its own type declarations, or is typed by its DefinitelyTyped `@types/` package, or is a dependency of another package that has no declarations at all, and -- ships readable JavaScript: unminified, or minified by a bundler whose helpers keep their names. +- ships JavaScript that isn't a webpack bundle. Minified JavaScript compiles like unminified JavaScript. The declarations check your program; the package's JavaScript compiles with the types scriptc infers from it. A program that doesn't typecheck against the inferred types, for example because an `@types/` package describes another release, fails the build at that use. @@ -92,6 +92,13 @@ scriptc implements a few packages itself and compiles that implementation in pla - graceful-fs's retry queue, which fs-extra uses under Node.js, isn't part of the implementation, so a program that runs out of file descriptors fails at the first `EMFILE` error instead of retrying. - An ES module that imports fs-extra can use any of its exports by name. Node.js only exposes the names its CommonJS lexer finds in fs-extra's source, so a name like `readFile` throws a `SyntaxError` there. +`@hono/node-server` 2.x is built in. `serve`, `createAdaptorServer`, `getRequestListener` and `RequestError` serve a fetch handler such as Hono's `app.fetch` over `node:http` with native `Request` and `Response` objects, and write each response the way the package does under Node.js: the same status line, header names, header order and content length, for buffered and streamed bodies, including the package's shortcut for a response that is still exactly what its constructor received. Request bodies stream to the handler as it reads them, and a body the handler leaves unread is drained after the response, up to 64 MiB or 500 ms, as the package does. Known differences: + +- Only HTTP/1.1 servers are declared: the `createServer` and `serverOptions` options for `node:https` and `node:http2`, the `websocket` option and `upgradeWebSocket` aren't declared, so using one is a type error at build time. Subpath modules such as `@hono/node-server/serve-static` compile from the package's own files. +- The fetch handler must return a `Response` or a promise of one; the handler's second argument holds `incoming` and `outgoing`. +- The global `Request` and `Response` stay the built-in classes, so `overrideGlobalObjects` has no effect, and a `Response` reads its default text content type as `text/plain;charset=UTF-8` where the package's replacement reads `text/plain; charset=UTF-8`. Written responses carry the package's spelling. +- A streamed response is written without waiting for `drain`, and a failed handler answers 504 only for an error whose `name` is `TimeoutError`. + Small polyfill packages whose module is exactly a Node.js built-in on Node.js 24 compile as that built-in, at the `require()` that loads them, from your program's CommonJS files, from compiled packages, and from esbuild bundles whose module paths name the package and its version (as pnpm installs do). The package's own files aren't compiled. diff --git a/docs/content/limitations/builtin-apis/crypto.mdx b/docs/content/limitations/builtin-apis/crypto.mdx index 971065408..e4b555806 100644 --- a/docs/content/limitations/builtin-apis/crypto.mdx +++ b/docs/content/limitations/builtin-apis/crypto.mdx @@ -8,7 +8,10 @@ `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, 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. + `createCipheriv` and `createDecipheriv` support aes-128, aes-192, and aes-256 in CBC, CTR, GCM, CCM, ECB, CFB, CFB8, CFB1, and OFB modes and as key wrap (the wrap and wrap-pad ciphers and their -inv forms), 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. + `getCipherInfo` answers Node's info for every cipher OpenSSL names, including the ones without a native implementation. + `getCipherInfo` options whose class defines `keyLength` or `ivLength` throw a coded SC2020 error. + The result is Node's object; the SIV ciphers and the SM4 GCM, CCM, and XTS ciphers have no `nid`, and reading it where the declared `CipherInfo` type gives `number` throws a TypeError. 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. @@ -29,6 +32,8 @@ 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/streams-and-readline.mdx b/docs/content/limitations/builtin-apis/streams-and-readline.mdx index fe6bb2dfb..45a784742 100644 --- a/docs/content/limitations/builtin-apis/streams-and-readline.mdx +++ b/docs/content/limitations/builtin-apis/streams-and-readline.mdx @@ -18,7 +18,7 @@ Stages may take the `{ signal }` argument. 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. + In typed code, a pipeline callback with an async function or async generator destination receives `null` for no error when its error parameter cannot hold `undefined` (@types/node declares it `NodeJS.ErrnoException | null`), and `undefined` when it can, as in Node. `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 may take the `{ signal }` argument, `map` may return any value, and `flatMap` may return any iterable, async iterable, or stream. @@ -32,9 +32,10 @@ `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. + The `construct` option and `_construct` method, `decodeStrings`, and `defaultEncoding` work on every stream class, and the `writev` option and `_writev` method on every class with a writable side. + A `decodeStrings` or `defaultEncoding` value known only at run time is unsupported after an option member whose value has effects (a call or an assignment). + In typed code, a `writev` entry's `encoding` declared as a string receives `""` where Node passes `undefined` or `null` (an object-mode write). `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. The `history`, `historySize`, `removeHistoryDuplicates`, `completer`, `tabSize`, and `escapeCodeTimeout` options are validated as in Node. @@ -42,7 +43,7 @@ The output's `'resize'` event does not redraw the line. The cursor never stops between the two UTF-16 units of a supplementary character: where Node's Up or Down movement through a multiline line would, the cursor moves to the character's start or end. In JavaScript, a `'history'` listener receives a copy of the history; its changes are written back when the listener returns, but the copy is not `rl.history` itself. - `rl.history`, `historySize`, `historyIndex`, `crlfDelay`, `tabSize`, `escapeCodeTimeout`, `line`, `cursor`, and `terminal` read as in Node, except that `rl.cursor` of an interface that is not a terminal throws instead of answering `undefined`. + `rl.history`, `historySize`, `historyIndex`, `crlfDelay`, `tabSize`, `escapeCodeTimeout`, `line`, `cursor`, and `terminal` read as in Node; `rl.cursor` is `undefined` on an interface that is not a terminal. `rl[Symbol.dispose]()` closes the interface, and `instanceof` with `readline.Interface` or `readlinePromises.Interface` answers whether the interface came from that module, as in Node. In JavaScript, the input and output can be any expression that evaluates to a stream, such as a function's result or an object's member; a value that is not a stream is unsupported. In JavaScript, `question()` and `setPrompt()` accept a value that is not a string: when such a prompt is written, an interface that is not a terminal throws `ERR_INVALID_ARG_TYPE` as Node does (the `readline/promises` question rejects with it) and the question stays pending, and a terminal interface writes `String(value)`. diff --git a/docs/content/limitations/record-identity-and-property-order.mdx b/docs/content/limitations/record-identity-and-property-order.mdx index 0381803f6..cc16f74d5 100644 --- a/docs/content/limitations/record-identity-and-property-order.mdx +++ b/docs/content/limitations/record-identity-and-property-order.mdx @@ -10,9 +10,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/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index c341da852..c3f1835ab 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -11660,9 +11660,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "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 9393fcaa8..ba8692b49 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -19332,6 +19332,10 @@ "tests/corpus/7151-cipher-legacy-errors.cjs", "tests/corpus/7152-crypto-prime.ts", "tests/corpus/7153-crypto-prime-errors.cjs", + "tests/corpus/7154-crypto-cipher-info.cjs", + "tests/corpus/7155-cipher-aes-ccm.cjs", + "tests/corpus/7156-cipher-key-wrap.cjs", + "tests/corpus/7157-crypto-cipher-info-typed.ts", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/node24-crypto-kdf.ts", @@ -19719,6 +19723,8 @@ "status": "partial", "evidence": "surface-manifest:node-builtin.crypto.createCipheriv", "tests": [ + "tests/corpus/7155-cipher-aes-ccm.cjs", + "tests/corpus/7156-cipher-key-wrap.cjs", "tests/corpus/7150-cipher-legacy-modes.ts", "tests/corpus/7151-cipher-legacy-errors.cjs", "tests/corpus/5250-cipher-aes-gcm.ts", @@ -19760,6 +19766,8 @@ "status": "partial", "evidence": "surface-manifest:node-builtin.crypto.createDecipheriv", "tests": [ + "tests/corpus/7155-cipher-aes-ccm.cjs", + "tests/corpus/7156-cipher-key-wrap.cjs", "tests/corpus/7150-cipher-legacy-modes.ts", "tests/corpus/7151-cipher-legacy-errors.cjs", "tests/corpus/5250-cipher-aes-gcm.ts", @@ -20434,8 +20442,12 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.crypto.getCipherInfo" + "status": "partial", + "evidence": "surface-manifest:node-builtin.crypto.getCipherInfo", + "tests": [ + "tests/corpus/7154-crypto-cipher-info.cjs", + "tests/corpus/7157-crypto-cipher-info-typed.ts" + ] }, "anchorSource": "exact" }, diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 06aad26cf..100882c1a 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -44,8 +44,8 @@ "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.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.createCipheriv": ["tests/corpus/7155-cipher-aes-ccm.cjs", "tests/corpus/7156-cipher-key-wrap.cjs", "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/7155-cipher-aes-ccm.cjs", "tests/corpus/7156-cipher-key-wrap.cjs", "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"], @@ -124,6 +124,7 @@ "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.getCipherInfo": ["tests/corpus/7154-crypto-cipher-info.cjs", "tests/corpus/7157-crypto-cipher-info-typed.ts"], "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/test/native-link-info.test.ts b/packages/cli/test/native-link-info.test.ts index bf5b3be14..8ac579449 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: 33, marker: "scr_runtime_abi_v33" }, + runtime_abi: { version: 37, marker: "scr_runtime_abi_v37" }, 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_v33"); + expect(error?.stderr).toContain("scr_runtime_abi_v37"); }); }); diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index b195c3fbb..d7abe70df 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -3171,13 +3171,17 @@ 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, 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(). */ + /* Cipheriv/Decipheriv: aes-128/192/256 in cbc, ctr, gcm, ccm, ecb, cfb, + * cfb8, cfb1, and ofb modes and their key wraps, des-ede3 and des-ede, + * chacha20, and chacha20-poly1305. Both are Transforms: written chunks go + * through update(), and the end pushes final(). */ + export type CipherCCMTypes = "aes-128-ccm" | "aes-192-ccm" | "aes-256-ccm"; export type CipherGCMTypes = "aes-128-gcm" | "aes-192-gcm" | "aes-256-gcm"; export type CipherChaCha20Poly1305Types = "chacha20-poly1305"; export type CipherKey = BinaryLike | KeyObject; + export interface CipherCCMOptions { + authTagLength: number; + } export interface CipherGCMOptions { authTagLength?: number; } @@ -3197,6 +3201,10 @@ declare module "crypto" { final(outputEncoding: BufferEncoding): string; setAutoPadding(autoPadding?: boolean): this; } + export interface CipherCCM extends Cipheriv { + setAAD(buffer: ArrayBufferView, options: { plaintextLength: number }): this; + getAuthTag(): Buffer; + } export interface CipherGCM extends Cipheriv { setAAD(buffer: ArrayBufferView, options?: { plaintextLength: number }): this; getAuthTag(): Buffer; @@ -3218,6 +3226,10 @@ declare module "crypto" { final(outputEncoding: BufferEncoding): string; setAutoPadding(autoPadding?: boolean): this; } + export interface DecipherCCM extends Decipheriv { + setAuthTag(buffer: ArrayBufferView): this; + setAAD(buffer: ArrayBufferView, options: { plaintextLength: number }): this; + } export interface DecipherGCM extends Decipheriv { setAuthTag(buffer: ArrayBufferView): this; setAAD(buffer: ArrayBufferView, options?: { plaintextLength: number }): this; @@ -3226,6 +3238,12 @@ declare module "crypto" { setAuthTag(buffer: ArrayBufferView): this; setAAD(buffer: ArrayBufferView, options?: { plaintextLength: number }): this; } + export function createCipheriv( + algorithm: CipherCCMTypes, + key: CipherKey, + iv: BinaryLike, + options: CipherCCMOptions, + ): CipherCCM; export function createCipheriv( algorithm: CipherGCMTypes, key: CipherKey, @@ -3244,6 +3262,12 @@ declare module "crypto" { iv: BinaryLike | null, options?: { authTagLength?: number }, ): Cipheriv; + export function createDecipheriv( + algorithm: CipherCCMTypes, + key: CipherKey, + iv: BinaryLike, + options: CipherCCMOptions, + ): DecipherCCM; export function createDecipheriv( algorithm: CipherGCMTypes, key: CipherKey, @@ -3468,6 +3492,34 @@ declare module "crypto" { * per member like http2.constants. */ export function getFips(): 1 | 0; export function getCiphers(): string[]; + export type CipherMode = + | "cbc" + | "ccm" + | "cfb" + | "ctr" + | "ecb" + | "gcm" + | "ocb" + | "ofb" + | "stream" + | "wrap" + | "xts"; + export interface CipherInfoOptions { + keyLength?: number | undefined; + ivLength?: number | undefined; + } + export interface CipherInfo { + name: string; + nid: number; + blockSize?: number | undefined; + ivLength?: number | undefined; + keyLength: number; + mode: CipherMode; + } + export function getCipherInfo( + nameOrNid: string | number, + options?: CipherInfoOptions, + ): CipherInfo | undefined; export function getHashes(): string[]; export function getCurves(): string[]; /* Node v24 crypto.constants, literal-typed (the Http2Constants @@ -5561,6 +5613,7 @@ declare module "http" { readonly trailersDistinct: { [name: string]: string[] | undefined }; readonly rawTrailers: string[]; resume(): void; + pause(): void; destroy(): void; setTimeout(msecs: number, callback?: () => void): this; /* setEncoding('utf8'): 'data' delivers strings (other real encodings @@ -5986,6 +6039,7 @@ declare module "http2" { export interface Http2Stream { on(event: "error" | "close", listener: (err: Error) => void): void; once(event: "error" | "close", listener: (err: Error) => void): void; + addListener(event: "error" | "close", listener: (err: Error) => void): void; destroy(): void; } export interface Http2ServerRequest { @@ -6086,11 +6140,6 @@ declare module "http2" { listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, ): void; on(event: "session", listener: (session: ServerHttp2Session) => void): void; - once( - event: "stream", - listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, - ): void; - once(event: "session", listener: (session: ServerHttp2Session) => void): void; /* HTTP CONNECT (and, in Node, the h2 extended CONNECT — which the * allowHTTP1 lowering never sees): the second argument is the raw * socket on HTTP/1.1 connections; narrow with instanceof. */ @@ -6108,6 +6157,42 @@ declare module "http2" { ): void; once(event: "error", listener: (err: Error) => void): void; once(event: "close", listener: () => void): void; + once(event: "sessionError", listener: (err: Error, session: ServerHttp2Session) => void): void; + once( + event: "stream", + listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, + ): void; + once(event: "session", listener: (session: ServerHttp2Session) => void): void; + once( + event: "connect", + listener: ( + req: Http2ServerRequest | import("http").IncomingMessage, + resOrSocket: Http2ServerResponse | Socket, + ) => void, + ): void; + addListener(event: "connection", listener: (socket: Socket) => void): void; + addListener( + event: "request", + listener: (req: Http2ServerRequest, res: Http2ServerResponse) => void, + ): void; + addListener(event: "error", listener: (err: Error) => void): void; + addListener(event: "close", listener: () => void): void; + addListener( + event: "sessionError", + listener: (err: Error, session: ServerHttp2Session) => void, + ): void; + addListener( + event: "stream", + listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, + ): void; + addListener(event: "session", listener: (session: ServerHttp2Session) => void): void; + addListener( + event: "connect", + listener: ( + req: Http2ServerRequest | import("http").IncomingMessage, + resOrSocket: Http2ServerResponse | Socket, + ) => void, + ): void; } export interface SecureServerOptions { allowHTTP1?: boolean; @@ -6449,6 +6534,28 @@ declare module "http2" { listener: (errorCode: number, lastStreamID: number, opaqueData?: Uint8Array) => void, ): void; once(event: "localSettings" | "remoteSettings", listener: (settings: any) => void): void; + once(event: "frameError", listener: (type: number, code: number, id: number) => void): void; + once(event: "ping", listener: (payload: Uint8Array) => void): void; + addListener(event: "close" | "timeout", listener: () => void): void; + addListener(event: "error", listener: (err: Error) => void): void; + addListener( + event: "connect", + listener: (session: Http2Session, socket: import("net").Socket) => void, + ): void; + addListener( + event: "stream", + listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, + ): void; + addListener( + event: "goaway", + listener: (errorCode: number, lastStreamID: number, opaqueData?: Uint8Array) => void, + ): void; + addListener(event: "localSettings" | "remoteSettings", listener: (settings: any) => void): void; + addListener( + event: "frameError", + listener: (type: number, code: number, id: number) => void, + ): void; + addListener(event: "ping", listener: (payload: Uint8Array) => void): void; } export interface ClientHttp2Session extends Http2Session { @@ -6526,7 +6633,30 @@ declare module "http2" { listener: () => void, ): void; once(event: "error", listener: (err: Error) => void): void; + once(event: "frameError", listener: (type: number, code: number, id: number) => void): void; once(event: "trailers", listener: (trailers: IncomingHttpHeaders, flags: number) => void): void; + addListener(event: "data", listener: (chunk: any) => void): void; + addListener( + event: + | "end" + | "close" + | "aborted" + | "ready" + | "timeout" + | "drain" + | "finish" + | "wantTrailers", + listener: () => void, + ): void; + addListener(event: "error", listener: (err: Error) => void): void; + addListener( + event: "frameError", + listener: (type: number, code: number, id: number) => void, + ): void; + addListener( + event: "trailers", + listener: (trailers: IncomingHttpHeaders, flags: number) => void, + ): void; respond( headers?: OutgoingHttpHeaders, options?: { endStream?: boolean; waitForTrailers?: boolean }, @@ -6576,6 +6706,7 @@ declare module "http2" { on(event: "trailers", listener: (trailers: IncomingHttpHeaders, flags: number) => void): void; once(event: "response", listener: (headers: IncomingHttpHeaders, flags: number) => void): void; once(event: "push", listener: (headers: IncomingHttpHeaders, flags: number) => void): void; + once(event: "headers", listener: (headers: IncomingHttpHeaders, flags: number) => void): void; once(event: "data", listener: (chunk: any) => void): void; once( event: @@ -6590,6 +6721,42 @@ declare module "http2" { listener: () => void, ): void; once(event: "error", listener: (err: Error) => void): void; + once(event: "frameError", listener: (type: number, code: number, id: number) => void): void; + once(event: "trailers", listener: (trailers: IncomingHttpHeaders, flags: number) => void): void; + addListener( + event: "response", + listener: (headers: IncomingHttpHeaders, flags: number) => void, + ): void; + addListener( + event: "push", + listener: (headers: IncomingHttpHeaders, flags: number) => void, + ): void; + addListener( + event: "headers", + listener: (headers: IncomingHttpHeaders, flags: number) => void, + ): void; + addListener(event: "data", listener: (chunk: any) => void): void; + addListener( + event: + | "end" + | "close" + | "aborted" + | "ready" + | "timeout" + | "drain" + | "finish" + | "wantTrailers", + listener: () => void, + ): void; + addListener(event: "error", listener: (err: Error) => void): void; + addListener( + event: "frameError", + listener: (type: number, code: number, id: number) => void, + ): void; + addListener( + event: "trailers", + listener: (trailers: IncomingHttpHeaders, flags: number) => void, + ): void; } export interface Http2Server { @@ -6624,6 +6791,24 @@ declare module "http2" { ): void; once(event: "error", listener: (err: Error) => void): void; once(event: "close" | "listening" | "timeout", listener: () => void): void; + once(event: "connection", listener: (socket: import("net").Socket) => void): void; + once(event: "sessionError", listener: (err: Error, session: ServerHttp2Session) => void): void; + addListener( + event: "stream", + listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, + ): void; + addListener(event: "session", listener: (session: ServerHttp2Session) => void): void; + addListener( + event: "request", + listener: (req: Http2ServerRequest, res: Http2ServerResponse) => void, + ): void; + addListener(event: "error", listener: (err: Error) => void): void; + addListener(event: "close" | "listening" | "timeout", listener: () => void): void; + addListener(event: "connection", listener: (socket: import("net").Socket) => void): void; + addListener( + event: "sessionError", + listener: (err: Error, session: ServerHttp2Session) => void, + ): void; } export interface ServerOptions { diff --git a/packages/compiler/native/cli.profdata.gz b/packages/compiler/native/cli.profdata.gz index aa0e9c36b..b5f1e079f 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 eeead7eff..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 diff --git a/packages/compiler/shims/hono-node-server/v2.ts b/packages/compiler/shims/hono-node-server/v2.ts new file mode 100644 index 000000000..a4dcc6805 --- /dev/null +++ b/packages/compiler/shims/hono-node-server/v2.ts @@ -0,0 +1,645 @@ +// scriptc's built-in implementation of `@hono/node-server` 2.x: the Node.js +// adapter that serves a fetch-style handler (Hono's `app.fetch`) over +// node:http. The published package swaps the global Request and Response +// for lightweight objects and reparents prototypes, which only pays off +// under V8; this port builds native Request objects from the incoming +// message, with a body streamed from it as the app reads it, and writes +// the handler's Response the way the package's listener does (header +// record, content length for bodies that complete within the first reads, +// streamed bodies, error responses), so a client sees the same status +// lines, headers and bodies. A body the app leaves unread is drained after +// the response, within the package's limits. +// +// HTTP/1.1 only: the http2/https server options and WebSocket upgrades are +// not declared. A Response that is still exactly its constructor arguments +// takes the package's cached-response path (its lightweight Response), read +// from the runtime through scriptcResponseLight; every other Response takes +// the general path. +import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http"; +import type { AddressInfo } from "node:net"; + +export type HttpBindings = { + incoming: IncomingMessage; + outgoing: ServerResponse; +}; + +export type ServerType = Server; + +export type FetchCallback = (request: Request, env: HttpBindings) => Response | Promise; + +export type CustomErrorHandler = (err: unknown) => void | Response | Promise; + +export type Options = { + fetch: FetchCallback; + overrideGlobalObjects?: boolean; + autoCleanupIncoming?: boolean; + port?: number; + hostname?: string; +}; + +export interface RequestListenerOptions { + hostname?: string; + errorHandler?: CustomErrorHandler; + overrideGlobalObjects?: boolean; + autoCleanupIncoming?: boolean; +} + +export class RequestError extends Error { + constructor(message: string, options?: { cause?: unknown }) { + super(message, options); + this.name = "RequestError"; + } +} + +const DEFAULT_CONTENT_TYPE = "text/plain; charset=UTF-8"; +const X_ALREADY_SENT = "x-hono-already-sent"; + +const reValidRequestUrl = /^\/[!#$&-;=?-[\]_a-z~]*$/; +const reDotSegment = /\/\.\.?(?:[/?#]|$)/; +const reValidHost = /^[a-z0-9._-]+(?::(?:[1-5]\d{3,4}|[6-9]\d{3}))?$/; + +function buildUrl(scheme: string, host: string, incomingUrl: string): string { + const url = `${scheme}://${host}${incomingUrl}`; + if (!reValidHost.test(host)) { + const urlObj = new URL(url); + if ( + urlObj.hostname.length !== host.length && + urlObj.hostname !== (host.includes(":") ? host.replace(/:\d+$/, "") : host).toLowerCase() + ) + throw new RequestError("Invalid host header"); + return urlObj.href; + } + if (incomingUrl.length === 0) return url + "/"; + if (incomingUrl.charCodeAt(0) !== 47) throw new RequestError("Invalid URL"); + if (!reValidRequestUrl.test(incomingUrl) || reDotSegment.test(incomingUrl)) + return new URL(url).href; + return url; +} + +function normalizeIncomingMethod(method: string | undefined): string { + if (method === undefined || method.length === 0) return "GET"; + switch (method) { + case "DELETE": + case "GET": + case "HEAD": + case "OPTIONS": + case "PATCH": + case "POST": + case "PUT": + case "QUERY": + return method; + } + const upper = method.toUpperCase(); + switch (upper) { + case "DELETE": + case "GET": + case "HEAD": + case "OPTIONS": + case "POST": + case "PUT": + return upper; + default: + return method; + } +} + +/** The request URL the package builds: an absolute request target as is, + * otherwise the Host header (or the configured hostname) over http. */ +function requestUrl(incoming: IncomingMessage, defaultHostname: string | undefined): string { + const incomingUrl = incoming.url ?? ""; + if ( + incomingUrl[0] !== "/" && + (incomingUrl.startsWith("http://") || incomingUrl.startsWith("https://")) + ) { + try { + return new URL(incomingUrl).href; + } catch (e) { + throw new RequestError("Invalid absolute URL", { cause: e }); + } + } + const hostHeader = incoming.headers.host; + const host = hostHeader !== undefined && hostHeader !== "" ? hostHeader : (defaultHostname ?? ""); + if (host === "") throw new RequestError("Missing host header"); + try { + return buildUrl("http", host, incomingUrl); + } catch (e) { + if (e instanceof RequestError) throw e; + throw new RequestError("Invalid URL", { cause: e }); + } +} + +function requestHeaders(incoming: IncomingMessage): Headers { + const headers = new Headers(); + const raw = incoming.rawHeaders; + for (let i = 0; i < raw.length; i += 2) { + const name = raw[i]!; + if (!name.startsWith(":")) headers.append(name, raw[i + 1]!); + } + return headers; +} + +/** What the listener knows about the incoming body: whether it ended + * (the package's `readableEnded`) and whether a drain started. */ +type IncomingState = { ended: boolean; draining: boolean }; + +/** The incoming message's readableHighWaterMark: Readable.toWeb queues + * this many bytes before it pauses the message. */ +const BODY_HIGH_WATER_MARK = 64 * 1024; + +/** The request body as the package streams it: Readable.toWeb over the + * incoming message, read one chunk per pull. Chunks queue until + * BODY_HIGH_WATER_MARK bytes wait unread, then the message pauses until a + * pull takes them, so the socket holds the rest. The stream pulls once on + * its own when it is constructed; the package builds its stream only when + * the app reads the body, so until the app's first read a drain discards + * the chunks instead of queueing them. */ +function incomingBody(incoming: IncomingMessage, state: IncomingState): ReadableStream { + let pulls = 0; + let listening = false; + let ended = false; + let failure: Error | undefined = undefined; + const queue: Buffer[] = []; + let queued = 0; + let wake: (() => void) | undefined = undefined; + const woken = (): void => { + const resolve = wake; + wake = undefined; + if (resolve !== undefined) resolve(); + }; + const listen = (): void => { + listening = true; + incoming.on("data", (chunk: Buffer) => { + if (state.draining && pulls === 1) return; + queue.push(chunk); + queued += chunk.length; + if (queued >= BODY_HIGH_WATER_MARK) incoming.pause(); + woken(); + }); + incoming.on("end", () => { + ended = true; + woken(); + }); + incoming.on("error", (err: Error) => { + failure = err; + woken(); + }); + }; + return new ReadableStream({ + async pull(controller) { + pulls++; + if (!listening) listen(); + for (;;) { + const chunk = queue.shift(); + if (chunk !== undefined) { + queued -= chunk.length; + if (queued < BODY_HIGH_WATER_MARK) incoming.resume(); + controller.enqueue(chunk); + return; + } + if (failure !== undefined) { + controller.error(failure); + return; + } + if (ended) { + controller.close(); + return; + } + await new Promise((resolve) => { + wake = resolve; + }); + } + }, + }); +} + +function newRequest( + incoming: IncomingMessage, + defaultHostname: string | undefined, + state: IncomingState, +): Request { + const method = normalizeIncomingMethod(incoming.method); + const url = requestUrl(incoming, defaultHostname); + const headers = requestHeaders(incoming); + if (method === "GET" || method === "HEAD") return new Request(url, { method, headers }); + return new Request(url, { + method, + headers, + body: incomingBody(incoming, state), + duplex: "half", + }); +} + +const DRAIN_TIMEOUT_MS = 500; +const MAX_DRAIN_BYTES = 64 * 1024 * 1024; + +/** The package's drain of a body the app left unread: read and discard + * up to MAX_DRAIN_BYTES or DRAIN_TIMEOUT_MS, then close the connection + * once its writes are out. */ +function drainIncoming(incoming: IncomingMessage, state: IncomingState): void { + const socket = incoming.socket; + if (socket.destroyed || state.draining) return; + state.draining = true; + let bytesRead = 0; + let done = false; + const cleanup = (): void => { + if (done) return; + done = true; + clearTimeout(timer); + }; + const forceClose = (): void => { + if (done) return; + cleanup(); + if (!socket.destroyed) socket.destroySoon(); + }; + const timer = setTimeout(forceClose, DRAIN_TIMEOUT_MS); + timer.unref(); + incoming.on("data", (chunk: Buffer) => { + if (done) return; + bytesRead += chunk.length; + if (bytesRead > MAX_DRAIN_BYTES) forceClose(); + }); + incoming.on("end", cleanup); + incoming.on("error", cleanup); + incoming.resume(); +} + +/** The package's close handler cleanup: an unread body drains two timer + * turns after the response closes. */ +function drainAfterClose(incoming: IncomingMessage, state: IncomingState): void { + if (state.ended) return; + setTimeout(() => { + if (!state.ended) + setTimeout(() => { + drainIncoming(incoming, state); + }); + }); +} + +/** The package's outgoing header record: Headers order (lowercase, + * sorted), Set-Cookie values gathered into one array entry at the end, + * and the default content type for a response with a body. */ +function buildOutgoingHttpHeaders( + headers: Headers, + hasBody: boolean, +): Record { + const res: Record = {}; + if (headers.has("set-cookie")) { + const cookies: string[] = []; + headers.forEach((v, k) => { + if (k === "set-cookie") cookies.push(v); + else res[k] = v; + }); + // Headers.forEach joins nothing but set-cookie, which getSetCookie + // lists value by value, as the package's iteration does. + if (cookies.length > 0) res["set-cookie"] = headers.getSetCookie(); + } else { + headers.forEach((v, k) => { + res[k] = v; + }); + } + if (hasBody && res["content-type"] === undefined) res["content-type"] = DEFAULT_CONTENT_TYPE; + return res; +} + +/* Spelled out rather than ReadableStreamReadResult, whose + * declaration depends on the program's lib and @types/node settings: the + * shim compiles inside every program that imports the package. */ +type ReadResult = { done: boolean; value?: Uint8Array }; + +function readWithoutBlocking(read: Promise): Promise { + return Promise.race([read, Promise.resolve().then(() => Promise.resolve(undefined))]); +} + +function nextTick(): Promise { + return new Promise((resolve) => { + setTimeout(resolve); + }); +} + +/** Writes the rest of a body stream. As in the package, a response that + * closes before the stream ends (the client disconnected) cancels the + * stream, so its producer stops; the pending read then answers done. A + * response that fails closes too, and its stream is cancelled the same way + * (the package passes the error as the reason). The listener stays attached + * and does nothing once the stream has ended. */ +async function pump( + reader: ReadableStreamDefaultReader, + outgoing: ServerResponse, + pending: Promise | undefined, +): Promise { + let finished = false; + outgoing.on("close", () => { + if (!finished) reader.cancel().catch(() => {}); + }); + try { + let read = pending ?? reader.read(); + for (;;) { + const { done, value } = await read; + if (done) { + outgoing.end(); + return; + } + outgoing.write(value!); + read = reader.read(); + } + } finally { + finished = true; + } +} + +function handleResponseError(e: unknown, outgoing: ServerResponse): void { + const err = e instanceof Error ? e : new Error("unknown error", { cause: e }); + console.error(e); + if (!outgoing.headersSent) outgoing.writeHead(500, { "Content-Type": "text/plain" }); + outgoing.end(`Error: ${err.message}`); + outgoing.destroy(); +} + +function emptyResponse(status: number): Response { + return new Response(null, { status }); +} + +/** The lightweight state of a constructed Response (the runtime's + * scr_fetch_response_light): [form, flags, byte length, ...record pairs]. */ +declare function scriptcResponseLight(res: Response): string[]; + +const UNDICI_TEXT_TYPE = "text/plain;charset=UTF-8"; + +/** Content-Length as Node's implicit header: set the headers one by one and + * let end(body) compute the length, which Node then writes after its own + * Date/Connection headers, as the package's preset length does. */ +function endWithImplicitLength( + outgoing: ServerResponse, + status: number, + header: Record, + body: Uint8Array, +): void { + outgoing.statusCode = status; + for (const name of Object.keys(header)) outgoing.setHeader(name, header[name]!); + outgoing.end(body); +} + +/** The package's cached-response path: a Response that is still exactly + * its constructor arguments (no body or metadata read since) is written + * from them. Form 1 has no init headers, form 2 Headers, form 3 a plain + * record kept verbatim. */ +async function writeLightResponse( + res: Response, + outgoing: ServerResponse, + light: string[], + head: boolean, +): Promise { + const form = light[0]!; + const flags = light[1]!; + const length = Number(light[2]!); + const status = res.status; + const sized = length >= 0; + if (form === "1") { + if (length === -1) { + outgoing.writeHead(status); + outgoing.end(); + } else if (sized) { + const body = await res.bytes(); + if (head) { + outgoing.writeHead(status, { + "Content-Type": DEFAULT_CONTENT_TYPE, + "Content-Length": String(length), + }); + outgoing.end(body); + } else + endWithImplicitLength(outgoing, status, { "Content-Type": DEFAULT_CONTENT_TYPE }, body); + } else { + outgoing.writeHead(status, { "Content-Type": DEFAULT_CONTENT_TYPE }); + outgoing.flushHeaders(); + await pump(res.body!.getReader(), outgoing, undefined); + } + return; + } + let header: Record; + let hasContentLength = false; + let plain = false; + let canAutoLength = true; + if (form === "2") { + const headers = new Headers(res.headers); + if (flags.includes("a") && headers.get("content-type") === UNDICI_TEXT_TYPE) { + if (flags.includes("d")) headers.set("content-type", DEFAULT_CONTENT_TYPE); + else headers.delete("content-type"); + } else if (flags.includes("d") && !headers.has("content-type")) { + headers.set("content-type", DEFAULT_CONTENT_TYPE); + } + hasContentLength = headers.has("content-length"); + header = buildOutgoingHttpHeaders(headers, length !== -1); + } else { + plain = true; + header = {}; + for (let i = 3; i + 1 < light.length; i += 2) { + const name = light[i]!; + const value = light[i + 1]!; + const previous = header[name]; + if (previous === undefined) header[name] = value; + else if (typeof previous === "string") header[name] = [previous, value]; + else previous.push(value); + const lower = name.toLowerCase(); + if (lower === "content-length") hasContentLength = true; + else if (lower === "transfer-encoding" || lower === "trailer") canAutoLength = false; + } + } + const body = sized ? await res.bytes() : undefined; + if (!hasContentLength && body !== undefined) { + if (plain && canAutoLength && !head && status !== 204 && status !== 304) { + endWithImplicitLength(outgoing, status, header, body); + return; + } + header["Content-Length"] = String(length); + } + outgoing.writeHead(status, header); + if (length === -1) outgoing.end(); + else if (body !== undefined) outgoing.end(body); + else { + outgoing.flushHeaders(); + await pump(res.body!.getReader(), outgoing, undefined); + } +} + +async function writeResponse( + res: Response, + outgoing: ServerResponse, + head: boolean, +): Promise { + const light = scriptcResponseLight(res); + if (light[0] !== "0") return writeLightResponse(res, outgoing, light, head); + const body = res.body; + const record = buildOutgoingHttpHeaders(res.headers, body !== null); + if (body !== null) { + const reader: ReadableStreamDefaultReader = body.getReader(); + const values: Uint8Array[] = []; + let done = false; + let pending: Promise | undefined = undefined; + if (record["transfer-encoding"] !== "chunked") { + let maxReadCount = 2; + for (let i = 0; i < maxReadCount; i++) { + pending ??= reader.read(); + let chunk: ReadResult | undefined = undefined; + try { + chunk = await readWithoutBlocking(pending); + } catch (e) { + console.error(e); + done = true; + } + if (chunk === undefined) { + if (i === 1 && !done) { + await nextTick(); + maxReadCount = 3; + continue; + } + break; + } + pending = undefined; + if (chunk.value !== undefined) values.push(chunk.value); + if (chunk.done) { + done = true; + break; + } + } + if (done && record["content-length"] === undefined) { + let length = 0; + for (const value of values) length += value.length; + record["content-length"] = String(length); + } + } + outgoing.writeHead(res.status, record); + for (const value of values) outgoing.write(value); + if (done) outgoing.end(); + else { + if (values.length === 0) outgoing.flushHeaders(); + await pump(reader, outgoing, pending); + } + } else if (record[X_ALREADY_SENT] === undefined) { + outgoing.writeHead(res.status, record); + outgoing.end(); + } +} + +async function respond( + pending: Response | Promise, + outgoing: ServerResponse, + errorHandler: CustomErrorHandler | undefined, + head: boolean, +): Promise { + let res: Response; + if (pending instanceof Promise) { + try { + res = await pending; + } catch (err) { + if (errorHandler === undefined) { + res = handleFetchError(err); + } else { + const errRes = await errorHandler(err); + if (errRes === undefined) return; + res = errRes; + } + } + } else res = pending; + await writeResponse(res, outgoing, head); +} + +/** A Response the package writes from its cache without waiting: still + * exactly its constructor arguments, with no body or a string or bytes + * body. */ +function immediateCacheable(res: Response): boolean { + const light = scriptcResponseLight(res); + return light[0] !== "0" && Number(light[2]!) >= -1; +} + +function handleFetchError(e: unknown): Response { + return emptyResponse(e instanceof Error && e.name === "TimeoutError" ? 504 : 500); +} + +async function handleRequest( + fetchCallback: FetchCallback, + options: RequestListenerOptions, + incoming: IncomingMessage, + outgoing: ServerResponse, +): Promise { + let req: Request | undefined = undefined; + let res: Response | Promise | undefined = undefined; + const state: IncomingState = { ended: false, draining: false }; + let needsBodyCleanup = false; + let closeHandlerAttached = false; + const ensureCloseHandler = (): void => { + if (req === undefined || closeHandlerAttached) return; + closeHandlerAttached = true; + if (needsBodyCleanup) + outgoing.on("close", () => { + drainAfterClose(incoming, state); + }); + }; + try { + req = newRequest(incoming, options.hostname, state); + needsBodyCleanup = + options.autoCleanupIncoming !== false && + !(incoming.method === "GET" || incoming.method === "HEAD"); + if (needsBodyCleanup) + incoming.on("end", () => { + state.ended = true; + }); + res = fetchCallback(req, { incoming, outgoing }); + if (!(res instanceof Promise) && immediateCacheable(res)) { + if (needsBodyCleanup) + outgoing.once("finish", () => { + if (!state.ended) drainIncoming(incoming, state); + }); + } else ensureCloseHandler(); + } catch (e) { + if (res !== undefined) return handleResponseError(e, outgoing); + const errorHandler = options.errorHandler; + if (errorHandler !== undefined) { + ensureCloseHandler(); + const handled = await errorHandler( + req !== undefined ? e : e instanceof RequestError ? e : toRequestError(e), + ); + if (handled === undefined) return; + res = handled; + } else if (req === undefined) res = emptyResponse(400); + else res = handleFetchError(e); + } + try { + await respond(res, outgoing, options.errorHandler, incoming.method === "HEAD"); + } catch (e) { + handleResponseError(e, outgoing); + } +} + +function toRequestError(e: unknown): RequestError { + return new RequestError(e instanceof Error ? e.message : String(e), { cause: e }); +} + +export function getRequestListener( + fetchCallback: FetchCallback, + options: RequestListenerOptions = {}, +): (incoming: IncomingMessage, outgoing: ServerResponse) => Promise { + return (incoming, outgoing) => handleRequest(fetchCallback, options, incoming, outgoing); +} + +export function createAdaptorServer(options: Options): ServerType { + const listener = getRequestListener(options.fetch, { + hostname: options.hostname, + overrideGlobalObjects: options.overrideGlobalObjects, + autoCleanupIncoming: options.autoCleanupIncoming, + }); + return createServer((incoming, outgoing) => { + void listener(incoming, outgoing); + }); +} + +export function serve( + options: Options, + listeningListener?: (info: AddressInfo) => void, +): ServerType { + const server = createAdaptorServer(options); + server.listen(options.port ?? 3000, options.hostname, () => { + const info = server.address() as AddressInfo; + if (listeningListener !== undefined) listeningListener(info); + }); + return server; +} 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 21380de8f..379ad60ee 100644 --- a/packages/compiler/src/backend/immutability.ts +++ b/packages/compiler/src/backend/immutability.ts @@ -3,6 +3,7 @@ import type { DynamicFieldStores } from "../ir/int32-slots.js"; import type { IrExpr, IrModule, IrStmt } from "../ir/ir.js"; import { everyExprChild, everyStmtChild } from "../ir/traverse.js"; +import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "../ir/node-index.js"; import { computeCycleMutability, constructorEscapePoint, @@ -97,7 +98,10 @@ export interface ConstructionFacts { privateStores: Set; } -export function computeConstructionFacts(mod: IrModule): ConstructionFacts { +/** The nodes that can reach a constructor with a reused receiver. */ +const CONSTRUCTOR_USE_KINDS: readonly IrNodeKind[] = ["call", "closure"]; + +export function computeConstructionFacts(mod: IrModule, nodes?: IrNodeIndex): ConstructionFacts { const facts: ConstructionFacts = { privateStores: new Set() }; if (mod.lib !== undefined) return facts; const classes = new Map((mod.classes ?? []).map((c) => [c.name, c] as const)); @@ -122,6 +126,7 @@ export function computeConstructionFacts(mod: IrModule): ConstructionFacts { c = classes.get(c)!.base; } }; + const scan = nodes?.scan(CONSTRUCTOR_USE_KINDS); for (const fn of mod.functions) { const own = ctorClass.get(fn.name); const thisId = own !== undefined ? thisOf(own) : null; @@ -129,22 +134,19 @@ export function computeConstructionFacts(mod: IrModule): ConstructionFacts { own !== undefined && classes.get(own)!.base !== undefined ? `%${classes.get(own)!.base}.constructor` : null; - const expr = (e: IrExpr): boolean => { - if (e.kind === "call") { - const target = ctorClass.get(e.callee); + eachNodeOfKinds(fn, scan, (node) => { + if (node.kind === "call") { + const target = ctorClass.get(node.callee); if ( target !== undefined && - !(e.callee === baseCtor && e.args.length > 0 && isThisRef(e.args[0]!, thisId)) + !(node.callee === baseCtor && node.args.length > 0 && isThisRef(node.args[0]!, thisId)) ) markReused(target); - } else if (e.kind === "closure") { - const target = ctorClass.get(e.fnName); + } else if (node.kind === "closure") { + const target = ctorClass.get(node.fnName); if (target !== undefined) markReused(target); } - return everyExprChild(e, expr, stmt); - }; - const stmt = (s: IrStmt): boolean => everyStmtChild(s, expr, stmt); - fn.body.every(stmt); + }); } // The first escaping statement of each constructor (body length: none). @@ -192,32 +194,32 @@ export function computeConstructionFacts(mod: IrModule): ConstructionFacts { return facts; } +/** The nodes that write a binding: statement and expression assignments. */ +const ASSIGNMENT_KINDS: readonly IrNodeKind[] = ["assign", "assignExpr"]; + /** Module constants whose value never changes once initialized: immutable, * not TDZ-checked (every read follows the initialization), no import-cycle * init flag, and at most one assignment in the whole module. Such a global * owns its value for the rest of the program, so a read of it can be * borrowed across any later operand or call. */ -export function stableGlobals(mod: IrModule): Set { +export function stableGlobals(mod: IrModule, nodes?: IrNodeIndex): Set { const candidates = new Set( (mod.globals ?? []) .filter((g) => !g.mutable && g.tdz !== true && g.initFlag === undefined) .map((g) => g.id), ); const assigned = new Set(); - const expr = (e: IrExpr): boolean => { - if (e.kind === "assignExpr" && candidates.has(e.localId)) candidates.delete(e.localId); - return everyExprChild(e, expr, stmt); - }; - const stmt = (s: IrStmt): boolean => { - if (s.kind === "assign" && candidates.has(s.localId)) { - if (assigned.has(s.localId)) candidates.delete(s.localId); - assigned.add(s.localId); - } - return everyStmtChild(s, expr, stmt); - }; + const scan = nodes?.scan(ASSIGNMENT_KINDS); for (const fn of mod.functions) { if (candidates.size === 0) break; - fn.body.every(stmt); + eachNodeOfKinds(fn, scan, (node) => { + if (node.kind === "assignExpr" && candidates.has(node.localId)) + candidates.delete(node.localId); + else if (node.kind === "assign" && candidates.has(node.localId)) { + if (assigned.has(node.localId)) candidates.delete(node.localId); + assigned.add(node.localId); + } + }); } return candidates; } diff --git a/packages/compiler/src/backend/llvm/appended-array-literals.ts b/packages/compiler/src/backend/llvm/appended-array-literals.ts index 09a2976de..3c09904e3 100644 --- a/packages/compiler/src/backend/llvm/appended-array-literals.ts +++ b/packages/compiler/src/backend/llvm/appended-array-literals.ts @@ -9,8 +9,8 @@ * one directly or through a binding read from it. The choice never changes * behavior. */ -import type { IrExpr, IrModule, IrStmt } from "../../ir/ir.js"; -import { everyStmtList } from "../../ir/traverse.js"; +import type { IrExpr, IrModule } from "../../ir/ir.js"; +import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "../../ir/node-index.js"; const APPENDING_METHODS: ReadonlySet = new Set([ "push", @@ -30,7 +30,27 @@ function isEmptyLiteral(e: IrExpr): boolean { ); } -export function findAppendedArrayLiterals(module: IrModule): ReadonlySet { +/** The nodes that name an appended binding or field, and the stores that + * initialize one. */ +const TARGET_KINDS: readonly IrNodeKind[] = [ + "arrIntrinsic", + "assignExpr", + "arraySet", + "varDecl", + "assign", +]; +const STORE_KINDS: readonly IrNodeKind[] = [ + "assignExpr", + "varDecl", + "assign", + "fieldSet", + "recordSet", +]; + +export function findAppendedArrayLiterals( + module: IrModule, + index?: IrNodeIndex, +): ReadonlySet { // Bindings are function-scoped by id; module globals are shared. const localsOf = new Map>(); for (const fn of module.functions) { @@ -65,20 +85,15 @@ export function findAppendedArrayLiterals(module: IrModule): ReadonlySet const field = fieldOf(target); if (field !== null) appendedFields.add(field); }; + const targets = index?.scan(TARGET_KINDS); for (const fn of module.functions) { - everyStmtList(fn.body, { - expr: (e) => { - if (e.kind === "arrIntrinsic" && APPENDING_METHODS.has(e.method)) - noteTarget(fn.name, e.receiver); - else if (e.kind === "assignExpr") noteAlias(fn.name, e.localId, e.value); - return true; - }, - stmt: (s) => { - if (s.kind === "arraySet") noteTarget(fn.name, s.arr); - else if (s.kind === "varDecl") noteAlias(fn.name, s.localId, s.init); - else if (s.kind === "assign") noteAlias(fn.name, s.localId, s.value); - return true; - }, + eachNodeOfKinds(fn, targets, (node) => { + if (node.kind === "arrIntrinsic" && APPENDING_METHODS.has(node.method)) + noteTarget(fn.name, node.receiver); + else if (node.kind === "assignExpr") noteAlias(fn.name, node.localId, node.value); + else if (node.kind === "arraySet") noteTarget(fn.name, node.arr); + else if (node.kind === "varDecl") noteAlias(fn.name, node.localId, node.init); + else if (node.kind === "assign") noteAlias(fn.name, node.localId, node.value); }); } for (const binding of appendedBindings) @@ -89,23 +104,18 @@ export function findAppendedArrayLiterals(module: IrModule): ReadonlySet const consider = (value: IrExpr | null, appended: boolean): void => { if (value && appended && isEmptyLiteral(value)) result.add(value); }; + const stores = index?.scan(STORE_KINDS); for (const fn of module.functions) { const binding = (localId: string): boolean => appendedBindings.has(bindingKey(fn.name, localId)); - everyStmtList(fn.body, { - expr: (e) => { - if (e.kind === "assignExpr") consider(e.value, binding(e.localId)); - return true; - }, - stmt: (s: IrStmt) => { - if (s.kind === "varDecl") consider(s.init, binding(s.localId)); - else if (s.kind === "assign") consider(s.value, binding(s.localId)); - else if (s.kind === "fieldSet") - consider(s.value, appendedFields.has(fieldKey(s.className, s.field))); - else if (s.kind === "recordSet") - consider(s.value, appendedFields.has(fieldKey(s.shapeId, s.field))); - return true; - }, + eachNodeOfKinds(fn, stores, (node) => { + if (node.kind === "assignExpr") consider(node.value, binding(node.localId)); + else if (node.kind === "varDecl") consider(node.init, binding(node.localId)); + else if (node.kind === "assign") consider(node.value, binding(node.localId)); + else if (node.kind === "fieldSet") + consider(node.value, appendedFields.has(fieldKey(node.className, node.field))); + else if (node.kind === "recordSet") + consider(node.value, appendedFields.has(fieldKey(node.shapeId, node.field))); }); } return result; 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/constant-callbacks.ts b/packages/compiler/src/backend/llvm/constant-callbacks.ts index cf5c1b062..4b277dab7 100644 --- a/packages/compiler/src/backend/llvm/constant-callbacks.ts +++ b/packages/compiler/src/backend/llvm/constant-callbacks.ts @@ -1,9 +1,28 @@ import type { IrExpr, IrFunction, IrModule } from "../../ir/ir.js"; -import { exprFunctionReference, moduleFunctionReferences } from "../../ir/function-references.js"; +import { + exprFunctionReference, + FUNCTION_REFERENCE_KINDS, + moduleFunctionReferences, +} from "../../ir/function-references.js"; import { writtenLocals } from "../../ir/callback-specialization.js"; +import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "../../ir/node-index.js"; import { everyStmtList } from "../../ir/traverse.js"; import type { CallLifetimes } from "./call-lifetimes.js"; +/** The nodes the body census below reads. */ +const CENSUS_KINDS: readonly IrNodeKind[] = [ + "varDecl", + "assign", + "forOf", + "rethrow", + "tryCatch", + "call", + "callValue", + "assignExpr", + "incDec", + ...FUNCTION_REFERENCE_KINDS, +]; + /** Closed direct-call consumers can devirtualize a callback when every * incoming call supplies the same function. Keep its evaluated environment, * arguments and ordinary ownership; only the loaded code pointer changes. @@ -15,6 +34,7 @@ import type { CallLifetimes } from "./call-lifetimes.js"; export function findConstantCallbacks( mod: IrModule, lifetimes: CallLifetimes, + index?: IrNodeIndex, ): Map> { const functions = new Map(mod.functions.map((fn) => [fn.name, fn])); const definitions = new Map>(); @@ -50,7 +70,7 @@ export function findConstantCallbacks( const writes = new Set(); // Parameters and captures each function (or a closure sharing the // binding) writes after entry. - const rebinds = writtenLocals(mod); + const rebinds = writtenLocals(mod, index); // A function named anywhere but as a direct call's callee can be entered // by a generated adapter or the runtime, which a direct-call census cannot // specialize: the entry, exports, class helpers and methods here, and @@ -66,6 +86,7 @@ export function findConstantCallbacks( // The initializer of each immutable function-typed local declared once: // its only write, even when a capture box (not a stable slot) holds it. const constDefinitions = new Map>(); + const scan = index?.scan(CENSUS_KINDS); for (const fn of mod.functions) { const defs = new Map(); const consts = new Map(); @@ -85,51 +106,64 @@ export function findConstantCallbacks( const emptyDecls = new Map(); const assigned = new Map(); const rebound = new Set(); - everyStmtList(fn.body, { - stmt: (stmt) => { - if (stmt.kind === "varDecl" && stmt.init?.type.kind === "func" && stable?.has(stmt.localId)) - defs.set(stmt.localId, stmt.init); - if (stmt.kind === "varDecl" && immutable.has(stmt.localId)) - consts.set(stmt.localId, consts.has(stmt.localId) ? null : stmt.init); - if (stmt.kind === "varDecl" && hoisted.has(stmt.localId)) { - if (stmt.init !== null) rebound.add(stmt.localId); - emptyDecls.set(stmt.localId, (emptyDecls.get(stmt.localId) ?? 0) + 1); - } - if (stmt.kind === "assign" && hoisted.has(stmt.localId)) { - const values = assigned.get(stmt.localId) ?? []; - values.push(stmt.value); - assigned.set(stmt.localId, values); - } - if (stmt.kind === "forOf" || stmt.kind === "rethrow") rebound.add(stmt.localId); - if (stmt.kind === "tryCatch" && stmt.catchLocalId !== null) rebound.add(stmt.catchLocalId); - if (stmt.kind === "assign") { - if (globalIds.has(stmt.localId)) { - if (globals.has(stmt.localId)) writes.add(stmt.localId); - else globals.set(stmt.localId, stmt.value); + eachNodeOfKinds(fn, scan, (node) => { + switch (node.kind) { + case "varDecl": + if (node.init?.type.kind === "func" && stable?.has(node.localId)) + defs.set(node.localId, node.init); + if (immutable.has(node.localId)) + consts.set(node.localId, consts.has(node.localId) ? null : node.init); + if (hoisted.has(node.localId)) { + if (node.init !== null) rebound.add(node.localId); + emptyDecls.set(node.localId, (emptyDecls.get(node.localId) ?? 0) + 1); } + return; + case "assign": + if (hoisted.has(node.localId)) { + const values = assigned.get(node.localId) ?? []; + values.push(node.value); + assigned.set(node.localId, values); + } + if (globalIds.has(node.localId)) { + if (globals.has(node.localId)) writes.add(node.localId); + else globals.set(node.localId, node.value); + } + return; + case "forOf": + case "rethrow": + rebound.add(node.localId); + return; + case "tryCatch": + if (node.catchLocalId !== null) rebound.add(node.catchLocalId); + return; + case "call": + if (consumers.has(node.callee)) { + const calls = incoming.get(node.callee) ?? []; + calls.push({ owner: fn.name, args: node.args }); + incoming.set(node.callee, calls); + } + return; + case "callValue": + if (node.callee.kind === "varRef") callees.add(node.callee.localId); + return; + case "closure": { + escaped.add(node.fnName); + const sites = closureSites.get(node.fnName) ?? []; + sites.push({ owner: fn.name, captures: node.captures }); + closureSites.set(node.fnName, sites); + return; } - return true; - }, - expr: (expr) => { - if (expr.kind === "call" && consumers.has(expr.callee)) { - const calls = incoming.get(expr.callee) ?? []; - calls.push({ owner: fn.name, args: expr.args }); - incoming.set(expr.callee, calls); + case "orDefault": { + const referenced = exprFunctionReference(node); + if (referenced !== undefined) escaped.add(referenced); + return; } - if (expr.kind === "callValue" && expr.callee.kind === "varRef") - callees.add(expr.callee.localId); - const referenced = exprFunctionReference(expr); - if (referenced !== undefined) escaped.add(referenced); - if (expr.kind === "closure") { - const sites = closureSites.get(expr.fnName) ?? []; - sites.push({ owner: fn.name, captures: expr.captures }); - closureSites.set(expr.fnName, sites); - } - if ((expr.kind === "assignExpr" || expr.kind === "incDec") && globalIds.has(expr.localId)) - writes.add(expr.localId); - if (expr.kind === "assignExpr" || expr.kind === "incDec") rebound.add(expr.localId); - return true; - }, + case "assignExpr": + case "incDec": + if (globalIds.has(node.localId)) writes.add(node.localId); + rebound.add(node.localId); + return; + } }); for (const [id, values] of assigned) if (emptyDecls.get(id) === 1 && values.length === 1 && !rebound.has(id)) diff --git a/packages/compiler/src/backend/llvm/dense-array-access.ts b/packages/compiler/src/backend/llvm/dense-array-access.ts index 95612e57c..df37d9c58 100644 --- a/packages/compiler/src/backend/llvm/dense-array-access.ts +++ b/packages/compiler/src/backend/llvm/dense-array-access.ts @@ -505,8 +505,12 @@ export function emitDenseArrayState( indexExpr: IrExpr | undefined, kind: "state" | "has", width: number, + /** The index is a dense array loop counter its entry test proved below + * the hole-free prefix (provenDenseIndex): the slot is a present value. */ + proven = false, ): string { const B = host.B; + if (proven) return kind === "state" ? f64Lit(SCR_ARR_VALUE) : "true"; const fn = kind === "state" ? "scr_arr_state" : "scr_arr_has"; const ty = kind === "state" ? "double" : "i1"; host.declare(`declare ${kind === "state" ? "double" : "zeroext i1"} @${fn}(ptr, double)`); diff --git a/packages/compiler/src/backend/llvm/dense-array-loops.test.ts b/packages/compiler/src/backend/llvm/dense-array-loops.test.ts index f7a4bc892..a88c38575 100644 --- a/packages/compiler/src/backend/llvm/dense-array-loops.test.ts +++ b/packages/compiler/src/backend/llvm/dense-array-loops.test.ts @@ -1,9 +1,10 @@ import { expect, test } from "vitest"; import { BOOL, F64, arrayOf, type IrExpr, type IrLocal, type IrStmt } from "../../ir/ir.js"; -import type { IntegerCountedForLoop } from "../../ir/integer-loops.js"; +import type { IntegerCountedForLoop, IntegerPairedForLoop } from "../../ir/integer-loops.js"; import { countedDenseShape, matchDenseArrayLoop, + pairedDenseShape, type DenseLoopShape, } from "./dense-array-loops.js"; @@ -42,6 +43,8 @@ const locals = new Map([ ["a", { id: "a", name: "a", type: ARR, mutable: false }], ["b", { id: "b", name: "b", type: ARR, mutable: true }], ["c", { id: "c", name: "c", type: ARR, mutable: false }], + ["j", { id: "j", name: "j", type: F64, mutable: true }], + ["t", { id: "t", name: "t", type: F64, mutable: false }], ]); /** for (let i = 0; i < a.length; i++) body */ @@ -193,3 +196,100 @@ test("a loop to another invariant limit covers each array whose length it keeps" expect([...matchLimited(resizing, new Set(["b"]))!.arrays.keys()]).toEqual(["a"]); expect(matchLimited(resizing)).toBeNull(); }); + +test("a two-counter loop covers the arrays indexed at either counter", () => { + // for (let i = 1, j = k - 1; i < j; i++, j--) { t = a[i]; a[i] = a[j]; a[j] = t; } + const paired: IntegerPairedForLoop = { + low: { localId: "i", start: num(1), step: 1, startGuarded: false }, + high: { localId: "j", start: ref("k"), step: -1, startGuarded: true }, + inclusive: false, + guarded: true, + range: { min: -Number.MAX_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER }, + }; + const pairedShape = pairedDenseShape(paired); + const matchPaired = (body: IrStmt[]) => + matchDenseArrayLoop(loop(body), pairedShape, locals, new Set(), new Set(["a", "b"])); + const swap: IrStmt[] = [ + { kind: "varDecl", localId: "t", init: read("a"), loc }, + write("a", read("a", ref("j"))), + write("a", ref("t"), ref("j")), + ]; + const dense = matchPaired(swap); + expect([...dense!.arrays.keys()]).toEqual(["a"]); + expect(dense!.pair).toEqual({ high: "j", lowStart: num(1), highStart: ref("k") }); + expect(dense!.range).toEqual({ min: 0, max: 2147483646 }); + // Stores at any other index may extend the array. + expect(matchPaired([...swap, write("a", num(0), ref("k"))])).toBeNull(); +}); + +test("a present-undefined store is covered only where it stores a covered read's missing arm", () => { + // const v = b[i] (as number | undefined); + // if (v is undefined) c[i] = undefined; else c[i] = v is number ? v : throw; + const unionType = { kind: "union" as const, unionId: "u" }; + const v: IrExpr = { kind: "varRef", localId: "v", type: unionType, loc }; + const isTag = (tag: number): IrExpr => ({ + kind: "unionIsTag", + unionId: "u", + tag, + negated: false, + value: v, + type: BOOL, + loc, + }); + const reader = (array: string, index = ref("i")): IrExpr => ({ + kind: "call", + callee: "%arr.idxOr.f64", + args: [arr(array), index], + type: unionType, + loc, + }); + const undefinedStore: IrStmt = { kind: "arraySetUndefined", arr: arr("c"), index: ref("i"), loc }; + const guarded = (array: string): IrStmt[] => [ + { kind: "varDecl", localId: "v", init: reader(array), loc }, + { + kind: "if", + cond: isTag(1), + then: [undefinedStore], + else_: [ + write("c", { + kind: "ternary", + cond: isTag(0), + then: { kind: "unionNarrow", unionId: "u", tag: 0, value: v, type: F64, loc }, + else_: { + kind: "libCall", + fn: "error.nodeThrow", + args: [num(1), { kind: "strLit", value: "", type: { kind: "string" }, loc }], + type: F64, + loc, + }, + type: F64, + loc, + }), + ], + loc, + }, + ]; + const withV = new Map([ + ...locals, + ["v", { id: "v", name: "v", type: unionType, mutable: false }], + ]); + const matchStores = (body: IrStmt[], initialized: string[]) => + matchDenseArrayLoop(loop(body), shape, withV, new Set(), new Set(initialized)); + const dense = matchStores(guarded("b"), ["a", "b", "c"]); + expect([...dense!.arrays.keys()].sort()).toEqual(["b", "c"]); + // A read of an array the entry test does not cover may miss in the fast + // copy, and its undefined store would run there. + expect(matchStores(guarded("b"), ["a", "c"])).toBeNull(); + // A present undefined stored on its own leaves out its array and every + // array the other name may hold. + const alone: IrStmt[] = [{ kind: "exprStmt", expr: read("b"), loc }, undefinedStore]; + expect(matchStores(alone, ["a", "b", "c"])).toBeNull(); + const separate = matchDenseArrayLoop( + loop(alone), + shape, + withV, + new Set(["c"]), + new Set(["a", "b", "c"]), + ); + expect([...separate!.arrays.keys()]).toEqual(["b"]); +}); diff --git a/packages/compiler/src/backend/llvm/dense-array-loops.ts b/packages/compiler/src/backend/llvm/dense-array-loops.ts index 184ff1353..384c31b0c 100644 --- a/packages/compiler/src/backend/llvm/dense-array-loops.ts +++ b/packages/compiler/src/backend/llvm/dense-array-loops.ts @@ -20,16 +20,24 @@ * A counted loop with another invariant limit, such as `j < n` after * `const n = a.length` or `j < 5`, is covered the same way: the test bounds * the loop's exclusive limit by 2^31 - 1 and by each indexed array's - * `filled`, and the loop must provably never change any of their lengths. */ + * `filled`, and the loop must provably never change any of their lengths. + * + * A two-counter loop closing in on itself, such as + * `for (let i = 1, j = k - 1; i < j; i++, j--)`, indexes only between its + * low counter's start and its high counter's: the low counter only rises + * and stays below the high one, which only falls. Its test bounds the low + * start below by 0 and the high start by 2^31 - 2 and below each indexed + * array's `filled`, both starts being exact integers. */ import type { IrExpr, IrLocal, IrStmt } from "../../ir/ir.js"; import type { IntegerRange } from "../../ir/integer-ranges.js"; import { preservesArrayLength } from "../../ir/array-length-stability.js"; import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; -import type { IntegerCountedForLoop } from "../../ir/integer-loops.js"; +import type { IntegerCountedForLoop, IntegerPairedForLoop } from "../../ir/integer-loops.js"; import type { LlvmEmitterContext } from "./expr-context.js"; import { loadArrCount, SCR_ARR_FIELD } from "./common.js"; import { emitCountedLoopLimit } from "./counted-loops.js"; +import { f64Lit } from "./common.js"; export const INT32_MAX = 2147483647; @@ -47,6 +55,17 @@ export interface DenseArrayLoop { arrays: ReadonlyMap; /** The counter's range on every pass of the fast copy. */ range: IntegerRange; + /** A two-counter loop's high counter and both starts, when the counter + * is its low one (`length` and `limit` are then null). */ + pair: DensePair | null; +} + +/** The high counter of a two-counter dense loop, and the starts its entry + * test bounds. */ +export interface DensePair { + high: string; + lowStart: IrExpr; + highStart: IrExpr; } /** The fast copy duplicates the loop; keep the duplicated code small. */ @@ -62,6 +81,8 @@ export interface DenseLoopShape { /** The counter's proven range on every pass of the ordinary loop; the * fast copy keeps its minimum, which must be at least 0. */ range: IntegerRange; + /** A two-counter loop's high counter and starts. */ + pair?: DensePair; } /** `length.length - c` (c an integer literal, 0 when absent). */ @@ -107,6 +128,124 @@ export function countedDenseShape( return { counter: counted.localId, length: start.receiver, range: counted.range }; } +/** The shape of a two-counter loop whose low counter rises and high + * counter falls: both stay between the two starts, which the entry test + * bounds, so the fast copy's counters lie in [0, 2^31 - 2]. */ +export function pairedDenseShape(loop: IntegerPairedForLoop): DenseLoopShape { + return { + counter: loop.low.localId, + length: null, + range: { min: 0, max: INT32_MAX - 1 }, + pair: { high: loop.high.localId, lowStart: loop.low.start, highStart: loop.high.start }, + }; +} + +interface ElementRead { + arr: IrExpr; + index: IrExpr; +} + +/** The array and index of a call of an element-reader helper + * (`%arr.idxOr.*`, ir/int32-arrays.ts isElementReader): `a[i]` as + * `number | undefined`. */ +function elementReaderCall(e: IrExpr): ElementRead | null { + if (e.kind !== "call" || !e.callee.startsWith("%arr.idxOr.") || e.args.length !== 2) return null; + return { arr: e.args[0]!, index: e.args[1]! }; +} + +/** The array and index of an element read as `T | undefined`: an + * element-reader call, or its body inlined for an int32 array + * (`arrayState(a, i) === 1 ? wrap(a[i]) : wrap(undefined)`). */ +function elementRead(e: IrExpr): ElementRead | null { + const call = elementReaderCall(e); + if (call) return call; + if (e.kind !== "ternary") return null; + const cond = e.cond; + if (cond.kind !== "bin" || cond.op !== "===") return null; + const state = cond.left, + present = cond.right, + wrapped = e.then, + missing = e.else_; + if (state.kind !== "arrayState" || present.kind !== "numLit" || present.value !== 1) return null; + if (wrapped.kind !== "unionWrap" || missing.kind !== "unionWrap") return null; + const value = wrapped.value; + if (value.kind !== "arrayGet" || missing.value.kind !== "unitLit") return null; + const same = (x: IrExpr, y: IrExpr): boolean => + x.kind === "varRef" && y.kind === "varRef" && x.localId === y.localId; + return same(state.arr, value.arr) && same(state.index, value.index) + ? { arr: state.arr, index: state.index } + : null; +} + +/** The present-undefined stores of `a[i] = b[j]!` as lowering spells it, + * with the element read each one stores the missing arm of: + * + * const v = %arr.idxOr(b, j); + * if (v is undefined) a[i] = undefined; else a[i] = v is number ? v : throw; + * + * The read's local is declared once in the same list as the `if`, and only + * read after it. */ +function missingElementStores(body: IrStmt[]): Map { + const result = new Map(); + const writes = new Map(); + const count = (id: string): void => void writes.set(id, (writes.get(id) ?? 0) + 1); + const expr = (e: IrExpr): boolean => { + if (e.kind === "assignExpr" || e.kind === "incDec") count(e.localId); + return everyExprChild(e, expr, stmt); + }; + const stmt = (s: IrStmt): boolean => { + if (s.kind === "varDecl" || s.kind === "assign") count(s.localId); + return everyStmtChild(s, expr, stmt); + }; + body.every(stmt); + const list = (stmts: IrStmt[]): void => { + const reads = new Map(); + for (const s of stmts) { + if (s.kind === "varDecl" && s.init && writes.get(s.localId) === 1) { + const read = elementRead(s.init); + if (read) reads.set(s.localId, read); + } + if (s.kind === "if") { + const store = missingArmStore(s, reads); + if (store) result.set(store.stmt, store.read); + } + // Nested loops keep a loop out of the fast copy (matchDenseArrayLoop), + // and other statement lists leave their stores unmatched. + if (s.kind === "block") list(s.body); + if (s.kind === "if") { + list(s.then); + if (s.else_) list(s.else_); + } + } + }; + list(body); + return result; +} + +/** The present-undefined store of `if (v is undefined) a[i] = undefined; + * else a[i] = v is number ? v : throw;`, with the read `v` holds. */ +function missingArmStore( + s: IrStmt & { kind: "if" }, + reads: ReadonlyMap, +): { stmt: IrStmt; read: ElementRead } | null { + const cond = s.cond; + if (cond.kind !== "unionIsTag" || cond.negated || cond.value.kind !== "varRef") return null; + const local = cond.value.localId; + const read = reads.get(local); + if (!read || s.then.length !== 1 || s.else_?.length !== 1) return null; + const store = s.then[0]!, + other = s.else_[0]!; + if (store.kind !== "arraySetUndefined" || other.kind !== "arraySet") return null; + const value = other.value; + if (value.kind !== "ternary") return null; + const test = value.cond, + narrowed = value.then; + if (test.kind !== "unionIsTag" || test.negated || test.tag === cond.tag) return null; + if (test.value.kind !== "varRef" || test.value.localId !== local) return null; + if (narrowed.kind !== "unionNarrow" || narrowed.tag !== test.tag) return null; + return narrowed.type.kind === "f64" ? { stmt: store, read } : null; +} + /** Match the fast copy of a loop of the given shape. `initialized` holds the * bindings definitely assigned when the loop starts; `unaliased` the local * arrays no other binding holds. */ @@ -118,6 +257,8 @@ export function matchDenseArrayLoop( initialized: ReadonlySet, ): DenseArrayLoop | null { const { counter, length } = shape; + const high = shape.pair?.high ?? null; + const isCounter = (id: string): boolean => id === counter || id === high; if (shape.range.min < 0 || !loop.cond) return null; let nodes = 0, nested = false; @@ -126,13 +267,21 @@ export function matchDenseArrayLoop( // element write. const disturbed = new Set(); const access = (arr: IrExpr, index: IrExpr): void => { - if (arr.kind === "varRef" && index.kind === "varRef" && index.localId === counter) + if (arr.kind === "varRef" && index.kind === "varRef" && isCounter(index.localId)) candidates.set(arr.localId, arr); }; + const missing = missingElementStores(loop.body); + // The element reads behind the guarded present-undefined stores. + const guards: ElementRead[] = []; + // The arrays of the other present-undefined stores. The fast copy runs + // them, and the slot each one writes leaves the hole-free prefix. + const undefinedStores: IrExpr[] = []; const expr = (e: IrExpr): boolean => { if (++nodes > MAX_LOOP_NODES) return false; if (e.kind === "assignExpr" || e.kind === "incDec") disturbed.add(e.localId); if (e.kind === "arrayGet") access(e.arr, e.index); + const reader = elementReaderCall(e); + if (reader) access(reader.arr, reader.index); if (e.kind === "arrIntrinsic") { if (e.method === "getNumber" && e.args[0]) access(e.receiver, e.args[0]); else if (e.method !== "length" && e.receiver.kind === "varRef") @@ -147,9 +296,21 @@ export function matchDenseArrayLoop( if (s.kind === "assign" || s.kind === "varDecl") disturbed.add(s.localId); if (s.kind === "arraySet") { access(s.arr, s.index); - if (s.arr.kind === "varRef" && (s.index.kind !== "varRef" || s.index.localId !== counter)) + if (s.arr.kind === "varRef" && (s.index.kind !== "varRef" || !isCounter(s.index.localId))) disturbed.add(s.arr.localId); } + if (s.kind === "arraySetUndefined") { + // A present undefined at a counter only where it stores a missing + // element read (`a[i] = b[j]!`): the fast copy reads a present value + // there, so the store never runs in it. + const read = missing.get(s); + if (read === undefined || s.index.kind !== "varRef" || !isCounter(s.index.localId)) { + undefinedStores.push(s.arr); + } else { + access(s.arr, s.index); + guards.push(read); + } + } return everyStmtChild(s, expr, stmt); }; if (!loop.body.every(stmt) || !expr(loop.cond) || nested) return null; @@ -164,6 +325,15 @@ export function matchDenseArrayLoop( ) ) return null; + // A store through another name, such as `const b = a` or a field, lands + // in `a` unless one of the two bindings is a local array no other binding + // holds. + const mayStoreUndefined = (id: string): boolean => + undefinedStores.some((arr) => + arr.kind === "varRef" + ? arr.localId === id || (!unaliased.has(id) && !unaliased.has(arr.localId)) + : !unaliased.has(id), + ); const arrays = new Map(); for (const [id, ref] of candidates) { const local = locals.get(id); @@ -173,6 +343,7 @@ export function matchDenseArrayLoop( local.boxed || local.tdz || disturbed.has(id) || + mayStoreUndefined(id) || (id !== length?.localId && !initialized.has(id)) ) continue; @@ -180,18 +351,33 @@ export function matchDenseArrayLoop( // once: the loop must not change any of their lengths. if ( length === null && - !preservesArrayLength(nodesOfLoop, { receiver: id, inRange: counter }, locals, unaliased) + !preservesArrayLength( + nodesOfLoop, + { receiver: id, inRange: counter, inRangeHigh: high }, + locals, + unaliased, + ) ) continue; arrays.set(id, ref); } if (arrays.size === 0) return null; + // Each guarded store's read must be one the fast copy proves present. + for (const { arr, index } of guards) + if ( + arr.kind !== "varRef" || + !arrays.has(arr.localId) || + index.kind !== "varRef" || + !isCounter(index.localId) + ) + return null; return { counter, length, limit: length === null ? (shape.limit ?? null) : null, arrays, range: { min: shape.range.min, max: Math.min(shape.range.max, INT32_MAX - 1) }, + pair: shape.pair ?? null, }; } @@ -207,6 +393,7 @@ export function emitDenseLoopGuard(host: LlvmEmitterContext, loop: DenseArrayLoo host.markMemoryPointer(ptr, "array:header"); return ptr; }; + if (loop.pair !== null) return emitPairedDenseGuard(host, loop, loop.pair, header); if (loop.length === null) { // A limit at or below the start runs no pass, and passes the test. const limit = emitCountedLoopLimit(host, loop.limit!); @@ -254,3 +441,54 @@ export function emitDenseLoopGuard(host: LlvmEmitterContext, loop: DenseArrayLoo } return valid; } + +/** A two-counter loop's entry test: both starts are exact integers, the + * low one at least 0 and the high one at most 2^31 - 2 and below every + * indexed array's hole-free prefix bound. The starts are stable values + * (matchIntegerPairedForLoop), so evaluating them here again is unobservable. */ +function emitPairedDenseGuard( + host: LlvmEmitterContext, + loop: DenseArrayLoop, + pair: DensePair, + header: (array: string, field: number) => string, +): string { + const B = host.B; + const integer = (expr: IrExpr, min: number, max: number): { value: string; ok: string } => { + const start = host.emitExpr(expr); + const low = B.tmp(), + high = B.tmp(), + raw = B.tmp(), + value = B.tmp(), + back = B.tmp(), + exact = B.tmp(), + inRange = B.tmp(), + ok = B.tmp(); + B.line(`${low} = fcmp oge double ${start.name}, ${f64Lit(min)}`); + B.line(`${high} = fcmp ole double ${start.name}, ${f64Lit(max)}`); + B.line(`${inRange} = and i1 ${low}, ${high}`); + B.line(`${raw} = fptosi double ${start.name} to i64`); + B.line(`${value} = freeze i64 ${raw}`); + B.line(`${back} = sitofp i64 ${value} to double`); + B.line(`${exact} = fcmp oeq double ${back}, ${start.name}`); + B.line(`${ok} = and i1 ${inRange}, ${exact}`); + return { value, ok }; + }; + const low = integer(pair.lowStart, 0, INT32_MAX - 1); + const high = integer(pair.highStart, -1, INT32_MAX - 1); + let valid = B.tmp(); + B.line(`${valid} = and i1 ${low.ok}, ${high.ok}`); + for (const ref of loop.arrays.values()) { + const array = host.emitStableReceiver(ref, []); + const filledPtr = header(array.name, SCR_ARR_FIELD.filled), + filled = B.tmp(), + wide = B.tmp(), + covered = B.tmp(), + both = B.tmp(); + B.line(`${filled} = load i32, ptr ${filledPtr}${host.fieldAliasAttachment(filledPtr)}`); + B.line(`${wide} = zext i32 ${filled} to i64`); + B.line(`${covered} = icmp slt i64 ${high.value}, ${wide}`); + B.line(`${both} = and i1 ${valid}, ${covered}`); + valid = both; + } + return valid; +} diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index bb00e4563..86626d304 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -1,3 +1,4 @@ +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, SCR_ARR_STATES, f64Lit, sizeToDouble } from "./common.js"; @@ -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[]; @@ -365,6 +370,36 @@ export class LlDyn { B.line(`call void @scr_dyn_build_add(ptr %d, ptr @${name}, ptr ${value})`); } + /** The order a dyn value tries a union's arms in: the canonical order, + * except that among the record arms, those with more required fields go + * first, then those with more fields. Records are width-tolerant, so an + * arm with fewer fields (`{}`, or one whose fields are all optional, + * matches every object) would otherwise take an object that a wider arm + * also matches and drop the fields only the wider arm has. */ + private unionArmOrder(def: IrUnionDef): number[] { + const width = (i: number): [number, number] | null => { + const arm = def.arms[i]!; + if (arm.kind !== "record") return null; + const shape = this.host.recordsById.get(arm.shapeId); + if (!shape || shape.tuple) return null; + const required = shape.fields.filter( + (field) => + field.type.kind !== "union" || + !this.host.unionsById.get(field.type.unionId)?.arms.some((x) => x.kind === "undefinedT"), + ).length; + return [required, shape.fields.length]; + }; + const order = def.arms.map((_, i) => i); + const records = order.filter((i) => width(i) !== null); + const widest = [...records].sort((x, y) => { + const [rx, fx] = width(x)!; + const [ry, fy] = width(y)!; + return ry - rx || fy - fx || x - y; + }); + records.forEach((slot, k) => (order[slot] = widest[k]!)); + return order; + } + /** Branch to a variant only after its literal discriminator and complete * structure match. Use the same path for predicates and builders. */ private unionArmMatch( @@ -1107,9 +1142,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( @@ -1292,9 +1328,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)) ); @@ -1380,7 +1417,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) => { @@ -2156,7 +2195,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, @@ -2356,9 +2398,10 @@ export class LlDyn { throw new InternalCompilerError( `llvm emitter bug: dynCheck of unknown union ${t.unionId}`, ); - // Arms in CANONICAL order, first FULL match wins. The matched - // arm's builder can no longer fail. - def.arms.forEach((arm, i) => { + // Arms in unionArmOrder, first FULL match wins. The matched arm's + // builder can no longer fail. + for (const i of this.unionArmOrder(def)) { + const arm = def.arms[i]!; const lHit = B.newLabel("dcu.h"); const lNext = B.newLabel("dcu.n"); this.unionArmMatch(B, def, i, lHit, lNext, preserveRefs); @@ -2402,7 +2445,7 @@ export class LlDyn { B.terminate(`ret ptr ${u}`); } B.startBlock(lNext); - }); + } if (preserveRefs) { B.line( `call void @scr_dyn_check_fail(ptr %path, ptr ${host.cstr("reference with its original layout")}, ptr %d)`, @@ -2889,70 +2932,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)) { @@ -4526,6 +4578,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") { @@ -4657,6 +4734,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); } }); @@ -4689,7 +4786,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}`); } diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts index b6e489c19..16a1b1601 100644 --- a/packages/compiler/src/backend/llvm/emit-module.ts +++ b/packages/compiler/src/backend/llvm/emit-module.ts @@ -873,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(``); diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index c2846a9d4..48f7a1d5f 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -52,7 +52,12 @@ import { matchIntegerPairedForLoop, } from "../../ir/integer-loops.js"; import { emitByteWindowGuard, matchByteWindow } from "./byte-windows.js"; -import { countedDenseShape, emitDenseLoopGuard, matchDenseArrayLoop } from "./dense-array-loops.js"; +import { + countedDenseShape, + emitDenseLoopGuard, + matchDenseArrayLoop, + pairedDenseShape, +} from "./dense-array-loops.js"; import { emitCountedLoopLimit, emitCountedLoopSafety, @@ -966,7 +971,9 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { } : countedLoop && !countedLoop.guarded && countedLoop.followers.length === 0 ? countedDenseShape(s, countedLoop) - : null; + : pairedLoop + ? pairedDenseShape(pairedLoop) + : null; const dense = denseShape && !window ? matchDenseArrayLoop( @@ -1144,11 +1151,15 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { B.line(`${start} = fptosi double ${value.name} to i64`); B.line(`store i64 ${start}, ptr ${slot}`); pairedSlots.push(slot); + const dense = LOOPS.of(emitter).denseLoop; emitter.integerLoopBindings.set(counter.localId, { slot, type: "i64", signed: true, - range: pairedLoop.range, + range: + dense?.pair && dense.counter === pairedLoop.low.localId + ? dense.range + : pairedLoop.range, }); } } else if (s.init) { diff --git a/packages/compiler/src/backend/llvm/emitter.test.ts b/packages/compiler/src/backend/llvm/emitter.test.ts index d1fd62a83..e6334ab4c 100644 --- a/packages/compiler/src/backend/llvm/emitter.test.ts +++ b/packages/compiler/src/backend/llvm/emitter.test.ts @@ -459,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 }; @@ -472,6 +499,27 @@ test("class brand probes compare the program's key constant before the key text" const named = work(probe("dyn.typedRefIs"), dyn); expect(named).toContain("; ->v.typed_ref.type_key"); expect(named).toContain("@scr_dyn_typed_ref_is_key"); + // Another key pointer is rejected on its length and last eight bytes + // ("Example" plus the prefix's ':') before the out-of-line comparison. + expect(named).toContain("; ->v.typed_ref.type_key_len"); + expect(named).toContain("and i32 %t"); + expect(named).toMatch(/icmp eq i32 %t\d+, 14\b/); + const tail = Buffer.from("object:Example").readBigInt64LE(6); + expect(named).toContain(`icmp eq i64 %t`); + expect(named).toContain(`, ${tail}`); + const shortKey: IrExpr = { kind: "strLit", value: "o:E", type: STRING, loc: receiverLoc }; + const short = work( + { + kind: "libCall", + fn: "dyn.typedRefIs", + args: [ref(dyn), shortKey], + type: BOOL, + loc: receiverLoc, + }, + dyn, + ); + expect(short).toMatch(/icmp eq i32 %t\d+, 3\b/); + expect(short).not.toContain("load i64"); // The key-pointer test answers false for capsules boxed elsewhere. const exact = work(probe("dyn.typedRefKeyIs"), dyn); expect(exact).toContain("; ->v.typed_ref.type_key"); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index efe6097df..4aa9b65ad 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -68,7 +68,7 @@ import { type StringSplit, } from "./split-loops.js"; import { scalarizeNumericRecords } from "../../ir/scalar-records.js"; -import { addRecordResultEntries } from "./record-results.js"; +import { addRecordResultEntries, resultEntryName } from "./record-results.js"; import { specializeCallbackCalls } from "../../ir/callback-specialization.js"; import { type ChunkResult, @@ -82,6 +82,7 @@ import { versionLoopBoundParameters } from "../../ir/parameter-versions.js"; import { allocatedLiteral, fillAllocationLength, + int32ArrayKey, selectInt32Arrays, } from "../../ir/int32-arrays.js"; import { everyExpr, everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse.js"; @@ -92,6 +93,7 @@ import { 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"; @@ -168,6 +170,7 @@ import { type MapEntryFusion } from "./map-entry-fusion.js"; import { emitBorrowedFieldSequence, guardedValue } from "./borrowed-receivers.js"; import { emitBorrowedInputs } from "./borrowed-inputs.js"; import { findWalkBorrows, type WalkBorrowHost } from "./walk-borrows.js"; +import { IrNodeIndex } from "../../ir/node-index.js"; import { lastUseRead } from "./local-extents.js"; import { findReboundParameters } from "./rebound-parameters.js"; import { ReferenceEffects } from "./reference-effects.js"; @@ -508,6 +511,15 @@ function provenInt32Arrays(mod: IrModule): ReadonlySet { return new Set([...slots.int32Arrays, ...slots.checkedInt32Arrays]); } +/** The functions whose calls the backend may emit as calls of their + * result-slot entries (record-results.ts). */ +function resultEntryTargets(mod: IrModule): ReadonlySet { + const names = new Set(mod.functions.map((fn) => fn.name)); + const targets = new Set(); + for (const name of names) if (names.has(resultEntryName(name))) targets.add(name); + return targets; +} + /** 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 @@ -527,7 +539,9 @@ function moduleEmitter(input: IrModule, options: LlvmTargetOptions): LlEmitter { resolveSelfCaptures( hoistFinalFieldReads( freezeInitOnlyGlobals( - holdCharLocalsAsUnits(dropUnwrittenPropertyBags(mod)), + holdCharLocalsAsUnits( + dropUnwrittenPropertyBags(runPairedWhileLoopsAsFor(mod)), + ), ), ), ), @@ -541,6 +555,7 @@ function moduleEmitter(input: IrModule, options: LlvmTargetOptions): LlEmitter { ), ), provenInt32Arrays, + resultEntryTargets, ) : mod, options, @@ -777,7 +792,7 @@ export class LlEmitter { everyStmtList(fn.body, { stmt: (s) => { if (s.kind !== "varDecl" && s.kind !== "assign") return true; - const key = `${fn.name}\0${s.localId}`; + const key = int32ArrayKey(fn.name, s.localId); const checked = checkedInt32Arrays.has(key); if (!checked && !int32Arrays.has(key)) return true; const value = s.kind === "varDecl" ? s.init : s.value; @@ -795,15 +810,16 @@ export class LlEmitter { }); } - /** Whether `arr` is one of the current function's int32 arrays - * (ir/int32-arrays.ts, kept by the int32 slot analysis). */ + /** Whether `arr` reads one of the int32 arrays (ir/int32-arrays.ts, kept + * by the int32 slot analysis): a local or parameter of the current + * function, or a module global. */ 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}`) + this.int32Slots.int32Arrays.has(int32ArrayKey(this.currentFunctionName, arr.localId)) ); } @@ -970,8 +986,8 @@ export class LlEmitter { } /** 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. */ + * prefix: a 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 = LOOPS.of(this).denseLoop; return ( @@ -979,8 +995,8 @@ export class LlEmitter { arr.kind === "varRef" && loop.arrays.has(arr.localId) && index.kind === "varRef" && - index.localId === loop.counter && - this.integerLoopBindings.has(loop.counter) + (index.localId === loop.counter || index.localId === loop.pair?.high) && + this.integerLoopBindings.has(index.localId) ); } @@ -1076,6 +1092,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. */ @@ -1176,6 +1196,9 @@ export class LlEmitter { /** Module constants that keep one value once initialized * (immutability.ts stableGlobals). */ stableGlobals: ReadonlySet = new Set(); + /** Every function body's nodes in walk order (ir/node-index.ts), for the + * constructor's analyses. */ + readonly nodeIndex: IrNodeIndex; private readonly tdzGlobals = new Map(); readonly constantNumericTables: ReadonlyMap; /** Constant-key reads on checked-dynamic objects, each with its own @@ -1244,7 +1267,7 @@ export class LlEmitter { /** The module's runtime features (moduleRuntimeFeatures), found with one * walk of the module the first time any of them is read. */ get runtimeFeatures(): RuntimeFeatures { - return (this.runtimeFeaturesValue ??= moduleRuntimeFeatures(this.mod)); + return (this.runtimeFeaturesValue ??= moduleRuntimeFeatures(this.mod, this.nodeIndex)); } /** Whether timer and tick scheduling goes through the *_als entry * points (RuntimeFeatures asyncContext). */ @@ -1541,8 +1564,15 @@ export class LlEmitter { ((options.functionWorkers ?? 1) > 1 && options.forkPrimitives !== undefined && options.debugSources === undefined); - this.stackTraces = observesStacks(mod); - this.nullableUnions = new NullableUnions(mod, options.debugSources === undefined); + // One walk of every body, which the analyses below read instead of + // walking the bodies themselves. + this.nodeIndex = new IrNodeIndex(mod); + this.stackTraces = observesStacks(mod, this.nodeIndex); + this.nullableUnions = new NullableUnions( + mod, + options.debugSources === undefined, + this.nodeIndex, + ); this.mockTimers = this.runtimeFeatures.mockTimers; this.debug = options.debugSources === undefined @@ -1555,19 +1585,20 @@ export class LlEmitter { this.nullableUnions.armTypes(), ); this.constantNumericTables = findConstantNumericTables(mod); - this.initializerBindings = findInitializerBindings(mod); + this.initializerBindings = findInitializerBindings(mod, this.nodeIndex); // One walk finds the dynamic field stores every analysis below consults. - const dynamicStores = dynamicFieldStores(mod); + const dynamicStores = dynamicFieldStores(mod, undefined, undefined, this.nodeIndex); this.int32Slots = analyzeInt32Slots(mod, { dynamicStores, initializerBindings: this.initializerBindings, + nodes: this.nodeIndex, }); this.liveViewsReadOnly = mod.lib === undefined && !dynamicStores.all && dynamicStores.names.size === 0; this.int32Abis = this.debug === null ? findInt32Abis(mod, this.int32Slots) : new Map(); this.immortalGlobals = new Set(mod.contracts?.immortalGlobals ?? []); this.programLifetime = new Set(mod.contracts?.programLifetime ?? []); - this.finalFields = analyzeFinalFields(mod, false, dynamicStores); + this.finalFields = analyzeFinalFields(mod, false, dynamicStores, this.nodeIndex); this.sizeType = options.pointerBits === 32 ? "i32" : "i64"; this.ffiExtendNarrowIntegers = options.wasi === true || ffiExtendsNarrowIntegers(options.targetTriple); @@ -1615,7 +1646,7 @@ export class LlEmitter { this.ffiHasRetainedCallback = hasRetainedFfiCallback(mod.ffiImports ?? []); this.ffiHasForeignCallback = hasForeignFfiCallback(mod.ffiImports ?? []); for (const fn of mod.functions) this.fnByName.set(fn.name, fn); - this.appendedArrayLiterals = findAppendedArrayLiterals(mod); + this.appendedArrayLiterals = findAppendedArrayLiterals(mod, this.nodeIndex); for (const entry of mod.ffiImports ?? []) { this.ffiByName.set(entry.name, entry); } @@ -1623,10 +1654,11 @@ 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); + this.publishedTypes = computePublishedTypes(mod, this.nodeIndex); const mt = computeMayThrow(mod, { stackChecks: !this.wasi && mod.lib === undefined && !windows, published: this.publishedTypes, + nodes: this.nodeIndex, }); this.mayThrow = mt.fns; this.workerEntryPolls = mt.workerEntryPolls ?? null; @@ -1685,12 +1717,14 @@ export class LlEmitter { new Set((mod.globals ?? []).map((g) => g.id)), { unions: mod.unions, records: mod.records }, this.immutableFields, + this.nodeIndex, ); const walkParameters = new Map>(); if (this.debug === null) { const host = { pointerLocal: (local: IrLocal) => this.plainReference(local.type), borrowsWithoutOwning: (e: IrExpr) => this.borrowsWithoutOwning(e), + nodes: this.nodeIndex, }; for (const fn of this.fnByName.values()) { const preserving = this.referenceEffects.functions.has(fn.name); @@ -1743,7 +1777,7 @@ export class LlEmitter { ); this.lentStringParameters = this.debug === null ? findLentStringParameters(this.fnByName, this.callLifetimes) : new Map(); - this.constantCallbacks = findConstantCallbacks(mod, this.callLifetimes); + this.constantCallbacks = findConstantCallbacks(mod, this.callLifetimes, this.nodeIndex); // Integer clones signal a stack overflow through their result, which // needs the single-threaded guard; worker programs also poll for // termination, and debug builds and stack traces keep every frame. @@ -1844,6 +1878,7 @@ 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, @@ -1884,7 +1919,7 @@ export class LlEmitter { this.globalTypes.set(g.id, g.type); if (g.tdz) this.tdzGlobals.set(g.id, g.name); } - this.stableGlobals = stableGlobals(mod); + this.stableGlobals = stableGlobals(mod, this.nodeIndex); // Runtime error, EventEmitter, stream, EventTarget and Event classes // have known layouts. Subclasses embed the runtime prefixes from // classes.ts. @@ -2736,6 +2771,17 @@ export class LlEmitter { 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; } @@ -4853,6 +4899,15 @@ export class LlEmitter { 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; diff --git a/packages/compiler/src/backend/llvm/expr-calls.ts b/packages/compiler/src/backend/llvm/expr-calls.ts index a4e7cde2c..6ff52619e 100644 --- a/packages/compiler/src/backend/llvm/expr-calls.ts +++ b/packages/compiler/src/backend/llvm/expr-calls.ts @@ -44,6 +44,7 @@ import { LlvmUnsupportedError } from "./unsupported.js"; import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js"; import { f64Lit, ffiNativeTypeLl, ffiNativeParamLl, ffiNativeReturnLl } from "./common.js"; import { canStackUnion, emitStackUnion } from "./stack-unions.js"; +import { emitStackRecordKeyGet, scalarRecordKeyGet } from "./expr-records.js"; import { emitCallArrayRead } from "./local-array-reads.js"; import { emitScalarOptional, isScalarOptionalSource } from "./local-union-storage.js"; import { emitStackMapRead } from "./map-read-lifetimes.js"; @@ -166,6 +167,9 @@ export function emitCallExpr( return { name: host.emitOwnedNullableStack(a).box, type: a.type }; // An inline union field read copies its tag and bits. if (host.inlineFieldGet(a)) return host.emitReadReceiver(a); + // A scalar keyed record read writes its tag and bits. + if (a.kind === "recordKeyGet" && scalarRecordKeyGet(host, a)) + return { name: emitStackRecordKeyGet(host, a), type: a.type }; } if (inputs[index]) return host.emitReadReceiver(a); } diff --git a/packages/compiler/src/backend/llvm/expr-context.ts b/packages/compiler/src/backend/llvm/expr-context.ts index df6dfc608..c6dc506f4 100644 --- a/packages/compiler/src/backend/llvm/expr-context.ts +++ b/packages/compiler/src/backend/llvm/expr-context.ts @@ -46,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 9af677586..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]!]); } diff --git a/packages/compiler/src/backend/llvm/expr-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts index 1eec6a965..2c6ce35db 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.ts @@ -1076,9 +1076,10 @@ 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 proven = host.provenDenseIndex(e.arr, e.index); 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", slotWidth(kind), proven), )! : emitDenseArrayState( host, @@ -1087,6 +1088,7 @@ export function emitContainerExpr( e.index, "state", host.int32Array(e.arr) ? slotWidth("i32") : denseSlotWidth(host, e.arr.type.elem), + proven, ); return { name: t, type: e.type }; } diff --git a/packages/compiler/src/backend/llvm/expr-records.ts b/packages/compiler/src/backend/llvm/expr-records.ts index bca728e9d..59ab625e5 100644 --- a/packages/compiler/src/backend/llvm/expr-records.ts +++ b/packages/compiler/src/backend/llvm/expr-records.ts @@ -7,6 +7,7 @@ import type { LlvmEmitterContext, LlValue } from "./expr-context.js"; import { f64Lit, llvmCommentText } from "./common.js"; import { borrowableInputs, emitBorrowedInputs, emitBorrowedInput } from "./borrowed-inputs.js"; import { regexLiteralTemplate } from "./expr-primitives.js"; +import { isScalarUnionDef, loadInlineUnion, presentBits } from "./inline-unions.js"; import { preservesRegexInputs } from "./reference-effects.js"; /** Widen one tagged read. The source is borrowed unless the overflow map @@ -365,6 +366,114 @@ export function emitRecordKeyGet( return host.own({ name: t, type: e.type }); } +/** A keyed read whose result is a scalar union (`r[k]` on a + * `Record`, at `number | undefined`) and whose every hit + * is a number or boolean arm of it, or an inline field of the same union. + * Projections (`??`, `!`, tag tests) read it as a private stack box, so + * the read allocates nothing. */ +export function scalarRecordKeyGet(host: LlvmEmitterContext, e: IrExpr): boolean { + if (e.kind !== "recordKeyGet" || e.missNaN === true || e.type.kind !== "union") return false; + const resultType = e.type; + const def = host.unionsById.get(resultType.unionId); + if (!isScalarUnionDef(def) || undefinedArmTag(resultType, host.unionsById) < 0) return false; + const isArm = (t: IrType): boolean => + (t.kind === "f64" || t.kind === "bool") && def!.arms.some((a) => typeEquals(a, t)); + const shape = host.recordShape(e.shapeId); + if (e.overflowOnly !== true) { + for (const f of shape.fields) { + const inline = host.recordFieldInline(e.shapeId, f.name); + if (!isArm(f.type) && !(inline && typeEquals(f.type, resultType))) return false; + } + } + return shape.indexValue === undefined || isArm(shape.indexValue); +} + +/** A scalarRecordKeyGet read as a private stack box: the tag and bits of + * the declared field or overflow entry the key names, or the undefined arm + * for a miss. The box must never reach an RC entry point. */ +export function emitStackRecordKeyGet( + host: LlvmEmitterContext, + e: IrExpr & { kind: "recordKeyGet" }, +): string { + const B = host.B; + if (e.type.kind !== "union") + throw new InternalCompilerError("llvm emitter bug: stack keyed read of a non-union"); + const resultType = e.type; + const def = host.unionsById.get(resultType.unionId)!; + const armTag = (t: IrType): number => def.arms.findIndex((a) => typeEquals(a, t)); + const obj = host.emitExpr(e.obj); + const key = host.emitExpr(e.key); + const box = B.slot(); + B.entryAllocas.push(`${box} = alloca %ScrUnion`); + const tagPtr = B.tmp(), + bitsPtr = B.tmp(); + B.line(`store ${host.sizeType} -1, ptr ${box}`); + B.line(`${tagPtr} = getelementptr inbounds %ScrUnion, ptr ${box}, i64 0, i32 1`); + B.line(`${bitsPtr} = getelementptr inbounds %ScrUnion, ptr ${box}, i64 0, i32 5`); + const join = B.newLabel("rks.j"); + const storeScalar = (t: IrType, value: string): void => { + const bits = B.tmp(); + if (t.kind === "f64") B.line(`${bits} = bitcast double ${value} to i64`); + else B.line(`${bits} = zext i1 ${value} to i64`); + B.line(`store i32 ${armTag(t)}, ptr ${tagPtr}`); + B.line(`store i64 ${bits}, ptr ${bitsPtr}`); + }; + const shape = host.recordShape(e.shapeId); + for (const f of e.overflowOnly === true ? [] : shape.fields) { + const hit = B.tmp(); + B.line( + `${hit} = call zeroext i1 ${host.literalEquals(f.name)}(ptr ${key.name}) ; ${llvmCommentText(f.name)}`, + ); + const lh = B.newLabel("rks.h"); + const ln = B.newLabel("rks.n"); + B.condBr(hit, lh, ln); + B.startBlock(lh); + const { ptr, type, inline } = host.recordFieldPtr(obj.name, e.shapeId, f.name); + if (inline) { + const { tag, bits } = loadInlineUnion(B, ptr); + const present = presentBits(B, tag, bits, undefinedArmTag(type, host.unionsById)); + B.line(`store i32 ${tag}, ptr ${tagPtr}`); + B.line(`store i64 ${present}, ptr ${bitsPtr}`); + } else { + storeScalar(type, host.loadRecordField(ptr, type, false)); + } + B.br(join); + B.startBlock(ln); + } + const iv = shape.indexValue; + if (iv) { + const ovf = host.recordOvfPtr(obj.name, e.shapeId); + const outTy = iv.kind === "f64" ? "double" : "i8"; + const outSlot = B.slot(); + B.entryAllocas.push(`${outSlot} = alloca ${outTy}`); + B.line(`store ${outTy} ${iv.kind === "f64" ? f64Lit(0) : "0"}, ptr ${outSlot}`); + const getter = `scr_map_get_str_${iv.kind === "f64" ? "f64" : "bool"}`; + host.declare(`declare zeroext i1 @${getter}(ptr, ptr, ptr)`); + const found = B.tmp(); + B.line(`${found} = call zeroext i1 @${getter}(ptr ${ovf}, ptr ${key.name}, ptr ${outSlot})`); + const lh = B.newLabel("rks.h"); + const ln = B.newLabel("rks.n"); + B.condBr(found, lh, ln); + B.startBlock(lh); + const raw = B.tmp(); + B.line(`${raw} = load ${outTy}, ptr ${outSlot}`); + let value = raw; + if (iv.kind === "bool") { + value = B.tmp(); + B.line(`${value} = trunc i8 ${raw} to i1`); + } + storeScalar(iv, value); + B.br(join); + B.startBlock(ln); + } + // A miss is the undefined arm. + B.line(`store i32 ${undefinedArmTag(resultType, host.unionsById)}, ptr ${tagPtr}`); + B.line(`store i64 0, ptr ${bitsPtr}`); + B.br(join); + B.startBlock(join); + return box; +} + export function keyedRecordReadInto( host: LlvmEmitterContext, slot: string, 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 7cb0fa772..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,7 @@ 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` @@ -764,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(); @@ -776,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)) @@ -847,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})`); @@ -865,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) @@ -1065,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})`); @@ -1373,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 }`, ``, ); } @@ -1444,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) diff --git a/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts b/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts index 14a2e3432..b368c91f4 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts @@ -727,6 +727,17 @@ export function streamCbThunkFor( const ty = host.llType(p); host.declare(`declare zeroext i1 @scr_exc_pending()`); if (hasThis) host.declare(`declare void @scr_stream_release_v(ptr)`); + // An entry `encoding` declared as a string (the declarations' + // `encoding: string`) receives "" where Node passes undefined or + // null (an object-mode write), as a write's encoding parameter does. + const entry = + p.kind === "array" && p.elem.kind === "record" + ? host.recordsById.get(p.elem.shapeId) + : undefined; + if (entry?.fields.some((f) => f.name === "encoding" && f.type.kind === "string")) { + host.declare(`declare void @scr_stream_writev_fill_encodings(ptr)`); + d.push(` call void @scr_stream_writev_fill_encodings(ptr %chunks)`); + } d.push( ` %vt${i} = call ${ty === "i1" ? "zeroext i1" : ty} @${helper}(ptr %chunks, ptr null)`, ` %vtbad${i} = call zeroext i1 @scr_exc_pending()`, 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-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index cebe661d4..3f127e7e9 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -25,6 +25,9 @@ import { rawBytes } from "./lib-abi.js"; import { emitInlineAtomicsOp } from "./expr-bytes.js"; import { regexLiteralCaptures } from "./regex-matchers.js"; +/** ScrDyn.v.typed_ref.type_key_len: the low 27 bits of its bit-field word. */ +const TYPE_KEY_LEN_MASK = (1 << 27) - 1; + export function emitAssertInspectLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { const B = host.B; if (e.fn === "assert.shapeStr" || e.fn === "assert.shapeRe") { @@ -281,6 +284,38 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll B.line(`store i1 true, ptr ${slot}`); B.condBr(same, done, compare); B.startBlock(compare); + // A capsule with another key pointer usually belongs to another + // class: reject it on its key length and last eight bytes before + // the out-of-line name comparison, so a class table's chain of + // probes costs a few instructions per class it skips. + B.line(`store i1 false, ptr ${slot}`); + const bytes = Buffer.from(literal, "utf8"); + const lenSlot = B.tmp(), + lenWord = B.tmp(), + len = B.tmp(), + lenEq = B.tmp(); + B.line( + `${lenSlot} = getelementptr inbounds i8, ptr ${value.name}, i64 ${host.abiOffset(88, 52)} ; ->v.typed_ref.type_key_len`, + ); + B.line(`${lenWord} = load i32, ptr ${lenSlot}`); + B.line(`${len} = and i32 ${lenWord}, ${TYPE_KEY_LEN_MASK}`); + B.line(`${lenEq} = icmp eq i32 ${len}, ${bytes.length}`); + const full = B.newLabel("brand.full"); + if (bytes.length >= 8) { + const tail = B.newLabel("brand.tail"); + B.condBr(lenEq, tail, done); + B.startBlock(tail); + const tailPtr = B.tmp(), + word = B.tmp(), + wordEq = B.tmp(); + B.line(`${tailPtr} = getelementptr inbounds i8, ptr ${keyPtr}, i64 ${bytes.length - 8}`); + B.line(`${word} = load i64, ptr ${tailPtr}, align 1`); + B.line(`${wordEq} = icmp eq i64 ${word}, ${bytes.readBigInt64LE(bytes.length - 8)}`); + B.condBr(wordEq, full, done); + } else { + B.condBr(lenEq, full, done); + } + B.startBlock(full); } } if (e.fn === "dyn.typedRefIs") { @@ -297,6 +332,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)); diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 054d9341b..af32c6936 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -134,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", @@ -582,6 +585,7 @@ export const LIB_FN_SYMS: Record = { "crypto.ecdhConvertKey": "scr_crypto_ecdh_convert_key", "crypto.dhNew": "scr_crypto_dh_new", "crypto.checkPrimeSync": "scr_crypto_check_prime_sync", + "crypto.getCipherInfo": "scr_crypto_get_cipher_info", "crypto.checkPrimeCb": "scr_crypto_check_prime_async", "crypto.generatePrimeSync": "scr_crypto_generate_prime_sync", "crypto.generatePrimeCb": "scr_crypto_generate_prime_async", @@ -893,6 +897,7 @@ export const LIB_FN_SYMS: Record = { // ambient-this read never throw. The fs dyn read is the sync-fs story. "fetch.start": "scr_fetch_static", "fetch.responseNew": "scr_fetch_response_new", + "fetch.responseLight": "scr_fetch_response_light", "fetch.responseJsonStatic": "scr_fetch_response_json_static", "fetch.input": "scr_fetch_input", "abort.controllerNew": "scr_fetch_abort_controller_new", @@ -1135,6 +1140,8 @@ export const LIB_FN_SYMS: Record = { "readable.pushEncoding": "scr_stream_set_push_encoding", "stream.objectMode": "scr_stream_object_mode", "stream.decodeStrings": "scr_stream_decode_strings", + "stream.optionValues": "scr_stream_option_values", + "stream.checkEncodingOption": "scr_stream_check_encoding_option", "stream.duplexFromGen": "scr_stream_duplex_from_gen", "stream.duplexFromDyn": "scr_stream_duplex_from_dyn", "stream.duplexifyReadable": "scr_stream_duplexify_readable", @@ -1319,6 +1326,7 @@ export const LIB_FN_SYMS: Record = { "http.reqPipeClient": "scr_http_req_pipe_client", "http.reqPipeSock": "scr_http_req_pipe_sock", "http.reqResume": "scr_http_req_resume", + "http.reqPause": "scr_http_req_pause", "http.reqDestroy": "scr_http_req_destroy", "http.reqSetEncoding": "scr_http_req_set_encoding", "http.resSetHeader": "scr_http_res_set_header", @@ -1553,11 +1561,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", @@ -1571,6 +1581,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", @@ -1765,6 +1776,7 @@ export const USES_TIMERS_LIB_FNS = new Set([ "stream.setTransform", "stream.setFlush", "stream.setConstruct", + "stream.replaceConstruct", "stream.duplexFromGen", "stream.duplexFromDyn", "stream.duplexifyReadable", diff --git a/packages/compiler/src/backend/llvm/lib-stream.ts b/packages/compiler/src/backend/llvm/lib-stream.ts index 5fd4af1a9..fdbf13d4f 100644 --- a/packages/compiler/src/backend/llvm/lib-stream.ts +++ b/packages/compiler/src/backend/llvm/lib-stream.ts @@ -189,6 +189,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV e.fn === "stream.setTransform" || e.fn === "stream.setFlush" || e.fn === "stream.setConstruct" || + e.fn === "stream.replaceConstruct" || e.fn === "stream.setWritev" || e.fn === "stream.setWritevDefault" ) { @@ -203,6 +204,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV "stream.setTransform": "t", "stream.setFlush": "l", "stream.setConstruct": "c", + "stream.replaceConstruct": "c", "stream.setWritev": "v", "stream.setWritevDefault": "v", }; @@ -214,6 +216,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV "stream.setTransform": "scr_stream_set_transform", "stream.setFlush": "scr_stream_set_flush", "stream.setConstruct": "scr_stream_set_construct", + "stream.replaceConstruct": "scr_stream_replace_construct", "stream.setWritev": "scr_stream_set_writev", "stream.setWritevDefault": "scr_stream_set_writev_default", }; diff --git a/packages/compiler/src/backend/llvm/local-array-reads.ts b/packages/compiler/src/backend/llvm/local-array-reads.ts index 1c540295a..80ed2c440 100644 --- a/packages/compiler/src/backend/llvm/local-array-reads.ts +++ b/packages/compiler/src/backend/llvm/local-array-reads.ts @@ -797,7 +797,33 @@ export function emitLocalArrayRead( ); return null; }); - else if (inline) + else if ( + inline && + loopIndex !== null && + read.element.kind === "f64" && + host.provenDenseIndex(read.array, read.index) + ) { + // In a dense array loop's fast copy, the entry test proved the counter + // below the array's hole-free prefix: the slot holds a present value, + // and the read has no missing arm. + const dataPtr = B.tmp(), + data = B.tmp(), + valuePtr = B.tmp(), + value = B.tmp(), + width = int32 ? "i32" : "i64"; + B.line( + `${dataPtr} = getelementptr inbounds %ScrArr, ptr ${array.name}, i32 0, i32 ${SCR_ARR_FIELD.data}`, + ); + host.markMemoryPointer(dataPtr, "array:header"); + B.line(`${data} = load ptr, ptr ${dataPtr}${host.fieldAliasAttachment(dataPtr)}`); + B.line( + `${valuePtr} = getelementptr inbounds ${width}, ptr ${data}, ${host.sizeType} ${loopIndex}`, + ); + host.markMemoryPointer(valuePtr, "array:elements"); + B.line(`${value} = load ${width}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); + storeValue(value); + B.br(join); + } else if (inline) emitDenseReferenceArrayRead( host, array, diff --git a/packages/compiler/src/backend/llvm/nullable-unions.ts b/packages/compiler/src/backend/llvm/nullable-unions.ts index 2c355f551..d9b9cdfcf 100644 --- a/packages/compiler/src/backend/llvm/nullable-unions.ts +++ b/packages/compiler/src/backend/llvm/nullable-unions.ts @@ -6,6 +6,7 @@ import { type IrUnionDef, } from "../../ir/ir.js"; import { everyModuleNode } from "../../ir/traverse.js"; +import type { IrNodeIndex } from "../../ir/node-index.js"; /** A union of exactly one reference arm and one or two unit arms * (`C | undefined`, `C | null`, `C | null | undefined`) whose VALUES are @@ -120,12 +121,12 @@ export class NullableUnions { /** `shapes`: allow shape unions (debug builds keep boxes, whose * debug-info layouts describe every arm). */ - constructor(mod: IrModule, shapes = true) { + constructor(mod: IrModule, shapes = true, nodes?: IrNodeIndex) { const emitted = new Set(); for (const cls of mod.classes ?? []) if (!cls.runtime) emitted.add(cls.name); const records = new Set(); for (const shape of mod.records ?? []) records.add(shape.id); - const boxed = boxedUnionIds(mod); + const boxed = boxedUnionIds(mod, nodes); // Shape unions stay out of library builds (their host ABI marshals // boxes) and of programs with lifetime regions (whose walks report a // reference slot by its type's release entry point). @@ -277,13 +278,20 @@ function shapeUnion( /** Unions the runtime itself inspects as tagged boxes keep that * representation: Map keys and Set elements hash and compare union keys by * tag and payload inside the C map. */ -function boxedUnionIds(mod: IrModule): Set { +function boxedUnionIds(mod: IrModule, nodes?: IrNodeIndex): 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); }; + if (nodes !== undefined) { + for (const t of nodes.types) { + if (t.kind === "map") mark(t.key); + else if (t.kind === "set") mark(t.elem); + } + return boxed; + } everyModuleNode(mod, { expr: () => true, stmt: () => true, diff --git a/packages/compiler/src/backend/llvm/receivers.ts b/packages/compiler/src/backend/llvm/receivers.ts index 67ca17188..2fe9dd66b 100644 --- a/packages/compiler/src/backend/llvm/receivers.ts +++ b/packages/compiler/src/backend/llvm/receivers.ts @@ -18,6 +18,7 @@ import { canStackUnion, emitStackUnion } from "./stack-unions.js"; import { emitDowncastGuard, emitUnionNarrowGuard } from "./narrow-guards.js"; import { emitScalarOptional, isScalarOptionalSource } from "./local-union-storage.js"; import { borrowableInputs } from "./borrowed-inputs.js"; +import { emitStackRecordKeyGet, scalarRecordKeyGet } from "./expr-records.js"; import { type WalkBorrowHost, analyzeWalks } from "./walk-borrows.js"; /** Read a receiver whose value is consumed immediately by a field or tag @@ -95,6 +96,8 @@ export function emitReadReceiver(emitter: LlEmitter, e: IrExpr): LlValue { const narrowed = narrow ? emitCheckedNarrowReceiver(emitter, narrow) : null; if (narrowed) return narrowed; if (canStackUnion(e, emitter.boxedUnionsById)) return emitStackUnion(emitter, e).value; + if (e.kind === "recordKeyGet" && scalarRecordKeyGet(emitter, e)) + return { name: emitStackRecordKeyGet(emitter, e), type: e.type }; if (e.kind === "unionNarrow") { const union = emitter.emitUnionProjection(e.value); emitUnionNarrowGuard(emitter, union.name, e.unionId, e.tag); @@ -342,6 +345,7 @@ export function analyzeBorrowedReturns(emitter: LlEmitter): void { borrowsWithoutOwning: (e: IrExpr) => emitter.borrowsWithoutOwning(e), borrowedReturn: (callee: string) => emitter.borrowedReturns.has(callee), parameterAllowed: (id: string) => allowed.has(id), + nodes: emitter.nodeIndex, }; return emitter.referenceEffects.functions.has(fn.name) ? host @@ -409,6 +413,7 @@ export function isStackUnionSource(emitter: LlEmitter, e: IrExpr): boolean { emitter.optionalArrayReads.get(e) !== null || isScalarOptionalSource(e, emitter.unionSources) || emitter.scalarResultCall(e) || - emitter.inlineFieldGet(e) + emitter.inlineFieldGet(e) || + scalarRecordKeyGet(emitter, e) ); } diff --git a/packages/compiler/src/backend/llvm/reference-writes.ts b/packages/compiler/src/backend/llvm/reference-writes.ts index df2ec5032..89132a715 100644 --- a/packages/compiler/src/backend/llvm/reference-writes.ts +++ b/packages/compiler/src/backend/llvm/reference-writes.ts @@ -9,6 +9,7 @@ import { type IrUnionDef, } from "../../ir/ir.js"; import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; +import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "../../ir/node-index.js"; import { borrowsMapMutationReceiver, borrowsMapReadInputs } from "./map-read-lifetimes.js"; import { preservesRegexInputs, VirtualTargets } from "./reference-effects.js"; import { borrowsStringInputs } from "./string-lifetimes.js"; @@ -227,6 +228,8 @@ export class ReferenceWrites { /** Fields no code writes once their object is constructed * (immutability.ts, from cycle-mutability's construction-only facts). */ private readonly immutable: ImmutableFields | null = null, + /** The module's node index, read instead of walking the bodies. */ + private readonly nodes?: IrNodeIndex, ) { this.key(HEAP_KEY); this.unions = new Map((types.unions ?? []).map((u) => [u.id, u])); @@ -236,25 +239,25 @@ export class ReferenceWrites { const constructors = new Set(classes.map((cls) => `%${cls.name}.constructor`)); this.constructors = constructors; this.freshConstructors = [...functions.values()].every((fn) => - fn.body.every((s) => this.onlySuperCalls(fn, s, constructors)), + this.onlySuperCalls(fn, constructors), ); - this.callTargets = new CallTargets(functions, this.virtualTargets); + this.callTargets = new CallTargets(functions, this.virtualTargets, nodes); this.solve(constructors); } - /** Every direct call of a constructor is a `super` call from a + /** Every direct call of a constructor in `fn` is a `super` call from a * constructor on its own `this`. */ - private onlySuperCalls(fn: IrFunction, s: IrStmt, constructors: ReadonlySet): boolean { + private onlySuperCalls(fn: IrFunction, constructors: ReadonlySet): boolean { const thisId = constructors.has(fn.name) ? fn.params[0]?.localId : undefined; - const expr = (e: IrExpr): boolean => { - if (e.kind === "call" && constructors.has(e.callee)) { - const self = e.args[0]; - if (thisId === undefined || !self || !isThisRef(self, thisId)) return false; + let only = true; + eachNodeOfKinds(fn, this.nodes?.scan(CALL_KINDS), (node) => { + if (node.kind === "call" && constructors.has(node.callee)) { + const self = node.args[0]; + if (thisId === undefined || !self || !isThisRef(self, thisId)) only = false; } - return everyExprChild(e, expr, stmt); - }; - const stmt = (node: IrStmt): boolean => everyStmtChild(node, expr, stmt); - return stmt(s); + return only; + }); + return only; } private key(k: string): number { @@ -299,7 +302,7 @@ export class ReferenceWrites { } const thisId = external && constructors.has(fn.name) ? (fn.params[0]?.localId ?? null) : null; const boxed = boxedLocals(fn); - const privates = privateArrays(fn, boxed); + const privates = privateArrays(fn, boxed, this.nodes); const sink: WriteSink = { add: (k) => { // Interned for queries; a callee's locals are private to it. @@ -749,7 +752,7 @@ export class ReferenceWrites { enter(fn: IrFunction): void { this.current = fn; this.currentBoxed = boxedLocals(fn); - this.currentPrivateArrays = privateArrays(fn, this.currentBoxed); + this.currentPrivateArrays = privateArrays(fn, this.currentBoxed, this.nodes); this.currentLocals = new Set(fn.locals.map((local) => local.id)); this.inConstructor = this.constructors.has(fn.name); for (const p of fn.params) this.currentLocals.add(p.localId); @@ -973,6 +976,29 @@ function privateReceiver(arr: IrExpr, privates: ReadonlySet): boolean { * element out nor let the array escape. */ const PRIVATE_ARRAY_METHODS = new Set(["length", "push"]); +/** The nodes the scans below read. */ +const CALL_KINDS: readonly IrNodeKind[] = ["call"]; +const CALL_SITE_KINDS: readonly IrNodeKind[] = ["call", "new", "virtualCall", "closure"]; +const DEFINITION_KINDS: readonly IrNodeKind[] = ["assignExpr", "incDec", "varDecl", "assign"]; + +/** The nodes privateArrays reads. */ +const PRIVATE_ARRAY_KINDS: readonly IrNodeKind[] = [ + "varRef", + "assignExpr", + "incDec", + "closure", + "classRef", + "arrIntrinsic", + "arrayHas", + "varDecl", + "assign", + "arraySet", + "arraySetUndefined", + "arrayDelete", + "arraySetLength", + "return", +]; + /** Array locals whose storage the function allocated itself and that * nothing else can reach before the function returns them: declared once * from an array literal or `new Array(n)`, never rebound, unboxed, and @@ -985,7 +1011,11 @@ const PRIVATE_ARRAY_METHODS = new Set(["length", "push"]); * stay out of the function's summary (a caller's borrows predate the * array). The literal map, filter and fused-pipeline helpers fill their * results this way. */ -function privateArrays(fn: IrFunction, boxed: ReadonlySet): ReadonlySet { +function privateArrays( + fn: IrFunction, + boxed: ReadonlySet, + nodes: IrNodeIndex | undefined, +): ReadonlySet { if (fn.async || fn.generator) return new Set(); const candidates = new Set(); for (const local of fn.locals) @@ -1001,54 +1031,50 @@ function privateArrays(fn: IrFunction, boxed: ReadonlySet): ReadonlySet< if (e.kind === "varRef" && candidates.has(e.localId)) permitted.set(e, (permitted.get(e) ?? 0) + 1); }; - const expr = (e: IrExpr): boolean => { - switch (e.kind) { + eachNodeOfKinds(fn, nodes?.scan(PRIVATE_ARRAY_KINDS), (node) => { + switch (node.kind) { case "varRef": { - const left = permitted.get(e) ?? 0; - if (left === 0) rejected.add(e.localId); - else permitted.set(e, left - 1); + const left = permitted.get(node) ?? 0; + if (left === 0) rejected.add(node.localId); + else permitted.set(node, left - 1); break; } case "assignExpr": case "incDec": - rejected.add(e.localId); + rejected.add(node.localId); break; case "closure": case "classRef": - for (const id of e.captures ?? []) rejected.add(id); + for (const id of node.captures ?? []) rejected.add(id); break; case "arrIntrinsic": - if (PRIVATE_ARRAY_METHODS.has(e.method)) permit(e.receiver); + if (PRIVATE_ARRAY_METHODS.has(node.method)) permit(node.receiver); break; case "arrayHas": - permit(e.arr); + permit(node.arr); break; - } - return everyExprChild(e, expr, stmt); - }; - const stmt = (s: IrStmt): boolean => { - switch (s.kind) { case "varDecl": - declarations.set(s.localId, (declarations.get(s.localId) ?? 0) + 1); - if (s.init === null || (s.init.kind !== "arrayLit" && s.init.kind !== "arrayNewLen")) - rejected.add(s.localId); + declarations.set(node.localId, (declarations.get(node.localId) ?? 0) + 1); + if ( + node.init === null || + (node.init.kind !== "arrayLit" && node.init.kind !== "arrayNewLen") + ) + rejected.add(node.localId); break; case "assign": - rejected.add(s.localId); + rejected.add(node.localId); break; case "arraySet": case "arraySetUndefined": case "arrayDelete": case "arraySetLength": - permit(s.arr); + permit(node.arr); break; case "return": - if (s.value !== null) permit(s.value); + if (node.value !== null) permit(node.value); break; } - return everyStmtChild(s, expr, stmt); - }; - fn.body.every(stmt); + }); const out = new Set(); for (const id of candidates) if (!rejected.has(id) && declarations.get(id) === 1) out.add(id); return out; @@ -1081,30 +1107,30 @@ class CallTargets { constructor( private readonly functions: ReadonlyMap, virtualTargets: VirtualTargets, + private readonly nodes: IrNodeIndex | undefined, ) { const site = (callee: string, caller: IrFunction, args: readonly IrExpr[]): void => { let list = this.sites.get(callee); if (!list) this.sites.set(callee, (list = [])); list.push({ caller, args }); }; + const scan = nodes?.scan(CALL_SITE_KINDS); for (const fn of functions.values()) { if (fn.captures !== undefined || fn.classCaptures !== undefined) this.opaque.add(fn.name); - const expr = (e: IrExpr): boolean => { - if (e.kind === "call") site(e.callee, fn, e.args); - else if (e.kind === "new") site(`%${e.className}.constructor`, fn, [e, ...e.args]); - else if (e.kind === "virtualCall") { - const targets = virtualTargets.targets(e.className, e.method); - for (const t of targets ?? []) site(t, fn, e.args); - } else if (e.kind === "closure") { - this.opaque.add(e.fnName); - let list = this.creators.get(e.fnName); - if (!list) this.creators.set(e.fnName, (list = [])); + eachNodeOfKinds(fn, scan, (node) => { + if (node.kind === "call") site(node.callee, fn, node.args); + else if (node.kind === "new") + site(`%${node.className}.constructor`, fn, [node, ...node.args]); + else if (node.kind === "virtualCall") { + const targets = virtualTargets.targets(node.className, node.method); + for (const t of targets ?? []) site(t, fn, node.args); + } else if (node.kind === "closure") { + this.opaque.add(node.fnName); + let list = this.creators.get(node.fnName); + if (!list) this.creators.set(node.fnName, (list = [])); list.push(fn); } - return everyExprChild(e, expr, stmt); - }; - const stmt = (st: IrStmt): boolean => everyStmtChild(st, expr, stmt); - fn.body.every(stmt); + }); } // Fixpoint over function-typed parameter positions. const positions: [IrFunction, number][] = []; @@ -1225,18 +1251,13 @@ class CallTargets { defs = new Map(); const boxed = new Set(fn.locals.filter((l) => l.boxed).map((l) => l.id)); const counts = new Map(); - const expr = (e: IrExpr): boolean => { - if (e.kind === "assignExpr" || e.kind === "incDec") counts.set(e.localId, 2); - return everyExprChild(e, expr, stmt); - }; - const stmt = (st: IrStmt): boolean => { - if (st.kind === "varDecl") { - counts.set(st.localId, (counts.get(st.localId) ?? 0) + 1); - defs!.set(st.localId, st.init); - } else if (st.kind === "assign") counts.set(st.localId, 2); - return everyStmtChild(st, expr, stmt); - }; - fn.body.every(stmt); + eachNodeOfKinds(fn, this.nodes?.scan(DEFINITION_KINDS), (node) => { + if (node.kind === "assignExpr" || node.kind === "incDec") counts.set(node.localId, 2); + else if (node.kind === "varDecl") { + counts.set(node.localId, (counts.get(node.localId) ?? 0) + 1); + defs!.set(node.localId, node.init); + } else if (node.kind === "assign") counts.set(node.localId, 2); + }); for (const [local, n] of counts) if (n !== 1 || boxed.has(local)) defs.set(local, null); this.definitions.set(fn, defs); } diff --git a/packages/compiler/src/backend/llvm/walk-borrows.ts b/packages/compiler/src/backend/llvm/walk-borrows.ts index dc3e03c19..86b8f3d21 100644 --- a/packages/compiler/src/backend/llvm/walk-borrows.ts +++ b/packages/compiler/src/backend/llvm/walk-borrows.ts @@ -1,5 +1,5 @@ import { isRefCounted, type IrExpr, type IrFunction, type IrLocal } from "../../ir/ir.js"; -import { everyStmtList } from "../../ir/traverse.js"; +import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "../../ir/node-index.js"; /** What the emitter knows about a projection's storage. */ export interface WalkBorrowHost { @@ -22,8 +22,24 @@ export interface WalkBorrowHost { * (whose own rebinding releases nothing). Absent: the body preserves * every edge. */ unwritten?(value: IrExpr, candidates: ReadonlySet): boolean; + /** The module's node index, read instead of walking the body. */ + nodes?: IrNodeIndex; } +/** The node kinds analyzeWalks reads from a body. */ +const WALK_KINDS: readonly IrNodeKind[] = [ + "varDecl", + "assign", + "forOf", + "rethrow", + "tryCatch", + "return", + "assignExpr", + "incDec", + "closure", + "classRef", +]; + export interface WalkFacts { locals: ReadonlySet; /** Every return yields a walk (or a throw), so the body can return its @@ -71,44 +87,36 @@ export function analyzeWalks(fn: IrFunction, host: WalkBorrowHost): WalkFacts { if (!list) definitions.set(id, (list = [])); list.push(value); }; - everyStmtList(fn.body, { - stmt: (s) => { - switch (s.kind) { - case "varDecl": - define(s.localId, s.init); - break; - case "assign": - define(s.localId, s.value); - break; - case "forOf": - excluded.add(s.localId); - break; - case "rethrow": - excluded.add(s.localId); - break; - case "tryCatch": - guarded = true; - if (s.catchLocalId !== null) excluded.add(s.catchLocalId); - break; - case "return": - returns.push(s.value); - break; - } - return true; - }, - expr: (e) => { - switch (e.kind) { - case "assignExpr": - case "incDec": - excluded.add(e.localId); - break; - case "closure": - case "classRef": - for (const id of e.captures ?? []) excluded.add(id); - break; - } - return true; - }, + eachNodeOfKinds(fn, host.nodes?.scan(WALK_KINDS), (node) => { + switch (node.kind) { + case "varDecl": + define(node.localId, node.init); + break; + case "assign": + define(node.localId, node.value); + break; + case "forOf": + excluded.add(node.localId); + break; + case "rethrow": + excluded.add(node.localId); + break; + case "tryCatch": + guarded = true; + if (node.catchLocalId !== null) excluded.add(node.catchLocalId); + break; + case "return": + returns.push(node.value); + break; + case "assignExpr": + case "incDec": + excluded.add(node.localId); + break; + case "closure": + case "classRef": + for (const id of node.captures ?? []) excluded.add(id); + break; + } }); const plain = (id: string): boolean => { const local = locals.get(id); diff --git a/packages/compiler/src/backend/llvm/walkers.ts b/packages/compiler/src/backend/llvm/walkers.ts index 044cb79ae..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 @@ -328,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); } diff --git a/packages/compiler/src/backend/may-throw.ts b/packages/compiler/src/backend/may-throw.ts index a04cce856..0cfab9244 100644 --- a/packages/compiler/src/backend/may-throw.ts +++ b/packages/compiler/src/backend/may-throw.ts @@ -3,7 +3,8 @@ import type { IrExpr, IrStmt, IrModule, IrType, SrcLoc } from "../ir/ir.js"; import { isFfiCallbackParam, MAY_THROW_ARR_METHODS, MAY_THROW_BYTES_METHODS } from "../ir/ir.js"; import { MAY_THROW_LIB_FNS } from "../ir/builtin-effects.js"; -import { everyExprChild, everyStmtChild, everyStmtList } from "../ir/traverse.js"; +import { everyExprChild, everyStmtChild } from "../ir/traverse.js"; +import { eachNodeOfKinds, type IrNodeIndex } from "../ir/node-index.js"; import { hasRetainedFfiCallback } from "./ffi-callbacks.js"; import { computePublishedTypes, writesPublished } from "./publication.js"; @@ -40,6 +41,8 @@ export function computeMayThrow( explain?: boolean; /** computePublishedTypes(mod), when the caller already has it. */ published?: ReturnType; + /** The module's node index, read instead of walking the bodies. */ + nodes?: IrNodeIndex; } = {}, ): { fns: Set; @@ -52,8 +55,9 @@ export function computeMayThrow( * entry (and may therefore throw the stack-overflow RangeError). */ stackChecks?: Set; } { - const terminations = mod.workers === true ? workerTerminationPolls(mod) : null; - const stackChecks = options.stackChecks === true ? stackCheckedFunctions(mod) : null; + const terminations = mod.workers === true ? workerTerminationPolls(mod, options.nodes) : null; + const stackChecks = + options.stackChecks === true ? stackCheckedFunctions(mod, options.nodes) : null; // Stores that can reach a published object throw the frozen TypeError. const published = options.published !== undefined ? options.published : computePublishedTypes(mod); @@ -898,7 +902,19 @@ function boundedNode(rec: IrExpr | IrStmt): boolean { * finishes after finitely many steps or reaches a polling callee. `polls` * names the functions that poll (and therefore may unwind with the * termination sentinel); `entry` the subset that polls on entry. */ -export function workerTerminationPolls(mod: IrModule): { +/** The kinds call-graph census below can skip: boundedNode answers true + * for them, and the census reads nothing else from them. */ +const CENSUS_SKIPPED_KINDS: ReadonlySet = new Set( + [...BOUNDED_KINDS].filter( + (kind) => + !["while", "doWhile", "for", "forOf", "call", "new", "virtualCall", "closure"].includes(kind), + ), +); + +export function workerTerminationPolls( + mod: IrModule, + nodes?: IrNodeIndex, +): { entry: Set; polls: Set; } { @@ -914,11 +930,12 @@ export function workerTerminationPolls(mod: IrModule): { const edges = new Map(); const entry = new Set(); const polls = new Set(); + const scan = nodes?.scanExcept(CENSUS_SKIPPED_KINDS); for (const fn of mod.functions) { const out: string[] = []; let bounded = fn.async !== true && fn.generator === undefined && fn.name !== mod.entry; let loops = false; - const visit = (rec: IrExpr | IrStmt): boolean => { + const visit = (rec: IrExpr | IrStmt): void => { if (!boundedNode(rec)) bounded = false; switch (rec.kind) { case "while": @@ -937,9 +954,8 @@ export function workerTerminationPolls(mod: IrModule): { out.push(...(methodImpls.get(rec.method) ?? [])); break; } - return true; }; - everyStmtList(fn.body, { expr: visit, stmt: visit }); + eachNodeOfKinds(fn, scan, visit); edges.set( fn.name, out.filter((callee) => names.has(callee)), @@ -960,7 +976,7 @@ export function workerTerminationPolls(mod: IrModule): { * library call; see boundedNode) also reaches every function that such an * operation can invoke: closure targets and class members. Non-recursive * functions, including leaf callbacks, stay check-free. */ -export function stackCheckedFunctions(mod: IrModule): Set { +export function stackCheckedFunctions(mod: IrModule, nodes?: IrNodeIndex): Set { const reentry = "\0reentry"; const methodImpls = new Map(); const memberPrefixes = new Set(); @@ -975,12 +991,13 @@ export function stackCheckedFunctions(mod: IrModule): Set { const names = new Set(mod.functions.map((fn) => fn.name)); const edges = new Map(); const targets = new Set(); + const scan = nodes?.scanExcept(CENSUS_SKIPPED_KINDS); for (const fn of mod.functions) { const dot = fn.name.indexOf("."); if (dot > 0 && memberPrefixes.has(fn.name.slice(0, dot + 1))) targets.add(fn.name); const out: string[] = []; let reenters = false; - const visit = (rec: IrExpr | IrStmt): boolean => { + const visit = (rec: IrExpr | IrStmt): void => { if (!boundedNode(rec)) reenters = true; switch (rec.kind) { case "call": @@ -996,9 +1013,8 @@ export function stackCheckedFunctions(mod: IrModule): Set { targets.add(rec.fnName); break; } - return true; }; - everyStmtList(fn.body, { expr: visit, stmt: visit }); + eachNodeOfKinds(fn, scan, visit); const callees = out.filter((callee) => names.has(callee)); if (reenters) callees.push(reentry); edges.set(fn.name, callees); 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 1b90bb1f1..233b9cac2 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -279,6 +279,8 @@ 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. diff --git a/packages/compiler/src/backend/native/runtime-inputs.ts b/packages/compiler/src/backend/native/runtime-inputs.ts index bc9643670..6fb51fd32 100644 --- a/packages/compiler/src/backend/native/runtime-inputs.ts +++ b/packages/compiler/src/backend/native/runtime-inputs.ts @@ -72,6 +72,7 @@ export const EXECUTABLE_RUNTIME_SOURCES = [ "scr_itable.c", "scr_slots.c", "scr_alloc.c", + "scr_cipher_info.c", ] as const; export function runtimeSrcDir(): string { diff --git a/packages/compiler/src/backend/publication.ts b/packages/compiler/src/backend/publication.ts index af65fa39a..473a0cba4 100644 --- a/packages/compiler/src/backend/publication.ts +++ b/packages/compiler/src/backend/publication.ts @@ -3,7 +3,7 @@ * emitter guards them. */ import type { IrExpr, IrModule, IrStmt, IrType } from "../ir/ir.js"; import { typeKey } from "../ir/ir.js"; -import { everyStmtList } from "../ir/traverse.js"; +import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "../ir/node-index.js"; import { computeConstructionFacts } from "./immutability.js"; /** Array methods that write their receiver (the guarded ones). */ @@ -71,17 +71,22 @@ export class PublishedTypes { } } -export function computePublishedTypes(mod: IrModule): PublishedTypes | null { +/** The nodes that publish a value: intrinsics. */ +const PUBLISHING_KINDS: readonly IrNodeKind[] = ["intrinsic"]; + +export function computePublishedTypes(mod: IrModule, nodes?: IrNodeIndex): PublishedTypes | null { const roots: IrType[] = []; - const visit = (e: IrExpr): boolean => { - // Values read from an InternTable were published by whichever thread - // stored them. - if (e.kind === "intrinsic" && (e.name === "threads.publish" || e.name === "itable.value")) - roots.push(e.type); - return true; - }; + const scan = nodes?.scan(PUBLISHING_KINDS); for (const fn of mod.functions) - everyStmtList(fn.body, { expr: visit, stmt: (_s: IrStmt) => true }); + eachNodeOfKinds(fn, scan, (node) => { + // Values read from an InternTable were published by whichever thread + // stored them. + if ( + node.kind === "intrinsic" && + (node.name === "threads.publish" || node.name === "itable.value") + ) + roots.push(node.type); + }); if (roots.length === 0) return null; const classes = new Map((mod.classes ?? []).map((c) => [c.name, c])); @@ -95,7 +100,7 @@ export function computePublishedTypes(mod: IrModule): PublishedTypes | null { const records = new Map((mod.records ?? []).map((r) => [r.id, r])); const unions = new Map((mod.unions ?? []).map((u) => [u.id, u])); const out = new PublishedTypes(); - out.privateStores = computeConstructionFacts(mod).privateStores; + out.privateStores = computeConstructionFacts(mod, nodes).privateStores; const seen = new Set(); const stack = [...roots]; const addHierarchy = (className: string): void => { diff --git a/packages/compiler/src/backend/runtime-abi.ts b/packages/compiler/src/backend/runtime-abi.ts index 7d3dcbc02..11bde6c2b 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 = 33 as const; -export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v33" as const; +export const RUNTIME_ABI_VERSION = 37 as const; +export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v37" 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 532051b88..44c3d6290 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: 33, marker: "scr_runtime_abi_v33" }, + runtime_abi: { version: 37, marker: "scr_runtime_abi_v37" }, 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 18b04b9a6..e3bc999a7 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: 33, marker: "scr_runtime_abi_v33" }, + runtime_abi: { version: 37, marker: "scr_runtime_abi_v37" }, 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 c448897f1..0709d588e 100644 --- a/packages/compiler/src/backend/stack-observations.ts +++ b/packages/compiler/src/backend/stack-observations.ts @@ -1,15 +1,32 @@ /* Whole-program stack-trace gate (see observesStacks). Pure function of the * IR module; the emitter keeps a source frame in every function only when * it answers true. */ -import { exprFunctionReference, moduleFunctionReferences } from "../ir/function-references.js"; +import { + exprFunctionReference, + FUNCTION_REFERENCE_KINDS, + moduleFunctionReferences, +} from "../ir/function-references.js"; +import { + eachNodeOfKinds, + type IrKindScan, + type IrNodeIndex, + type IrNodeKind, +} from "../ir/node-index.js"; import { MAY_THROW_LIB_FNS } from "../ir/builtin-effects.js"; import type { IrExpr, IrFunction, IrModule, IrStmt, IrType } from "../ir/ir.js"; import { MAY_THROW_ARR_METHODS } from "../ir/ir.js"; -import { everyExprChild, everyStmtChild, everyStmtList } from "../ir/traverse.js"; +import { everyExprChild, everyStmtChild } from "../ir/traverse.js"; import { unreachableStatements } from "./may-throw.js"; import type { PublishedTypes } from "./publication.js"; import { writesPublished } from "./publication.js"; +/** The nodes the scans below read: stack reads, the "stack" literal, call + * sites and function references, declarations. */ +const STACK_READ_KINDS: readonly IrNodeKind[] = ["libCall", "dynKeyGet"]; +const STRING_KINDS: readonly IrNodeKind[] = ["strLit"]; +const CENSUS_KINDS: readonly IrNodeKind[] = ["call", ...FUNCTION_REFERENCE_KINDS]; +const DECLARATION_KINDS: readonly IrNodeKind[] = ["varDecl"]; + /** Lib calls that read a captured stack trace. */ const STACK_READS: ReadonlySet = new Set(["error.stack"]); @@ -40,10 +57,13 @@ class StackKeys { // 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) {} + constructor( + private readonly mod: IrModule, + private readonly nodes: IrNodeIndex | undefined, + ) {} private spellsStack(): boolean { - return (this.spelled ??= spellsStack(this.mod)); + return (this.spelled ??= spellsStack(this.mod, this.nodes)); } private callCensus(): { @@ -56,20 +76,18 @@ class StackKeys { 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)); + const scan = this.nodes?.scan(CENSUS_KINDS); for (const fn of mod.functions) { if (isClassMember(fn.name, classNames)) named.add(fn.name); - everyStmtList(fn.body, { - stmt: () => true, - expr: (expr) => { - if (expr.kind === "call") { - const sites = calls.get(expr.callee) ?? []; - sites.push({ caller: fn, args: expr.args }); - calls.set(expr.callee, sites); - } - const referenced = exprFunctionReference(expr); + eachNodeOfKinds(fn, scan, (node) => { + if (node.kind === "call") { + const sites = calls.get(node.callee) ?? []; + sites.push({ caller: fn, args: node.args }); + calls.set(node.callee, sites); + } else if (node.kind === "closure" || node.kind === "orDefault") { + const referenced = exprFunctionReference(node); if (referenced !== undefined) named.add(referenced); - return true; - }, + } }); } return (this.census = { calls, named }); @@ -127,13 +145,9 @@ class StackKeys { let inits = this.inits.get(fn); if (inits === undefined) { inits = new Map(); - everyStmtList(fn.body, { - stmt: (stmt) => { - if (stmt.kind === "varDecl") - inits!.set(stmt.localId, inits!.has(stmt.localId) ? null : (stmt.init ?? null)); - return true; - }, - expr: () => true, + eachNodeOfKinds(fn, this.nodes?.scan(DECLARATION_KINDS), (node) => { + if (node.kind === "varDecl") + inits!.set(node.localId, inits!.has(node.localId) ? null : (node.init ?? null)); }); this.inits.set(fn, inits); } @@ -161,21 +175,25 @@ function readsStack(fn: IrFunction, expr: IrExpr, keys: StackKeys): boolean { } /** Whether the module contains the string literal "stack". */ -function spellsStack(mod: IrModule): boolean { - return mod.functions.some( - (fn) => - !everyStmtList(fn.body, { - stmt: () => true, - expr: (expr) => !(expr.kind === "strLit" && expr.value === "stack"), - }), - ); +function spellsStack(mod: IrModule, nodes: IrNodeIndex | undefined): boolean { + const scan = nodes?.scan(STRING_KINDS); + return mod.functions.some((fn) => { + let found = false; + eachNodeOfKinds(fn, scan, (node) => { + found = node.kind === "strLit" && node.value === "stack"; + return !found; + }); + return found; + }); } -function containsStackRead(fn: IrFunction, keys: StackKeys): boolean { - return !everyStmtList(fn.body, { - stmt: () => true, - expr: (expr) => !readsStack(fn, expr, keys), +function containsStackRead(fn: IrFunction, keys: StackKeys, scan: IrKindScan | undefined): boolean { + let found = false; + eachNodeOfKinds(fn, scan, (node, statement) => { + found = !statement && readsStack(fn, node as IrExpr, keys); + return !found; }); + return found; } /** Visit every expression of `body` that execution can reach: statements @@ -199,9 +217,10 @@ function visitLiveExprs(body: IrStmt[], visit: (expr: IrExpr) => void): void { * Flight server keep development-only stack parsing behind `throw` stubs, * and those reads must not cost every call of the program a frame. * Programs without any stack read answer false without the walk. */ -export function observesStacks(mod: IrModule): boolean { - const keys = new StackKeys(mod); - if (!mod.functions.some((fn) => containsStackRead(fn, keys))) return false; +export function observesStacks(mod: IrModule, nodes?: IrNodeIndex): boolean { + const keys = new StackKeys(mod, nodes); + const reads = nodes?.scan(STACK_READ_KINDS); + if (!mod.functions.some((fn) => containsStackRead(fn, keys, reads))) return false; const byName = new Map(mod.functions.map((fn) => [fn.name, fn])); const classNames = new Set((mod.classes ?? []).map((cls) => cls.name)); const pending: string[] = [...moduleFunctionReferences(mod)]; diff --git a/packages/compiler/src/cli/usage.ts b/packages/compiler/src/cli/usage.ts index 23e57c66f..c4d07656a 100644 --- a/packages/compiler/src/cli/usage.ts +++ b/packages/compiler/src/cli/usage.ts @@ -77,9 +77,9 @@ Options: --npm-static compile the named npm packages' shipped JS statically as program modules (repeatable; "auto" opts in every eligible - direct import: own .d.ts, unminified JS, no build-transform - markers; "pkg..." also names every package pkg depends - on). Packages that fail preflight are refused with a + direct import: own .d.ts, no build-transform markers; + "pkg..." also names every package pkg depends on). + Packages that fail preflight are refused with a diagnostic. Eligible dependencies compile automatically. --provenance-sources EXPERIMENTAL: compile npm dependencies from their 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..eaf7486df --- /dev/null +++ b/packages/compiler/src/frontend/decl-projection.ts @@ -0,0 +1,870 @@ +/* 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("__@")); + } + + /** Whether an interface type and every type it extends are declared in + * user files. */ + private userInterface(type: ts.Type, seen = new Set()): boolean { + if (!declaredInUserFile(type.getSymbol())) return false; + const target = type.isTypeReference() ? type.getTarget() : type; + if (target === undefined || !target.isClassOrInterface() || seen.has(target)) return true; + seen.add(target); + return this.checker.getBaseTypes(target).every((base) => this.userInterface(base, seen)); + } + + 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), and + // only when every interface they extend is user-declared too: a user + // interface extending a package one (effect's Request.Request) + // inherits members the package's own constructors create. + if (symbol.flags & ts.SymbolFlags.Interface && !this.userInterface(type)) 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/lowering/builtins/crypto.ts b/packages/compiler/src/frontend/lowering/builtins/crypto.ts index 47e66f9b2..051ec18c4 100644 --- a/packages/compiler/src/frontend/lowering/builtins/crypto.ts +++ b/packages/compiler/src/frontend/lowering/builtins/crypto.ts @@ -869,9 +869,17 @@ 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}`), - ), + ...["128", "192", "256"].flatMap((bits) => [ + ...["ecb", "cfb", "cfb8", "cfb1", "ofb", "ccm"].map((mode) => `aes-${bits}-${mode}`), + `id-aes${bits}-ccm`, + ...["wrap", "wrap-pad"].flatMap((wrap) => [ + `aes-${bits}-${wrap}`, + `aes${bits}-${wrap}`, + `id-aes${bits}-${wrap}`, + `aes-${bits}-${wrap}-inv`, + `aes${bits}-${wrap}-inv`, + ]), + ]), "des-ede3-cbc", "des3", "des-ede3", @@ -889,9 +897,10 @@ const IMPLEMENTED_CIPHERS = new Set([ ]); const SUPPORTED_CIPHERS_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"; + "the supported ciphers are aes-128, aes-192 and aes-256 in cbc, ctr, gcm, ccm, ecb, cfb, cfb8, " + + "cfb1 and ofb modes and their wrap, wrap-pad, wrap-inv and wrap-pad-inv key wraps, 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 @@ -1380,10 +1389,11 @@ function takesUndefinedError(lowerer: Lowerer, callback: ts.Expression): boolean } /** 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. */ + * (scr_dh.c), and getCipherInfo (scr_cipher_info.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, bigint or info object converted to the call site's type. + * Null for other members. */ function lowerPrimeCall( lowerer: Lowerer, expr: ts.CallExpression, @@ -1395,6 +1405,7 @@ function lowerPrimeCall( 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])"], + getCipherInfo: ["crypto.getCipherInfo", 0, 2, "getCipherInfo(nameOrNid[, options])"], generatePrime: ["crypto.generatePrimeCb", 1, 3, "generatePrime(size[, options], callback)"], }; const form = forms[member]; @@ -1455,14 +1466,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/readline.ts b/packages/compiler/src/frontend/lowering/builtins/readline.ts index 846b9a926..aca209017 100644 --- a/packages/compiler/src/frontend/lowering/builtins/readline.ts +++ b/packages/compiler/src/frontend/lowering/builtins/readline.ts @@ -6,9 +6,11 @@ import { DYN, F64, type IrExpr, + type IrType, type IrStmt, type SrcLoc, STRING, + UNDEFINED_T, VOID, arrayOf, BYTES_U8, @@ -54,6 +56,21 @@ export function isReadlineTyped(lowerer: Lowerer, node: ts.Expression): boolean }); } +/** `rl.cursor`: @types/node declares a number, but an interface that is + * not a terminal answers undefined, so the read keeps its + * `number | undefined` union. */ +export function isOptionalReadlineCursorRead( + lowerer: Lowerer, + expr: ts.PropertyAccessExpression, +): boolean { + return ( + expr.name.text === "cursor" && + !expr.questionDotToken && + isReadlineTyped(lowerer, expr.expression) && + lowerer.isStdlibMember(expr) + ); +} + /** The runtime's listener event numbers (scr_readline.c). */ const RL_EVENTS: Record = { line: 0, @@ -1341,7 +1358,7 @@ export function lowerReadlineCreateCall( /** Property reads on an Interface: `history` (the history option itself, * or the interface's own empty array), `historySize`, `historyIndex`, * `crlfDelay`, `tabSize`, `escapeCodeTimeout`, `line`, `terminal` and - * `cursor` (a non-terminal interface's undefined cursor throws SC2020). + * `cursor` (undefined on a non-terminal interface). * Null for other receivers. */ export function lowerReadlineProperty( lowerer: Lowerer, @@ -1373,10 +1390,38 @@ export function lowerReadlineProperty( loc, }; } - const reads: Record = { + if (name === "cursor") { + // `number | undefined`: a non-terminal interface has no cursor + const recv = lowerer.lowerExprExpecting(access.expression, F64); + const type: IrType = { kind: "union", unionId: lowerer.unions.intern([F64, UNDEFINED_T]) }; + const local = recv.kind === "varRef" ? null : lowerer.declareHiddenLocal("%rlCursor", F64); + const id: IrExpr = local ? { kind: "varRef", localId: local.id, type: F64, loc } : recv; + const result: IrExpr = { + kind: "ternary", + cond: { kind: "libCall", fn: "rl.isTerminal", args: [id], type: BOOL, loc }, + then: lowerer.coerceToExpected( + { kind: "libCall", fn: "rl.cursor", args: [id], type: F64, loc }, + type, + ), + else_: lowerer.coerceToExpected( + { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }, + type, + ), + type, + loc, + }; + if (!local) return result; + return { + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: local.id, init: recv, loc }], + result, + type, + loc, + }; + } + const reads: Record = { line: ["rl.line", STRING], terminal: ["rl.isTerminal", BOOL], - cursor: ["rl.cursor", F64], }; const read = reads[name]; if (read !== undefined) { diff --git a/packages/compiler/src/frontend/lowering/cjs-inline-require.ts b/packages/compiler/src/frontend/lowering/cjs-inline-require.ts index 3d28dfae3..10549a030 100644 --- a/packages/compiler/src/frontend/lowering/cjs-inline-require.ts +++ b/packages/compiler/src/frontend/lowering/cjs-inline-require.ts @@ -31,6 +31,7 @@ import { requireBindingEarlyUse7, requireSpecOf, resolveImport, + splitRequireCallOf, } from "../program.js"; import { createRequireCalleeFileOf } from "./builtins/module-bindings.js"; import { isCjsRequireGlobal } from "./lower-node-module.js"; @@ -172,6 +173,10 @@ function computeSplitRequireBinding( const req = inlineProgramRequireOf(lowerer, assign.right); if (req === null) return null; + // Preflight treats the same bindings as require declarations (module edge, + // cycle admission, early-read fence); a binding it does not know keeps + // ordinary storage. + if (splitRequireCallOf(lowerer.program, decl) !== req.call) return null; // Any other write makes the value depend on runtime control flow. let writes = 0; 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-dynamic-dispatch.test.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts index b717452c2..bbdf271c0 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: [ @@ -558,3 +560,53 @@ test("class branches read their exact class's bag, also for classes with subclas then: [{ kind: "return", value: { kind: "dynObjLit", fields: [] } }], }); }); + +test("records a class's bag initializer only when all its class-table branches agree", () => { + const { lowerer } = context(); + const classBagInitializers = new Map(); + Object.assign(lowerer, { classBagInitializers }); + for (const name of ["Material", "Texture"]) + lowerer.classes.set(name, { def: { name, tracksOwnFields: true } } as unknown as ClassInfo); + const initializer = ( + className: string, + prototype: string, + start: number, + ): Extract => { + const at = { ...loc, start }; + return { + kind: "fieldSet", + obj: { + kind: "dynCheck", + value: { kind: "varRef", localId: "p.0", type: DYN, loc: at }, + type: { kind: "object", className }, + loc: at, + }, + className, + field: "%bag", + value: { + kind: "libCall", + fn: "dyn.objCreate", + args: [{ kind: "call", callee: prototype, args: [], type: DYN, loc: at }], + type: DYN, + loc: at, + }, + loc: at, + }; + }; + const dispatch = new ClassDynamicDispatch(); + const tables = (dispatch as unknown as { bagInitializers: Map }) + .bagInitializers; + // A base class whose own instances get its prototype and whose subclass + // instances get theirs through a per-instance dispatch has no single bag. + tables.set("Material", [ + initializer("Material", "%prototype.data.Material", 1), + initializer("Material", "%class.bagPrototype:Material", 2), + ]); + // Branches that differ only in source location agree. + tables.set("Texture", [ + initializer("Texture", "%prototype.data.Texture", 3), + initializer("Texture", "%prototype.data.Texture", 4), + ]); + dispatch.finalize(lowerer); + expect([...classBagInitializers.keys()]).toEqual(["Texture"]); +}); diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index e394718d1..ab3ebb34e 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -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,108 @@ 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, + }, + ]; +} + +/** `value` as JSON without its source locations. */ +function withoutLocations(value: IrExpr): string { + return JSON.stringify(value, (key, field: unknown) => (key === "loc" ? undefined : field)); +} + +/** 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 +1062,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 +1109,15 @@ 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. */ @@ -961,6 +1171,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); @@ -1092,33 +1303,6 @@ export class ClassDynamicDispatch { }); changed = true; } - if (this.prototypeIdentityHelper && !this.prototypeIdentityClasses.has(className)) { - this.prototypeIdentityClasses.add(className); - const loc = this.prototypeIdentityHelper.loc; - const type: IrType = { kind: "object", className }; - const value = varRef("value", DYN, loc); - const prototype = classPrototypeData(lowerer, info, loc, { - kind: "dynCheck", - value, - type, - loc, - }); - if (prototype) - this.prototypeIdentityHelper.body.unshift({ - kind: "if", - cond: { - kind: "libCall", - fn: "dyn.typedRefIs", - args: [value, { kind: "strLit", value: typeKey(type), type: STRING, loc }], - type: BOOL, - loc, - }, - then: [{ kind: "return", value: prototype, loc }], - else_: null, - loc, - }); - changed = true; - } if ( this.errorPrototypeHelper && !this.errorPrototypeClasses.has(className) && @@ -1229,6 +1413,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; @@ -1309,7 +1588,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) { @@ -1320,6 +1603,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; } } @@ -1591,29 +1879,41 @@ export class ClassDynamicDispatch { } 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, @@ -3561,6 +3861,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); @@ -4440,6 +4777,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, @@ -4516,6 +4874,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(); @@ -4536,6 +4895,94 @@ 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; + // The class-table branches can give the class's instances different + // bags (a base class's own prototype for its instances, the + // instance's class prototype for subclass instances); only a class + // whose branches all agree has one initializer for every instance. + const shape = withoutLocations(value); + if ( + initializers.some( + (other) => + withoutLocations(withTypedReceiver(structuredClone(other.value), self)) !== shape, + ) + ) + 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[] { @@ -4609,10 +5056,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", @@ -4653,6 +5107,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 }); } 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/coercions/structural.ts b/packages/compiler/src/frontend/lowering/coercions/structural.ts index c3b4ad906..4321a5ead 100644 --- a/packages/compiler/src/frontend/lowering/coercions/structural.ts +++ b/packages/compiler/src/frontend/lowering/coercions/structural.ts @@ -288,6 +288,14 @@ 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": @@ -775,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/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/containers/string-replacement.ts b/packages/compiler/src/frontend/lowering/containers/string-replacement.ts index 780c77d0f..156f74869 100644 --- a/packages/compiler/src/frontend/lowering/containers/string-replacement.ts +++ b/packages/compiler/src/frontend/lowering/containers/string-replacement.ts @@ -2,6 +2,7 @@ import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import { BOOL, + DYN, F64, REGEX, STRING, @@ -14,7 +15,7 @@ import { type IrType, type SrcLoc, } from "../../../ir/ir.js"; -import { numLit, strLit, varRef } from "../../../ir/build.js"; +import { dynUndefinedExpr, numLit, strLit, varRef } from "../../../ir/build.js"; import { InternalCompilerError } from "../../../errors.js"; import { locOf } from "../../program.js"; import type { Lowerer } from "../lowerer.js"; @@ -65,14 +66,14 @@ export function lowerStringReplacement( const search = argument(args[0], false); const replacement = argument(args[1], true); if (search.type.kind === "dyn" || replacement.type.kind === "dyn") { - if (replacement.type.kind === "func") { - lowerer.noLowering(`string .${method} with checked search values and callbacks`, call); - } // Evaluate all inputs once before choosing the search protocol. Regexes // retain their native pattern; plain values use the string-search helper. + // A typed callback receives the regex match's arguments or the plain + // search's (match, offset, subject). const values = [receiver, search, replacement]; const locals = values.map((value) => lowerer.declareHiddenLocal("%replaceArg", value.type)); const refs = locals.map((local) => varRef(local.id, local.type, loc)); + const callback = replacement.type.kind === "func"; const checkedReplacement = replacement.type.kind === "dyn" ? lowerer.coerceInto(args[1] ?? call, refs[2]!, STRING) @@ -91,14 +92,27 @@ export function lowerStringReplacement( ? { kind: "ternary", cond: { kind: "libCall", fn: "dyn.nativeRegexIs", args: [refs[1]!], type: BOOL, loc }, - then: { - kind: "regexIntrinsic", - method, - receiver: refs[0]!, - args: [lowerer.coerceInto(args[0] ?? call, refs[1]!, REGEX), checkedReplacement], - type: STRING, - loc, - }, + then: callback + ? { + kind: "libCall", + fn: "regex.replaceCallback", + args: [ + refs[0]!, + lowerer.coerceInto(args[0] ?? call, refs[1]!, REGEX), + lowerer.coerceToExpected(refs[2]!, DYN), + { kind: "boolLit", value: method === "replaceAll", type: BOOL, loc }, + ], + type: STRING, + loc, + } + : { + kind: "regexIntrinsic", + method, + receiver: refs[0]!, + args: [lowerer.coerceInto(args[0] ?? call, refs[1]!, REGEX), checkedReplacement], + type: STRING, + loc, + }, else_: plain, type: STRING, loc, @@ -148,11 +162,17 @@ function lowerStringReplacementValues( if (callback.kind !== "func") { throw new InternalCompilerError(`string .${method} replacement of type ${callback.kind}`); } + // A checked parameter receives the boxed argument (undefined after the + // subject), and a checked, number or boolean result converts with + // ToString. const full = [STRING, F64, STRING]; + const ret = callback.ret.kind; if ( - callback.ret.kind !== "string" || - callback.params.length > full.length || - !callback.params.every((type, index) => typeEquals(type, full[index]!)) + (ret !== "string" && ret !== "dyn" && ret !== "f64" && ret !== "bool") || + !callback.params.every( + (type, index) => + type.kind === "dyn" || (index < full.length && typeEquals(type, full[index]!)), + ) ) { lowerer.noLowering( `string .${method} callback signature`, @@ -175,13 +195,21 @@ function lowerStringReplacementValues( const chunksType = arrayOf(STRING); const chunks = varRef("chunks.0", chunksType, loc); const builder = stringBuilder(loc, chunks); - const value: IrExpr = { + const result: IrExpr = { kind: "callValue", callee: replace, - args: [needle, match, subject].slice(0, callback.params.length), - type: STRING, + args: callback.params.map((type, index) => { + const arg = [needle, match, subject][index]; + if (arg === undefined) return dynUndefinedExpr(loc); + return type.kind === "dyn" ? lowerer.coerceToExpected(arg, DYN) : arg; + }), + type: callback.ret, loc, }; + const value: IrExpr = + ret === "dyn" + ? { kind: "libCall", fn: "dyn.toStringCoerce", args: [result], type: STRING, loc } + : lowerer.ensureString(result, call); const stop: IrStmt = { kind: "break", loc }; const body: IrStmt[] = [ builder.declare("chunks.0", { kind: "arrayLit", elems: [], type: chunksType, loc }), diff --git a/packages/compiler/src/frontend/lowering/dispatch-sites.ts b/packages/compiler/src/frontend/lowering/dispatch-sites.ts index f5b8ebdb2..b9ab8f069 100644 --- a/packages/compiler/src/frontend/lowering/dispatch-sites.ts +++ b/packages/compiler/src/frontend/lowering/dispatch-sites.ts @@ -81,6 +81,7 @@ const DISPATCH_LIB_CALLS = new Set([ "dyn.propertyIsEnumerableComputed", "dyn.defineProperty", "dyn.getPrototype", + "dyn.getPrototypeForIdentity", "dyn.inheritsErrorPrototype", "dyn.reflectGet", "dyn.reflectSet", diff --git a/packages/compiler/src/frontend/lowering/divergences.ts b/packages/compiler/src/frontend/lowering/divergences.ts index c971c672d..87169539f 100644 --- a/packages/compiler/src/frontend/lowering/divergences.ts +++ b/packages/compiler/src/frontend/lowering/divergences.ts @@ -727,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/binary.ts b/packages/compiler/src/frontend/lowering/expressions/binary.ts index 58f6e16b5..17aef7b85 100644 --- a/packages/compiler/src/frontend/lowering/expressions/binary.ts +++ b/packages/compiler/src/frontend/lowering/expressions/binary.ts @@ -2841,6 +2841,24 @@ function lowerLogicalPair(lowerer: Lowerer, expr: ts.BinaryExpression, left: IrE loc, }; } + // A JavaScript default that tsc types `any` (`e.title || null` in an + // instance of a function with untyped parameters, where `e.title` is a + // string): the checked domain, as for union operands above. + if ( + target === null && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && + lowerer.dynConvertible(left.type) && + lowerer.dynConvertible(right.type) + ) { + return { + kind: "logical", + op: op === ts.SyntaxKind.AmpersandAmpersandToken ? "&&" : "||", + left: lowerer.coerceInto(expr.left, left, DYN), + right: lowerer.coerceInto(expr.right, right, DYN), + type: DYN, + loc, + }; + } lowerer.unsupported("SC1042", expr); } return { diff --git a/packages/compiler/src/frontend/lowering/expressions/element-access.ts b/packages/compiler/src/frontend/lowering/expressions/element-access.ts index 0be9be81a..0ea94ec7a 100644 --- a/packages/compiler/src/frontend/lowering/expressions/element-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/element-access.ts @@ -566,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 && @@ -618,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); diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts index 332fb182c..6d88d5db1 100644 --- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts +++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts @@ -865,7 +865,21 @@ 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)) { diff --git a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts index acac99610..01cab9901 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, @@ -32,6 +33,7 @@ import { isOptionalProcessVersionsRead, } from "../builtins/process.js"; import { requestGeneralStreamSurface } from "../general-streams.js"; +import { isOptionalReadlineCursorRead } from "../builtins/readline.js"; import { literalValues } from "../../literal-values.js"; import { literalUnionArm } from "../../union-discriminants.js"; import { checkedArmExtraction } from "../coercions/unions.js"; @@ -47,6 +49,7 @@ import { 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 @@ -615,6 +618,8 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx if (ts.isPropertyAccessExpression(node) && isOptionalProcessStreamProperty(lowerer, node)) return expr; if (isOptionalProcessVersionsRead(lowerer, node)) return expr; + if (ts.isPropertyAccessExpression(node) && isOptionalReadlineCursorRead(lowerer, node)) + return expr; // Represented fields and index values can include undefined even when // checker inference or lib.d.ts declares strings (RegExp captures). @@ -1516,6 +1521,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 @@ -1603,3 +1614,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 429abf793..4cc0bee1a 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -2145,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; 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 55ce433d3..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 diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts index b731e4912..f1590e9cc 100644 --- a/packages/compiler/src/frontend/lowering/expressions/relational.ts +++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts @@ -58,6 +58,44 @@ import { isExecExceptionType, refuseExecExceptionCode } from "../builtins/errors 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). @@ -400,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 @@ -432,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()` @@ -441,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", @@ -455,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( @@ -493,8 +598,11 @@ 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, @@ -551,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). @@ -978,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. */ diff --git a/packages/compiler/src/frontend/lowering/js-class-fields.ts b/packages/compiler/src/frontend/lowering/js-class-fields.ts index 2f726ce97..28a7ceeda 100644 --- a/packages/compiler/src/frontend/lowering/js-class-fields.ts +++ b/packages/compiler/src/frontend/lowering/js-class-fields.ts @@ -2,11 +2,19 @@ import * as ts from "../ts7/adapter.js"; import { isClassOwnEnumerableFieldName } from "../../ir/ir.js"; import type { ClassInfo } from "./lower-classes.js"; -/** Reflection that can remove a property or change its attributes. */ -const PROPERTY_REFLECTION = new Set(["deleteProperty", "freeze", "seal", "preventExtensions"]); +/* Object.freeze, Object.seal and Object.preventExtensions do not appear + * here: they never create or remove a property, and a class instance keeps + * its integrity state outside the field layout. A statically typed call + * sets the instance's CLASS_ATTRIBUTES word (class-attributes.ts), which + * TypeScript classes use with the same fixed layouts; through an untyped + * value the runtime refuses the operation on any class instance + * (scr_dyn_class_integrity_fence). Lifting that fence must revisit this. */ -/** `Object.freeze({ ... })` and the like apply to a new literal, which is - * not a class instance. */ +/** Reflection that can remove any property. */ +const PROPERTY_REFLECTION = new Set(["deleteProperty"]); + +/** `Reflect.deleteProperty({ ... }, key)` applies to a new literal, which + * is not a class instance. */ function freshLiteralArgument(callee: ts.PropertyAccessExpression): boolean { const call = callee.parent; if (!ts.isCallExpression(call) || call.expression !== callee) return false; @@ -141,12 +149,35 @@ export function constructionPrefix( return { created, sealed }; } -/** Whether a class declares a getter or setter, which can intercept a - * base constructor's field assignment. */ -function declaresAccessor(info: ClassInfo): boolean { - return [...info.methods.keys()].some( - (name) => name.startsWith("get:") || name.startsWith("set:"), - ); +/** The names of the getters and setters among a class's own methods. */ +export function accessorNames(methods: Iterable): Set { + const names = new Set(); + for (const name of methods) + if (name.startsWith("get:") || name.startsWith("set:")) names.add(name.slice(4)); + return names; +} + +/** Whether one of `accessors` can intercept an assignment the base + * constructions make: a base constructor's `this.name = value` runs a + * subclass accessor of that name instead of creating the field. Only field + * names can reach the created prefix; any other assignment already ends + * it. */ +export function accessorInterceptsBase(accessors: ReadonlySet, base: ClassInfo): boolean { + for (const name of accessors) if (base.fields.has(name)) return true; + return false; +} + +/** Whether an accessor that instances of `info` inherit or declare shares + * a name with one of their fields. Such a field and accessor disagree on + * what the instance's property is; any other accessor lives on the + * prototype beside the fields and never touches the layout. */ +function accessorShadowsField(info: ClassInfo): boolean { + for (let current: ClassInfo | null = info; current; current = current.base) { + if (current.def.runtime) break; + for (const name of accessorNames(current.methods.keys())) + if (info.fields.has(name)) return true; + } + return false; } /** `constructionPrefix` for a collected class and its bases. */ @@ -156,7 +187,8 @@ export function construction(info: ClassInfo): Construction | null { if (info.base) { base = construction(info.base); if (!base) return null; - if (declaresAccessor(info)) base = { created: base.created, sealed: false }; + if (accessorInterceptsBase(accessorNames(info.methods.keys()), info.base)) + base = { created: base.created, sealed: false }; } return constructionPrefix(info.decl, base, (name) => info.fields.has(name)); } @@ -169,7 +201,7 @@ export function construction(info: ClassInfo): Construction | null { export function fixedFieldFamily(root: ClassInfo, deleted: Set | null): boolean { if (deleted === null) return false; const fixed = (info: ClassInfo): boolean => { - if (declaresAccessor(info)) return false; + if (accessorShadowsField(info)) return false; const built = construction(info); if (!built) return false; for (const field of info.fieldOrder) { 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 8632bad47..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", () => { @@ -337,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( [ @@ -354,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( [ @@ -366,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"), { @@ -376,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( [ @@ -399,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 4adfb87ab..aa84c68c2 100644 --- a/packages/compiler/src/frontend/lowering/key-order-witness.ts +++ b/packages/compiler/src/frontend/lowering/key-order-witness.ts @@ -41,20 +41,66 @@ * 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 = /** An explicit key (`definite`), or a conditional spread's key, which * exists only when its condition holds. */ | { kind: "key"; name: string; definite: boolean } /** `...source` of a record shape; `maybeAbsent` when the whole source - * can be undefined (`...options` of `T | undefined`). */ - | { kind: "spread"; shapeId: string; maybeAbsent: boolean } + * 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" }; @@ -227,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); @@ -329,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": @@ -392,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. @@ -713,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-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index bb006f530..672801e9e 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"; @@ -73,6 +74,7 @@ import { isDynTypedRefType, isUnitType, shapeHasAccessorSlots, + shapeKeyOrders, typeEquals, RUNTIME_EMITTER_CLASS, } from "../../ir/ir.js"; @@ -267,8 +269,62 @@ 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)) + ); +} + +/** The `f` of a `(0, f)` callee (a numeric left operand, so dropping it + * changes only the call's receiver), or null. */ +function receiverlessCalleeOf(callee: ts.Expression): ts.Expression | null { + let e = callee; + while (ts.isParenthesizedExpression(e)) e = e.expression; + if ( + !ts.isBinaryExpression(e) || + e.operatorToken.kind !== ts.SyntaxKind.CommaToken || + !ts.isNumericLiteral(e.left) + ) + return null; + let right = e.right; + while (ts.isParenthesizedExpression(right)) right = right.expression; + return right; +} + 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 ( @@ -289,6 +345,17 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const json = lowerJsonRequireCall(lowerer, expr); if (json) return json; + // `(0, ns.member)(...)` over a builtin module (tsc's and esbuild's call + // without a receiver): Node's module functions do not read `this`, so it + // is the member call. + { + const member = receiverlessCalleeOf(expr.expression); + if (member !== null && ts.isPropertyAccessExpression(member)) { + const builtin = lowerer.lowerNamespaceBuiltinCall(expr, member); + if (builtin) return builtin; + } + } + // eval(...) and Function(...): Node's EvalError under // --disallow-code-generation-from-strings (code-generation.ts). { @@ -2400,6 +2467,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", @@ -2440,6 +2521,7 @@ function lowerBigIntConstructorValue(lowerer: Lowerer, arg: IrExpr, loc: SrcLoc) arm.kind === "string" || arm.kind === "f64" || arm.kind === "bool" || + arm.kind === "bytes" || isUnitType(arm), ) ) @@ -3797,16 +3879,19 @@ 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)`), or an untyped receiver (an object whose - // prototype defines `trim`), 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) && @@ -7141,8 +7226,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", @@ -7159,12 +7248,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; }; @@ -8867,8 +8957,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", @@ -9030,8 +9124,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; }; @@ -10192,6 +10287,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, @@ -10200,6 +10312,7 @@ export function lowerObjectMethodCall( fallback, loc, ts.getTextOfNode(access), + original, ); return { kind: "seqExpr", @@ -11176,6 +11289,7 @@ const SHIM_INTRINSICS: ReadonlyMap; 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; @@ -2734,8 +2743,8 @@ export function collectClassShapeInner( // state, so an override or shadowing field would split-brain. // Underscore methods are the SUPPORTED override form — but only // the ones the class's own base consumes (a `_read` on a - // Transform, or `_writev`/`_construct` anywhere, would be consumed - // by Node machinery that has no lowering here). + // Transform, or `_writev` on a Readable, would be consumed by Node + // machinery that has no lowering here). if (memberName && ts.isIdentifier(memberName)) { const streamBase = (() => { for (let c = base; c; c = c.base) if (c.builtinStream) return c; @@ -2754,7 +2763,7 @@ export function collectClassShapeInner( lowerer.unsupported( "SC1090", memberName, - `declaring '_writev' on a ${streamBase.def.name.slice(1)} subclass (batched writes are supported on Writable and Duplex subclasses)`, + `declaring '_writev' on a ${streamBase.def.name.slice(1)} subclass (a Readable has no writable side)`, ); } const accepted = streamCtorShape(streamBase.def.name).accepted; @@ -4110,7 +4119,7 @@ export function collectClassShapeInner( // A subclass field created before anything can observe the instance // keeps its type: super() cannot run code that reads it earlier. const baseConstruction = - base && ![...methods.keys()].some((m) => m.startsWith("get:") || m.startsWith("set:")) + base && !accessorInterceptsBase(accessorNames(methods.keys()), base) ? construction(base) : null; const sealedPrefix = baseConstruction diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index 7c2ec4a2d..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))), ); diff --git a/packages/compiler/src/frontend/lowering/lower-server.ts b/packages/compiler/src/frontend/lowering/lower-server.ts index d9746e1f6..cb79accea 100644 --- a/packages/compiler/src/frontend/lowering/lower-server.ts +++ b/packages/compiler/src/frontend/lowering/lower-server.ts @@ -7581,7 +7581,7 @@ function lowerHttpReqMethodCall( loc, }; } - if (name === "resume" || name === "destroy") { + if (name === "resume" || name === "pause" || name === "destroy") { requireStatementPosition(lowerer, call, `req.${name}()`); if (args.length !== 0) { lowerer.noLowering( @@ -7591,7 +7591,8 @@ function lowerHttpReqMethodCall( ); } const receiver = coerceToHandle(lowerer, access.expression, HTTPREQ_T); - const fn: IrLibFn = name === "resume" ? "http.reqResume" : "http.reqDestroy"; + const fn: IrLibFn = + name === "resume" ? "http.reqResume" : name === "pause" ? "http.reqPause" : "http.reqDestroy"; return { kind: "libCall", fn, args: [receiver], type: VOID, loc }; } if (name === "setEncoding") { diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index c37ae9074..04c2fa043 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -106,6 +106,61 @@ function pureRead(node: ts.Expression): boolean { return false; } +/** A literal, `undefined`, or a function expression: a value that reads + * no binding or property. */ +function constantOptionValue(node: ts.Expression): boolean { + let e = node; + while (ts.isParenthesizedExpression(e) || ts.isAsExpression(e) || ts.isTypeAssertion(e)) + e = e.expression; + if (ts.isIdentifier(e)) return e.text === "undefined"; + if ( + ts.isPrefixUnaryExpression(e) && + (e.operator === ts.SyntaxKind.MinusToken || + e.operator === ts.SyntaxKind.PlusToken || + e.operator === ts.SyntaxKind.ExclamationToken) + ) + return constantOptionValue(e.operand); + return ( + ts.isStringLiteralLike(e) || + ts.isNumericLiteral(e) || + ts.isBigIntLiteral(e) || + e.kind === ts.SyntaxKind.TrueKeyword || + e.kind === ts.SyntaxKind.FalseKeyword || + e.kind === ts.SyntaxKind.NullKeyword || + ts.isArrowFunction(e) || + ts.isFunctionExpression(e) + ); +} + +/** Whether a runtime decodeStrings or defaultEncoding value (`prop`'s + * `value`) may be read before the other option members. Those values are + * read into locals before the constructor evaluates the other members, + * while Node evaluates the members in order, so every earlier member must + * evaluate without effects, and when the value itself has effects, every + * earlier member must also read nothing those effects could change. */ +function earlierOptionsPure( + options: ts.ObjectLiteralExpression, + prop: ts.Node, + value: ts.Expression, +): boolean { + const valueHasEffects = !sideEffectFreeOptionValue(value) && !pureRead(value); + for (const member of options.properties.nodes) { + if (member === prop) return true; + if (ts.isPropertyAssignment(member)) { + const init = member.initializer; + if ( + valueHasEffects + ? !constantOptionValue(init) + : !sideEffectFreeOptionValue(init) && !pureRead(init) + ) + return false; + } else if (ts.isShorthandPropertyAssignment(member)) { + if (valueHasEffects) return false; + } else if (ts.isSpreadAssignment(member)) return false; + } + return true; +} + /** The stream sides of a receiver class: the nearest stream-class * ancestor's, or null off the stream hierarchy. */ export function streamSidesOf( @@ -179,15 +234,15 @@ export const UNDERSCORE_METHODS: ReadonlyMap = new Map([ /** 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). */ + * on every stream class, and writev on every class with a writable side + * (a Transform's batches reach writev, not the transform, as in Node). */ 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"]; + return cls === "%Readable" ? ["construct"] : ["construct", "writev"]; } /** The completion callback's parameter position in each underscore @@ -442,6 +497,13 @@ interface StreamOptions { * right after construction (Node's constructors call addAbortSignal * once the state exists), or null when absent. */ signal: IrExpr | null; + /** Non-literal decodeStrings and defaultEncoding values: valueDecls + * evaluates them into locals before construction (the defaultEncoding + * validated there, as the state constructors throw before the stream + * exists), and stream.optionValues applies them to the fresh stream. */ + valueDecls: IrStmt[]; + decodeStringsValue: IrExpr | null; + defaultEncodingValue: IrExpr | null; /** The objectMode option's sides (a Duplex's readableObjectMode and * writableObjectMode set one side): applied with stream.objectMode * right after construction. */ @@ -732,6 +794,12 @@ function pinAnyDataCompletion( if (!symbol || lowerer.stageSourceTypes.has(symbol)) return null; const declared = lowerer.paramShape(param).type; if (declared.kind !== "func" || declared.rest || declared.params[1]?.kind !== "dyn") return null; + // Only when every use calls the callback with a Buffer, a string or + // nothing as `data`: another value (or a callback that escapes) keeps + // `data` checked-dynamic, so the runtime pushes it as push(data) does + // and reports ERR_INVALID_ARG_TYPE through 'error', as Node does. + if (fnNode.body === undefined || !completionDataIsChunk(lowerer, fnNode.body, symbol, param.name)) + return null; const data = unionOf(lowerer, [BYTES, STRING, { kind: "undefinedT" }]); lowerer.stageSourceTypes.set( symbol, @@ -740,6 +808,59 @@ function pinAnyDataCompletion( return symbol; } +/** Whether every reference to the completion callback `symbol` in `body` + * calls it with a `data` argument that is a Buffer, a string, null or + * undefined (or with no data). */ +function completionDataIsChunk( + lowerer: Lowerer, + body: ts.Node, + symbol: ts.Symbol, + declaration: ts.Node, +): boolean { + let ok = true; + const visit = (node: ts.Node): void => { + if (!ok) return; + if ( + ts.isIdentifier(node) && + node !== declaration && + lowerer.checker.getSymbolAtLocation(node) === symbol + ) { + const call = node.parent; + if (!call || !ts.isCallExpression(call) || call.expression !== node) { + ok = false; + return; + } + const data = call.arguments.nodes[1]; + if (data !== undefined) { + let t: IrType | null; + try { + t = lowerer.mapTypeOf(lowerer.typeOf(data)); + } catch { + t = null; + } + const arms = + t?.kind === "union" ? (lowerer.unions.get(t.unionId)?.arms ?? null) : t ? [t] : null; + if ( + arms === null || + !arms.every( + (a) => + (a.kind === "bytes" && a.elem === "u8") || + a.kind === "string" || + a.kind === "undefinedT" || + a.kind === "nullT", + ) + ) { + ok = false; + return; + } + } + } + ts.forEachChild(node, visit); + }; + visit(body); + return ok; +} + /** Parses the options object literal of a stream constructor. `which` * names the callbacks this class accepts, in canonical flag order. */ function parseStreamOptions( @@ -767,6 +888,9 @@ function parseStreamOptions( badEncoding: null, pushEncoding: null, signal: null, + valueDecls: [], + decodeStringsValue: null, + defaultEncodingValue: null, readableObject: false, writableObject: false, decodeStringsOff: false, @@ -802,6 +926,21 @@ function parseStreamOptions( optsLit !== undefined && ts.isObjectLiteralExpression(optsLit) && literalWritesValues(optsLit, ctorName !== "Readable" && ctorName !== "Writable"); + // In JavaScript a decodeStrings value known only at run time may let + // string writes through, so the write and transform callbacks' chunk + // parameters read as any (the declarations type it Buffer). + const runtimeDecodeStrings = + optsLit !== undefined && + isJsSourceFile(ts.getSourceFileOfNode(optsLit)) && + ts.isObjectLiteralExpression(optsLit) && + (optsLit.properties?.nodes ?? []).some( + (p) => + ts.isPropertyAssignment(p) && + (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) && + p.name.text === "decodeStrings" && + p.initializer.kind !== ts.SyntaxKind.TrueKeyword && + p.initializer.kind !== ts.SyntaxKind.FalseKeyword, + ); if (out.readableObject) out.hwmR = numLit(16, loc); if (out.writableObject) out.hwmW = numLit(16, loc); const tuples = cbTuples(lowerer); @@ -851,6 +990,7 @@ function parseStreamOptions( } const full = tuples[name]!.map((t) => (t === "fn" ? null : t)); const pinned = out.readableObject ? null : pinAnyDataCompletion(lowerer, name, fnNode); + let anyChunk: ts.Symbol | undefined; try { // The completion-callback tail: typed by the checker contextually; // accept the user's declared func type as the tuple's entry so the @@ -867,12 +1007,22 @@ function parseStreamOptions( } return declared ?? funcOf([], VOID); }); + const chunkParam = + runtimeDecodeStrings && (name === "write" || name === "transform") + ? fnNode.parameters.nodes[0] + : undefined; + anyChunk = + chunkParam !== undefined && ts.isIdentifier(chunkParam.name) && !chunkParam.type + ? lowerer.checker.getSymbolAtLocation(chunkParam.name) + : undefined; + if (anyChunk) lowerer.anySymbols.add(anyChunk); present.set( name, lowerStreamCallback(lowerer, name, fnNode, thisType, tuple, writesValues), ); } finally { if (pinned) lowerer.stageSourceTypes.delete(pinned); + if (anyChunk) lowerer.anySymbols.delete(anyChunk); } continue; } @@ -925,11 +1075,25 @@ function parseStreamOptions( if (writableClass) out.decodeStringsOff = true; break; } - lowerer.noLowering( - `${ctorName} with a runtime 'decodeStrings' value`, - prop, - "write 'decodeStrings: true' or 'decodeStrings: false'", - ); + // a runtime value: WritableState's `options.decodeStrings !== false` + if ( + optsLit !== undefined && + ts.isObjectLiteralExpression(optsLit) && + !earlierOptionsPure(optsLit, prop, value) + ) + lowerer.noLowering( + `the ${ctorName} option 'decodeStrings' with a runtime value that an earlier option value could observe or change`, + prop, + "compute the option values into consts before the options object (the runtime value is read before the other options run)", + ); + const local = lowerer.declareHiddenLocal("%decodeStrings", DYN); + out.valueDecls.push({ + kind: "varDecl", + localId: local.id, + init: lowerer.lowerExprExpecting(value, DYN), + loc, + }); + if (writableClass) out.decodeStringsValue = varRef(local.id, DYN, loc); break; } case "encoding": { @@ -973,11 +1137,43 @@ function parseStreamOptions( break; } } - lowerer.noLowering( - `the ${ctorName} option 'defaultEncoding' with a runtime value`, - prop, - "write the encoding as a string literal", - ); + { + // a runtime value: null, undefined and utf8 are the default, + // another name Buffer.isEncoding knows is kept as given, and any + // other value throws ERR_UNKNOWN_ENCODING before construction + if ( + optsLit !== undefined && + ts.isObjectLiteralExpression(optsLit) && + !earlierOptionsPure(optsLit, prop, value) + ) + lowerer.noLowering( + `the ${ctorName} option 'defaultEncoding' with a runtime value that an earlier option value could observe or change`, + prop, + "compute the option values into consts before the options object (the runtime value is read before the other options run)", + ); + const local = lowerer.declareHiddenLocal("%defaultEncoding", DYN); + const ref = varRef(local.id, DYN, loc); + out.valueDecls.push( + { + kind: "varDecl", + localId: local.id, + init: lowerer.lowerExprExpecting(value, DYN), + loc, + }, + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "stream.checkEncodingOption", + args: [ref], + type: VOID, + loc, + }, + loc, + }, + ); + out.defaultEncodingValue = ref; + } break; case "signal": { // `if (options.signal) addAbortSignal(options.signal, this)`: @@ -1041,9 +1237,7 @@ function parseStreamOptions( lowerer.noLowering( `the ${ctorName} option 'writev'`, prop, - ctorName === "Readable" - ? "a Readable has no writable side" - : "batched writes on a Transform would bypass its transform — writev is supported on Writable and Duplex", + "a Readable has no writable side", ); break; default: @@ -1083,6 +1277,9 @@ function streamCtorArgs( badEncoding: string | null; pushEncoding: string | null; signal: IrExpr | null; + valueDecls: IrStmt[]; + decodeStringsValue: IrExpr | null; + defaultEncodingValue: IrExpr | null; readableObject: boolean; writableObject: boolean; decodeStringsOff: boolean; @@ -1117,6 +1314,9 @@ function streamCtorArgs( badEncoding: o.badEncoding, pushEncoding: o.pushEncoding, signal: o.signal, + valueDecls: o.valueDecls, + decodeStringsValue: o.decodeStringsValue, + defaultEncodingValue: o.defaultEncodingValue, readableObject: o.readableObject, writableObject: o.writableObject, decodeStringsOff: o.decodeStringsOff, @@ -1237,6 +1437,18 @@ export function lowerStreamNew(lowerer: Lowerer, expr: ts.NewExpression, info: C loc, }; } + const values = optionValuesArgs(parsed, loc); + if (values) { + // runtime decodeStrings and defaultEncoding values (answers the + // receiver +1) + built = { + kind: "libCall", + fn: "stream.optionValues", + args: [built, ...values], + type: thisType, + loc, + }; + } if (parsed.pushEncoding !== null) { // { defaultEncoding }: chain the push-side decode default (the // setEncoding chaining shape — answers the receiver +1). @@ -1269,8 +1481,11 @@ export function lowerStreamNew(lowerer: Lowerer, expr: ts.NewExpression, info: C loc, }; } - if (!parsed.decodeStringsOff && parsed.writeEncoding === null && parsed.extras.length === 0) - return built; + if (!parsed.decodeStringsOff && parsed.writeEncoding === null && parsed.extras.length === 0) { + return parsed.valueDecls.length > 0 + ? { kind: "seqExpr", stmts: parsed.valueDecls, result: built, type: thisType, loc } + : built; + } const fresh = lowerer.declareHiddenLocal("%stream", thisType); const followUps = streamOptionFollowUps( parsed, @@ -1280,13 +1495,30 @@ export function lowerStreamNew(lowerer: Lowerer, expr: ts.NewExpression, info: C ); return { kind: "seqExpr", - stmts: [{ kind: "varDecl", localId: fresh.id, init: built, loc }, ...followUps], + stmts: [ + ...parsed.valueDecls, + { kind: "varDecl", localId: fresh.id, init: built, loc }, + ...followUps, + ], result: varRef(fresh.id, thisType, loc), type: thisType, loc, }; } +/** stream.optionValues' (decodeStrings, defaultEncoding) arguments, or + * null when both options are literals or absent. */ +function optionValuesArgs( + parsed: { decodeStringsValue: IrExpr | null; defaultEncodingValue: IrExpr | null }, + loc: SrcLoc, +): IrExpr[] | null { + if (parsed.decodeStringsValue === null && parsed.defaultEncodingValue === null) return null; + return [ + parsed.decodeStringsValue ?? dynUndefinedExpr(loc), + parsed.defaultEncodingValue ?? dynUndefinedExpr(loc), + ]; +} + /** True when a value of Node's argument type `passed` widens implicitly * into the declared parameter type `declared` (one of its union arms, or a * sub-union of it). */ @@ -1589,6 +1821,7 @@ export function lowerStreamSuperCall( ]; } const out: IrStmt[] = [ + ...parsed.valueDecls, { kind: "exprStmt", expr: { @@ -1614,6 +1847,20 @@ export function lowerStreamSuperCall( loc, }); } + const values = optionValuesArgs(parsed, loc); + if (values) { + out.push({ + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "stream.optionValues", + args: [thisRef(), ...values], + type: thisType, + loc, + }, + loc, + }); + } if (parsed.pushEncoding !== null) { out.push({ kind: "exprStmt", @@ -1697,16 +1944,15 @@ export function lowerStreamUnderscoreAssign( const loc = locOf(expr); const cls = base.def.name; const clsName = cls.slice(1); - 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, - "declare _construct on the class or pass the construct option", - ); - } - const which = methodName === "_writev" ? "writev" : optionByMethod.get(methodName)!; + // `_construct`: Node's constructNT reads it on the construction tick, so + // an assignment before that tick replaces a scheduled hook + // (stream.replaceConstruct) and has no effect after it or without one. + const which = + methodName === "_writev" + ? "writev" + : methodName === "_construct" + ? "construct" + : optionByMethod.get(methodName)!; const { accepted } = streamCtorShape(cls); if (!accepted.includes(which) && !streamExtraCallbacks(cls).includes(which)) { lowerer.noLowering( @@ -1745,6 +1991,7 @@ export function lowerStreamUnderscoreAssign( transform: "stream.setTransform", flush: "stream.setFlush", writev: "stream.setWritev", + construct: "stream.replaceConstruct", }; return { kind: "exprStmt", @@ -2699,6 +2946,22 @@ class StreamArgTypeThrow extends Error { } } +/** Whether a checked-dynamic pipeline callback can take undefined as its + * error: a callback that is already checked-dynamic, one without + * parameters, and one whose first parameter (or rest element) type holds + * undefined. */ +function callbackErrorAdmitsUndefined(lowerer: Lowerer, cb: IrExpr): boolean { + if (cb.kind !== "dynFrom" || cb.value.type.kind !== "func") return true; + const fnT = cb.value.type; + const first = fnT.params[0]; + if (first === undefined) return true; + const p0 = + fnT.rest === true && fnT.params.length === 1 && first.kind === "array" ? first.elem : first; + if (p0.kind === "dyn" || p0.kind === "jsval" || p0.kind === "undefinedT") return true; + if (p0.kind !== "union") return false; + return lowerer.unions.get(p0.unionId)?.arms.some((a) => a.kind === "undefinedT") ?? false; +} + /** The finished/pipeline completion callback: an inline function lowers * through the option-callback machinery (leading `this`, an `Error | * null` prefix — Node binds the stream and passes the error); any other @@ -3160,12 +3423,16 @@ export function lowerStreamModuleCall( // 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 + // SCR_PIPE_TYPED_CB: a typed callback whose error parameter cannot + // hold undefined takes null for no error; others take undefined. 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))) + if ( + !isJsSourceFile(ts.getSourceFileOfNode(call)) && + !callbackErrorAdmitsUndefined(lowerer, cb) + ) stagedArgs[3] = numLit(flagsArg.value | 128, loc); return staged.wrap({ kind: "libCall", diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 2b76080fd..d483a56ae 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -1,3 +1,4 @@ +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"; @@ -498,7 +499,7 @@ import { lowerBuiltinExtraProperty } from "./builtins/properties.js"; import { registerPromisifiedBuiltinDecl } from "./builtins/callable-aliases.js"; import { lowerExecFileAsyncCall } from "./builtins/child-process-async.js"; import { lowerStringDecoderMethodCall, lowerTextCodecCall } from "./builtins/text-codecs.js"; -import { lowerReadlineMethodCall } from "./builtins/readline.js"; +import { isOptionalReadlineCursorRead, lowerReadlineMethodCall } from "./builtins/readline.js"; import { lowerDiagnosticsChannelMethodCall, lowerDiagnosticsChannelProperty, @@ -2424,6 +2425,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 @@ -3218,6 +3264,11 @@ 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 @@ -3600,6 +3651,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, @@ -3722,8 +3788,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 } : {}), @@ -5512,6 +5581,7 @@ export class Lowerer { if (isIndexedRead(e)) return true; if (ts.isPropertyAccessExpression(e) && isRecordIndexPropertyRead(e)) return true; if (ts.isPropertyAccessExpression(e) && isOptionalProcessStreamProperty(this, e)) return true; + if (ts.isPropertyAccessExpression(e) && isOptionalReadlineCursorRead(this, e)) return true; if (isOptionalProcessVersionsRead(this, e)) return true; if (ts.isPropertyAccessExpression(e) && isSettledIteratorValueRead(this, e)) return true; if (ts.isPropertyAccessExpression(e) && isOsConstantsSymbol(symbolOf(e.expression), this)) @@ -7758,6 +7828,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() } : {}), @@ -10633,6 +10704,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 { @@ -10700,6 +10811,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)), @@ -10729,6 +10855,7 @@ export class Lowerer { builtinOption("URLFormatOptions") || !this.mapTypeOf(this.typeOf(x)) || this.excessJsContextualProperty(x, parts) || + this.reshapeableContextualLiteral(x, parts) || holdsEvolvingArray(this, x) ) { return lowerDynObjectLiteral(this, x); diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 8b826304b..44564f2f1 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -1593,6 +1593,9 @@ export const BUILTIN_MODULE_FNS: Record< // Prime checks and generation (lowerPrimeCall): checked-dynamic // arguments, results converted to the call site's type. checkPrimeSync: { fn: "crypto.checkPrimeSync", params: [], result: DYN }, + // Checked-dynamic arguments, the result converted to the call site's + // type (lowerPrimeCall). + getCipherInfo: { fn: "crypto.getCipherInfo", params: [], result: DYN }, checkPrime: { fn: "crypto.checkPrimeCb", params: [], result: VOID }, generatePrimeSync: { fn: "crypto.generatePrimeSync", params: [], result: DYN }, generatePrime: { fn: "crypto.generatePrimeCb", params: [], result: VOID }, @@ -3101,14 +3104,6 @@ export const BUILTIN_MODULE_FENCE_HINTS: Record< diffieHellman: "crypto.diffieHellman takes DH, EC or X25519 KeyObjects and has no lowering; " + "the objects of createDiffieHellman, getDiffieHellman and createECDH compute shared secrets", - ...Object.fromEntries( - ["getCipherInfo"].map((m) => [ - m, - "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", - ]), - ), setFips: "a compiled binary has no FIPS provider to enable, and Node itself throws on setFips(true) " + "in a non-FIPS build — getFips() answers 0 here", diff --git a/packages/compiler/src/frontend/lowering/var-hoisting.ts b/packages/compiler/src/frontend/lowering/var-hoisting.ts index 3f950f3ca..0b415bb3d 100644 --- a/packages/compiler/src/frontend/lowering/var-hoisting.ts +++ b/packages/compiler/src/frontend/lowering/var-hoisting.ts @@ -6,9 +6,11 @@ * * A binding settles at the first statement of its hoisting scope (the * function body or the source file) that stores it unconditionally: a - * direct `var x = e`, `x = e;`, or a loop declaring it (`for (var i = 0; - * ...)`, `for (var k in o)`), whose own condition, update and body run - * after the store. A read settles when it runs after that statement: + * direct `var x = e`, `x = e;`, or a `for` loop declaring it (`for (var + * i = 0; ...)`), whose own condition, update and body run after the store. + * A `for (var k in o)` or `for (var v of xs)` loop stores the binding only + * when it iterates, so it settles the reads in its body and no others. A + * read settles when it runs after the settling statement: * - a read in the scope's own code, in a TypeScript file: tsc's * definite-assignment analysis rejects any earlier one; * - a read in the scope's own code, in a JavaScript file: its statement @@ -194,6 +196,18 @@ function referencesBySymbol( return out; } +/** Where a binding is first stored unconditionally: the scope statement's + * index, and for a declarator of a `var` statement or a `for` initializer, + * the list and the declarator's end. */ +interface Settle { + index: number; + list: ts.VariableDeclarationList | null; + end: number; + /** The first for-in/of loop declaring the binding before `index`, whose + * body runs after its store, or -1. */ + loop: number; +} + class ScopeAnalysis { private readonly js: boolean; /** Whether a function declaration's references all settle, by scope @@ -214,10 +228,13 @@ class ScopeAnalysis { } /** The index of the scope statement that first stores `symbol` - * unconditionally, or Infinity. */ - settleIndex(scope: Scope, symbol: ts.Symbol): number { + * unconditionally (Infinity when none does), and, when that statement + * declares it in a `var` statement or a `for` initializer, the end of the + * storing declarator: later declarators of the list run after it. */ + settleIndex(scope: Scope, symbol: ts.Symbol): Settle { const statements = statementsOf(scope); const checker = this.lowerer.checker; + let loopIndex = -1; for (let i = 0; i < statements.length; i++) { const s = statements[i]!; const list = ts.isVariableStatement(s) @@ -233,8 +250,10 @@ class ScopeAnalysis { if ( (d.initializer !== undefined || loop) && boundIdentifiersOf(d.name).some((n) => checker.getSymbolAtLocation(n) === symbol) - ) - return i; + ) { + if (!loop) return { index: i, list, end: d.end, loop: loopIndex }; + if (loopIndex < 0) loopIndex = i; + } } } else if ( ts.isExpressionStatement(s) && @@ -243,20 +262,21 @@ class ScopeAnalysis { ts.isIdentifier(s.expression.left) && checker.getSymbolAtLocation(s.expression.left) === symbol ) { - return i; + return { index: i, list: null, end: -1, loop: loopIndex }; } } - return Infinity; + return { index: Infinity, list: null, end: -1, loop: loopIndex }; } /** Whether `ref` (inside `scope`, or in another module) runs only after - * the statement at index `settle`. `read`: `ref` reads the binding - * itself, so tsc vouches for one in the scope's own code. */ - settles(scope: Scope, settle: number, ref: ts.Identifier, read = true): boolean { + * the store `settle` describes. `read`: `ref` reads the binding itself, + * so tsc vouches for one in the scope's own code. */ + settles(scope: Scope, settle: Settle, ref: ts.Identifier, read = true): boolean { if (ts.getSourceFileOfNode(ref) !== this.sf) return true; const statements = statementsOf(scope); let deferred: ts.Node | null = null; let top: ts.Node | null = null; + const container: ts.Node | undefined = ts.isSourceFile(scope) ? scope : scope.body; for (let n: ts.Node = ref; n.parent !== undefined && n !== scope; n = n.parent) { if ( ts.isFunctionLike(n) || @@ -265,18 +285,26 @@ class ScopeAnalysis { ts.isClassStaticBlockDeclaration(n) ) deferred = n; - if (n.parent === scope || (!ts.isSourceFile(scope) && n.parent === scope.body)) top = n; + // The scope's own statement, not the function body that holds it. + if (n.parent === container) top = n; } if (top === null) return false; if (deferred === null && read && !this.js) return true; if (deferred !== null && ts.isFunctionDeclaration(deferred) && deferred.name !== undefined) return this.functionSettles(scope, settle, deferred); const index = statements.indexOf(top as ts.Statement); - return index > settle || (index === settle && settlesWithin(top, ref)); + return ( + index > settle.index || + (index === settle.index && + (settlesWithin(top, ref) || + // A later declarator of the storing list (`var a = {}, b = { ...a }`). + (settle.list !== null && isInside(ref, settle.list) && ref.pos >= settle.end))) || + (index === settle.loop && settlesWithin(top, ref)) + ); } - private functionSettles(scope: Scope, settle: number, fn: ts.FunctionDeclaration): boolean { - const key = `${fn.pos}:${settle}`; + private functionSettles(scope: Scope, settle: Settle, fn: ts.FunctionDeclaration): boolean { + const key = `${fn.pos}:${settle.index}:${settle.end}:${settle.loop}`; const known = this.functionMemo.get(key); if (known !== undefined) return known; this.functionMemo.set(key, true); diff --git a/packages/compiler/src/frontend/npm-static-bundled-chunks.test.ts b/packages/compiler/src/frontend/npm-static-bundled-chunks.test.ts index 0779db3ce..dc238c622 100644 --- a/packages/compiler/src/frontend/npm-static-bundled-chunks.test.ts +++ b/packages/compiler/src/frontend/npm-static-bundled-chunks.test.ts @@ -276,13 +276,14 @@ export default require_f(); ).toBeNull(); }); -test("an identifier-minified bundle names its lost helper names", () => { - // esbuild --minify output: the helper shapes survive, their names do not. - const result = split( +test("an identifier-minified bundle splits through its helper's shape", () => { + // esbuild --minify output: the helper shape survives, its name does not. + const answer = expectSameBehavior( `var c=(r,e)=>()=>(e||r((e={exports:{}}).exports,e),e.exports);` + - `var x=c((t,m)=>{m.exports={v:1}});console.log(x().v);`, + `var x=c((t,m)=>{m.exports={v:1}});var y=c(t=>{t.w=x().v+1});` + + `console.log(x().v,y().w,x()===x());`, ); - expect(result).toEqual({ degrade: expect.stringContaining("identifier-minified") as string }); + expect(answer.chunks.map((c) => basename(c.path))).toEqual(["index.js.x.cjs", "index.js.y.cjs"]); }); test("an ES-module bundle of CommonJS chunks names the chunk interop", () => { diff --git a/packages/compiler/src/frontend/npm-static-bundled-chunks.ts b/packages/compiler/src/frontend/npm-static-bundled-chunks.ts index 37a74f2be..66c25403b 100644 --- a/packages/compiler/src/frontend/npm-static-bundled-chunks.ts +++ b/packages/compiler/src/frontend/npm-static-bundled-chunks.ts @@ -525,7 +525,7 @@ export function splitBundledChunks( } } } - const helperNames = [...top.keys()].filter((n) => helperBase(n) === "__commonJS"); + let helperNames = [...top.keys()].filter((n) => helperBase(n) === "__commonJS"); // Every name a recognizer relies on is written nowhere and declared // once at the top level; `Object` itself is never rebound. @@ -552,18 +552,14 @@ export function splitBundledChunks( ); }; if (helperNames.length === 0) { - // The helper's shape under another name: an identifier-minified bundle, - // whose other interop helpers lost their names as well. - for (const [name, t] of top) { + // An identifier-minified bundle: the helper's shape under another name. + helperNames = [...top].flatMap(([name, t]) => { const init = ts.isVariableDeclaration(t.node) ? t.node.initializer : undefined; - if (init !== undefined && stable(name) && helperForm(init, sf, ownNames) !== null) - return { - degrade: - "its shipped JS is an identifier-minified bundle whose bundler-interop helpers " + - "(__commonJS and the rest) have lost the names the recognizers verify", - }; - } - return null; + return init !== undefined && stable(name) && helperForm(init, sf, ownNames) !== null + ? [name] + : []; + }); + if (helperNames.length === 0) return null; } const forms = new Map(); for (const name of helperNames) { diff --git a/packages/compiler/src/frontend/npm-static-rewrite-syntax.ts b/packages/compiler/src/frontend/npm-static-rewrite-syntax.ts index c48420625..917041603 100644 --- a/packages/compiler/src/frontend/npm-static-rewrite-syntax.ts +++ b/packages/compiler/src/frontend/npm-static-rewrite-syntax.ts @@ -260,11 +260,21 @@ function definePropertyExportOf( value = getterReturnOf(p); } } - const esModuleStamp = - name === "__esModule" && value !== null && value.kind === ts.SyntaxKind.TrueKeyword; + const esModuleStamp = name === "__esModule" && value !== null && isTrueLiteral(value); return { name, value, esModuleStamp }; } +/** `true`, or `!0`, its minified spelling. */ +function isTrueLiteral(expr: ts.Expression): boolean { + return ( + expr.kind === ts.SyntaxKind.TrueKeyword || + (ts.isPrefixUnaryExpression(expr) && + expr.operator === ts.SyntaxKind.ExclamationToken && + ts.isNumericLiteral(expr.operand) && + expr.operand.text === "0") + ); +} + /** Space-pads [start, end) of `text` preserving newlines — the recognized * statement disappears while every original offset survives. */ function padSpan(text: string, start: number, end: number): string { diff --git a/packages/compiler/src/frontend/npm-static.ts b/packages/compiler/src/frontend/npm-static.ts index 800d02eff..eed02c95c 100644 --- a/packages/compiler/src/frontend/npm-static.ts +++ b/packages/compiler/src/frontend/npm-static.ts @@ -614,27 +614,12 @@ export function clearProgramBuiltinAliasCache(): void { * but the bundler-emitted CJS export rewrite (npm-static-rewrite.ts) * types the getter-table shapes and the offender attribution degrades a * package whose surface still breaks its consumers — so those bundles are - * worth ATTEMPTING: eligible when the .d.ts and unminified criteria hold, - * gracefully per-package-degraded when the attempt fails. */ + * worth ATTEMPTING: eligible when the .d.ts criteria hold, gracefully + * per-package-degraded when the attempt fails. Minified JS is attempted + * too: a minified dist is ordinary JavaScript, and the bundle recognizers + * know esbuild's __commonJS helper by its shape under any name. */ const TRANSFORM_MARKERS = ["__webpack_require__"]; -/** Unminified-JS heuristic over an entry source: at least two lines and - * an average line length under 200 characters (minified dists are one - * enormous line; readable code averages well under 100). */ -function looksUnminified(source: string): boolean { - const lines = source.split("\n"); - if (lines.length < 2) return false; - return source.length / lines.length <= 200; -} - -/** A whitespace/syntax-minified bundler dist whose interop helpers keep - * their names (`var __toESM=…,__toCommonJS=mod=>…`): the recognizers - * verify those shapes and refuse the package precisely when they cannot, - * so the minification heuristic need not refuse it up front. */ -function namedBundlerHelpers(source: string): boolean { - return /\b__(?:toCommonJS|commonJS|toESM)\d*\s*=\s*(?:\(|[A-Za-z_$][\w$]*\s*=>)/.test(source); -} - function hasTransformMarkers(source: string): boolean { return TRANSFORM_MARKERS.some((m) => source.includes(m)); } @@ -676,8 +661,6 @@ export function npmStaticIneligibleReason( if (jsEntry === null) return "no runtime JS entry resolves"; const source = trackedReadFile(jsEntry); if (source === null) return `its runtime entry ${jsEntry} cannot be read`; - if (!looksUnminified(source) && !namedBundlerHelpers(source)) - return "its shipped JS looks minified"; if (hasTransformMarkers(source)) { return "its shipped JS carries build-transform markers (bundled/transpiled dist)"; } diff --git a/packages/compiler/src/frontend/package-shims.ts b/packages/compiler/src/frontend/package-shims.ts index a0c2e4cb9..10beeaae8 100644 --- a/packages/compiler/src/frontend/package-shims.ts +++ b/packages/compiler/src/frontend/package-shims.ts @@ -63,7 +63,7 @@ interface ImplementationShim { * 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`. */ + * is not `index.d.ts` (a `.d.mts` or `.d.cts` entry included). */ typesFile?: string; /** An alternative entry for this installation (relative to the shim * root), or undefined for the version's own entry. */ @@ -117,6 +117,15 @@ const IMPLEMENTATION_SHIMS: ReadonlyMap = new Map< variants: ["hot-shots/v8-unix-dgram.ts"], }, ], + [ + "@hono/node-server", + { + name: "@hono/node-server", + mainFile: "dist/index.mjs", + typesFile: "dist/index.d.mts", + versions: new Map([[2, "hono-node-server/v2.ts"]]), + }, + ], [ "undici", { @@ -324,7 +333,7 @@ export function isShimmedImport(fromFile: string, specifier: string): boolean { /** 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 (!/\.d\.[cm]?ts$/.test(path)) return null; if (path.endsWith("/index.d.ts")) { const pkgDir = dirname(path); const shim = IMPLEMENTATION_SHIMS.get(pkgDir.slice(pkgDir.lastIndexOf("/") + 1)); @@ -377,7 +386,7 @@ export function isPackageShimFile(fileName: string): boolean { /** 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"); + return name.startsWith("scriptcUnixDgram") || name === "scriptcResponseLight"; } /** Resets per-compile caches. */ diff --git a/packages/compiler/src/frontend/pipeline.ts b/packages/compiler/src/frontend/pipeline.ts index 193e8adc0..c2f528b25 100644 --- a/packages/compiler/src/frontend/pipeline.ts +++ b/packages/compiler/src/frontend/pipeline.ts @@ -228,7 +228,8 @@ function detectAutoPackages( } else if (mode === "lib" && !viaTransitive) { // CJS packages spell their dep edges as top-level requires; the // import-statement scan alone would miss every one of them. - for (const req of requiresOf(stmt)) edges.push({ spec: req.spec, loc: locOf(req.node) }); + for (const req of requiresOf(stmt, load.program)) + edges.push({ spec: req.spec, loc: locOf(req.node) }); } } // Transitive selection counts every literal require() and import() @@ -685,8 +686,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 +696,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/program-analysis.ts b/packages/compiler/src/frontend/program-analysis.ts index e49b9307e..d4ec9fa98 100644 --- a/packages/compiler/src/frontend/program-analysis.ts +++ b/packages/compiler/src/frontend/program-analysis.ts @@ -1,4 +1,5 @@ import type { + CallExpression, ExportDeclaration, Identifier, SourceFile, @@ -30,6 +31,13 @@ export class ProgramAnalysis { readonly requireEarlyReferences = new Map(); /** Per file: the first top-level statement that can run user code. */ readonly requireFirstRunnable = new Map(); + /** Per file: split require bindings (`var m; m = require("./x")`), from + * declaration to the require call of their one assignment + * (splitRequireBindings7). */ + readonly splitRequireBindings = new Map< + SourceFile, + ReadonlyMap + >(); clear(): void { this.createRequireReasons.clear(); @@ -41,5 +49,6 @@ export class ProgramAnalysis { this.moduleEarlyBindings = null; this.requireEarlyReferences.clear(); this.requireFirstRunnable.clear(); + this.splitRequireBindings.clear(); } } diff --git a/packages/compiler/src/frontend/program.test.ts b/packages/compiler/src/frontend/program.test.ts index bdd6260ed..6a068353a 100644 --- a/packages/compiler/src/frontend/program.test.ts +++ b/packages/compiler/src/frontend/program.test.ts @@ -371,15 +371,60 @@ test("repeated cycle edges reuse a file's binding index", () => { test.for([ [ - // A shared instance built by a user constructor: the reads of it in - // the partner's function can run early; the class cannot. + // A shared instance built by a user constructor that calls the + // partner's function: its read of the instance can run early. + { + "main.ts": 'import { shared } from "./a.ts"; console.log(shared.n);', + "a.ts": + 'import { f } from "./b.ts"; export class C { n = 1; constructor() { console.log(f()); } } export const shared = new C();', + "b.ts": 'import { shared } from "./a.ts"; export function f(): number { return shared.n; }', + }, + ["shared"], + ], + [ + // User code runs while the instance is built, but nothing it reaches + // calls the partner's function before the declaration completed. { "main.ts": 'import { shared } from "./a.ts"; console.log(shared.n);', "a.ts": 'import { f } from "./b.ts"; export class C { n = 1; } export const shared = new C(); console.log(f());', "b.ts": 'import { shared } from "./a.ts"; export function f(): number { return shared.n; }', }, - ["shared"], + [], + ], + [ + // The partner's top level runs first and calls a hoisted function of + // the importer that reads a const declared later; a second function + // reads another const but nothing calls it early. + { + "main.ts": 'import { late } from "./a.ts"; console.log(late());', + "a.ts": + 'import { probe } from "./b.ts"; export function read(): string { return name; } export function late(): string { return other + probe; } export const name = "n"; export const other = "o";', + "b.ts": + 'import { read } from "./a.ts"; function attempt(): string { try { return read(); } catch { return "tdz"; } } export const probe = attempt();', + }, + ["name"], + ], + [ + // The partner's class calls a method through `this` in a static + // initializer; the method reads a const of the importer declared later. + { + "main.ts": 'import { name } from "./a.ts"; console.log(name);', + "a.ts": 'import { Probe } from "./b.ts"; export const name = "n"; console.log(Probe.value);', + "b.ts": + 'import { name } from "./a.ts"; export class Probe { static read(): string { return name; } static value = this.read(); }', + }, + ["name"], + ], + [ + // The same through a static block of a class stored by a const. + { + "main.ts": 'import { name } from "./a.ts"; console.log(name);', + "a.ts": 'import { Box } from "./b.ts"; export const name = "n"; console.log(Box.value);', + "b.ts": + 'import { name } from "./a.ts"; export const Box = class { static value = ""; static read(): string { return name; } static { this.value = this.read(); } };', + }, + ["name"], ], [ // The partner's top level reads the importer's const before it ran. diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index 507d3392d..42fa6116e 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 @@ -1287,17 +1418,31 @@ function isRequireStatement7(stmt: ts.Statement): boolean { ); } +/** A require() occurrence of a top-level statement. `split` marks the + * assignment of a split require binding (splitRequireBindings7): `node` + * is its require call and `decl` the binding's declaration. */ +interface RequireOccurrence7 { + spec: string; + node: ts.Node; + decl: ts.VariableDeclaration | null; + split?: true; +} + /** The require() occurrences of one top-level statement: the declaration - * forms and the bare side-effect call. Empty for everything else. */ -function requiresOf7( - stmt: ts.Statement, -): { spec: string; node: ts.Node; decl: ts.VariableDeclaration | null }[] { + * forms, the bare side-effect call, and the assignment of a split require + * binding. Empty for everything else. */ +function requiresOf7(stmt: ts.Statement, program: ts.Program): RequireOccurrence7[] { if (ts.isExpressionStatement(stmt)) { + const split = splitRequireOfStatement7(program, stmt); + if (split !== null) + return [ + { spec: requireSpecOf7(split.call)!, node: split.call, decl: split.decl, split: true }, + ]; const spec = requireSpecOf7(stmt.expression); return spec !== null ? [{ spec, node: stmt.expression, decl: null }] : []; } if (!ts.isVariableStatement(stmt)) return []; - const out: { spec: string; node: ts.Node; decl: ts.VariableDeclaration | null }[] = []; + const out: RequireOccurrence7[] = []; for (const decl of stmt.declarationList.declarations.nodes) { const spec = decl.initializer !== undefined ? requireSpecOf7(decl.initializer) : null; if (spec !== null) out.push({ spec, node: decl, decl }); @@ -1305,8 +1450,178 @@ function requiresOf7( return out; } +/** CommonJS require bindings split into a declaration and one later + * assignment (`var m; m = require("./x")`): a top-level `var` or `let` + * declaration without initializer in a CommonJS file, assigned by exactly one + * top-level `m = require("")` statement of the require global over + * a compiled program module, and written nowhere else. The binding holds the + * module's exports from the assignment on, as `const m = require("./x")` + * does, so preflight treats the assignment as that require: its module edge, + * its cycle admission and its early-read fence. The lowering's recognizer + * (splitRequireBindingOf in cjs-inline-require.ts) accepts only these + * bindings. Keyed by declaration, with the assignment's require call. */ +function splitRequireBindings7( + program: ts.Program, + sf: ts.SourceFile, +): ReadonlyMap { + const memo = program.analysis.splitRequireBindings; + const cached = memo.get(sf); + if (cached !== undefined) return cached; + const out = new Map(); + memo.set(sf, out); + if (sf.isDeclarationFile || !isCjsJsFile7(sf, program) || !sf.text.includes("require")) + return out; + const checker = program.getTypeChecker(); + const stmts = sf.statements.nodes; + const candidates = new Map< + ts.Symbol, + { decl: ts.VariableDeclaration; call: ts.CallExpression | null; assigns: number } + >(); + for (const stmt of stmts) { + if (!ts.isVariableStatement(stmt)) continue; + if ((stmt.declarationList.flags & (ts.NodeFlags.Const | ts.NodeFlags.Using)) !== 0) continue; + for (const decl of stmt.declarationList.declarations.nodes) { + if (!ts.isIdentifier(decl.name) || decl.initializer !== undefined) continue; + const symbol = checker.getSymbolAtLocation(decl.name); + if (symbol === undefined || symbol.declarations.length !== 1) continue; + candidates.set(symbol, { decl, call: null, assigns: 0 }); + } + } + if (candidates.size === 0) return out; + // The one top-level `m = ...` statement, whose right side is the require. + for (const stmt of stmts) { + if (!ts.isExpressionStatement(stmt) || !ts.isBinaryExpression(stmt.expression)) continue; + const assign = stmt.expression; + if (assign.operatorToken.kind !== ts.SyntaxKind.EqualsToken || !ts.isIdentifier(assign.left)) + continue; + const symbol = checker.getSymbolAtLocation(assign.left); + const candidate = symbol !== undefined ? candidates.get(symbol) : undefined; + if (candidate === undefined) continue; + candidate.assigns++; + let right = assign.right; + while (ts.isParenthesizedExpression(right)) right = right.expression; + if (ts.isCallExpression(right) && splitRequireTarget7(program, sf, right)) + candidate.call = right; + } + const names = new Set(); + for (const candidate of candidates.values()) { + if (candidate.assigns === 1 && candidate.call !== null) + names.add((candidate.decl.name as ts.Identifier).text); + } + if (names.size === 0) return out; + // Exactly one write in the whole file: the assignment itself. + const writes = new Map(); + ts.walkPreorder(sf, (node) => { + if (!ts.isIdentifier(node) || !names.has(node.text)) return undefined; + const parent = node.parent; + const symbol = + parent !== undefined && ts.isShorthandPropertyAssignment(parent) && parent.name === node + ? (checker.getShorthandAssignmentValueSymbol(parent) ?? checker.getSymbolAtLocation(node)) + : checker.getSymbolAtLocation(node); + if (symbol === undefined || !candidates.has(symbol) || !isWriteReference7(node)) + return undefined; + writes.set(symbol, (writes.get(symbol) ?? 0) + 1); + return undefined; + }); + for (const [symbol, candidate] of candidates) { + if (candidate.assigns === 1 && candidate.call !== null && writes.get(symbol) === 1) + out.set(candidate.decl, candidate.call); + } + return out; +} + +/** True when `call` is a require of the CommonJS global over a compiled + * program module: not a builtin, a JSON document or a declaration file. */ +function splitRequireTarget7( + program: ts.Program, + sf: ts.SourceFile, + call: ts.CallExpression, +): boolean { + const spec = requireSpecOf7(call); + if (spec === null || canonicalBuiltinModule(spec) !== null) return false; + if (requireCallBindingKind7(program, call) !== "ambient") return false; + const dep = + resolveImport7(program, sf, spec, "require") ?? npmStaticDepSf7(program, sf, spec, "require"); + return dep !== null && !dep.isDeclarationFile && !dep.fileName.endsWith(".json"); +} + +/** The split require binding a top-level statement assigns, or null. */ +function splitRequireOfStatement7( + program: ts.Program, + stmt: ts.Statement, +): { decl: ts.VariableDeclaration; call: ts.CallExpression } | null { + if ( + !ts.isExpressionStatement(stmt) || + !ts.isBinaryExpression(stmt.expression) || + !ts.isSourceFile(stmt.parent) + ) + return null; + let right = stmt.expression.right; + while (ts.isParenthesizedExpression(right)) right = right.expression; + for (const [decl, call] of splitRequireBindings7(program, stmt.parent)) + if (call === right) return { decl, call }; + return null; +} + +/** The require call of a split require binding's declaration, or null. */ +function splitRequireCallOf7( + program: ts.Program, + decl: ts.VariableDeclaration, +): ts.CallExpression | null { + return splitRequireBindings7(program, ts.getSourceFileOfNode(decl)).get(decl) ?? null; +} + +/** True when `id` is written: an assignment target (compound and + * destructuring included), an update operand, or a for-in/of target. */ +function isWriteReference7(id: ts.Identifier): boolean { + let n: ts.Node = id; + for (;;) { + const p: ts.Node | undefined = n.parent; + if (p === undefined) return false; + if (ts.isBinaryExpression(p)) { + const k = p.operatorToken.kind; + return ( + p.left === n && k >= ts.SyntaxKind.FirstAssignment && k <= ts.SyntaxKind.LastAssignment + ); + } + if (ts.isPostfixUnaryExpression(p) || ts.isPrefixUnaryExpression(p)) { + return ( + (p.operator === ts.SyntaxKind.PlusPlusToken || + p.operator === ts.SyntaxKind.MinusMinusToken) && + p.operand === n + ); + } + if (ts.isForOfStatement(p) || ts.isForInStatement(p)) return p.initializer === n; + if ( + ts.isParenthesizedExpression(p) || + ts.isArrayLiteralExpression(p) || + ts.isSpreadElement(p) || + ts.isSpreadAssignment(p) || + ts.isShorthandPropertyAssignment(p) || + ts.isObjectLiteralExpression(p) || + (ts.isPropertyAssignment(p) && p.initializer === n) + ) { + n = p; + continue; + } + return false; + } +} + +/** `true`, or `!0`, its minified spelling. */ +function isTrueLiteral(expr: ts.Expression): boolean { + return ( + expr.kind === ts.SyntaxKind.TrueKeyword || + (ts.isPrefixUnaryExpression(expr) && + expr.operator === ts.SyntaxKind.ExclamationToken && + ts.isNumericLiteral(expr.operand) && + expr.operand.text === "0") + ); +} + /** The exact `Object.defineProperty(exports|module.exports, "__esModule", - * { value: true })` interop stamp (shared by preflight and the no-op lowering). */ + * { value: true })` interop stamp (shared by preflight and the no-op lowering); + * `{ value: !0 }` is the same stamp. */ export function isEsModuleStamp(expr: ts.Expression): boolean { if (!ts.isCallExpression(expr) || expr.questionDotToken !== undefined) return false; const callee = expr.expression; @@ -1340,7 +1655,7 @@ export function isEsModuleStamp(expr: ts.Expression): boolean { ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === "value" && - p.initializer.kind === ts.SyntaxKind.TrueKeyword + isTrueLiteral(p.initializer) ); } @@ -1432,7 +1747,7 @@ function purePrefixDecl7(decl: ts.VariableDeclaration): boolean { function purePrefixStmt7(program: ts.Program, s: ts.Statement): boolean { if (ts.isEmptyStatement(s) || ts.isFunctionDeclaration(s)) return true; if (ts.isExpressionStatement(s) && ts.isStringLiteral(s.expression)) return true; // directive - if (isRequireStatement7(s)) return true; + if (isRequireStatement7(s) || splitRequireOfStatement7(program, s) !== null) return true; if (isCjsJsFile7(ts.getSourceFileOfNode(s), program)) { if (ts.isExpressionStatement(s) && isEsModuleStamp(s.expression)) { const call = s.expression as ts.CallExpression; @@ -1559,12 +1874,20 @@ function requireEarlyReference7( const cached = memo.get(decl); if (cached !== undefined) return cached; let found: ts.Identifier | null = null; - const stmt = decl.parent?.parent; const sf = ts.getSourceFileOfNode(decl); - if (stmt !== undefined && ts.isVariableStatement(stmt) && ts.isSourceFile(stmt.parent)) { + // A split require binding is bound at its assignment statement. + const splitCall = splitRequireCallOf7(program, decl); + const stmt = splitCall !== null ? splitCall.parent?.parent : decl.parent?.parent; + if ( + stmt !== undefined && + (ts.isVariableStatement(stmt) || splitCall !== null) && + ts.isSourceFile(stmt.parent) + ) { const stmts = sf.statements.nodes; - const k = stmts.indexOf(stmt); - const declarations = stmt.declarationList.declarations.nodes; + const k = stmts.indexOf(stmt as ts.Statement); + const declarations = ts.isVariableStatement(stmt) + ? stmt.declarationList.declarations.nodes + : []; // Earlier declarators run too: `const x = read(), dep = require('./dep')`. const precedingDecls = declarations.slice(0, declarations.indexOf(decl)); let firstRunnable = program.analysis.requireFirstRunnable.get(sf); @@ -1617,6 +1940,12 @@ export function requireBindingEarlyUse7( if (sym) bindings.add(sym); } if (bindings.size === 0) return null; + // A split require binding's assignment writes the binding; it reads nothing. + let splitWrite: ts.Node | null = splitRequireCallOf7(program, decl); + while (splitWrite !== null && ts.isParenthesizedExpression(splitWrite.parent)) + splitWrite = splitWrite.parent; + splitWrite = + splitWrite !== null && ts.isBinaryExpression(splitWrite.parent) ? splitWrite.parent.left : null; const stmts = sf.statements.nodes; const cjs = isCjsJsFile7(sf, program); const symbolOf = (id: ts.Identifier): ts.Symbol | undefined => { @@ -1840,7 +2169,7 @@ export function requireBindingEarlyUse7( const sym = symbolOf(n); if (sym === undefined) return undefined; if (bindings.has(sym)) { - if (exactEarlyRead?.(n) === true) return undefined; + if (n === splitWrite || exactEarlyRead?.(n) === true) return undefined; hit = n; return "stop"; } @@ -3351,7 +3680,7 @@ function cjsCycleRefusal7( } // Bindings of other members must stay unreachable from top-level code. for (const st of stmts) { - for (const req of requiresOf7(st)) { + for (const req of requiresOf7(st, program)) { if (req.decl === null) continue; const dep = resolveImport7(program, m, req.spec, "require"); if (dep === null || !members.has(dep)) continue; @@ -3760,22 +4089,7 @@ function computeCycleEarlyBindings7( const evaluation = known ? comp.slice().sort((a, b) => position.get(a)! - position.get(b)!) : comp.slice(); - interface Candidate { - symbol: ts.Symbol; - name: ts.Identifier; - kind: CycleEarlyBinding["kind"]; - module: ts.SourceFile; - /** User code can run between the cycle's start and the end of the - * declaration (always true when the order is unknown). */ - exposed: boolean; - rank: number; - start: number; - end: number; - reference: ts.Identifier | null; - folded: ts.Identifier | null; - localNames: Map; - } - const candidates = new Map(); + const candidates = new Map(); const names = new Set(); let dirty = !known; evaluation.forEach((m, rank) => { @@ -3812,6 +4126,7 @@ function computeCycleEarlyBindings7( start: ts.tokenStartOfNode(stmt), end: stmt.end, reference: null, + deferredReference: null, folded: null, localNames: new Map(), }); @@ -3880,15 +4195,27 @@ function computeCycleEarlyBindings7( ref.end <= c.end ) continue; + if (inDeferredPosition7(ref)) { + if (c.exposed && c.deferredReference === null) c.deferredReference = ref; + continue; + } let early: boolean; - if (inDeferredPosition7(ref)) early = c.exposed; - else if (sf === c.module) early = ts.tokenStartOfNode(ref) < c.start; + if (sf === c.module) early = ts.tokenStartOfNode(ref) < c.start; else if (!memberSet.has(sf) || !rankOf.has(sf)) early = false; else early = !known || rankOf.get(sf)! < c.rank; if (early) c.reference = ref; } } + // With a static evaluation order, a deferred reference only runs early + // when the code that runs before the declaration completes can reach + // the function holding it. + const reached = + known && [...candidates.values()].some((c) => c.reference === null && c.deferredReference) + ? cycleReachedDeferredReferences7(program, evaluation, candidates) + : null; for (const c of candidates.values()) { + if (c.reference === null && c.deferredReference !== null) + c.reference = reached === null ? c.deferredReference : (reached.get(c.symbol) ?? null); if (c.reference === null) continue; const settled = new Set(order.filter((sf) => !rankOf.has(sf))); if (known) for (const m of evaluation.slice(c.rank + 1)) settled.add(m); @@ -3907,6 +4234,299 @@ function computeCycleEarlyBindings7( return out; } +/** Collects into `units` the callables an initializer expression stores + * (see moduleEarlyBindingsOf7), recording in `immediate` the parts of a + * stored node that run where it is evaluated. */ +function storedCallables7( + checker: ts.TypeChecker, + e: ts.Expression, + units: ts.Node[], + immediate: Map, +): void { + while ( + ts.isParenthesizedExpression(e) || + ts.isAsExpression(e) || + ts.isSatisfiesExpression(e) || + ts.isNonNullExpression(e) + ) + e = e.expression; + if (ts.isArrowFunction(e) || ts.isFunctionExpression(e)) { + units.push(e); + } else if (ts.isClassExpression(e)) { + units.push(e); + immediate.set(e, classSyncParts7(e)); + } else if (ts.isObjectLiteralExpression(e)) { + for (const p of e.properties.nodes) { + if ( + ts.isMethodDeclaration(p) || + ts.isGetAccessorDeclaration(p) || + ts.isSetAccessorDeclaration(p) + ) { + // A computed key evaluates where the literal does; a plain + // member-chain key (`[Equal.symbol]`) runs no user code. + if (!ts.isComputedPropertyName(p.name)) units.push(p); + else if (memberChainKey7(p.name.expression)) { + units.push(p); + immediate.set(p, [p.name]); + } + } else if (ts.isPropertyAssignment(p)) + storedCallables7(checker, p.initializer, units, immediate); + } + } else if (ts.isArrayLiteralExpression(e)) { + for (const el of e.elements.nodes) + if (!ts.isSpreadElement(el)) storedCallables7(checker, el, units, immediate); + } else if (ts.isCallExpression(e)) { + // `const f = wrap(callback)` where wrap only returns closures over + // the callback: it runs when the stored result is called. + e.arguments.nodes.forEach((arg, index) => { + let fn: ts.Expression = arg; + while (ts.isParenthesizedExpression(fn)) fn = fn.expression; + if ( + (ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) && + returnsOnlyClosuresOver7(checker, e.expression, index) + ) + units.push(fn); + }); + } +} + +/** A binding of an import cycle and what the reference scan found. */ +interface CycleCandidate7 { + symbol: ts.Symbol; + name: ts.Identifier; + kind: CycleEarlyBinding["kind"]; + module: ts.SourceFile; + /** User code can run between the cycle's start and the end of the + * declaration (always true when the order is unknown). */ + exposed: boolean; + rank: number; + start: number; + end: number; + /** A reference read at top level before the declaration ran. */ + reference: ts.Identifier | null; + /** A deferred reference (function body, parameter default, instance + * field) when user code may run before the declaration completes. */ + deferredReference: ts.Identifier | null; + folded: ts.Identifier | null; + localNames: Map; +} + +/** For one import cycle with a static evaluation order: the first deferred + * reference to each candidate that code running before the candidate's + * declaration completes can reach, or null when the walk cannot be precise + * (top-level await, a default-exported function or class declaration). + * + * The cycle's top-level code runs in evaluation order. A stored callable + * (a function or class declaration, or a function, class or object-literal + * method stored by a variable initializer) becomes reachable once + * reachable code names its binding, through an import alias or a + * namespace member too; every other function-like in reachable code counts + * as reachable, since it may be handed to a callee. A namespace import of + * a cycle member used other than for a member read makes every stored + * callable reachable. moduleEarlyBindingsOf7 is the single-module form. */ +function cycleReachedDeferredReferences7( + program: ts.Program, + evaluation: readonly ts.SourceFile[], + candidates: ReadonlyMap, +): Map | null { + const checker = program.getTypeChecker(); + const members = new Set(evaluation); + for (const sf of evaluation) { + if (hasTopLevelAwait7(sf)) return null; + for (const stmt of sf.statements.nodes) { + if ( + (ts.isFunctionDeclaration(stmt) || ts.isClassDeclaration(stmt)) && + (ts.getCombinedModifierFlags(stmt as unknown as ts.Declaration) & + ts.ModifierFlags.Default) !== + 0 + ) + return null; + } + } + const stored = new Set(); + const immediate = new Map(); + const storedNames: ts.Identifier[] = []; + const unitsOfName = new Map(); + const store = (name: ts.Identifier, units: ts.Node[]): void => { + if (units.length === 0) return; + for (const u of units) stored.add(u); + storedNames.push(name); + unitsOfName.set(name, units); + }; + for (const sf of evaluation) { + for (const stmt of sf.statements.nodes) { + if ( + (ts.getCombinedModifierFlags(stmt as unknown as ts.Declaration) & + ts.ModifierFlags.Ambient) !== + 0 + ) + continue; + if (ts.isFunctionDeclaration(stmt) && stmt.name !== undefined && stmt.body !== undefined) { + store(stmt.name, [stmt]); + } else if (ts.isClassDeclaration(stmt) && stmt.name !== undefined) { + store(stmt.name, [stmt]); + } else if (ts.isVariableStatement(stmt)) { + for (const d of stmt.declarationList.declarations.nodes) { + if (ts.isIdentifier(d.name) && d.initializer !== undefined) { + const units: ts.Node[] = []; + storedCallables7(checker, d.initializer, units, immediate); + store(d.name, units); + } + } + } + } + } + const unitsBySymbol = new Map(); + const allUnits: ts.Node[] = []; + for (const name of storedNames) { + const symbol = checker.getSymbolAtLocation(name); + if (symbol === undefined) continue; + const units = unitsOfName.get(name)!; + unitsBySymbol.set(symbol, [...(unitsBySymbol.get(symbol) ?? []), ...units]); + allUnits.push(...units); + } + // A named class expression's inner name (`class Impl { static x = + // Impl.m(); }`) names the class inside its own body, as the binding it + // is stored in names it outside. + const innerNames: ts.Identifier[] = []; + for (const unit of allUnits) { + if (!ts.isClassExpression(unit) || unit.name === undefined) continue; + const symbol = checker.getSymbolAtLocation(unit.name); + if (symbol === undefined) continue; + innerNames.push(unit.name); + unitsBySymbol.set(symbol, [...(unitsBySymbol.get(symbol) ?? []), unit]); + } + // The identifier texts worth resolving: declared names, and the local + // names of imports (named, default and namespace) in cycle members. + const names = new Set(); + for (const c of candidates.values()) names.add(c.name.text); + for (const name of storedNames) names.add(name.text); + for (const name of innerNames) names.add(name.text); + for (const sf of evaluation) { + for (const stmt of sf.statements.nodes) { + if (!ts.isImportDeclaration(stmt) || stmt.importClause === undefined) continue; + const clause = stmt.importClause; + if (clause.name !== undefined) names.add(clause.name.text); + const bindings = clause.namedBindings; + if (bindings === undefined) continue; + if (ts.isNamedImports(bindings)) + for (const element of bindings.elements.nodes) names.add(element.name.text); + else names.add(bindings.name.text); + } + } + const idents: ts.Identifier[] = []; + for (const sf of evaluation) { + ts.walkPreorder(sf, (node) => { + if (ts.isImportDeclaration(node)) return "skip"; + if (ts.isIdentifier(node) && names.has(node.text)) idents.push(node); + return undefined; + }); + } + checker.prefetchSymbolNodesExact(idents); + const symbolOf = (id: ts.Identifier): ts.Symbol | undefined => { + const parent = id.parent; + let symbol = checker.getSymbolAtLocation(id); + if (parent !== undefined && ts.isShorthandPropertyAssignment(parent) && parent.name === id) { + symbol = checker.getShorthandAssignmentValueSymbol(parent) ?? symbol; + } + if (symbol !== undefined && symbol.flags & ts.SymbolFlags.Alias) { + symbol = checker.getAliasedSymbol(symbol); + } + return symbol; + }; + const isMemberNamespace = (symbol: ts.Symbol): boolean => + (symbol.flags & ts.SymbolFlags.ValueModule) !== 0 && + symbol.declarations.some((d) => ts.isSourceFile(d) && members.has(d)); + const declared = new Set(); + const reachedUnits = new Set(); + const out = new Map(); + const work: ts.Node[] = []; + const reach = (unit: ts.Node): void => { + if (reachedUnits.has(unit)) return; + reachedUnits.add(unit); + work.push(unit); + }; + const visit = (root: ts.Node): void => { + ts.walkPreorder(root, (node) => { + if (node !== root && stored.has(node)) { + for (const part of immediate.get(node) ?? []) visit(part); + return "skip"; + } + if (ts.isImportDeclaration(node)) return "skip"; + if (node.kind === ts.SyntaxKind.ThisKeyword) { + // `this` in a static initializer or block names the class, so it + // reaches the class's methods as naming the class does. + const owner = staticThisClass7(node); + if (owner !== undefined && stored.has(owner)) reach(owner); + return undefined; + } + if (!ts.isIdentifier(node) || !names.has(node.text)) return undefined; + if (inTypePosition7(node)) return undefined; + const parent = node.parent; + if (parent !== undefined && ts.isExportSpecifier(parent)) return undefined; + if (parent !== undefined && declaredName7(parent) === node) return undefined; + const symbol = symbolOf(node); + if (symbol === undefined) return undefined; + if (isMemberNamespace(symbol)) { + // `ns.member` resolves through the member's own identifier; any + // other use can reach every export. + if ( + !( + parent !== undefined && + ts.isPropertyAccessExpression(parent) && + parent.expression === node + ) + ) + for (const unit of allUnits) reach(unit); + return undefined; + } + const c = candidates.get(symbol); + if ( + c !== undefined && + node !== c.name && + !declared.has(symbol) && + !out.has(symbol) && + inDeferredPosition7(node) && + !( + c.kind === "class" && + ts.getSourceFileOfNode(node) === c.module && + ts.tokenStartOfNode(node) >= c.start && + node.end <= c.end + ) + ) + out.set(symbol, node); + for (const unit of unitsBySymbol.get(symbol) ?? []) reach(unit); + return undefined; + }); + }; + const drain = (): void => { + while (work.length > 0) visit(work.pop()!); + }; + const declare = (name: ts.BindingName): void => { + for (const id of boundNames7(name)) { + const symbol = checker.getSymbolAtLocation(id); + if (symbol !== undefined) declared.add(symbol); + } + }; + for (const sf of evaluation) { + for (const stmt of sf.statements.nodes) { + if (ts.isClassDeclaration(stmt)) { + for (const part of classSyncParts7(stmt)) visit(part); + drain(); + if (stmt.name !== undefined) declare(stmt.name); + } else if (ts.isVariableStatement(stmt)) { + for (const d of stmt.declarationList.declarations.nodes) { + visit(d); + drain(); + declare(d.name); + } + } else if (!ts.isFunctionDeclaration(stmt)) visit(stmt); + drain(); + } + } + return out; +} + /** Top-level let/const and class bindings, in modules outside the * exactly-initialized cycles, that code running before their declaration * completes can read. @@ -3971,51 +4591,8 @@ function moduleEarlyBindingsOf7( storedBy.set(name.text, list); }; const checker = program.getTypeChecker(); - const storedIn = (e: ts.Expression, units: ts.Node[]): void => { - while ( - ts.isParenthesizedExpression(e) || - ts.isAsExpression(e) || - ts.isSatisfiesExpression(e) || - ts.isNonNullExpression(e) - ) - e = e.expression; - if (ts.isArrowFunction(e) || ts.isFunctionExpression(e)) { - units.push(e); - } else if (ts.isClassExpression(e)) { - units.push(e); - immediate.set(e, classSyncParts7(e)); - } else if (ts.isObjectLiteralExpression(e)) { - for (const p of e.properties.nodes) { - if ( - ts.isMethodDeclaration(p) || - ts.isGetAccessorDeclaration(p) || - ts.isSetAccessorDeclaration(p) - ) { - // A computed key evaluates where the literal does; a plain - // member-chain key (`[Equal.symbol]`) runs no user code. - if (!ts.isComputedPropertyName(p.name)) units.push(p); - else if (memberChainKey7(p.name.expression)) { - units.push(p); - immediate.set(p, [p.name]); - } - } else if (ts.isPropertyAssignment(p)) storedIn(p.initializer, units); - } - } else if (ts.isArrayLiteralExpression(e)) { - for (const el of e.elements.nodes) if (!ts.isSpreadElement(el)) storedIn(el, units); - } else if (ts.isCallExpression(e)) { - // `const f = wrap(callback)` where wrap only returns closures over - // the callback: it runs when the stored result is called. - e.arguments.nodes.forEach((arg, index) => { - let fn: ts.Expression = arg; - while (ts.isParenthesizedExpression(fn)) fn = fn.expression; - if ( - (ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) && - returnsOnlyClosuresOver7(checker, e.expression, index) - ) - units.push(fn); - }); - } - }; + const storedIn = (e: ts.Expression, units: ts.Node[]): void => + storedCallables7(checker, e, units, immediate); sf.statements.nodes.forEach((stmt, index) => { if (ambient(stmt)) return; if (ts.isFunctionDeclaration(stmt) && stmt.name !== undefined && stmt.body !== undefined) { @@ -4268,6 +4845,40 @@ function returnsOnlyClosuresOver7( * expressions, decorators, computed member keys, static field * initializers and static blocks. Methods and instance fields wait for a * call or a construction. */ +/** The class whose static field initializer or static block evaluates `this` + * (through arrow functions, which keep the enclosing `this`), or undefined + * when another function or a class's instance side binds it. */ +function staticThisClass7(node: ts.Node): ts.ClassLikeDeclaration | undefined { + let child = node; + for (let n = node.parent; n !== undefined; child = n, n = n.parent) { + if (ts.isArrowFunction(n)) continue; + if (ts.isClassStaticBlockDeclaration(n)) return n.parent as ts.ClassLikeDeclaration; + if (ts.isPropertyDeclaration(n)) { + if (n.initializer !== child) return undefined; + return (ts.getCombinedModifierFlags(n) & ts.ModifierFlags.Static) !== 0 + ? (n.parent as ts.ClassLikeDeclaration) + : undefined; + } + if (ts.isComputedPropertyName(n)) { + // A computed key evaluates in the scope around the class. + const member = n.parent; + const cls = member?.parent; + if (cls === undefined) return undefined; + child = cls; + n = cls; + continue; + } + if ( + ts.isFunctionLike(n) || + ts.isClassDeclaration(n) || + ts.isClassExpression(n) || + ts.isSourceFile(n) + ) + return undefined; + } + return undefined; +} + function classSyncParts7(c: ts.ClassLikeDeclaration): ts.Node[] { const decorators = (n: ts.Node): ts.Node[] => ((n as { modifiers?: ts.NodeArray }).modifiers?.nodes ?? []).filter( @@ -4801,7 +5412,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); @@ -5343,7 +5956,7 @@ function preflight7(load: LoadResult): { // module aliases cannot represent; createRequire bindings have no // checked form either. const earlyRequireFence = ( - req: { decl: ts.VariableDeclaration | null }, + req: RequireOccurrence7, bindingKind: RequireCallBindingKind7, loc: { file: string; start: number; end: number }, ): boolean => { @@ -5351,6 +5964,7 @@ function preflight7(load: LoadResult): { if ( early === null || (bindingKind !== "createRequire" && + req.split === undefined && ((req.decl!.parent?.flags ?? 0) & (ts.NodeFlags.Const | ts.NodeFlags.Let)) !== 0) ) { return false; @@ -5361,14 +5975,16 @@ function preflight7(load: LoadResult): { loc, bindingKind === "createRequire" ? `createRequire() binding '${early.text}' below code that can reach it (Node throws ReferenceError on any earlier access, which is not modeled — move the require above the code that can run first)` - : `var require() binding '${early.text}' below code that can reach it (Node reads undefined before the require runs, which is not modeled — declare it with const or let, or move the require above the code that can run first)`, + : req.split !== undefined + ? `require() binding '${early.text}' assigned below code that can reach it (Node reads undefined before the assignment runs, which is not modeled — move the assignment above the code that can run first)` + : `var require() binding '${early.text}' below code that can reach it (Node reads undefined before the require runs, which is not modeled — declare it with const or let, or move the require above the code that can run first)`, ), ); return true; }; for (let k = 0; k < stmts.length; k++) { const stmt = stmts[k]!; - for (const req of requiresOf7(stmt)) { + for (const req of requiresOf7(stmt, program)) { const loc = { file: sf.fileName, start: ts.tokenStartOfNode(req.node), @@ -5395,8 +6011,8 @@ function preflight7(load: LoadResult): { const jsonDep = resolveImport7(program, sf, req.spec, "require"); if (jsonDep !== null && jsonDep.fileName.endsWith(".json")) { if (bindingKind === "createRequire") continue; - if (ts.isVariableStatement(stmt) && requiresOf7(stmt).length > 1) { - const others = requiresOf7(stmt).some((r) => { + if (ts.isVariableStatement(stmt) && requiresOf7(stmt, program).length > 1) { + const others = requiresOf7(stmt, program).some((r) => { const d = resolveImport7(program, sf, r.spec, "require"); return d === null || !d.fileName.endsWith(".json"); }); @@ -5570,6 +6186,13 @@ function preflight7(load: LoadResult): { if (jsRequireFile && !isNodeEsmFile7(sf, program)) { for (const call of inlineRequiresOf7(sf)) { if (requireCallBindingKind7(program, call) !== "ambient") continue; + // A split require binding's assignment is a statement-level + // require edge (requiresOf7 above). + if ( + call.parent?.parent !== undefined && + splitRequireOfStatement7(program, call.parent.parent as ts.Statement) !== null + ) + continue; const spec = requireSpecOf7(call)!; if (load.externalTypes.has(spec) || canonicalBuiltinModule(spec) !== null) continue; const lazy = inDeferredPosition7(call); @@ -6318,6 +6941,7 @@ export { requireSpecOf7 as requireSpecOf, isRequireStatement7 as isRequireStatement, requiresOf7 as requiresOf, + splitRequireCallOf7 as splitRequireCallOf, isNodeEsmFile7 as isNodeEsmFile, resolveImport7 as resolveImport, resolveNpmImport7 as resolveNpmImport, diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 35514d992..f1e86067e 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -43,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"; @@ -555,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 @@ -4112,6 +4127,17 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { fields.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)); return { kind: "record", shapeId: ctx.shapes.intern(fields, false, undefined, order) }; } + // crypto.getCipherInfo's result is Node's info object itself, read as a + // checked-dynamic value: Node leaves out the fields a cipher lacks, + // including the nid the declaration requires, so a record cannot hold + // every result. + if ( + isStdlibInterface("CipherInfo") && + psym!.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && isDeclaredInAmbientModule(d, "crypto"), + ) + ) + return DYN; // Filesystem options are data records. Keep their declared optional fields // instead of allowing arbitrary standard-library interfaces into records. if ( diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index daa5af366..4f7b93cfd 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -601,6 +601,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 } : {}), diff --git a/packages/compiler/src/ir/array-length-stability.test.ts b/packages/compiler/src/ir/array-length-stability.test.ts index f9b3cc9c2..97270e059 100644 --- a/packages/compiler/src/ir/array-length-stability.test.ts +++ b/packages/compiler/src/ir/array-length-stability.test.ts @@ -193,3 +193,34 @@ test("number and boolean writes to class fields run no user code", () => { // A reference stored into a field releases the old one. expect(preserves([fieldSet(arr("b"))], "i", new Set(), scope)).toBe(false); }); + +test("optional element reads, their assertion errors and present-undefined stores at an in-range index keep the length", () => { + const optional = (receiver: string, index: IrExpr): IrExpr => ({ + kind: "call", + callee: "%arr.idxOr.f64", + args: [arr(receiver), index], + type: F64, + loc, + }); + const undefinedAt = (receiver: string, index: IrExpr): IrStmt => ({ + kind: "arraySetUndefined", + arr: arr(receiver), + index, + loc, + }); + const thrown: IrExpr = { kind: "libCall", fn: "error.nodeThrow", args: [num(1)], type: F64, loc }; + expect(preserves([stmt(optional("a", ref("i"))), stmt(thrown)])).toBe(true); + expect(preserves([undefinedAt("a", ref("i"))])).toBe(true); + // Past the counter, a present undefined extends the array. + expect(preserves([undefinedAt("a", plus(ref("i"), num(1)))])).toBe(false); + // The high counter of a two-counter loop is in range as well. + const paired = (body: IrStmt[]): boolean => + preservesArrayLength( + [cond, ...body], + { receiver: "a", inRange: "i", inRangeHigh: "j" }, + locals, + new Set(), + ); + expect(paired([undefinedAt("a", ref("j")), set("a", ref("j"), num(0))])).toBe(true); + expect(preserves([set("a", ref("j"), num(0))])).toBe(false); +}); diff --git a/packages/compiler/src/ir/array-length-stability.ts b/packages/compiler/src/ir/array-length-stability.ts index ed57972b6..7715ea1e1 100644 --- a/packages/compiler/src/ir/array-length-stability.ts +++ b/packages/compiler/src/ir/array-length-stability.ts @@ -162,6 +162,10 @@ export interface ArrayLengthLoop { * long as the length itself has not changed). An element write at this * index replaces an existing element or sets a non-index property. */ inRange: string | null; + /** A second such binding: the high counter of a two-counter loop + * (`for (let i = lo, j = hi; i < j; i++, j--)`), whose values stay + * between the low counter's start and its own. */ + inRangeHigh?: string | null; } /** Whether running `nodes` (a loop's condition, body and update) can never @@ -191,7 +195,10 @@ export function preservesArrayLength( // escaping. const receiver = (e: IrExpr): boolean => (e.kind === "varRef" && e.localId === loop.receiver) || expr(e); - const written = (id: string): boolean => id === loop.receiver || id === loop.inRange; + const inRange = (id: string): boolean => + (loop.inRange !== null && id === loop.inRange) || + (loop.inRangeHigh != null && id === loop.inRangeHigh); + const written = (id: string): boolean => id === loop.receiver || inRange(id); const expr = (e: IrExpr): boolean => { switch (e.kind) { case "varRef": @@ -224,12 +231,21 @@ export function preservesArrayLength( case "call": // A checked union extraction (`x!`, coercions/unions.ts): the // asserted arm's payload or a TypeError, and no user code. - return e.callee.startsWith("%union.narrow.") && e.args.every(expr); + if (e.callee.startsWith("%union.narrow.")) return e.args.every(expr); + // An element read as `T | undefined` (int32-arrays.ts isElementReader). + return ( + e.callee.startsWith("%arr.idxOr.") && + e.args.length === 2 && + receiver(e.args[0]!) && + expr(e.args[1]!) + ); case "mapIntrinsic": return READING_MAP_METHODS.has(e.method) && expr(e.receiver) && e.args.every(expr); case "setIntrinsic": return READING_SET_METHODS.has(e.method) && expr(e.receiver) && e.args.every(expr); case "libCall": + // The TypeError of an assertion past a union arm leaves the loop. + if (e.fn === "error.nodeThrow") return e.args.every(expr); return ( isBorrowSafeMath(e.fn) && e.args.every((arg) => arg.type.kind === "f64") && @@ -254,14 +270,18 @@ export function preservesArrayLength( break; case "arraySet": return ( - (separate(s.arr) || - (s.index.kind === "varRef" && - loop.inRange !== null && - s.index.localId === loop.inRange)) && + (separate(s.arr) || (s.index.kind === "varRef" && inRange(s.index.localId))) && receiver(s.arr) && expr(s.index) && expr(s.value) ); + case "arraySetUndefined": + // A present undefined replacing an element below the length. + return ( + (separate(s.arr) || (s.index.kind === "varRef" && inRange(s.index.localId))) && + receiver(s.arr) && + expr(s.index) + ); case "return": return s.value === null || expr(s.value); case "fieldSet": diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index d8e78be08..53f4c3eda 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -16,6 +16,8 @@ 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 nameOrNid and options errors. + "crypto.getCipherInfo", // Node's candidate, size, options, add and rem errors, and OpenSSL's // generation errors. "crypto.checkPrimeSync", @@ -362,6 +364,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "transform.newDyn", "passthrough.newDyn", "readable.initDyn", + "stream.checkEncodingOption", "writable.initDyn", "duplex.initDyn", "transform.initDyn", @@ -446,6 +449,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", @@ -610,6 +616,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", @@ -632,6 +639,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 diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 7cb666ccf..1be0f95cd 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -66,6 +66,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "fetch.start": { argTypes: [STRING, DYN], result: { kind: "promise", inner: DYN } }, "fetch.responseNew": { argTypes: [DYN, DYN], result: DYN }, "fetch.responseJsonStatic": { argTypes: [DYN, DYN], result: DYN }, + "fetch.responseLight": { argTypes: [DYN], result: arrayOf(STRING) }, "fetch.input": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, "abort.controllerNew": { argTypes: [], result: DYN }, /** EventTarget / Event / CustomEvent construction (scr_event_target.c): @@ -1835,7 +1836,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * connection (+1, the same handle net.connect would give); * sockRemoteAddress answers `string | undefined` (NULL after the fd * closed, Node's destroyed-socket undefined; a dual-stack accept of an - * IPv4 peer reads "::ffff:a.b.c.d" like Node). reqResume/reqDestroy and + * IPv4 peer reads "::ffff:a.b.c.d" like Node). reqResume/reqPause/reqDestroy and * the req error/close listener slots complete the IncomingMessage * surface portless's client responses use; resDestroy/resOnClose and * resWriteHeadPairs ([k0,v0,k1,v1,...] — the env.pairs helper's flat @@ -1902,6 +1903,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "http.reqPipeClient": { argTypes: [HTTPREQ_T, HTTPCLIENTREQ_T], result: VOID }, "http.reqPipeSock": { argTypes: [HTTPREQ_T, NETSOCKET_T], result: VOID }, "http.reqResume": { argTypes: [HTTPREQ_T], result: VOID }, + "http.reqPause": { argTypes: [HTTPREQ_T], result: VOID }, "http.reqDestroy": { argTypes: [HTTPREQ_T], result: VOID }, "http.reqOnError": { argTypes: [HTTPREQ_T, null, BOOL], result: VOID }, "http.reqOnClose": { @@ -2844,6 +2846,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * undefined, and call it in a later macrotask; Node's argument errors * throw (may-throw). */ "crypto.checkPrimeSync": { argTypes: [DYN, DYN], result: DYN }, + /** getCipherInfo(nameOrNid, options) over checked-dynamic arguments + * (scr_cipher_info.c): the info object or undefined. */ + "crypto.getCipherInfo": { 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 }, @@ -3429,6 +3434,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 @@ -3745,6 +3758,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 @@ -3752,6 +3769,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 }, @@ -3777,6 +3797,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 @@ -3967,6 +3995,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "stream.setTransform": { argTypes: [null, null], result: VOID }, "stream.setFlush": { argTypes: [null, null], result: VOID }, "stream.setConstruct": { argTypes: [null, null], result: VOID }, + "stream.replaceConstruct": { 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 @@ -4103,6 +4132,11 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * after construction; answers the receiver (+1, the chaining shape). */ "stream.objectMode": { argTypes: [null, BOOL, BOOL], result: VOID }, "stream.decodeStrings": { argTypes: [null, BOOL], result: VOID }, + /** Runtime decodeStrings and defaultEncoding option values, applied + * after construction (answers the receiver +1); the encoding was + * validated by stream.checkEncodingOption before construction. */ + "stream.optionValues": { argTypes: [null, DYN, DYN], result: VOID }, + "stream.checkEncodingOption": { argTypes: [DYN], 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, diff --git a/packages/compiler/src/ir/callback-specialization.ts b/packages/compiler/src/ir/callback-specialization.ts index 00432621d..6038ccaa5 100644 --- a/packages/compiler/src/ir/callback-specialization.ts +++ b/packages/compiler/src/ir/callback-specialization.ts @@ -2,6 +2,7 @@ import type { IrContracts, IrExpr, IrFunction, IrModule, IrStmt } from "./ir.js" import { exprFunctionReference, moduleFunctionReferences } from "./function-references.js"; import { reparseExpr, reparseStmt } from "./serialize.js"; import { everyStmtList, mapExprChildren, mapStmtChildren } from "./traverse.js"; +import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "./node-index.js"; /** Largest body (IR nodes) that is copied per callback target. */ const MAX_FUNCTION_NODES = 300; @@ -15,11 +16,24 @@ const COPIED_NODE_FRACTION = 0.25; * closure it creates writes through its environment (and transitively the * closures those create). A parameter or capture absent from its function's * set holds the value it entered with for the whole call. */ -export function writtenLocals(mod: IrModule): Map> { - return rebindings(mod).written; +export function writtenLocals(mod: IrModule, nodes?: IrNodeIndex): Map> { + return rebindings(mod, nodes).written; } -function rebindings(mod: IrModule): { +/** The nodes that rebind a local or capture bindings. */ +const REBINDING_KINDS: readonly IrNodeKind[] = [ + "assign", + "forOf", + "tryCatch", + "assignExpr", + "incDec", + "closure", +]; + +function rebindings( + mod: IrModule, + nodes?: IrNodeIndex, +): { written: Map>; /** Bindings of each function that a closure it creates writes. */ closureWritten: Map>; @@ -27,20 +41,15 @@ function rebindings(mod: IrModule): { const written = new Map>(); const closureWritten = new Map>(); const sites: { owner: string; fnName: string; captures: string[] }[] = []; + const scan = nodes?.scan(REBINDING_KINDS); for (const fn of mod.functions) { const ids = new Set(); - everyStmtList(fn.body, { - stmt: (stmt) => { - if (stmt.kind === "assign" || stmt.kind === "forOf") ids.add(stmt.localId); - if (stmt.kind === "tryCatch" && stmt.catchLocalId !== null) ids.add(stmt.catchLocalId); - return true; - }, - expr: (expr) => { - if (expr.kind === "assignExpr" || expr.kind === "incDec") ids.add(expr.localId); - if (expr.kind === "closure" && expr.captures.length) - sites.push({ owner: fn.name, fnName: expr.fnName, captures: expr.captures }); - return true; - }, + eachNodeOfKinds(fn, scan, (node) => { + if (node.kind === "assign" || node.kind === "forOf") ids.add(node.localId); + else if (node.kind === "tryCatch" && node.catchLocalId !== null) ids.add(node.catchLocalId); + else if (node.kind === "assignExpr" || node.kind === "incDec") ids.add(node.localId); + else if (node.kind === "closure" && node.captures.length) + sites.push({ owner: fn.name, fnName: node.fnName, captures: node.captures }); }); written.set(fn.name, ids); } diff --git a/packages/compiler/src/ir/final-fields.ts b/packages/compiler/src/ir/final-fields.ts index b0340cbaa..991339c6c 100644 --- a/packages/compiler/src/ir/final-fields.ts +++ b/packages/compiler/src/ir/final-fields.ts @@ -1,6 +1,7 @@ import { isRefCounted, type IrClassDef, type IrExpr, type IrModule, type IrStmt } from "./ir.js"; import { dynamicFieldStores, type DynamicFieldStores } from "./int32-slots.js"; import { everyExprChild, everyStmtChild } from "./traverse.js"; +import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "./node-index.js"; /** Whole-program final reference fields. * @@ -75,6 +76,9 @@ export function constructorClass(fnName: string): string | null { : null; } +/** The nodes that write a class field. */ +const FIELD_WRITE_KINDS: readonly IrNodeKind[] = ["fieldSet", "fieldIncDec"]; + /** `scalars` extends the analysis from reference-counted fields to fields of * every type, for consumers that read a final field's value once instead of * borrowing it (field-values.ts). */ @@ -82,6 +86,7 @@ export function analyzeFinalFields( mod: IrModule, scalars = false, stores?: DynamicFieldStores, + nodes?: IrNodeIndex, ): FinalFields { const classes = new Map((mod.classes ?? []).map((c) => [c.name, c])); const finals = new FinalFields(classes); @@ -121,30 +126,25 @@ export function analyzeFinalFields( } if (finals.size === 0) return finals; + const scan = nodes?.scan(FIELD_WRITE_KINDS); for (const fn of mod.functions) { const ctor = constructorClass(fn.name); const self = ctor !== null ? fn.params[0]?.localId : undefined; const selfLocal = self !== undefined ? fn.locals.find((l) => l.id === self) : undefined; // `this` must name the object under construction for the whole body. const constructing = selfLocal !== undefined && !selfLocal.boxed && !reassigns(fn.body, self!); - const stmt = (s: IrStmt): boolean => { - if (s.kind === "fieldSet") { - const family = finals.family(s.className, s.field); - if (family !== null && finals.isFinal(s.className, s.field)) { + eachNodeOfKinds(fn, scan, (node) => { + if (node.kind === "fieldSet") { + const family = finals.family(node.className, node.field); + if (family !== null && finals.isFinal(node.className, node.field)) { const own = - constructing && ctor !== null && inFamily(ctor, family) && isSelf(s.obj, self!); + constructing && ctor !== null && inFamily(ctor, family) && isSelf(node.obj, self!); if (!own) finals.drop(family, `written in ${fn.name}`); } - } - return everyStmtChild(s, expr, stmt); - }; - // `o.n++` updates a numeric field in place; reference fields never see it. - const expr = (e: IrExpr): boolean => { - if (e.kind === "fieldIncDec" && finals.isFinal(e.className, e.field)) - finals.drop(finals.family(e.className, e.field)!, `updated in ${fn.name}`); - return everyExprChild(e, expr, stmt); - }; - for (const s of fn.body) stmt(s); + } else if (node.kind === "fieldIncDec" && finals.isFinal(node.className, node.field)) + // `o.n++` updates a numeric field in place; reference fields never see it. + finals.drop(finals.family(node.className, node.field)!, `updated in ${fn.name}`); + }); } return finals; } diff --git a/packages/compiler/src/ir/function-references.test.ts b/packages/compiler/src/ir/function-references.test.ts index 45d046db9..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", diff --git a/packages/compiler/src/ir/function-references.ts b/packages/compiler/src/ir/function-references.ts index 0342172b6..db474e9bb 100644 --- a/packages/compiler/src/ir/function-references.ts +++ b/packages/compiler/src/ir/function-references.ts @@ -11,6 +11,9 @@ export function exprFunctionReference(expr: IrExpr): string | undefined { return undefined; } +/** The expression kinds exprFunctionReference answers for. */ +export const FUNCTION_REFERENCE_KINDS = ["closure", "orDefault"] as const; + /** Functions the module's metadata names: the entry, library exports and * the class prototype helpers the runtime calls. Class methods and * constructors follow the `%.` convention instead of being diff --git a/packages/compiler/src/ir/initializer-bindings.ts b/packages/compiler/src/ir/initializer-bindings.ts index cbda4241e..cb2cebc16 100644 --- a/packages/compiler/src/ir/initializer-bindings.ts +++ b/packages/compiler/src/ir/initializer-bindings.ts @@ -1,11 +1,27 @@ import type { IrExpr, IrFunction, IrGlobal, IrModule, IrStmt } from "./ir.js"; -import { everyExprChild, everyStmtChild, everyStmtList } from "./traverse.js"; +import { everyExprChild, everyStmtChild } from "./traverse.js"; +import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "./node-index.js"; + +/** The nodes findInitializerBindings reads: binding uses and captures. */ +const USE_KINDS: readonly IrNodeKind[] = [ + "varRef", + "assignExpr", + "incDec", + "closure", + "assign", + "varDecl", + "forOf", + "tryCatch", +]; /** Globals private to a guarded, synchronous initializer have local numeric * dataflow: no other function can replace their slots, and reentrant module * evaluation returns before reaching them. Keep the actual global storage * and lifetime; these facts are only inputs to numeric and extent analysis. */ -export function findInitializerBindings(mod: IrModule): ReadonlyMap { +export function findInitializerBindings( + mod: IrModule, + nodes?: IrNodeIndex, +): ReadonlyMap { const result = new Map(); // Embedders can reset library state between entries. Async initialization // and lifted captures also remain outside this synchronous proof. @@ -17,6 +33,7 @@ export function findInitializerBindings(mod: IrModule): ReadonlyMap { - if (e.kind === "varRef") use(e.localId, false); - if (e.kind === "assignExpr" || e.kind === "incDec") use(e.localId, true); - if (e.kind === "closure") for (const id of e.captures) excluded.add(id); - return true; - }, - stmt: (s) => { - if (s.kind === "assign" || s.kind === "varDecl" || s.kind === "forOf") use(s.localId, true); - if (s.kind === "tryCatch" && s.catchLocalId) excluded.add(s.catchLocalId); - return true; - }, + eachNodeOfKinds(fn, scan, (node) => { + if (node.kind === "varRef") use(node.localId, false); + else if (node.kind === "assignExpr" || node.kind === "incDec") use(node.localId, true); + else if (node.kind === "closure") for (const id of node.captures) excluded.add(id); + else if (node.kind === "assign" || node.kind === "varDecl" || node.kind === "forOf") + use(node.localId, true); + else if (node.kind === "tryCatch" && node.catchLocalId) excluded.add(node.catchLocalId); }); } const guarded = new Set(); diff --git a/packages/compiler/src/ir/int32-arrays.ts b/packages/compiler/src/ir/int32-arrays.ts index 02de7268f..353755c05 100644 --- a/packages/compiler/src/ir/int32-arrays.ts +++ b/packages/compiler/src/ir/int32-arrays.ts @@ -1,3 +1,4 @@ +import { exprFunctionReference, moduleFunctionReferences } from "./function-references.js"; import { F64, type IrExpr, type IrFunction, type IrModule, type IrStmt } from "./ir.js"; type SwitchCase = Extract["cases"][number]; @@ -6,22 +7,29 @@ import { everyExprChild, everyStmtChild, everyStmtList, + mapExprChildren, + mapStmtChildren, transformExpr, transformStmt, transformStmtList, type IrTransform, } from "./traverse.js"; -/** Local number arrays the backend may store as int32 slots. +/** 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. + * A candidate is a `number[]` binding that only compiled element + * operations reach: a local of a synchronous function, a module global, or + * a parameter. Every write of a local or a global stores a fresh array (an + * array literal, or `new Array(n).fill(v)`); a parameter is never written. + * Every read of a candidate is the receiver of an element read, write, + * state test, `length`, `push` or `pop` (a discarded `pop` included), the + * array of an element-reader helper call, or the argument of a direct call + * whose parameter at that position is a candidate itself. A parameter is a + * candidate only when its function is reached by direct calls alone and + * every call passes a candidate there, so every binding that can hold one + * of these arrays is a candidate: 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 @@ -33,6 +41,18 @@ import { * calls are inlined first (rewriteCandidates), so no shared helper ever * receives an int32 array. */ +/** The candidate key of the binding `localId` as `fnName` reads it: + * `function\0local` for a local or parameter, `\0global` for a module + * global (`%g.` ids), which every function reads alike. */ +export function int32ArrayKey(fnName: string, localId: string): string { + return localId.startsWith("%g.") ? `\0${localId}` : `${fnName}\0${localId}`; +} + +/** The int32 element slot (int32-slots.ts) of a candidate key. */ +export function int32ArraySlot(key: string): string { + return key.startsWith("\0") ? `g:${key.slice(1)}` : `l:${key}`; +} + /** 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. */ @@ -202,10 +222,70 @@ function writesLocal(e: IrExpr, id: string): boolean { return !expr(e); } -/** The candidate locals of `fn` (see the module comment). */ -export function int32ArrayCandidates(fn: IrFunction, readers: ReadonlySet): Set { +/** Expression kinds an index can be built from without running any code. */ +const PURE_INDEX_KINDS: ReadonlySet = new Set([ + "varRef", + "numLit", + "boolLit", + "bin", + "unary", + "ternary", + "logical", + "arrayGet", + "arrayState", +]); + +/** Whether evaluating an index runs no code (no call, accessor or + * conversion): plain reads, literals and their operators only. */ +function pureIndex(e: IrExpr): boolean { + return PURE_INDEX_KINDS.has(e.kind) && everyExprChild(e, pureIndex, () => false); +} + +/** The parameters and globals a module's functions share as candidates, + * still assumed to qualify. */ +export interface SharedCandidates { + /** Candidate parameters, as `function\0local` keys. */ + params: ReadonlySet; + /** Candidate globals, as `%g.` ids. */ + globals: ReadonlySet; + /** Whether the parameter at `index` of `callee` is a candidate. */ + paramAt: (callee: string, index: number) => boolean; +} + +const NO_SHARED: SharedCandidates = { + params: new Set(), + globals: new Set(), + paramAt: () => false, +}; + +/** The candidates of `fn` (see the module comment): its own locals, its + * parameters in `shared.params`, and the globals of `shared.globals` that + * it reads only in candidate positions. `written` receives the globals it + * assigns fresh arrays. */ +export function int32ArrayCandidates( + fn: IrFunction, + readers: ReadonlySet, + shared: SharedCandidates = NO_SHARED, + written: Set = new Set(), +): Set { const result = new Set(); - if (fn.async || fn.generator) return result; + if (fn.async || fn.generator) { + // Globals read here leave the candidates. + for (const id of shared.globals) result.add(id); + everyStmtList(fn.body, { + expr: (e) => { + if ((e.kind === "varRef" || e.kind === "assignExpr") && result.has(e.localId)) + result.delete(e.localId); + return true; + }, + stmt: (s) => { + if ((s.kind === "varDecl" || s.kind === "assign") && result.has(s.localId)) + result.delete(s.localId); + return true; + }, + }); + return result; + } const captured = new Set( [...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId), ); @@ -217,11 +297,12 @@ export function int32ArrayCandidates(fn: IrFunction, readers: ReadonlySet(); + const allocated = new Set(); // A receiver position: the candidate is read there without escaping. const receiver = (e: IrExpr): void => { if (e.kind !== "varRef") expr(e); @@ -255,13 +336,28 @@ export function int32ArrayCandidates(fn: IrFunction, readers: ReadonlySet { + if (shared.paramAt(e.callee, index)) receiver(arg); + else expr(arg); + }); return true; } } @@ -272,7 +368,8 @@ export function int32ArrayCandidates(fn: IrFunction, readers: ReadonlySet | undefined, live: ReadonlySet): Set { + const result = new Set(); + if (ids) for (const id of ids) if (live.has(id)) result.add(id); + return result; +} + +/** The candidates of every function of `mod` that has any, only those + * whose keys are in `keep` when given. Parameters and globals are shared: + * starting from every one that may qualify, each pass drops the ones a + * function reads outside candidate positions, the parameters some call + * passes anything but a candidate, and the globals never assigned a fresh + * array, until a pass drops nothing. Functions in `redirected` keep their + * parameters: some of their calls reach another function. */ +export function moduleInt32ArrayCandidates( + mod: IrModule, + readers: ReadonlySet, + keep: ReadonlySet | null = null, + redirected: ReadonlySet = new Set(), +): Map> { + const kept = (key: string): boolean => keep === null || keep.has(key); + const arrayParams = new Map(); + for (const fn of mod.functions) { + // Class methods, constructors and runtime helpers (`%` names) keep + // their parameters. + if (fn.name.startsWith("%") || fn.async || fn.generator || redirected.has(fn.name)) continue; + const indexes: number[] = []; + fn.params.forEach((p, i) => { + if (p.type.kind === "array" && p.type.elem.kind === "f64") indexes.push(i); + }); + if (indexes.length > 0) arrayParams.set(fn.name, indexes); + } + let globals = new Set(); + for (const g of mod.globals ?? []) + if ( + g.type.kind === "array" && + g.type.elem.kind === "f64" && + g.initFlag === undefined && + kept(`\0${g.id}`) + ) + globals.add(g.id); + const result = new Map>(); + if (arrayParams.size === 0 && globals.size === 0) { + for (const fn of mod.functions) { + const candidates = int32ArrayCandidates(fn, readers); + for (const id of candidates) if (!kept(`${fn.name}\0${id}`)) candidates.delete(id); + if (candidates.size > 0) result.set(fn.name, candidates); + } + return result; + } + // One walk: the functions reached other than by direct calls, the direct + // calls of functions with number[] parameters, the globals each function + // reads or writes, and whether a module namespace object exists (its + // member reads can reach any global by name). + const reached = new Set(moduleFunctionReferences(mod)); + const calls: { caller: string; call: IrExpr & { kind: "call" } }[] = []; + const globalsIn = new Map>(); + let namespaces = false; + for (const fn of mod.functions) { + const used = new Set(); + const note = (id: string): void => { + if (globals.has(id)) used.add(id); + }; + everyStmtList(fn.body, { + expr: (e) => { + const name = exprFunctionReference(e); + if (name !== undefined) reached.add(name); + if (e.kind === "moduleNsRef") namespaces = true; + if (e.kind === "call" && arrayParams.has(e.callee)) + calls.push({ caller: fn.name, call: e }); + if (e.kind === "varRef" || e.kind === "assignExpr") note(e.localId); + return true; + }, + stmt: (s) => { + if (s.kind === "varDecl" || s.kind === "assign") note(s.localId); + return true; + }, + }); + if (used.size > 0) globalsIn.set(fn.name, used); + } + if (namespaces) { + globals = new Set(); + globalsIn.clear(); + } + const paramIds = new Map(); + let params = new Set(); + for (const fn of mod.functions) { + const indexes = arrayParams.get(fn.name); + if (!indexes || reached.has(fn.name)) continue; + paramIds.set( + fn.name, + fn.params.map((p) => p.localId), + ); + for (const i of indexes) { + const key = `${fn.name}\0${fn.params[i]!.localId}`; + if (kept(key)) params.add(key); + } + } + for (;;) { + const current = params; + const written = new Set(); + const liveGlobals = new Set(globals); + result.clear(); + for (const fn of mod.functions) { + const shared: SharedCandidates = { + params: current, + globals: liveIn(globalsIn.get(fn.name), globals), + paramAt: (callee, index) => { + const id = paramIds.get(callee)?.[index]; + return id !== undefined && current.has(`${callee}\0${id}`); + }, + }; + const candidates = int32ArrayCandidates(fn, readers, shared, written); + for (const id of shared.globals) if (!candidates.has(id)) liveGlobals.delete(id); + for (const id of candidates) + if (!shared.globals.has(id) && !kept(`${fn.name}\0${id}`)) candidates.delete(id); + if (candidates.size > 0) result.set(fn.name, candidates); + } + for (const id of liveGlobals) if (!written.has(id)) liveGlobals.delete(id); + const live = new Set( + [...current].filter((key) => { + const cut = key.indexOf("\0"); + return result.get(key.slice(0, cut))?.has(key.slice(cut + 1)) === true; + }), + ); + // A parameter needs a candidate at that position of every call; a call + // with fewer arguments passes undefined. + for (const { caller, call } of calls) { + const ids = paramIds.get(call.callee); + if (!ids) continue; + ids.forEach((id, i) => { + const key = `${call.callee}\0${id}`; + if (!live.has(key)) return; + const arg = call.args[i]; + const ok = + arg?.kind === "varRef" && + (arg.localId.startsWith("%g.") + ? liveGlobals.has(arg.localId) + : result.get(caller)?.has(arg.localId) === true); + if (!ok) live.delete(key); + }); + } + if (live.size === current.size && liveGlobals.size === globals.size) break; + params = live; + globals = liveGlobals; + } + return result; +} + +/** The candidates one array can reach together: a direct call's argument + * and the parameter it binds hold the same array, so their element facts + * stand or fall together. `group` maps each of `keys` to its group's keys + * (itself alone for most), and `globalReaders` each candidate global's + * key to the functions that read or write it. */ +export function int32ArrayGroups( + mod: IrModule, + keys: ReadonlySet, +): { group: Map; globalReaders: Map> } { + const parent = new Map(); + const root = (key: string): string => { + let r = key; + while (parent.has(r)) r = parent.get(r)!; + if (r !== key) parent.set(key, r); + return r; + }; + const globalReaders = new Map>(); + const params = new Map(mod.functions.map((fn) => [fn.name, fn.params])); + const paramKeys = new Set( + mod.functions.flatMap((fn) => fn.params.map((p) => `${fn.name}\0${p.localId}`)), + ); + if ([...keys].some((key) => key.startsWith("\0") || paramKeys.has(key))) { + for (const fn of mod.functions) { + const touch = (id: string): void => { + const key = int32ArrayKey(fn.name, id); + if (!key.startsWith("\0") || !keys.has(key)) return; + const set = globalReaders.get(key) ?? new Set(); + set.add(fn.name); + globalReaders.set(key, set); + }; + everyStmtList(fn.body, { + expr: (e) => { + if (e.kind === "varRef" || e.kind === "assignExpr") touch(e.localId); + if (e.kind === "call") + e.args.forEach((arg, i) => { + const param = params.get(e.callee)?.[i]; + if (arg.kind !== "varRef" || !param) return; + const a = int32ArrayKey(fn.name, arg.localId), + p = `${e.callee}\0${param.localId}`; + if (!keys.has(a) || !keys.has(p)) return; + const ra = root(a), + rp = root(p); + if (ra !== rp) parent.set(ra, rp); + }); + return true; + }, + stmt: (s) => { + if (s.kind === "varDecl" || s.kind === "assign") touch(s.localId); + return true; + }, + }); + } + } + const members = new Map(); + for (const key of keys) { + const r = root(key); + const list = members.get(r); + if (list) list.push(key); + else members.set(r, [key]); + } + const group = new Map(); + for (const key of keys) group.set(key, members.get(root(key))!); + return { group, globalReaders }; +} + /** The element values stored into each candidate: element writes, pushed * values, literal elements and fill values. */ export function int32ArrayStores( @@ -341,16 +656,34 @@ export function int32ArrayStores( return stores; } +const REPEATED_INDEX_OPS = new Set(["+", "-", "*", "&", "|", "^", "<<", ">>", ">>>"]); + +/** An index the inlined read may evaluate at each of its uses: a plain read, + * a literal, or integer arithmetic over them. Repeating it keeps the forms + * the backend derives from its operands at every use, such as the exact i64 + * of an int32 product, where a bound local holds only the double. */ +function repeatableIndex(e: IrExpr): boolean { + if (e.kind === "varRef" || e.kind === "numLit") return true; + if (e.kind === "unary") return e.op === "-" && repeatableIndex(e.operand); + return ( + e.kind === "bin" && + REPEATED_INDEX_OPS.has(e.op) && + repeatableIndex(e.left) && + repeatableIndex(e.right) + ); +} + /** 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. */ + * the same. An index that is not repeatable (repeatableIndex) 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, @@ -374,14 +707,13 @@ function rewriteCandidates( 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]; + if (!call || repeatableIndex(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 }]; @@ -444,21 +776,23 @@ function rewriteCandidates( if (ret.kind !== "return" || !ret.value) return e; let index = call.args[1]!; const stmts: IrStmt[] = []; - if (!simple(index)) { + if (!repeatableIndex(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 arguments' copies are not searched again: an index may read a + // local that has the id of one of the helper's parameters. + const substituteExpr = (x: IrExpr): IrExpr => { + if (x.kind !== "varRef") return mapExprChildren(x, substituteExpr, substituteStmt); + if (x.localId === arrayParam!.localId) return reparseExpr(array); + if (x.localId === indexParam!.localId) return reparseExpr(operand); + return x; + }; + const substituteStmt = (x: IrStmt): IrStmt => + mapStmtChildren(x, substituteExpr, substituteStmt); + const body = substituteExpr(reparseExpr(ret.value)); // The ternary is a fresh copy (reparseExpr): it takes the call's location. const typed = body; typed.loc = e.loc; @@ -471,40 +805,45 @@ function rewriteCandidates( return { ...fn, body, locals: locals.filter((local) => !popLocals.has(local.id)) }; } -/** Propose the module's int32 array candidates (`function\0local` keys in +/** Propose the module's int32 array candidates (int32ArrayKey keys in * `int32ArrayCandidates`), only those in `keep` when given, and rewrite * their functions. */ export function markInt32ArrayCandidates( mod: IrModule, keep: ReadonlySet | null = null, + redirected: ReadonlySet = new Set(), ): 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 all = moduleInt32ArrayCandidates(mod, new Set(readers.keys()), keep, redirected); + if (all.size === 0) return mod; + const keys = new Set(); 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); + const candidates = all.get(fn.name); + if (!candidates) return fn; + for (const id of candidates) keys.add(int32ArrayKey(fn.name, id)); + return rewriteCandidates(fn, candidates, readers); }); - return keys.length === 0 ? mod : { ...mod, functions, int32ArrayCandidates: keys }; + return { ...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. */ + * marked is kept. `redirected` names the functions some of whose calls the + * backend emits as calls of another function (a result-slot entry): their + * parameters keep doubles. */ export function selectInt32Arrays( mod: IrModule, prove: (marked: IrModule) => ReadonlySet, + redirected: (mod: IrModule) => ReadonlySet = () => new Set(), ): IrModule { let keep: ReadonlySet | null = null; + const others = redirected(mod); for (let round = 0; round < 4; round++) { - const marked = markInt32ArrayCandidates(mod, keep); + const marked = markInt32ArrayCandidates(mod, keep, others); if (marked === mod) return mod; const kept = prove(marked); const proposed = marked.int32ArrayCandidates ?? []; diff --git a/packages/compiler/src/ir/int32-slots.ts b/packages/compiler/src/ir/int32-slots.ts index 4a2abc5b0..85f373c89 100644 --- a/packages/compiler/src/ir/int32-slots.ts +++ b/packages/compiler/src/ir/int32-slots.ts @@ -8,10 +8,26 @@ import { type IntegerRanges, type IntegerSlotFacts, } from "./integer-ranges.js"; -import { exprFunctionReference, moduleFunctionReferences } from "./function-references.js"; +import { + exprFunctionReference, + FUNCTION_REFERENCE_KINDS, + moduleFunctionReferences, +} from "./function-references.js"; +import { + eachNodeOfKinds, + type IrKindScan, + type IrNodeIndex, + type IrNodeKind, +} from "./node-index.js"; import { findInitializerBindings, withInitializerBindings } from "./initializer-bindings.js"; -import { checkableInt32Allocations, int32ArrayStores } from "./int32-arrays.js"; -import { everyExprChild, everyStmtChild, everyStmtList, everyTypeChild } from "./traverse.js"; +import { + checkableInt32Allocations, + int32ArrayGroups, + int32ArrayKey, + int32ArraySlot, + int32ArrayStores, +} from "./int32-arrays.js"; +import { everyExprChild, everyStmtChild, everyTypeChild } from "./traverse.js"; /** The storage slot an array-valued expression reads, as an * IrContracts.int32Elements key: a local or parameter @@ -247,9 +263,10 @@ export class Int32Slots { /** 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. */ + /** Number arrays stored as int32 slots (int32ArrayKey keys: locals and + * parameters as `function\0local`, globals as `\0global`): the module's + * int32 array candidates whose every stored element, through every + * binding that can hold the same array, 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 @@ -391,6 +408,7 @@ const DYN_READ_ONLY = new Set([ "dyn.forInStable", "dyn.getOwnPropertyNames", "dyn.getPrototype", + "dyn.getPrototypeForIdentity", "dyn.hasKey", "dyn.hasOwn", "dyn.in", @@ -466,10 +484,61 @@ const DYN_READ_ONLY = new Set([ "text.decodeStream", "worker.new", ]); +/** The node kinds each walk below reads. */ +const LIB_CALL_KINDS: readonly IrNodeKind[] = ["libCall"]; +const CALL_FACT_KINDS: readonly IrNodeKind[] = [ + ...FUNCTION_REFERENCE_KINDS, + "virtualCall", + "instanceOfValue", + "varRef", + "assignExpr", + "incDec", + "classRef", + "varDecl", + "assign", + "forOf", +]; +const CENSUS_KINDS: readonly IrNodeKind[] = [ + "call", + "new", + "varRef", + "fieldGet", + "fieldIncDec", + "closure", + "classRef", + "incDec", + "forOf", + "tryCatch", +]; +const DECLARATION_KINDS: readonly IrNodeKind[] = ["varDecl"]; +const CANDIDATE_WRITE_KINDS: readonly IrNodeKind[] = [ + "assign", + "varDecl", + "fieldSet", + "return", + "call", + "new", + "assignExpr", +]; +const CALL_KINDS: readonly IrNodeKind[] = ["call", "new"]; +const SLOT_WRITE_KINDS: readonly IrNodeKind[] = ["return", "call", "new"]; +const NARROW_WRITE_KINDS: readonly IrNodeKind[] = ["assign", "varDecl", "fieldSet", "assignExpr"]; + +/** Visits the nodes of `fn` in walk order; with `scan`, only those of its + * kinds, read from the module's node index (the visitors must ignore the + * other kinds). */ function walkFunction( fn: IrFunction, + scan: IrKindScan | undefined, visit: { expr?: (e: IrExpr) => void; stmt?: (s: IrStmt) => void }, ): void { + if (scan !== undefined) { + eachNodeOfKinds(fn, scan, (node, statement) => { + if (statement) visit.stmt?.(node as IrStmt); + else visit.expr?.(node as IrExpr); + }); + return; + } const expr = (e: IrExpr): boolean => { visit.expr?.(e); return everyExprChild(e, expr, stmt); @@ -497,6 +566,7 @@ export function dynamicFieldStores( mod: IrModule, records = new Map((mod.records ?? []).map((r) => [r.id, r])), unions = new Map((mod.unions ?? []).map((u) => [u.id, u])), + nodes?: IrNodeIndex, ): DynamicFieldStores { const dynTypes = new Map(); const dynNamed = new Map(); @@ -536,9 +606,10 @@ export function dynamicFieldStores( let dynWritesAll = false; // A store whose key is not a literal names every field: later stores add // nothing, so the walk stops there. + const scan = nodes?.scan(LIB_CALL_KINDS); for (const fn of mod.functions) { if (dynWritesAll) break; - walkFunction(fn, { + walkFunction(fn, scan, { expr: (e) => { if (dynWritesAll) return; if ( @@ -641,7 +712,7 @@ export function dynamicallyStoredFamilies( } }; for (const fn of mod.functions) - walkFunction(fn, { + walkFunction(fn, undefined, { expr: (e) => { const dynResult = DYN_KINDS.has(e.type.kind); const boundary = BOUNDARY_KINDS.has(e.kind); @@ -681,6 +752,7 @@ export function functionCallFacts( mod: IrModule, classes: ReadonlyMap, subtree: (className: string) => string[], + nodes?: IrNodeIndex, ): { valueEscaped: Set; virtualTargets: Set; @@ -715,7 +787,7 @@ export function functionCallFacts( const count = (map: Map, id: string): void => { map.set(id, (map.get(id) ?? 0) + 1); }; - walkFunction(fn, { + walkFunction(fn, nodes?.scan(CALL_FACT_KINDS), { expr: (e) => { const referenced = exprFunctionReference(e); if (referenced !== undefined) escaped.add(referenced); @@ -751,9 +823,12 @@ export function functionCallFacts( export interface Int32SlotInputs { dynamicStores?: DynamicFieldStores; initializerBindings?: ReadonlyMap; + /** The module's node index, read instead of walking the bodies. */ + nodes?: IrNodeIndex; } export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): Int32Slots { + const nodes = inputs.nodes; const classes = new Map((mod.classes ?? []).map((c) => [c.name, c])); const slots = new Int32Slots(classes); if (mod.lib) return slots; @@ -779,17 +854,29 @@ 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. + // elements until a store the fixpoint cannot prove drops one, with every + // candidate the same array can reach (its group). const arrayCandidates = new Map>(); - slots.narrowedReads = (mod.int32ArrayCandidates?.length ?? 0) > 0; - for (const key of mod.int32ArrayCandidates ?? []) { + const globalArrays = new Set(); + const arrayKeys = new Set(mod.int32ArrayCandidates ?? []); + slots.narrowedReads = arrayKeys.size > 0; + for (const key of arrayKeys) { 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}`); + if (cut === 0) globalArrays.add(key.slice(1)); + else { + 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(int32ArraySlot(key)); } + const arrayGroups = arrayKeys.size > 0 ? int32ArrayGroups(mod, arrayKeys) : null; + // Functions that read or write a candidate global. + const globalArrayFunctions = new Set(); + if (arrayGroups) + for (const names of arrayGroups.globalReaders.values()) + for (const name of names) globalArrayFunctions.add(name); /* ── candidate fields ── */ const children = new Map(); @@ -843,7 +930,7 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): /* ── functions: escapes, parameter and return candidates ── */ const functions = new Map(mod.functions.map((fn) => [fn.name, fn])); - const calls = functionCallFacts(mod, classes, subtree); + const calls = functionCallFacts(mod, classes, subtree, nodes); const escaped = new Set([...calls.valueEscaped, ...calls.virtualTargets]); const virtual = calls.runtimeVirtual; @@ -900,7 +987,7 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): for (const capture of fn.classCaptures ?? []) unstableBoxes.add(boxKey(fn.name, capture.localId)); const read = new Set(); - walkFunction(fn, { + walkFunction(fn, nodes?.scan(CENSUS_KINDS), { expr: (e) => { if (e.kind === "call") { called.add(e.callee); @@ -1000,7 +1087,7 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): let ids = initializedDeclarations.get(fn.name); if (ids === undefined) { const found = new Set(); - walkFunction(fn, { + walkFunction(fn, nodes?.scan(DECLARATION_KINDS), { stmt: (s) => { if (s.kind === "varDecl" && s.init) found.add(s.localId); }, @@ -1068,28 +1155,46 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): } return false; }; + const candidateWrites = nodes?.scan(CANDIDATE_WRITE_KINDS); const hasCandidateWrite = (fn: IrFunction): boolean => { if (arrayCandidates.has(fn.name)) return true; + if (globalArrayFunctions.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, { - stmt: (s) => { - if (s.kind === "assign") return !box(s.localId); - if (s.kind === "varDecl") return s.init === null || !box(s.localId); - if (s.kind === "fieldSet") { - const family = slots.family(s.className, s.field); - return !family || !slots.hasFamily(family); + let found = false; + eachNodeOfKinds(fn, candidateWrites, (node) => { + switch (node.kind) { + case "assign": + found = box(node.localId); + break; + case "varDecl": + found = node.init !== null && box(node.localId); + break; + case "fieldSet": { + const family = slots.family(node.className, node.field); + found = !!family && slots.hasFamily(family); + break; } - return !(s.kind === "return" && returns); - }, - expr: (e) => { - if (e.kind === "call") return !candidateArgs(functions.get(e.callee), e.args.length, 0); - if (e.kind === "new") - return !candidateArgs(functions.get(`%${e.className}.constructor`), e.args.length, 1); - if (e.kind === "assignExpr") return !box(e.localId); - return true; - }, + case "return": + found = returns; + break; + case "call": + found = candidateArgs(functions.get(node.callee), node.args.length, 0); + break; + case "new": + found = candidateArgs( + functions.get(`%${node.className}.constructor`), + node.args.length, + 1, + ); + break; + case "assignExpr": + found = box(node.localId); + break; + } + return !found; }); + return found; }; const provenByFunction = new Map(); // Each function's parameter and result writes under its latest analysis: @@ -1106,7 +1211,7 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): 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)); + if (ranges.size > 0) lastWrites.set(fn.name, slotWrites(fn, ranges, functions, nodes)); else lastWrites.delete(fn.name); const proven: IrExpr[] = []; // The walk below acts only at writes into candidate slots. @@ -1124,25 +1229,49 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): // (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; + const locals = arrayCandidates.get(fn.name); + let arrays = locals; + if (globalArrayFunctions.has(fn.name)) { + const both = new Set(); + if (locals) for (const id of locals) both.add(id); + for (const id of globalArrays) both.add(id); + arrays = both; + } + if (arrays && arrayGroups) { + const requeue = (name: string): void => { + if (queued.has(name)) return; + queued.add(name); + queue.push(name); + }; + const params = new Set(fn.params.map((param) => param.localId)); 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 key = int32ArrayKey(fn.name, id); + if (!slots.hasInt32ElementSlot(int32ArraySlot(key)) || values.every(int32)) continue; + // The array's group drops together: every function reading a + // member is analyzed again without its facts. + const group = arrayGroups.group.get(key) ?? [key]; + for (const member of group) { + slots.dropInt32Elements(int32ArraySlot(member)); + if (member.startsWith("\0")) { + const names = arrayGroups.globalReaders.get(member); + if (names) for (const name of names) requeue(name); + } else requeue(member.slice(0, member.indexOf("\0"))); + } + // Checked stores serve a lone local: its allocations are the only + // arrays it holds. 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); + if ( + group.length === 1 && + !key.startsWith("\0") && + !params.has(id) && + values.every(checkable) && + checkableInt32Allocations(fn, id) + ) + slots.checkedInt32Arrays.add(key); + else for (const member of group) reason(`array:${member}`, "an element store is not int32"); } } // An int32 written into a slot still assumed non-negative: widen the @@ -1181,7 +1310,7 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): if (int32(value)) settle(`box:${group}`, value); else if (dropBox(group, `write in ${fn.name} is not int32`)) invalidate(`box:${group}`); }; - walkFunction(fn, { + walkFunction(fn, nodes?.scan(CANDIDATE_WRITE_KINDS), { stmt: (s) => { if (s.kind === "assign") boxWrite(s.localId, s.value); else if (s.kind === "varDecl" && s.init) boxWrite(s.localId, s.init); @@ -1213,14 +1342,25 @@ 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}`), + for (const key of arrayKeys) + if (slots.hasInt32ElementSlot(int32ArraySlot(key))) slots.int32Arrays.add(key); + const ranged = rangedParameters( + slots, + functions, + retry, + numeric, + (callee) => readers.get(`ret:${callee}`), + nodes, + ); + narrowSlotRanges( + slots, + functions, + numeric, + lastWrites, + ranged, + (slot) => readers.get(slot), + nodes, ); - narrowSlotRanges(slots, functions, numeric, lastWrites, ranged, (slot) => readers.get(slot)); return slots; } @@ -1255,6 +1395,7 @@ function rangedParameters( retry: ReadonlySet, numeric: (fn: IrFunction) => IrFunction, callersOf: (callee: string) => ReadonlySet | undefined, + nodes: IrNodeIndex | undefined, ): Set { const changed = new Set(); if (retry.size === 0) return changed; @@ -1324,7 +1465,7 @@ function rangedParameters( contribute(callee, param.localId, ranges.get(arg) ?? null); }); }; - walkFunction(fn, { + walkFunction(fn, nodes?.scan(CALL_KINDS), { expr: (e) => { if (e.kind === "call") args(functions.get(e.callee), e.args, 0); else if (e.kind === "new") args(functions.get(`%${e.className}.constructor`), e.args, 1); @@ -1370,6 +1511,7 @@ function slotWrites( fn: IrFunction, ranges: IntegerRanges, functions: ReadonlyMap, + nodes: IrNodeIndex | undefined, ): Map { const writes = new Map(); const write = (key: string, value: IrExpr | null): void => { @@ -1384,7 +1526,7 @@ function slotWrites( if (param) write(`param:${callee.name}\0${param.localId}`, arg); }); }; - walkFunction(fn, { + walkFunction(fn, nodes?.scan(SLOT_WRITE_KINDS), { stmt: (s) => { if (s.kind === "return") write(`ret:${fn.name}`, s.value); }, @@ -1427,6 +1569,7 @@ function narrowSlotRanges( lastWrites: ReadonlyMap>, changed: ReadonlySet, readersOf: (slot: string) => ReadonlySet | undefined, + nodes: IrNodeIndex | undefined, ): void { // Every parameter and result slot with a fact, and that fact. const proven = new Map(); @@ -1485,7 +1628,7 @@ function narrowSlotRanges( const level = slots.isNonNegative(`box:${group}`) ? NON_NEGATIVE_INT32_RANGE : INT32_RANGE; if (!rangeWithin(range, level)) sound = false; }; - walkFunction(fn, { + walkFunction(fn, nodes?.scan(NARROW_WRITE_KINDS), { stmt: (s) => { if (s.kind === "assign") box(s.localId, s.value); else if (s.kind === "varDecl" && s.init) box(s.localId, s.init); @@ -1496,7 +1639,7 @@ function narrowSlotRanges( if (e.kind === "assignExpr") box(e.localId, e.value); }, }); - return { writes: slotWrites(fn, ranges, functions), sound }; + return { writes: slotWrites(fn, ranges, functions, nodes), sound }; }; const writesByFunction = new Map(lastWrites); const join = (): Map => { 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 f54f6618b..d6942718a 100644 --- a/packages/compiler/src/ir/integer-loops.ts +++ b/packages/compiler/src/ir/integer-loops.ts @@ -382,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; @@ -412,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); @@ -485,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.test.ts b/packages/compiler/src/ir/integer-ranges.test.ts index fcb040275..5ae04d28f 100644 --- a/packages/compiler/src/ir/integer-ranges.test.ts +++ b/packages/compiler/src/ir/integer-ranges.test.ts @@ -440,6 +440,37 @@ test("int32-closed locals keep the int32 range across loops and switches", () => expect(switched.get(afterSwitch)).toEqual(INT32); }); +test("loop-carried locals written from Int32[] element reads keep the int32 range", () => { + const arrType = { kind: "array", elem: F64 } as const; + const loop = (localId: string): { after: IrExpr; f: IrFunction } => { + const after = ref(); + // `x = a[x]!` in a loop: a cursor that follows indices stored in the array. + const read: IrExpr = { + loc, + kind: "arrayGet", + arr: { loc, kind: "varRef", localId, type: arrType }, + index: ref(), + type: F64, + }; + const f = fn([ + assign(num(0)), + { + loc, + kind: "while", + cond: condition("<", ref("unknown"), num(1)), + body: [assign(read)], + }, + observe(after), + ]); + return { after, f }; + }; + const elements = { elements: (arr: IrExpr) => arr.kind === "varRef" && arr.localId === "ints" }; + const ints = loop("ints"); + expect(analyzeIntegerRanges(ints.f, elements).get(ints.after)).toEqual(INT32); + const floats = loop("floats"); + expect(analyzeIntegerRanges(floats.f, elements).get(floats.after) ?? null).toBeNull(); +}); + test("uint32-closed locals keep the uint32 range across loop back edges", () => { // for (let i = 0; i < n; i++) { v = unknown >>> 0; acc = (acc + v) >>> 0; ... } const loop = (start: IrExpr | null, writes: IrExpr[]) => { diff --git a/packages/compiler/src/ir/integer-ranges.ts b/packages/compiler/src/ir/integer-ranges.ts index 0e5ca426d..683ceab75 100644 --- a/packages/compiler/src/ir/integer-ranges.ts +++ b/packages/compiler/src/ir/integer-ranges.ts @@ -480,6 +480,7 @@ export function analyzeIntegerRangeFacts( return boxedRange(e.localId); } case "fieldGet": + case "arrayGet": case "call": return slotRange(e); default: diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index c9a316501..878088d65 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -1044,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). */ @@ -1697,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 @@ -3423,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; @@ -3872,8 +3898,10 @@ export type IrExpr = * not shape equality; missing or wrong-typed fields throw, EXCEPT that a * missing key for an undefined-armed union field — an optional field — * builds the interned undefined arm instead); arrays check every element; - * unions try arms in canonical order and the first FULL match wins (no - * match → throw; an undefined arm matches no dyn value); JSON null matches + * unions try arms in canonical order, except that record arms with more + * required fields and then more fields go first, and the first FULL match + * wins (no match → throw; an undefined arm matches no dyn value); JSON + * null matches * exactly the nullT arm of a union target (bare null targets cannot * exist). MAY THROW: * backends' may-throw analyses must treat it like a `throw` statement. @@ -3886,10 +3914,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/node-index.ts b/packages/compiler/src/ir/node-index.ts new file mode 100644 index 000000000..2aac7df92 --- /dev/null +++ b/packages/compiler/src/ir/node-index.ts @@ -0,0 +1,214 @@ +/* One walk over a module's function bodies, recorded for analyses that + * only collect facts from nodes of a few kinds. Each such analysis used to + * walk every body itself; reading the recorded kind codes instead visits + * the same nodes in the same order without recursing through the tree. */ +import { InternalCompilerError } from "../errors.js"; +import type { IrExpr, IrFunction, IrModule, IrStmt, IrType } from "./ir.js"; +import { everyStmtList, everyTypeChild } from "./traverse.js"; + +/** An expression or statement kind. The two sets of kinds are disjoint. */ +export type IrNodeKind = IrExpr["kind"] | IrStmt["kind"]; + +/** The code of a kind no node has: it matches no recorded node. */ +const ABSENT = 255; + +/** Every expression and statement of each function body of a module, in + * the order everyStmtList visits them (a node before its children), with + * a kind code per node and, per function, the set of kinds present. + * + * The bodies must not change while the index is in use. Its owner (the + * LLVM emitter) builds it before its analyses run; no later pass rewrites + * a recorded body in place, and a rewritten copy of a function is walked. */ +export class IrNodeIndex { + /** Every type object the typed module traversal (everyModuleNode) + * reaches, nested ones included, each once. */ + readonly types: readonly IrType[]; + private readonly nodes: (IrExpr | IrStmt)[] = []; + private readonly codes: Uint8Array; + private readonly kindCodes = new Map(); + /** By function object: an analysis may see a rewritten copy of a + * function under the same name, whose nodes are not recorded here. */ + private readonly functionIndex = new Map(); + /** nodes[starts[i]] .. nodes[starts[i + 1] - 1] belong to function i. */ + private readonly starts: Int32Array; + /** Per function, eight words of kind-present bits. */ + private readonly present: Uint32Array; + /** By the kind list (or excluded set) a scan was made from. */ + private readonly scans = new Map(); + /** By kind code: 1 for statement kinds. */ + private readonly statements = new Uint8Array(256); + + constructor(mod: IrModule) { + const nodes = this.nodes; + const types: IrType[] = []; + const seenTypes = new Set(); + // A type object's children never change: one visit covers its tree. + const addType = (type: IrType): boolean => { + if (seenTypes.has(type)) return true; + seenTypes.add(type); + types.push(type); + return everyTypeChild(type, addType); + }; + const statementKinds = new Set(); + const visitor = { + expr: (node: IrExpr): boolean => { + nodes.push(node); + addType(node.type); + return true; + }, + stmt: (node: IrStmt): boolean => { + nodes.push(node); + statementKinds.add(node.kind); + return true; + }, + }; + const functions = mod.functions; + this.starts = new Int32Array(functions.length + 1); + for (let f = 0; f < functions.length; f++) { + this.functionIndex.set(functions[f]!, f); + this.starts[f] = nodes.length; + everyStmtList(functions[f]!.body, visitor); + } + this.starts[functions.length] = nodes.length; + // The other type slots everyModuleNode visits. + for (const fn of functions) { + for (const param of fn.params) addType(param.type); + addType(fn.returnType); + for (const local of fn.locals) addType(local.type); + for (const capture of fn.captures ?? []) addType(capture.type); + for (const capture of fn.classCaptures ?? []) addType(capture.type); + if (fn.generator !== undefined) { + addType(fn.generator.yieldT); + addType(fn.generator.nextT); + addType(fn.generator.resultType); + } + } + for (const cls of mod.classes ?? []) { + for (const capture of cls.localCaptures ?? []) addType(capture.type); + for (const field of cls.fields) addType(field.type); + } + for (const global of mod.globals ?? []) addType(global.type); + for (const record of mod.records ?? []) { + for (const field of record.fields) addType(field.type); + if (record.indexValue !== undefined) addType(record.indexValue); + } + for (const union of mod.unions ?? []) for (const arm of union.arms) addType(arm); + this.types = types; + const codes = new Uint8Array(nodes.length); + const present = new Uint32Array(functions.length * 8); + let previousKind = ""; + let previousCode = ABSENT; + for (let f = 0; f < functions.length; f++) { + const end = this.starts[f + 1]!; + for (let i = this.starts[f]!; i < end; i++) { + const kind = nodes[i]!.kind; + if (kind !== previousKind) { + let code = this.kindCodes.get(kind); + if (code === undefined) { + code = this.kindCodes.size; + if (code >= ABSENT) throw new InternalCompilerError("more IR node kinds than codes"); + this.kindCodes.set(kind, code); + } + previousKind = kind; + previousCode = code; + } + codes[i] = previousCode; + present[f * 8 + (previousCode >>> 5)]! |= 1 << (previousCode & 31); + } + } + this.codes = codes; + this.present = present; + for (const [kind, code] of this.kindCodes) + if (statementKinds.has(kind)) this.statements[code] = 1; + } + + /** A scan for the nodes of `kinds`, for eachNodeOfKinds. Pass a + * constant list: scans are kept by the list's identity. */ + scan(kinds: readonly IrNodeKind[]): IrKindScan { + const known = this.scans.get(kinds); + if (known !== undefined) return known; + const codes: number[] = []; + for (const kind of kinds) { + const code = this.kindCodes.get(kind); + if (code !== undefined) codes.push(code); + } + return this.scanOf(kinds, codes); + } + + /** A scan for every expression. */ + expressions(): IrKindScan { + const known = this.scans.get(this.statements); + if (known !== undefined) return known; + const codes: number[] = []; + for (const code of this.kindCodes.values()) if (this.statements[code] === 0) codes.push(code); + return this.scanOf(this.statements, codes); + } + + /** A scan for the nodes of every kind but those in `excluded`. Pass a + * constant set: scans are kept by the set's identity. */ + scanExcept(excluded: ReadonlySet): IrKindScan { + const known = this.scans.get(excluded); + if (known !== undefined) return known; + const codes: number[] = []; + for (const [kind, code] of this.kindCodes) if (!excluded.has(kind)) codes.push(code); + return this.scanOf(excluded, codes); + } + + private scanOf(key: object, codes: readonly number[]): IrKindScan { + const table = new Uint8Array(256); + const words = new Uint32Array(8); + for (const code of codes) { + table[code] = 1; + words[code >>> 5]! |= 1 << (code & 31); + } + const scan = { index: this, table, words }; + this.scans.set(key, scan); + return scan; + } + + /** Calls `note` with each recorded node of `fn` whose kind `scan` names, + * in walk order, until it answers false. False (nothing called) when + * `fn` is not recorded. */ + visit(fn: IrFunction, scan: IrKindScan, note: IrNodeNote): boolean { + const f = this.functionIndex.get(fn); + if (f === undefined) return false; + let any = 0; + for (let w = 0; w < 8; w++) any |= this.present[f * 8 + w]! & scan.words[w]!; + if (any === 0) return true; + const { nodes, codes, statements } = this; + const table = scan.table; + const end = this.starts[f + 1]!; + for (let i = this.starts[f]!; i < end; i++) { + const code = codes[i]!; + if (table[code] !== 0 && note(nodes[i]!, statements[code] !== 0) === false) break; + } + return true; + } +} + +/** Receives a node and whether it is a statement; false stops the visit. */ +export type IrNodeNote = (node: IrExpr | IrStmt, statement: boolean) => boolean | void; + +/** The kinds one analysis reads, resolved against one index. */ +export interface IrKindScan { + readonly index: IrNodeIndex; + readonly table: Uint8Array; + readonly words: Uint32Array; +} + +/** Calls `note` for the nodes of `fn` in walk order (everyStmtList's) + * until it answers false: with `scan`, only those of its kinds, read from + * the index; without one (or for a function the index did not record), + * every node, by walking. `note` must ignore the kinds the scan does not + * name, so both answer the same. */ +export function eachNodeOfKinds( + fn: IrFunction, + scan: IrKindScan | undefined, + note: IrNodeNote, +): void { + if (scan !== undefined && scan.index.visit(fn, scan, note)) return; + everyStmtList(fn.body, { + expr: (node) => note(node, false) !== false, + stmt: (node) => note(node, true) !== false, + }); +} 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/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 85910369a..4ad3dcc5c 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -8,6 +8,7 @@ import { RUNTIME_STREAM_CLASSES, } from "./ir.js"; import { everyModuleNode } from "./traverse.js"; +import { eachNodeOfKinds, type IrNodeIndex } from "./node-index.js"; import { JsIcSites, hasJsIcSite } from "./js-ic.js"; const DYN_BUFFER_NUM_METHODS: ReadonlySet = new Set( @@ -219,6 +220,8 @@ const STREAM_LIB_FNS: ReadonlySet = new Set([ "stream.chunkDone", "stream.objectMode", "stream.decodeStrings", + "stream.optionValues", + "stream.checkEncodingOption", "stream.duplexFromGen", "stream.duplexFromDyn", "stream.duplexFromPromise", @@ -400,8 +403,10 @@ export function isDynUndefined(e: IrExpr): boolean { return e.kind === "dynFrom" && e.value.kind === "unitLit" && e.value.unit === "undefined"; } -export function moduleRuntimeFeatures(mod: IrModule): RuntimeFeatures { - return scanRuntimeFeatures(mod); +/** With `nodes` (the module's node index), the scan reads the recorded + * expressions and types instead of walking the module. */ +export function moduleRuntimeFeatures(mod: IrModule, nodes?: IrNodeIndex): RuntimeFeatures { + return scanRuntimeFeatures(mod, undefined, nodes); } /** The module's record shapes, unions and class layouts by id. */ @@ -858,6 +863,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 @@ -1081,6 +1090,7 @@ function noRuntimeFeatures(): RuntimeFeatures { stream: false, streamDyn: false, handleProps: false, + declAdopt: false, dynForAwait: false, dynBuffer: false, builtinValues: false, @@ -1121,7 +1131,11 @@ for (const name of RUNTIME_FEATURE_NAMES) { if (rule.expr) FEATURE_EXPR_TESTS.push(name); } -function scanRuntimeFeatures(mod: IrModule, stopAt?: RuntimeFeatureName): RuntimeFeatures { +function scanRuntimeFeatures( + mod: IrModule, + stopAt?: RuntimeFeatureName, + nodes?: IrNodeIndex, +): RuntimeFeatures { const features = noRuntimeFeatures(); const scans: { name: RuntimeFeatureName; scan: RuleScan }[] = []; for (const name of RUNTIME_FEATURE_NAMES) { @@ -1156,7 +1170,18 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: RuntimeFeatureName): Runtim for (const name of FEATURES_BY_TYPE.get(node.kind) ?? []) features[name] = true; return keepGoing(); }; - if (keepGoing()) everyModuleNode(mod, { expr, stmt: keepGoing, type }); + // Each condition is a fact about one expression or one type (the rule + // scans combine such facts in no particular order), so the index's + // expressions and distinct types answer as the walk does. An early stop + // (stopAt) keeps the walk. + if (nodes !== undefined && stopAt === undefined) { + const expressions = nodes.expressions(); + for (const fn of mod.functions) + eachNodeOfKinds(fn, expressions, (node) => { + expr(node as IrExpr); + }); + for (const t of nodes.types) type(t); + } else 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). diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 770fee6c6..a404c918a 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -120,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) @@ -4144,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" ) { @@ -5495,6 +5500,7 @@ function validateFunction( e.fn === "stream.destroyErr" || e.fn === "stream.objectMode" || e.fn === "stream.decodeStrings" || + e.fn === "stream.optionValues" || e.fn === "stream.duplexFromGen" || e.fn === "stream.duplexFromDyn" || e.fn === "stream.duplexifyReadable" || @@ -5505,6 +5511,7 @@ function validateFunction( e.fn === "stream.duplexFromGenSignal" || e.fn === "stream.duplexFromAsyncFnSignal" || e.fn === "stream.setConstruct" || + e.fn === "stream.replaceConstruct" || e.fn === "stream.setWritev" || e.fn === "stream.setWritevDefault" || e.fn === "stream.iteratorClose" || @@ -5867,6 +5874,7 @@ function validateFunction( e.fn === "readable.pushEncoding" || e.fn === "stream.objectMode" || e.fn === "stream.decodeStrings" || + e.fn === "stream.optionValues" || e.fn === "writable.writeEncoding" || e.fn === "writable.setDefaultEncoding" || e.fn === "stream.destroy" || 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/surface-manifest.json b/packages/compiler/surface-manifest.json index 81fb23ae2..e44f5d31e 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -612,9 +612,7 @@ "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, gcm, ecb, cfb, cfb8, cfb1, and ofb modes, des-ede3 and des-ede, chacha20, and chacha20-poly1305" + "status": "static" }, { "id": "node-builtin.crypto.getDiffieHellman", 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 index 0b68a5d3e..605f64e04 100644 --- a/packages/compiler/test/int32-element-array-emission.test.ts +++ b/packages/compiler/test/int32-element-array-emission.test.ts @@ -77,6 +77,19 @@ function literalCounter(n: number): number { for (let i = 0; i < n; i++) a[0] = a[0]! + 1; return a[0]!; } +function walk(n: number, steps: number): number { + const next: number[] = []; + for (let i = 0; i < n; i++) next.push(i + 1 < n ? i + 1 : 0); + let at = next[0]!; + let total = 0; + for (let s = 0; s < steps; s++) { + const t = next[at]!; + next[at] = at; + at = t; + total = (total + at) | 0; + } + return total; +} function joined(n: number): string { const a = [n | 0, 2]; return a.join(","); @@ -91,6 +104,7 @@ console.log( shortCounter(), literalCounter(n), joined(n), + walk(n | 0, 5), ); `; @@ -119,6 +133,16 @@ test("arrays storing only int32 elements get 4-byte slots", async () => { expect(counts).not.toContain("popArray"); }); +test("an array that stores a cursor read back from its own elements gets 4-byte slots", async () => { + const llvm = await emitLlvm(source); + const walk = functions(llvm, "walk"); + expect(walk).not.toBe(""); + // `at` is assigned `next[at]!` in the loop and stored back into `next`: + // it holds an int32 whenever the array's elements are int32s. + expect(walk).toMatch(/@scr_arr_new_small\(i32 7\)/); + expect(walk).toMatch(/getelementptr inbounds i32, ptr %\w+, i64 %\w+\n\s+%\w+ = load i32/); +}); + 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"]) { @@ -196,3 +220,42 @@ test("only the candidates the proof keeps are rewritten", async () => { for (const fn of selected.functions) if (fn.name !== "compose") expect(fn).toBe(mod.functions.find((f) => f.name === fn.name)); }); + +const sharedSource = ` +const data: number[] = []; +for (let i = 0; i < 64; i++) data.push((i * 7) & 255); +function fnv(values: number[]): number { + let h = 2166136261; + for (let i = 0; i < values.length; i++) h = Math.imul(h ^ values[i]!, 16777619) >>> 0; + return h; +} +function mean(values: number[]): number { + let s = 0; + for (let i = 0; i < values.length; i++) s += values[i]!; + return s / values.length; +} +function first(values: number[]): number { + return values[0]!; +} +const ints = [1, 2, 3]; +const halves = [0.5, 1.5]; +const pick: (values: number[]) => number = first; +console.log(fnv(data), mean(ints), mean(halves), pick([4])); +`; + +test("arrays passed to helpers and held in module globals get 4-byte slots", async () => { + const mod = await lowerIr(sharedSource); + const proposed = markInt32ArrayCandidates(mod).int32ArrayCandidates ?? []; + // A global and the parameter every call passes it to; a function used as + // a value keeps its parameter. + expect(proposed).toEqual(expect.arrayContaining(["\0%g.e.data", "fnv\0values.0"])); + expect(proposed.some((key) => key.startsWith("first\0"))).toBe(false); + const llvm = await emitLlvm(sharedSource); + // The helper reads int32 slots; `mean`, also called with fractions, + // reads doubles. + expect(functions(llvm, "fnv")).toMatch( + /getelementptr inbounds i32, ptr %\w+, i64 %\w+\n\s+%\w+ = load i32/, + ); + expect(functions(llvm, "mean")).not.toMatch(/getelementptr inbounds i32, ptr/); + expect(llvm).toMatch(/@scr_arr_new(_small)?\(i32 7/); +}); diff --git a/packages/compiler/test/js-class-layouts.test.ts b/packages/compiler/test/js-class-layouts.test.ts index c8491f3e8..110d007ee 100644 --- a/packages/compiler/test/js-class-layouts.test.ts +++ b/packages/compiler/test/js-class-layouts.test.ts @@ -101,6 +101,51 @@ delete loose.label; expect(layout(deleted, "Shape").tracks).toBe(true); }); +test("freezing values other than class instances keeps JavaScript class layouts fixed", async () => { + const module = await lower(`${SHAPES} +/** @param {{ value: any }} data */ +function settle(data) { + data.value = Object.freeze(data.value); + Object.seal(data); + return Object.preventExtensions(data.value); +} +settle({ value: { sides: 1 } }); +const square = new Square(); +Object.freeze(square); +`); + expect(layout(module, "Shape").tracks).toBe(false); + expect(layout(module, "Square").tracks).toBe(false); +}); + +test("accessors that share no field's name keep JavaScript class layouts fixed", async () => { + const module = await lower(` +class Path { + constructor(key) { + this.cache = []; + this.key = key; + } + get path() { + if (!this.cache.length) this.cache.push(this.key); + return this.cache; + } +} +class Named extends Path { + constructor(key) { + super(key); + this.label = "named"; + } + /** @param {string} value */ + set label2(value) { + this.label = value; + } +} +console.log(new Named("k").path.length); +`); + expect(layout(module, "Path")).toEqual({ fields: "cache:dyn key:dyn", tracks: false }); + expect(layout(module, "Named").tracks).toBe(false); + expect(layout(module, "Named").fields).toBe("cache:dyn key:dyn label:string"); +}); + test("JavaScript subclass fields keep their types when the base constructor only assigns fields", async () => { const module = await lower(` class Base { diff --git a/packages/compiler/test/native-codegen-integration.test.ts b/packages/compiler/test/native-codegen-integration.test.ts index 0fd5249d3..cecebb49c 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_v33"); + expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v37"); 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_v33"); + expect(undefinedSymbols).toContain("_scr_runtime_abi_v37"); }); test("helper and clang object paths retain the same outbound FFI C ABI", async () => { diff --git a/packages/compiler/test/node-index.test.ts b/packages/compiler/test/node-index.test.ts new file mode 100644 index 000000000..23fbc5203 --- /dev/null +++ b/packages/compiler/test/node-index.test.ts @@ -0,0 +1,157 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { beforeAll, expect, test } from "vitest"; +import { compile, deserializeModule } from "../src/index.js"; +import type { IrExpr, IrModule, IrStmt, IrType } from "../src/ir/ir.js"; +import { eachNodeOfKinds, IrNodeIndex, type IrNodeKind } from "../src/ir/node-index.js"; +import { everyModuleNode, everyStmtList } from "../src/ir/traverse.js"; + +async function lowerSource(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-node-index-")); + try { + const entry = join(dir, "main.ts"); + await writeFile(entry, source); + const outPath = join(dir, "main.ir.json"); + const result = await compile(entry, { outPath, outDir: dir, outputKind: "ir" }); + if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n")); + return deserializeModule(await readFile(outPath, "utf8")); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +let mod: IrModule; +let index: IrNodeIndex; + +beforeAll(async () => { + mod = await lowerSource(` +class Shape { + constructor(public readonly name: string, protected sides: number[]) {} + area(): number { return 0; } +} +class Square extends Shape { + constructor(private side: number) { super("square", [side, side, side, side]); } + override area(): number { return this.side * this.side; } +} +type Result = { ok: true; value: number } | { ok: false; error: string }; +function parse(text: string): Result { + const n = Number(text); + return Number.isNaN(n) ? { ok: false, error: "bad " + text } : { ok: true, value: n }; +} +function* count(limit: number): Generator { + for (let i = 0; i < limit; i++) yield i; +} +const totals = new Map(); +const seen = new Set(); +let calls = 0; +function record(key: string, value: number): void { + calls++; + totals.set(key, (totals.get(key) ?? 0) + value); + seen.add(key); +} +const adder = (k: number) => (x: number): number => x + k; +const inc = adder(1); +try { + for (const text of ["1", "x", "3"]) { + const r = parse(text); + if (r.ok) record("ok", inc(r.value)); + else throw new Error(r.error); + } +} catch (e) { + record("error", String(e).length); +} +let sum = 0; +for (const v of count(4)) sum += v; +const shapes: Shape[] = [new Square(2), new Shape("none", [])]; +console.log(sum, calls, [...totals], seen.size, shapes.map((s) => s.area()).join(",")); +`); + index = new IrNodeIndex(mod); +}); + +/** Every node everyStmtList visits in `body`, with whether it is a statement. */ +function walked(body: IrStmt[]): [IrExpr | IrStmt, boolean][] { + const out: [IrExpr | IrStmt, boolean][] = []; + everyStmtList(body, { + expr: (node) => (out.push([node, false]), true), + stmt: (node) => (out.push([node, true]), true), + }); + return out; +} + +function noted( + fn: IrModule["functions"][number], + scan: ReturnType | undefined, +): [IrExpr | IrStmt, boolean][] { + const out: [IrExpr | IrStmt, boolean][] = []; + eachNodeOfKinds(fn, scan, (node, statement) => { + out.push([node, statement]); + }); + return out; +} + +const NO_KINDS: ReadonlySet = new Set(); + +test("the index visits each body's nodes in everyStmtList's order", () => { + expect(mod.functions.length).toBeGreaterThan(5); + const all = index.scanExcept(NO_KINDS); + for (const fn of mod.functions) expect(noted(fn, all)).toEqual(walked(fn.body)); +}); + +const SOME_KINDS: readonly IrNodeKind[] = ["call", "varDecl", "closure", "return", "fieldGet"]; + +test("a scan visits only its kinds, in walk order, and the walk answers the same", () => { + const scan = index.scan(SOME_KINDS); + let visited = 0; + for (const fn of mod.functions) { + const expected = walked(fn.body).filter(([node]) => SOME_KINDS.includes(node.kind)); + expect(noted(fn, scan)).toEqual(expected); + // Without a scan, every node reaches `note`, as the walk visits them. + expect(noted(fn, undefined)).toEqual(walked(fn.body)); + visited += expected.length; + } + expect(visited).toBeGreaterThan(20); + expect(index.scan(SOME_KINDS)).toBe(scan); +}); + +test("a false answer stops the visit, with or without the index", () => { + const all = index.scanExcept(NO_KINDS); + for (const fn of mod.functions) { + for (const scan of [all, undefined]) { + let calls = 0; + eachNodeOfKinds(fn, scan, () => ++calls < 3); + expect(calls).toBe(Math.min(3, walked(fn.body).length)); + } + } +}); + +test("a function the index did not record is walked", () => { + const all = index.scanExcept(NO_KINDS); + for (const fn of mod.functions) { + const copy = { ...fn }; + expect(index.visit(copy, all, () => {})).toBe(false); + expect(noted(copy, all)).toEqual(walked(fn.body)); + } +}); + +test("the expression scan visits every expression", () => { + const expressions = index.expressions(); + for (const fn of mod.functions) + expect(noted(fn, expressions)).toEqual(walked(fn.body).filter(([, statement]) => !statement)); +}); + +test("the index's types are the type objects everyModuleNode visits, each once", () => { + const visited = new Set(); + everyModuleNode(mod, { + expr: () => true, + stmt: () => true, + type: (type) => (visited.add(type), true), + }); + expect(new Set(index.types)).toEqual(visited); + expect(index.types.length).toBe(visited.size); +}); + +test("a kind no node has matches nothing", () => { + const scan = index.scan(["regexLit"]); + for (const fn of mod.functions) expect(noted(fn, scan)).toEqual([]); +}); 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 a8620c1ce..9843739cd 100644 --- a/packages/compiler/test/package-shim-guard.test.ts +++ b/packages/compiler/test/package-shim-guard.test.ts @@ -51,6 +51,7 @@ describe("package shims compile without refusals", () => { expect(entries.map((e) => e.slice(shimRoot.length + 1))).toEqual([ "chalk/v5.ts", "fs-extra/index.ts", + "hono-node-server/v2.ts", "hot-shots/v8-unix-dgram.ts", "hot-shots/v8.ts", "undici/v5.ts", 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/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/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/7154-crypto-cipher-info.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7154-crypto-cipher-info.cjs.json new file mode 100644 index 000000000..65e5f162e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7154-crypto-cipher-info.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7154-crypto-cipher-info.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7155-cipher-aes-ccm.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7155-cipher-aes-ccm.cjs.json new file mode 100644 index 000000000..a52818c74 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7155-cipher-aes-ccm.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7155-cipher-aes-ccm.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7156-cipher-key-wrap.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7156-cipher-key-wrap.cjs.json new file mode 100644 index 000000000..f2473c303 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7156-cipher-key-wrap.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7156-cipher-key-wrap.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7157-crypto-cipher-info-typed.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7157-crypto-cipher-info-typed.ts.json new file mode 100644 index 000000000..340d6e7b1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7157-crypto-cipher-info-typed.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7157-crypto-cipher-info-typed.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/7264-array-dense-paired-loops.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7264-array-dense-paired-loops.ts.json new file mode 100644 index 000000000..8396d237c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7264-array-dense-paired-loops.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7264-array-dense-paired-loops.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7281-http-server-connect-tunnel.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7281-http-server-connect-tunnel.ts.json new file mode 100644 index 000000000..2328d508b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7281-http-server-connect-tunnel.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7281-http-server-connect-tunnel.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7282-http2-once-add-listener.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7282-http2-once-add-listener.ts.json new file mode 100644 index 000000000..96c879197 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7282-http2-once-add-listener.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7282-http2-once-add-listener.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7287-http-held-request-backpressure.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7287-http-held-request-backpressure.ts.json new file mode 100644 index 000000000..3a4358d78 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7287-http-held-request-backpressure.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7287-http-held-request-backpressure.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-cycle-deferred-folded-key/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7380-cycle-deferred-folded-key/main.ts.json new file mode 100644 index 000000000..18ef785c5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7380-cycle-deferred-folded-key/main.ts.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/7380-cycle-deferred-folded-key/util.ts", + "/tests/corpus/7380-cycle-deferred-folded-key/core.ts", + "/tests/corpus/7380-cycle-deferred-folded-key/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-cycle-deferred-reached-tdz/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-cycle-deferred-reached-tdz/main.ts.json new file mode 100644 index 000000000..65d9b19e9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-cycle-deferred-reached-tdz/main.ts.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/7381-cycle-deferred-reached-tdz/beta.ts", + "/tests/corpus/7381-cycle-deferred-reached-tdz/alpha.ts", + "/tests/corpus/7381-cycle-deferred-reached-tdz/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7382-cycle-static-this-tdz/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7382-cycle-static-this-tdz/main.ts.json new file mode 100644 index 000000000..986683684 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7382-cycle-static-this-tdz/main.ts.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/7382-cycle-static-this-tdz/beta.ts", + "/tests/corpus/7382-cycle-static-this-tdz/alpha.ts", + "/tests/corpus/7382-cycle-static-this-tdz/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7383-cycle-class-expression-name-tdz/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7383-cycle-class-expression-name-tdz/main.ts.json new file mode 100644 index 000000000..69b6d63bb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7383-cycle-class-expression-name-tdz/main.ts.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/7383-cycle-class-expression-name-tdz/beta.ts", + "/tests/corpus/7383-cycle-class-expression-name-tdz/alpha.ts", + "/tests/corpus/7383-cycle-class-expression-name-tdz/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7390-record-key-read-presence.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7390-record-key-read-presence.ts.json new file mode 100644 index 000000000..74bd7e30d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7390-record-key-read-presence.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7390-record-key-read-presence.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/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/7447-union-record-arm-width.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7447-union-record-arm-width.mjs.json new file mode 100644 index 000000000..68bd750d9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7447-union-record-arm-width.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7447-union-record-arm-width.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7448-union-record-arm-width.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7448-union-record-arm-width.ts.json new file mode 100644 index 000000000..498ee884a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7448-union-record-arm-width.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7448-union-record-arm-width.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7460-transform-writev.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7460-transform-writev.mjs.json new file mode 100644 index 000000000..1015e1354 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7460-transform-writev.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7460-transform-writev.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7461-transform-writev.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7461-transform-writev.cjs.json new file mode 100644 index 000000000..4d3c86215 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7461-transform-writev.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7461-transform-writev.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7462-transform-writev.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7462-transform-writev.ts.json new file mode 100644 index 000000000..9b42f2d60 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7462-transform-writev.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7462-transform-writev.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7463-stream-option-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7463-stream-option-values.ts.json new file mode 100644 index 000000000..5aa543dea --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7463-stream-option-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7463-stream-option-values.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7464-stream-option-values.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7464-stream-option-values.mjs.json new file mode 100644 index 000000000..2a438d418 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7464-stream-option-values.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7464-stream-option-values.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7465-transform-callback-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7465-transform-callback-values.ts.json new file mode 100644 index 000000000..17633d208 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7465-transform-callback-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7465-transform-callback-values.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7466-stream-construct-assign.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7466-stream-construct-assign.mjs.json new file mode 100644 index 000000000..f5bb52517 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7466-stream-construct-assign.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7466-stream-construct-assign.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7500-pipeline-callback-undefined.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7500-pipeline-callback-undefined.ts.json new file mode 100644 index 000000000..fe65b63f0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7500-pipeline-callback-undefined.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7500-pipeline-callback-undefined.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7501-pipeline-callback-undefined.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7501-pipeline-callback-undefined.mjs.json new file mode 100644 index 000000000..8d52980fe --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7501-pipeline-callback-undefined.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7501-pipeline-callback-undefined.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7502-readline-cursor-undefined.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7502-readline-cursor-undefined.ts.json new file mode 100644 index 000000000..6fd23042e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7502-readline-cursor-undefined.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7502-readline-cursor-undefined.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/7543-var-list-declarator-order.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7543-var-list-declarator-order.js.json new file mode 100644 index 000000000..d193e84ed --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7543-var-list-declarator-order.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7543-var-list-declarator-order.js" + ], + "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/7545-string-regex-checked-arguments.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7545-string-regex-checked-arguments.js.json new file mode 100644 index 000000000..03b3197c4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7545-string-regex-checked-arguments.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7545-string-regex-checked-arguments.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7548-cjs-receiverless-builtin-calls/main.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7548-cjs-receiverless-builtin-calls/main.cjs.json new file mode 100644 index 000000000..f3b8ba419 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7548-cjs-receiverless-builtin-calls/main.cjs.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/7548-cjs-receiverless-builtin-calls/lib.cjs", + "/tests/corpus/7548-cjs-receiverless-builtin-calls/main.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7549-cjs-minified-esmodule-stamp/main.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7549-cjs-minified-esmodule-stamp/main.cjs.json new file mode 100644 index 000000000..08adae885 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7549-cjs-minified-esmodule-stamp/main.cjs.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/7549-cjs-minified-esmodule-stamp/stamped.cjs", + "/tests/corpus/7549-cjs-minified-esmodule-stamp/module-stamped.cjs", + "/tests/corpus/7549-cjs-minified-esmodule-stamp/main.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7553-var-for-in-of-zero-iterations.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7553-var-for-in-of-zero-iterations.js.json new file mode 100644 index 000000000..9f470ca37 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7553-var-for-in-of-zero-iterations.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7553-var-for-in-of-zero-iterations.js" + ], + "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/7580-int32-arrays-across-calls.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7580-int32-arrays-across-calls.ts.json new file mode 100644 index 000000000..33282756c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7580-int32-arrays-across-calls.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7580-int32-arrays-across-calls.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/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/7620-js-class-view-descriptors.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7620-js-class-view-descriptors.js.json new file mode 100644 index 000000000..2cb1ece22 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7620-js-class-view-descriptors.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7620-js-class-view-descriptors.js" + ], + "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/7640-child-spawn-before-fast-exit.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7640-child-spawn-before-fast-exit.ts.json new file mode 100644 index 000000000..0b82f6091 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7640-child-spawn-before-fast-exit.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7640-child-spawn-before-fast-exit.ts" + ], + "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/corpus/7740-handle-method-values.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7740-handle-method-values.js.json new file mode 100644 index 000000000..ab7f75639 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7740-handle-method-values.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7740-handle-method-values.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7741-abort-signal-listener-value.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7741-abort-signal-listener-value.js.json new file mode 100644 index 000000000..c556e243e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7741-abort-signal-listener-value.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7741-abort-signal-listener-value.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7790-js-class-integrity-layouts.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7790-js-class-integrity-layouts.js.json new file mode 100644 index 000000000..526b86044 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7790-js-class-integrity-layouts.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7790-js-class-integrity-layouts.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7791-js-class-accessor-layouts.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7791-js-class-accessor-layouts.js.json new file mode 100644 index 000000000..5350539f0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7791-js-class-accessor-layouts.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7791-js-class-accessor-layouts.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-brand-probe-keys.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-brand-probe-keys.ts.json new file mode 100644 index 000000000..fd60e3ff9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-brand-probe-keys.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/class-brand-probe-keys.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fetch-headers-copy-cache.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fetch-headers-copy-cache.ts.json new file mode 100644 index 000000000..bd69651b1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fetch-headers-copy-cache.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/fetch-headers-copy-cache.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/split-cjs-require-binding/main.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/split-cjs-require-binding/main.cjs.json index c01617f80..60b9b0736 100644 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/split-cjs-require-binding/main.cjs.json +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/split-cjs-require-binding/main.cjs.json @@ -1,9 +1,9 @@ { "order": [ + "/tests/corpus/split-cjs-require-binding/lib.cjs", "/tests/corpus/split-cjs-require-binding/whole.cjs", "/tests/corpus/split-cjs-require-binding/reexport.cjs", "/tests/corpus/split-cjs-require-binding/reexport-extra.cjs", - "/tests/corpus/split-cjs-require-binding/lib.cjs", "/tests/corpus/split-cjs-require-binding/chain.cjs", "/tests/corpus/split-cjs-require-binding/chain-wrap.cjs", "/tests/corpus/split-cjs-require-binding/main.cjs" diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/split-cjs-require-cycle/main.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/split-cjs-require-cycle/main.cjs.json new file mode 100644 index 000000000..0ea3366d3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/split-cjs-require-cycle/main.cjs.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/split-cjs-require-cycle/odd.cjs", + "/tests/corpus/split-cjs-require-cycle/even.cjs", + "/tests/corpus/split-cjs-require-cycle/main.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/string-last-index-of-ascii.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/string-last-index-of-ascii.ts.json new file mode 100644 index 000000000..1d3107cd5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/string-last-index-of-ascii.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/string-last-index-of-ascii.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/split-cjs-require-cycle/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/split-cjs-require-cycle/main.js.json new file mode 100644 index 000000000..90d4a2353 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/split-cjs-require-cycle/main.js.json @@ -0,0 +1,19 @@ +{ + "order": [ + "/tests/diagnostics/split-cjs-require-cycle/b.js", + "/tests/diagnostics/split-cjs-require-cycle/main.js" + ], + "diags": [ + { + "code": "SC1016", + "message": "circular imports (/tests/diagnostics/split-cjs-require-cycle/main.js → /tests/diagnostics/split-cjs-require-cycle/b.js → /tests/diagnostics/split-cjs-require-cycle/main.js; the cycle closes through a require() edge: /tests/diagnostics/split-cjs-require-cycle/main.js uses its exports at /tests/diagnostics/split-cjs-require-cycle/main.js:10 outside the export statements that precede its first require, so a module of the cycle can observe them before they settle) are not supported yet", + "loc": { + "file": "/tests/diagnostics/split-cjs-require-cycle/main.js", + "start": 0, + "end": 0 + }, + "milestone": "later", + "hint": "ES-module cycles of let/const/function bindings compile as-is; rewrite the named construct (a named import instead of a namespace, a const instead of a default-export expression or var, a class declared before the code that can reach it) or break the named edge" + } + ] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/split-cjs-require-early/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/split-cjs-require-early/main.js.json new file mode 100644 index 000000000..a0a094bf5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/split-cjs-require-early/main.js.json @@ -0,0 +1,17 @@ +{ + "order": [ + "/tests/diagnostics/split-cjs-require-early/main.js" + ], + "diags": [ + { + "code": "SC1013", + "message": "require() binding 'lib' assigned below code that can reach it (Node reads undefined before the assignment runs, which is not modeled — move the assignment above the code that can run first) is not supported yet", + "loc": { + "file": "/tests/diagnostics/split-cjs-require-early/main.js", + "start": 396, + "end": 415 + }, + "milestone": "later" + } + ] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/stream-runtime-option-order.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/stream-runtime-option-order.ts.json new file mode 100644 index 000000000..40b64edac --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/stream-runtime-option-order.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/stream-runtime-option-order.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/7546-minified-esm-rules/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/7546-minified-esm-rules/main.ts.json new file mode 100644 index 000000000..d026fb402 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/7546-minified-esm-rules/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/7546-minified-esm-rules/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/7547-minified-cjs-chunks/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/7547-minified-cjs-chunks/main.ts.json new file mode 100644 index 000000000..212551ccd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/7547-minified-cjs-chunks/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/7547-minified-cjs-chunks/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/hono-node-server/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hono-node-server/main.ts.json new file mode 100644 index 000000000..58f450083 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hono-node-server/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/packages/compiler/shims/hono-node-server/v2.ts", + "/tests/fixtures/npm/cases/hono-node-server/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/types-boundary-prototype/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/types-boundary-prototype/main.ts.json new file mode 100644 index 000000000..2abe1bd8f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/types-boundary-prototype/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/types-boundary-prototype/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/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index e17312713..e492c36b4 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: 33, marker: "scr_runtime_abi_v33" }, + runtime_abi: { version: 37, marker: "scr_runtime_abi_v37" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index d0194be05..89b6eaf19 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -49,6 +49,8 @@ export const BASE_RUNTIME_SOURCES = [ "scr_itable.c", "scr_slots.c", "scr_alloc.c", + // crypto.getCipherInfo's table; it links only into programs that call it. + "scr_cipher_info.c", ]; export const OPTIONAL_RUNTIME_SOURCES = [ @@ -127,6 +129,7 @@ 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")], diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index 60ef9dc49..e22f2a37c 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: 33, marker: "scr_runtime_abi_v33" }, + runtime_abi: { version: 37, marker: "scr_runtime_abi_v37" }, compiler: { command: compiler, identity: compilerVersion, 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_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 df7543500..a70b10894 100644 --- a/packages/runtime/src/scr_child.c +++ b/packages/runtime/src/scr_child.c @@ -2780,7 +2780,8 @@ static void scr_child_settle(ScrChild *c) { scr_children_reffed_n--; } if (c->state == SCR_CHILD_EXITED) { - scr_child_stream_drain_now(c->out_stream); + if (!c->spawn_done) scr_child_fire_spawn(c); + if (!scr_exc_pending()) scr_child_stream_drain_now(c->out_stream); if (!scr_exc_pending()) scr_child_stream_drain_now(c->err_stream); if (!scr_exc_pending()) scr_ipc_settle(c->ipc); } @@ -5607,9 +5608,12 @@ static void scr_child_settle(ScrChild *c) { /* Streams first, settled after: an exit listener registered from a * draining 'data'/'end' callback still fires below, like Node's. The * SPAWN-FAILURE path skips the drain — Node fires 'error' first and - * the streams' immediate EOF delivers 'end' on the next turn. */ + * the streams' immediate EOF delivers 'end' on the next turn. A child + * spawned during this pass can exit before the next pass fires 'spawn'; + * Node emits 'spawn' before any data or 'exit', so it fires here first. */ if (c->state == SCR_CHILD_EXITED) { - scr_child_stream_drain_now(c->out_stream); + if (!c->spawn_done) scr_child_fire_spawn(c); + if (!scr_exc_pending()) scr_child_stream_drain_now(c->out_stream); if (!scr_exc_pending()) scr_child_stream_drain_now(c->err_stream); if (!scr_exc_pending()) scr_ipc_settle(c->ipc); } diff --git a/packages/runtime/src/scr_cipher.c b/packages/runtime/src/scr_cipher.c index 00d229bad..a2db11434 100644 --- a/packages/runtime/src/scr_cipher.c +++ b/packages/runtime/src/scr_cipher.c @@ -37,17 +37,23 @@ typedef enum { SC_CFB1, SC_OFB, SC_CHACHA20, + /* CCM and the key wraps stay last: sc_transform tests mode >= SC_CCM. */ + SC_CCM, + SC_WRAP, /* RFC 3394 AES key wrap */ + SC_WRAP_PAD, /* RFC 5649 AES key wrap with padding */ } ScCipherMode; typedef struct { const char *name; ScCipherMode mode; unsigned key_len; - bool des; /* triple DES (two- or three-key) rather than AES */ + bool des; /* triple DES (two- or three-key) rather than AES */ + bool inverse; /* key wrap with the inverse cipher (the -inv names) */ } ScCipherSpec; /* Node's names for the implemented ciphers, including OpenSSL's aliases. - * Lookup is case-insensitive, like OpenSSL's. */ + * Lookup is case-insensitive, like OpenSSL's, and scans in order, so the + * CCM and key wrap names follow the others. */ static const ScCipherSpec sc_specs[] = { {"aes-128-cbc", SC_CBC, 16}, {"aes-192-cbc", SC_CBC, 24}, {"aes-256-cbc", SC_CBC, 32}, {"aes128", SC_CBC, 16}, {"aes192", SC_CBC, 24}, {"aes256", SC_CBC, 32}, @@ -68,6 +74,38 @@ static const ScCipherSpec sc_specs[] = { {"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}, + {"aes-128-ccm", SC_CCM, 16}, {"aes-192-ccm", SC_CCM, 24}, {"aes-256-ccm", SC_CCM, 32}, + {"id-aes128-ccm", SC_CCM, 16}, {"id-aes192-ccm", SC_CCM, 24}, {"id-aes256-ccm", SC_CCM, 32}, + {"aes-128-wrap", SC_WRAP, 16, false, false}, + {"aes128-wrap", SC_WRAP, 16, false, false}, + {"id-aes128-wrap", SC_WRAP, 16, false, false}, + {"aes-128-wrap-inv", SC_WRAP, 16, false, true}, + {"aes128-wrap-inv", SC_WRAP, 16, false, true}, + {"aes-128-wrap-pad", SC_WRAP_PAD, 16, false, false}, + {"aes128-wrap-pad", SC_WRAP_PAD, 16, false, false}, + {"id-aes128-wrap-pad", SC_WRAP_PAD, 16, false, false}, + {"aes-128-wrap-pad-inv", SC_WRAP_PAD, 16, false, true}, + {"aes128-wrap-pad-inv", SC_WRAP_PAD, 16, false, true}, + {"aes-192-wrap", SC_WRAP, 24, false, false}, + {"aes192-wrap", SC_WRAP, 24, false, false}, + {"id-aes192-wrap", SC_WRAP, 24, false, false}, + {"aes-192-wrap-inv", SC_WRAP, 24, false, true}, + {"aes192-wrap-inv", SC_WRAP, 24, false, true}, + {"aes-192-wrap-pad", SC_WRAP_PAD, 24, false, false}, + {"aes192-wrap-pad", SC_WRAP_PAD, 24, false, false}, + {"id-aes192-wrap-pad", SC_WRAP_PAD, 24, false, false}, + {"aes-192-wrap-pad-inv", SC_WRAP_PAD, 24, false, true}, + {"aes192-wrap-pad-inv", SC_WRAP_PAD, 24, false, true}, + {"aes-256-wrap", SC_WRAP, 32, false, false}, + {"aes256-wrap", SC_WRAP, 32, false, false}, + {"id-aes256-wrap", SC_WRAP, 32, false, false}, + {"aes-256-wrap-inv", SC_WRAP, 32, false, true}, + {"aes256-wrap-inv", SC_WRAP, 32, false, true}, + {"aes-256-wrap-pad", SC_WRAP_PAD, 32, false, false}, + {"aes256-wrap-pad", SC_WRAP_PAD, 32, false, false}, + {"id-aes256-wrap-pad", SC_WRAP_PAD, 32, false, false}, + {"aes-256-wrap-pad-inv", SC_WRAP_PAD, 32, false, true}, + {"aes256-wrap-pad-inv", SC_WRAP_PAD, 32, false, true}, }; /* Ciphers OpenSSL 3 keeps in its legacy provider, which Node 24 does not @@ -139,7 +177,8 @@ typedef struct ScCipher { int tag_state; unsigned char tag[16]; unsigned char block[16]; /* CBC: chaining value; CTR: counter block; CFB, OFB: - shift register; chacha20: the IV */ + shift register; chacha20, CCM: the IV; key wrap: + the integrity check value */ unsigned char buf[16]; /* CBC, ECB: buffered input; CTR: keystream block */ size_t bufsz; /* CBC, ECB: buffered bytes; CTR, CFB, OFB: keystream offset */ @@ -152,6 +191,19 @@ typedef struct ScCipher { uint64_t aad_len; /* chacha20-poly1305 AAD bytes */ uint64_t text_len; /* chacha20-poly1305 data bytes */ bool aad_open; /* AAD was added since the last data or padding */ + /* CCM: OpenSSL's ccm128 state (the counter block, whose first byte holds + * the L and M parameters and the Adata flag, and the CBC-MAC), its + * provider's flags, and Node's one-update rule. A decipher's expected + * tag is `tag`. */ + size_t ccm_iv_len; + unsigned char ccm_nonce[16]; + unsigned char ccm_cmac[16]; + bool ccm_iv_set, ccm_len_set, ccm_tag_set; + bool ccm_updated; /* a decipher's one update() ran; key wrap: the one update() */ + bool ccm_auth_failed; /* that update failed to authenticate */ + bool ccm_verified; /* a decipher's tag matched the one it computed */ + double ccm_max_message; + bool wrap_decrypt_block; /* key wrap: the AES key schedule decrypts */ ScrStr *decoder; /* the StringDecoder encoding once string output starts */ double pending; /* the decoder's carried bytes */ /* The Transform of a cipher used as a stream (+1) and its release @@ -513,6 +565,10 @@ static size_t sc_iv_length(ScCipherMode mode, bool des) { return 12; case SC_CHACHA20: return 16; + case SC_WRAP: + return 8; + case SC_WRAP_PAD: + return 4; default: return des ? 8 : 16; } @@ -530,9 +586,9 @@ static void sc_unsupported_cipher(const ScrStr *name) { 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, 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"); + "aes-256 in cbc, ctr, gcm, ccm, ecb, cfb, cfb8, cfb1 and ofb modes and their wrap, " + "wrap-pad, wrap-inv and wrap-pad-inv key wraps, 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); @@ -618,7 +674,10 @@ static ScrDyn *sc_cipher_new(bool decrypt, ScrDyn *cipher, ScrDyn *key, ScrDyn * goto done; } 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)) { + bool iv_ok = spec->mode == SC_GCM ? iv_len > 0 + : spec->mode == SC_CCM ? iv_len >= 7 && iv_len <= 13 + : iv_len == expected_iv && (expected_iv == 0 || iv_bytes); + if (!iv_ok) { sc_throw(SCR_ERR_TYPE, "ERR_CRYPTO_INVALID_IV", "Invalid initialization vector"); goto done; } @@ -646,6 +705,24 @@ static ScrDyn *sc_cipher_new(bool decrypt, ScrDyn *cipher, ScrDyn *key, ScrDyn * sc_invalid_tag((unsigned long)tag_option); goto done; } + } else if (spec->mode == SC_CCM) { + /* InitAuthenticated: CCM needs the tag length, which OpenSSL takes when + * it is even and 4 to 16 bytes. */ + if (tag_option < 0) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, "authTagLength required for "); + scr_jb_put_str(&b, cipher->v.str); + ScrStr *message = scr_jb_finish(&b); + scr_throw_error_msg_code(SCR_ERR_TYPE, message->data, message->len, "ERR_CRYPTO_INVALID_AUTH_TAG"); + scr_str_release(message); + goto done; + } + if (tag_option < 4 || tag_option > 16 || (unsigned long)tag_option % 2 != 0) { + sc_invalid_tag((unsigned long)tag_option); + goto done; + } + tag_len = (int)tag_option; } if (key_bytes->len != spec->key_len) { sc_throw(SCR_ERR_RANGE, "ERR_CRYPTO_INVALID_KEYLEN", "Invalid key length"); @@ -709,6 +786,29 @@ static ScrDyn *sc_cipher_new(bool decrypt, ScrDyn *cipher, ScrDyn *key, ScrDyn * rc = mbedtls_aes_setkey_enc(&c->aes, key_bytes->data, bits); memcpy(c->block, iv_bytes->data, 16); break; + case SC_WRAP: + case SC_WRAP_PAD: + /* SP 800-38F: wrapping is the forward transformation (encryption) and + * unwrapping the inverse, and the -inv ciphers swap the two. The IV is + * the wrap's integrity check value. */ + rc = decrypt != spec->inverse ? mbedtls_aes_setkey_dec(&c->aes, key_bytes->data, bits) + : mbedtls_aes_setkey_enc(&c->aes, key_bytes->data, bits); + c->wrap_decrypt_block = decrypt != spec->inverse; + memcpy(c->block, iv_bytes->data, iv_len); + break; + case SC_CCM: { + /* CRYPTO_ccm128_init with M = the tag length and L = 15 - the IV + * length; Node caps the message at what a 12- or 13-byte IV's length + * field holds, and at INT_MAX otherwise. */ + rc = mbedtls_aes_setkey_enc(&c->aes, key_bytes->data, bits); + unsigned L = 15 - (unsigned)iv_len; + c->ccm_nonce[0] = (unsigned char)(((L - 1) & 7) | ((((unsigned)tag_len - 2) / 2) & 7) << 3); + memcpy(c->block, iv_bytes->data, iv_len); + c->ccm_iv_len = iv_len; + c->ccm_iv_set = true; + c->ccm_max_message = iv_len == 13 ? 65535 : iv_len == 12 ? 16777215 : 2147483647; + break; + } case SC_GCM: rc = mbedtls_gcm_setkey(&c->gcm, MBEDTLS_CIPHER_ID_AES, key_bytes->data, bits); if (rc == 0) @@ -970,7 +1070,12 @@ static bool sc_chacha_finish(ScCipher *c, unsigned char tag[16]) { /* The cipher output of an update over len bytes (+1 Buffer), or NULL * after a backend failure (for example AES-GCM's 2^36 - 32 byte message * limit) has retired the context and thrown Node's update error. */ +static ScrBytes *sc_ccm_update(ScCipher *c, const unsigned char *in, size_t len); +static ScrBytes *sc_wrap_update(ScCipher *c, const unsigned char *in, size_t len); + static ScrBytes *sc_transform(ScCipher *c, const unsigned char *in, size_t len) { + /* CCM and the key wraps, the last modes, take their own update. */ + if (c->mode >= SC_CCM) return c->mode == SC_CCM ? sc_ccm_update(c, in, len) : sc_wrap_update(c, in, len); size_t room = len + 16; ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, (double)room); out->is_buffer = true; @@ -1006,6 +1111,10 @@ static ScrBytes *sc_transform(ScCipher *c, const unsigned char *in, size_t len) ok = len == 0 || sc_chacha20_update(c, in, len, out->data); produced = len; break; + case SC_CCM: + case SC_WRAP: + case SC_WRAP_PAD: + break; } if (!ok) { mbedtls_platform_zeroize(out->data, room); @@ -1019,6 +1128,379 @@ static ScrBytes *sc_transform(ScCipher *c, const unsigned char *in, size_t len) return out; } +/* ── CCM ────────────────────────────────────────────────────────────── + * OpenSSL's ccm128 (crypto/modes/ccm128.c) over the AES block, step for + * step, with its provider's flags (ciphercommon_ccm.c) and Node's rules + * (one update, the message-length cap, a decipher's failure reported by + * final()), so every call sequence answers as Node's does. */ + +static void sc_ccm_block(ScCipher *c, const unsigned char in[16], unsigned char out[16]) { + mbedtls_aes_crypt_ecb(&c->aes, MBEDTLS_AES_ENCRYPT, in, out); +} + +/* CRYPTO_ccm128_setiv: the nonce and the message length. */ +static void sc_ccm_setiv(ScCipher *c, uint64_t mlen) { + unsigned L = c->ccm_nonce[0] & 7; + if (L >= 3) { + c->ccm_nonce[8] = (unsigned char)(mlen >> 56); + c->ccm_nonce[9] = (unsigned char)(mlen >> 48); + c->ccm_nonce[10] = (unsigned char)(mlen >> 40); + c->ccm_nonce[11] = (unsigned char)(mlen >> 32); + } else { + memset(c->ccm_nonce + 8, 0, 8); + } + c->ccm_nonce[12] = (unsigned char)(mlen >> 24); + c->ccm_nonce[13] = (unsigned char)(mlen >> 16); + c->ccm_nonce[14] = (unsigned char)(mlen >> 8); + c->ccm_nonce[15] = (unsigned char)mlen; + c->ccm_nonce[0] &= (unsigned char)~0x40; + memcpy(c->ccm_nonce + 1, c->block, 14 - L); +} + +/* CRYPTO_ccm128_aad. */ +static void sc_ccm_aad(ScCipher *c, const unsigned char *aad, size_t alen) { + if (alen == 0) return; + c->ccm_nonce[0] |= 0x40; + sc_ccm_block(c, c->ccm_nonce, c->ccm_cmac); + size_t i; + if (alen < 0x10000 - 0x100) { + c->ccm_cmac[0] ^= (unsigned char)(alen >> 8); + c->ccm_cmac[1] ^= (unsigned char)alen; + i = 2; + } else if ((uint64_t)alen >= (uint64_t)1 << 32) { + c->ccm_cmac[0] ^= 0xFF; + c->ccm_cmac[1] ^= 0xFF; + for (int k = 0; k < 8; k++) c->ccm_cmac[2 + k] ^= (unsigned char)((uint64_t)alen >> (56 - 8 * k)); + i = 10; + } else { + c->ccm_cmac[0] ^= 0xFF; + c->ccm_cmac[1] ^= 0xFE; + for (int k = 0; k < 4; k++) c->ccm_cmac[2 + k] ^= (unsigned char)((uint64_t)alen >> (24 - 8 * k)); + i = 6; + } + do { + for (; i < 16 && alen; ++i, ++aad, --alen) c->ccm_cmac[i] ^= *aad; + sc_ccm_block(c, c->ccm_cmac, c->ccm_cmac); + i = 0; + } while (alen); +} + +/* ctr64_inc: the counter block's low eight bytes, big-endian. */ +static void sc_ccm_ctr_inc(unsigned char *counter) { + for (int n = 15; n >= 8; n--) + if (++counter[n]) return; +} + +/* CRYPTO_ccm128_encrypt and CRYPTO_ccm128_decrypt: false (-1) for a + * length that differs from the one the counter block holds. */ +static bool sc_ccm_crypt(ScCipher *c, const unsigned char *in, unsigned char *out, size_t len, bool decrypt) { + unsigned char flags0 = c->ccm_nonce[0]; + unsigned char scratch[16]; + if (!(flags0 & 0x40)) sc_ccm_block(c, c->ccm_nonce, c->ccm_cmac); + unsigned L = flags0 & 7; + c->ccm_nonce[0] = (unsigned char)L; + uint64_t n = 0; + for (unsigned i = 15 - L; i < 15; ++i) { + n |= c->ccm_nonce[i]; + c->ccm_nonce[i] = 0; + n <<= 8; + } + n |= c->ccm_nonce[15]; + c->ccm_nonce[15] = 1; + if (n != (uint64_t)len) return false; + /* Each full block runs the counter block's encryption before the + * CBC-MAC's, in both directions, and XORs in 64-bit words, so the two + * AES operations of a block overlap. `out` may be `in`. */ + while (len >= 16) { + uint64_t data[2], mac[2], pad[2]; + sc_ccm_block(c, c->ccm_nonce, scratch); + sc_ccm_ctr_inc(c->ccm_nonce); + memcpy(data, in, 16); + memcpy(mac, c->ccm_cmac, 16); + memcpy(pad, scratch, 16); + pad[0] ^= data[0]; + pad[1] ^= data[1]; + /* The MAC covers the plaintext: the input when encrypting, the output + * when decrypting. */ + mac[0] ^= decrypt ? pad[0] : data[0]; + mac[1] ^= decrypt ? pad[1] : data[1]; + memcpy(out, pad, 16); + memcpy(c->ccm_cmac, mac, 16); + sc_ccm_block(c, c->ccm_cmac, c->ccm_cmac); + in += 16; + out += 16; + len -= 16; + } + if (len) { + if (decrypt) { + sc_ccm_block(c, c->ccm_nonce, scratch); + for (size_t i = 0; i < len; ++i) c->ccm_cmac[i] ^= (out[i] = (unsigned char)(scratch[i] ^ in[i])); + sc_ccm_block(c, c->ccm_cmac, c->ccm_cmac); + } else { + for (size_t i = 0; i < len; ++i) c->ccm_cmac[i] ^= in[i]; + sc_ccm_block(c, c->ccm_cmac, c->ccm_cmac); + sc_ccm_block(c, c->ccm_nonce, scratch); + for (size_t i = 0; i < len; ++i) out[i] = (unsigned char)(scratch[i] ^ in[i]); + } + } + for (unsigned i = 15 - L; i < 16; ++i) c->ccm_nonce[i] = 0; + sc_ccm_block(c, c->ccm_nonce, scratch); + for (int i = 0; i < 16; i++) c->ccm_cmac[i] ^= scratch[i]; + c->ccm_nonce[0] = flags0; + mbedtls_platform_zeroize(scratch, sizeof scratch); + return true; +} + +/* CRYPTO_ccm128_tag: the MAC, when the counter block still holds the + * tag length M (a failed call clears it). */ +static bool sc_ccm_tag(const ScCipher *c, unsigned char *tag) { + unsigned M = ((c->ccm_nonce[0] >> 3) & 7) * 2 + 2; + if (M != (unsigned)c->tag_len) return false; + memcpy(tag, c->ccm_cmac, M); + return true; +} + +/* The provider's ccm_cipher_internal for data (`out` given): the length + * is set by the first data unless setAAD set it; encryption marks the tag + * computed; decryption needs the tag and checks it, and on success clears + * the flags so a second call fails. */ +static bool sc_ccm_data(ScCipher *c, const unsigned char *in, unsigned char *out, size_t len) { + if (!c->ccm_iv_set) return false; + if (!c->ccm_len_set) { + sc_ccm_setiv(c, len); + c->ccm_len_set = true; + } + if (!c->decrypt) { + if (!sc_ccm_crypt(c, in, out, len, false)) return false; + c->ccm_tag_set = true; + return true; + } + if (!c->ccm_tag_set) return false; + unsigned char computed[16]; + bool ok = sc_ccm_crypt(c, in, out, len, true) && sc_ccm_tag(c, computed) && + sc_tag_equal(computed, c->tag, (size_t)c->tag_len); + mbedtls_platform_zeroize(computed, sizeof computed); + if (!ok) { + if (len) mbedtls_platform_zeroize(out, len); + return false; + } + c->ccm_iv_set = c->ccm_tag_set = c->ccm_len_set = false; + c->ccm_verified = true; + return true; +} + +static bool sc_ccm_length_ok(const ScCipher *c, double len) { + if (len <= c->ccm_max_message) return true; + sc_throw(SCR_ERR_RANGE, "ERR_CRYPTO_INVALID_MESSAGELEN", "Invalid message length"); + return false; +} + +/* update(): a decipher takes one, and its authentication failure leaves + * empty output for final() to report. A decipher authenticates even an + * empty message, as OpenSSL does: its tag still covers the nonce, the AAD + * and the length. A cipher's empty update is skipped, and final() then + * runs the empty message. */ +static ScrBytes *sc_ccm_update(ScCipher *c, const unsigned char *in, size_t len) { + if (!sc_ccm_length_ok(c, (double)len)) return NULL; + if (c->decrypt && c->ccm_updated) { + sc_throw(SCR_ERR_ERROR, NULL, "Trying to add data in unsupported state"); + return NULL; + } + ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, (double)(len ? len : 1)); + out->is_buffer = true; + bool ok = (len == 0 && !c->decrypt) || sc_ccm_data(c, len ? in : out->data, out->data, len); + if (c->decrypt) { + c->ccm_updated = true; + if (!ok) c->ccm_auth_failed = true; + out->len = ok ? len : 0; + return out; + } + if (!ok) { + mbedtls_platform_zeroize(out->data, len); + scr_bytes_release(out); + sc_throw(SCR_ERR_ERROR, NULL, "Trying to add data in unsupported state"); + return NULL; + } + out->len = len; + return out; +} + +/* final(): a decipher succeeds only when its update verified the tag; a + * cipher runs the provider's final (an empty message when no data was + * encrypted) and reads the tag, which retires the provider's state. */ +static bool sc_ccm_final(ScCipher *c) { + if (c->decrypt) return c->tag_state == SC_TAG_SET_BY_USER && c->ccm_verified; + unsigned char dummy = 0, sink = 0; + if (c->ccm_iv_set && !c->ccm_tag_set && !sc_ccm_data(c, &dummy, &sink, 0)) return false; + if (!c->ccm_tag_set || !sc_ccm_tag(c, c->tag)) return false; + c->tag_state = SC_TAG_COMPUTED; + c->ccm_tag_set = c->ccm_iv_set = c->ccm_len_set = false; + return true; +} + +/* setAAD(buffer, { plaintextLength }): the length goes first, as Node + * passes it to OpenSSL, then the AAD. False when the provider refuses. */ +static bool sc_ccm_set_aad(ScCipher *c, const ScrBytes *aad, double plaintext_length) { + if (!c->ccm_iv_set) return false; + sc_ccm_setiv(c, (uint64_t)plaintext_length); + c->ccm_len_set = true; + sc_ccm_aad(c, aad->data, aad->len); + return true; +} + +/* ── key wrap ───────────────────────────────────────────────────────── + * OpenSSL's CRYPTO_128_wrap, CRYPTO_128_unwrap and their RFC 5649 + * padded forms (crypto/modes/wrap128.c) over the key's AES transform, and + * Node's one-update rule: a failed update() leaves the cipher usable, an + * empty one counts, and final() without one fails. */ + +static void sc_wrap_block(ScCipher *c, const unsigned char in[16], unsigned char out[16]) { + mbedtls_aes_crypt_ecb(&c->aes, c->wrap_decrypt_block ? MBEDTLS_AES_DECRYPT : MBEDTLS_AES_ENCRYPT, in, out); +} + +static void sc_wrap_xor_t(unsigned char *a, size_t t) { + a[7] ^= (unsigned char)(t & 0xff); + if (t > 0xff) { + a[6] ^= (unsigned char)((t >> 8) & 0xff); + a[5] ^= (unsigned char)((t >> 16) & 0xff); + a[4] ^= (unsigned char)((t >> 24) & 0xff); + } +} + +/* CRYPTO_128_wrap: 0 unless the input is at least 16 bytes in 8-byte + * blocks. `out` may be `in`. */ +static size_t sc_wrap(ScCipher *c, const unsigned char *iv, unsigned char *out, const unsigned char *in, size_t inlen) { + if ((inlen & 7) || inlen < 16) return 0; + unsigned char b[16]; + size_t t = 1; + memmove(out + 8, in, inlen); + memcpy(b, iv, 8); + for (int j = 0; j < 6; j++) { + unsigned char *r = out + 8; + for (size_t i = 0; i < inlen; i += 8, t++, r += 8) { + memcpy(b + 8, r, 8); + sc_wrap_block(c, b, b); + sc_wrap_xor_t(b, t); + memcpy(r, b + 8, 8); + } + } + memcpy(out, b, 8); + mbedtls_platform_zeroize(b, sizeof b); + return inlen + 8; +} + +/* crypto_128_unwrap_raw: the plaintext into `out` and the recovered + * integrity check value into `aiv`. */ +static size_t sc_unwrap_raw(ScCipher *c, unsigned char aiv[8], unsigned char *out, const unsigned char *in, size_t inlen) { + inlen -= 8; + if ((inlen & 7) || inlen < 16) return 0; + unsigned char b[16]; + size_t t = 6 * (inlen >> 3); + memcpy(b, in, 8); + memmove(out, in + 8, inlen); + for (int j = 0; j < 6; j++) { + unsigned char *r = out + inlen - 8; + for (size_t i = 0; i < inlen; i += 8, t--, r -= 8) { + sc_wrap_xor_t(b, t); + memcpy(b + 8, r, 8); + sc_wrap_block(c, b, b); + memcpy(r, b + 8, 8); + } + } + memcpy(aiv, b, 8); + mbedtls_platform_zeroize(b, sizeof b); + return inlen; +} + +static size_t sc_unwrap(ScCipher *c, const unsigned char *iv, unsigned char *out, const unsigned char *in, size_t inlen) { + unsigned char got[8]; + size_t ret = sc_unwrap_raw(c, got, out, in, inlen); + if (!ret) return 0; + if (!sc_tag_equal(got, iv, 8)) { + mbedtls_platform_zeroize(out, ret); + return 0; + } + return ret; +} + +/* CRYPTO_128_wrap_pad: the 4-byte ICV and the 32-bit length, then the + * data padded with zeros to 8 bytes; one block is a single encryption. */ +static size_t sc_wrap_pad(ScCipher *c, const unsigned char *icv, unsigned char *out, const unsigned char *in, size_t inlen) { + size_t padded = (inlen + 7) / 8 * 8; + if (inlen == 0 || inlen >= ((size_t)1 << 31)) return 0; + unsigned char aiv[8]; + memcpy(aiv, icv, 4); + aiv[4] = (unsigned char)((inlen >> 24) & 0xFF); + aiv[5] = (unsigned char)((inlen >> 16) & 0xFF); + aiv[6] = (unsigned char)((inlen >> 8) & 0xFF); + aiv[7] = (unsigned char)(inlen & 0xFF); + if (padded == 8) { + memmove(out + 8, in, inlen); + memcpy(out, aiv, 8); + memset(out + 8 + inlen, 0, padded - inlen); + sc_wrap_block(c, out, out); + return 16; + } + memmove(out, in, inlen); + memset(out + inlen, 0, padded - inlen); + return sc_wrap(c, aiv, out, out, padded); +} + +/* CRYPTO_128_unwrap_pad: the ICV, the length within the last 8 bytes and + * zero padding must all check. */ +static size_t sc_unwrap_pad(ScCipher *c, const unsigned char *icv, unsigned char *out, const unsigned char *in, size_t inlen) { + if ((inlen & 7) || inlen < 16) return 0; + size_t n = inlen / 8 - 1, padded; + unsigned char aiv[8]; + if (inlen == 16) { + unsigned char buff[16]; + sc_wrap_block(c, in, buff); + memcpy(aiv, buff, 8); + memcpy(out, buff + 8, 8); + padded = 8; + mbedtls_platform_zeroize(buff, sizeof buff); + } else { + padded = inlen - 8; + if (sc_unwrap_raw(c, aiv, out, in, inlen) != padded) { + mbedtls_platform_zeroize(out, inlen); + return 0; + } + } + size_t plain = (size_t)aiv[4] << 24 | (size_t)aiv[5] << 16 | (size_t)aiv[6] << 8 | aiv[7]; + static const unsigned char zeros[8] = {0}; + if (!sc_tag_equal(aiv, icv, 4) || 8 * (n - 1) >= plain || plain > 8 * n || + !sc_tag_equal(out + plain, zeros, padded - plain)) { + mbedtls_platform_zeroize(out, padded); + return 0; + } + return plain; +} + +static ScrBytes *sc_wrap_update(ScCipher *c, const unsigned char *in, size_t len) { + if (c->ccm_updated) { + sc_throw(SCR_ERR_ERROR, NULL, "Trying to add data in unsupported state"); + return NULL; + } + ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, (double)(len + 16)); + out->is_buffer = true; + size_t produced = 0; + if (len > 0) { + bool pad = c->mode == SC_WRAP_PAD; + produced = !c->decrypt ? (pad ? sc_wrap_pad(c, c->block, out->data, in, len) : sc_wrap(c, c->block, out->data, in, len)) + : (pad ? sc_unwrap_pad(c, c->block, out->data, in, len) + : sc_unwrap(c, c->block, out->data, in, len)); + if (produced == 0) { + mbedtls_platform_zeroize(out->data, len + 16); + scr_bytes_release(out); + sc_throw(SCR_ERR_ERROR, NULL, "Trying to add data in unsupported state"); + return NULL; + } + } + c->ccm_updated = true; + out->len = produced; + return out; +} + /* ── output encodings ─────────────────────────────────────────────────── */ /* Node's getDecoder: the StringDecoder for this encoding, created on first @@ -1131,7 +1613,7 @@ static ScrDyn *sc_update(ScCipher *c, ScrDyn *data, ScrDyn *input_encoding, ScrD } static bool sc_aead(const ScCipher *c) { - return c->mode == SC_GCM || c->mode == SC_CHACHA; + return c->mode == SC_GCM || c->mode == SC_CHACHA || c->mode == SC_CCM; } /* final()'s output bytes (+1 Buffer), or NULL with the error thrown. */ @@ -1164,6 +1646,13 @@ static ScrBytes *sc_final_bytes(ScCipher *c) { case SC_OFB: case SC_CHACHA20: break; + case SC_CCM: + ok = sc_ccm_final(c); + break; + case SC_WRAP: + case SC_WRAP_PAD: + backend = c->ccm_updated; + break; case SC_GCM: { unsigned char full[16]; size_t ignored; @@ -1231,6 +1720,20 @@ static ScrDyn *sc_set_aad(ScCipher *c, ScrDyn *self, ScrDyn *buffer, ScrDyn *opt ScrBytes *aad = sc_buffer_or_view(buffer, "aadbuf", encoding); scr_dyn_release(encoding); if (!aad) return NULL; + if (c->mode == SC_CCM && c->active) { + bool ok = true; + if (plaintext_length < 0) { + sc_throw(SCR_ERR_TYPE, "ERR_MISSING_ARGS", "options.plaintextLength required for CCM mode with AAD"); + ok = false; + } else if (!sc_ccm_length_ok(c, plaintext_length)) { + ok = false; + } else if (!sc_ccm_set_aad(c, aad, plaintext_length)) { + sc_invalid_state("setAAD"); + ok = false; + } + scr_bytes_release(aad); + return ok ? scr_dyn_retain(self) : NULL; + } /* After data, GCM refuses any AAD and chacha20-poly1305 refuses * non-empty AAD. */ bool ok = c->active && sc_aead(c) && !(c->data_started && (c->mode == SC_GCM || aad->len > 0)); @@ -1314,6 +1817,7 @@ static ScrDyn *sc_set_auth_tag(ScCipher *c, ScrDyn *self, ScrDyn *buffer, ScrDyn c->tag_len = (int)len; memcpy(c->tag, tag->data, len); c->tag_state = SC_TAG_SET_BY_USER; + if (c->mode == SC_CCM) c->ccm_tag_set = true; scr_bytes_release(tag); return scr_dyn_retain(self); } diff --git a/packages/runtime/src/scr_cipher_info.c b/packages/runtime/src/scr_cipher_info.c new file mode 100644 index 000000000..c785724cb --- /dev/null +++ b/packages/runtime/src/scr_cipher_info.c @@ -0,0 +1,200 @@ +/* scriptc runtime — crypto.getCipherInfo(nameOrNid[, options]) over the + * table Node's OpenSSL ciphers generate (scr_cipher_info.h): cipher.js's + * argument checks, then GetCipherInfo's keyLength and ivLength rules and + * its info object, in Node's key order. Names match case-insensitively; + * nids are OpenSSL's. Arguments are borrowed. */ +#include "scr_runtime.h" + +#include +#include +#include +#include + +#include "scr_cipher_info.h" + +#define CI_COUNT(table) (sizeof(table) / sizeof((table)[0])) + +/* validateInt32(value, name): false with Node's error pending. */ +static bool ci_validate_int32(const char *name, const ScrDyn *value) { + if (value->kind != SCR_DYN_NUM) { + scr_dyn_arg_type_fail(name, "of type number", value); + return false; + } + double v = value->v.num; + bool integer = isfinite(v) && trunc(v) == v; + if (integer && v >= INT32_MIN && v <= INT32_MAX) return true; + char received[48]; + scr_num_received(v, received); + char text[192]; + int n = integer ? snprintf(text, sizeof text, + "The value of \"%s\" is out of range. It must be >= -2147483648 && <= 2147483647. " + "Received %s", + name, received) + : snprintf(text, sizeof text, + "The value of \"%s\" is out of range. It must be an integer. Received %s", name, + received); + scr_throw_error_msg_code(SCR_ERR_RANGE, text, n < 0 ? 0 : (size_t)n, "ERR_OUT_OF_RANGE"); + return false; +} + +/* A plain view of a statically typed argument (+1), or NULL after a + * throw. */ +static ScrDyn *ci_plain(ScrDyn *value) { + if (value->kind == SCR_DYN_TYPED_REF) return scr_dyn_typed_ref_materialize(value); + return scr_dyn_retain(value); +} + +static const ScCipherInfo *ci_by_name(const ScrStr *name) { + char lower[64]; + if (name->len == 0 || name->len >= sizeof lower) return NULL; + for (size_t i = 0; i < name->len; i++) { + unsigned char ch = (unsigned char)name->data[i]; + lower[i] = (char)(ch >= 'A' && ch <= 'Z' ? ch + 32 : ch); + } + lower[name->len] = '\0'; + size_t lo = 0, hi = CI_COUNT(sc_cipher_info_names); + while (lo < hi) { + size_t mid = lo + (hi - lo) / 2; + int cmp = strcmp(sc_cipher_info_names[mid].name, lower); + if (cmp == 0) return &sc_cipher_infos[sc_cipher_info_names[mid].info]; + if (cmp < 0) lo = mid + 1; + else hi = mid; + } + return NULL; +} + +static const ScCipherInfo *ci_by_nid(double nid) { + size_t lo = 0, hi = CI_COUNT(sc_cipher_info_nids); + while (lo < hi) { + size_t mid = lo + (hi - lo) / 2; + double at = sc_cipher_info_nids[mid].nid; + if (at == nid) return &sc_cipher_infos[sc_cipher_info_nids[mid].info]; + if (at < nid) lo = mid + 1; + else hi = mid; + } + return NULL; +} + +/* GetCipherInfo's option rules: a cipher that takes neither option (the + * legacy provider's) answers undefined for either; keyLength must equal + * the cipher's; ivLength must equal its IV length, or lie in 7 to 13 for + * CCM and 1 to 15 for OCB, and GCM takes any. */ +static bool ci_accepts(const ScCipherInfo *info, bool has_key, int key, bool has_iv, int iv) { + if ((has_key || has_iv) && (info->key_rule == SC_INFO_NONE || info->iv_rule == SC_INFO_NONE)) return false; + if (has_key && key != info->key_length) return false; + if (!has_iv) return true; + switch (info->iv_rule) { + case SC_INFO_CCM: return iv >= 7 && iv <= 13; + case SC_INFO_OCB: return iv >= 1 && iv <= 15; + case SC_INFO_ANY: return true; + default: return iv == (info->iv_length < 0 ? 0 : info->iv_length); + } +} + +static void ci_set_str(ScrDyn *obj, const char *key, const char *text) { + ScrStr *s = scr_str_new(text, strlen(text)); + ScrDyn *v = scr_dyn_new_str(s); + scr_str_release(s); + scr_dyn_obj_set(obj, key, strlen(key), v); +} + +static void ci_set_num(ScrDyn *obj, const char *key, double value) { + scr_dyn_obj_set(obj, key, strlen(key), scr_dyn_new_num(value)); +} + +/* The info object: mode, name, nid, blockSize (not for stream ciphers), + * ivLength (not when it is 0) and keyLength. */ +static ScrDyn *ci_object(const ScCipherInfo *info, int iv) { + ScrDyn *obj = scr_dyn_new_obj(); + if (info->mode) ci_set_str(obj, "mode", info->mode); + ci_set_str(obj, "name", info->name); + if (info->nid >= 0) ci_set_num(obj, "nid", info->nid); + if (info->block_size >= 0) ci_set_num(obj, "blockSize", info->block_size); + if (iv > 0) ci_set_num(obj, "ivLength", iv); + ci_set_num(obj, "keyLength", info->key_length); + return obj; +} + +/* Whether options is a class instance whose class defines keyLength or + * ivLength, which the runtime cannot read through the class's accessors. */ +static bool ci_class_option(const ScrDyn *options) { + if (options->kind != SCR_DYN_TYPED_REF) return false; + bool found = false; + static const char *const keys[] = {"keyLength", "ivLength"}; + for (size_t i = 0; i < 2 && !found; i++) { + ScrStr *key = scr_str_new(keys[i], strlen(keys[i])); + found = scr_dyn_typed_ref_class_has(options, key); + scr_str_release(key); + } + return found; +} + +/* An option as `const { keyLength, ivLength } = options` reads it: own and + * inherited properties, through getters. +1, or NULL with the getter's + * error pending. */ +static ScrDyn *ci_option(const ScrDyn *options, const char *key) { + if (options->kind == SCR_DYN_OBJ || options->kind == SCR_DYN_PROXY) + return scr_dyn_obj_read(options, key, strlen(key)); + return scr_dyn_retain((ScrDyn *)scr_dyn_undefined()); +} + +static ScrDyn *ci_get(const ScrDyn *name_or_nid, const ScrDyn *options, bool class_option) { + if (name_or_nid->kind != SCR_DYN_STR && name_or_nid->kind != SCR_DYN_NUM) { + scr_dyn_arg_type_fail("nameOrNid", "one of type string or number", name_or_nid); + return NULL; + } + if (name_or_nid->kind == SCR_DYN_NUM && !ci_validate_int32("nameOrNid", name_or_nid)) return NULL; + bool has_key = false, has_iv = false; + int key = 0, iv = 0; + if (options->kind != SCR_DYN_UNDEF) { + ScrStr *type = scr_dyn_typeof(options); + bool object = type->len == 6 && memcmp(type->data, "object", 6) == 0; + scr_str_release(type); + if (!object || options->kind == SCR_DYN_NULL || options->kind == SCR_DYN_ARR) { + scr_dyn_arg_type_fail("options", "of type object", options); + return NULL; + } + if (class_option) { + static const char message[] = "getCipherInfo options whose class defines keyLength or ivLength have no " + "lowering; pass an object with those properties"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + return NULL; + } + /* Both are read before either is validated, as in Node. */ + ScrDyn *key_value = ci_option(options, "keyLength"); + if (!key_value) return NULL; + ScrDyn *iv_value = ci_option(options, "ivLength"); + if (!iv_value) { + scr_dyn_release(key_value); + return NULL; + } + bool ok = true; + if (key_value->kind != SCR_DYN_UNDEF) { + ok = ci_validate_int32("options.keyLength", key_value); + has_key = true; + key = ok ? (int)key_value->v.num : 0; + } + if (ok && iv_value->kind != SCR_DYN_UNDEF) { + ok = ci_validate_int32("options.ivLength", iv_value); + has_iv = true; + iv = ok ? (int)iv_value->v.num : 0; + } + scr_dyn_release(key_value); + scr_dyn_release(iv_value); + if (!ok) return NULL; + } + const ScCipherInfo *info = + name_or_nid->kind == SCR_DYN_STR ? ci_by_name(name_or_nid->v.str) : ci_by_nid(name_or_nid->v.num); + if (!info || !ci_accepts(info, has_key, key, has_iv, iv)) return scr_dyn_retain((ScrDyn *)scr_dyn_undefined()); + return ci_object(info, has_iv ? iv : info->iv_length); +} + +ScrDyn *scr_crypto_get_cipher_info(ScrDyn *name_or_nid, ScrDyn *options) { + ScrDyn *name = ci_plain(name_or_nid); + if (!name) return NULL; + ScrDyn *opts = ci_plain(options); + ScrDyn *result = opts ? ci_get(name, opts, ci_class_option(options)) : NULL; + if (opts) scr_dyn_release(opts); + scr_dyn_release(name); + return result; +} diff --git a/packages/runtime/src/scr_cipher_info.h b/packages/runtime/src/scr_cipher_info.h new file mode 100644 index 000000000..edb18caea --- /dev/null +++ b/packages/runtime/src/scr_cipher_info.h @@ -0,0 +1,615 @@ +/* Generated by scripts/gen-cipher-info.mjs from Node v24.22.0's + * crypto.getCipherInfo: every cipher OpenSSL names, with the fields Node + * reports (-1 or NULL for a field it leaves out) and the keyLength and + * ivLength options each accepts. Do not edit. */ +#ifndef SCR_CIPHER_INFO_H +#define SCR_CIPHER_INFO_H + +enum { SC_INFO_NONE, SC_INFO_EXACT, SC_INFO_CCM, SC_INFO_OCB, SC_INFO_ANY }; + +typedef struct { + const char *mode; + const char *name; + int nid; + int block_size; + int iv_length; + int key_length; + unsigned char key_rule; + unsigned char iv_rule; +} ScCipherInfo; + +static const ScCipherInfo sc_cipher_infos[] = { + {"ecb", "camellia-128-ecb", 754, 16, -1, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ecb", "camellia-192-ecb", 755, 16, -1, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ofb", "camellia-192-ofb", 767, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "camellia-192-cfb", 758, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ctr", "camellia-192-ctr", 967, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ofb", "camellia-128-ofb", 766, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "camellia-128-cfb", 757, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ecb", "camellia-256-ecb", 756, 16, -1, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ofb", "camellia-256-ofb", 768, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "camellia-256-cfb", 759, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ctr", "camellia-256-ctr", 971, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ctr", "camellia-128-ctr", 963, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ecb", "sm4-ecb", 1133, 16, -1, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "sm4-cbc", 1134, 16, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ofb", "sm4-ofb", 1135, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "sm4-cfb", 1137, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ctr", "sm4-ctr", 1139, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "camellia-128-cbc", 751, 16, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "camellia-192-cbc", 752, 16, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "camellia-256-cbc", 753, 16, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ecb", "aria-128-ecb", 1065, 16, -1, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ctr", "aria-192-ctr", 1074, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ecb", "aria-256-ecb", 1075, 16, -1, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "aria-256-cbc", 1076, 16, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aria-256-cfb", 1077, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ofb", "aria-256-ofb", 1078, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ctr", "aria-256-ctr", 1079, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "aria-128-cbc", 1066, 16, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aria-128-cfb", 1067, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"gcm", "aria-128-gcm", 1123, 1, 12, 16, SC_INFO_EXACT, SC_INFO_ANY}, + {"gcm", "aria-192-gcm", 1124, 1, 12, 24, SC_INFO_EXACT, SC_INFO_ANY}, + {"gcm", "aria-256-gcm", 1125, 1, 12, 32, SC_INFO_EXACT, SC_INFO_ANY}, + {"ccm", "aria-128-ccm", 1120, 1, 12, 16, SC_INFO_EXACT, SC_INFO_CCM}, + {"ccm", "aria-192-ccm", 1121, 1, 12, 24, SC_INFO_EXACT, SC_INFO_CCM}, + {"ccm", "aria-256-ccm", 1122, 1, 12, 32, SC_INFO_EXACT, SC_INFO_CCM}, + {"ofb", "aria-128-ofb", 1068, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ctr", "aria-128-ctr", 1069, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ecb", "aria-192-ecb", 1070, 16, -1, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "aria-192-cbc", 1071, 16, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aria-192-cfb", 1072, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ofb", "aria-192-ofb", 1073, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"wrap", "id-smime-alg-cms3deswrap", 246, 8, -1, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "des-ede3-cbc", 44, 8, 8, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"xts", "aes-128-xts", 913, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"xts", "aes-256-xts", 914, 1, 16, 64, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ecb", "des-ede", 32, 8, -1, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ecb", "aes-128-ecb", 418, 16, -1, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "aes-128-cbc", 419, 16, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ecb", "aes-192-ecb", 422, 16, -1, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "aes-192-cbc", 423, 16, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ofb", "aes-192-ofb", 424, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aes-192-cfb", 425, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"wrap", "id-aes192-wrap", 789, 8, 8, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"gcm", "id-aes192-gcm", 898, 1, 12, 24, SC_INFO_EXACT, SC_INFO_ANY}, + {"ccm", "id-aes192-ccm", 899, 1, 12, 24, SC_INFO_EXACT, SC_INFO_CCM}, + {"wrap", "id-aes192-wrap-pad", 900, 8, 4, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ofb", "aes-128-ofb", 420, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aes-128-cfb", 421, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ecb", "aes-256-ecb", 426, 16, -1, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "aes-256-cbc", 427, 16, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ofb", "aes-256-ofb", 428, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aes-256-cfb", 429, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"wrap", "id-aes256-wrap", 790, 8, 8, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"gcm", "id-aes256-gcm", 901, 1, 12, 32, SC_INFO_EXACT, SC_INFO_ANY}, + {"ccm", "id-aes256-ccm", 902, 1, 12, 32, SC_INFO_EXACT, SC_INFO_CCM}, + {"wrap", "id-aes256-wrap-pad", 903, 8, 4, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"wrap", "id-aes128-wrap", 788, 8, 8, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"gcm", "id-aes128-gcm", 895, 1, 12, 16, SC_INFO_EXACT, SC_INFO_ANY}, + {"ccm", "id-aes128-ccm", 896, 1, 12, 16, SC_INFO_EXACT, SC_INFO_CCM}, + {"wrap", "id-aes128-wrap-pad", 897, 8, 4, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "aes-128-cbc-cts", -1, 16, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aes-128-cfb1", 650, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aes-128-cfb8", 653, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ctr", "aes-128-ctr", 904, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"gcm-siv", "aes-128-gcm-siv", -1, 1, 12, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ocb", "aes-128-ocb", 958, 16, 12, 16, SC_INFO_EXACT, SC_INFO_OCB}, + {"siv", "aes-128-siv", -1, 1, -1, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"wrap", "aes-128-wrap-inv", -1, 8, 8, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"wrap", "aes-128-wrap-pad-inv", -1, 8, 4, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "aes-192-cbc-cts", -1, 16, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aes-192-cfb1", 651, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aes-192-cfb8", 654, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ctr", "aes-192-ctr", 905, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"gcm-siv", "aes-192-gcm-siv", -1, 1, 12, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ocb", "aes-192-ocb", 959, 16, 12, 24, SC_INFO_EXACT, SC_INFO_OCB}, + {"siv", "aes-192-siv", -1, 1, -1, 48, SC_INFO_EXACT, SC_INFO_EXACT}, + {"wrap", "aes-192-wrap-inv", -1, 8, 8, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"wrap", "aes-192-wrap-pad-inv", -1, 8, 4, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "aes-256-cbc-cts", -1, 16, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aes-256-cfb1", 652, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aes-256-cfb8", 655, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ctr", "aes-256-ctr", 906, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"gcm-siv", "aes-256-gcm-siv", -1, 1, 12, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ocb", "aes-256-ocb", 960, 16, 12, 32, SC_INFO_EXACT, SC_INFO_OCB}, + {"siv", "aes-256-siv", -1, 1, -1, 64, SC_INFO_EXACT, SC_INFO_EXACT}, + {"wrap", "aes-256-wrap-inv", -1, 8, 8, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"wrap", "aes-256-wrap-pad-inv", -1, 8, 4, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aria-128-cfb1", 1080, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aria-128-cfb8", 1083, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aria-192-cfb1", 1081, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aria-192-cfb8", 1084, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aria-256-cfb1", 1082, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aria-256-cfb8", 1085, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "bf-cbc", 91, 8, 8, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"cfb", "bf-cfb", 93, 1, 8, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"ecb", "bf-ecb", 92, 8, -1, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"ofb", "bf-ofb", 94, 1, 8, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"cbc", "camellia-128-cbc-cts", -1, 16, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "camellia-128-cfb1", 760, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "camellia-128-cfb8", 763, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "camellia-192-cbc-cts", -1, 16, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "camellia-192-cfb1", 761, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "camellia-192-cfb8", 764, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "camellia-256-cbc-cts", -1, 16, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "camellia-256-cfb1", 762, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "camellia-256-cfb8", 765, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "cast5-cbc", 108, 8, 8, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"cfb", "cast5-cfb", 110, 1, 8, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"ecb", "cast5-ecb", 109, 8, -1, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"ofb", "cast5-ofb", 111, 1, 8, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"stream", "chacha20", 1019, -1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"stream", "chacha20-poly1305", 1018, -1, 12, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "des-cbc", 31, 8, 8, 8, SC_INFO_NONE, SC_INFO_NONE}, + {"cfb", "des-cfb", 30, 1, 8, 8, SC_INFO_NONE, SC_INFO_NONE}, + {"cfb", "des-cfb1", 656, 1, 8, 8, SC_INFO_NONE, SC_INFO_NONE}, + {"cfb", "des-cfb8", 657, 1, 8, 8, SC_INFO_NONE, SC_INFO_NONE}, + {"ecb", "des-ecb", 29, 8, -1, 8, SC_INFO_NONE, SC_INFO_NONE}, + {"cbc", "des-ede-cbc", 43, 8, 8, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "des-ede-cfb", 60, 1, 8, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ofb", "des-ede-ofb", 62, 1, 8, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ecb", "des-ede3", 33, 8, -1, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "des-ede3-cfb", 61, 1, 8, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "des-ede3-cfb1", 658, 1, 8, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "des-ede3-cfb8", 659, 1, 8, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ofb", "des-ede3-ofb", 63, 1, 8, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ofb", "des-ofb", 45, 1, 8, 8, SC_INFO_NONE, SC_INFO_NONE}, + {"cbc", "desx-cbc", 80, 8, 8, 24, SC_INFO_NONE, SC_INFO_NONE}, + {"cbc", "idea-cbc", 34, 8, 8, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"cfb", "idea-cfb", 35, 1, 8, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"ecb", "idea-ecb", 36, 8, -1, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"ofb", "idea-ofb", 46, 1, 8, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"cbc", "rc2-cbc", 37, 8, 8, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"cbc", "rc2-40-cbc", 98, 8, 8, 5, SC_INFO_NONE, SC_INFO_NONE}, + {"cbc", "rc2-64-cbc", 166, 8, 8, 8, SC_INFO_NONE, SC_INFO_NONE}, + {"cfb", "rc2-cfb", 39, 1, 8, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"ecb", "rc2-ecb", 38, 8, -1, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"ofb", "rc2-ofb", 40, 1, 8, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"stream", "rc4", 5, -1, -1, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"stream", "rc4-40", 97, -1, -1, 5, SC_INFO_NONE, SC_INFO_NONE}, + {"stream", "rc4-hmac-md5", 915, -1, -1, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"cbc", "seed-cbc", 777, 16, 16, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"cfb", "seed-cfb", 779, 1, 16, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"ecb", "seed-ecb", 776, 16, -1, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"ofb", "seed-ofb", 778, 1, 16, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"ccm", "sm4-ccm", -1, 1, 12, 16, SC_INFO_EXACT, SC_INFO_CCM}, + {"gcm", "sm4-gcm", -1, 1, 12, 16, SC_INFO_EXACT, SC_INFO_ANY}, + {"xts", "sm4-xts", -1, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, +}; + +/* Lowercase names, sorted, with their record. */ +static const struct { + const char *name; + unsigned short info; +} sc_cipher_info_names[] = { + {"0.3.4401.5.3.1.9.1", 0}, + {"0.3.4401.5.3.1.9.21", 1}, + {"0.3.4401.5.3.1.9.23", 2}, + {"0.3.4401.5.3.1.9.24", 3}, + {"0.3.4401.5.3.1.9.29", 4}, + {"0.3.4401.5.3.1.9.3", 5}, + {"0.3.4401.5.3.1.9.4", 6}, + {"0.3.4401.5.3.1.9.41", 7}, + {"0.3.4401.5.3.1.9.43", 8}, + {"0.3.4401.5.3.1.9.44", 9}, + {"0.3.4401.5.3.1.9.49", 10}, + {"0.3.4401.5.3.1.9.9", 11}, + {"1.2.156.10197.1.104.1", 12}, + {"1.2.156.10197.1.104.2", 13}, + {"1.2.156.10197.1.104.3", 14}, + {"1.2.156.10197.1.104.4", 15}, + {"1.2.156.10197.1.104.7", 16}, + {"1.2.392.200011.61.1.1.1.2", 17}, + {"1.2.392.200011.61.1.1.1.3", 18}, + {"1.2.392.200011.61.1.1.1.4", 19}, + {"1.2.410.200046.1.1.1", 20}, + {"1.2.410.200046.1.1.10", 21}, + {"1.2.410.200046.1.1.11", 22}, + {"1.2.410.200046.1.1.12", 23}, + {"1.2.410.200046.1.1.13", 24}, + {"1.2.410.200046.1.1.14", 25}, + {"1.2.410.200046.1.1.15", 26}, + {"1.2.410.200046.1.1.2", 27}, + {"1.2.410.200046.1.1.3", 28}, + {"1.2.410.200046.1.1.34", 29}, + {"1.2.410.200046.1.1.35", 30}, + {"1.2.410.200046.1.1.36", 31}, + {"1.2.410.200046.1.1.37", 32}, + {"1.2.410.200046.1.1.38", 33}, + {"1.2.410.200046.1.1.39", 34}, + {"1.2.410.200046.1.1.4", 35}, + {"1.2.410.200046.1.1.5", 36}, + {"1.2.410.200046.1.1.6", 37}, + {"1.2.410.200046.1.1.7", 38}, + {"1.2.410.200046.1.1.8", 39}, + {"1.2.410.200046.1.1.9", 40}, + {"1.2.840.113549.1.9.16.3.6", 41}, + {"1.2.840.113549.3.7", 42}, + {"1.3.111.2.1619.0.1.1", 43}, + {"1.3.111.2.1619.0.1.2", 44}, + {"1.3.14.3.2.17", 45}, + {"2.16.840.1.101.3.4.1.1", 46}, + {"2.16.840.1.101.3.4.1.2", 47}, + {"2.16.840.1.101.3.4.1.21", 48}, + {"2.16.840.1.101.3.4.1.22", 49}, + {"2.16.840.1.101.3.4.1.23", 50}, + {"2.16.840.1.101.3.4.1.24", 51}, + {"2.16.840.1.101.3.4.1.25", 52}, + {"2.16.840.1.101.3.4.1.26", 53}, + {"2.16.840.1.101.3.4.1.27", 54}, + {"2.16.840.1.101.3.4.1.28", 55}, + {"2.16.840.1.101.3.4.1.3", 56}, + {"2.16.840.1.101.3.4.1.4", 57}, + {"2.16.840.1.101.3.4.1.41", 58}, + {"2.16.840.1.101.3.4.1.42", 59}, + {"2.16.840.1.101.3.4.1.43", 60}, + {"2.16.840.1.101.3.4.1.44", 61}, + {"2.16.840.1.101.3.4.1.45", 62}, + {"2.16.840.1.101.3.4.1.46", 63}, + {"2.16.840.1.101.3.4.1.47", 64}, + {"2.16.840.1.101.3.4.1.48", 65}, + {"2.16.840.1.101.3.4.1.5", 66}, + {"2.16.840.1.101.3.4.1.6", 67}, + {"2.16.840.1.101.3.4.1.7", 68}, + {"2.16.840.1.101.3.4.1.8", 69}, + {"aes-128-cbc", 47}, + {"aes-128-cbc-cts", 70}, + {"aes-128-ccm", 68}, + {"aes-128-cfb", 57}, + {"aes-128-cfb1", 71}, + {"aes-128-cfb8", 72}, + {"aes-128-ctr", 73}, + {"aes-128-ecb", 46}, + {"aes-128-gcm", 67}, + {"aes-128-gcm-siv", 74}, + {"aes-128-ocb", 75}, + {"aes-128-ofb", 56}, + {"aes-128-siv", 76}, + {"aes-128-wrap", 66}, + {"aes-128-wrap-inv", 77}, + {"aes-128-wrap-pad", 69}, + {"aes-128-wrap-pad-inv", 78}, + {"aes-128-xts", 43}, + {"aes-192-cbc", 49}, + {"aes-192-cbc-cts", 79}, + {"aes-192-ccm", 54}, + {"aes-192-cfb", 51}, + {"aes-192-cfb1", 80}, + {"aes-192-cfb8", 81}, + {"aes-192-ctr", 82}, + {"aes-192-ecb", 48}, + {"aes-192-gcm", 53}, + {"aes-192-gcm-siv", 83}, + {"aes-192-ocb", 84}, + {"aes-192-ofb", 50}, + {"aes-192-siv", 85}, + {"aes-192-wrap", 52}, + {"aes-192-wrap-inv", 86}, + {"aes-192-wrap-pad", 55}, + {"aes-192-wrap-pad-inv", 87}, + {"aes-256-cbc", 59}, + {"aes-256-cbc-cts", 88}, + {"aes-256-ccm", 64}, + {"aes-256-cfb", 61}, + {"aes-256-cfb1", 89}, + {"aes-256-cfb8", 90}, + {"aes-256-ctr", 91}, + {"aes-256-ecb", 58}, + {"aes-256-gcm", 63}, + {"aes-256-gcm-siv", 92}, + {"aes-256-ocb", 93}, + {"aes-256-ofb", 60}, + {"aes-256-siv", 94}, + {"aes-256-wrap", 62}, + {"aes-256-wrap-inv", 95}, + {"aes-256-wrap-pad", 65}, + {"aes-256-wrap-pad-inv", 96}, + {"aes-256-xts", 44}, + {"aes128", 47}, + {"aes128-wrap", 66}, + {"aes128-wrap-inv", 77}, + {"aes128-wrap-pad", 69}, + {"aes128-wrap-pad-inv", 78}, + {"aes192", 49}, + {"aes192-wrap", 52}, + {"aes192-wrap-inv", 86}, + {"aes192-wrap-pad", 55}, + {"aes192-wrap-pad-inv", 87}, + {"aes256", 59}, + {"aes256-wrap", 62}, + {"aes256-wrap-inv", 95}, + {"aes256-wrap-pad", 65}, + {"aes256-wrap-pad-inv", 96}, + {"aria-128-cbc", 27}, + {"aria-128-ccm", 32}, + {"aria-128-cfb", 28}, + {"aria-128-cfb1", 97}, + {"aria-128-cfb8", 98}, + {"aria-128-ctr", 36}, + {"aria-128-ecb", 20}, + {"aria-128-gcm", 29}, + {"aria-128-ofb", 35}, + {"aria-192-cbc", 38}, + {"aria-192-ccm", 33}, + {"aria-192-cfb", 39}, + {"aria-192-cfb1", 99}, + {"aria-192-cfb8", 100}, + {"aria-192-ctr", 21}, + {"aria-192-ecb", 37}, + {"aria-192-gcm", 30}, + {"aria-192-ofb", 40}, + {"aria-256-cbc", 23}, + {"aria-256-ccm", 34}, + {"aria-256-cfb", 24}, + {"aria-256-cfb1", 101}, + {"aria-256-cfb8", 102}, + {"aria-256-ctr", 26}, + {"aria-256-ecb", 22}, + {"aria-256-gcm", 31}, + {"aria-256-ofb", 25}, + {"aria128", 27}, + {"aria192", 38}, + {"aria256", 23}, + {"bf", 103}, + {"bf-cbc", 103}, + {"bf-cfb", 104}, + {"bf-ecb", 105}, + {"bf-ofb", 106}, + {"blowfish", 103}, + {"camellia-128-cbc", 17}, + {"camellia-128-cbc-cts", 107}, + {"camellia-128-cfb", 6}, + {"camellia-128-cfb1", 108}, + {"camellia-128-cfb8", 109}, + {"camellia-128-ctr", 11}, + {"camellia-128-ecb", 0}, + {"camellia-128-ofb", 5}, + {"camellia-192-cbc", 18}, + {"camellia-192-cbc-cts", 110}, + {"camellia-192-cfb", 3}, + {"camellia-192-cfb1", 111}, + {"camellia-192-cfb8", 112}, + {"camellia-192-ctr", 4}, + {"camellia-192-ecb", 1}, + {"camellia-192-ofb", 2}, + {"camellia-256-cbc", 19}, + {"camellia-256-cbc-cts", 113}, + {"camellia-256-cfb", 9}, + {"camellia-256-cfb1", 114}, + {"camellia-256-cfb8", 115}, + {"camellia-256-ctr", 10}, + {"camellia-256-ecb", 7}, + {"camellia-256-ofb", 8}, + {"camellia128", 17}, + {"camellia192", 18}, + {"camellia256", 19}, + {"cast", 116}, + {"cast-cbc", 116}, + {"cast5-cbc", 116}, + {"cast5-cfb", 117}, + {"cast5-ecb", 118}, + {"cast5-ofb", 119}, + {"chacha20", 120}, + {"chacha20-poly1305", 121}, + {"des", 122}, + {"des-cbc", 122}, + {"des-cfb", 123}, + {"des-cfb1", 124}, + {"des-cfb8", 125}, + {"des-ecb", 126}, + {"des-ede", 45}, + {"des-ede-cbc", 127}, + {"des-ede-cfb", 128}, + {"des-ede-ecb", 45}, + {"des-ede-ofb", 129}, + {"des-ede3", 130}, + {"des-ede3-cbc", 42}, + {"des-ede3-cfb", 131}, + {"des-ede3-cfb1", 132}, + {"des-ede3-cfb8", 133}, + {"des-ede3-ecb", 130}, + {"des-ede3-ofb", 134}, + {"des-ofb", 135}, + {"des3", 42}, + {"des3-wrap", 41}, + {"desx", 136}, + {"desx-cbc", 136}, + {"id-aes128-ccm", 68}, + {"id-aes128-gcm", 67}, + {"id-aes128-wrap", 66}, + {"id-aes128-wrap-pad", 69}, + {"id-aes192-ccm", 54}, + {"id-aes192-gcm", 53}, + {"id-aes192-wrap", 52}, + {"id-aes192-wrap-pad", 55}, + {"id-aes256-ccm", 64}, + {"id-aes256-gcm", 63}, + {"id-aes256-wrap", 62}, + {"id-aes256-wrap-pad", 65}, + {"id-smime-alg-cms3deswrap", 41}, + {"idea-cbc", 137}, + {"idea-cfb", 138}, + {"idea-ecb", 139}, + {"idea-ofb", 140}, + {"rc2", 141}, + {"rc2-128", 141}, + {"rc2-40", 142}, + {"rc2-40-cbc", 142}, + {"rc2-64", 143}, + {"rc2-64-cbc", 143}, + {"rc2-cbc", 141}, + {"rc2-cfb", 144}, + {"rc2-ecb", 145}, + {"rc2-ofb", 146}, + {"rc4", 147}, + {"rc4-40", 148}, + {"rc4-hmac-md5", 149}, + {"seed", 150}, + {"seed-cbc", 150}, + {"seed-cfb", 151}, + {"seed-ecb", 152}, + {"seed-ofb", 153}, + {"sm4", 13}, + {"sm4-cbc", 13}, + {"sm4-ccm", 154}, + {"sm4-cfb", 15}, + {"sm4-cfb128", 15}, + {"sm4-ctr", 16}, + {"sm4-ecb", 12}, + {"sm4-gcm", 155}, + {"sm4-ofb", 14}, + {"sm4-ofb128", 14}, + {"sm4-xts", 156}, +}; + +/* Nids, ascending, with their record. */ +static const struct { + unsigned short nid; + unsigned short info; +} sc_cipher_info_nids[] = { + {5, 147}, + {29, 126}, + {30, 123}, + {31, 122}, + {32, 45}, + {33, 130}, + {34, 137}, + {35, 138}, + {36, 139}, + {37, 141}, + {38, 145}, + {39, 144}, + {40, 146}, + {43, 127}, + {44, 42}, + {45, 135}, + {46, 140}, + {60, 128}, + {61, 131}, + {62, 129}, + {63, 134}, + {80, 136}, + {91, 103}, + {92, 105}, + {93, 104}, + {94, 106}, + {97, 148}, + {98, 142}, + {108, 116}, + {109, 118}, + {110, 117}, + {111, 119}, + {166, 143}, + {246, 41}, + {418, 46}, + {419, 47}, + {420, 56}, + {421, 57}, + {422, 48}, + {423, 49}, + {424, 50}, + {425, 51}, + {426, 58}, + {427, 59}, + {428, 60}, + {429, 61}, + {650, 71}, + {651, 80}, + {652, 89}, + {653, 72}, + {654, 81}, + {655, 90}, + {656, 124}, + {657, 125}, + {658, 132}, + {659, 133}, + {751, 17}, + {752, 18}, + {753, 19}, + {754, 0}, + {755, 1}, + {756, 7}, + {757, 6}, + {758, 3}, + {759, 9}, + {760, 108}, + {761, 111}, + {762, 114}, + {763, 109}, + {764, 112}, + {765, 115}, + {766, 5}, + {767, 2}, + {768, 8}, + {776, 152}, + {777, 150}, + {778, 153}, + {779, 151}, + {788, 66}, + {789, 52}, + {790, 62}, + {895, 67}, + {896, 68}, + {897, 69}, + {898, 53}, + {899, 54}, + {900, 55}, + {901, 63}, + {902, 64}, + {903, 65}, + {904, 73}, + {905, 82}, + {906, 91}, + {913, 43}, + {914, 44}, + {915, 149}, + {958, 75}, + {959, 84}, + {960, 93}, + {963, 11}, + {967, 4}, + {971, 10}, + {1018, 121}, + {1019, 120}, + {1065, 20}, + {1066, 27}, + {1067, 28}, + {1068, 35}, + {1069, 36}, + {1070, 37}, + {1071, 38}, + {1072, 39}, + {1073, 40}, + {1074, 21}, + {1075, 22}, + {1076, 23}, + {1077, 24}, + {1078, 25}, + {1079, 26}, + {1080, 97}, + {1081, 99}, + {1082, 101}, + {1083, 98}, + {1084, 100}, + {1085, 102}, + {1120, 32}, + {1121, 33}, + {1122, 34}, + {1123, 29}, + {1124, 30}, + {1125, 31}, + {1133, 12}, + {1134, 13}, + {1135, 14}, + {1137, 15}, + {1139, 16}, + {1198, 76}, + {1199, 85}, + {1200, 94}, + {1248, 155}, + {1249, 154}, + {1290, 156}, +}; + +#endif diff --git a/packages/runtime/src/scr_console.c b/packages/runtime/src/scr_console.c index f8dd9a0c0..086340014 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_v33(void) {} +void scr_runtime_abi_v37(void) {} void scr_init(void) { #ifdef SCR_WORKERS diff --git a/packages/runtime/src/scr_core.h b/packages/runtime/src/scr_core.h index d716f0de3..3707b8d50 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_v33(void); +void scr_runtime_abi_v37(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 0e6c6e07f..f61d27406 100644 --- a/packages/runtime/src/scr_crypto.h +++ b/packages/runtime/src/scr_crypto.h @@ -108,6 +108,9 @@ 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.getCipherInfo(nameOrNid, options) (scr_cipher_info.c): the info + * object or undefined (+1), or NULL with Node's argument error pending. */ +ScrDyn *scr_crypto_get_cipher_info(ScrDyn *name_or_nid, ScrDyn *options); /* 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 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_dyn.h b/packages/runtime/src/scr_dyn.h index cf50194c8..6f0c28371 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -364,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 @@ -383,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; @@ -467,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 @@ -476,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 @@ -506,6 +540,10 @@ _Static_assert(offsetof(ScrDyn, null_proto) == sizeof(size_t) + sizeof(ScrDynKin "dyn-to-record conversions read null_proto at byte 13 (9 on wasm32)"); _Static_assert(offsetof(ScrDyn, v.typed_ref.type_key) == 16 + 3 * sizeof(void *), "typed_ref.type_key offset"); +/* Emitted brand probes read type_key_len as the low 27 bits of the 32-bit + * word after `casts` (native 88, wasm32 52). */ +_Static_assert(offsetof(ScrDyn, v.typed_ref.casts) == 16 + 8 * sizeof(void *), + "typed_ref.casts offset"); /* The region links ride in payload space the capsule variant already * reserves, so dyn nodes keep their size (and allocation size class). */ _Static_assert(sizeof(((ScrDyn *)0)->v.arr) <= sizeof(((ScrDyn *)0)->v.typed_ref) && @@ -617,7 +655,9 @@ 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. */ @@ -922,6 +962,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); @@ -1017,6 +1060,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. */ @@ -1296,15 +1350,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; "" @@ -1455,6 +1513,19 @@ ScrDyn *scr_dyn_visit_view(const ScrDyn *capsule); * (scr_http_dyn_install / scr_net_dyn_install — emitted main() calls * them exactly when the unit is linked, the scr_net_install story), so * this always-linked core never references gated units. */ +/* A method a handle class answers through invoke, read as a value + * (`typeof req.json`, `raw[key]()`): the read answers one function per + * (tag, method) that calls invoke on its `this`. length is the method's + * Function.prototype.length; rejects marks a method whose call on another + * receiver answers a rejected promise instead of throwing (the async + * WebIDL operations: Body's json(), ReadableStream's cancel()). */ +typedef struct ScrDynHandleMethod { + const char *name; + uint32_t length; + bool rejects; + const char *invalid_this; /* NULL = the class's (ScrDynHandleOps) */ +} ScrDynHandleMethod; + typedef struct ScrDynHandleOps { const char *cls; /* Node's constructor name — error texts and dynCheck's "got ..." */ void *(*retain)(void *h); @@ -1520,6 +1591,16 @@ typedef struct ScrDynHandleOps { * stream unit builds the handle's Transform from these operations on its * first stream use. NULL = the class is not a stream. */ const struct ScrDynHandleTransform *transform; + /* The class's methods as values, terminated by a NULL name; NULL = a + * property read answers no methods (member calls still reach invoke). + * See ScrDynHandleMethod. */ + const ScrDynHandleMethod *methods; + /* The TypeError (code ERR_INVALID_THIS) a method value called on another + * receiver raises; NULL = WebIDL's "Illegal invocation". */ + const char *invalid_this; + /* A promise rejected with the pending exception (+1), for the rejects + * methods; the owning unit supplies it, since promises are a gated unit. */ + ScrDyn *(*rejected)(void); } ScrDynHandleOps; /* The operations a Transform handle gives the stream unit (scr_stream.c): diff --git a/packages/runtime/src/scr_dyn_builtin_stream.c b/packages/runtime/src/scr_dyn_builtin_stream.c index 926f85eea..2875cfa06 100644 --- a/packages/runtime/src/scr_dyn_builtin_stream.c +++ b/packages/runtime/src/scr_dyn_builtin_stream.c @@ -34,7 +34,7 @@ * receiver without a reference; the hook closures outlive the record. * * Not supported here (each throws the "not supported yet" Error where it - * runs): the options scr_stream.c's dyn option walk refuses, `_writev` and `_construct`, symbol event names, listeners() / + * runs): the options scr_stream.c's dyn option walk refuses, symbol event names, listeners() / * rawListeners(), `wrap()`, and pipes into values that are neither runtime * streams nor constructor-function streams. */ #include "scr_runtime.h" @@ -898,20 +898,15 @@ static ScrDyn *bcs_construct(int kind, ScrDyn *self, ScrDyn *const *args, size_t } bool failed = false; bool writes = kind != BCS_READABLE; - 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 (writes && !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) diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index 14d11dc67..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; } 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_event_target.c b/packages/runtime/src/scr_event_target.c index 7c8a465a6..9ca91879a 100644 --- a/packages/runtime/src/scr_event_target.c +++ b/packages/runtime/src/scr_event_target.c @@ -1043,6 +1043,9 @@ static ScrDyn *scr_et_method_value(const char *key, size_t len, bool target) { const char *name = scr_et_method_names[i]; if (strlen(name) != len || memcmp(name, key, len) != 0) continue; if (!scr_et_method_values[i]) { + /* An AbortSignal answers these without the unit ever installing: + * exit releases the cached function either way. */ + scr_et_register_teardown(); ScrClosure *clo = scr_closure_new(NULL, 1); clo->caps[0] = scr_box_new(SCR_BOX_F64); scr_box_set_f64(clo->caps[0], (double)i); @@ -1054,6 +1057,10 @@ static ScrDyn *scr_et_method_value(const char *key, size_t len, bool target) { return NULL; } +ScrDyn *scr_event_target_method_value(const char *key, size_t len) { + return scr_et_method_value(key, len, true); +} + static ScrDyn *scr_event_target_value(const ScrEvent *ev) { return ev->target ? scr_dyn_retain(ev->target) : scr_dyn_new_null(); } diff --git a/packages/runtime/src/scr_event_target.h b/packages/runtime/src/scr_event_target.h index e787cdbdb..e1ff5d0da 100644 --- a/packages/runtime/src/scr_event_target.h +++ b/packages/runtime/src/scr_event_target.h @@ -56,6 +56,11 @@ void scr_et_core_drop(ScrEventTargetCore *core); ScrDyn *scr_et_add_listener(ScrEventTargetCore *core, ScrDyn *const *args, size_t argc); ScrDyn *scr_et_remove_listener(ScrEventTargetCore *core, ScrDyn *const *args, size_t argc); ScrDyn *scr_et_dispatch_event(ScrEventTargetCore *core, ScrDyn *self, ScrDyn *const *args, size_t argc); +/* EventTarget.prototype's method `key` (addEventListener, + * removeEventListener, dispatchEvent) as a function value (+1), the one + * every EventTarget answers, AbortSignal included: it runs on any target + * its `this` is. NULL for another name. */ +ScrDyn *scr_event_target_method_value(const char *key, size_t len); /* Dispatches an event handle (borrowed) without the argument checks. */ void scr_et_dispatch(ScrEventTargetCore *core, ScrDyn *self, ScrDyn *event); /* `on` attributes (Node's defineEventHandler): get answers +1 (null diff --git a/packages/runtime/src/scr_fetch.c b/packages/runtime/src/scr_fetch.c index 96e90e47c..0cc1ba02f 100644 --- a/packages/runtime/src/scr_fetch.c +++ b/packages/runtime/src/scr_fetch.c @@ -172,7 +172,14 @@ struct SfStream { struct SfHeaders { size_t rc; ScrArr *pairs; + /* Fetch's "sort and combine" list of `pairs` as flat name/value pairs, + * built on the first iteration step and dropped by every mutation. */ + ScrArr *sorted; bool immutable; + /* `pairs` is in the form sf_add_converted_headers builds: lowercase valid + * names, trimmed valid UTF-8 values, one combined pair per name except + * set-cookie. Headers built from wire lines are not (duplicates stay). */ + bool canonical; }; struct SfResponse { @@ -184,6 +191,23 @@ struct SfResponse { int status; bool redirected; bool null_body; + /* @hono/node-server's lightweight-response state (scr_fetch_response_light): + * whether a constructed Response is still one the package would write + * from its constructor arguments, and in which header form. 0: not (it + * was materialized by a body/metadata read, or never qualified); 1: no + * init headers; 2: a Headers (or pair list) form; 3: a plain record, + * whose raw entries light_record keeps in order. */ + int8_t light; + /* Form 2 reached from form 1 by a headers read: the package's Headers + * start with its default text content type when the body is not null. */ + bool light_default_ct; + /* The constructor added the text content type (a string body without + * one), which the package's lightweight headers never hold. */ + bool auto_ct; + /* The body's byte length (string or Uint8Array), -1 for a null body, + * -2 for a stream. */ + double light_length; + ScrArr *light_record; }; #ifndef SCR_FETCH_SIGNAL_ONLY @@ -713,7 +737,8 @@ static ScrDyn *sf_signal_get(void *ptr, const char *key, size_t len) { : scr_dyn_retain(scr_dyn_undefined()); } if (sf_name(key, len, "onabort")) return scr_et_handler_get(&s->core, "abort"); - return NULL; + /* Inherited from EventTarget: the function every target shares. */ + return scr_event_target_method_value(key, len); } static bool sf_signal_set(void *ptr, const char *key, size_t len, @@ -2146,6 +2171,24 @@ static ScrDyn *sf_controller_get(void *ptr, const char *key, size_t len) { static bool sf_header_name_ok(const char *s, size_t len); +/* A borrowed element of a runtime-built header pair array. These arrays are + * only ever appended to or rewritten whole, so they are dense. */ +static inline ScrStr *sf_pair_at(ScrArr *pairs, size_t i) { + if (i < pairs->filled) return (ScrStr *)(uintptr_t)scr_arr_dense_get(pairs, i); + return (ScrStr *)scr_arr_peek_ref(pairs, (double)i); +} + +static ScrStr *sf_header_name_lower(const char *data, size_t len); + +/* `name` in lowercase, +1: the same string when it has no uppercase byte. */ +static ScrStr *sf_header_name_lower_str(ScrStr *name) { + for (size_t i = 0; i < name->len; i++) { + char c = name->data[i]; + if (c >= 'A' && c <= 'Z') return sf_header_name_lower(name->data, name->len); + } + return scr_str_retain(name); +} + static ScrStr *sf_header_name_lower(const char *data, size_t len) { char *lower = malloc(len ? len : 1); if (!lower) sf_oom(); @@ -2214,13 +2257,8 @@ static SfHeaders *sf_headers_new_response(ScrArr *raw_pairs) { size_t n = (size_t)scr_arr_len(raw_pairs); ScrArr *pairs = scr_arr_new(SCR_ELEM_STR, n); for (size_t i = 0; i + 1 < n; i += 2) { - ScrStr *raw_name = scr_arr_get_ref(raw_pairs, (double)i); - scr_arr_push_ref( - pairs, sf_header_name_lower(raw_name->data, raw_name->len)); - scr_str_release(raw_name); - ScrStr *raw = scr_arr_get_ref(raw_pairs, (double)(i + 1)); - scr_arr_push_ref(pairs, sf_latin1_to_utf8(raw)); - scr_str_release(raw); + scr_arr_push_ref(pairs, sf_header_name_lower_str(sf_pair_at(raw_pairs, i))); + scr_arr_push_ref(pairs, sf_latin1_to_utf8(sf_pair_at(raw_pairs, i + 1))); } scr_arr_release(raw_pairs); return sf_headers_new(pairs, true); @@ -2234,9 +2272,17 @@ static SfHeaders *sf_headers_retain(SfHeaders *h) { static void sf_headers_release(SfHeaders *h) { if (!h || --h->rc > 0) return; scr_arr_release(h->pairs); + if (h->sorted) scr_arr_release(h->sorted); free(h); } +/* Every change to h->pairs goes through here first. */ +static void sf_headers_changed(SfHeaders *h) { + if (!h->sorted) return; + scr_arr_release(h->sorted); + h->sorted = NULL; +} + static void *sf_headers_retain_v(void *p) { return sf_headers_retain((SfHeaders *)p); } @@ -2276,7 +2322,7 @@ static ScrStr *sf_headers_name(const ScrDyn *value, const char *method) { scr_str_release(raw); return NULL; } - ScrStr *out = sf_header_name_lower(raw->data, raw->len); + ScrStr *out = sf_header_name_lower_str(raw); scr_str_release(raw); return out; } @@ -2287,40 +2333,36 @@ static bool sf_headers_key_eq(const ScrStr *left, const ScrStr *right) { } static ScrStr *sf_headers_get_value(SfHeaders *h, const ScrStr *name) { - size_t n = (size_t)scr_arr_len(h->pairs); + ScrArr *pairs = h->pairs; + size_t n = (size_t)scr_arr_len(pairs); size_t count = 0; size_t total = 0; + ScrStr *first = NULL; for (size_t i = 0; i + 1 < n; i += 2) { - ScrStr *key = scr_arr_get_ref(h->pairs, (double)i); - ScrStr *value = scr_arr_get_ref(h->pairs, (double)(i + 1)); - if (sf_headers_key_eq(key, name)) { - if (count > 0) total += 2; - total += value->len; - count++; - } - scr_str_release(key); - scr_str_release(value); + if (!sf_headers_key_eq(sf_pair_at(pairs, i), name)) continue; + ScrStr *value = sf_pair_at(pairs, i + 1); + if (count > 0) total += 2; + else first = value; + total += value->len; + count++; } if (count == 0) return NULL; + if (count == 1) return scr_str_retain(first); char *joined = malloc(total > 0 ? total : 1); if (!joined) sf_oom(); size_t at = 0; size_t emitted = 0; for (size_t i = 0; i + 1 < n; i += 2) { - ScrStr *key = scr_arr_get_ref(h->pairs, (double)i); - ScrStr *value = scr_arr_get_ref(h->pairs, (double)(i + 1)); - if (sf_headers_key_eq(key, name)) { - if (emitted > 0) { - joined[at++] = ','; - joined[at++] = ' '; - } - memcpy(joined + at, value->data, value->len); - at += value->len; - emitted++; + if (!sf_headers_key_eq(sf_pair_at(pairs, i), name)) continue; + ScrStr *value = sf_pair_at(pairs, i + 1); + if (emitted > 0) { + joined[at++] = ','; + joined[at++] = ' '; } - scr_str_release(key); - scr_str_release(value); + memcpy(joined + at, value->data, value->len); + at += value->len; + emitted++; } ScrStr *out = scr_str_new(joined, total); free(joined); @@ -2330,17 +2372,17 @@ static ScrStr *sf_headers_get_value(SfHeaders *h, const ScrStr *name) { static void sf_headers_delete_name(SfHeaders *h, const ScrStr *name) { ScrArr *old = h->pairs; size_t n = (size_t)scr_arr_len(old); + bool present = false; + for (size_t i = 0; i + 1 < n && !present; i += 2) + present = sf_headers_key_eq(sf_pair_at(old, i), name); + if (!present) return; + sf_headers_changed(h); ScrArr *next = scr_arr_new(SCR_ELEM_STR, n); for (size_t i = 0; i + 1 < n; i += 2) { - ScrStr *key = scr_arr_get_ref(old, (double)i); - ScrStr *value = scr_arr_get_ref(old, (double)(i + 1)); - if (sf_headers_key_eq(key, name)) { - scr_str_release(key); - scr_str_release(value); - continue; - } - scr_arr_push_ref(next, key); - scr_arr_push_ref(next, value); + ScrStr *key = sf_pair_at(old, i); + if (sf_headers_key_eq(key, name)) continue; + scr_arr_push_ref(next, scr_str_retain(key)); + scr_arr_push_ref(next, scr_str_retain(sf_pair_at(old, i + 1))); } h->pairs = next; scr_arr_release(old); @@ -2381,15 +2423,13 @@ static bool sf_headers_convert_pair( * Fetch-specific semicolon separator for Cookie. */ static void sf_headers_append_pair( SfHeaders *h, ScrStr *name, ScrStr *value) { + sf_headers_changed(h); if (!(name->len == 10 && memcmp(name->data, "set-cookie", 10) == 0)) { size_t n = (size_t)scr_arr_len(h->pairs); for (size_t i = 0; i + 1 < n; i += 2) { - ScrStr *key = scr_arr_get_ref(h->pairs, (double)i); - bool match = sf_headers_key_eq(key, name); - scr_str_release(key); - if (!match) continue; + if (!sf_headers_key_eq(sf_pair_at(h->pairs, i), name)) continue; - ScrStr *previous = scr_arr_get_ref(h->pairs, (double)(i + 1)); + ScrStr *previous = sf_pair_at(h->pairs, i + 1); const char *separator = name->len == 6 && memcmp(name->data, "cookie", 6) == 0 ? "; " @@ -2402,7 +2442,10 @@ static void sf_headers_append_pair( memcpy(joined_data + previous->len + 2, value->data, value->len); ScrStr *joined = scr_str_new(joined_data, joined_len); free(joined_data); - scr_str_release(previous); + /* Appending an empty value leaves the separator's space at the end of + * the combined value, which a copy trims again: the pairs are no + * longer in their canonical, trimmed form. */ + if (value->len == 0) h->canonical = false; scr_str_release(name); scr_str_release(value); scr_arr_set_ref(h->pairs, (double)(i + 1), joined); @@ -2429,7 +2472,7 @@ static ScrStr **sf_headers_sorted_names(SfHeaders *h, size_t *count_out) { if (capacity && !names) sf_oom(); size_t count = 0; for (size_t i = 0; i + 1 < n; i += 2) { - ScrStr *key = scr_arr_get_ref(h->pairs, (double)i); + ScrStr *key = sf_pair_at(h->pairs, i); bool seen = false; for (size_t j = 0; j < count; j++) { if (sf_headers_key_eq(names[j], key)) { @@ -2437,8 +2480,7 @@ static ScrStr **sf_headers_sorted_names(SfHeaders *h, size_t *count_out) { break; } } - if (seen) scr_str_release(key); - else names[count++] = key; + if (!seen) names[count++] = key; } if (count > 1) { qsort(names, count, sizeof *names, &sf_headers_name_cmp); @@ -2451,44 +2493,72 @@ static bool sf_headers_is_set_cookie(const ScrStr *name) { return name->len == 10 && memcmp(name->data, "set-cookie", 10) == 0; } -/* The `index`th entry of Fetch's "sort and combine" list: names in - * byte order, each with its combined value, except that every set-cookie - * value is its own entry (in insertion order), as Node's Headers - * iterator yields them. False past the end. Outputs are +1. */ +/* Fetch's "sort and combine" list as flat name/value pairs: names in byte + * order, each with its combined value, except that every set-cookie value + * is its own entry (in insertion order), as Node's Headers iterator yields + * them. Cached until the next mutation; borrowed. */ +static ScrArr *sf_headers_sorted(SfHeaders *h) { + if (h->sorted) return h->sorted; + size_t count = 0; + ScrStr **names = sf_headers_sorted_names(h, &count); /* borrowed names */ + size_t n = (size_t)scr_arr_len(h->pairs); + ScrArr *sorted = scr_arr_new(SCR_ELEM_STR, n); + for (size_t i = 0; i < count; i++) { + if (!sf_headers_is_set_cookie(names[i])) { + scr_arr_push_ref(sorted, scr_str_retain(names[i])); + scr_arr_push_ref(sorted, sf_headers_get_value(h, names[i])); + continue; + } + for (size_t j = 0; j + 1 < n; j += 2) { + if (!sf_headers_is_set_cookie(sf_pair_at(h->pairs, j))) continue; + scr_arr_push_ref(sorted, scr_str_retain(names[i])); + scr_arr_push_ref(sorted, scr_str_retain(sf_pair_at(h->pairs, j + 1))); + } + } + free(names); + h->sorted = sorted; + return sorted; +} + +/* The `index`th entry of the sort-and-combine list (sf_headers_sorted). + * False past the end. Outputs are +1. */ static bool sf_headers_sorted_entry(SfHeaders *h, size_t index, ScrStr **name_out, ScrStr **value_out) { - size_t count = 0; - ScrStr **names = sf_headers_sorted_names(h, &count); - bool found = false; - size_t at = 0; - for (size_t i = 0; i < count && !found; i++) { - if (!sf_headers_is_set_cookie(names[i])) { - if (at == index) { - *name_out = scr_str_retain(names[i]); - *value_out = sf_headers_get_value(h, names[i]); - found = true; - } - at++; - continue; - } - size_t n = (size_t)scr_arr_len(h->pairs); - for (size_t j = 0; j + 1 < n && !found; j += 2) { - ScrStr *key = scr_arr_get_ref(h->pairs, (double)j); - bool match = sf_headers_is_set_cookie(key); - scr_str_release(key); - if (!match) continue; - if (at == index) { - *name_out = scr_str_retain(names[i]); - *value_out = scr_arr_get_ref(h->pairs, (double)(j + 1)); - found = true; - } - at++; + ScrArr *sorted = sf_headers_sorted(h); + size_t entries = (size_t)scr_arr_len(sorted) / 2; + if (index >= entries) return false; + *name_out = scr_str_retain(sf_pair_at(sorted, 2 * index)); + *value_out = scr_str_retain(sf_pair_at(sorted, 2 * index + 1)); + return true; +} + +/* 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()); } - for (size_t j = 0; j < count; j++) scr_str_release(names[j]); - free(names); - return found; + 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, @@ -2496,6 +2566,11 @@ static ScrDyn *sf_headers_invoke(void *ptr, ScrDyn *self, 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"); @@ -2519,14 +2594,8 @@ static ScrDyn *sf_headers_invoke(void *ptr, ScrDyn *self, ScrDyn *out = scr_dyn_new_arr(); size_t n = (size_t)scr_arr_len(h->pairs); for (size_t i = 0; i + 1 < n; i += 2) { - ScrStr *key = scr_arr_get_ref(h->pairs, (double)i); - ScrStr *value = scr_arr_get_ref(h->pairs, (double)(i + 1)); - if (key->len == 10 && - memcmp(key->data, "set-cookie", 10) == 0) { - scr_dyn_arr_push(out, scr_dyn_new_str(value)); - } - scr_str_release(key); - scr_str_release(value); + if (sf_headers_is_set_cookie(sf_pair_at(h->pairs, i))) + scr_dyn_arr_push(out, scr_dyn_new_str(sf_pair_at(h->pairs, i + 1))); } return out; } @@ -2627,6 +2696,7 @@ static void sf_response_release(SfResponse *r) { sf_headers_release(r->headers); scr_str_release(r->url); scr_str_release(r->status_text); + scr_arr_release(r->light_record); free(r); } @@ -2637,6 +2707,13 @@ static void sf_response_release_v(void *p) { sf_response_release((SfResponse *)p); } +/* A read the package's lightweight Response answers from its native + * response materializes it; status, ok and headers keep it light, and a + * headers read turns the header form into a Headers object. */ +static void sf_response_materialize(SfResponse *r) { + r->light = 0; +} + static ScrPromise *sf_response_collect(SfResponse *r, int mode) { if (!r->null_body) return sf_stream_collect(r->body, mode); @@ -2675,6 +2752,7 @@ static ScrDyn *sf_response_invoke(void *ptr, ScrDyn *self, ScrDyn *const *args, size_t argc, const char *what) { SfResponse *r = ptr; + sf_response_materialize(r); int mode = -1; if (strcmp(method, "json") == 0) mode = SF_COLLECT_JSON; else if (strcmp(method, "text") == 0) mode = SF_COLLECT_TEXT; @@ -2698,6 +2776,19 @@ static ScrDyn *sf_response_get(void *ptr, const char *key, size_t len) { if (sf_name(key, len, "ok")) { return scr_dyn_new_bool(r->status >= 200 && r->status <= 299); } + if (sf_name(key, len, "headers")) { + if (r->light == 1) { + r->light = 2; + r->light_default_ct = !r->null_body; + } else if (r->light == 3) { + r->light = 2; + } + return scr_dyn_new_handle(r->headers, SCR_DYNH_FETCH_HEADERS); + } + if (sf_name(key, len, "body") || sf_name(key, len, "bodyUsed") || + sf_name(key, len, "url") || sf_name(key, len, "redirected") || + sf_name(key, len, "statusText")) + sf_response_materialize(r); if (sf_name(key, len, "bodyUsed")) { return scr_dyn_new_bool(!r->null_body && r->body->disturbed); } @@ -2713,9 +2804,6 @@ static ScrDyn *sf_response_get(void *ptr, const char *key, size_t len) { if (sf_name(key, len, "statusText")) { return scr_dyn_new_str(r->status_text); } - if (sf_name(key, len, "headers")) { - return scr_dyn_new_handle(r->headers, SCR_DYNH_FETCH_HEADERS); - } return NULL; } @@ -2727,6 +2815,7 @@ static ScrPromise *sf_response_collect_dyn(ScrDyn *response, int mode) { &scr_dyn_release_v, NULL); } SfResponse *r = response->v.handle.ptr; + sf_response_materialize(r); return sf_response_collect(r, mode); } @@ -3252,14 +3341,13 @@ static bool sf_pairs_have(ScrArr *pairs, const char *name) { size_t n = (size_t)scr_arr_len(pairs); size_t want = strlen(name); for (size_t i = 0; i + 1 < n; i += 2) { - ScrStr *key = (ScrStr *)scr_arr_get_ref(pairs, (double)i); + ScrStr *key = sf_pair_at(pairs, i); bool same = key->len == want; for (size_t j = 0; j < want && same; j++) { char c = key->data[j]; if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a'); same = c == name[j]; } - scr_str_release(key); if (same) return true; } return false; @@ -3287,6 +3375,10 @@ static bool sf_header_value_ok(const ScrStr *value) { } static ScrStr *sf_header_value_bytestring(ScrStr *value) { + /* An ASCII value is its own ByteString: each byte is one UTF-16 unit. */ + size_t ascii = 0; + while (ascii < value->len && (unsigned char)value->data[ascii] < 0x80) ascii++; + if (ascii == value->len) return scr_str_retain(value); size_t len = (size_t)scr_str_utf16_len(value); char *bytes = malloc(len ? len : 1); if (!bytes) sf_oom(); @@ -3355,6 +3447,21 @@ static bool sf_request_header_ok( !sf_header_name_ci(name, name_len, "expect"); } +/* Whether every value in `pairs` is already trimmed (sf_trim_header_value + * would keep it): the form a copy can take unchanged (SfHeaders.canonical). + * Combining duplicate names with an empty value leaves the separator's + * space at the end of the combined value, which a copy trims again. */ +static bool sf_header_values_trimmed(ScrArr *pairs) { + size_t n = (size_t)scr_arr_len(pairs); + for (size_t i = 1; i < n; i += 2) { + const ScrStr *value = sf_pair_at(pairs, i); + if (value->len == 0) continue; + char first = value->data[0], last = value->data[value->len - 1]; + if (first == ' ' || first == '\t' || last == ' ' || last == '\t') return false; + } + return true; +} + static ScrStr *sf_trim_header_value(const ScrStr *value) { size_t start = 0; size_t end = value->len; @@ -3366,11 +3473,14 @@ static ScrStr *sf_trim_header_value(const ScrStr *value) { (value->data[end - 1] == ' ' || value->data[end - 1] == '\t')) { end--; } + if (start == 0 && end == value->len) return scr_str_retain((ScrStr *)value); return scr_str_new(value->data + start, end - start); } -static bool sf_push_header(ScrArr *pairs, const char *name, size_t name_len, +static bool sf_push_header(ScrArr *pairs, ScrStr *name_str, ScrStr *value, bool request_guard) { + const char *name = name_str->data; + size_t name_len = name_str->len; if (!sf_header_name_ok(name, name_len)) { ScrStr *display = scr_str_new(name, name_len); sf_headers_invalid("append", display, "name"); @@ -3396,7 +3506,7 @@ static bool sf_push_header(ScrArr *pairs, const char *name, size_t name_len, if (!sf_header_name_ci(name, name_len, "set-cookie")) { size_t n = (size_t)scr_arr_len(pairs); for (size_t i = 0; i + 1 < n; i += 2) { - ScrStr *key = scr_arr_get_ref(pairs, (double)i); + ScrStr *key = sf_pair_at(pairs, i); bool match = key->len == name_len; for (size_t j = 0; j < name_len && match; j++) { unsigned char a = (unsigned char)key->data[j]; @@ -3405,11 +3515,9 @@ static bool sf_push_header(ScrArr *pairs, const char *name, size_t name_len, if (b >= 'A' && b <= 'Z') b = (unsigned char)(b - 'A' + 'a'); match = a == b; } - scr_str_release(key); if (!match) continue; - ScrStr *previous = - scr_arr_get_ref(pairs, (double)(i + 1)); + ScrStr *previous = sf_pair_at(pairs, i + 1); const char *separator = sf_header_name_ci(name, name_len, "cookie") ? "; " : ", "; size_t separator_len = 2; @@ -3423,7 +3531,6 @@ static bool sf_push_header(ScrArr *pairs, const char *name, size_t name_len, normalized->data, normalized->len); ScrStr *joined = scr_str_new(joined_data, joined_len); free(joined_data); - scr_str_release(previous); scr_str_release(normalized); if (request_guard && !sf_request_header_ok(name, name_len, joined)) { scr_str_release(joined); @@ -3442,8 +3549,8 @@ static bool sf_push_header(ScrArr *pairs, const char *name, size_t name_len, scr_arr_push_ref( pairs, request_guard - ? scr_str_new(name, name_len) - : sf_header_name_lower(name, name_len)); + ? scr_str_retain(name_str) + : sf_header_name_lower_str(name_str)); scr_arr_push_ref(pairs, normalized); return true; } @@ -3470,10 +3577,13 @@ static void sf_headers_pair_length_error(size_t len) { * leaves header-name/value syntax validation for initialization. Response * construction converts this snapshot before status/statusText and only * validates it after body extraction, matching Undici's two phases. */ -static ScrArr *sf_headers_convert_init(const ScrDyn *headers) { +/* *canonical_out reports that the result copies a canonical Headers + * object's pairs (SfHeaders.canonical). */ +static ScrArr *sf_headers_convert_init(const ScrDyn *headers, bool *canonical_out) { + *canonical_out = false; if (headers && headers->kind == SCR_DYN_TYPED_REF) { ScrDyn *materialized = scr_dyn_typed_ref_materialize(headers); - ScrArr *converted = sf_headers_convert_init(materialized); + ScrArr *converted = sf_headers_convert_init(materialized, canonical_out); scr_dyn_release(materialized); return converted; } @@ -3564,13 +3674,9 @@ static ScrArr *sf_headers_convert_init(const ScrDyn *headers) { headers->v.handle.tag == SCR_DYNH_FETCH_HEADERS) { SfHeaders *source = (SfHeaders *)headers->v.handle.ptr; size_t n = (size_t)scr_arr_len(source->pairs); - for (size_t i = 0; i + 1 < n; i += 2) { - ScrStr *name = scr_arr_get_ref(source->pairs, (double)i); - ScrStr *value = - scr_arr_get_ref(source->pairs, (double)(i + 1)); - scr_arr_push_ref(converted, name); - scr_arr_push_ref(converted, value); - } + for (size_t i = 0; i < n - n % 2; i++) + scr_arr_push_ref(converted, scr_str_retain(sf_pair_at(source->pairs, i))); + *canonical_out = source->canonical; return converted; } /* Functions are WebIDL objects and an ordinary function has no @@ -3585,25 +3691,29 @@ fail: } static bool sf_add_converted_headers( - ScrArr *pairs, ScrArr *converted, bool request_guard) { + ScrArr *pairs, ScrArr *converted, bool request_guard, bool canonical) { size_t n = (size_t)scr_arr_len(converted); + /* Pushing canonical pairs into an empty list one by one through + * sf_push_header would reproduce them exactly. */ + if (canonical && !request_guard && scr_arr_len(pairs) == 0) { + for (size_t i = 0; i < n; i++) + scr_arr_push_ref(pairs, scr_str_retain(sf_pair_at(converted, i))); + return true; + } for (size_t i = 0; i + 1 < n; i += 2) { - ScrStr *name = scr_arr_get_ref(converted, (double)i); - ScrStr *value = scr_arr_get_ref(converted, (double)(i + 1)); - bool ok = sf_push_header( - pairs, name->data, name->len, value, request_guard); - scr_str_release(name); - scr_str_release(value); - if (!ok) return false; + if (!sf_push_header(pairs, sf_pair_at(converted, i), + sf_pair_at(converted, i + 1), request_guard)) + return false; } return true; } static bool sf_add_headers( ScrArr *pairs, const ScrDyn *headers, bool request_guard) { - ScrArr *converted = sf_headers_convert_init(headers); + bool canonical = false; + ScrArr *converted = sf_headers_convert_init(headers, &canonical); if (!converted) return false; - bool ok = sf_add_converted_headers(pairs, converted, request_guard); + bool ok = sf_add_converted_headers(pairs, converted, request_guard, canonical); scr_arr_release(converted); return ok; } @@ -4719,11 +4829,71 @@ static ScrDyn *sf_response_json_serialize(const ScrDyn *value) { return serialized; } +/* The lightweight header form of a constructor's init headers (see + * SfResponse.light): absent headers are form 1 (Response.json's plain + * {"content-type": "application/json"} record is form 3), a Headers object + * or pair list form 2, and a plain record form 3, whose names and values + * *record receives in order (an array value contributes one entry per + * element, as writeHead writes it). Response.json with init headers is + * form 2 whatever their shape: the package's Response.json passes them on + * as a Headers object, adding the JSON content type when they lack one. + * 0 for any other init. */ +static int8_t sf_light_header_form(const ScrDyn *headers, bool json_body, + ScrArr **record) { + if (!headers || headers->kind == SCR_DYN_UNDEF) { + if (!json_body) return 1; + *record = scr_arr_new(SCR_ELEM_STR, 2); + sf_push_header_text(*record, "content-type", "application/json", 16); + return 3; + } + if (json_body) return 2; + if (headers->kind == SCR_DYN_ARR || + (headers->kind == SCR_DYN_HANDLE && + headers->v.handle.tag == SCR_DYNH_FETCH_HEADERS)) + return 2; + ScrDyn *materialized = NULL; + if (headers->kind == SCR_DYN_TYPED_REF) { + materialized = scr_dyn_typed_ref_materialize(headers); + if (!materialized) return 0; + headers = materialized; + } + int8_t form = 0; + if (headers->kind == SCR_DYN_OBJ) { + ScrArr *out = scr_arr_new(SCR_ELEM_STR, 2 * headers->v.obj.len); + form = 3; + for (size_t i = 0; i < headers->v.obj.len && form == 3; i++) { + const ScrDynEntry *e = &headers->v.obj.entries[i]; + const ScrDyn *value = e->value; + size_t count = value->kind == SCR_DYN_ARR ? value->v.arr.len : 1; + for (size_t j = 0; j < count; j++) { + const ScrDyn *item = value->kind == SCR_DYN_ARR ? value->v.arr.items[j] : value; + if (item->kind != SCR_DYN_STR && item->kind != SCR_DYN_NUM && + item->kind != SCR_DYN_BOOL) { + form = 0; + break; + } + ScrStr *text = scr_dyn_string_coerce_js(item); + if (!text) { + form = 0; + break; + } + scr_arr_push_ref(out, scr_str_new(e->key, e->key_len)); + scr_arr_push_ref(out, text); + } + } + if (form == 3) *record = out; + else scr_arr_release(out); + } + scr_dyn_release(materialized); + return form; +} + static ScrDyn *sf_response_construct(ScrDyn *body, ScrDyn *init, const ScrDyn *json_data, SfJsonSerialize serialize) { ScrArr *header_pairs = NULL; ScrArr *converted_headers = NULL; + bool converted_canonical = false; ScrBytes *body_bytes = NULL; ScrStr *status_text = NULL; SfStream *stream = NULL; @@ -4733,6 +4903,23 @@ static ScrDyn *sf_response_construct(ScrDyn *body, ScrDyn *init, bool text_body = false; int status = 200; bool status_range_error = false; + /* The lightweight-response classification (see SfResponse.light): a + * Response init qualifies only while it is itself still light. */ + bool light_ok = !(init && init->kind == SCR_DYN_HANDLE && + init->v.handle.tag == SCR_DYNH_FETCH_RESPONSE && + ((SfResponse *)init->v.handle.ptr)->light == 0); + if (!json_body && !null_body) { + if (body->kind == SCR_DYN_BYTES) + light_ok = light_ok && body->v.bytes->elem == SCR_BYTES_U8 && + !body->v.bytes->is_data_view; + else if (!(body->kind == SCR_DYN_STR || + (body->kind == SCR_DYN_HANDLE && + body->v.handle.tag == SCR_DYNH_WEB_STREAM))) + light_ok = false; + } + int8_t light_form = 0; + ScrArr *light_record = NULL; + size_t light_length_bytes = 0; /* WebIDL converts constructor arguments from left to right. BodyInit's * string arm therefore runs before ResponseInit's dictionary conversion, @@ -4770,7 +4957,8 @@ static ScrDyn *sf_response_construct(ScrDyn *body, ScrDyn *init, * coercions precede status and statusText. Header syntax validation is a * later response-initialization step, after body extraction. */ ScrDyn *init_headers = sf_response_init_get(init, "headers", 7); - converted_headers = sf_headers_convert_init(init_headers); + if (light_ok) light_form = sf_light_header_form(init_headers, json_body, &light_record); + converted_headers = sf_headers_convert_init(init_headers, &converted_canonical); scr_dyn_release(init_headers); if (!converted_headers) goto fail; @@ -4826,6 +5014,7 @@ static ScrDyn *sf_response_construct(ScrDyn *body, ScrDyn *init, scr_str_release(encoding); scr_dyn_release(serialized); stream = sf_stream_new_native(); + light_length_bytes = body_bytes->len; sf_stream_enqueue_bytes(stream, body_bytes); sf_stream_close(stream); scr_bytes_release(body_bytes); @@ -4844,6 +5033,7 @@ static ScrDyn *sf_response_construct(ScrDyn *body, ScrDyn *init, sf_stream_retain(stream); } else { stream = sf_stream_new_native(); + light_length_bytes = body_bytes->len; sf_stream_enqueue_bytes(stream, body_bytes); sf_stream_close(stream); scr_bytes_release(body_bytes); @@ -4863,7 +5053,7 @@ static ScrDyn *sf_response_construct(ScrDyn *body, ScrDyn *init, header_pairs = scr_arr_new(SCR_ELEM_STR, 8); if (!sf_add_converted_headers( - header_pairs, converted_headers, false)) goto fail; + header_pairs, converted_headers, false, converted_canonical)) goto fail; scr_arr_release(converted_headers); converted_headers = NULL; @@ -4877,9 +5067,11 @@ static ScrDyn *sf_response_construct(ScrDyn *body, ScrDyn *init, sf_type_error(message); goto fail; } + bool auto_ct = false; if (text_body && !sf_pairs_have(header_pairs, "content-type")) { sf_push_header_text(header_pairs, "content-type", "text/plain;charset=UTF-8", 24); + auto_ct = true; } if (json_body && !sf_pairs_have(header_pairs, "content-type")) { static const char json_type[16] = "application/json"; @@ -4892,11 +5084,20 @@ static ScrDyn *sf_response_construct(ScrDyn *body, ScrDyn *init, response->rc = 1; response->body = stream; response->headers = sf_headers_new(header_pairs, false); + response->headers->canonical = sf_header_values_trimmed(header_pairs); response->url = scr_str_new("", 0); response->status_text = status_text; response->status = status; response->redirected = false; response->null_body = null_body; + response->light = light_form; + response->auto_ct = auto_ct; + response->light_record = light_record; + light_record = NULL; + response->light_length = + null_body ? -1.0 + : (!json_body && body->kind == SCR_DYN_HANDLE) ? -2.0 + : (double)light_length_bytes; ScrDyn *out = scr_dyn_new_handle(response, SCR_DYNH_FETCH_RESPONSE); sf_response_release(response); @@ -4908,9 +5109,43 @@ fail: scr_str_release(status_text); scr_arr_release(header_pairs); scr_arr_release(converted_headers); + scr_arr_release(light_record); return NULL; } +/* @hono/node-server's built-in implementation reads a Response's + * lightweight state to write it as the package does: [form, flags, + * byte length, ...raw record name/value pairs], with form as in + * SfResponse.light ("0" alone when the response is not light), flags "d" + * (the package's Headers hold its default content type) and "a" (the + * constructor added the text content type), and the length -1 for a null + * body or -2 for a stream. */ +ScrArr *scr_fetch_response_light(ScrDyn *response) { + ScrArr *out = scr_arr_new(SCR_ELEM_STR, 4); + if (!response || response->kind != SCR_DYN_HANDLE || + response->v.handle.tag != SCR_DYNH_FETCH_RESPONSE) { + scr_arr_push_ref(out, scr_str_new("0", 1)); + return out; + } + SfResponse *r = response->v.handle.ptr; + char buf[32]; + int n = snprintf(buf, sizeof buf, "%d", (int)r->light); + scr_arr_push_ref(out, scr_str_new(buf, (size_t)n)); + if (r->light == 0) return out; + char flags[3]; + size_t f = 0; + if (r->light_default_ct) flags[f++] = 'd'; + if (r->auto_ct) flags[f++] = 'a'; + scr_arr_push_ref(out, scr_str_new(flags, f)); + n = snprintf(buf, sizeof buf, "%.0f", r->light_length); + scr_arr_push_ref(out, scr_str_new(buf, (size_t)n)); + if (r->light == 3 && r->light_record) { + for (size_t i = 0; i < r->light_record->len; i++) + scr_arr_push_ref(out, scr_arr_get_ref(r->light_record, (double)i)); + } + return out; +} + ScrDyn *scr_fetch_response_new(ScrDyn *body, ScrDyn *init) { return sf_response_construct(body, init, NULL, NULL); } @@ -4957,7 +5192,7 @@ static ScrDyn *sf_request_get(void *ptr, const char *key, size_t len) { if (default_value) { ScrStr *s = scr_str_new(default_value, strlen(default_value)); ScrDyn *v = scr_dyn_new_str(s); scr_str_release(s); return v; } if (sf_name(key, len, "keepalive") || sf_name(key, len, "isReloadNavigation") || sf_name(key, len, "isHistoryNavigation")) return scr_dyn_new_bool(false); if (sf_name(key, len, "body") || sf_name(key, len, "bodyUsed") || sf_name(key, len, "headers")) return sf_response_get(r->payload, key, len); - return scr_dyn_undefined(); + return NULL; } static ScrDyn *sf_request_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what) { SfRequest *r = ptr; @@ -4965,13 +5200,15 @@ static ScrDyn *sf_request_invoke(void *ptr, ScrDyn *self, const char *method, Sc } ScrDyn *scr_fetch_headers_new(ScrDyn *init) { - ScrArr *converted = sf_headers_convert_init(init); + bool canonical = false; + ScrArr *converted = sf_headers_convert_init(init, &canonical); if (!converted) return NULL; ScrArr *pairs = scr_arr_new(SCR_ELEM_STR, 8); - bool ok = sf_add_converted_headers(pairs, converted, false); + bool ok = sf_add_converted_headers(pairs, converted, false, canonical); scr_arr_release(converted); if (!ok) { scr_arr_release(pairs); return NULL; } SfHeaders *headers = sf_headers_new(pairs, false); + headers->canonical = sf_header_values_trimmed(pairs); ScrDyn *result = scr_dyn_new_handle(headers, SCR_DYNH_FETCH_HEADERS); sf_headers_release(headers); return result; @@ -5462,32 +5699,70 @@ static bool sf_no_set(void *ptr, const char *key, size_t len, return false; } +/* The members invoke answers, as values (ScrDynHandleMethod), with Node's + * lengths and the brand check each one runs on another receiver. The + * signal's EventTarget methods are EventTarget's own (sf_signal_get). */ +static const ScrDynHandleMethod sf_signal_methods[] = { + {"throwIfAborted", 0, false}, {NULL, 0, false}}; +static const ScrDynHandleMethod sf_stream_methods[] = { + {"cancel", 0, true}, {"getReader", 0, false}, {NULL, 0, false}}; +static const ScrDynHandleMethod sf_reader_methods[] = { + {"read", 0, true}, {"releaseLock", 0, false}, {"cancel", 0, true}, + {NULL, 0, false}}; +static const ScrDynHandleMethod sf_body_methods[] = { + {"json", 0, true}, {"text", 0, true}, {"bytes", 0, true}, + {"arrayBuffer", 0, true}, {NULL, 0, false}}; +static const ScrDynHandleMethod sf_headers_methods[] = { + {"append", 2, false}, {"delete", 1, false}, {"entries", 0, false}, + {"forEach", 1, false}, {"get", 1, false}, {"getSetCookie", 0, false}, + {"has", 1, false}, {"keys", 0, false}, {"set", 2, false}, + {"values", 0, false}, {NULL, 0, false}}; + +static ScrDyn *sf_rejected(void) { + ScrPromise *p = scr_promise_settled_ref(NULL, &scr_dyn_retain_v, + &scr_dyn_release_v, NULL); + ScrDyn *out = scr_dyn_new_promise(p); + scr_promise_release(p); + return out; +} + static const ScrDynHandleOps sf_signal_ops = { "AbortSignal", &sf_signal_retain_v, &sf_signal_release_v, - &sf_signal_invoke, &sf_signal_get, &sf_signal_set, NULL}; + &sf_signal_invoke, &sf_signal_get, &sf_signal_set, NULL, + .methods = sf_signal_methods, + .invalid_this = "Value of \"this\" must be of type AbortSignal"}; static const ScrDynHandleOps sf_abort_controller_ops = { "AbortController", &sf_signal_retain_v, &sf_signal_release_v, &sf_abort_controller_invoke, &sf_abort_controller_get, &sf_no_set, NULL}; static const ScrDynHandleOps sf_stream_ops = { "ReadableStream", &sf_stream_retain_v, &sf_stream_release_v, - &sf_stream_invoke, &sf_stream_get, &sf_no_set, NULL}; + &sf_stream_invoke, &sf_stream_get, &sf_no_set, NULL, + .methods = sf_stream_methods, + .invalid_this = "Value of \"this\" must be of type ReadableStream", + .rejected = &sf_rejected}; static const ScrDynHandleOps sf_reader_ops = { "ReadableStreamDefaultReader", &sf_reader_handle_retain_v, &sf_reader_handle_release_v, &sf_reader_invoke, &sf_reader_get, - &sf_no_set, NULL}; + &sf_no_set, NULL, .methods = sf_reader_methods, + .invalid_this = + "Value of \"this\" must be of type ReadableStreamDefaultReader", + .rejected = &sf_rejected}; static const ScrDynHandleOps sf_controller_ops = { "ReadableStreamDefaultController", &sf_stream_retain_v, &sf_stream_release_v, &sf_controller_invoke, &sf_controller_get, &sf_no_set, NULL}; static const ScrDynHandleOps sf_request_ops = { "Request", &sf_request_retain_v, &sf_request_release_v, - &sf_request_invoke, &sf_request_get, &sf_no_set, NULL}; + &sf_request_invoke, &sf_request_get, &sf_no_set, NULL, + .methods = sf_body_methods, .rejected = &sf_rejected}; static const ScrDynHandleOps sf_response_ops = { "Response", &sf_response_retain_v, &sf_response_release_v, - &sf_response_invoke, &sf_response_get, &sf_no_set, NULL}; + &sf_response_invoke, &sf_response_get, &sf_no_set, NULL, + .methods = sf_body_methods, .rejected = &sf_rejected}; 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, .methods = sf_headers_methods}; static void sf_teardown(void) { while (sf_live) { @@ -5538,9 +5813,13 @@ static bool sf_no_set(void *ptr, const char *key, size_t len, const ScrDyn *valu (void)ptr; (void)key; (void)len; (void)value; return false; } +static const ScrDynHandleMethod sf_signal_methods[] = { + {"throwIfAborted", 0, false}, {NULL, 0, false}}; static const ScrDynHandleOps sf_signal_ops = { "AbortSignal", &sf_signal_retain_v, &sf_signal_release_v, - &sf_signal_invoke, &sf_signal_get, &sf_signal_set, NULL }; + &sf_signal_invoke, &sf_signal_get, &sf_signal_set, NULL, + .methods = sf_signal_methods, + .invalid_this = "Value of \"this\" must be of type AbortSignal" }; static const ScrDynHandleOps sf_abort_controller_ops = { "AbortController", &sf_signal_retain_v, &sf_signal_release_v, &sf_abort_controller_invoke, &sf_abort_controller_get, &sf_no_set, NULL }; diff --git a/packages/runtime/src/scr_generator.h b/packages/runtime/src/scr_generator.h index c3dac861d..828f339d5 100644 --- a/packages/runtime/src/scr_generator.h +++ b/packages/runtime/src/scr_generator.h @@ -181,6 +181,7 @@ ScrDyn *scr_fetch_headers_new(ScrDyn *init); ScrPromise *scr_fetch_static(ScrStr *url, ScrDyn *init); /* +1 promise */ ScrDyn *scr_fetch_response_new(ScrDyn *body, ScrDyn *init); /* borrowed args; +1 Response handle or NULL pending */ ScrDyn *scr_fetch_response_json_static(ScrDyn *data, ScrDyn *init); /* Response.json; borrowed args; +1 Response handle or NULL pending */ +ScrArr *scr_fetch_response_light(ScrDyn *response); /* @hono/node-server shim: borrowed; +1 string[] */ ScrPromise *scr_fetch_response_json(ScrDyn *response); /* +1 promise */ ScrPromise *scr_fetch_response_text(ScrDyn *response); /* +1 promise */ ScrPromise *scr_fetch_response_array_buffer(ScrDyn *response); /* +1 promise */ diff --git a/packages/runtime/src/scr_http.c b/packages/runtime/src/scr_http.c index cb88297bf..29eb52ef7 100644 --- a/packages/runtime/src/scr_http.c +++ b/packages/runtime/src/scr_http.c @@ -239,10 +239,11 @@ struct ScrHttpReq { bool close_queued; bool close_emitted; /* settled: err/close listeners dropped */ bool join_dup; /* joinDuplicateHeaders: repeated names read joined ", " */ - /* pause(): delivery holds — arrived body bytes buffer in pend (the - * parser keeps consuming; memory is the buffer, a documented bound) - * and 'end' defers (end_pending) until resume() drains through the - * emit queue, never the resuming stack. */ + /* pause(): delivery holds — arrived body bytes buffer in pend and + * 'end' defers (end_pending) until resume() drains through the emit + * queue, never the resuming stack. A held HTTP/1 server request stops + * its socket's reads once pend reaches SCR_HTTP_REQ_HOLD_BYTES + * (sock_held), as Node's readStop does. */ bool paused; bool end_pending; bool drain_queued; @@ -257,6 +258,7 @@ struct ScrHttpReq { bool received; /* the whole body arrived (req.complete) */ bool body_seen; /* body bytes arrived */ bool destroyed; /* req.destroy()/the teardown ran — req.destroyed */ + bool sock_held; /* this request stopped its socket's reads */ ScrNetLs aborted_ls; /* req.on('aborted') — the h2 compat event */ }; @@ -330,7 +332,10 @@ void scr_http_req_release(ScrHttpReq *r) { scr_http_client_release(r->pipe_client); scr_net_sock_release(r->pipe_sock); free(r->pend); - if (r->sock) scr_net_sock_release(r->sock); + if (r->sock) { + if (r->sock_held) scr_net_sock_hold(r->sock, false); + scr_net_sock_release(r->sock); + } if (r->h2_stream) scr_http_h2_ops->release(r->h2_stream); #ifdef SCR_RC_AUDIT scr_http_live--; @@ -638,6 +643,22 @@ static bool scr_http_req_holding(const ScrHttpReq *r) { return r->paused || (r->gated && !r->flowing); } +/* IncomingMessage's readableHighWaterMark (its socket's, 64 KiB): a held + * request buffers this many body bytes before its socket stops reading. */ +#define SCR_HTTP_REQ_HOLD_BYTES 65536u + +/* Stops the socket's reads while an HTTP/1 server request holds a full + * buffer, and restarts them once it flows, ends or is dumped, as Node's + * readStop when push() returns false. Client responses and h2 compat + * requests keep reading. */ +static void scr_http_req_update_hold(ScrHttpReq *r) { + bool hold = r->sock != NULL && r->status == -1 && !r->http2 && !r->ended && + scr_http_req_holding(r) && r->pend_len >= SCR_HTTP_REQ_HOLD_BYTES; + if (hold == r->sock_held) return; + r->sock_held = hold; + scr_net_sock_hold(r->sock, hold); +} + static void scr_http_req_queue_drain(ScrHttpReq *r) { if (r->drain_queued) return; r->drain_queued = true; @@ -650,6 +671,7 @@ static void scr_http_req_flow(ScrHttpReq *r) { r->paused = false; r->flowing = true; if (r->pend_len > 0 || r->end_pending) scr_http_req_queue_drain(r); + scr_http_req_update_hold(r); } /* A 'data' listener or pipe() starts the flow unless pause() stopped it, @@ -658,9 +680,7 @@ static void scr_http_req_consume(ScrHttpReq *r) { if (r->gated && !r->paused && !r->flowing) scr_http_req_flow(r); } -/* resume(): releases what pause() or the initial paused state held. This - * parser always consumes body bytes from the socket (SEMANTICS.md); held - * bytes buffer in memory. */ +/* resume(): releases what pause() or the initial paused state held. */ void scr_http_req_resume(ScrHttpReq *r) { if (!scr_http_req_holding(r)) return; scr_http_req_flow(r); @@ -676,6 +696,7 @@ static void scr_http_req_dump(ScrHttpReq *r) { r->pend_len = r->pend_cap = 0; r->paused = false; r->flowing = true; + scr_http_req_update_hold(r); } /* pause(): delivery holds until resume() (scr_http_req_deliver buffers; @@ -759,6 +780,7 @@ static void scr_http_req_finish(ScrHttpReq *r, bool fire) { return; } r->ended = true; + scr_http_req_update_hold(r); r->end_consumed = fire && (r->data_ls.n > 0 || r->pipe_res || r->pipe_client || r->pipe_sock); if (fire) scr_net_fire0_this(&r->end_ls, r, SCR_DYNH_HTTP_REQ); scr_net_ls_drop(&r->data_ls); @@ -2305,6 +2327,7 @@ static void scr_http_proto_sweep(void) { req->pend_len = req->pend_cap = 0; if (held_len > 0) scr_http_req_deliver(req, held, held_len); free(held); + scr_http_req_update_hold(req); if (!scr_exc_pending() && !req->paused && req->end_pending) { req->end_pending = false; scr_http_req_finish(req, true); @@ -2758,8 +2781,8 @@ static void scr_http_req_deliver(ScrHttpReq *r, const char *data, size_t n) { if (!r || n == 0 || r->ended) return; r->body_seen = true; if (scr_http_req_holding(r)) { - /* paused: the parser keeps consuming (memory is the buffer — a - * documented bound), delivery waits for the drain */ + /* held: delivery waits for the drain; a full buffer stops the + * socket's reads */ if (r->pend_len + n > r->pend_cap) { size_t cap = r->pend_cap ? r->pend_cap : 4096; while (cap < r->pend_len + n) cap *= 2; @@ -2769,6 +2792,7 @@ static void scr_http_req_deliver(ScrHttpReq *r, const char *data, size_t n) { } memcpy(r->pend + r->pend_len, data, n); r->pend_len += n; + scr_http_req_update_hold(r); return; } bool piped = r->pipe_res != NULL || r->pipe_client != NULL || r->pipe_sock != NULL; diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 3fd57da73..da18a1c0b 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -2223,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 @@ -3414,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; @@ -3437,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; @@ -3652,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; } } @@ -3670,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; } } @@ -3678,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; @@ -3709,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) { @@ -4101,28 +4191,33 @@ 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; } @@ -4415,6 +4510,11 @@ ScrDyn *scr_dyn_class_prototype(ScrDyn *value, ScrDyn *base) { * 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); } @@ -4944,19 +5044,103 @@ ScrDyn *scr_dyn_promise_key_get(const ScrDyn *d, const ScrStr *k) { return method ? method : scr_dyn_retain(scr_dyn_undefined()); } +/* ── handle methods as values (ScrDynHandleMethod) ────────────────── + * One function per (tag, method), as Node's prototype methods are shared + * by every instance. The function calls invoke on its `this` when that is + * a handle of its tag, and otherwise raises the class's ERR_INVALID_THIS + * TypeError (or answers a promise rejected with it), as the WebIDL brand + * checks do. closure->fn holds the method entry, caps[0] the tag. */ +static SCR_TL ScrDyn **scr_dynh_method_values[SCR_DYNH_CAPACITY]; +static SCR_TL bool scr_dynh_method_values_registered; + +static void scr_dynh_method_values_cleanup(void) { + for (size_t tag = 0; tag < SCR_DYNH_CAPACITY; tag++) { + ScrDyn **values = scr_dynh_method_values[tag]; + if (!values) continue; + scr_dynh_method_values[tag] = NULL; + const ScrDynHandleMethod *methods = scr_dynh_ops[tag]->methods; + for (size_t i = 0; methods[i].name; i++) + if (values[i]) scr_dyn_release(values[i]); + free(values); + } +} + +static ScrDyn *scr_dynh_method_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + const ScrDynHandleMethod *method = closure->fn; + ScrDynHandleTag tag = (ScrDynHandleTag)scr_box_get_f64(closure->caps[0]); + const ScrDynHandleOps *ops = scr_dyn_handle_ops(tag); + ScrDyn *self = scr_dyn_this_get(); + ScrDyn *result; + if (self->kind == SCR_DYN_HANDLE && self->v.handle.tag == tag) { + result = ops->invoke(self->v.handle.ptr, self, method->name, args, argc, method->name); + } else { + const char *message = method->invalid_this ? method->invalid_this + : ops->invalid_this ? ops->invalid_this + : "Illegal invocation"; + scr_throw_error_msg_code(SCR_ERR_TYPE, message, strlen(message), "ERR_INVALID_THIS"); + result = method->rejects && ops->rejected ? ops->rejected() : NULL; + } + scr_dyn_release(self); + return result; +} + +/* The method `key` of the handle's class as a function value (+1), or NULL + * when the class has no such method. */ +static ScrDyn *scr_dynh_method_value(ScrDynHandleTag tag, const ScrDynHandleOps *ops, const char *key, size_t len) { + const ScrDynHandleMethod *methods = ops->methods; + if (!methods || !ops->invoke) return NULL; + for (size_t i = 0; methods[i].name; i++) { + if (strlen(methods[i].name) != len || memcmp(methods[i].name, key, len) != 0) continue; + if (!scr_dynh_method_values[tag]) { + size_t count = i + 1; + while (methods[count].name) count++; + scr_dynh_method_values[tag] = calloc(count, sizeof(ScrDyn *)); + if (!scr_dynh_method_values[tag]) scr_trap("scriptc: out of memory\n"); + if (!scr_dynh_method_values_registered) { + scr_dynh_method_values_registered = true; + scr_atexit(scr_dynh_method_values_cleanup); + } + } + ScrDyn **slot = &scr_dynh_method_values[tag][i]; + if (!*slot) { + ScrClosure *closure = scr_closure_new((void *)&methods[i], 1); + closure->caps[0] = scr_box_new(SCR_BOX_F64); + scr_box_set_f64(closure->caps[0], (double)tag); + *slot = scr_dyn_new_func(closure, scr_dynh_method_call, methods[i].length, "", methods[i].name); + } + return scr_dyn_retain(*slot); + } + return NULL; +} + ScrDyn *scr_dyn_handle_key_get(const ScrDyn *d, const ScrStr *k) { - const ScrDynHandleOps *ops = scr_dyn_handle_ops(d->v.handle.tag); + ScrDynHandleTag tag = d->v.handle.tag; + const ScrDynHandleOps *ops = scr_dyn_handle_ops(tag); ScrDyn *r = ops->get(d->v.handle.ptr, k->data, k->len); if (scr_exc_pending()) { scr_dyn_release(r); return NULL; } + if (!r) r = scr_dynh_method_value(tag, ops, k->data, k->len); /* Unmodeled names answer undefined — the checked-dynamic tree's own-property stance * (real-but-unmodeled members throw "not supported yet" inside ops->get instead; * SEMANTICS.md documents the remainder). */ return r ? r : scr_dyn_retain(scr_dyn_undefined()); } +/* `key in handle`: the class's methods, and the properties its get models. */ +static bool scr_dyn_handle_in(const ScrDyn *d, const ScrStr *k) { + const ScrDynHandleOps *ops = scr_dyn_handle_ops(d->v.handle.tag); + const ScrDynHandleMethod *methods = ops->invoke ? ops->methods : NULL; + for (size_t i = 0; methods && methods[i].name; i++) + if (strlen(methods[i].name) == k->len && memcmp(methods[i].name, k->data, k->len) == 0) return true; + if (!methods) return false; + ScrDyn *value = ops->get(d->v.handle.ptr, k->data, k->len); + if (!value) return scr_dyn_object_proto_name(k); + scr_dyn_release(value); + return true; +} + /* ── the ambient receiver (scr_runtime.h's design note) ─────────────── * A strictly nested push/pop stack: firing sites bind the owner around * each listener call, dyn OBJ method dispatch binds the object, and the @@ -5048,6 +5232,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 @@ -8010,35 +8196,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); @@ -8134,8 +8321,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); @@ -9152,6 +9351,7 @@ bool scr_dyn_in(const ScrDyn *v, const ScrStr *key) { } if (v->kind == SCR_DYN_PROXY) return scr_dyn_proxy_has(v, key); if (v->kind == SCR_DYN_BYTES) return scr_dyn_bytes_in(v, key); + if (v->kind == SCR_DYN_HANDLE) return scr_dyn_handle_in(v, key); if (scr_dyn_has_key(v, key)) return true; if (scr_exc_pending()) return false; if (v->kind == SCR_DYN_OBJ) return scr_dyn_builtin_inherited(v, key); @@ -13698,19 +13898,74 @@ ScrDyn *scr_dyn_object_rest(const ScrDyn *source, const ScrDyn *excluded) { /* Compiler-owned native class views retain every property and its flags, * including nonenumerable data, without invoking accessors. */ -ScrDyn *scr_dyn_copy_property_descriptors(ScrDyn *target, const ScrDyn *src) { - if (!src || src->kind == SCR_DYN_UNDEF) return scr_dyn_retain(target); - ScrDyn *keys = scr_dyn_obj_own_keys(src); - if (!keys) return NULL; - for (size_t i = 0; i < keys->v.arr.len; i++) { - ScrDyn *key = keys->v.arr.items[i]; - ScrDyn *descriptor = scr_dyn_own_descriptor(src, key->v.str); +static void scr_dyn_copy_property_descriptor(ScrDyn *target, const ScrDyn *src, + const ScrDynEntry *from) { + /* Exactly scr_dyn_define_property(target, key, own descriptor of key) + * when `target` has no such property and accepts new ones: the entry + * takes the source's attributes, value or accessor pair. */ + ScrDynEntry *prior = scr_dyn_find_entry(target, from->key, from->key_len); + if (prior || target->non_extensible) { + ScrStr *name = scr_str_new(from->key, from->key_len); + ScrDyn *key = scr_dyn_new_str(name); + ScrDyn *descriptor = scr_dyn_own_descriptor(src, name); ScrDyn *result = descriptor ? scr_dyn_define_property(target, key, descriptor) : NULL; scr_dyn_release(descriptor); scr_dyn_release(result); - if (scr_exc_pending()) break; + scr_dyn_release(key); + scr_str_release(name); + return; + } + /* The names whose definition may move into a native Error's slots + * (scr_error_sync_slots). */ + static const char *const slots[] = {"cause", "stack", "name", "message", "code"}; + bool slot = false; + for (size_t i = 0; i < sizeof slots / sizeof *slots && !slot; i++) + slot = from->key_len == strlen(slots[i]) && memcmp(from->key, slots[i], from->key_len) == 0; + bool accessor = from->accessor, writable = !accessor && from->writable; + bool enumerable = from->enumerable, configurable = from->configurable; + ScrDyn *value = scr_dyn_retain(accessor ? scr_dyn_undefined() : from->value); + ScrDyn *getter = accessor && from->getter ? scr_dyn_retain(from->getter) : NULL; + ScrDyn *setter = accessor && from->setter ? scr_dyn_retain(from->setter) : NULL; + ScrStr *name = slot ? scr_str_new(from->key, from->key_len) : NULL; + scr_dyn_obj_set(target, from->key, from->key_len, value); + ScrDynEntry *entry = &target->v.obj.entries[target->v.obj.len - 1]; + entry->accessor = accessor; + entry->getter = getter; + entry->setter = setter; + entry->writable = writable; + entry->enumerable = enumerable; + entry->configurable = configurable; + if (name) { + scr_error_sync_cause(target, name); + scr_str_release(name); + } +} + +ScrDyn *scr_dyn_copy_property_descriptors(ScrDyn *target, const ScrDyn *src) { + if (!src || src->kind == SCR_DYN_UNDEF) return scr_dyn_retain(target); + if (src->kind == SCR_DYN_OBJ && target->kind == SCR_DYN_OBJ && src != target && + src != scr_builtin_object_prototype && target != scr_builtin_error_prototype) { + /* Plain objects on both sides: no fence, proxy trap or accessor runs, + * so each property is copied from its entry without building the key + * and descriptor objects the general path goes through. */ + if (src->v.obj.len) scr_dyn_region_store(target, NULL); + size_t *order = scr_dyn_obj_key_order(src); + for (size_t i = 0; i < src->v.obj.len && !scr_exc_pending(); i++) + scr_dyn_copy_property_descriptor(target, src, &src->v.obj.entries[order ? order[i] : i]); + free(order); + } else { + ScrDyn *keys = scr_dyn_obj_own_keys(src); + if (!keys) return NULL; + for (size_t i = 0; i < keys->v.arr.len; i++) { + ScrDyn *key = keys->v.arr.items[i]; + ScrDyn *descriptor = scr_dyn_own_descriptor(src, key->v.str); + ScrDyn *result = descriptor ? scr_dyn_define_property(target, key, descriptor) : NULL; + scr_dyn_release(descriptor); + scr_dyn_release(result); + if (scr_exc_pending()) break; + } + scr_dyn_release(keys); } - scr_dyn_release(keys); if (!scr_exc_pending()) { ScrDyn *symbols = scr_dyn_get_own_property_symbols(src); if (!symbols) return NULL; @@ -16044,9 +16299,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_net.c b/packages/runtime/src/scr_net.c index 29a4af393..4d5a37f77 100644 --- a/packages/runtime/src/scr_net.c +++ b/packages/runtime/src/scr_net.c @@ -556,6 +556,10 @@ struct ScrNetSocket { * unconsumed stream). resume() clears user_paused and sets flowing. */ bool user_paused; bool flowing; + /* scr_net_sock_hold: a protocol consumer (an HTTP/1 server request + * holding unread body bytes) stops reads without touching the user's + * pause state */ + bool hold; /* EOF arrived while the socket buffered for a consumer it does not have * yet: 'end' waits until one drains the buffer (Node's readable state) */ bool eof_pending; @@ -978,7 +982,7 @@ static ScrNetSocket *scr_net_sock_new(void) { #define SCR_NET_READ_AHEAD 65536u static void scr_net_sock_update_read(ScrNetSocket *s) { - bool want = s->fd >= 0 && !s->connecting && !s->rd_eof && !s->user_paused && + bool want = s->fd >= 0 && !s->connecting && !s->rd_eof && !s->user_paused && !s->hold && (s->data_ls.n > 0 || s->pipe_dst != NULL || s->native_data != NULL || (s->tunnel != NULL && !s->tunnel->send_only) /* the proxy's CONNECT answer */ || s->flowing /* resume(): flow (and discard sans listeners) */ || @@ -1303,7 +1307,7 @@ static void scr_net_sock_read(ScrNetSocket *s) { if (s->tunnel != NULL) return; /* readiness feeds the tunnel pump instead */ if (s->tops && !s->t_est) return; /* readiness feeds the handshake pump instead */ for (;;) { - if (s->fd < 0 || s->user_paused) return; + if (s->fd < 0 || s->user_paused || s->hold) return; bool flowing = s->data_ls.n > 0 || s->pipe_dst || s->native_data || s->flowing; /* buffered (peeked/unshifted) bytes first — they precede the * kernel's in the stream; a TLS engine drains them through its bio */ @@ -3102,6 +3106,15 @@ ScrNetSocket *scr_net_sock_pause(ScrNetSocket *s) { return scr_net_sock_retain(s); } +/* Stops (hold) or restarts reads for a protocol consumer that has + * buffered as much as it accepts, as Node's readStop when push() returns + * false. Separate from pause()/resume(), which stay the user's. */ +void scr_net_sock_hold(ScrNetSocket *s, bool hold) { + if (s->hold == hold) return; + s->hold = hold; + scr_net_sock_update_read(s); +} + /* socket.resume(): flowing mode — a consumer even with no 'data' * listener (arrived bytes discard, so 'end' can be reached, Node's * resumed-but-unconsumed stream). Buffered bytes deliver from the next @@ -5271,7 +5284,7 @@ static bool scr_net_flags_pending(void) { * plaintext inside a transport, or unshifted bytes in the receive * buffer: the poller can't re-signal them, the sweep must deliver */ bool consumers = (s->data_ls.n > 0 || s->pipe_dst || s->native_data || s->flowing) && - !s->user_paused; + !s->user_paused && !s->hold; if (consumers && s->fd >= 0) { if (s->tops && s->t_est && s->tops->pending && s->tops->pending(s->tctx)) return true; if (!s->tops && s->rlen > s->rhead) return true; @@ -5377,7 +5390,7 @@ static void scr_net_sweep(void) { while (sock) { ScrNetSocket *next = sock->next; /* may unregister below */ scr_net_sock_retain(sock); /* callbacks may drop every other ref */ - if (sock->fd >= 0 && !sock->user_paused && + if (sock->fd >= 0 && !sock->user_paused && !sock->hold && (sock->data_ls.n > 0 || sock->pipe_dst || sock->native_data || sock->flowing) && ((sock->tops && sock->t_est && sock->tops->pending && sock->tops->pending(sock->tctx)) || (!sock->tops && sock->rlen > sock->rhead))) { diff --git a/packages/runtime/src/scr_net.h b/packages/runtime/src/scr_net.h index ffa9b2f72..6df6945ea 100644 --- a/packages/runtime/src/scr_net.h +++ b/packages/runtime/src/scr_net.h @@ -232,6 +232,7 @@ void scr_net_sock_destroy(ScrNetSocket *s); * counters/flags the compat surface reads. */ ScrNetSocket *scr_net_sock_pause(ScrNetSocket *s); /* +1: chaining */ ScrNetSocket *scr_net_sock_resume(ScrNetSocket *s); /* +1: chaining */ +void scr_net_sock_hold(ScrNetSocket *s, bool hold); /* protocol read hold (not pause) */ ScrNetSocket *scr_net_sock_set_nodelay(ScrNetSocket *s, bool enable); /* +1: chaining */ void scr_net_sock_set_keepalive(ScrNetSocket *s, bool enable, double delay_ms); void scr_net_sock_destroy_soon(ScrNetSocket *s); 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_stream.c b/packages/runtime/src/scr_stream.c index 275c03039..cd95e12cd 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -269,6 +269,7 @@ struct ScrStreamState { * destroyed and runs its _destroy once construction completes. */ bool constructing, destroy_deferred; bool construct_finished; /* onConstruct ran: a second callback is ERR_MULTIPLE_CALLBACK */ + bool construct_called; /* the construct tick called the hook */ ScrError *destroy_err; /* owned: the deferred destroy's error */ ScrClosure *construct_cb; ScrStreamPlainInv construct_inv; @@ -2157,6 +2158,24 @@ static ScrDyn *scr_stream_wentry_encoding(const ScrStreamState *st, ScrDyn *enc) return scr_dyn_retain(enc ? enc : scr_dyn_undefined()); } +/* A compiled writev whose entries declare `encoding` a string: an entry + * without one (an object-mode write passes undefined or null) gets "". + * The batch array is the one this call receives. */ +void scr_stream_writev_fill_encodings(ScrDyn *chunks) { + if (!chunks || chunks->kind != SCR_DYN_ARR) return; + double n = scr_dyn_arr_len(chunks); + for (double i = 0; i < n; i++) { + ScrDyn *entry = scr_dyn_arr_at(chunks, i); /* +1 */ + ScrDyn *enc = entry->kind == SCR_DYN_OBJ ? scr_dyn_obj_get(entry, "encoding", 8) : NULL; + if (entry->kind == SCR_DYN_OBJ && (!enc || enc->kind == SCR_DYN_UNDEF || enc->kind == SCR_DYN_NULL)) { + ScrStr *empty = scr_str_new("", 0); + scr_dyn_obj_set(entry, "encoding", 8, scr_dyn_new_str(empty)); + scr_str_release(empty); + } + scr_dyn_release(entry); + } +} + /* 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; @@ -2675,9 +2694,8 @@ void *scr_stream_native(ScrStream *s, void (*release)(void *)) { * way the compiler-emitted dyn thunks do (chunk as Buffer-flavored bytes, * encoding 'buffer', the error via the boundary encoding, the completion * callback as a callable dyn over the *_done glue). Options that Node's - * base WOULD consume but have no lowering here (objectMode truthy, - * decodeStrings:false, non-utf8 defaultEncoding, signal, construct, - * writev, and read/write/final on the transformish bases) throw the + * base WOULD consume but have no lowering here (signal, and + * read/write/final on the transformish bases) throw the * "not supported yet" Error — the compile-time fence's runtime twin. */ /* The boxed dyn callable (+1 — scr_box_get_ref retains; callers release @@ -2903,11 +2921,8 @@ static bool scr_stream_parse_dyn_opts(const ScrDyn *opts, char base, ScrStreamDy } } out->construct = scr_stream_dynopt_fn(opts, "construct", 9); + /* a Transform's batches reach writev, not the transform, as in Node */ 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); @@ -6296,6 +6311,55 @@ ScrStream *scr_stream_set_default_encoding(ScrStream *s, ScrDyn *enc) { return scr_stream_retain(s); } +/* Whether a defaultEncoding option value is the default: null, undefined, + * 'utf8' or 'utf-8' (the state constructors' kDefaultUTF8Encoding). */ +static bool scr_stream_default_encoding_value(const ScrDyn *v) { + if (!v || v->kind == SCR_DYN_UNDEF || v->kind == SCR_DYN_NULL) return true; + return 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)); +} + +/* A runtime defaultEncoding option value, checked before construction as + * the state constructors check it: the default, a name Buffer.isEncoding + * knows, or ERR_UNKNOWN_ENCODING (thrown). Borrows v. */ +void scr_stream_check_encoding_option(ScrDyn *v) { + if (scr_stream_default_encoding_value(v)) return; + if (v->kind == SCR_DYN_STR) { + ScrStr *canonical = scr_bytes_normalize_encoding(v->v.str); + if (canonical) scr_str_release(canonical); + return; + } + ScrStr *shown = scr_dyn_string_coerce_js(v); + if (!shown) return; + char msg[128]; + int len = snprintf(msg, sizeof msg, "Unknown encoding: %.*s", (int)(shown->len < 64 ? shown->len : 64), + shown->data); + scr_str_release(shown); + scr_throw_error_msg_code(SCR_ERR_TYPE, msg, (size_t)(len < 0 ? 0 : len), "ERR_UNKNOWN_ENCODING"); +} + +/* Runtime decodeStrings and defaultEncoding option values over a fresh + * stream (after its objectMode): decodeStrings === false keeps string + * writes, and a known defaultEncoding is the write side's default as + * given and how a byte-mode readable side decodes push(string). Borrows + * both; answers the receiver (+1). */ +ScrStream *scr_stream_option_values(ScrStream *s, ScrDyn *decode_strings, ScrDyn *default_encoding) { + ScrStreamState *st = s->st; + if (st->has_w && decode_strings && decode_strings->kind == SCR_DYN_BOOL && !decode_strings->v.b) + scr_stream_set_decode_strings(s, false); + if (!scr_stream_default_encoding_value(default_encoding) && default_encoding->kind == SCR_DYN_STR) { + ScrStr *canonical = scr_bytes_normalize_encoding(default_encoding->v.str); + if (canonical) { + scr_stream_set_write_encoding(s, default_encoding->v.str); + if (st->has_r && !st->r.object_entries) scr_stream_release(scr_stream_set_push_encoding(s, canonical)); + scr_str_release(canonical); + } else if (scr_exc_pending()) { + scr_exc_clear(); /* checked before construction */ + } + } + return scr_stream_retain(s); +} + /* stream.objectMode: scr_stream_set_object_mode, answering the receiver * (+1). */ ScrStream *scr_stream_object_mode(ScrStream *s, bool readable, bool writable) { @@ -7460,6 +7524,17 @@ void scr_stream_set_construct(ScrStream *s, ScrClosure *cb /*moves*/, ScrStreamP scr_stream_construct(s, cb, inv); } +void scr_stream_replace_construct(ScrStream *s, ScrClosure *cb /*moves*/, ScrStreamPlainInv inv) { + ScrStreamState *st = s->st; + if (!st->constructing || st->construct_called || !st->construct_cb) { + scr_closure_release(cb); + return; + } + scr_closure_release(st->construct_cb); + st->construct_cb = cb; + st->construct_inv = 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) { @@ -7728,6 +7803,7 @@ static void scr_stream_run_tick(ScrStreamTick *t) { scr_stream_error_buffer(s); break; case SCR_ST_CONSTRUCT: { + st->construct_called = true; if (st->construct_cb) { st->construct_inv(st->construct_cb, s); /* a throw inside _construct reaches onConstruct on a tick */ diff --git a/packages/runtime/src/scr_stream.h b/packages/runtime/src/scr_stream.h index d26077b5c..a4798c629 100644 --- a/packages/runtime/src/scr_stream.h +++ b/packages/runtime/src/scr_stream.h @@ -561,8 +561,15 @@ 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); +/* `s._construct = fn` after construction: replaces a construct hook whose + * tick has not run yet (Node's constructNT reads _construct then), and has + * no effect otherwise (cb moves). */ +void scr_stream_replace_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); +/* A compiled writev whose entries declare `encoding` a string: entries + * without an encoding (object mode) get "" before the conversion. */ +void scr_stream_writev_fill_encodings(struct ScrDyn *chunks); /* 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 @@ -605,6 +612,11 @@ ScrStream *scr_stream_duplex_from_promise(struct ScrPromise *p, ScrClosure *box) * forms answer the stream (+1). */ void scr_stream_set_decode_strings(ScrStream *s, bool decode); ScrStream *scr_stream_decode_strings(ScrStream *s, bool decode); +/* Runtime decodeStrings/defaultEncoding option values: checked before + * construction (ERR_UNKNOWN_ENCODING thrown), then applied to the fresh + * stream, answering it (+1). Values borrowed. */ +void scr_stream_check_encoding_option(struct ScrDyn *v); +ScrStream *scr_stream_option_values(ScrStream *s, struct ScrDyn *decode_strings, struct ScrDyn *default_encoding); 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); diff --git a/packages/runtime/src/scr_string.c b/packages/runtime/src/scr_string.c index f94554642..37b92e48c 100644 --- a/packages/runtime/src/scr_string.c +++ b/packages/runtime/src/scr_string.c @@ -1928,6 +1928,22 @@ static __attribute__((noinline)) double scr_str_unit_search(const ScrStr *s, con /* Search backwards from a clamped UTF-16 position. A low-surrogate position * maps to its scalar's first byte, so that scalar remains searchable. */ double scr_str_last_index_of_from(ScrStr *s, ScrStr *needle, double position) { + /* A proven-ASCII receiver: byte offsets are UTF-16 indexes, and a needle + * with other bytes (including lone surrogate halves) is never found. */ + if (s->ascii) { + size_t start = isnan(position) || position >= (double)s->len ? s->len + : position <= 0 ? 0 : (size_t)trunc(position); + if (needle->len == 0) return (double)start; + if (needle->len > s->len) return -1.0; + size_t max_start = s->len - needle->len; + if (start < max_start) max_start = start; + char first = needle->data[0]; + for (size_t i = max_start + 1; i-- > 0;) { + if (s->data[i] == first && memcmp(s->data + i, needle->data, needle->len) == 0) + return (double)i; + } + return -1.0; + } ScrSidx *e = scr_sidx_for(s); size_t len16 = scr_sidx_len(s, e); size_t start16 = isnan(position) || position >= (double)len16 ? len16 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/test/test_json.c b/packages/runtime/test/test_json.c index 2c072006f..865f1c757 100644 --- a/packages/runtime/test/test_json.c +++ b/packages/runtime/test/test_json.c @@ -551,6 +551,91 @@ static void test_class_values(void) { scr_str_release(name); } +/* The class-view copy (scr_dyn_copy_property_descriptors) gives each + * property of the bag the attributes, value or accessors, and own-key + * position that defining it from its own descriptor would. */ +static ScrDyn *define_from(ScrDyn *target, const char *key, ScrDyn *value, bool writable, + bool enumerable, bool configurable) { + ScrDyn *descriptor = scr_dyn_new_obj(); + scr_dyn_obj_set(descriptor, "value", 5, value); + scr_dyn_obj_set(descriptor, "writable", 8, scr_dyn_new_bool(writable)); + scr_dyn_obj_set(descriptor, "enumerable", 10, scr_dyn_new_bool(enumerable)); + scr_dyn_obj_set(descriptor, "configurable", 12, scr_dyn_new_bool(configurable)); + ScrStr *name = S(key); + ScrDyn *k = scr_dyn_new_str(name); + ScrDyn *result = scr_dyn_define_property(target, k, descriptor); + scr_dyn_release(k); + scr_str_release(name); + scr_dyn_release(descriptor); + return result; +} + +static const ScrDynEntry *entry_of(const ScrDyn *obj, const char *key) { + for (size_t i = 0; i < obj->v.obj.len; i++) + if (obj->v.obj.entries[i].key_len == strlen(key) && + memcmp(obj->v.obj.entries[i].key, key, strlen(key)) == 0) + return &obj->v.obj.entries[i]; + return NULL; +} + +static void test_copy_property_descriptors(void) { + ScrDyn *bag = scr_dyn_new_obj(); + ScrDyn *getter = scr_dyn_new_obj(); + scr_dyn_release(define_from(bag, "label", scr_dyn_new_num(1), true, true, true)); + scr_dyn_release(define_from(bag, "hidden", scr_dyn_new_num(2), false, false, true)); + scr_dyn_obj_set(bag, "7", 1, scr_dyn_new_num(7)); + scr_dyn_obj_set(bag, "3", 1, scr_dyn_new_num(3)); + /* An accessor entry (the getter object stands in for its function). */ + scr_dyn_obj_set(bag, "computed", 8, scr_dyn_retain(scr_dyn_undefined())); + ScrDynEntry *accessor = (ScrDynEntry *)entry_of(bag, "computed"); + accessor->accessor = true; + accessor->getter = scr_dyn_retain(getter); + accessor->writable = false; + accessor->enumerable = false; + accessor->configurable = false; + + ScrDyn *view = scr_dyn_new_obj(); + scr_dyn_release(scr_dyn_copy_property_descriptors(view, bag)); + check(!scr_exc_pending() && view->v.obj.len == 5, "copy keeps every bag property"); + ScrDyn *keys = scr_dyn_obj_own_keys(view); + const char *order[] = {"3", "7", "label", "hidden", "computed"}; + bool ordered = keys->v.arr.len == 5; + for (size_t i = 0; ordered && i < 5; i++) ordered = str_is(keys->v.arr.items[i]->v.str, order[i]); + check(ordered, "copy keeps the bag's own-key order"); + scr_dyn_release(keys); + const ScrDynEntry *hidden = entry_of(view, "hidden"); + check(hidden && !hidden->accessor && hidden->value->v.num == 2 && !hidden->writable && + !hidden->enumerable && hidden->configurable, + "copy keeps data attributes"); + const ScrDynEntry *copied = entry_of(view, "computed"); + check(copied && copied->accessor && copied->getter == getter && !copied->setter && + !copied->writable && !copied->enumerable && !copied->configurable && + copied->value->kind == SCR_DYN_UNDEF, + "copy keeps an accessor without calling it"); + + /* A property the target already has is redefined like defineProperty. */ + ScrDyn *existing = scr_dyn_new_obj(); + scr_dyn_obj_set(existing, "label", 5, scr_dyn_new_num(9)); + scr_dyn_release(scr_dyn_copy_property_descriptors(existing, bag)); + const ScrDynEntry *label = entry_of(existing, "label"); + check(!scr_exc_pending() && existing->v.obj.len == 5 && existing->v.obj.entries[0].key_len == 5 && + label->value->v.num == 1, + "copy redefines an existing property in place"); + + /* A target that takes no new properties throws as defineProperty does. */ + ScrDyn *closed = scr_dyn_new_obj(); + scr_dyn_release(scr_dyn_prevent_extensions(closed)); + ScrDyn *refused = scr_dyn_copy_property_descriptors(closed, bag); + check(!refused && scr_exc_pending() && closed->v.obj.len == 0, "copy onto a non-extensible object throws"); + scr_exc_clear(); + + scr_dyn_release(closed); + scr_dyn_release(existing); + scr_dyn_release(view); + scr_dyn_release(bag); + scr_dyn_release(getter); +} + static void test_primitive_owners(void) { ScrDyn *values[] = {scr_dyn_new_null(), scr_dyn_new_bool(false), scr_dyn_new_bool(true)}; const char *texts[] = {"null", "false", "true"}; @@ -579,6 +664,7 @@ int main(void) { test_class_values(); scr_init(); test_primitive_owners(); + test_copy_property_descriptors(); /* ── primitives ─────────────────────────────────────────────────── */ ScrDyn *d = parse_ok("null", "parse null"); diff --git a/scripts/gen-cipher-info.mjs b/scripts/gen-cipher-info.mjs new file mode 100644 index 000000000..06bd8bce3 --- /dev/null +++ b/scripts/gen-cipher-info.mjs @@ -0,0 +1,125 @@ +// Writes packages/runtime/src/scr_cipher_info.h from the running Node's +// crypto.getCipherInfo: one record per cipher, the names (lowercase) and nids +// that find it, and the keyLength and ivLength options each accepts. The +// candidate names are getCiphers() and `openssl list -cipher-algorithms` +// (OpenSSL's aliases and its legacy-provider ciphers, which getCiphers() +// leaves out). Run with the pinned Node: node scripts/gen-cipher-info.mjs +import crypto from "node:crypto"; +import { execFileSync } from "node:child_process"; +import fs from "node:fs"; + +const listed = execFileSync("openssl", ["list", "-cipher-algorithms"], { encoding: "utf8" }) + .match(/[A-Za-z0-9][A-Za-z0-9._-]*/g) + .filter((word) => !["Legacy", "Provided", "default", "legacy", "base", "fips"].includes(word)); +const candidates = new Set([...crypto.getCiphers(), ...listed]); +for (let nid = 1; nid < 1400; nid++) { + const info = crypto.getCipherInfo(nid); + if (info) candidates.add(info.name); +} + +function ivRule(name, info) { + const accepted = []; + for (let v = 0; v <= 70; v++) if (crypto.getCipherInfo(name, { ivLength: v })) accepted.push(v); + const large = !!crypto.getCipherInfo(name, { ivLength: 100000 }); + const own = info.ivLength ?? 0; + if (accepted.length === 0) return "SC_INFO_NONE"; + if (accepted.length === 1 && accepted[0] === own && !large) return "SC_INFO_EXACT"; + if (accepted.join() === "7,8,9,10,11,12,13" && !large) return "SC_INFO_CCM"; + if (accepted.join() === [...Array(15)].map((_, i) => i + 1).join() && !large) + return "SC_INFO_OCB"; + if (accepted.length === 71 && large) return "SC_INFO_ANY"; + throw new Error(`unclassified ivLength rule for ${name}: ${accepted}`); +} +function keyRule(name, info) { + const accepted = []; + for (let k = 0; k <= 80; k++) if (crypto.getCipherInfo(name, { keyLength: k })) accepted.push(k); + if (accepted.length === 0) return "SC_INFO_NONE"; + if (accepted.length === 1 && accepted[0] === info.keyLength) return "SC_INFO_EXACT"; + throw new Error(`unclassified keyLength rule for ${name}: ${accepted}`); +} + +const records = []; +const recordIndex = new Map(); +function recordOf(name, info) { + const key = JSON.stringify(info); + let index = recordIndex.get(key); + const rules = [keyRule(name, info), ivRule(name, info)]; + if (index === undefined) { + index = records.length; + records.push({ info, rules }); + recordIndex.set(key, index); + } else if (records[index].rules.join() !== rules.join()) { + throw new Error(`rules differ for ${name}`); + } + return index; +} + +const names = []; +for (const name of [...candidates].map((n) => n.toLowerCase()).sort()) { + if (names.length && names.at(-1)[0] === name) continue; + const info = crypto.getCipherInfo(name); + if (info) names.push([name, recordOf(name, info)]); +} +const nids = []; +for (let nid = 1; nid < 1400; nid++) { + const info = crypto.getCipherInfo(nid); + if (info) nids.push([nid, recordOf(info.name, info)]); +} + +const str = (s) => (s === undefined ? "NULL" : JSON.stringify(s)); +const num = (n) => (n === undefined ? -1 : n); +let out = `/* Generated by scripts/gen-cipher-info.mjs from Node ${process.version}'s + * crypto.getCipherInfo: every cipher OpenSSL names, with the fields Node + * reports (-1 or NULL for a field it leaves out) and the keyLength and + * ivLength options each accepts. Do not edit. */ +#ifndef SCR_CIPHER_INFO_H +#define SCR_CIPHER_INFO_H + +enum { SC_INFO_NONE, SC_INFO_EXACT, SC_INFO_CCM, SC_INFO_OCB, SC_INFO_ANY }; + +typedef struct { + const char *mode; + const char *name; + int nid; + int block_size; + int iv_length; + int key_length; + unsigned char key_rule; + unsigned char iv_rule; +} ScCipherInfo; + +static const ScCipherInfo sc_cipher_infos[] = { +`; +for (const { info, rules } of records) + out += ` {${str(info.mode)}, ${str(info.name)}, ${num(info.nid)}, ${num(info.blockSize)}, ${num(info.ivLength)}, ${info.keyLength}, ${rules[0]}, ${rules[1]}},\n`; +out += `}; + +/* Lowercase names, sorted, with their record. */ +static const struct { + const char *name; + unsigned short info; +} sc_cipher_info_names[] = { +`; +for (const [name, index] of names) out += ` {${JSON.stringify(name)}, ${index}},\n`; +out += `}; + +/* Nids, ascending, with their record. */ +static const struct { + unsigned short nid; + unsigned short info; +} sc_cipher_info_nids[] = { +`; +for (const [nid, index] of nids) out += ` {${nid}, ${index}},\n`; +out += `}; + +#endif +`; +for (const { info } of records) { + const keys = Object.keys(info).join(); + const expected = ["mode", "name", "nid", "blockSize", "ivLength", "keyLength"] + .filter((k) => k in info) + .join(); + if (keys !== expected) throw new Error(`unexpected key order ${keys}`); +} +fs.writeFileSync(new URL("../packages/runtime/src/scr_cipher_info.h", import.meta.url), out); +console.log(records.length, "records,", names.length, "names,", nids.length, "nids"); 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/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/1006-json-fs-config.ts b/tests/corpus/1006-json-fs-config.ts index f2b1d48c0..1783202c9 100644 --- a/tests/corpus/1006-json-fs-config.ts +++ b/tests/corpus/1006-json-fs-config.ts @@ -11,7 +11,7 @@ function tail(path: string): string { } return path.slice(i + 1); } -const path = "tmp-1006-" + tail(process.argv[1]) + ".json"; +const path = "tmp-1006-" + tail(process.argv[1]) + "-" + process.pid + ".json"; type Config = { name: string; diff --git a/tests/corpus/1354-builtin-bare-specifiers.ts b/tests/corpus/1354-builtin-bare-specifiers.ts index cd0db6984..826c3e7a0 100644 --- a/tests/corpus/1354-builtin-bare-specifiers.ts +++ b/tests/corpus/1354-builtin-bare-specifiers.ts @@ -15,7 +15,7 @@ function tail(path: string): string { return path.slice(i + 1); } -const scratch = "tmp-1354-" + tail(process.argv[1]); +const scratch = "tmp-1354-" + tail(process.argv[1]) + "-" + process.pid; if (existsSync(scratch)) { if (existsSync(j(scratch, "f.txt"))) { rmSync(j(scratch, "f.txt")); diff --git a/tests/corpus/1357-fsp-roundtrip.ts b/tests/corpus/1357-fsp-roundtrip.ts index 4ccae7f3a..7368da360 100644 --- a/tests/corpus/1357-fsp-roundtrip.ts +++ b/tests/corpus/1357-fsp-roundtrip.ts @@ -15,7 +15,7 @@ function tail(path: string): string { } async function main(): Promise { - const scratch = "tmp-1357-" + tail(process.argv[1]); + const scratch = "tmp-1357-" + tail(process.argv[1]) + "-" + process.pid; if (existsSync(scratch)) { if (existsSync(scratch + "/a.txt")) { rmSync(scratch + "/a.txt"); diff --git a/tests/corpus/2844-node-disposable-resources.ts b/tests/corpus/2844-node-disposable-resources.ts index 042f617ce..ebeda87a2 100644 --- a/tests/corpus/2844-node-disposable-resources.ts +++ b/tests/corpus/2844-node-disposable-resources.ts @@ -14,7 +14,7 @@ async function main(): Promise { console.log(timeout.hasRef(), immediate.hasRef()); } - const path = "/tmp/scriptc-disposable-file.txt"; + const path = `/tmp/scriptc-disposable-file-${process.pid}.txt`; await writeFile(path, "resource"); { await using handle = await open(path, "r"); diff --git a/tests/corpus/5566-fs-dirent-dyn-boundaries/main.mjs b/tests/corpus/5566-fs-dirent-dyn-boundaries/main.mjs index 95bcbecee..77bdf8905 100644 --- a/tests/corpus/5566-fs-dirent-dyn-boundaries/main.mjs +++ b/tests/corpus/5566-fs-dirent-dyn-boundaries/main.mjs @@ -8,7 +8,7 @@ function tail(path) { while (i >= 0 && path.charAt(i) !== "/" && path.charAt(i) !== "\\") i = i - 1; return path.slice(i + 1); } -const dir = `/tmp/scr-dirent-dyn-${tail(process.argv[1] === undefined ? "x" : process.argv[1])}`; +const dir = `/tmp/scr-dirent-dyn-${tail(process.argv[1] === undefined ? "x" : process.argv[1])}-${process.pid}`; if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true }); fs.mkdirSync(`${dir}/sub`, { recursive: true }); fs.writeFileSync(`${dir}/a.txt`, "x"); 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/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/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/7154-crypto-cipher-info.cjs b/tests/corpus/7154-crypto-cipher-info.cjs new file mode 100644 index 000000000..1498f7183 --- /dev/null +++ b/tests/corpus/7154-crypto-cipher-info.cjs @@ -0,0 +1,88 @@ +// crypto.getCipherInfo: every getCiphers() name, OpenSSL's aliases in any +// case and its legacy-provider ciphers, lookups by nid, the keyLength and +// ivLength options (exact lengths, CCM's 7 to 13 bytes, OCB's 1 to 15 and +// any length for GCM; the legacy ciphers answer undefined for either), +// and Node's argument errors. +const crypto = require("crypto"); + +for (const name of crypto.getCiphers()) console.log(name, JSON.stringify(crypto.getCipherInfo(name))); +for (const name of ["AES-128-CBC", "Aes256", "id-aes128-CCM", "chacha20-POLY1305", "des", "bf", "bf-cbc", "rc4", "seed-cbc", "cast5-cbc", "rc2-cbc", "idea-cbc", "des-ecb", "desx", "nope", ""]) + console.log(JSON.stringify(name), JSON.stringify(crypto.getCipherInfo(name))); +for (const nid of [0, -1, 1, 5, 31, 91, 419, 427, 895, 896, 901, 1018, 1198, 1248, 99999]) + console.log("nid", nid, JSON.stringify(crypto.getCipherInfo(nid))); + +const options = [ + { keyLength: 16 }, + { keyLength: 32 }, + { ivLength: 16 }, + { ivLength: 12 }, + { ivLength: 7 }, + { ivLength: 13 }, + { ivLength: 14 }, + { ivLength: 0 }, + { ivLength: 1 }, + { ivLength: 15 }, + { ivLength: 1000 }, + { keyLength: 16, ivLength: 12 }, + { keyLength: undefined, ivLength: undefined }, + {}, +]; +for (const name of ["aes-128-cbc", "aes-128-gcm", "aes-128-ccm", "aes-128-ocb", "aes-128-ecb", "chacha20-poly1305", "aes-128-wrap-pad", "bf-cbc", "rc4"]) + for (const option of options) console.log(name, JSON.stringify(option), JSON.stringify(crypto.getCipherInfo(name, option))); + +function attempt(label, run) { + try { + console.log(label, "ok", JSON.stringify(run())); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } +} +attempt("no argument", () => crypto.getCipherInfo()); +attempt("null", () => crypto.getCipherInfo(null)); +attempt("boolean", () => crypto.getCipherInfo(true)); +attempt("object", () => crypto.getCipherInfo({})); +attempt("fraction", () => crypto.getCipherInfo(1.5)); +attempt("NaN", () => crypto.getCipherInfo(NaN)); +attempt("2^31", () => crypto.getCipherInfo(2 ** 31)); +attempt("options null", () => crypto.getCipherInfo("aes-128-cbc", null)); +attempt("options number", () => crypto.getCipherInfo("aes-128-cbc", 5)); +attempt("options string", () => crypto.getCipherInfo("aes-128-cbc", "x")); +attempt("options array", () => crypto.getCipherInfo("aes-128-cbc", [])); +attempt("keyLength string", () => crypto.getCipherInfo("aes-128-cbc", { keyLength: "16" })); +attempt("keyLength fraction", () => crypto.getCipherInfo("aes-128-cbc", { keyLength: 16.5 })); +attempt("ivLength 2^31", () => crypto.getCipherInfo("aes-128-cbc", { ivLength: 2 ** 31 })); +attempt("ivLength null", () => crypto.getCipherInfo("aes-128-cbc", { ivLength: null })); +attempt("unknown name with bad options", () => crypto.getCipherInfo("nope", { keyLength: "x" })); +const info = crypto.getCipherInfo("aes-256-gcm", { ivLength: 7 }); +console.log(Object.keys(info).join(), Object.getPrototypeOf(info) === Object.prototype); + +// The options are read as destructuring reads them: inherited properties +// and getters count, and both are read before either is validated. +attempt("getter keyLength", () => crypto.getCipherInfo("aes-128-cbc", { get keyLength() { return 17; } })); +attempt("getter ivLength", () => crypto.getCipherInfo("aes-128-cbc", { get ivLength() { return 5; } })); +attempt("getter valid", () => crypto.getCipherInfo("aes-128-cbc", { get keyLength() { return 16; } })); +attempt("inherited keyLength", () => crypto.getCipherInfo("aes-128-cbc", Object.create({ keyLength: 17 }))); +attempt("inherited valid", () => crypto.getCipherInfo("aes-128-cbc", Object.create({ keyLength: 16, ivLength: 16 }))); +class LengthOptions { + constructor(ivLength) { + this.ivLength = ivLength; + } +} +attempt("class field", () => crypto.getCipherInfo("aes-128-cbc", new LengthOptions(5))); +attempt("class field valid", () => crypto.getCipherInfo("aes-128-cbc", new LengthOptions(16))); +attempt("getter wrong type", () => crypto.getCipherInfo("aes-128-cbc", { get keyLength() { return "x"; } })); +attempt("getter throws", () => crypto.getCipherInfo("aes-128-cbc", { get keyLength() { throw new Error("getter"); } })); +const reads = []; +attempt("read order", () => + crypto.getCipherInfo("aes-128-cbc", { + get ivLength() { + reads.push("ivLength"); + return "x"; + }, + get keyLength() { + reads.push("keyLength"); + return "y"; + }, + }), +); +console.log("reads", reads.join()); diff --git a/tests/corpus/7155-cipher-aes-ccm.cjs b/tests/corpus/7155-cipher-aes-ccm.cjs new file mode 100644 index 000000000..5e6407e52 --- /dev/null +++ b/tests/corpus/7155-cipher-aes-ccm.cjs @@ -0,0 +1,199 @@ +// AES-CCM through createCipheriv and createDecipheriv: RFC 3610 and NIST +// SP 800-38C vectors, every tag and IV length, AAD with plaintextLength, +// the message-length caps of 12- and 13-byte IVs, a decipher's failed +// authentication reported by final(), the one-update rule, the AAD and +// tag calls in each state, and as a stream. +const crypto = require("crypto"); + +function attempt(label, run) { + try { + const v = run(); + console.log(label, "ok", Buffer.isBuffer(v) ? v.toString("hex") : v); + } catch (e) { + console.log(label, e.name, e.code, JSON.stringify(e.message), JSON.stringify(Object.keys(e))); + } +} +const hex = (s) => Buffer.from(s, "hex"); + +// NIST SP 800-38C examples C.1 to C.3. +const nistKey = hex("404142434445464748494a4b4c4d4e4f"); +for (const [iv, aad, pt, tagLen] of [ + ["10111213141516", "0001020304050607", "20212223", 4], + ["1011121314151617", "000102030405060708090a0b0c0d0e0f", "202122232425262728292a2b2c2d2e2f", 6], + ["101112131415161718191a1b", "000102030405060708090a0b0c0d0e0f10111213", "202122232425262728292a2b2c2d2e2f3031323334353637", 8], +]) { + attempt(`nist iv ${iv.length / 2}`, () => { + const c = crypto.createCipheriv("aes-128-ccm", nistKey, hex(iv), { authTagLength: tagLen }); + c.setAAD(hex(aad), { plaintextLength: pt.length / 2 }); + return Buffer.concat([c.update(hex(pt)), c.final(), c.getAuthTag()]); + }); +} +// RFC 3610 packet vector 1. +attempt("rfc3610 #1", () => { + const c = crypto.createCipheriv("aes-128-ccm", hex("c0c1c2c3c4c5c6c7c8c9cacbcccdcecf"), hex("00000003020100a0a1a2a3a4a5"), { authTagLength: 8 }); + c.setAAD(hex("0001020304050607"), { plaintextLength: 23 }); + return Buffer.concat([c.update(hex("08090a0b0c0d0e0f101112131415161718191a1b1c1d1e")), c.final(), c.getAuthTag()]); +}); + +const key = Buffer.alloc(16, 1); +const pt = Buffer.from("hello world, ccm mode, three blocks of it!"); +for (const size of [16, 24, 32]) + for (const name of [`aes-${size * 8}-ccm`, `id-aes${size * 8}-ccm`, `AES-${size * 8}-CCM`]) + attempt(name, () => { + const c = crypto.createCipheriv(name, Buffer.alloc(size, 3), Buffer.alloc(12, 2), { authTagLength: 10 }); + return Buffer.concat([c.update(pt), c.final(), c.getAuthTag()]); + }); +for (const tagLen of [2, 3, 4, 5, 6, 8, 10, 12, 14, 15, 16, 18]) + attempt(`tag ${tagLen}`, () => { + const c = crypto.createCipheriv("aes-128-ccm", key, Buffer.alloc(12), { authTagLength: tagLen }); + return Buffer.concat([c.update(pt), c.final(), c.getAuthTag()]); + }); +for (const ivLen of [0, 6, 7, 8, 11, 12, 13, 14]) + attempt(`iv ${ivLen}`, () => { + const c = crypto.createCipheriv("aes-128-ccm", key, Buffer.alloc(ivLen, 9), { authTagLength: 16 }); + return Buffer.concat([c.update(pt), c.final(), c.getAuthTag()]); + }); +attempt("no tag length", () => crypto.createCipheriv("aes-128-ccm", key, Buffer.alloc(12))); +attempt("no tag length, mixed case", () => crypto.createDecipheriv("Aes-128-Ccm", key, Buffer.alloc(12))); +attempt("tag length string", () => crypto.createCipheriv("aes-128-ccm", key, Buffer.alloc(12), { authTagLength: "16" })); +attempt("bad iv and no tag length", () => crypto.createCipheriv("aes-128-ccm", key, Buffer.alloc(5))); +attempt("bad key", () => crypto.createCipheriv("aes-128-ccm", Buffer.alloc(15), Buffer.alloc(12), { authTagLength: 16 })); + +const iv = Buffer.alloc(12, 2), aad = Buffer.from("header"); +function seal(steps) { + const c = crypto.createCipheriv("aes-128-ccm", key, iv, { authTagLength: 16 }); + const parts = []; + for (const step of steps) { + try { + const out = step(c); + if (Buffer.isBuffer(out)) parts.push(out.toString("hex")); + } catch (e) { + parts.push(`<${e.code ?? e.name}: ${e.message}>`); + } + } + try { + parts.push("tag " + c.getAuthTag().toString("hex")); + } catch (e) { + parts.push(`<${e.code}: ${e.message}>`); + } + return parts.join(" "); +} +const update = (data) => (c) => c.update(data); +const final = () => (c) => c.final(); +const setAAD = (data, options) => (c) => (c.setAAD(data, options), Buffer.alloc(0)); +console.log("plain", seal([update(pt), final()])); +console.log("aad", seal([setAAD(aad, { plaintextLength: pt.length }), update(pt), final()])); +console.log("aad without length", seal([setAAD(aad), update(pt), final()])); +console.log("aad bad length", seal([setAAD(aad, { plaintextLength: -1 }), update(pt), final()])); +console.log("aad length mismatch", seal([setAAD(aad, { plaintextLength: 5 }), update(pt), final()])); +console.log("aad twice", seal([setAAD(aad, { plaintextLength: pt.length }), setAAD(aad, { plaintextLength: pt.length }), update(pt), final()])); +console.log("empty aad", seal([setAAD(Buffer.alloc(0), { plaintextLength: pt.length }), update(pt), final()])); +console.log("aad after update", seal([update(pt), setAAD(aad, { plaintextLength: 3 }), final()])); +console.log("update twice", seal([update(pt), update(pt), final()])); +console.log("update empty twice", seal([update(Buffer.alloc(0)), update(Buffer.alloc(0)), final()])); +console.log("no update", seal([final()])); +console.log("aad, no update", seal([setAAD(aad, { plaintextLength: pt.length }), final()])); +console.log("aad length 0, no update", seal([setAAD(aad, { plaintextLength: 0 }), final()])); +console.log("tag before final", seal([update(pt)])); +console.log("final twice", seal([update(pt), final(), final()])); +console.log("update after final", seal([update(pt), final(), update(pt)])); +console.log("auto padding", seal([(c) => (c.setAutoPadding(false), Buffer.alloc(0)), update(pt), final()])); +console.log("strings", seal([(c) => Buffer.from(c.update("abcd", "utf8", "hex"), "hex"), (c) => Buffer.from(c.final("hex"), "hex")])); + +// Message-length caps: 2^16 - 1 bytes with a 13-byte IV, 2^24 - 1 with 12. +for (const [ivLen, size] of [[13, 65535], [13, 65536], [12, 65536]]) + attempt(`length ${size}, iv ${ivLen}`, () => { + const c = crypto.createCipheriv("aes-128-ccm", key, Buffer.alloc(ivLen), { authTagLength: 4 }); + return crypto.createHash("sha256").update(c.update(Buffer.alloc(size, 1))).digest("hex").slice(0, 16); + }); +attempt("aad length 65536, iv 13", () => crypto.createCipheriv("aes-128-ccm", key, Buffer.alloc(13), { authTagLength: 4 }).setAAD(aad, { plaintextLength: 65536 })); +attempt("aad length 16777216, iv 12", () => crypto.createCipheriv("aes-128-ccm", key, Buffer.alloc(12), { authTagLength: 4 }).setAAD(aad, { plaintextLength: 16777216 })); + +const sealer = crypto.createCipheriv("aes-128-ccm", key, iv, { authTagLength: 16 }); +sealer.setAAD(aad, { plaintextLength: pt.length }); +const ct = Buffer.concat([sealer.update(pt), sealer.final()]); +const tag = sealer.getAuthTag(); +function open(steps) { + const d = crypto.createDecipheriv("aes-128-ccm", key, iv, { authTagLength: 16 }); + const parts = []; + for (const step of steps) { + try { + const out = step(d); + if (Buffer.isBuffer(out)) parts.push(JSON.stringify(out.toString())); + } catch (e) { + parts.push(`<${e.code ?? e.name}: ${e.message}>`); + } + } + return parts.join(" "); +} +const setAuthTag = (t) => (d) => (d.setAuthTag(t), Buffer.alloc(0)); +console.log("open", open([setAuthTag(tag), setAAD(aad, { plaintextLength: ct.length }), update(ct), final()])); +console.log("open, tag after aad", open([setAAD(aad, { plaintextLength: ct.length }), setAuthTag(tag), update(ct), final()])); +console.log("open without tag", open([setAAD(aad, { plaintextLength: ct.length }), update(ct), final()])); +console.log("open bad tag", open([setAuthTag(Buffer.alloc(16)), setAAD(aad, { plaintextLength: ct.length }), update(ct), final()])); +console.log("open without aad", open([setAuthTag(tag), update(ct), final()])); +console.log("open tampered", open([setAuthTag(tag), setAAD(aad, { plaintextLength: ct.length }), update(Buffer.concat([ct.subarray(0, 5), Buffer.from([ct[5] ^ 1]), ct.subarray(6)])), final()])); +console.log("open tag after update", open([setAAD(aad, { plaintextLength: ct.length }), update(ct), setAuthTag(tag), final()])); +console.log("open tag twice", open([setAuthTag(tag), setAuthTag(tag), setAAD(aad, { plaintextLength: ct.length }), update(ct), final()])); +console.log("open tag wrong length", open([setAuthTag(tag.subarray(0, 8))])); +console.log("open update twice", open([setAuthTag(tag), setAAD(aad, { plaintextLength: ct.length }), update(ct), update(ct), final()])); +console.log("open final only", open([setAuthTag(tag), final()])); +console.log("open length mismatch", open([setAuthTag(tag), setAAD(aad, { plaintextLength: 3 }), update(ct), final()])); +console.log("open getAuthTag", open([(d) => d.getAuthTag()])); +console.log("open empty message", (() => { + const c = crypto.createCipheriv("aes-128-ccm", key, iv, { authTagLength: 8 }); + c.update(Buffer.alloc(0)); + c.final(); + const t = c.getAuthTag(); + const d = crypto.createDecipheriv("aes-128-ccm", key, iv, { authTagLength: 8 }); + d.setAuthTag(t); + return JSON.stringify(Buffer.concat([d.update(Buffer.alloc(0)), d.final()]).toString()); +})()); + +// An empty message is still authenticated: its tag covers the nonce, the +// AAD and the declared length, so a wrong tag, other AAD or another +// plaintextLength fails in final(), as does a decipher without an update. +const empty = Buffer.alloc(0); +const sealEmpty = (aadBuf) => { + const c = crypto.createCipheriv("aes-128-ccm", key, iv, { authTagLength: 16 }); + if (aadBuf) c.setAAD(aadBuf, { plaintextLength: 0 }); + c.update(empty); + c.final(); + return c.getAuthTag(); +}; +const emptyTag = sealEmpty(aad); +const emptyTagNoAad = sealEmpty(null); +const flipped = Buffer.from(emptyTag); +flipped[0] ^= 1; +const openEmpty = (steps) => { + const d = crypto.createDecipheriv("aes-128-ccm", key, iv, { authTagLength: 16 }); + const out = []; + try { + for (const step of steps) { + const v = step(d); + if (Buffer.isBuffer(v)) out.push(v); + } + return `ok ${JSON.stringify(Buffer.concat(out).toString())}`; + } catch (e) { + return `${e.name} ${e.code} ${JSON.stringify(e.message)} after ${out.length} outputs`; + } +}; +console.log("empty tag", emptyTag.toString("hex"), emptyTagNoAad.toString("hex")); +console.log("empty, right tag", openEmpty([setAuthTag(emptyTag), setAAD(aad, { plaintextLength: 0 }), update(empty), final()])); +console.log("empty, wrong tag", openEmpty([setAuthTag(Buffer.alloc(16, 9)), setAAD(aad, { plaintextLength: 0 }), update(empty), final()])); +console.log("empty, flipped tag", openEmpty([setAuthTag(flipped), setAAD(aad, { plaintextLength: 0 }), update(empty), final()])); +console.log("empty, other aad", openEmpty([setAuthTag(emptyTag), setAAD(Buffer.from("other"), { plaintextLength: 0 }), update(empty), final()])); +console.log("empty, plaintextLength 5", openEmpty([setAuthTag(emptyTag), setAAD(aad, { plaintextLength: 5 }), update(empty), final()])); +console.log("empty, no update", openEmpty([setAuthTag(emptyTag), setAAD(aad, { plaintextLength: 0 }), final()])); +console.log("empty, no tag", openEmpty([setAAD(aad, { plaintextLength: 0 }), update(empty), final()])); +console.log("empty, no aad, right tag", openEmpty([setAuthTag(emptyTagNoAad), update(empty), final()])); +console.log("empty, no aad, wrong tag", openEmpty([setAuthTag(Buffer.alloc(16, 9)), update(empty), final()])); +console.log("empty, aad dropped", openEmpty([setAuthTag(emptyTag), update(empty), final()])); +console.log("empty, a string update", openEmpty([setAuthTag(Buffer.alloc(16, 9)), (d) => d.update(""), final()])); + +// As a stream: one write and the end. +const stream = crypto.createCipheriv("aes-128-ccm", key, iv, { authTagLength: 12 }); +const chunks = []; +stream.on("data", (chunk) => chunks.push(chunk)); +stream.on("end", () => console.log("stream", Buffer.concat(chunks).toString("hex"), stream.getAuthTag().toString("hex"))); +stream.end(pt); diff --git a/tests/corpus/7156-cipher-key-wrap.cjs b/tests/corpus/7156-cipher-key-wrap.cjs new file mode 100644 index 000000000..a958d16c5 --- /dev/null +++ b/tests/corpus/7156-cipher-key-wrap.cjs @@ -0,0 +1,103 @@ +// The AES key-wrap ciphers through createCipheriv and createDecipheriv: +// RFC 3394 and RFC 5649 vectors under every name (aes-N-wrap, aesN-wrap, +// id-aesN-wrap, the -pad forms and the -inv ciphers), the input and IV +// lengths each takes, integrity failures, and the one-update rule. +const crypto = require("crypto"); + +function attempt(label, run) { + try { + const v = run(); + console.log(label, "ok", Buffer.isBuffer(v) ? v.toString("hex") : v); + } catch (e) { + console.log(label, e.name, e.code, JSON.stringify(e.message), JSON.stringify(Object.keys(e))); + } +} +const hex = (s) => Buffer.from(s, "hex"); +const wrap = (name, key, iv, input) => { + const c = crypto.createCipheriv(name, key, iv); + return Buffer.concat([c.update(input), c.final()]); +}; +const unwrap = (name, key, iv, input) => { + const d = crypto.createDecipheriv(name, key, iv); + return Buffer.concat([d.update(input), d.final()]); +}; +const iv8 = hex("a6a6a6a6a6a6a6a6"), iv4 = hex("a65959a6"); + +// RFC 3394 section 4. +const kek = hex("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"); +const keyData = hex("00112233445566778899aabbccddeeff000102030405060708090a0b0c0d0e0f"); +for (const [kekBits, dataBits] of [[128, 128], [192, 128], [256, 128], [192, 192], [256, 192], [256, 256]]) { + const k = kek.subarray(0, kekBits / 8), d = keyData.subarray(0, dataBits / 8); + for (const name of [`aes-${kekBits}-wrap`, `aes${kekBits}-wrap`, `id-aes${kekBits}-wrap`, `ID-AES${kekBits}-WRAP`]) { + attempt(`${name} ${dataBits}`, () => wrap(name, k, iv8, d)); + attempt(`${name} ${dataBits} unwrap`, () => unwrap(name, k, iv8, wrap(name, k, iv8, d))); + } +} +// RFC 5649 section 6. +const kek5649 = hex("5840df6e29b02af1ab493b705bf16ea1ae8338f4dcc176a8"); +for (const data of ["c37b7e6492584340bed12207808941155068f738", "466f7250617369"]) + for (const name of ["aes-192-wrap-pad", "aes192-wrap-pad", "id-aes192-wrap-pad"]) { + attempt(`${name} ${data.length / 2}`, () => wrap(name, kek5649, iv4, hex(data))); + attempt(`${name} ${data.length / 2} unwrap`, () => unwrap(name, kek5649, iv4, wrap(name, kek5649, iv4, hex(data)))); + } + +const k128 = kek.subarray(0, 16); +for (const size of [0, 1, 7, 8, 9, 15, 16, 17, 24, 40]) { + attempt(`wrap ${size} bytes`, () => wrap("id-aes128-wrap", k128, iv8, Buffer.alloc(size, 5))); + attempt(`wrap-pad ${size} bytes`, () => { + const w = wrap("id-aes128-wrap-pad", k128, iv4, Buffer.alloc(size, 5)); + return w.toString("hex") + " -> " + unwrap("id-aes128-wrap-pad", k128, iv4, w).toString("hex"); + }); +} +for (const size of [8, 16, 20, 24, 32]) + for (const name of ["id-aes128-wrap", "id-aes128-wrap-pad"]) { + const iv = name.endsWith("pad") ? iv4 : iv8; + attempt(`${name} unwrap ${size} bytes`, () => unwrap(name, k128, iv, Buffer.alloc(size, 1))); + } +attempt("other iv", () => wrap("id-aes128-wrap", k128, Buffer.alloc(8, 1), keyData.subarray(0, 16))); +attempt("unwrap with another iv", () => unwrap("id-aes128-wrap", k128, Buffer.alloc(8, 1), wrap("id-aes128-wrap", k128, iv8, keyData.subarray(0, 16)))); +attempt("pad other iv", () => wrap("id-aes128-wrap-pad", k128, Buffer.alloc(4, 7), Buffer.from("key"))); +attempt("pad unwrap with another iv", () => unwrap("id-aes128-wrap-pad", k128, Buffer.alloc(4, 7), wrap("id-aes128-wrap-pad", k128, iv4, Buffer.from("key")))); +attempt("pad tampered length", () => { + const w = wrap("id-aes128-wrap-pad", k128, iv4, Buffer.alloc(20, 9)); + w[3] ^= 1; + return unwrap("id-aes128-wrap-pad", k128, iv4, w); +}); +for (const name of ["aes-128-wrap-inv", "aes128-wrap-inv", "aes-256-wrap-inv", "aes-128-wrap-pad-inv", "aes256-wrap-pad-inv"]) { + const bits = Number(name.match(/\d+/)[0]); + const iv = name.includes("pad") ? iv4 : iv8; + attempt(name, () => { + const w = wrap(name, kek.subarray(0, bits / 8), iv, keyData.subarray(0, 24)); + return w.toString("hex") + " -> " + unwrap(name, kek.subarray(0, bits / 8), iv, w).toString("hex"); + }); +} +attempt("null iv", () => crypto.createCipheriv("id-aes128-wrap", k128, null)); +attempt("iv 16", () => crypto.createCipheriv("id-aes128-wrap", k128, Buffer.alloc(16))); +attempt("pad iv 8", () => crypto.createCipheriv("id-aes128-wrap-pad", k128, iv8)); +attempt("key 24", () => crypto.createCipheriv("id-aes128-wrap", Buffer.alloc(24), iv8)); + +function steps(list) { + const c = crypto.createCipheriv("id-aes128-wrap", k128, iv8); + return list + .map((step) => { + try { + const out = step(c); + return Buffer.isBuffer(out) ? out.toString("hex") || '""' : String(out); + } catch (e) { + return `<${e.code ?? e.name}: ${e.message}>`; + } + }) + .join(" "); +} +const data = keyData.subarray(0, 16); +console.log("update twice", steps([(c) => c.update(data), (c) => c.update(data), (c) => c.final()])); +console.log("final only", steps([(c) => c.final()])); +console.log("final twice", steps([(c) => c.update(data), (c) => c.final(), (c) => c.final()])); +console.log("update after final", steps([(c) => c.update(data), (c) => c.final(), (c) => c.update(data)])); +console.log("bad then good", steps([(c) => c.update(Buffer.alloc(12)), (c) => c.update(data), (c) => c.final()])); +console.log("bad then final", steps([(c) => c.update(Buffer.alloc(12)), (c) => c.final()])); +console.log("empty then data", steps([(c) => c.update(Buffer.alloc(0)), (c) => c.update(data), (c) => c.final()])); +console.log("hex", steps([(c) => c.update(data, undefined, "hex"), (c) => c.final("hex")])); +console.log("auto padding", steps([(c) => c.setAutoPadding(false) === c, (c) => c.update(data)])); +console.log("setAAD", steps([(c) => c.setAAD(Buffer.alloc(1))])); +console.log("getAuthTag", steps([(c) => c.update(data), (c) => c.final(), (c) => c.getAuthTag()])); diff --git a/tests/corpus/7157-crypto-cipher-info-typed.ts b/tests/corpus/7157-crypto-cipher-info-typed.ts new file mode 100644 index 000000000..2de0f0069 --- /dev/null +++ b/tests/corpus/7157-crypto-cipher-info-typed.ts @@ -0,0 +1,42 @@ +// getCipherInfo, AES-CCM and AES key wrap through their declared types: the +// CipherInfo record's fields, absent fields and key order, the keyLength and +// ivLength options, and the CipherCCM and DecipherCCM members. +import { createCipheriv, createDecipheriv, getCipherInfo, type CipherInfo } from "node:crypto"; + +const info: CipherInfo | undefined = getCipherInfo("aes-128-ccm"); +if (info) console.log(info.name, info.nid, info.mode, info.keyLength, info.ivLength, info.blockSize); +const byNid = getCipherInfo(901); +console.log(byNid?.name, byNid?.mode); +const sized = getCipherInfo("aes-128-ccm", { ivLength: 7 }); +console.log(sized?.ivLength, getCipherInfo("aes-128-ccm", { ivLength: 6 })); +console.log(getCipherInfo("nope")); +console.log(JSON.stringify(getCipherInfo("AES-256-WRAP-PAD")!)); +console.log(Object.keys(getCipherInfo("chacha20")!), JSON.stringify(getCipherInfo("aes-128-ecb")!)); +for (const name of ["rc4", "des-ede3-cbc", "aes-256-xts", "chacha20-poly1305"]) { + const found = getCipherInfo(name, { keyLength: 32 }); + console.log(name, found ? Object.keys(found).join() : "none", found?.blockSize, found?.ivLength); +} + +const key = Buffer.alloc(16, 1); +const iv = Buffer.alloc(12, 2); +const aad = Buffer.from("header"); +const plain = Buffer.from("typed ccm message"); +const cipher = createCipheriv("aes-128-ccm", key, iv, { authTagLength: 8 }); +cipher.setAAD(aad, { plaintextLength: plain.length }); +const ciphertext = Buffer.concat([cipher.update(plain), cipher.final()]); +const tag: Buffer = cipher.getAuthTag(); +console.log(ciphertext.toString("hex"), tag.toString("hex")); +const decipher = createDecipheriv("aes-128-ccm", key, iv, { authTagLength: 8 }); +decipher.setAuthTag(tag); +decipher.setAAD(aad, { plaintextLength: ciphertext.length }); +const opened: Buffer = decipher.update(ciphertext); +decipher.final(); +console.log(opened.toString()); + +const kek = Buffer.alloc(24, 3); +const icv = Buffer.from("A6A6A6A6A6A6A6A6", "hex"); +const wrap = createCipheriv("aes192-wrap", kek, icv); +const wrapped = Buffer.concat([wrap.update(Buffer.alloc(16, 9)), wrap.final()]); +console.log(wrapped.toString("base64")); +const unwrap = createDecipheriv("id-aes192-wrap", kek, icv); +console.log(Buffer.concat([unwrap.update(wrapped), unwrap.final()]).toString("hex")); 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/7264-array-dense-paired-loops.ts b/tests/corpus/7264-array-dense-paired-loops.ts new file mode 100644 index 000000000..59cedf60b --- /dev/null +++ b/tests/corpus/7264-array-dense-paired-loops.ts @@ -0,0 +1,120 @@ +// Two-counter loops that index number arrays at both counters +// (`for (let i = lo, j = hi; i < j; i++, j--)`), and element copies through +// `a[i]!` reads, both inside and outside the ranges a single entry test can +// cover: filled prefixes, holes, present undefined, reads past the end, +// fractional and negative starts, int32 arrays and long checked arrays. + +function reversePrefix(a: number[], end: number): void { + for (let i = 0, j = end; i < j; i++, j--) { + const t = a[i]!; + a[i] = a[j]!; + a[j] = t; + } +} + +function show(label: string, a: number[]): void { + console.log(label, a.length, JSON.stringify(a), a); +} + +// The whole prefix is present: the entry test passes. +const plain: number[] = []; +for (let k = 0; k < 8; k++) plain[k] = k * 3; +reversePrefix(plain, 5); +show("plain", plain); +reversePrefix(plain, 7); +show("whole", plain); +reversePrefix(plain, 0); +show("empty range", plain); + +// The high start is past the end: the reads there miss, the stores extend. +const short = [1, 2, 3]; +reversePrefix(short, 5); +show("past the end", short); + +// A hole inside the range. +const holey: number[] = [10, 20]; +holey[3] = 40; +holey[4] = 50; +reversePrefix(holey, 4); +show("hole", holey); + +// A present undefined inside the range, copied by a mirror loop. +const loose: number[] = [7, 8, 9, 10]; +(loose as (number | undefined)[])[2] = undefined; +for (let i = 0, j = 3; i < j; i++, j--) loose[i] = loose[j]!; +show("undefined", loose); + +// Fractional and negative starts index non-element properties. +const fractional = [1, 2, 3, 4]; +reversePrefix(fractional, 2.5); +show("fractional", fractional); +const negative = [5, 6, 7]; +for (let i = -1, j = 2; i < j; i++, j--) negative[i] = negative[j]!; +show("negative", negative); + +// A local int32 array (every stored element is a small integer). +function int32Flips(start: number): void { + const counts = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + let flips = 0; + for (let first = counts[start]!; first !== 0; first = counts[0]!) { + for (let i = 0, j = first; i < j; i++, j--) { + const t = counts[i]!; + counts[i] = counts[j]!; + counts[j] = t; + } + flips++; + if (flips > 20) break; + } + console.log("int32", flips, counts.join(",")); +} +int32Flips(3); +int32Flips(9); + +// A local long array with checked int32 stores, widened by a fraction. +function longChecked(n: number, fraction: number): void { + const long = new Array(n).fill(0); + for (let k = 0; k < long.length; k++) long[k] = k; + for (let i = 0, j = n - 1; i < j; i++, j--) { + const t = long[i]!; + long[i] = long[j]!; + long[j] = t; + } + console.log("long", long[0], long[1], long[n - 2], long[n - 1]); + long[10] = fraction; + for (let i = 0, j = 20; i < j; i++, j--) { + const t = long[i]!; + long[i] = long[j]!; + long[j] = t; + } + console.log("widened", long.slice(0, 12).join(","), long[20]); +} +longChecked(5000, 0.5); + +// Element copies through `a[i]!`: the source is shorter than the range. +function copy(to: number[], from: number[], n: number): void { + for (let i = 0; i < n; i++) to[i] = from[i]!; +} +const into = [0, 0, 0, 0, 0, 0]; +copy(into, [1, 2, 3, 4, 5, 6], 6); +show("copy", into); +copy(into, [9, 8], 4); +show("short source", into); +const gappy: number[] = [1]; +gappy[2] = 3; +gappy[3] = 4; +copy(into, gappy, 4); +show("gappy source", into); + +// Two names for one array. +function aliased(n: number): void { + const same: number[] = []; + for (let k = 0; k < n; k++) same[k] = k + 1; + const alias = same; + for (let i = 0, j = n - 1; i < j; i++, j--) { + const t = alias[i]!; + same[i] = alias[j]!; + same[j] = t; + } + show("alias", same); +} +aliased(5); diff --git a/tests/corpus/7281-http-server-connect-tunnel.ts b/tests/corpus/7281-http-server-connect-tunnel.ts new file mode 100644 index 000000000..d8b396c07 --- /dev/null +++ b/tests/corpus/7281-http-server-connect-tunnel.ts @@ -0,0 +1,81 @@ +// An HTTP CONNECT proxy on node:http: the server's 'connect' listener gets +// the request, the raw client socket and the bytes read past the request +// head, opens a TCP connection to the requested host and pipes both ways. +// 'once' and 'addListener' register the same event, and 'upgrade' hands +// over the socket for an Upgrade request. +import * as http from "node:http"; +import * as net from "node:net"; + +const target = net.createServer((socket) => { + socket.on("data", (d) => socket.write(`echo:${d.toString()}`)); +}); + +const proxy = http.createServer((_req, res) => res.end("plain")); +const seen: string[] = []; +proxy.on("connect", (req, clientSocket, head) => { + seen.push(`connect ${req.method} ${req.url} head=${head.length}`); + const [host, port] = (req.url ?? "").split(":"); + const upstream = net.connect(Number(port), host, () => { + clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n"); + if (head.length > 0) upstream.write(head); + upstream.pipe(clientSocket); + clientSocket.pipe(upstream); + }); +}); +proxy.once("connect", (req) => seen.push(`once ${req.url}`)); +proxy.addListener("upgrade", (req, socket, head) => { + seen.push(`upgrade ${req.headers.upgrade} head=${head.length}`); + socket.end("HTTP/1.1 101 Switching Protocols\r\nUpgrade: echo\r\nConnection: Upgrade\r\n\r\n"); +}); + +function tunnel( + proxyPort: number, + targetPort: number, + payload: string, + withHead: boolean, +): Promise { + return new Promise((resolve) => { + const c = net.connect(proxyPort, "127.0.0.1", () => { + const request = `CONNECT 127.0.0.1:${targetPort} HTTP/1.1\r\nHost: 127.0.0.1:${targetPort}\r\n\r\n`; + c.write(withHead ? request + payload : request); + }); + let text = ""; + let sent = withHead; + c.on("data", (d) => { + text += d.toString(); + if (!sent && text.includes("\r\n\r\n")) { + sent = true; + c.write(payload); + } + if (sent && text.includes(`echo:${payload}`)) { + c.end(); + resolve(text); + } + }); + }); +} + +function upgrade(proxyPort: number): Promise { + return new Promise((resolve) => { + const u = net.connect(proxyPort, "127.0.0.1", () => { + u.write("GET /ws HTTP/1.1\r\nHost: x\r\nConnection: Upgrade\r\nUpgrade: echo\r\n\r\n"); + }); + let text = ""; + u.on("data", (d) => (text += d.toString())); + u.on("end", () => resolve(text.split("\r\n")[0] ?? "")); + }); +} + +target.listen(0, "127.0.0.1", () => { + const targetPort = (target.address() as net.AddressInfo).port; + proxy.listen(0, "127.0.0.1", async () => { + const proxyPort = (proxy.address() as net.AddressInfo).port; + console.log(JSON.stringify(await tunnel(proxyPort, targetPort, "first", false))); + console.log(JSON.stringify(await tunnel(proxyPort, targetPort, "second", false))); + console.log(JSON.stringify(await tunnel(proxyPort, targetPort, "with-head", true))); + console.log(JSON.stringify(await upgrade(proxyPort))); + console.log(seen.join("\n").replaceAll(String(targetPort), "PORT")); + proxy.close(); + target.close(); + }); +}); diff --git a/tests/corpus/7282-http2-once-add-listener.ts b/tests/corpus/7282-http2-once-add-listener.ts new file mode 100644 index 000000000..ab1e24a9f --- /dev/null +++ b/tests/corpus/7282-http2-once-add-listener.ts @@ -0,0 +1,54 @@ +// node:http2 listeners registered with once and addListener on the server, +// the client session and both stream kinds. A once listener runs for the +// first event only; addListener is on. +import * as http2 from "node:http2"; +import * as net from "node:net"; + +const log: string[] = []; +const accepted: string[] = []; +const server = http2.createServer(); +server.once("connection", (socket: net.Socket) => accepted.push(`connection ${typeof socket.remotePort}`)); +server.addListener("session", () => accepted.push("session")); +server.once("stream", () => log.push("server first stream")); +server.addListener("stream", (stream, headers) => { + const header = headers[":path"]; + const path = typeof header === "string" ? header : ""; + log.push(`server stream ${path}`); + let body = ""; + stream.addListener("data", (chunk) => (body += chunk.toString())); + stream.once("end", () => { + stream.respond({ ":status": 200, "x-path": path }); + stream.end(`${path}:${body}`); + }); +}); +server.once("close", () => log.push("server close")); + +function request(client: http2.ClientHttp2Session, path: string, body: string): Promise { + return new Promise((resolve) => { + const req = client.request({ ":path": path, ":method": "POST" }); + req.once("response", (headers) => { + const status = headers[":status"]; + const echoed = headers["x-path"]; + log.push(`client response ${typeof status === "number" ? status : "?"} ${typeof echoed === "string" ? echoed : "?"}`); + }); + let text = ""; + req.addListener("data", (chunk) => (text += chunk.toString())); + req.once("end", () => resolve(text)); + req.end(body); + }); +} + +server.listen(0, () => { + const port = (server.address() as net.AddressInfo).port; + const client = http2.connect(`http://localhost:${port}`); + client.once("connect", () => log.push("client connect")); + client.addListener("close", () => { + log.push("client close"); + server.close(() => console.log([accepted.sort().join(", "), ...log].join("\n"))); + }); + void (async () => { + console.log(await request(client, "/a", "one")); + console.log(await request(client, "/b", "two")); + client.close(); + })(); +}); diff --git a/tests/corpus/7287-http-held-request-backpressure.ts b/tests/corpus/7287-http-held-request-backpressure.ts new file mode 100644 index 000000000..1953b8ec6 --- /dev/null +++ b/tests/corpus/7287-http-held-request-backpressure.ts @@ -0,0 +1,106 @@ +// A server request that holds its body (nothing reads it yet, or its +// reader paused it) stops reading its socket once its buffer is full, so +// a client that keeps uploading is held back by TCP. Each client writes +// 64 KiB chunks, one after the previous write's callback, and stops when +// the response arrives. An unread body followed by a pipelined request is +// discarded once the response finishes, and the second request is served. +import * as http from "node:http"; +import * as net from "node:net"; +import type { AddressInfo } from "node:net"; + +const MiB = 1024 * 1024; + +function upload(port: number, path: string): Promise { + return new Promise((resolve) => { + const socket = net.connect(port, "127.0.0.1"); + const frame = Buffer.concat([Buffer.from("10000\r\n"), Buffer.alloc(65536, 98), Buffer.from("\r\n")]); + let sent = 0; + let sentBeforeResponse = -1; + let response = ""; + const writeNext = (): void => { + if (sentBeforeResponse >= 0 || socket.destroyed || sent >= 256 * MiB) return; + sent += 65536; + socket.write(frame, () => { + writeNext(); + }); + }; + socket.on("connect", () => { + socket.write(`POST ${path} HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: chunked\r\n\r\n`); + writeNext(); + }); + socket.on("data", (chunk: Buffer) => { + response += chunk.toString("latin1"); + if (sentBeforeResponse < 0 && response.includes("\r\n\r\n")) { + sentBeforeResponse = sent; + socket.destroy(); + resolve(sentBeforeResponse); + } + }); + socket.on("error", () => {}); + }); +} + +function pipelined(port: number): Promise { + return new Promise((resolve) => { + const socket = net.connect(port, "127.0.0.1"); + const body = "c".repeat(300000); + let response = ""; + socket.on("connect", () => { + socket.write( + `POST /unread HTTP/1.1\r\nHost: localhost\r\nContent-Length: ${body.length}\r\n\r\n${body}` + + "GET /second HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n", + ); + }); + socket.on("data", (chunk: Buffer) => { + response += chunk.toString("latin1"); + }); + socket.on("end", () => { + const lines = response.split("\r\n").filter((line) => line.startsWith("HTTP/") || line.startsWith("served")); + resolve(lines); + }); + }); +} + +let pausedRequest: http.IncomingMessage | undefined; + +const server = http.createServer((req, res) => { + if (req.url === "/unconsumed") { + setTimeout(() => { + res.writeHead(413, { "content-type": "text/plain" }); + res.end("unconsumed"); + }, 300); + return; + } + if (req.url === "/paused") { + let seen = 0; + req.on("data", (chunk: Buffer) => { + seen += chunk.length; + if (pausedRequest === undefined) { + pausedRequest = req; + req.pause(); + setTimeout(() => { + res.writeHead(413, { "content-type": "text/plain" }); + res.end(`paused after ${seen > 0}`); + req.resume(); + }, 300); + } + }); + return; + } + res.writeHead(200, { "content-type": "text/plain" }); + res.end(`served ${req.url}`); +}); + +server.listen(0, "127.0.0.1", () => { + const port = (server.address() as AddressInfo).port; + void (async () => { + const before = process.memoryUsage().rss; + const unconsumed = await upload(port, "/unconsumed"); + console.log("unconsumed: client stopped before 32 MiB:", unconsumed < 32 * MiB); + const paused = await upload(port, "/paused"); + console.log("paused: client stopped before 32 MiB:", paused < 32 * MiB); + console.log("memory grew under 64 MiB:", process.memoryUsage().rss - before < 64 * MiB); + for (const line of await pipelined(port)) console.log(line); + server.close(() => console.log("closed")); + })(); +}); 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-cycle-deferred-folded-key/core.ts b/tests/corpus/7380-cycle-deferred-folded-key/core.ts new file mode 100644 index 000000000..7f4f4ab14 --- /dev/null +++ b/tests/corpus/7380-cycle-deferred-folded-key/core.ts @@ -0,0 +1,5 @@ +import { describe, kind, mark } from "./util.ts"; + +export const version = 3; +export const marked = mark({}); +console.log("core body", kind(), describe()); diff --git a/tests/corpus/7380-cycle-deferred-folded-key/main.ts b/tests/corpus/7380-cycle-deferred-folded-key/main.ts new file mode 100644 index 000000000..92617eba3 --- /dev/null +++ b/tests/corpus/7380-cycle-deferred-folded-key/main.ts @@ -0,0 +1,10 @@ +// A cycle member runs user code before it declares a constant that only +// functions read, one of them as a computed key and one under `typeof` +// (both resolved from the constant's literal type). Nothing calls those +// functions before the declaration ran, so they read the constant +// normally once the other member's top level calls them. +import { marked } from "./core.ts"; +import { describe, kind, mark } from "./util.ts"; + +console.log(JSON.stringify(marked), kind()); +console.log(JSON.stringify(mark({ seen: false })), describe()); diff --git a/tests/corpus/7380-cycle-deferred-folded-key/util.ts b/tests/corpus/7380-cycle-deferred-folded-key/util.ts new file mode 100644 index 000000000..a038d9e63 --- /dev/null +++ b/tests/corpus/7380-cycle-deferred-folded-key/util.ts @@ -0,0 +1,23 @@ +import { version } from "./core.ts"; + +export function makeCounter(): () => number { + let n = 0; + return () => ++n; +} +const next = makeCounter(); +console.log("util body", next(), next()); + +export const TAG = "~tagged"; + +export function mark(box: Record): Record { + box[TAG] = true; + return box; +} + +export function kind(): string { + return typeof TAG; +} + +export function describe(): string { + return `core v${version}`; +} diff --git a/tests/corpus/7381-cycle-deferred-reached-tdz/alpha.ts b/tests/corpus/7381-cycle-deferred-reached-tdz/alpha.ts new file mode 100644 index 000000000..8dc002c27 --- /dev/null +++ b/tests/corpus/7381-cycle-deferred-reached-tdz/alpha.ts @@ -0,0 +1,13 @@ +import { probe } from "./beta.ts"; + +export function report(): string { + return `alpha ${NAME}`; +} + +export function late(): string { + return `${OTHER} after ${probe}`; +} + +export const NAME = "ready"; +export const OTHER = "other"; +console.log("alpha body", report()); diff --git a/tests/corpus/7381-cycle-deferred-reached-tdz/beta.ts b/tests/corpus/7381-cycle-deferred-reached-tdz/beta.ts new file mode 100644 index 000000000..c3d1551ff --- /dev/null +++ b/tests/corpus/7381-cycle-deferred-reached-tdz/beta.ts @@ -0,0 +1,12 @@ +import { report } from "./alpha.ts"; + +function attempt(): string { + try { + return report(); + } catch (error) { + return error instanceof ReferenceError ? `ReferenceError: ${error.message}` : "other error"; + } +} + +export const probe = attempt(); +console.log("beta body", probe); diff --git a/tests/corpus/7381-cycle-deferred-reached-tdz/main.ts b/tests/corpus/7381-cycle-deferred-reached-tdz/main.ts new file mode 100644 index 000000000..6a0b7e3ca --- /dev/null +++ b/tests/corpus/7381-cycle-deferred-reached-tdz/main.ts @@ -0,0 +1,7 @@ +// The first cycle member to run calls a function of the other member +// before that member declared the constant the function reads: Node's +// temporal-dead-zone ReferenceError. A second function of the same member +// reads another constant but runs only after the cycle finished. +import { late, report } from "./alpha.ts"; + +console.log(report(), late()); diff --git a/tests/corpus/7382-cycle-static-this-tdz/alpha.ts b/tests/corpus/7382-cycle-static-this-tdz/alpha.ts new file mode 100644 index 000000000..ff8eb4a87 --- /dev/null +++ b/tests/corpus/7382-cycle-static-this-tdz/alpha.ts @@ -0,0 +1,11 @@ +import { Probe, Lazy } from "./beta.ts"; + +export { Probe }; +export const label = "ready"; +export const other = "other"; + +export function later(): string { + return `${Lazy.describe()} ${other}`; +} + +console.log("alpha body", Probe.result, Lazy.value); diff --git a/tests/corpus/7382-cycle-static-this-tdz/beta.ts b/tests/corpus/7382-cycle-static-this-tdz/beta.ts new file mode 100644 index 000000000..428907967 --- /dev/null +++ b/tests/corpus/7382-cycle-static-this-tdz/beta.ts @@ -0,0 +1,26 @@ +import { label, other } from "./alpha.ts"; + +function guard(read: () => string): string { + try { + return read(); + } catch (error) { + return error instanceof ReferenceError ? `ReferenceError: ${error.message}` : "other error"; + } +} + +export class Probe { + static helper(): string { + return guard(() => `label ${label}`); + } + static result = this.helper(); +} + +export class Lazy { + static read(): string { + return guard(() => `other ${other}`); + } + static describe(): string { + return `lazy ${other}`; + } + static value = [0].map(() => this.read()).join(""); +} diff --git a/tests/corpus/7382-cycle-static-this-tdz/main.ts b/tests/corpus/7382-cycle-static-this-tdz/main.ts new file mode 100644 index 000000000..e3bbf1581 --- /dev/null +++ b/tests/corpus/7382-cycle-static-this-tdz/main.ts @@ -0,0 +1,8 @@ +// The first cycle member to run builds classes whose static initializers +// call a method through `this`, directly and from an arrow function; both +// methods read constants of the other member before it declared them: +// Node's temporal-dead-zone ReferenceError. A third method reads a constant +// only after the cycle finished. +import { label, Probe, later } from "./alpha.ts"; + +console.log(label, Probe.result, later()); diff --git a/tests/corpus/7383-cycle-class-expression-name-tdz/alpha.ts b/tests/corpus/7383-cycle-class-expression-name-tdz/alpha.ts new file mode 100644 index 000000000..ff8eb4a87 --- /dev/null +++ b/tests/corpus/7383-cycle-class-expression-name-tdz/alpha.ts @@ -0,0 +1,11 @@ +import { Probe, Lazy } from "./beta.ts"; + +export { Probe }; +export const label = "ready"; +export const other = "other"; + +export function later(): string { + return `${Lazy.describe()} ${other}`; +} + +console.log("alpha body", Probe.result, Lazy.value); diff --git a/tests/corpus/7383-cycle-class-expression-name-tdz/beta.ts b/tests/corpus/7383-cycle-class-expression-name-tdz/beta.ts new file mode 100644 index 000000000..d1d89ad1a --- /dev/null +++ b/tests/corpus/7383-cycle-class-expression-name-tdz/beta.ts @@ -0,0 +1,26 @@ +import { label, other } from "./alpha.ts"; + +function guard(read: () => string): string { + try { + return read(); + } catch (error) { + return error instanceof ReferenceError ? `ReferenceError: ${error.message}` : "other error"; + } +} + +export const Probe = class ProbeImpl { + static helper(): string { + return guard(() => `label ${label}`); + } + static result = ProbeImpl.helper(); +}; + +export const Lazy = class LazyImpl { + static read(): string { + return guard(() => `other ${other}`); + } + static describe(): string { + return `lazy ${other}`; + } + static value = [0].map(() => LazyImpl.read()).join(""); +}; diff --git a/tests/corpus/7383-cycle-class-expression-name-tdz/main.ts b/tests/corpus/7383-cycle-class-expression-name-tdz/main.ts new file mode 100644 index 000000000..93b7a47ee --- /dev/null +++ b/tests/corpus/7383-cycle-class-expression-name-tdz/main.ts @@ -0,0 +1,9 @@ +// The first cycle member to run builds classes from named class +// expressions whose static initializers call a method through the inner +// name, directly and from an arrow function; both methods read constants +// of the other member before it declared them: Node's temporal-dead-zone +// ReferenceError. A third method reads a constant only after the cycle +// finished. +import { label, Probe, later } from "./alpha.ts"; + +console.log(label, Probe.result, later()); diff --git a/tests/corpus/7390-record-key-read-presence.ts b/tests/corpus/7390-record-key-read-presence.ts new file mode 100644 index 000000000..76bb355ca --- /dev/null +++ b/tests/corpus/7390-record-key-read-presence.ts @@ -0,0 +1,49 @@ +// Keyed reads of string-indexed records whose values are numbers or +// booleans, consumed by `??`, `!`, comparisons with undefined and typeof: +// declared fields, an optional declared field, index-signature entries and +// missing keys. A stored NaN is a value, so `??` keeps it; -0 survives the +// read; a missing key is undefined. +interface Scores { + base: number; + bonus?: number; + [name: string]: number | undefined; +} + +const scores: Scores = { base: 10 }; +scores["alice"] = 3; +scores["nan"] = NaN; +scores["negzero"] = -0; + +const keys = ["base", "bonus", "alice", "nan", "negzero", "missing"]; +for (const key of keys) { + const read = scores[key]; + console.log( + key, + scores[key] ?? "default", + scores[key] === undefined, + typeof scores[key], + Object.is(scores[key] ?? 1, -0), + (scores[key] ?? 0) + 1, + read, + ); +} +scores.bonus = 5; +console.log("bonus", scores["bonus"] ?? "default", scores["bonus"]! * 2); + +const counts: Record = {}; +for (const word of "a b a c b a d".split(" ")) counts[word] = (counts[word] ?? 0) + 1; +let total = 0; +for (const word in counts) total += counts[word]!; +console.log(JSON.stringify(counts), total, counts["zzz"] ?? -1); + +const flags: Record = { on: true, off: false }; +for (const key of ["on", "off", "unset"]) { + console.log(key, flags[key] ?? "unset", flags[key] === undefined, typeof flags[key]); +} +let seen = 0; +for (let i = 0; i < 1000; i++) { + const key = "k" + (i % 7); + flags[key] = !(flags[key] ?? false); + if (flags[key]!) seen++; +} +console.log(seen, Object.keys(flags).length); 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/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/7447-union-record-arm-width.mjs b/tests/corpus/7447-union-record-arm-width.mjs new file mode 100644 index 000000000..dc12f8a59 --- /dev/null +++ b/tests/corpus/7447-union-record-arm-width.mjs @@ -0,0 +1,37 @@ +// Values read back from arrays whose elements mix object literals of +// different widths, an empty object among them, keep every field: the +// record arm with more fields is tried before `{}`, which matches every +// object. +const pairs = [ + ["a", { publicKeyEncoding: 5 }], + ["b", {}], + ["c", { publicKeyEncoding: 6, extra: true }], +]; +for (const [label, value] of pairs) console.log(label, Object.keys(value), JSON.stringify(value)); + +const options = [ + ["rsa", { modulusLength: 1024 }], + ["ec", { namedCurve: "P-256", paramEncoding: "named" }], + ["none", {}], +]; +for (const [type, value] of options) { + const { modulusLength, namedCurve, paramEncoding } = value; + console.log(type, modulusLength, namedCurve, paramEncoding); +} + +const mixed = [{ a: 1 }, {}, "s", { a: 2, b: "two" }]; +for (const value of mixed) { + console.log(typeof value, JSON.stringify(value), typeof value === "object" ? Object.keys(value).length : -1); +} + +function describe(value) { + return Object.entries(value) + .map(([key, field]) => `${key}=${field}`) + .join(";"); +} +const rows = [ + [1, { x: 1, y: 2 }], + [2, {}], + [3, { x: 3 }], +]; +for (const [id, value] of rows) console.log(id, describe(value), value.x, value.y); diff --git a/tests/corpus/7448-union-record-arm-width.ts b/tests/corpus/7448-union-record-arm-width.ts new file mode 100644 index 000000000..1518dd575 --- /dev/null +++ b/tests/corpus/7448-union-record-arm-width.ts @@ -0,0 +1,21 @@ +// A checked cast to a union of record types keeps every field of the +// value: arms with more required fields, then more fields, are tried +// before an arm whose fields are all optional, which matches every object. +type Full = { p: number; q: string }; +type Bare = { p?: number }; +type Other = { r: boolean }; + +const values = JSON.parse('[{"p":5,"q":"x"},{},{"r":true},{"p":7}]') as (Full | Bare | Other)[]; +for (const value of values) { + const q = "q" in value ? value.q : "-"; + const r = "r" in value ? value.r : "-"; + console.log(JSON.stringify(value), "q" in value, "r" in value, q, r); +} + +const one = JSON.parse('{"p":1,"q":"y"}') as Bare | Full; +console.log(JSON.stringify(one), "q" in one ? one.q : "no q"); + +type Point = { x: number; y?: number }; +type Labeled = { x: number; label: string }; +const points = JSON.parse('[{"x":1,"label":"a"},{"x":2,"y":3},{"x":4}]') as (Point | Labeled)[]; +for (const point of points) console.log(point.x, "label" in point ? point.label : "none", "y" in point ? point.y : "none"); diff --git a/tests/corpus/7460-transform-writev.mjs b/tests/corpus/7460-transform-writev.mjs new file mode 100644 index 000000000..d78bdfc77 --- /dev/null +++ b/tests/corpus/7460-transform-writev.mjs @@ -0,0 +1,33 @@ +// writev on a Transform and a PassThrough (the writev option): a batch of +// buffered writes reaches writev instead of the transform, a lone write +// still reaches the transform, and the readable side gets what writev +// pushes. +import { Transform, PassThrough } from "node:stream"; +const t = new Transform({ + transform(chunk, enc, cb) { console.log("transform", String(chunk), enc); cb(null, chunk); }, + writev(chunks, cb) { + console.log("writev", chunks.map((c) => `${c.chunk}/${c.encoding}`).join(",")); + for (const { chunk } of chunks) this.push(String(chunk).toUpperCase()); + cb(); + }, +}); +t.on("data", (d) => console.log("data", String(d))); +t.on("finish", () => console.log("finish")); +t.on("end", () => console.log("end")); +t.write("a"); +t.cork(); +t.write("b"); +t.write("c", "latin1"); +process.nextTick(() => { t.uncork(); t.end("d"); }); + +const p = new PassThrough({ + objectMode: true, + writev(chunks, cb) { console.log("pt writev", JSON.stringify(chunks.map((c) => c.chunk))); cb(); }, +}); +p.on("data", (d) => console.log("pt data", JSON.stringify(d))); +p.cork(); +p.write({ x: 1 }); +p.write([2]); +p.uncork(); +p.write("solo"); +p.end(); diff --git a/tests/corpus/7461-transform-writev.cjs b/tests/corpus/7461-transform-writev.cjs new file mode 100644 index 000000000..dd45cf09d --- /dev/null +++ b/tests/corpus/7461-transform-writev.cjs @@ -0,0 +1,33 @@ +// Constructor-function Transform and PassThrough streams with _writev: +// the prototype's _writev takes the batches, and a writev option shadows +// it with the stream as this. +'use strict'; +const { Transform, PassThrough } = require('node:stream'); +function T(opts) { Transform.call(this, opts); } +Object.setPrototypeOf(T.prototype, Transform.prototype); +Object.setPrototypeOf(T, Transform); +T.prototype._transform = function (chunk, enc, cb) { cb(null, 'T(' + chunk + ')'); }; +T.prototype._writev = function (chunks, cb) { + this.push('V(' + chunks.map((c) => String(c.chunk)).join('+') + ')'); + cb(); +}; +const t = Object.create(T.prototype); +T.call(t, {}); +t.on('data', (d) => console.log('t data', String(d))); +t.on('end', () => console.log('t end')); +t.write('a'); +t.cork(); +t.write('b'); +t.write('c'); +process.nextTick(() => { t.uncork(); t.end(); }); +function P(opts) { PassThrough.call(this, opts); } +Object.setPrototypeOf(P.prototype, PassThrough.prototype); +Object.setPrototypeOf(P, PassThrough); +const p = Object.create(P.prototype); +P.call(p, { writev(chunks, cb) { console.log('p writev', chunks.length, this === p); cb(); } }); +p.on('data', (d) => console.log('p data', String(d))); +p.cork(); +p.write('x'); +p.write('y'); +p.uncork(); +p.write('z'); diff --git a/tests/corpus/7462-transform-writev.ts b/tests/corpus/7462-transform-writev.ts new file mode 100644 index 000000000..ca079b3c9 --- /dev/null +++ b/tests/corpus/7462-transform-writev.ts @@ -0,0 +1,109 @@ +// Typed writev on Transform and PassThrough: an options writev with +// decodeStrings: false and flush, a Transform subclass's _writev, a +// batch completing with an error, and object-mode entries whose +// encoding is declared as a string or as Node's string | null | +// undefined. +import { Transform, PassThrough, Writable } from "node:stream"; +type WritevChunks = Array<{ chunk: any; encoding: BufferEncoding }>; +type Cb = (error?: Error | null) => void; +type TransformCallback = (error?: Error | null, data?: any) => void; +const log: string[] = []; +const t = new Transform({ + decodeStrings: false, + transform(chunk: string, enc: BufferEncoding, cb: TransformCallback): void { + log.push(`transform ${chunk} ${enc}`); + cb(null, chunk); + }, + flush(cb: TransformCallback): void { + log.push("flush"); + cb(null, "!"); + }, + writev(chunks: WritevChunks, cb: Cb): void { + log.push(`writev ${chunks.map((c) => `${c.chunk}/${c.encoding}`).join(",")}`); + this.push(chunks.map((c) => String(c.chunk)).join("")); + cb(); + }, +}); +t.on("data", (d: Buffer) => log.push(`data ${d.toString()}`)); +t.on("end", () => log.push("end")); +t.on("finish", () => log.push("finish")); +t.on("close", () => { + console.log(log.join("\n")); + next(); +}); +t.write("one"); +t.cork(); +t.write("two", "latin1"); +t.write("three"); +t.uncork(); +t.end("four"); + +class Batcher extends Transform { + _transform(chunk: Buffer, _enc: BufferEncoding, cb: TransformCallback): void { + console.log("batcher transform", chunk.toString()); + cb(null, chunk); + } + _writev(chunks: Array<{ chunk: any; encoding: BufferEncoding }>, cb: Cb): void { + console.log("batcher writev", chunks.length); + this.push(Buffer.concat(chunks.map((c) => c.chunk as Buffer))); + cb(); + } +} + +function next(): void { + const b = new Batcher(); + b.on("data", (d: Buffer) => console.log("batcher data", d.toString())); + b.write("x"); + b.cork(); + b.write("y"); + b.write("z"); + process.nextTick(() => { + b.uncork(); + b.end(); + }); + b.on("finish", () => { + const p = new PassThrough({ + writev(chunks: WritevChunks, cb: Cb): void { + console.log("pt writev", chunks.length, String(chunks[0]!.chunk)); + cb(new Error("batch failed")); + }, + }); + p.on("error", (e: Error) => { + console.log("pt error", e.message); + objectEntries(); + }); + p.cork(); + p.write("1"); + p.write("2"); + p.uncork(); + }); +} + +function objectEntries(): void { + const declared = new Writable({ + objectMode: true, + write(_chunk: any, _enc: BufferEncoding, cb: Cb): void { cb(); }, + writev(chunks: WritevChunks, cb: Cb): void { + console.log("string encodings", JSON.stringify(chunks.map((c) => c.chunk))); + cb(); + }, + }); + declared.write({ n: 0 }); + declared.cork(); + declared.write({ n: 1 }); + declared.write({ n: 2 }, () => {}); + declared.uncork(); + const nodeTyped = new Writable({ + objectMode: true, + write(_chunk: any, _enc: BufferEncoding, cb: Cb): void { cb(); }, + writev(chunks: Array<{ chunk: any; encoding: string | null | undefined }>, cb: Cb): void { + console.log("node encodings", JSON.stringify(chunks.map((c) => [c.chunk, c.encoding === undefined ? "undefined" : c.encoding]))); + cb(); + }, + }); + nodeTyped.write({ n: 0 }); + nodeTyped.cork(); + nodeTyped.write({ n: 1 }); + nodeTyped.write({ n: 2 }, () => {}); + nodeTyped.uncork(); +} diff --git a/tests/corpus/7463-stream-option-values.ts b/tests/corpus/7463-stream-option-values.ts new file mode 100644 index 000000000..04f497b6b --- /dev/null +++ b/tests/corpus/7463-stream-option-values.ts @@ -0,0 +1,58 @@ +// Typed decodeStrings and defaultEncoding options whose values are known +// only at run time: decodeStrings === false keeps string writes, a known +// defaultEncoding is the write default as given and how push(string) +// decodes, null and utf8 are the default, an unknown name throws +// ERR_UNKNOWN_ENCODING before construction, and a subclass's super() +// options take them the same way. +import { Writable, Readable, Duplex } from "node:stream"; +type Cb = (error?: Error | null) => void; +const flags: boolean[] = JSON.parse("[false, true]"); +const encodings: Array = JSON.parse('["hex", "latin1", null, "UTF8"]'); +for (const decode of flags) { + const w = new Writable({ + decodeStrings: decode, + defaultEncoding: encodings[0] as BufferEncoding, + write(chunk: any, enc: string, cb: Cb): void { + console.log("write", decode, typeof chunk, enc, String(chunk)); + cb(); + }, + }); + w.write("6869"); +} +const r = new Readable({ defaultEncoding: encodings[1] as BufferEncoding, read(): void {} }); +r.push("é"); +r.push(null); +r.on("data", (d: Buffer) => console.log("readable", d.toString("hex"))); +const dflt = new Writable({ + defaultEncoding: encodings[2] as any, + write(chunk: Buffer, enc: string, cb: Cb): void { + console.log("null default", enc, chunk.toString()); + cb(); + }, +}); +dflt.write("ok"); +const upper = new Duplex({ + defaultEncoding: encodings[3] as BufferEncoding, + decodeStrings: flags[0], + read(): void {}, + write(chunk: any, enc: string, cb: Cb): void { + console.log("duplex", typeof chunk, enc); + cb(); + }, +}); +upper.write("x"); +try { + new Writable({ defaultEncoding: JSON.parse('"nope"') }); +} catch (e) { + console.log("bad", (e as Error & { code?: string }).code, (e as Error).message); +} +class Sink extends Writable { + constructor(decode: boolean, encoding: BufferEncoding) { + super({ decodeStrings: decode, defaultEncoding: encoding }); + } + _write(chunk: any, enc: string, cb: Cb): void { + console.log("sink", typeof chunk, enc); + cb(); + } +} +new Sink(flags[0]!, encodings[1] as BufferEncoding).write("y"); diff --git a/tests/corpus/7464-stream-option-values.mjs b/tests/corpus/7464-stream-option-values.mjs new file mode 100644 index 000000000..dfeb7a447 --- /dev/null +++ b/tests/corpus/7464-stream-option-values.mjs @@ -0,0 +1,28 @@ +// Option values known only at run time in JavaScript: decodeStrings and +// defaultEncoding read from an object, a write callback whose chunk is +// then a string, decodeStrings: 0 (only false turns decoding off), a +// defaultEncoding that is not a string, and a _construct assigned after +// construction, which Node's constructor never reads. +import { Writable, Readable } from "node:stream"; +const opts = { decode: false, enc: "base64" }; +const w = new Writable({ + decodeStrings: opts.decode, + defaultEncoding: opts.enc, + write(c, e, cb) { console.log("js", typeof c, e, String(c)); cb(); }, +}); +w.write("aGk="); +try { new Writable({ defaultEncoding: 5 }); } catch (e) { console.log("bad", e.code, e.message); } +const zero = new Writable({ decodeStrings: 0, write(c, e, cb) { console.log("zero", Buffer.isBuffer(c), e); cb(); } }); +zero.write("z"); +const late = new Writable({ write(chunk, enc, cb) { console.log("write", chunk.toString()); cb(); } }); +let ran = false; +late._construct = (cb) => { ran = true; cb(); }; +late.write("a"); +late.end(() => console.log("ended, construct ran:", ran)); +const r = new Readable({ read() {} }); +const order = []; +(order.push("receiver"), r)._construct = (order.push("value"), function (cb) { console.log("never"); cb(); }); +console.log(order.join(",")); +r.push("x"); +r.push(null); +r.on("data", (d) => console.log("data", d.toString())); diff --git a/tests/corpus/7465-transform-callback-values.ts b/tests/corpus/7465-transform-callback-values.ts new file mode 100644 index 000000000..e7003e894 --- /dev/null +++ b/tests/corpus/7465-transform-callback-values.ts @@ -0,0 +1,41 @@ +// A transform or flush callback passing a value that is not a Buffer or a +// string: the push reports Node's ERR_INVALID_ARG_TYPE through 'error' +// and destroys the stream, for option callbacks and _transform methods, +// while Buffer and string values take the typed path. +import { Transform } from "node:stream"; +type TransformCallback = (error?: Error | null, data?: any) => void; +const t = new Transform({ + transform(chunk: Buffer, _enc: BufferEncoding, cb: TransformCallback): void { + console.log("transform", chunk.toString()); + cb(null, 42); + }, +}); +t.on("error", (e: Error & { code?: string }) => console.log("error", e.name, e.code, e.message)); +t.on("data", (d: Buffer) => console.log("data", d.toString())); +t.on("close", () => console.log("close", t.destroyed)); +console.log("write", t.write("a")); +const f = new Transform({ + transform(chunk: Buffer, _enc: BufferEncoding, cb: TransformCallback): void { cb(null, chunk); }, + flush(cb: TransformCallback): void { cb(null, { tail: true }); }, +}); +f.on("error", (e: Error & { code?: string }) => console.log("flush error", e.code, e.message)); +f.on("data", (d: Buffer) => console.log("flush data", d.toString())); +f.on("close", () => console.log("flush close")); +f.end("z"); +class Bad extends Transform { + _transform(chunk: Buffer, _enc: BufferEncoding, cb: TransformCallback): void { + cb(null, chunk.length); + } +} +const bad = new Bad(); +bad.on("error", (e: Error & { code?: string }) => console.log("error", e.code, e.message)); +bad.on("close", () => console.log("close")); +bad.write("abc"); +class Good extends Transform { + _transform(chunk: Buffer, _enc: BufferEncoding, cb: TransformCallback): void { + cb(null, chunk.toString().toUpperCase()); + } +} +const good = new Good(); +good.on("data", (d: Buffer) => console.log("good", d.toString())); +good.end("xyz"); diff --git a/tests/corpus/7466-stream-construct-assign.mjs b/tests/corpus/7466-stream-construct-assign.mjs new file mode 100644 index 000000000..37684afcd --- /dev/null +++ b/tests/corpus/7466-stream-construct-assign.mjs @@ -0,0 +1,46 @@ +// Assigning _construct after construction: Node reads it on the +// construction tick, so an assignment before that tick replaces the +// scheduled hook, and one after it, or on a stream without a hook, has no +// effect. +import { Readable, Writable } from "node:stream"; + +const w = new Writable({ + construct(cb) { + console.log("original construct"); + cb(); + }, + write(chunk, _enc, cb) { + console.log("write", chunk.toString()); + cb(); + }, +}); +w._construct = function (cb) { + console.log("replacement construct"); + cb(); +}; +w.write("a"); +w.end(); +w.on("finish", () => { + w._construct = function (cb) { + console.log("late construct"); + cb(); + }; + console.log("finish"); + readable(); +}); + +// A stream without a construct hook: the assignment has no effect. +function readable() { + const r = new Readable({ + read() { + this.push("x"); + this.push(null); + }, + }); + r._construct = function (cb) { + console.log("never"); + cb(); + }; + r.on("data", (d) => console.log("data", d.toString())); + r.on("end", () => console.log("end")); +} diff --git a/tests/corpus/7500-pipeline-callback-undefined.ts b/tests/corpus/7500-pipeline-callback-undefined.ts new file mode 100644 index 000000000..e1503ff6b --- /dev/null +++ b/tests/corpus/7500-pipeline-callback-undefined.ts @@ -0,0 +1,120 @@ +// pipeline() with an async function or async generator destination: a +// typed callback whose error parameter admits undefined receives +// undefined when the pipeline succeeds, as in Node, and the error when it +// fails. +import { pipeline, Readable } from "node:stream"; + +function source(): Readable { + return Readable.from(["a", "b", "c"]); +} + +async function join(src: AsyncIterable): Promise { + let s = ""; + for await (const c of src) s += c; + return s; +} + +function optional(): Promise { + return new Promise((resolve) => { + pipeline(source(), join, (err?: Error | null, value?: string) => { + console.log("optional", err, err === undefined, value); + resolve(); + }); + }); +} + +function declared(): Promise { + return new Promise((resolve) => { + pipeline( + source(), + async function (src: AsyncIterable) { + let n = 0; + for await (const c of src) n += c.length; + return n; + }, + (err: Error | null | undefined, value?: number) => { + console.log("declared", err, typeof err, value); + resolve(); + }, + ); + }); +} + +function unknownParam(): Promise { + return new Promise((resolve) => { + pipeline(source(), join, (err: unknown, value: unknown) => { + console.log("unknown", err, value); + resolve(); + }); + }); +} + +function rest(): Promise { + return new Promise((resolve) => { + pipeline( + source(), + async function* (src: AsyncIterable) { + for await (const c of src) yield c.toUpperCase(); + }, + async function (src: AsyncIterable) { + for await (const c of src) void c; + }, + (...args: unknown[]) => { + console.log("rest", args.length, args); + resolve(); + }, + ); + }); +} + +function generatorDestination(): Promise { + return new Promise((resolve) => { + pipeline( + source(), + async function* (src: AsyncIterable) { + for await (const c of src) yield c; + }, + (err: Error | null | undefined, value: unknown) => { + console.log("generator", err, value); + resolve(); + }, + ); + }); +} + +function named(): Promise { + return new Promise((resolve) => { + function done(err: Error | null | undefined, value: string | undefined): void { + console.log("named", err, value); + resolve(); + } + pipeline(source(), join, done); + }); +} + +function failing(): Promise { + return new Promise((resolve) => { + pipeline( + source(), + async function (src: AsyncIterable) { + for await (const c of src) if (c === "b") throw new Error("boom at " + c); + return "never"; + }, + (err?: Error | null, value?: string) => { + console.log("failing", err?.message, value); + resolve(); + }, + ); + }); +} + +async function main(): Promise { + await optional(); + await declared(); + await unknownParam(); + await rest(); + await generatorDestination(); + await named(); + await failing(); +} +void main(); diff --git a/tests/corpus/7501-pipeline-callback-undefined.mjs b/tests/corpus/7501-pipeline-callback-undefined.mjs new file mode 100644 index 000000000..c51738e77 --- /dev/null +++ b/tests/corpus/7501-pipeline-callback-undefined.mjs @@ -0,0 +1,22 @@ +// pipeline() with an async function destination in JavaScript: the +// callback receives undefined and the destination's value on success. +import { pipeline, Readable } from "node:stream"; + +pipeline( + Readable.from(["x", "y"]), + async function (src) { + const out = []; + for await (const c of src) out.push(c); + return out.join("-"); + }, + (err, value) => { + console.log("callback", err, typeof err, value); + pipeline( + Readable.from(["z"]), + async function (src) { + for await (const c of src) void c; + }, + (...args) => console.log("no value", args), + ); + }, +); diff --git a/tests/corpus/7502-readline-cursor-undefined.ts b/tests/corpus/7502-readline-cursor-undefined.ts new file mode 100644 index 000000000..abac985cb --- /dev/null +++ b/tests/corpus/7502-readline-cursor-undefined.ts @@ -0,0 +1,28 @@ +// rl.cursor answers undefined on an interface that is not a terminal and +// the cursor position on a terminal interface. +import * as readline from "node:readline"; +import { PassThrough, Writable } from "node:stream"; + +const input = new PassThrough(); +const rl = readline.createInterface({ input }); +const c = rl.cursor; +console.log("cursor", c, typeof rl.cursor, rl.cursor === undefined); +console.log("arithmetic", rl.cursor + 1, Number.isNaN(rl.cursor * 2)); +console.log("fallback", rl.cursor ?? -1, `${rl.cursor}`); + +const interfaces = [rl]; +function first(): readline.Interface { + return interfaces[0]!; +} +console.log("call receiver", first().cursor); +rl.close(); + +const sink = new Writable({ + write(_chunk: Buffer, _enc, cb) { + cb(); + }, +}); +const terminal = readline.createInterface({ input: new PassThrough(), output: sink, terminal: true }); +terminal.write("abc"); +console.log("terminal", terminal.cursor, typeof terminal.cursor, terminal.cursor + 1); +terminal.close(); 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/7543-var-list-declarator-order.js b/tests/corpus/7543-var-list-declarator-order.js new file mode 100644 index 000000000..8fae06ff9 --- /dev/null +++ b/tests/corpus/7543-var-list-declarator-order.js @@ -0,0 +1,66 @@ +// A `var` read in a later declarator of the statement that stores it runs +// after the store; a read in the same or an earlier declarator sees undefined. +var base = { a: 1, b: "x" }, + more = { ...base, c: 2 }, + keys = Object.keys({ ...more }).join(","), + first = base.a + 1; +console.log(JSON.stringify(more), keys, first); + +function describeBase() { + return base.b + more.c; +} +var rules = { space: /\s+/, word: /\w+/ }, + merged = { ...rules, digit: /\d/ }, + described = describeBase(); +console.log(Object.keys(merged).join(","), merged.word.test("ab"), described); + +var table = { one: 1, two: 2 }, + lookup = function (key) { + return table[key]; + }, + arrow = (key) => ({ ...table })[key]; +console.log(lookup("one"), arrow("two")); + +var readLater = function () { + return later; + }, + early = readLater(), + later = { k: 1 }, + after = readLater().k; +console.log(early, after); + +var self = self ? 1 : 2; +console.log(self); + +for (var i = 0, j = i + 10; i < 2; i++) console.log(i, j); + +function nested() { + var inner = [1, 2, 3], + copy = [...inner, 4], + total = copy.reduce((sum, n) => sum + n, 0); + return { copy, total }; +} +console.log(JSON.stringify(nested())); + +function earlyInFunction() { + show("before"); + var point = { n: 1 }, + next = { m: point.n + 1 }; + show("after"); + function show(when) { + console.log(when, typeof point, point === undefined ? "none" : point.n, next === undefined ? "none" : next.m); + } + var check = () => console.log("check", items === undefined); + check(); + var items = [1]; + check(); +} +earlyInFunction(); + +function loopLists() { + for (var k = 0, sum = k; k < 3; k++) sum += k; + var doubled = sum * 2, + again = doubled + 1; + return [sum, doubled, again]; +} +console.log(JSON.stringify(loopLists())); 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/7545-string-regex-checked-arguments.js b/tests/corpus/7545-string-regex-checked-arguments.js new file mode 100644 index 000000000..a7ace9cdb --- /dev/null +++ b/tests/corpus/7545-string-regex-checked-arguments.js @@ -0,0 +1,89 @@ +// String search, match and replace with untyped pattern values: a RegExp +// is used as is, and any other value is converted as Node converts it. +const rules = JSON.parse('{"other":{}}'); +rules.other.space = /\s+/g; +rules.other.word = /o+/; +rules.other.text = "l"; +rules.other.dot = "."; +rules.other.none = undefined; +rules.other.nul = null; +rules.other.one = /(l)(o)/; +rules.other.many = /l+/g; +const t = " hello world"; + +console.log( + t.search(rules.other.word), + t.search(rules.other.text), + t.search(rules.other.none), + t.search(rules.other.nul), + t.search(rules.other.space), + "a.b".search(rules.other.dot), +); +console.log(rules.other.space.lastIndex); + +let conversions = 0; +rules.other.obj = { + toString() { + conversions++; + return "w"; + }, +}; +console.log(t.search(rules.other.obj), t.match(rules.other.obj)[0], conversions); + +const all = t.match(rules.other.many); +console.log(JSON.stringify(all), rules.other.many.lastIndex); +const pair = t.match(rules.other.one); +console.log(pair[0], pair[1], pair[2], pair.index, pair.length, pair.input === t); +const literal = t.match(rules.other.dot); +console.log(literal[0], literal.index, literal.length); +console.log(JSON.stringify(t.match(rules.other.none)), t.match(rules.other.zz)); +rules.other.zz = "zz"; +console.log(t.match(rules.other.zz)); +function first(text, re) { + const m = text.match(re); + return m ? m[0] : "none"; +} +console.log(first("abc", rules.other.dot), first("", rules.other.dot)); + +console.log(t.replace(rules.other.space, (m) => "[" + m.length + "]")); +console.log(t.replace(rules.other.word, (m, offset) => m.toUpperCase() + offset)); +console.log(t.replace(rules.other.text, (m, offset, subject) => "<" + m + offset + subject.length + ">")); +console.log(t.replaceAll(rules.other.text, (m) => m.toUpperCase())); +console.log(t.replaceAll(rules.other.space, () => "_")); +console.log(t.replace(rules.other.one, (m, p1, p2, offset) => p1 + "|" + p2 + "|" + offset)); +console.log(t.replace(rules.other.text, (m, offset, subject, extra) => typeof extra)); +console.log(t.replace(rules.other.text, (m) => m.length)); +try { + t.replaceAll(rules.other.word, (m) => m); +} catch (e) { + console.log(e.constructor.name, e.message); +} + +function tabs(text, width) { + let column = 0; + let out = ""; + for (const ch of text) { + if (ch === "\t") { + const pad = 4 - (column % 4); + out += " ".repeat(pad); + column += pad; + } else { + out += ch; + column++; + } + } + return out + width; +} +class Lexer { + constructor(table) { + this.rules = table; + } + indent(line, width) { + return line.replace(this.rules.leading, (w) => tabs(w, width)); + } + firstText(line) { + return line.search(this.rules.nonSpace); + } +} +const lexer = new Lexer({ leading: /^( {1,4}| {0,3}\t)/, nonSpace: /[^ ]/ }); +console.log(JSON.stringify(lexer.indent("\tabc", 2)), lexer.firstText(" x")); diff --git a/tests/corpus/7548-cjs-receiverless-builtin-calls/lib.cjs b/tests/corpus/7548-cjs-receiverless-builtin-calls/lib.cjs new file mode 100644 index 000000000..248e66270 --- /dev/null +++ b/tests/corpus/7548-cjs-receiverless-builtin-calls/lib.cjs @@ -0,0 +1,10 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: !0 }); +exports.label = exports.describe = void 0; +const node_util_1 = require("node:util"); +const node_path_1 = require("node:path"); +function describe(name, size) { + return (0, node_util_1.format)("%s holds %d (%s)", name, size, (0, node_path_1.basename)("/a/b/c.txt")); +} +exports.describe = describe; +exports.label = "lib"; diff --git a/tests/corpus/7548-cjs-receiverless-builtin-calls/main.cjs b/tests/corpus/7548-cjs-receiverless-builtin-calls/main.cjs new file mode 100644 index 000000000..6fd0b78a9 --- /dev/null +++ b/tests/corpus/7548-cjs-receiverless-builtin-calls/main.cjs @@ -0,0 +1,19 @@ +"use strict"; +// tsc and esbuild output: the __esModule stamp spelled { value: !0 }, and +// calls of builtin module functions without a receiver, (0, ns.member)(...). +const lib = require("./lib.cjs"); +const dc = require("node:diagnostics_channel"); +const util = require("node:util"); +const os = require("node:os"); +const path = require("node:path"); +console.log(lib.label, lib.describe("cache", 3)); +const metrics = (0, dc.channel)("cache:metrics"); +console.log(metrics.name, metrics.hasSubscribers); +const seen = []; +dc.subscribe("cache:metrics", (message) => seen.push(message.key)); +metrics.publish({ key: "a" }); +console.log(seen.join(","), metrics.hasSubscribers); +const tracing = (0, dc.tracingChannel)("cache"); +console.log(tracing.hasSubscribers); +console.log((0, util.inspect)({ a: [1, 2] }), (0, util.isDeepStrictEqual)([1], [1])); +console.log((0, path.join)("a", "b"), typeof (0, os.platform)()); diff --git a/tests/corpus/7549-cjs-minified-esmodule-stamp/main.cjs b/tests/corpus/7549-cjs-minified-esmodule-stamp/main.cjs new file mode 100644 index 000000000..c619a847a --- /dev/null +++ b/tests/corpus/7549-cjs-minified-esmodule-stamp/main.cjs @@ -0,0 +1,7 @@ +"use strict"; +// Minifiers spell `true` as `!0`: Object.defineProperty(exports, +// "__esModule", { value: !0 }) is the same interop stamp as { value: true }. +const stamped = require("./stamped.cjs"); +const moduleStamped = require("./module-stamped.cjs"); +console.log(stamped.answer, stamped.twice(4), moduleStamped.label); +console.log(typeof stamped.twice, moduleStamped.label.length); diff --git a/tests/corpus/7549-cjs-minified-esmodule-stamp/module-stamped.cjs b/tests/corpus/7549-cjs-minified-esmodule-stamp/module-stamped.cjs new file mode 100644 index 000000000..96a2aae7b --- /dev/null +++ b/tests/corpus/7549-cjs-minified-esmodule-stamp/module-stamped.cjs @@ -0,0 +1,3 @@ +"use strict"; +Object.defineProperty(module.exports, "__esModule", { value: !0 }); +module.exports.label = "module"; diff --git a/tests/corpus/7549-cjs-minified-esmodule-stamp/stamped.cjs b/tests/corpus/7549-cjs-minified-esmodule-stamp/stamped.cjs new file mode 100644 index 000000000..cd3b9c400 --- /dev/null +++ b/tests/corpus/7549-cjs-minified-esmodule-stamp/stamped.cjs @@ -0,0 +1,5 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: !0 }); +exports.answer = void 0; +exports.answer = 42; +exports.twice = (n) => n * 2; diff --git a/tests/corpus/7553-var-for-in-of-zero-iterations.js b/tests/corpus/7553-var-for-in-of-zero-iterations.js new file mode 100644 index 000000000..1a439f715 --- /dev/null +++ b/tests/corpus/7553-var-for-in-of-zero-iterations.js @@ -0,0 +1,18 @@ +// A for-in or for-of loop stores its var binding only when it iterates: +// after a loop that ran zero times, closures read undefined. +function f(o) { + for (var key in o) {} + const read = () => typeof key; + const same = () => key === undefined; + return [read(), same()]; +} +console.log(JSON.stringify(f({})), JSON.stringify(f({ a: 1 }))); +function g(xs) { + for (var x of xs) {} + const show = () => [typeof x, String(x)]; + return show(); +} +console.log(JSON.stringify(g([])), JSON.stringify(g([4]))); +for (var top in {}) {} +const readTop = () => typeof top; +console.log(readTop()); 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/7580-int32-arrays-across-calls.ts b/tests/corpus/7580-int32-arrays-across-calls.ts new file mode 100644 index 000000000..df180b264 --- /dev/null +++ b/tests/corpus/7580-int32-arrays-across-calls.ts @@ -0,0 +1,166 @@ +// Number arrays of proven int32s that reach other functions: passed to +// helpers called directly, along chains of helpers and recursion, and held +// in module globals that several functions read. A helper that also +// receives an array of other numbers or one that is not a fresh array, +// stores a fraction through its parameter, or escapes as a value keeps +// every array it can reach in doubles, as does a global an async function +// reads. Each case uses its own arrays and helpers. + +const data: number[] = []; +let seed = 7; +for (let i = 0; i < 2000; i++) { + seed = (Math.imul(seed, 48271) + 11) & 0x7fffffff; + data.push(seed % 1000); +} + +function fnv(values: number[]): number { + let h = 2166136261; + for (let i = 0; i < values.length; i++) { + h ^= values[i]! & 0xff; + h = Math.imul(h, 16777619) >>> 0; + } + return h; +} + +function histogram(values: number[], buckets: number): number[] { + const counts: number[] = new Array(buckets).fill(0); + for (let i = 0; i < values.length; i++) { + const b = values[i]! % buckets; + counts[b] = counts[b]! + 1; + } + return counts; +} + +// A helper that writes through its parameter and calls another helper. +function scale(values: number[], factor: number): number { + for (let i = 0; i < values.length; i++) values[i] = (values[i]! * factor) & 0xffff; + return sum(values); +} + +function sum(values: number[]): number { + let s = 0; + for (let i = 0; i < values.length; i++) s = (s + values[i]!) | 0; + return s; +} + +// Recursion passes the array on, and the elements index another array. +function chase(next: number[], at: number, steps: number): number { + if (steps === 0) return at; + return chase(next, next[at]!, steps - 1); +} + +function pushAll(target: number[], count: number): void { + for (let i = 0; i < count; i++) target.push((i * 3) & 0xffff); +} + +// `n` is a power of two: the masked elements are indexes of `perm`. +function locals(n: number): string { + const perm: number[] = []; + for (let i = 0; i < n; i++) perm.push((i * 37 + 11) & (n - 1)); + const copy: number[] = []; + pushAll(copy, n); + const s1 = scale(copy, 5); + return `locals ${chase(perm, 0, 1000)} ${s1} ${copy[n - 1]} ${copy.length} ${sum(perm)}`; +} + +// Read only through the element reader (`a[i] ?? 0`) and states. +function readSparse(values: number[], at: number): string { + return `${values[at] ?? -1} ${at in values} ${values.length}`; +} + +// The same helper receives an array of other numbers too: the arrays it +// can reach keep doubles. +function mean(values: number[]): number { + let s = 0; + for (let i = 0; i < values.length; i++) s += values[i]!; + return s / values.length; +} + +// A store through the parameter that is not an int32. +function halve(values: number[]): void { + for (let i = 0; i < values.length; i++) values[i] = values[i]! / 2; +} + +// A function used as a value keeps its parameters. +function first(values: number[]): number { + return values[0]!; +} + +let reassigned: number[] = [1, 2, 3]; +function regrow(n: number): number { + reassigned = []; + for (let i = 0; i < n; i++) reassigned.push(i & 15); + let s = 0; + for (let i = 0; i < reassigned.length; i++) s += reassigned[i]!; + return s; +} + +// Not a fresh array: the result of a call. +function total(values: number[]): number { + let s = 0; + for (let i = 0; i < values.length; i++) s += values[i]!; + return s; +} + +const awaited: number[] = [4, 5, 6]; +async function later(): Promise { + return awaited[0]! + awaited.length; +} + +// A helper whose record result its callers only project: the backend may +// call its result-slot entry instead, so its parameter keeps doubles. +interface Span { + lo: number; + hi: number; +} +function spanAt(values: number[], i: number): Span { + return { lo: values[i]!, hi: values[i + 1]! }; +} +const spans: number[] = [5, 9, 2, 7, 11]; +function spanTotal(): number { + let total = 0; + for (let i = 0; i < 4; i++) { + const span = spanAt(spans, i); + total += span.hi * 10 - span.lo; + } + return total; +} + +const ints: number[] = [3, 4, 5]; +const fractions: number[] = [0.5, 1.5, 2.5]; +const halved: number[] = [8, 6, 4]; +halve(halved); +const pick: (values: number[]) => number = first; +const viaValue: number[] = [9, 8, 7]; + +// An index that calls a function reassigning the global: the read uses the +// array the global held before the index ran, as in Node. +let swapped: number[] = [1, 2, 3]; +function swapIn(): number { + swapped = []; + for (let i = 0; i < 4; i++) swapped.push(i + 10); + return 1; +} +function fillSwapped(n: number): number { + swapped = []; + for (let i = 0; i < n; i++) swapped.push(i & 15); + let s = 0; + for (let i = 0; i < swapped.length; i++) s += swapped[i]!; + return s; +} +function readSwapped(): number | undefined { + return swapped[swapIn()]; +} + +console.log(`fnv ${fnv(data)} ${fnv(data)}`); +const h = histogram(data, 10); +console.log(`hist ${h.length} ${h[0]} ${h[9]} ${total(h)}`); +console.log(locals(128)); +console.log(readSparse(data, 5), readSparse(data, 5000)); +console.log(`mean ${mean(ints)} ${mean(fractions)}`); +console.log(`halved ${halved[0]} ${halved[2]} ${halved.length}`); +console.log(`value ${pick(viaValue)} ${viaValue.length}`); +console.log(`regrow ${regrow(40)} ${reassigned.length} ${reassigned[39]} ${regrow(5)}`); +console.log(`span ${spanTotal()}`); +console.log(`swap ${fillSwapped(6)} ${readSwapped()} ${swapped[1]}`); +void later().then((v) => console.log(`later ${v}`)); 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/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/7620-js-class-view-descriptors.js b/tests/corpus/7620-js-class-view-descriptors.js new file mode 100644 index 000000000..a0dc42813 --- /dev/null +++ b/tests/corpus/7620-js-class-view-descriptors.js @@ -0,0 +1,91 @@ +// An instance of a JS class with public fields keeps its own properties in +// a property bag, and untyped code that calls a method on it sees a view +// built from that bag: data properties with their attributes, accessors, +// index-like keys (which come first, in ascending order), symbols, and +// properties added or removed after construction. Every refresh of the view +// must copy the bag exactly. +'use strict'; + +class Box { + label = 'box'; + count = 0; + constructor(n) { + this.count = n; + } + describe() { + return `${this.label}:${this.count}`; + } + bump() { + this.count++; + return this; + } +} + +function call(target, method, ...args) { + return target[method](...args); +} + +function put(target, key, value) { + target[key] = value; +} + +function drop(target, key) { + delete target[key]; +} + +function show(title, target) { + const lines = []; + for (const key of Reflect.ownKeys(target)) { + const d = Object.getOwnPropertyDescriptor(target, key); + const kind = 'value' in d ? `value=${String(d.value)} writable=${d.writable}` : `get=${typeof d.get} set=${typeof d.set}`; + lines.push(` ${String(key)}: ${kind} enumerable=${d.enumerable} configurable=${d.configurable}`); + } + console.log(`${title}\n${lines.join('\n')}`); +} + +const box = new Box(2); +Object.defineProperty(box, 'hidden', { value: 42, enumerable: false, writable: false, configurable: true }); +Object.defineProperty(box, 'computed', { + get() { + return this.count * 10; + }, + set(v) { + this.count = v / 10; + }, + enumerable: true, + configurable: true, +}); +Object.defineProperty(box, 'getterOnly', { get: () => 'g', enumerable: false, configurable: false }); +put(box, 2, 'two'); +put(box, 1, 'one'); +put(box, 'extra', 'x'); +const tag = Symbol('tag'); +put(box, tag, 'symbol'); + +console.log(call(box, 'describe')); +show('after construction', box); +console.log('keys', Object.keys(box).join(',')); + +call(box, 'bump'); +put(box, 'computed', 70); +drop(box, 'extra'); +put(box, 'later', true); +console.log(call(box, 'describe')); +show('after changes', box); +console.log('computed', box.computed, 'hidden', box.hidden, 'getterOnly', box.getterOnly); + +// Many views in a row see the same, current properties. +let total = 0; +for (let i = 0; i < 5; i++) { + call(box, 'bump'); + total += Object.keys(box).length; +} +console.log('total keys', total, call(box, 'describe')); + +const other = new Box(7); +console.log(call(other, 'describe'), Object.keys(other).join(',')); +try { + call(box, 'missing'); +} catch (error) { + console.log(error instanceof TypeError, error.message); +} 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/7640-child-spawn-before-fast-exit.ts b/tests/corpus/7640-child-spawn-before-fast-exit.ts new file mode 100644 index 000000000..9c232d96c --- /dev/null +++ b/tests/corpus/7640-child-spawn-before-fast-exit.ts @@ -0,0 +1,20 @@ +// A child spawned from a 'close' listener that exits before the listener +// returns still emits 'spawn' before 'exit'. +import { spawn } from "node:child_process"; + +function busyWait(ms: number): void { + const end = Date.now() + ms; + while (Date.now() < end) {} +} + +const events: string[] = []; +spawn("sh", ["-c", "exit 0"], { stdio: "ignore" }).on("close", () => { + const child = spawn("sh", ["-c", "exit 4"], { stdio: "ignore" }); + child.on("spawn", () => events.push("spawn")); + child.on("exit", (code) => events.push(`exit ${code}`)); + child.on("close", (code) => { + events.push(`close ${code}`); + console.log(events.join(", ")); + }); + busyWait(300); +}); 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/corpus/7740-handle-method-values.js b/tests/corpus/7740-handle-method-values.js new file mode 100644 index 000000000..dffcba306 --- /dev/null +++ b/tests/corpus/7740-handle-method-values.js @@ -0,0 +1,126 @@ +// Methods of native Web objects read as values in untyped code: hono's +// HonoRequest calls `raw[key]()` with a computed body-method name, and code +// probes `typeof x.json` or `"get" in headers`. Each method is one shared +// function that runs on its `this`; another receiver fails Node's brand check. +const report = (label, e) => console.log(label, e.name, e.message, e.code); +// An untyped holder, as values reach a JavaScript library. +const untyped = (value) => { + const holder = JSON.parse("{}"); + holder.value = value; + return holder.value; +}; + +async function bodies() { + const holder = JSON.parse("{}"); + holder.req = new Request("http://example.test/books", { method: "POST", body: '{"title":"Dune"}' }); + const cache = {}; + const cachedBody = (key) => { + const raw = holder.req; + if (cache[key]) return cache[key]; + return (cache[key] = raw[key]()); + }; + console.log(await cachedBody("json")); + for (const key of ["text", "arrayBuffer", "bytes"]) { + const res = new Response("abc"); + const value = await res[key](); + console.log(key, typeof value, value instanceof ArrayBuffer, value.length ?? value.byteLength); + } +} + +function shape() { + const req = JSON.parse("{}"); + req.value = new Request("http://example.test/"); + const r = req.value; + console.log(typeof r.json, typeof r.text, typeof r.blobby, typeof r.url); + console.log("json" in r, "url" in r, "toString" in r, "missing" in r); + const other = JSON.parse("{}"); + other.res = new Response("x"); + console.log(r.json === r.json, r.json === other.res.json, r.json === r.text); + console.log(r.json.name, r.json.length, r.arrayBuffer.name); + const box = { h: untyped(new Headers({ a: "1" })) }; + console.log(box.h.append.length, box.h.forEach.length, box.h.get.name, "has" in box.h, "nope" in box.h); + const name = "get"; + console.log(box.h[name]("a"), box.h["has"]("b")); + box.h["set"]("b", "2"); + const entries = []; + box.h["forEach"]((v, k) => entries.push(k + "=" + v)); + console.log(entries.join(",")); +} + +async function receivers() { + const box = { + h: untyped(new Headers({ a: "1" })), + req: untyped(new Request("http://example.test/", { method: "POST", body: "x" })), + }; + const get = box.h.get; + console.log(get.call(box.h, "a"), get.bind(box.h)("a"), get.apply(box.h, ["a"])); + try { + get("a"); + } catch (e) { + report("unbound get", e); + } + try { + get.call(box.req, "a"); + } catch (e) { + report("foreign get", e); + } + const text = box.req.text; + const pending = text(); + console.log("text returns", pending instanceof Promise); + await pending.then( + () => console.log("unexpected"), + (e) => report("unbound text", e), + ); + await text.call(box.h).catch((e) => report("foreign text", e)); + console.log(await text.call(box.req)); + const signal = { s: untyped(new AbortController().signal) }; + const throwIfAborted = signal.s.throwIfAborted; + try { + throwIfAborted(); + } catch (e) { + report("unbound throwIfAborted", e); + } + try { + signal.s.addEventListener.call(box.h, "abort", () => {}); + } catch (e) { + report("foreign addEventListener", e); + } + // The signal's EventTarget methods are EventTarget's own: one function + // shared with every target, which runs on any EventTarget receiver. + const target = untyped(new EventTarget()); + const add = signal.s.addEventListener; + console.log(add === target.addEventListener, signal.s.dispatchEvent === target.dispatchEvent); + console.log(add.name, add.length, "addEventListener" in signal.s, "throwIfAborted" in target); + const heard = (event) => console.log("heard", event.type, event.target === target); + add.call(target, "ping", heard); + console.log(signal.s.dispatchEvent.call(target, new Event("ping"))); + signal.s.removeEventListener.call(target, "ping", heard); + target.dispatchEvent(new Event("ping")); + try { + throwIfAborted.call(target); + } catch (e) { + report("target throwIfAborted", e); + } +} + +async function streams() { + const box = { body: untyped(new Response("stream").body) }; + const key = "getReader"; + const reader = box.body[key](); + const read = reader.read; + const first = await read.call(reader); + console.log(first.done, new TextDecoder().decode(first.value)); + await read().catch((e) => report("unbound read", e)); + try { + box.body.getReader.call(reader); + } catch (e) { + report("foreign getReader", e); + } +} + +(async () => { + await bodies(); + shape(); + await receivers(); + await streams(); +})(); diff --git a/tests/corpus/7741-abort-signal-listener-value.js b/tests/corpus/7741-abort-signal-listener-value.js new file mode 100644 index 000000000..1a8a201d7 --- /dev/null +++ b/tests/corpus/7741-abort-signal-listener-value.js @@ -0,0 +1,10 @@ +// An AbortSignal's EventTarget methods read as values, in a program that +// never adds a listener or makes an EventTarget of its own: the shared +// functions are released at exit like every other value. +const holder = JSON.parse("{}"); +holder.controller = new AbortController(); +const signal = holder.controller.signal; +const add = signal.addEventListener; +console.log(typeof add, add.name, add.length, signal.dispatchEvent.name); +console.log("removeEventListener" in signal, signal.removeEventListener.length); +console.log(signal.aborted); diff --git a/tests/corpus/7790-js-class-integrity-layouts.js b/tests/corpus/7790-js-class-integrity-layouts.js new file mode 100644 index 000000000..f889c02f8 --- /dev/null +++ b/tests/corpus/7790-js-class-integrity-layouts.js @@ -0,0 +1,139 @@ +"use strict"; +// Object.freeze, Object.seal and Object.preventExtensions next to JavaScript +// classes whose fields all exist from construction. The integrity state of +// a class instance lives beside its fields, so classes keep fixed layouts +// whether or not a program freezes values; frozen instances still refuse +// writes and additions as in Node. + +class Point { + constructor(x, y) { + this.x = x; + this.y = y; + } + norm() { + return Math.abs(this.x) + Math.abs(this.y); + } +} + +class Point3 extends Point { + constructor(x, y, z) { + super(x, y); + this.z = z; + } + norm() { + return super.norm() + Math.abs(this.z); + } +} + +class Status { + constructor() { + this.value = "valid"; + } + dirty() { + if (this.value === "valid") this.value = "dirty"; + } +} + +/** @param {any} target @param {string} key @param {unknown} value */ +function put(target, key, value) { + target[key] = value; +} + +/** @param {any} value */ +function describe(value) { + const keys = Object.keys(value); + const parts = []; + for (const key in value) parts.push(`${key}=${String(value[key])}`); + return `${keys.join(",")} | ${parts.join(" ")} | ${JSON.stringify(value)}`; +} + +/** @param {() => void} fn */ +function attempt(label, fn) { + try { + fn(); + console.log(label, "ok"); + } catch (e) { + console.log(label, e instanceof TypeError ? "TypeError" : "other", e.message); + } +} + +// The zod shape: freeze whatever value a result carries. Here it is always +// plain data, never a class instance. +/** @param {{ status: string, value: any }} data */ +function freezeResult(data) { + if (data.status === "valid") data.value = Object.freeze(data.value); + return data; +} + +const results = [ + freezeResult({ status: "valid", value: { a: 1, list: [1, 2] } }), + freezeResult({ status: "valid", value: [3, 4] }), + freezeResult({ status: "aborted", value: { b: 2 } }), + freezeResult({ status: "valid", value: "text" }), +]; +for (const result of results) console.log(result.status, Object.isFrozen(result.value), JSON.stringify(result.value)); +/** @param {() => void} fn */ +function throws(fn) { + try { + fn(); + return false; + } catch (e) { + return e instanceof TypeError; + } +} +console.log(throws(() => put(results[0].value, "a", 9)), throws(() => put(results[2].value, "b", 3))); +console.log(JSON.stringify(results[2].value)); + +// Class instances that only ever travel through untyped code. +const status = new Status(); +status.dirty(); +const p = new Point(1, -2); +const q = new Point3(3, 4, -5); +put(p, "tag", "p"); +put(q, "w", 6); +console.log(describe(status), status.value); +console.log(describe(p), p.norm()); +console.log(describe(q), q.norm()); +console.log(Object.isFrozen(p), Object.isExtensible(q), Object.isSealed(status)); + +// Statically typed freeze, seal and preventExtensions of instances. +const frozen = new Point(5, 6); +Object.freeze(frozen); +attempt("frozen field write", () => { + /** @type {Point} */ (frozen).x = 50; +}); +attempt("frozen field write untyped", () => put(frozen, "y", 60)); +attempt("frozen addition", () => put(frozen, "extra", 1)); +console.log(describe(frozen), Object.isFrozen(frozen), Object.isSealed(frozen), Object.isExtensible(frozen)); +console.log(JSON.stringify(Object.getOwnPropertyDescriptor(frozen, "x"))); + +const sealed = new Point3(1, 2, 3); +put(sealed, "before", true); +Object.seal(sealed); +attempt("sealed field write", () => { + sealed.z = 30; +}); +attempt("sealed added write", () => put(sealed, "before", false)); +attempt("sealed addition", () => put(sealed, "after", 1)); +console.log(describe(sealed), Object.isFrozen(sealed), Object.isSealed(sealed), Object.isExtensible(sealed)); +console.log(JSON.stringify(Object.getOwnPropertyDescriptor(sealed, "z"))); + +const closed = new Status(); +Object.preventExtensions(closed); +closed.dirty(); +attempt("closed addition", () => put(closed, "more", 1)); +console.log(describe(closed), Object.isFrozen(closed), Object.isExtensible(closed)); + +// A subclass instance frozen through its base type. +/** @type {Point} */ +const asBase = new Point3(7, 8, 9); +Object.freeze(asBase); +attempt("frozen subclass field", () => put(asBase, "z", 90)); +console.log(describe(asBase), asBase.norm(), Object.isFrozen(asBase)); + +// Fresh instances of the same classes stay writable. +const fresh = new Point3(0, 0, 0); +fresh.x = 1; +put(fresh, "y", 2); +put(fresh, "added", 3); +console.log(describe(fresh), fresh.norm(), Object.isFrozen(fresh)); diff --git a/tests/corpus/7791-js-class-accessor-layouts.js b/tests/corpus/7791-js-class-accessor-layouts.js new file mode 100644 index 000000000..6c87989cb --- /dev/null +++ b/tests/corpus/7791-js-class-accessor-layouts.js @@ -0,0 +1,85 @@ +"use strict"; +// Getters and setters on JavaScript classes. An accessor whose name no +// field uses lives on the prototype beside the fields; an accessor that +// shares a field's name decides what an assignment does. Both behave as in +// Node, for direct use and for instances passed to untyped code. + +class LazyPath { + 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; + } +} + +class Temperature { + constructor(celsius) { + this.celsius = celsius; + } + get fahrenheit() { + return this.celsius * 1.8 + 32; + } + set fahrenheit(value) { + this.celsius = (value - 32) / 1.8; + } +} + +class Labeled extends Temperature { + constructor(celsius, label) { + super(celsius); + this.label = label; + } + get description() { + return `${this.label}: ${this.celsius.toFixed(1)}C`; + } +} + +/** @param {any} target @param {string} key @param {unknown} value */ +function put(target, key, value) { + target[key] = value; +} + +/** @param {any} value */ +function describe(value) { + const parts = []; + for (const key in value) parts.push(key); + return `${Object.keys(value).join(",")} | ${parts.join(",")} | ${JSON.stringify(value)}`; +} + +/** @param {any} value @param {string} key */ +function probe(value, key) { + return `${key}: in=${key in value} own=${Object.prototype.hasOwnProperty.call(value, key)} value=${JSON.stringify(value[key])}`; +} + +const root = { common: true }; +const paths = []; +for (let i = 0; i < 3; i++) paths.push(new LazyPath(root, { n: i }, ["items"], i)); +paths.push(new LazyPath(root, null, ["a"], ["b", "c"])); +for (const item of paths) console.log(item.path.join("."), item.data && item.data.n, item.parent === root); +console.log(describe(paths[0])); +console.log(probe(paths[1], "path"), probe(paths[1], "_key")); +put(paths[2], "extra", 1); +console.log(describe(paths[2]), paths[2].path.length); + +const t = new Temperature(100); +console.log(t.fahrenheit, describe(t)); +t.fahrenheit = 32; +console.log(t.celsius, t.fahrenheit); +put(t, "fahrenheit", 212); +console.log(t.celsius, probe(t, "fahrenheit"), probe(t, "celsius")); +put(t, "note", "added"); +console.log(describe(t)); + +const l = new Labeled(20, "room"); +console.log(l.description, l.fahrenheit, describe(l)); +l.fahrenheit = 50; +console.log(l.description, probe(l, "description")); diff --git a/tests/corpus/957-builtins-namespace.ts b/tests/corpus/957-builtins-namespace.ts index 4b27bfe79..f6980cda6 100644 --- a/tests/corpus/957-builtins-namespace.ts +++ b/tests/corpus/957-builtins-namespace.ts @@ -17,7 +17,7 @@ function tail(p: string): string { return p.slice(i + 1); } -const scratch = "tmp-957-" + tail(process.argv[1]); +const scratch = "tmp-957-" + tail(process.argv[1]) + "-" + process.pid; if (fs.existsSync(scratch)) { fs.rmSync(scratch + "/a.txt"); fs.rmdirSync(scratch); diff --git a/tests/corpus/992-fs-roundtrip.ts b/tests/corpus/992-fs-roundtrip.ts index a039c936f..92b527fb1 100644 --- a/tests/corpus/992-fs-roundtrip.ts +++ b/tests/corpus/992-fs-roundtrip.ts @@ -32,7 +32,7 @@ function freshDir(dir: string): void { } mkdirSync(dir); } -const scratch = "tmp-992-" + tail(process.argv[1]); +const scratch = "tmp-992-" + tail(process.argv[1]) + "-" + process.pid; freshDir(scratch); const file = scratch + "/notes.txt"; diff --git a/tests/corpus/993-fs-readdir.ts b/tests/corpus/993-fs-readdir.ts index e948d3878..b0409f2ef 100644 --- a/tests/corpus/993-fs-readdir.ts +++ b/tests/corpus/993-fs-readdir.ts @@ -26,7 +26,7 @@ function freshDir(dir: string): void { } mkdirSync(dir); } -const scratch = "tmp-993-" + tail(process.argv[1]); +const scratch = "tmp-993-" + tail(process.argv[1]) + "-" + process.pid; freshDir(scratch); console.log(readdirSync(scratch).length); // fresh dir is empty diff --git a/tests/corpus/994-fs-errors.ts b/tests/corpus/994-fs-errors.ts index 74a1ed74e..d317c1aba 100644 --- a/tests/corpus/994-fs-errors.ts +++ b/tests/corpus/994-fs-errors.ts @@ -30,7 +30,7 @@ function freshDir(dir: string): void { } mkdirSync(dir); } -const scratch = "tmp-994-" + tail(process.argv[1]); +const scratch = "tmp-994-" + tail(process.argv[1]) + "-" + process.pid; freshDir(scratch); let caught = 0; diff --git a/tests/corpus/996-fs-rc-stress.ts b/tests/corpus/996-fs-rc-stress.ts index d6a8f0ba3..063845309 100644 --- a/tests/corpus/996-fs-rc-stress.ts +++ b/tests/corpus/996-fs-rc-stress.ts @@ -28,7 +28,7 @@ function freshDir(dir: string): void { } mkdirSync(dir); } -const scratch = "tmp-996-" + tail(process.argv[1]); +const scratch = "tmp-996-" + tail(process.argv[1]) + "-" + process.pid; freshDir(scratch); // Contents into an array, back through map/filter/join. diff --git a/tests/corpus/997-fs-modules/store.ts b/tests/corpus/997-fs-modules/store.ts index 65e59df1b..1b2a0f1fe 100644 --- a/tests/corpus/997-fs-modules/store.ts +++ b/tests/corpus/997-fs-modules/store.ts @@ -17,7 +17,7 @@ export function scratchDir(tag: string): string { while (i >= 0 && script.charAt(i) !== "/" && script.charAt(i) !== "\\") { i = i - 1; } - const dir = tag + script.slice(i + 1); + const dir = tag + script.slice(i + 1) + "-" + process.pid; if (existsSync(dir)) { for (const name of readdirSync(dir)) { rmSync(dir + "/" + name); diff --git a/tests/corpus/class-brand-probe-keys.ts b/tests/corpus/class-brand-probe-keys.ts new file mode 100644 index 000000000..77b5ff138 --- /dev/null +++ b/tests/corpus/class-brand-probe-keys.ts @@ -0,0 +1,68 @@ +// Untyped reads of class instances dispatch on the capsule's class key. +// These classes have keys of equal length, keys that share their last +// eight bytes, and short keys, so every stage of the brand probe decides. +class FooHandlerA { + kind = "foo-a"; + label(): string { + return `FooHandlerA:${this.kind}`; + } +} +class BarHandlerA { + kind = "bar-a"; + label(): string { + return `BarHandlerA:${this.kind}`; + } +} +class FooHandlerB extends FooHandlerA { + override kind = "foo-b"; + extra = 2; +} +class Q { + kind = "q"; + label(): string { + return "Q"; + } +} +class R { + kind = "r"; + label(): string { + return "R"; + } +} +class LongerClassNameWithSharedTail { + kind = "long"; + label(): string { + return "long"; + } +} +class OtherClassNameWithSharedTail { + kind = "other"; + label(): string { + return "other"; + } +} + +const values: any[] = [ + new FooHandlerA(), + new BarHandlerA(), + new FooHandlerB(), + new Q(), + new R(), + new LongerClassNameWithSharedTail(), + new OtherClassNameWithSharedTail(), + { kind: "plain", label: () => "plain" }, +]; + +for (const value of values) { + const extra = value.extra; + console.log(value.kind, value.label(), extra === undefined ? "-" : extra, value.constructor.name); +} + +// Writes through the untyped view reach the right instance. +for (const value of values) value.kind = `${value.kind}!`; +console.log(values.map((value) => value.kind).join(",")); + +// Computed reads go through the same tables. +const key: string = ["kind", "label"][values.length % 2]; +console.log(values.map((value) => typeof value[key]).join(",")); +console.log(values.filter((value) => value instanceof FooHandlerA).length); diff --git a/tests/corpus/fetch-headers-copy-cache.ts b/tests/corpus/fetch-headers-copy-cache.ts new file mode 100644 index 000000000..cbbc7fc6d --- /dev/null +++ b/tests/corpus/fetch-headers-copy-cache.ts @@ -0,0 +1,125 @@ +// Headers copies and the cached sort-and-combine list: copies are +// independent of their source, reads see every mutation, and live +// iterators re-read the current list after a change. +function dump(label: string, h: Headers): void { + const parts: string[] = []; + h.forEach((value, name) => parts.push(`${name}=${value}`)); + console.log(`${label}: ${parts.join(" | ")}`); +} + +// The corpus lib has no DOM iterable typings for Headers, so iterators +// are reached through an untyped view. +const iter = (h: Headers): any => h; + +const source = new Headers({ "X-B": " two ", "x-a": "one", Accept: "text/html" }); +source.append("accept", "application/json"); +source.append("Set-Cookie", "a=1"); +source.append("set-cookie", "b=2"); +dump("source", source); + +// A Headers copy keeps the combined values and stays separate. +const copy = new Headers(source); +source.set("x-a", "changed"); +copy.append("x-c", "three"); +dump("source after set", source); +dump("copy after append", copy); +console.log(JSON.stringify(copy.get("accept")), JSON.stringify(copy.get("set-cookie"))); +console.log(JSON.stringify(copy.getSetCookie())); + +// Copying into an empty Response and a Request, then appending a repeat. +const response = new Response("body", { headers: copy, status: 201 }); +response.headers.append("accept", "*/*"); +response.headers.append("cookie", "c=3"); +response.headers.append("Cookie", "d=4"); +dump("response", response.headers); +dump("copy unchanged", copy); +const clone = new Response(response.body, response); +dump("response clone", clone.headers); +const request = new Request("http://example.test/", { headers: copy }); +console.log("request x-b", JSON.stringify(request.headers.get("X-B"))); + +// Every read after a mutation sees it: get, has, iteration, spread. +const h = new Headers([["x-z", "z"], ["x-m", "m"]]); +console.log([...iter(h).entries()].map(([n, v]) => `${n}:${v}`).join(",")); +h.delete("x-z"); +console.log([...iter(h).keys()].join(","), h.has("x-z")); +h.delete("x-missing"); +console.log([...iter(h).values()].join(",")); +h.append("x-a", "first"); +console.log([...iter(h).entries()].map(([n, v]) => `${n}:${v}`).join(",")); +h.set("X-M", "again"); +console.log(JSON.stringify(h.get("x-m")), [...iter(h).keys()].join(",")); + +// A live iterator: mutations between steps change what comes next. +const it = iter(h).entries(); +console.log(JSON.stringify(it.next().value)); +h.append("x-0", "before"); +console.log(JSON.stringify(it.next().value)); +h.delete("x-m"); +h.append("x-zz", "after"); +console.log(JSON.stringify(it.next()), JSON.stringify(it.next())); + +// forEach that mutates as it walks. +const walk = new Headers([["b", "2"], ["d", "4"]]); +const seen: string[] = []; +walk.forEach((value, name) => { + seen.push(`${name}=${value}`); + if (name === "b") { + walk.append("c", "3"); + walk.append("a", "1"); + } + if (name === "c") walk.set("d", "four"); +}); +console.log(seen.join(" | ")); + +// Invalid input in a copy source is still rejected (plain object source). +try { + new Response("x", { headers: { "bad name": "v" } }); +} catch (error) { + console.log((error as Error).name, (error as Error).message); +} +try { + new Headers(source).append("x", "a\nb"); +} catch (error) { + console.log((error as Error).name, (error as Error).message); +} + +// Appending an empty value leaves the separator's space at the end of the +// combined value; a copy trims it again, as Node's does. A leading +// separator has no whitespace to trim. +const spaced = new Headers(); +spaced.append("a", "x"); +spaced.append("a", ""); +spaced.append("b", ""); +spaced.append("b", "y"); +spaced.append("c", ""); +spaced.append("c", ""); +spaced.append("cookie", "k=v"); +spaced.append("cookie", ""); +const spacedList = (headers: Headers): string => { + const out: string[] = []; + headers.forEach((value, name) => out.push(`${name}=${JSON.stringify(value)}`)); + return out.join(" "); +}; +console.log(spacedList(spaced)); +console.log(spacedList(new Headers(spaced))); +console.log(spacedList(new Headers(new Headers(spaced)))); +console.log(spacedList(new Response("", { headers: spaced }).headers)); +console.log(spacedList(new Request("http://example.test/", { headers: spaced }).headers)); + +// The same through duplicate constructor entries, in a Headers object and +// in a Response's own headers. +const entries: [string, string][] = [ + ["a", "x"], + ["a", ""], + ["c", ""], + ["c", ""], + ["d", "plain"], +]; +const built = new Headers(entries); +console.log(spacedList(built)); +console.log(spacedList(new Headers(built))); +const builtResponse = new Response("", { headers: entries }); +console.log(spacedList(builtResponse.headers)); +console.log(spacedList(new Headers(builtResponse.headers))); +console.log(spacedList(new Request("http://example.test/", { headers: built }).headers)); diff --git a/tests/corpus/split-cjs-require-cycle/even.cjs b/tests/corpus/split-cjs-require-cycle/even.cjs new file mode 100644 index 000000000..94d74ea87 --- /dev/null +++ b/tests/corpus/split-cjs-require-cycle/even.cjs @@ -0,0 +1,10 @@ +var odd; +exports.name = "even"; +exports.isEven = function (n) { + return n === 0 ? true : odd.isOdd(n - 1); +}; +exports.describe = function (n) { + return n + " is " + (exports.isEven(n) ? "even" : "odd") + " (asked " + odd.name + ")"; +}; +odd = require("./odd.cjs"); +console.log("even: loaded"); diff --git a/tests/corpus/split-cjs-require-cycle/main.cjs b/tests/corpus/split-cjs-require-cycle/main.cjs new file mode 100644 index 000000000..c4788ddbe --- /dev/null +++ b/tests/corpus/split-cjs-require-cycle/main.cjs @@ -0,0 +1,11 @@ +// Split require bindings (`var m; m = require("./x.cjs")`) that close a +// CommonJS cycle: each module publishes its exports before its require, and +// reads the other module's binding only in functions that run after both +// modules finish, the cycle shape the const form compiles too. +console.log("main: start"); +var even, odd; +even = require("./even.cjs"); +odd = require("./odd.cjs"); +console.log(even.isEven(10), odd.isOdd(7), even.isEven(3)); +console.log(even.describe(4), "|", even.describe(9)); +console.log("odd calls", odd.calls(), even.name, odd.name); diff --git a/tests/corpus/split-cjs-require-cycle/odd.cjs b/tests/corpus/split-cjs-require-cycle/odd.cjs new file mode 100644 index 000000000..c066a1e33 --- /dev/null +++ b/tests/corpus/split-cjs-require-cycle/odd.cjs @@ -0,0 +1,12 @@ +var even; +let calls = 0; +exports.name = "odd"; +exports.isOdd = function (n) { + calls++; + return n === 0 ? false : even.isEven(n - 1); +}; +exports.calls = function () { + return calls; +}; +even = require("./even.cjs"); +console.log("odd: loaded"); diff --git a/tests/corpus/split-cjs-require-cycle/package.json b/tests/corpus/split-cjs-require-cycle/package.json new file mode 100644 index 000000000..a3c15a7a6 --- /dev/null +++ b/tests/corpus/split-cjs-require-cycle/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/corpus/string-last-index-of-ascii.ts b/tests/corpus/string-last-index-of-ascii.ts new file mode 100644 index 000000000..8dcf405f5 --- /dev/null +++ b/tests/corpus/string-last-index-of-ascii.ts @@ -0,0 +1,21 @@ +// lastIndexOf on ASCII receivers: every position form, empty and long +// needles, needles with non-ASCII text, and receivers built at run time. +const ids = ["bk_00042", "mb_00410", "plain", "_lead", "trail_", "a_b_c_d", ""]; +for (const id of ids) console.log(JSON.stringify(id), id.lastIndexOf("_"), Number(id.slice(id.lastIndexOf("_") + 1))); + +const text = "abcabcabc"; +const positions = [Infinity, -Infinity, NaN, -0, 0, 1, 2.9, 3, 5, 8, 9, 100, -5]; +for (const needle of ["abc", "c", "a", "", "abcabcabcabc", "cab", "x", "é", "\ud83d", "😀"]) { + const row = positions.map((position) => text.lastIndexOf(needle, position)); + console.log(JSON.stringify(needle), text.lastIndexOf(needle), row.join(",")); +} + +// Receivers built at run time (concatenation, repeat, join, slices). +const built = ["x", "y"].join("-").repeat(3) + String(42); +console.log(built, built.lastIndexOf("-"), built.lastIndexOf("x-", 3), built.lastIndexOf("42", 6)); +const slice = built.slice(2, 9); +console.log(slice, slice.lastIndexOf("y"), slice.lastIndexOf("y", 0), slice.lastIndexOf("-y-")); + +// A non-ASCII receiver keeps UTF-16 positions. +const mixed = "é_ü_😀_z"; +console.log(mixed.lastIndexOf("_"), mixed.lastIndexOf("_", 3), mixed.lastIndexOf("😀"), mixed.lastIndexOf("\ude00")); 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/diagnostics/split-cjs-require-cycle/b.js b/tests/diagnostics/split-cjs-require-cycle/b.js new file mode 100644 index 000000000..ad5a7441a --- /dev/null +++ b/tests/diagnostics/split-cjs-require-cycle/b.js @@ -0,0 +1,7 @@ +var a; +a = require('./main.js'); +exports.fromB = 'b-value'; +exports.readA = function () { + return a.early + '/' + a.late; +}; +console.log('b sees a.early:', a.early, 'a.late:', a.late); diff --git a/tests/diagnostics/split-cjs-require-cycle/main.js b/tests/diagnostics/split-cjs-require-cycle/main.js new file mode 100644 index 000000000..286de459a --- /dev/null +++ b/tests/diagnostics/split-cjs-require-cycle/main.js @@ -0,0 +1,11 @@ +// A split require binding (`var b; b = require("./b.js")`) whose require +// closes a CommonJS cycle that top-level code can observe: b.js reads a.js's +// partial exports while a.js is still evaluating, which the compiled export +// table does not model. Preflight refuses it with SC1016, as it refuses the +// `const b = require("./b.js")` form; tests/corpus/split-cjs-require-cycle +// compiles the cycle shape both forms admit. +exports.early = 'a-early'; +var b; +b = require('./b.js'); +exports.late = 'a-late'; +console.log('a sees', b.fromB, b.readA()); diff --git a/tests/diagnostics/split-cjs-require-cycle/package.json b/tests/diagnostics/split-cjs-require-cycle/package.json new file mode 100644 index 000000000..a3c15a7a6 --- /dev/null +++ b/tests/diagnostics/split-cjs-require-cycle/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/diagnostics/split-cjs-require-early/lib.js b/tests/diagnostics/split-cjs-require-early/lib.js new file mode 100644 index 000000000..2a9669e12 --- /dev/null +++ b/tests/diagnostics/split-cjs-require-early/lib.js @@ -0,0 +1,8 @@ +'use strict'; + +/** @param {string} who */ +function greet(who) { + return `hello ${who}`; +} + +module.exports = { greet }; diff --git a/tests/diagnostics/split-cjs-require-early/main.js b/tests/diagnostics/split-cjs-require-early/main.js new file mode 100644 index 000000000..3e0e43aa3 --- /dev/null +++ b/tests/diagnostics/split-cjs-require-early/main.js @@ -0,0 +1,16 @@ +// A split require binding (`let lib;` then `lib = require(...)`) that code +// above the assignment can reach: Node reads undefined there, a value the +// compiled module alias cannot hold, so the assignment keeps a named SC1013 +// fence. Assigned before any such code, the binding compiles +// (tests/corpus/split-cjs-require-binding). +'use strict'; + +let lib; +console.log('boot', helper()); +lib = require('./lib.js'); + +function helper() { + return lib === undefined ? 'not loaded' : lib.greet('late'); +} + +console.log(helper()); diff --git a/tests/diagnostics/split-cjs-require-early/package.json b/tests/diagnostics/split-cjs-require-early/package.json new file mode 100644 index 000000000..a3c15a7a6 --- /dev/null +++ b/tests/diagnostics/split-cjs-require-early/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/diagnostics/stream-runtime-option-order.ts b/tests/diagnostics/stream-runtime-option-order.ts new file mode 100644 index 000000000..357907d5d --- /dev/null +++ b/tests/diagnostics/stream-runtime-option-order.ts @@ -0,0 +1,42 @@ +import { Writable } from "node:stream"; + +// Runtime decodeStrings and defaultEncoding values are read before the +// other option members are evaluated, while Node evaluates the members in +// order: a value after a member with effects refuses, and so does a value +// with effects after a member that reads a property those effects change. +let encoding = (process.env["STREAM_ENCODING"] ?? "utf8") as BufferEncoding; +let decode = process.env["STREAM_DECODE"] !== "0"; +function bump(): number { + encoding = "hex"; + decode = false; + return 16; +} +const sink = new Writable({ + highWaterMark: bump(), + defaultEncoding: encoding, + write(_chunk, _encoding, done) { + done(); + }, +}); +const other = new Writable({ + highWaterMark: bump(), + decodeStrings: decode, + write(_chunk, _encoding, done) { + done(); + }, +}); +const sizes = { hwm: 16 }; +function pickEncoding(): BufferEncoding { + sizes.hwm = 1; + return "hex"; +} +const third = new Writable({ + highWaterMark: sizes.hwm, + defaultEncoding: pickEncoding(), + write(_chunk, _encoding, done) { + done(); + }, +}); +sink.end(); +other.end(); +third.end(); diff --git a/tests/fixtures/node-types/fenced.ts b/tests/fixtures/node-types/fenced.ts index 27e68a618..7c468de01 100644 --- a/tests/fixtures/node-types/fenced.ts +++ b/tests/fixtures/node-types/fenced.ts @@ -74,7 +74,7 @@ http2.connect("https://localhost"); * 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 }); +createCipheriv("aes-128-ocb", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); secureHeapUsed(); setFips(false); fetch("https://example.invalid/", { diff --git a/tests/fixtures/npm/cases/7546-minified-esm-rules/main.ts b/tests/fixtures/npm/cases/7546-minified-esm-rules/main.ts new file mode 100644 index 000000000..0c94acb0d --- /dev/null +++ b/tests/fixtures/npm/cases/7546-minified-esm-rules/main.ts @@ -0,0 +1,22 @@ +// A one-line minified ES module. Its module-level var statement spreads +// earlier declarators into later ones, its tokenizer reads regexes through +// an untyped rules table (search, match and replace with callbacks), and +// one function receives both typed and untyped link objects. +import { render } from "gtminimark"; + +const doc = [ + "# Title \\*one\\*", + "## Sub", + '[a](/x "T\\*")', + "[b](/y)", + '[r]: /ref "Ref"', + "[n]: /none", + "[R]", + "[n]", + "[z]", + "\tcode *em* line", + " three spaces *x* and *y*", + "plain words here", +].join("\n"); +console.log(render(doc)); +console.log(render(doc, { pedantic: true })); diff --git a/tests/fixtures/npm/cases/7547-minified-cjs-chunks/main.ts b/tests/fixtures/npm/cases/7547-minified-cjs-chunks/main.ts new file mode 100644 index 000000000..ad2303f47 --- /dev/null +++ b/tests/fixtures/npm/cases/7547-minified-cjs-chunks/main.ts @@ -0,0 +1,12 @@ +// An identifier-minified esbuild CommonJS bundle (lru-cache's shape): its +// __commonJS helper and chunk parameters lost their names, the chunks +// stamp __esModule with { value: !0 }, and they call node:util and +// node:diagnostics_channel functions in the (0, ns.member)(...) form. +import { Cache } from "gtminicache"; + +const cache = new Cache(2); +cache.set("a", 1).set("b", 2); +cache.get("a"); +cache.set("c", 3); +console.log(cache.keys().join(","), cache.get("b"), cache.get("c")); +console.log(cache.describe("cache")); 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/hono-node-server/main.ts b/tests/fixtures/npm/cases/hono-node-server/main.ts new file mode 100644 index 000000000..3cca4aa44 --- /dev/null +++ b/tests/fixtures/npm/cases/hono-node-server/main.ts @@ -0,0 +1,387 @@ +// @hono/node-server 2.x (compiled from scriptc's built-in implementation) +// serving a fetch handler: a raw HTTP client prints each exchange's status +// line, wire-order headers (Date dropped) and body, covering buffered and +// streamed bodies, header forms, cookies, request bodies, URL handling, +// HEAD, empty statuses, handler errors, a client that disconnects during +// a streamed body, and request bodies the handler reads, leaves unread or +// rejects while the upload is still arriving. +import * as http from "node:http"; +import * as net from "node:net"; +import type { AddressInfo } from "node:net"; +import { getRequestListener, RequestError, serve } from "@hono/node-server"; + +function delay(ms: number): Promise { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + +function slowStream(parts: string[]): ReadableStream { + const encoder = new TextEncoder(); + let index = 0; + return new ReadableStream({ + async pull(controller) { + await delay(5); + if (index < parts.length) controller.enqueue(encoder.encode(parts[index++]!)); + else controller.close(); + }, + }); +} + +function readyStream(parts: string[]): ReadableStream { + const encoder = new TextEncoder(); + return new ReadableStream({ + start(controller) { + for (const part of parts) controller.enqueue(encoder.encode(part)); + controller.close(); + }, + }); +} + +// A stream that produces until it is cancelled (bounded, so a missed cancel +// ends the program instead of hanging it). +let reportCancel: (reason: string) => void = () => {}; +const endlessCancelled = new Promise((resolve) => { + reportCancel = resolve; +}); +function endlessStream(): ReadableStream { + const encoder = new TextEncoder(); + let pulls = 0; + return new ReadableStream({ + async pull(controller) { + await delay(5); + if (++pulls > 400) controller.close(); + else controller.enqueue(encoder.encode("tick\n")); + }, + cancel(reason) { + reportCancel(`cancelled with ${String(reason)}`); + }, + }); +} + +async function handler(request: Request): Promise { + const url = new URL(request.url); + switch (url.pathname) { + case "/echo": { + const body = await request.text(); + const headers = new Headers({ "content-type": "application/json" }); + headers.set("x-echo", "yes"); + return new Response( + JSON.stringify({ + method: request.method, + url: request.url.replace(/:\d+\//, ":PORT/"), + type: request.headers.get("content-type"), + agent: request.headers.get("x-agent"), + body, + }), + { status: 201, headers }, + ); + } + case "/headers-object": { + const headers = new Headers(); + headers.set("x-b", "2"); + headers.set("X-A", "1"); + headers.set("content-type", "text/html; charset=utf-8"); + return new Response("

hi

", { status: 202, headers }); + } + case "/cookies": { + const headers = new Headers(); + headers.append("set-cookie", "a=1; Path=/"); + headers.set("x-before", "b"); + headers.append("set-cookie", "b=2; HttpOnly"); + headers.set("content-type", "text/plain"); + return new Response("cookies", { headers }); + } + case "/no-content": + return new Response(null, { status: 204, headers: new Headers({ "x-empty": "1" }) }); + case "/declared-length": { + const headers = new Headers({ "content-length": "5", "content-type": "text/plain" }); + return new Response("12345", { headers }); + } + case "/ready-stream": { + const headers = new Headers({ "content-type": "text/plain" }); + return new Response(readyStream(["one ", "two"]), { headers }); + } + case "/slow-stream": { + const headers = new Headers({ "content-type": "application/x-ndjson" }); + return new Response(slowStream(['{"n":1}\n', '{"n":2}\n', '{"n":3}\n']), { headers }); + } + case "/chunked-stream": { + const headers = new Headers({ "content-type": "text/event-stream", "transfer-encoding": "chunked" }); + return new Response(readyStream(["data: a\n\n", "data: b\n\n"]), { headers }); + } + case "/bytes": { + const headers = new Headers({ "content-type": "application/octet-stream" }); + return new Response(new Uint8Array([104, 105, 33]), { headers }); + } + case "/null-body": { + return new Response(null, { status: 404, headers: new Headers({ "x-missing": "yes" }) }); + } + case "/reject": + throw new Error("handler failed"); + case "/light-text": + return new Response("light"); + case "/light-record": + return new Response("rec", { status: 201, headers: { "X-Custom": "a", "Content-Type": "text/html" } }); + case "/light-json": + return Response.json({ ok: true, n: [1, 2] }); + case "/light-json-headers": + return Response.json({ ok: true }, { status: 201, headers: { "X-Test": "yes" } }); + case "/light-json-typed": + return Response.json([1], { headers: { "Content-Type": "application/vnd.test+json" } }); + case "/endless": + return new Response(endlessStream(), { headers: new Headers({ "content-type": "text/plain" }) }); + case "/light-touched": { + const r = new Response("touched"); + r.headers.set("x-touched", "1"); + return r; + } + case "/light-record-touched": { + const r = new Response("rt", { headers: { "X-Rec": "1" } }); + r.headers.append("x-more", "2"); + return r; + } + case "/materialized": { + const r = new Response("m", { headers: { "X-M": "1" } }); + return new Response(r.body, r); + } + case "/light-stream": + return new Response(readyStream(["s1 ", "s2"])); + case "/light-null": + return new Response(null, { status: 202 }); + case "/light-length": + return new Response("abc", { headers: { "Content-Length": "3" } }); + case "/light-bytes": + return new Response(new Uint8Array([111, 107])); + default: { + const headers = new Headers({ "content-type": "text/plain; charset=UTF-8" }); + return new Response(`path ${url.pathname}${url.search}`, { headers }); + } + } +} + +interface Exchange { + method: string; + path: string; + headers?: Record; + body?: string; +} + +function exchange(port: number, ex: Exchange): Promise { + return new Promise((resolve, reject) => { + const headers: Record = { ...(ex.headers ?? {}) }; + if (ex.body !== undefined) headers["content-length"] = String(Buffer.byteLength(ex.body)); + const req = http.request( + { host: "127.0.0.1", port, method: ex.method, path: ex.path, headers: headers }, + (res) => { + const chunks: Buffer[] = []; + res.on("data", (chunk: Buffer) => { + chunks.push(chunk); + }); + res.on("end", () => { + const lines: string[] = [`HTTP/${res.httpVersion} ${res.statusCode} ${res.statusMessage}`]; + for (let i = 0; i < res.rawHeaders.length; i += 2) { + const name = res.rawHeaders[i]!; + if (name.toLowerCase() === "date") continue; + lines.push(`${name}: ${res.rawHeaders[i + 1]}`); + } + resolve(`### ${ex.method} ${ex.path}\n${lines.join("\n")}\n\n${Buffer.concat(chunks).toString("utf8")}\n`); + }); + }, + ); + req.on("error", reject); + if (ex.body !== undefined) req.write(ex.body); + req.end(); + }); +} + +const exchanges: Exchange[] = [ + { method: "GET", path: "/plain?q=1" }, + { method: "POST", path: "/echo", headers: { "content-type": "text/plain", "x-agent": "probe" }, body: "payload body" }, + { method: "PUT", path: "/echo", headers: { "content-type": "application/json" }, body: '{"k":[1,2]}' }, + { method: "GET", path: "/headers-object" }, + { method: "GET", path: "/cookies" }, + { method: "GET", path: "/no-content" }, + { method: "GET", path: "/declared-length" }, + { method: "GET", path: "/ready-stream" }, + { method: "GET", path: "/slow-stream" }, + { method: "GET", path: "/chunked-stream" }, + { method: "GET", path: "/bytes" }, + { method: "GET", path: "/null-body" }, + { method: "HEAD", path: "/plain" }, + { method: "GET", path: "/light-text" }, + { method: "HEAD", path: "/light-text" }, + { method: "GET", path: "/light-record" }, + { method: "HEAD", path: "/light-record" }, + { method: "GET", path: "/light-json" }, + { method: "GET", path: "/light-json-headers" }, + { method: "GET", path: "/light-json-typed" }, + { method: "GET", path: "/light-touched" }, + { method: "GET", path: "/light-record-touched" }, + { method: "GET", path: "/materialized" }, + { method: "GET", path: "/light-stream" }, + { method: "GET", path: "/light-null" }, + { method: "GET", path: "/light-length" }, + { method: "GET", path: "/light-bytes" }, + { method: "GET", path: "/reject" }, + { method: "GET", path: "/a/../b/./c?x=%20y" }, + { method: "GET", path: "/upper", headers: { host: "Example.COM:8080" } }, + { method: "GET", path: "/bad-host", headers: { host: "bad host" } }, + { method: "get", path: "/lower-method" }, +]; + +/** Reads the first chunk of /endless, then destroys the response: the + * server cancels the response's stream. */ +function disconnectMidStream(port: number): Promise { + return new Promise((resolve) => { + const req = http.request({ host: "127.0.0.1", port, method: "GET", path: "/endless" }, (res) => { + res.once("data", () => { + res.destroy(); + }); + }); + req.end(); + const timeout = new Promise((done) => { + setTimeout(() => done("not cancelled"), 3000); + }); + void Promise.race([endlessCancelled, timeout]).then((outcome) => { + resolve(`### disconnect GET /endless\n${outcome}\n`); + }); + }); +} + +const server = serve({ fetch: handler, port: 0, hostname: "127.0.0.1" }, (info: AddressInfo) => { + console.log("listening", info.family, info.address, typeof info.port); + void (async () => { + for (const ex of exchanges) console.log(await exchange(info.port, ex)); + console.log(await disconnectMidStream(info.port)); + server.close(() => { + console.log("closed"); + startSecond(); + }); + })(); +}); + +// getRequestListener with an error handler, on a plain node:http server. +const listener = getRequestListener( + async (request) => { + if (request.url.endsWith("/fail")) throw new RequestError("listener failure"); + return new Response("ok", { headers: new Headers({ "content-type": "text/plain" }) }); + }, + { + errorHandler: (err) => + new Response(`handled: ${err instanceof Error ? `${err.name} ${err.message}` : "?"}`, { + status: 503, + headers: new Headers({ "content-type": "text/plain" }), + }), + }, +); +function startSecond(): void { + const second = http.createServer((req, res) => { + void listener(req, res); + }); + second.listen(0, "127.0.0.1", () => { + const port = (second.address() as AddressInfo).port; + void (async () => { + console.log(await exchange(port, { method: "GET", path: "/ok" })); + console.log(await exchange(port, { method: "GET", path: "/fail" })); + second.close(() => { + console.log("second closed"); + startUploads(); + }); + })(); + }); +} + +// Request bodies stream to the handler. /size reads the whole body. The +// other routes leave it unread: /unread answers at once (the package's +// cached-response path) and /unread-async through a promise, and /reject +// answers 413 after 300 ms while the client keeps uploading, so the server +// must stop reading the socket. +const MiB = 1024 * 1024; + +interface UploadResult { + status: string; + body: string; + sentBeforeResponse: number; +} + +/** A chunked POST over a raw socket: 64 KiB chunks, each written after + * the previous write's callback, until `limit` bytes are out (then the + * last chunk, when `finish` is set) or the response arrives. */ +function chunkedUpload(port: number, path: string, limit: number, finish: boolean): Promise { + return new Promise((resolve) => { + const socket = net.connect(port, "127.0.0.1"); + const frame = Buffer.concat([Buffer.from("10000\r\n"), Buffer.alloc(65536, 97), Buffer.from("\r\n")]); + let sent = 0; + let sentBeforeResponse = -1; + let response = ""; + let settled = false; + const settle = (): void => { + if (settled) return; + settled = true; + const headEnd = response.indexOf("\r\n\r\n"); + const status = response.slice(0, response.indexOf("\r\n")); + resolve({ status, body: headEnd < 0 ? "" : response.slice(headEnd + 4), sentBeforeResponse }); + socket.destroy(); + }; + const writeNext = (): void => { + if (sentBeforeResponse >= 0 || socket.destroyed) return; + if (sent >= limit) { + if (finish) socket.write("0\r\n\r\n"); + return; + } + sent += 65536; + socket.write(frame, () => { + writeNext(); + }); + }; + socket.on("connect", () => { + socket.write(`POST ${path} HTTP/1.1\r\nHost: 127.0.0.1\r\nTransfer-Encoding: chunked\r\n\r\n`); + writeNext(); + }); + socket.on("data", (chunk: Buffer) => { + response += chunk.toString("latin1"); + if (sentBeforeResponse < 0 && response.includes("\r\n\r\n")) sentBeforeResponse = sent; + const match = /content-length: (\d+)/i.exec(response); + const headEnd = response.indexOf("\r\n\r\n"); + if (match !== null && headEnd >= 0 && response.length - headEnd - 4 >= Number(match[1])) settle(); + }); + socket.on("error", () => settle()); + socket.on("close", () => settle()); + }); +} + +function startUploads(): void { + const server = serve( + { + fetch: (request: Request): Response | Promise => { + const url = new URL(request.url); + if (url.pathname === "/size") + return request.arrayBuffer().then((body) => new Response(`size ${body.byteLength}`)); + if (url.pathname === "/reject") + return delay(300).then(() => new Response("too large", { status: 413 })); + if (url.pathname === "/unread-async") return Promise.resolve(new Response("unread async")); + return new Response(`unread ${url.pathname}`); + }, + port: 0, + hostname: "127.0.0.1", + }, + (info: AddressInfo) => { + void (async () => { + const read = await chunkedUpload(info.port, "/size", 3 * MiB, true); + console.log(`### chunked POST /size\n${read.status}\n${read.body}\n`); + console.log(await exchange(info.port, { method: "POST", path: "/unread", body: "x".repeat(300000) })); + console.log(await exchange(info.port, { method: "GET", path: "/after-unread" })); + console.log(await exchange(info.port, { method: "POST", path: "/unread-async", body: "y".repeat(300000) })); + console.log(await exchange(info.port, { method: "GET", path: "/after-unread-async" })); + const before = process.memoryUsage().rss; + const rejected = await chunkedUpload(info.port, "/reject", 512 * MiB, false); + const grown = process.memoryUsage().rss - before; + console.log(`### chunked POST /reject\n${rejected.status}\n${rejected.body}`); + console.log("client stopped before 32 MiB:", rejected.sentBeforeResponse >= 0 && rejected.sentBeforeResponse < 32 * MiB); + console.log("server memory grew under 64 MiB:", grown < 64 * MiB); + server.close(() => console.log("uploads closed")); + })(); + }, + ); +} diff --git a/tests/fixtures/npm/cases/hono-node-server/package.json b/tests/fixtures/npm/cases/hono-node-server/package.json new file mode 100644 index 000000000..5ffd9800b --- /dev/null +++ b/tests/fixtures/npm/cases/hono-node-server/package.json @@ -0,0 +1 @@ +{ "type": "module" } diff --git a/tests/fixtures/npm/cases/types-boundary-prototype/main.ts b/tests/fixtures/npm/cases/types-boundary-prototype/main.ts new file mode 100644 index 000000000..6645535e4 --- /dev/null +++ b/tests/fixtures/npm/cases/types-boundary-prototype/main.ts @@ -0,0 +1,13 @@ +// A user interface extending a package interface types values the +// package's constructors create with a prototype (effect's Request.tagged +// pattern): they stay JavaScript values, so the package still finds its +// prototype members when the value goes back to it. +import { run, tagged, type Req } from "reqtag"; + +interface Double extends Req { + readonly _tag: "Double"; + readonly value: number; +} +const Double = tagged("Double"); +const request = Double({ value: 21 }); +console.log(run(request), request.value * 2, request._tag); diff --git a/tests/fixtures/npm/cases/types-boundary-prototype/node_modules/reqtag/index.d.ts b/tests/fixtures/npm/cases/types-boundary-prototype/node_modules/reqtag/index.d.ts new file mode 100644 index 000000000..731b6a07f --- /dev/null +++ b/tests/fixtures/npm/cases/types-boundary-prototype/node_modules/reqtag/index.d.ts @@ -0,0 +1,8 @@ +export declare const ReqTypeId: unique symbol; +export interface Req { + readonly [ReqTypeId]: { readonly _A: A }; +} +export declare function tagged & { readonly _tag: string }>( + tag: R["_tag"], +): (args: Omit) => R; +export declare function run(request: Req): string; diff --git a/tests/fixtures/npm/cases/types-boundary-prototype/node_modules/reqtag/index.js b/tests/fixtures/npm/cases/types-boundary-prototype/node_modules/reqtag/index.js new file mode 100644 index 000000000..196bf555e --- /dev/null +++ b/tests/fixtures/npm/cases/types-boundary-prototype/node_modules/reqtag/index.js @@ -0,0 +1,15 @@ +// Requests share a prototype that carries a symbol-keyed marker and a +// method, as effect's Request.tagged constructors do. +export const ReqTypeId = Symbol.for("reqtag/Req"); +const proto = { + [ReqTypeId]: true, + describe() { + return `${this._tag}(${this.value})`; + }, +}; +export function tagged(tag) { + return (args) => Object.assign(Object.create(proto), args, { _tag: tag }); +} +export function run(request) { + return request[ReqTypeId] === true ? request.describe() : "not a request"; +} diff --git a/tests/fixtures/npm/cases/types-boundary-prototype/node_modules/reqtag/package.json b/tests/fixtures/npm/cases/types-boundary-prototype/node_modules/reqtag/package.json new file mode 100644 index 000000000..e461981ee --- /dev/null +++ b/tests/fixtures/npm/cases/types-boundary-prototype/node_modules/reqtag/package.json @@ -0,0 +1 @@ +{ "name": "reqtag", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/fixtures/npm/cases/types-boundary-prototype/package.json b/tests/fixtures/npm/cases/types-boundary-prototype/package.json new file mode 100644 index 000000000..ac2988811 --- /dev/null +++ b/tests/fixtures/npm/cases/types-boundary-prototype/package.json @@ -0,0 +1 @@ +{ "name": "types-boundary-prototype-case", "private": true, "type": "module" } 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/node_modules/@hono/node-server/LICENSE b/tests/fixtures/npm/node_modules/@hono/node-server/LICENSE new file mode 100644 index 000000000..5f84afaff --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 - present, Yusuke Wada and Hono contributors + +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/node_modules/@hono/node-server/README.md b/tests/fixtures/npm/node_modules/@hono/node-server/README.md new file mode 100644 index 000000000..25af664e8 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/README.md @@ -0,0 +1,450 @@ +# Node.js Adapter for Hono + +This adapter `@hono/node-server` allows you to run your Hono application on Node.js. +Initially, Hono wasn't designed for Node.js, but with this adapter, you can now use Hono on Node.js. It utilizes web standard APIs implemented in Node.js. + +## Benchmarks + +Hono is 3.1 times faster than Express. + +Express v5: + +```txt +$ bombardier -d 10s --fasthttp http://localhost:3000/ + +Statistics Avg Stdev Max + Reqs/sec 24380.41 1409.74 26604.88 + Latency 5.12ms 2.95ms 305.21ms + HTTP codes: + 1xx - 0, 2xx - 243915, 3xx - 0, 4xx - 0, 5xx - 0 + others - 0 + Throughput: 6.98MB/s +``` + +Hono + `@hono/node-server`: + +```txt +$ bombardier -d 10s --fasthttp http://localhost:3000/ + +Statistics Avg Stdev Max + Reqs/sec 74428.96 3815.27 88228.32 + Latency 1.68ms 621.25us 106.20ms + HTTP codes: + 1xx - 0, 2xx - 744378, 3xx - 0, 4xx - 0, 5xx - 0 + others - 0 + Throughput: 16.82MB/s +``` + +## Requirements + +It works on Node.js versions greater than 20.x. + +## Installation + +You can install it from the npm registry: + +```sh +npm install @hono/node-server +pnpm add @hono/node-server +``` + +## Usage + +Just import `@hono/node-server` at the top and write the code as usual. +The same code that runs on Cloudflare Workers, Deno, and Bun will work. + +```ts +import { serve } from '@hono/node-server' +import { Hono } from 'hono' + +const app = new Hono() +app.get('/', (c) => c.text('Hono meets Node.js')) + +serve(app, (info) => { + console.log(`Listening on http://localhost:${info.port}`) // Listening on http://localhost:3000 +}) +``` + +## WebSocket + +You can upgrade WebSocket connections with `upgradeWebSocket` from `@hono/node-server`. +To enable this, install `ws` (and `@types/ws`) in your project, then create and provide a `WebSocketServer` as shown in the example below. + +```ts +import { serve, upgradeWebSocket } from '@hono/node-server' +import { WebSocketServer } from 'ws' +import { Hono } from 'hono' + +const app = new Hono() + +app.get( + '/ws', + upgradeWebSocket(() => ({ + onMessage(event, ws) { + ws.send(event.data) + }, + })) +) + +const wss = new WebSocketServer({ noServer: true }) +serve({ + fetch: app.fetch, + websocket: { server: wss }, +}) +``` + +For example, run it using `ts-node`. Then an HTTP server will be launched. The default port is `3000`. + +```sh +ts-node ./index.ts +``` + +Open `http://localhost:3000` with your browser. + +## Options + +### `port` + +```ts +serve({ + fetch: app.fetch, + port: 8787, // Port number, default is 3000 +}) +``` + +### `createServer` + +```ts +import { createServer } from 'node:https' +import fs from 'node:fs' + +//... + +serve({ + fetch: app.fetch, + createServer: createServer, + serverOptions: { + key: fs.readFileSync('test/fixtures/keys/agent1-key.pem'), + cert: fs.readFileSync('test/fixtures/keys/agent1-cert.pem'), + }, +}) +``` + +### `overrideGlobalObjects` + +The default value is `true`. The Node.js Adapter rewrites the global Request/Response and uses a lightweight Request/Response to improve performance. If you don't want to do that, set `false`. + +```ts +serve({ + fetch: app.fetch, + overrideGlobalObjects: false, +}) +``` + +### `autoCleanupIncoming` + +The default value is `true`. The Node.js Adapter automatically cleans up (explicitly call `destroy()` method) if application is not finished to consume the incoming request. If you don't want to do that, set `false`. + +If the application accepts connections from arbitrary clients, this cleanup must be done otherwise incomplete requests from clients may cause the application to stop responding. If your application only accepts connections from trusted clients, such as in a reverse proxy environment and there is no process that returns a response without reading the body of the POST request all the way through, you can improve performance by setting it to `false`. + +```ts +serve({ + fetch: app.fetch, + autoCleanupIncoming: false, +}) +``` + +### `websocket` + +provide a websocket server to enable websocket support. + +```ts +import { serve, upgradeWebSocket } from '@hono/node-server' +import { WebSocketServer } from 'ws' + +// ... +const wss = new WebSocketServer({ noServer: true }) + +serve({ + fetch: app.fetch, + websocket: { server: wss }, +}) +``` + +## Middleware + +Most built-in middleware also works with Node.js. +Read [the documentation](https://hono.dev/middleware/builtin/basic-auth) and use the Middleware of your liking. + +```ts +import { serve } from '@hono/node-server' +import { Hono } from 'hono' +import { prettyJSON } from 'hono/pretty-json' + +const app = new Hono() + +app.get('*', prettyJSON()) +app.get('/', (c) => c.json({ 'Hono meets': 'Node.js' })) + +serve(app) +``` + +## Serve Static Middleware + +Use Serve Static Middleware that has been created for Node.js. + +```ts +import { serveStatic } from '@hono/node-server/serve-static' + +//... + +app.use('/static/*', serveStatic({ root: './' })) +``` + +If using a relative path, `root` will be relative to the current working directory from which the app was started. + +This can cause confusion when running your application locally. + +Imagine your project structure is: + +``` +my-hono-project/ + src/ + index.ts + static/ + index.html +``` + +Typically, you would run your app from the project's root directory (`my-hono-project`), +so you would need the following code to serve the `static` folder: + +```ts +app.use('/static/*', serveStatic({ root: './static' })) +``` + +Notice that `root` here is not relative to `src/index.ts`, rather to `my-hono-project`. + +Only `GET` and `HEAD` requests are served. Requests with other methods are passed to the next middleware. + +### Options + +#### `allowPercentInPath` + +By default, `serveStatic` skips request paths that still contain `%` after routing. This prevents a second decoding step from serving a file under a different path than the one checked by route middleware. Set `allowPercentInPath: true` to keep the previous behavior when serving files whose names contain percent signs. + +If you restrict access to only some paths under `root` (for example, `/static/admin/*`), enabling this option may let requests bypass those restrictions. Apply access control to the same route pattern as `serveStatic` (for example, `/static/*`), before the static middleware: + +```ts +app.use('/static/*', authMiddleware) +app.use('/static/*', serveStatic({ root: './static', allowPercentInPath: true })) +``` + +#### `rewriteRequestPath` + +If you want to serve files in `./.foojs` with the request path `/__foo/*`, you can write like the following. + +```ts +app.use( + '/__foo/*', + serveStatic({ + root: './.foojs/', + rewriteRequestPath: (path: string) => path.replace(/^\/__foo/, ''), + }) +) +``` + +#### `onFound` + +You can specify handling when the requested file is found with `onFound`. + +```ts +app.use( + '/static/*', + serveStatic({ + // ... + onFound: (_path, c) => { + c.header('Cache-Control', `public, immutable, max-age=31536000`) + }, + }) +) +``` + +#### `onNotFound` + +The `onNotFound` is useful for debugging. You can write a handle for when a file is not found. + +```ts +app.use( + '/static/*', + serveStatic({ + root: './non-existent-dir', + onNotFound: (path, c) => { + console.log(`${path} is not found, request to ${c.req.path}`) + }, + }) +) +``` + +#### `precompressed` + +The `precompressed` option checks if files with extensions like `.br` or `.gz` are available and serves them based on the `Accept-Encoding` header. It prioritizes Brotli, then Zstd, and Gzip. If none are available, it serves the original file. + +```ts +app.use( + '/static/*', + serveStatic({ + precompressed: true, + }) +) +``` + +## ConnInfo Helper + +You can use the [ConnInfo Helper](https://hono.dev/docs/helpers/conninfo) by importing `getConnInfo` from `@hono/node-server/conninfo`. + +```ts +import { getConnInfo } from '@hono/node-server/conninfo' + +app.get('/', (c) => { + const info = getConnInfo(c) // info is `ConnInfo` + return c.text(`Your remote address is ${info.remote.address}`) +}) +``` + +## Accessing Node.js API + +You can access the Node.js API from `c.env` in Node.js. For example, if you want to specify a type, you can write the following. + +```ts +import { serve } from '@hono/node-server' +import type { HttpBindings } from '@hono/node-server' +import { Hono } from 'hono' + +const app = new Hono<{ Bindings: HttpBindings }>() + +app.get('/', (c) => { + return c.json({ + remoteAddress: c.env.incoming.socket.remoteAddress, + }) +}) + +serve(app) +``` + +The APIs that you can get from `c.env` are as follows. + +```ts +type HttpBindings = { + incoming: IncomingMessage + outgoing: ServerResponse +} + +type Http2Bindings = { + incoming: Http2ServerRequest + outgoing: Http2ServerResponse +} +``` + +## Early Hints Middleware + +You can send HTTP 103 Early Hints to instruct browsers to preload or preconnect resources before the final response is prepared. The middleware is supported under Node.js bindings (HTTP/1.1 and HTTP/2). + +### Usage + +Import `earlyHints` from `@hono/node-server/early-hints`: + +#### Static links + +```ts +import { serve } from '@hono/node-server' +import { earlyHints } from '@hono/node-server/early-hints' +import { Hono } from 'hono' + +const app = new Hono() + +app.use( + earlyHints({ + link: '; rel=preload; as=style', + }) +) + +app.get('/', (c) => { + return c.html('

Hello Hono!

') +}) + +serve(app) +``` + +#### Dynamic links + +```ts +app.use( + earlyHints({ + link: (c) => + c.req.query('theme') === 'dark' + ? '; rel=preload; as=style' + : '; rel=preload; as=style', + }) +) +``` + +> [!NOTE] +> Early Hints are sent only for requests that look like document navigations. If `Sec-Fetch-Mode` or `Sec-Fetch-Dest` is present with a value other than `navigate` or `document`, for example a `fetch()` or XHR call from a browser, a subresource request, or an iframe navigation, the middleware skips the hints and continues to the handler. Requests without these headers, such as `curl` or `fetch()` from a JavaScript runtime, are treated as navigations and do receive Early Hints. + +## Direct response from Node.js API + +You can directly respond to the client from the Node.js API. +In that case, the response from Hono should be ignored, so return `RESPONSE_ALREADY_SENT`. + +> [!NOTE] +> This feature can be used when migrating existing Node.js applications to Hono, but we recommend using Hono's API for new applications. + +```ts +import { serve } from '@hono/node-server' +import type { HttpBindings } from '@hono/node-server' +import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response' +import { Hono } from 'hono' + +const app = new Hono<{ Bindings: HttpBindings }>() + +app.get('/', (c) => { + const { outgoing } = c.env + outgoing.writeHead(200, { 'Content-Type': 'text/plain' }) + outgoing.end('Hello World\n') + + return RESPONSE_ALREADY_SENT +}) + +serve(app) +``` + +## Listen to a UNIX domain socket + +You can configure the HTTP server to listen to a UNIX domain socket instead of a TCP port. + +```ts +import { createAdaptorServer } from '@hono/node-server' + +// ... + +const socketPath = '/tmp/example.sock' + +const server = createAdaptorServer(app) +server.listen(socketPath, () => { + console.log(`Listening on ${socketPath}`) +}) +``` + +## Related projects + +- Hono - +- Hono GitHub repository - + +## Authors + +- Yusuke Wada +- Taku Amano + +## License + +MIT diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.cjs b/tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.cjs new file mode 100644 index 000000000..b41116486 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.cjs @@ -0,0 +1,22 @@ +Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); + +//#region src/conninfo.ts +/** +* ConnInfo Helper for Node.js +* @param c Context +* @returns ConnInfo +*/ +const getConnInfo = (c) => { + const bindings = c.env.server ? c.env.server : c.env; + const address = bindings.incoming.socket.remoteAddress; + const port = bindings.incoming.socket.remotePort; + const family = bindings.incoming.socket.remoteFamily; + return { remote: { + address, + port, + addressType: family === "IPv4" ? "IPv4" : family === "IPv6" ? "IPv6" : void 0 + } }; +}; + +//#endregion +exports.getConnInfo = getConnInfo; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.d.cts b/tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.d.cts new file mode 100644 index 000000000..ed43a48f0 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.d.cts @@ -0,0 +1,11 @@ +import { GetConnInfo } from "hono/conninfo"; + +//#region src/conninfo.d.ts +/** + * ConnInfo Helper for Node.js + * @param c Context + * @returns ConnInfo + */ +declare const getConnInfo: GetConnInfo; +//#endregion +export { getConnInfo }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.d.mts b/tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.d.mts new file mode 100644 index 000000000..ed43a48f0 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.d.mts @@ -0,0 +1,11 @@ +import { GetConnInfo } from "hono/conninfo"; + +//#region src/conninfo.d.ts +/** + * ConnInfo Helper for Node.js + * @param c Context + * @returns ConnInfo + */ +declare const getConnInfo: GetConnInfo; +//#endregion +export { getConnInfo }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.mjs b/tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.mjs new file mode 100644 index 000000000..29ec46d0d --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.mjs @@ -0,0 +1,20 @@ +//#region src/conninfo.ts +/** +* ConnInfo Helper for Node.js +* @param c Context +* @returns ConnInfo +*/ +const getConnInfo = (c) => { + const bindings = c.env.server ? c.env.server : c.env; + const address = bindings.incoming.socket.remoteAddress; + const port = bindings.incoming.socket.remotePort; + const family = bindings.incoming.socket.remoteFamily; + return { remote: { + address, + port, + addressType: family === "IPv4" ? "IPv4" : family === "IPv6" ? "IPv6" : void 0 + } }; +}; + +//#endregion +export { getConnInfo }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/constants-BLSFu_RU.mjs b/tests/fixtures/npm/node_modules/@hono/node-server/dist/constants-BLSFu_RU.mjs new file mode 100644 index 000000000..db7c31e48 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/constants-BLSFu_RU.mjs @@ -0,0 +1,5 @@ +//#region src/utils/response/constants.ts +const X_ALREADY_SENT = "x-hono-already-sent"; + +//#endregion +export { X_ALREADY_SENT as t }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/constants-BXAKTxRC.cjs b/tests/fixtures/npm/node_modules/@hono/node-server/dist/constants-BXAKTxRC.cjs new file mode 100644 index 000000000..8888a5804 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/constants-BXAKTxRC.cjs @@ -0,0 +1,11 @@ + +//#region src/utils/response/constants.ts +const X_ALREADY_SENT = "x-hono-already-sent"; + +//#endregion +Object.defineProperty(exports, 'X_ALREADY_SENT', { + enumerable: true, + get: function () { + return X_ALREADY_SENT; + } +}); \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.cjs b/tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.cjs new file mode 100644 index 000000000..aa3ceba6a --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.cjs @@ -0,0 +1,35 @@ +Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); + +//#region src/early-hints.ts +/** +* Early Hints middleware for Node.js +* Automatically sends a 103 Early Hints informational response with the specified Link header(s). +* +* @param options EarlyHintsOptions +* @returns MiddlewareHandler +*/ +const earlyHints = (options) => { + let warned = false; + return async (c, next) => { + const mode = c.req.header("Sec-Fetch-Mode"); + const dest = c.req.header("Sec-Fetch-Dest"); + if (mode && mode !== "navigate" || dest && dest !== "document") return next(); + const env = c.env || {}; + const outgoing = ("server" in env ? env.server : env)?.outgoing; + if (typeof outgoing?.writeEarlyHints !== "function") { + if (!warned) { + console.warn("Early Hints Middleware is not supported because writeEarlyHints is not defined."); + warned = true; + } + return await next(); + } + if (!outgoing.headersSent) { + const link = typeof options.link === "function" ? options.link(c) : options.link; + if (link !== void 0 && (Array.isArray(link) ? link.length > 0 : Boolean(link))) outgoing.writeEarlyHints({ link }); + } + await next(); + }; +}; + +//#endregion +exports.earlyHints = earlyHints; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.d.cts b/tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.d.cts new file mode 100644 index 000000000..9bd27d5e3 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.d.cts @@ -0,0 +1,16 @@ +import { Context, Env, MiddlewareHandler } from "hono"; + +//#region src/early-hints.d.ts +type EarlyHintsOptions = { + link: string | string[] | ((c: Context) => string | string[] | undefined); +}; +/** + * Early Hints middleware for Node.js + * Automatically sends a 103 Early Hints informational response with the specified Link header(s). + * + * @param options EarlyHintsOptions + * @returns MiddlewareHandler + */ +declare const earlyHints: (options: EarlyHintsOptions) => MiddlewareHandler; +//#endregion +export { EarlyHintsOptions, earlyHints }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.d.mts b/tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.d.mts new file mode 100644 index 000000000..9bd27d5e3 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.d.mts @@ -0,0 +1,16 @@ +import { Context, Env, MiddlewareHandler } from "hono"; + +//#region src/early-hints.d.ts +type EarlyHintsOptions = { + link: string | string[] | ((c: Context) => string | string[] | undefined); +}; +/** + * Early Hints middleware for Node.js + * Automatically sends a 103 Early Hints informational response with the specified Link header(s). + * + * @param options EarlyHintsOptions + * @returns MiddlewareHandler + */ +declare const earlyHints: (options: EarlyHintsOptions) => MiddlewareHandler; +//#endregion +export { EarlyHintsOptions, earlyHints }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.mjs b/tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.mjs new file mode 100644 index 000000000..7999a3d0c --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.mjs @@ -0,0 +1,33 @@ +//#region src/early-hints.ts +/** +* Early Hints middleware for Node.js +* Automatically sends a 103 Early Hints informational response with the specified Link header(s). +* +* @param options EarlyHintsOptions +* @returns MiddlewareHandler +*/ +const earlyHints = (options) => { + let warned = false; + return async (c, next) => { + const mode = c.req.header("Sec-Fetch-Mode"); + const dest = c.req.header("Sec-Fetch-Dest"); + if (mode && mode !== "navigate" || dest && dest !== "document") return next(); + const env = c.env || {}; + const outgoing = ("server" in env ? env.server : env)?.outgoing; + if (typeof outgoing?.writeEarlyHints !== "function") { + if (!warned) { + console.warn("Early Hints Middleware is not supported because writeEarlyHints is not defined."); + warned = true; + } + return await next(); + } + if (!outgoing.headersSent) { + const link = typeof options.link === "function" ? options.link(c) : options.link; + if (link !== void 0 && (Array.isArray(link) ? link.length > 0 : Boolean(link))) outgoing.writeEarlyHints({ link }); + } + await next(); + }; +}; + +//#endregion +export { earlyHints }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/index.cjs b/tests/fixtures/npm/node_modules/@hono/node-server/dist/index.cjs new file mode 100644 index 000000000..e578de054 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/index.cjs @@ -0,0 +1,1340 @@ +Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +const require_constants = require('./constants-BXAKTxRC.cjs'); +let node_http = require("node:http"); +let node_http2 = require("node:http2"); +let node_stream = require("node:stream"); +let hono_ws = require("hono/ws"); + +//#region src/error.ts +var RequestError = class extends Error { + constructor(message, options) { + super(message, options); + this.name = "RequestError"; + } +}; + +//#endregion +//#region src/headers.ts +const nonJoinedHeaders = new Set([ + "age", + "authorization", + "content-length", + "content-type", + "etag", + "expires", + "from", + "host", + "if-modified-since", + "if-unmodified-since", + "last-modified", + "location", + "max-forwards", + "proxy-authorization", + "referer", + "retry-after", + "server", + "user-agent" +]); +const validHeaderName = /^[!#$%&'*+\-.^_`|~\dA-Za-z]+$/; +const isHttpWhitespace = (code) => code === 9 || code === 10 || code === 13 || code === 32; +const normalizeHeaderValue = (value) => { + if (!isHttpWhitespace(value.charCodeAt(0)) && !isHttpWhitespace(value.charCodeAt(value.length - 1))) return value; + let start = 0; + let end = value.length; + while (start < end && isHttpWhitespace(value.charCodeAt(start))) start++; + while (end > start && isHttpWhitespace(value.charCodeAt(end - 1))) end--; + return value.slice(start, end); +}; +const forbiddenHeaderValue = /[\0\r\n]/; +const GlobalHeaders = globalThis.Headers; +const materializeHeaders = (rawHeaders, HeadersCtor = GlobalHeaders) => { + const headers = new HeadersCtor(); + for (let i = 0; i < rawHeaders.length; i += 2) { + const name = rawHeaders[i]; + if (!name.startsWith(":")) headers.append(name, rawHeaders[i + 1]); + } + return headers; +}; +var RequestHeaders = class { + #incoming; + #rawHeaders; + #headers; + #invalidValue; + constructor(incoming) { + this.#incoming = incoming; + if (incoming instanceof node_http2.Http2ServerRequest) this.#rawHeaders = incoming.rawHeaders.slice(); + } + get #lazyRawHeaders() { + return this.#rawHeaders ??= this.#incoming.rawHeaders.slice(); + } + get #native() { + if (!this.#headers) { + this.#headers = materializeHeaders(this.#lazyRawHeaders); + this.#rawHeaders = void 0; + } + return this.#headers; + } + #normalizedName(name) { + if (typeof name !== "string") return; + if (!validHeaderName.test(name)) throw new TypeError(`Invalid header name: ${name}`); + return name.toLowerCase(); + } + #lookupHttp1(lowerName) { + const headers = this.#incoming instanceof node_http2.Http2ServerRequest ? void 0 : this.#incoming.headers; + if (!headers || nonJoinedHeaders.has(lowerName) || lowerName === "set-cookie" || lowerName === "__proto__") return; + if (!Object.hasOwn(headers, lowerName)) return null; + const rawValue = headers[lowerName]; + if (typeof rawValue === "string") { + const value = normalizeHeaderValue(rawValue); + return forbiddenHeaderValue.test(value) ? void 0 : value; + } + } + #lookup(rawHeaders, lowerName) { + const separator = lowerName === "cookie" ? "; " : ", "; + let value = null; + for (let i = 0; i < rawHeaders.length; i += 2) { + const rawName = rawHeaders[i]; + if (rawName.length === lowerName.length && rawName.toLowerCase() === lowerName) { + const rawValue = normalizeHeaderValue(rawHeaders[i + 1]); + if (forbiddenHeaderValue.test(rawValue)) { + this.#invalidValue = true; + return; + } + value = value === null ? rawValue : value + separator + rawValue; + } + } + return value; + } + append(name, value) { + this.#native.append(name, value); + } + delete(name) { + this.#native.delete(name); + } + get(name) { + const lowerName = this.#normalizedName(name); + if (lowerName && !this.#headers && !this.#invalidValue) { + const http1Value = this.#lookupHttp1(lowerName); + if (http1Value !== void 0) return http1Value; + const value = this.#lookup(this.#lazyRawHeaders, lowerName); + if (value !== void 0) return value; + } + return this.#native.get(name); + } + has(name) { + const lowerName = this.#normalizedName(name); + if (lowerName && !this.#headers && !this.#invalidValue) { + const http1Value = this.#lookupHttp1(lowerName); + if (http1Value !== void 0) return http1Value !== null; + const value = this.#lookup(this.#lazyRawHeaders, lowerName); + if (value !== void 0) return value !== null; + } + return this.#native.has(name); + } + set(name, value) { + this.#native.set(name, value); + } + getSetCookie() { + return this.#native.getSetCookie(); + } + keys() { + return this.#native.keys(); + } + values() { + return this.#native.values(); + } + entries() { + return this.#native.entries(); + } + forEach(callback, thisArg) { + this.#native.forEach((value, key) => { + callback.call(thisArg, value, key, this); + }); + } + [Symbol.iterator]() { + return this.entries(); + } +}; +Object.defineProperty(RequestHeaders.prototype, Symbol.for("nodejs.util.inspect.custom"), { value: function(depth, options, inspectFn) { + return `Headers (lightweight) ${inspectFn(Object.fromEntries(this), { + ...options, + depth: depth == null ? null : depth - 1 + })}`; +} }); +Object.setPrototypeOf(RequestHeaders.prototype, GlobalHeaders.prototype); +const newHeadersFromIncoming = (incoming) => globalThis.Headers === GlobalHeaders ? new RequestHeaders(incoming) : materializeHeaders(incoming.rawHeaders, globalThis.Headers); + +//#endregion +//#region src/url.ts +const reValidRequestUrl = /^\/[!#$&-;=?-\[\]_a-z~]*$/; +const reDotSegment = /\/\.\.?(?:[/?#]|$)/; +const reValidHost = /^[a-z0-9._-]+(?::(?:[1-5]\d{3,4}|[6-9]\d{3}))?$/; +const buildUrl = (scheme, host, incomingUrl) => { + const url = `${scheme}://${host}${incomingUrl}`; + if (!reValidHost.test(host)) { + const urlObj = new URL(url); + if (urlObj.hostname.length !== host.length && urlObj.hostname !== (host.includes(":") ? host.replace(/:\d+$/, "") : host).toLowerCase()) throw new RequestError("Invalid host header"); + return urlObj.href; + } else if (incomingUrl.length === 0) return url + "/"; + else { + if (incomingUrl.charCodeAt(0) !== 47) throw new RequestError("Invalid URL"); + if (!reValidRequestUrl.test(incomingUrl) || reDotSegment.test(incomingUrl)) return new URL(url).href; + return url; + } +}; + +//#endregion +//#region src/request.ts +const toRequestError = (e) => { + if (e instanceof RequestError) return e; + return new RequestError(e.message, { cause: e }); +}; +const GlobalRequest = global.Request; +var Request$1 = class extends GlobalRequest { + constructor(input, options) { + if (typeof input === "object" && getRequestCache in input) { + const hasReplacementBody = options !== void 0 && "body" in options && options.body != null; + if (input[bodyConsumedDirectlyKey] && !hasReplacementBody) throw new TypeError("Cannot construct a Request with a Request object that has already been used."); + input = input[getRequestCache](); + } + if (typeof (options?.body)?.getReader !== "undefined") options.duplex ??= "half"; + super(input, options); + } +}; +const wrapBodyStream = Symbol("wrapBodyStream"); +const byteExactEncodings = new Set([ + "latin1", + "binary", + "hex", + "base64", + "base64url" +]); +const isByteExactEncoding = (encoding) => encoding === null || byteExactEncodings.has(encoding); +const bodyBufferedBeforeDisconnectKey = Symbol("bodyBufferedBeforeDisconnect"); +const bodyBufferedLengthBeforeDisconnectKey = Symbol("bodyBufferedLengthBeforeDisconnect"); +const toBufferChunk = (chunk, encoding) => Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding ?? "utf8"); +const isRecoverableDisconnectedIncoming = (incoming) => !(incoming instanceof node_http2.Http2ServerRequest) && !!incoming.complete && !!incoming.readableAborted && typeof incoming.read === "function" && isByteExactEncoding(incoming.readableEncoding); +const recordBodyBufferedBeforeDisconnect = (incoming) => { + if (incoming.readableDidRead || !isRecoverableDisconnectedIncoming(incoming)) return; + const incomingWithRecovery = incoming; + incomingWithRecovery[bodyBufferedLengthBeforeDisconnectKey] ??= incoming.readableLength; +}; +const readBodyBufferedBeforeDisconnect = (incoming, chunks) => { + if (incoming.readableDidRead && !chunks || !isRecoverableDisconnectedIncoming(incoming)) return; + const incomingWithRecovery = incoming; + if (incomingWithRecovery[bodyBufferedBeforeDisconnectKey] !== void 0) return incomingWithRecovery[bodyBufferedBeforeDisconnectKey]; + let result; + const errored = incoming.errored; + if (errored && errored.code !== "ECONNRESET") result = errored; + else if (incomingWithRecovery[bodyBufferedLengthBeforeDisconnectKey] !== void 0 && incoming.readableLength !== incomingWithRecovery[bodyBufferedLengthBeforeDisconnectKey]) result = newBodyUnusableError(); + else { + const bodyChunks = chunks ?? []; + const chunk = incoming.read(); + if (chunk !== null) bodyChunks.push(toBufferChunk(chunk, incoming.readableEncoding)); + const buffer = bodyChunks.length === 1 ? bodyChunks[0] : Buffer.concat(bodyChunks); + result = buffer; + const contentLength = incoming.headers["content-length"]; + if (typeof contentLength === "string" && /^\d+$/.test(contentLength)) { + const expectedLength = Number(contentLength); + if (Number.isSafeInteger(expectedLength) && buffer.length !== expectedLength) result = newBodyUnusableError(); + } + } + incomingWithRecovery[bodyBufferedBeforeDisconnectKey] = result; + return result; +}; +const enqueueBufferedBody = (controller, buffered) => { + if (buffered instanceof Error) { + controller.error(buffered); + return; + } + if (buffered.length > 0) controller.enqueue(buffered); + controller.close(); +}; +const newRequestFromIncoming = (method, url, headers, incoming, abortController) => { + const init = { + method, + headers, + signal: abortController.signal + }; + if (method === "TRACE") { + init.method = "GET"; + const req = new Request$1(url, init); + Object.defineProperty(req, "method", { get() { + return "TRACE"; + } }); + return req; + } + if (!(method === "GET" || method === "HEAD")) if ("rawBody" in incoming && incoming.rawBody instanceof Buffer) init.body = new ReadableStream({ start(controller) { + controller.enqueue(incoming.rawBody); + controller.close(); + } }); + else if (incoming[wrapBodyStream]) { + let reader; + init.body = new ReadableStream({ async pull(controller) { + try { + if (!reader) { + const buffered = readBodyBufferedBeforeDisconnect(incoming); + if (buffered !== void 0) { + enqueueBufferedBody(controller, buffered); + return; + } + } + reader ||= node_stream.Readable.toWeb(incoming).getReader(); + const { done, value } = await reader.read(); + if (done) controller.close(); + else controller.enqueue(value); + } catch (error) { + controller.error(error); + } + } }); + } else { + const buffered = readBodyBufferedBeforeDisconnect(incoming); + if (buffered !== void 0) init.body = new ReadableStream({ start(controller) { + enqueueBufferedBody(controller, buffered); + } }); + else init.body = node_stream.Readable.toWeb(incoming); + } + return new Request$1(url, init); +}; +const getRequestCache = Symbol("getRequestCache"); +const requestCache = Symbol("requestCache"); +const incomingKey = Symbol("incomingKey"); +const urlKey = Symbol("urlKey"); +const methodKey = Symbol("methodKey"); +const headersKey = Symbol("headersKey"); +const abortControllerKey = Symbol("abortControllerKey"); +const getAbortController = Symbol("getAbortController"); +const abortRequest = Symbol("abortRequest"); +const bodyBufferKey = Symbol("bodyBuffer"); +const bodyReadPromiseKey = Symbol("bodyReadPromise"); +const bodyConsumedDirectlyKey = Symbol("bodyConsumedDirectly"); +const bodyLockReaderKey = Symbol("bodyLockReader"); +const abortReasonKey = Symbol("abortReason"); +const newBodyUnusableError = () => { + return /* @__PURE__ */ new TypeError("Body is unusable"); +}; +const rejectBodyUnusable = () => { + return Promise.reject(newBodyUnusableError()); +}; +const textDecoder = new TextDecoder(); +const consumeBodyDirectOnce = (request) => { + if (request[bodyConsumedDirectlyKey]) return rejectBodyUnusable(); + request[bodyConsumedDirectlyKey] = true; +}; +const toArrayBuffer = (buf) => { + return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); +}; +const contentType = (request) => { + return (request[headersKey] ||= newHeadersFromIncoming(request[incomingKey])).get("content-type") || ""; +}; +const methodTokenRegExp = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/; +const normalizeIncomingMethod = (method) => { + if (typeof method !== "string" || method.length === 0) return "GET"; + switch (method) { + case "DELETE": + case "GET": + case "HEAD": + case "OPTIONS": + case "PATCH": + case "POST": + case "PUT": + case "QUERY": return method; + } + const upper = method.toUpperCase(); + switch (upper) { + case "DELETE": + case "GET": + case "HEAD": + case "OPTIONS": + case "POST": + case "PUT": return upper; + default: return method; + } +}; +const validateDirectReadMethod = (method) => { + if (!methodTokenRegExp.test(method)) return /* @__PURE__ */ new TypeError(`'${method}' is not a valid HTTP method.`); + const normalized = method.toUpperCase(); + if (normalized === "CONNECT" || normalized === "TRACK" || normalized === "TRACE" && method !== "TRACE") return /* @__PURE__ */ new TypeError(`'${method}' HTTP method is unsupported.`); +}; +const readBodyWithFastPath = (request, method, fromBuffer) => { + if (request[bodyConsumedDirectlyKey]) return rejectBodyUnusable(); + const methodName = request.method; + if (methodName === "GET" || methodName === "HEAD") return request[getRequestCache]()[method](); + const methodValidationError = validateDirectReadMethod(methodName); + if (methodValidationError) return Promise.reject(methodValidationError); + if (request[requestCache]) { + if (methodName !== "TRACE") return request[requestCache][method](); + } + const alreadyUsedError = consumeBodyDirectOnce(request); + if (alreadyUsedError) return alreadyUsedError; + const raw = readRawBodyIfAvailable(request); + if (raw) { + const result = Promise.resolve(fromBuffer(raw, request)); + request[bodyBufferKey] = void 0; + return result; + } + return readBodyDirect(request).then((buf) => { + const result = fromBuffer(buf, request); + request[bodyBufferKey] = void 0; + return result; + }); +}; +const readRawBodyIfAvailable = (request) => { + const incoming = request[incomingKey]; + if ("rawBody" in incoming && incoming.rawBody instanceof Buffer) return incoming.rawBody; +}; +const normalizeAbortError = (request, incoming) => { + if (incoming.errored) return incoming.errored; + const reason = request[abortReasonKey]; + if (reason !== void 0) return reason instanceof Error ? reason : new Error(String(reason)); + return /* @__PURE__ */ new Error("Client connection prematurely closed."); +}; +const readBodyDirect = (request) => { + if (request[bodyBufferKey]) return Promise.resolve(request[bodyBufferKey]); + if (request[bodyReadPromiseKey]) return request[bodyReadPromiseKey]; + const incoming = request[incomingKey]; + if (incoming.readableDidRead) return rejectBodyUnusable(); + const buffered = readBodyBufferedBeforeDisconnect(incoming); + if (buffered !== void 0) { + if (buffered instanceof Error) return Promise.reject(buffered); + request[bodyBufferKey] = buffered; + return Promise.resolve(buffered); + } + const promise = new Promise((resolve, reject) => { + const chunks = []; + let settled = false; + const finish = (callback) => { + if (settled) return; + settled = true; + cleanup(); + callback(); + }; + const recoverCompleteBodyAfterDisconnect = (error) => { + const streamError = incoming.errored ?? error; + if (!isRecoverableDisconnectedIncoming(incoming) || streamError && streamError.code !== "ECONNRESET") return false; + finish(() => { + const recovered = readBodyBufferedBeforeDisconnect(incoming, chunks); + if (recovered instanceof Error) reject(recovered); + else if (recovered === void 0) reject(error ?? normalizeAbortError(request, incoming)); + else { + request[bodyBufferKey] = recovered; + resolve(recovered); + } + }); + return true; + }; + const onData = (chunk) => { + chunks.push(toBufferChunk(chunk, incoming.readableEncoding)); + }; + const onEnd = () => { + finish(() => { + const buffer = chunks.length === 1 ? chunks[0] : Buffer.concat(chunks); + request[bodyBufferKey] = buffer; + resolve(buffer); + }); + }; + const onError = (error) => { + if (recoverCompleteBodyAfterDisconnect(error)) return; + finish(() => { + reject(error); + }); + }; + const onClose = () => { + if (incoming.readableEnded) { + onEnd(); + return; + } + if (recoverCompleteBodyAfterDisconnect()) return; + finish(() => { + reject(normalizeAbortError(request, incoming)); + }); + }; + const cleanup = () => { + incoming.off("data", onData); + incoming.off("end", onEnd); + incoming.off("error", onError); + incoming.off("close", onClose); + request[bodyReadPromiseKey] = void 0; + }; + incoming.on("data", onData); + incoming.on("end", onEnd); + incoming.on("error", onError); + incoming.on("close", onClose); + queueMicrotask(() => { + if (settled) return; + if (incoming.readableEnded) onEnd(); + else if (incoming.errored) onError(incoming.errored); + else if (incoming.destroyed) onClose(); + }); + }); + request[bodyReadPromiseKey] = promise; + return promise; +}; +const requestPrototype = { + get method() { + return this[methodKey]; + }, + get url() { + return this[urlKey]; + }, + get headers() { + return this[headersKey] ||= newHeadersFromIncoming(this[incomingKey]); + }, + [abortRequest](reason) { + if (this[abortReasonKey] === void 0) this[abortReasonKey] = reason; + const abortController = this[abortControllerKey]; + if (abortController && !abortController.signal.aborted) abortController.abort(reason); + }, + [getAbortController]() { + this[abortControllerKey] ||= new AbortController(); + if (this[abortReasonKey] !== void 0 && !this[abortControllerKey].signal.aborted) this[abortControllerKey].abort(this[abortReasonKey]); + return this[abortControllerKey]; + }, + [getRequestCache]() { + const abortController = this[getAbortController](); + if (this[requestCache]) return this[requestCache]; + const method = this.method; + if (this[bodyConsumedDirectlyKey] && !(method === "GET" || method === "HEAD")) { + this[bodyBufferKey] = void 0; + const init = { + method: method === "TRACE" ? "GET" : method, + headers: this.headers, + signal: abortController.signal + }; + if (method !== "TRACE") { + init.body = new ReadableStream({ start(c) { + c.close(); + } }); + init.duplex = "half"; + } + const req = new Request$1(this[urlKey], init); + if (method === "TRACE") Object.defineProperty(req, "method", { get() { + return "TRACE"; + } }); + return this[requestCache] = req; + } + return this[requestCache] = newRequestFromIncoming(this.method, this[urlKey], this.headers, this[incomingKey], abortController); + }, + get body() { + if (!this[bodyConsumedDirectlyKey]) return this[getRequestCache]().body; + const request = this[getRequestCache](); + if (!this[bodyLockReaderKey] && request.body) this[bodyLockReaderKey] = request.body.getReader(); + return request.body; + }, + get bodyUsed() { + if (this[bodyConsumedDirectlyKey]) return true; + if (this[requestCache]) return this[requestCache].bodyUsed; + return false; + } +}; +Object.defineProperty(requestPrototype, "signal", { get() { + return this[getAbortController]().signal; +} }); +[ + "cache", + "credentials", + "destination", + "integrity", + "mode", + "redirect", + "referrer", + "referrerPolicy", + "keepalive" +].forEach((k) => { + Object.defineProperty(requestPrototype, k, { get() { + return this[getRequestCache]()[k]; + } }); +}); +["clone", "formData"].forEach((k) => { + Object.defineProperty(requestPrototype, k, { value: function() { + if (this[bodyConsumedDirectlyKey]) { + if (k === "clone") throw newBodyUnusableError(); + return rejectBodyUnusable(); + } + return this[getRequestCache]()[k](); + } }); +}); +Object.defineProperty(requestPrototype, "text", { value: function() { + return readBodyWithFastPath(this, "text", (buf) => textDecoder.decode(buf)); +} }); +Object.defineProperty(requestPrototype, "arrayBuffer", { value: function() { + return readBodyWithFastPath(this, "arrayBuffer", (buf) => toArrayBuffer(buf)); +} }); +Object.defineProperty(requestPrototype, "blob", { value: function() { + return readBodyWithFastPath(this, "blob", (buf, request) => { + const type = contentType(request); + const init = type ? { headers: { "content-type": type } } : void 0; + return new Response(buf, init).blob(); + }); +} }); +Object.defineProperty(requestPrototype, "json", { value: function() { + if (this[bodyConsumedDirectlyKey]) return rejectBodyUnusable(); + return this.text().then(JSON.parse); +} }); +Object.defineProperty(requestPrototype, Symbol.for("nodejs.util.inspect.custom"), { value: function(depth, options, inspectFn) { + return `Request (lightweight) ${inspectFn({ + method: this.method, + url: this.url, + headers: this.headers, + nativeRequest: this[requestCache] + }, { + ...options, + depth: depth == null ? null : depth - 1 + })}`; +} }); +Object.setPrototypeOf(requestPrototype, Request$1.prototype); +const newRequest = (incoming, defaultHostname) => { + const req = Object.create(requestPrototype); + req[incomingKey] = incoming; + req[methodKey] = normalizeIncomingMethod(incoming.method); + const incomingUrl = incoming.url || ""; + if (incomingUrl[0] !== "/" && (incomingUrl.startsWith("http://") || incomingUrl.startsWith("https://"))) { + if (incoming instanceof node_http2.Http2ServerRequest) throw new RequestError("Absolute URL for :path is not allowed in HTTP/2"); + try { + req[urlKey] = new URL(incomingUrl).href; + } catch (e) { + throw new RequestError("Invalid absolute URL", { cause: e }); + } + return req; + } + const host = (incoming instanceof node_http2.Http2ServerRequest ? incoming.authority : incoming.headers.host) || defaultHostname; + if (!host) throw new RequestError("Missing host header"); + let scheme; + if (incoming instanceof node_http2.Http2ServerRequest) { + scheme = incoming.scheme; + if (!(scheme === "http" || scheme === "https")) throw new RequestError("Unsupported scheme"); + } else scheme = incoming.socket && incoming.socket.encrypted ? "https" : "http"; + try { + req[urlKey] = buildUrl(scheme, host, incomingUrl); + } catch (e) { + if (e instanceof RequestError) throw e; + else throw new RequestError("Invalid URL", { cause: e }); + } + return req; +}; + +//#endregion +//#region src/response.ts +const defaultContentType = "text/plain; charset=UTF-8"; +const responseCache = Symbol("responseCache"); +const getResponseCache = Symbol("getResponseCache"); +const cacheKey = Symbol("cache"); +const GlobalResponse = global.Response; +var Response$1 = class Response$1 { + #body; + #init; + [getResponseCache]() { + const cache = this[cacheKey]; + const liveHeaders = cache && cache[2] instanceof Headers ? cache[2] : void 0; + delete this[cacheKey]; + return this[responseCache] ||= new GlobalResponse(this.#body, liveHeaders ? { + status: this.#init?.status, + statusText: this.#init?.statusText, + headers: liveHeaders + } : this.#init); + } + constructor(body, init) { + let headers; + this.#body = body; + if (init instanceof GlobalResponse) { + const cachedGlobalResponse = init[responseCache]; + if (cachedGlobalResponse) { + this.#init = cachedGlobalResponse; + this[getResponseCache](); + return; + } + this.#init = init instanceof Response$1 ? init.#init : init; + headers = new Headers(init.headers); + } else this.#init = init; + if (body == null || typeof body === "string" || typeof body?.getReader !== "undefined" || body instanceof Blob || body instanceof Uint8Array) this[cacheKey] = [ + init?.status || 200, + body ?? null, + headers || init?.headers + ]; + } + get headers() { + const cache = this[cacheKey]; + if (cache) { + if (!(cache[2] instanceof Headers)) cache[2] = new Headers(cache[2] || (cache[1] === null ? void 0 : { "content-type": defaultContentType })); + return cache[2]; + } + return this[getResponseCache]().headers; + } + get status() { + return this[cacheKey]?.[0] ?? this[getResponseCache]().status; + } + get ok() { + const status = this.status; + return status >= 200 && status < 300; + } +}; +[ + "body", + "bodyUsed", + "redirected", + "statusText", + "trailers", + "type", + "url" +].forEach((k) => { + Object.defineProperty(Response$1.prototype, k, { get() { + return this[getResponseCache]()[k]; + } }); +}); +[ + "arrayBuffer", + "blob", + "clone", + "formData", + "json", + "text" +].forEach((k) => { + Object.defineProperty(Response$1.prototype, k, { value: function() { + return this[getResponseCache]()[k](); + } }); +}); +Object.defineProperty(Response$1.prototype, Symbol.for("nodejs.util.inspect.custom"), { value: function(depth, options, inspectFn) { + return `Response (lightweight) ${inspectFn({ + status: this.status, + headers: this.headers, + ok: this.ok, + nativeResponse: this[responseCache] + }, { + ...options, + depth: depth == null ? null : depth - 1 + })}`; +} }); +Object.setPrototypeOf(Response$1, GlobalResponse); +Object.setPrototypeOf(Response$1.prototype, GlobalResponse.prototype); +const validRedirectUrl = /^https?:\/\/[!#-;=?-[\]_a-z~A-Z]+$/; +const parseRedirectUrl = (url) => { + if (url instanceof URL) return url.href; + if (validRedirectUrl.test(url)) return url; + return new URL(url).href; +}; +const validRedirectStatuses = new Set([ + 301, + 302, + 303, + 307, + 308 +]); +Object.defineProperty(Response$1, "redirect", { + value: function redirect(url, status = 302) { + if (!validRedirectStatuses.has(status)) throw new RangeError("Invalid status code"); + return new Response$1(null, { + status, + headers: { location: parseRedirectUrl(url) } + }); + }, + writable: true, + configurable: true +}); +Object.defineProperty(Response$1, "json", { + value: function json(data, init) { + const body = JSON.stringify(data); + if (body === void 0) throw new TypeError("The data is not JSON serializable"); + const initHeaders = init?.headers; + let headers; + if (initHeaders) { + headers = new Headers(initHeaders); + if (!headers.has("content-type")) headers.set("content-type", "application/json"); + } else headers = { "content-type": "application/json" }; + return new Response$1(body, { + status: init?.status ?? 200, + statusText: init?.statusText, + headers + }); + }, + writable: true, + configurable: true +}); + +//#endregion +//#region src/utils.ts +async function readWithoutBlocking(readPromise) { + return Promise.race([readPromise, Promise.resolve().then(() => Promise.resolve(void 0))]); +} +function writeFromReadableStreamDefaultReader(reader, writable, currentReadPromise) { + const cancel = (error) => { + reader.cancel(error).catch(() => {}); + }; + writable.on("close", cancel); + writable.on("error", cancel); + (currentReadPromise ?? reader.read()).then(flow, handleStreamError); + return reader.closed.finally(() => { + writable.off("close", cancel); + writable.off("error", cancel); + }); + function handleStreamError(error) { + if (error) writable.destroy(error); + } + function onDrain() { + reader.read().then(flow, handleStreamError); + } + function flow({ done, value }) { + try { + if (done) writable.end(); + else if (!writable.write(value)) writable.once("drain", onDrain); + else return reader.read().then(flow, handleStreamError); + } catch (e) { + handleStreamError(e); + } + } +} +function writeFromReadableStream(stream, writable) { + if (stream.locked) throw new TypeError("ReadableStream is locked."); + else if (writable.destroyed) return; + return writeFromReadableStreamDefaultReader(stream.getReader(), writable); +} +const buildOutgoingHttpHeaders = (headers, defaultContentType) => { + const res = {}; + if (!(headers instanceof Headers)) headers = new Headers(headers ?? void 0); + if (headers.has("set-cookie")) { + const cookies = []; + for (const [k, v] of headers) if (k === "set-cookie") cookies.push(v); + else res[k] = v; + if (cookies.length > 0) res["set-cookie"] = cookies; + } else for (const [k, v] of headers) res[k] = v; + if (defaultContentType) res["content-type"] ??= defaultContentType; + return res; +}; + +//#endregion +//#region src/listener.ts +const outgoingEnded = Symbol("outgoingEnded"); +const incomingDraining = Symbol("incomingDraining"); +const DRAIN_TIMEOUT_MS = 500; +const MAX_DRAIN_BYTES = 64 * 1024 * 1024; +const drainIncoming = (incoming) => { + const incomingWithDrainState = incoming; + if (incoming.destroyed || incomingWithDrainState[incomingDraining]) return; + incomingWithDrainState[incomingDraining] = true; + if (incoming instanceof node_http2.Http2ServerRequest) { + try { + incoming.stream?.close?.(node_http2.constants.NGHTTP2_NO_ERROR); + } catch {} + return; + } + let bytesRead = 0; + const cleanup = () => { + clearTimeout(timer); + incoming.off("data", onData); + incoming.off("end", cleanup); + incoming.off("error", cleanup); + }; + const forceClose = () => { + cleanup(); + const socket = incoming.socket; + if (socket && !socket.destroyed) { + if (typeof socket.destroySoon === "function") socket.destroySoon(); + else if (typeof socket.destroy === "function") socket.destroy(); + } + }; + const timer = setTimeout(forceClose, DRAIN_TIMEOUT_MS); + timer.unref?.(); + const onData = (chunk) => { + bytesRead += chunk.length; + if (bytesRead > MAX_DRAIN_BYTES) forceClose(); + }; + incoming.on("data", onData); + incoming.on("end", cleanup); + incoming.on("error", cleanup); + incoming.resume(); +}; +const makeCloseHandler = (req, incoming, outgoing, needsBodyCleanup) => () => { + if (incoming.errored) { + recordBodyBufferedBeforeDisconnect(incoming); + req[abortRequest](incoming.errored.toString()); + } else if (!outgoing.writableFinished || incoming instanceof node_http2.Http2ServerRequest && incoming.aborted) { + recordBodyBufferedBeforeDisconnect(incoming); + req[abortRequest]("Client connection prematurely closed."); + } + if (needsBodyCleanup && !incoming.readableEnded) setTimeout(() => { + if (!incoming.readableEnded) setTimeout(() => { + drainIncoming(incoming); + }); + }); +}; +const isImmediateCacheableResponse = (res) => { + if (!(cacheKey in res)) return false; + const body = res[cacheKey][1]; + return body === null || typeof body === "string" || body instanceof Uint8Array; +}; +const handleRequestError = () => new Response(null, { status: 400 }); +const handleFetchError = (e) => new Response(null, { status: e instanceof Error && (e.name === "TimeoutError" || e.constructor.name === "TimeoutError") ? 504 : 500 }); +const handleResponseError = (e, outgoing) => { + const err = e instanceof Error ? e : new Error("unknown error", { cause: e }); + if (err.code === "ERR_STREAM_PREMATURE_CLOSE") console.info("The user aborted a request."); + else { + console.error(e); + if (!outgoing.headersSent) { + if (outgoing instanceof node_http.ServerResponse) outgoing._contentLength = null; + outgoing.writeHead(500, { "Content-Type": "text/plain" }); + } + outgoing.end(`Error: ${err.message}`); + outgoing.destroy(err); + } +}; +const flushHeaders = (outgoing) => { + if ("flushHeaders" in outgoing && outgoing.writable) outgoing.flushHeaders(); +}; +const trySetContentLength = (outgoing, status, length) => { + const http1 = outgoing; + if (http1._contentLength === null && http1._hasBody && http1.useChunkedEncodingByDefault && !http1._removedContLen && status >= 200 && status !== 204 && status !== 304 && !outgoing.hasHeader("content-length") && !outgoing.hasHeader("transfer-encoding") && !outgoing.hasHeader("trailer")) { + http1._contentLength = length; + return true; + } + return false; +}; +const writeDefaultHeaders = (outgoing, status, length) => { + if (trySetContentLength(outgoing, status, length)) outgoing.writeHead(status, { "Content-Type": defaultContentType }); + else outgoing.writeHead(status, { + "Content-Type": defaultContentType, + "Content-Length": length + }); +}; +const responseViaCache = async (res, outgoing) => { + let [status, body, header] = res[cacheKey]; + if (!header) { + if (body === null) { + outgoing.writeHead(status); + outgoing.end(); + } else if (typeof body === "string") { + writeDefaultHeaders(outgoing, status, Buffer.byteLength(body)); + outgoing.end(body); + } else if (body instanceof Uint8Array) { + writeDefaultHeaders(outgoing, status, body.byteLength); + outgoing.end(body); + } else if (body instanceof Blob) { + writeDefaultHeaders(outgoing, status, body.size); + outgoing.end(new Uint8Array(await body.arrayBuffer())); + } else { + outgoing.writeHead(status, { "Content-Type": defaultContentType }); + flushHeaders(outgoing); + await writeFromReadableStream(body, outgoing)?.catch((e) => handleResponseError(e, outgoing)); + } + outgoing[outgoingEnded]?.(); + return; + } + let hasContentLength = false; + let plainHeaders = false; + let canAutoLength = true; + if (header instanceof Headers) { + hasContentLength = header.has("content-length"); + header = buildOutgoingHttpHeaders(header, body === null ? void 0 : defaultContentType); + } else if (Array.isArray(header)) { + const headerObj = new Headers(header); + hasContentLength = headerObj.has("content-length"); + header = buildOutgoingHttpHeaders(headerObj, body === null ? void 0 : defaultContentType); + } else { + plainHeaders = true; + for (const key in header) { + if (key.length === 14 && key.toLowerCase() === "content-length") { + hasContentLength = true; + break; + } + if (key.length === 17 && key.toLowerCase() === "transfer-encoding" || key.length === 7 && key.toLowerCase() === "trailer") canAutoLength = false; + } + } + if (!hasContentLength) { + let length; + if (typeof body === "string") length = Buffer.byteLength(body); + else if (body instanceof Uint8Array) length = body.byteLength; + else if (body instanceof Blob) length = body.size; + if (length !== void 0 && (!plainHeaders || !canAutoLength || !trySetContentLength(outgoing, status, length))) { + if (plainHeaders) header = { ...header }; + header["Content-Length"] = length; + } + } + outgoing.writeHead(status, header); + if (body == null) outgoing.end(); + else if (typeof body === "string" || body instanceof Uint8Array) outgoing.end(body); + else if (body instanceof Blob) outgoing.end(new Uint8Array(await body.arrayBuffer())); + else { + flushHeaders(outgoing); + await writeFromReadableStream(body, outgoing)?.catch((e) => handleResponseError(e, outgoing)); + } + outgoing[outgoingEnded]?.(); +}; +const isPromise = (res) => typeof res.then === "function"; +const responseViaResponseObject = async (res, outgoing, options = {}) => { + if (isPromise(res)) if (options.errorHandler) try { + res = await res; + } catch (err) { + const errRes = await options.errorHandler(err); + if (!errRes) return; + res = errRes; + } + else res = await res.catch(handleFetchError); + if (cacheKey in res) return responseViaCache(res, outgoing); + const resHeaderRecord = buildOutgoingHttpHeaders(res.headers, res.body === null ? void 0 : defaultContentType); + if (res.body) { + const reader = res.body.getReader(); + const values = []; + let done = false; + let currentReadPromise = void 0; + if (resHeaderRecord["transfer-encoding"] !== "chunked") { + let maxReadCount = 2; + for (let i = 0; i < maxReadCount; i++) { + currentReadPromise ||= reader.read(); + const chunk = await readWithoutBlocking(currentReadPromise).catch((e) => { + console.error(e); + done = true; + }); + if (!chunk) { + if (i === 1) { + await new Promise((resolve) => setTimeout(resolve)); + maxReadCount = 3; + continue; + } + break; + } + currentReadPromise = void 0; + if (chunk.value) values.push(chunk.value); + if (chunk.done) { + done = true; + break; + } + } + if (done && !("content-length" in resHeaderRecord)) resHeaderRecord["content-length"] = values.reduce((acc, value) => acc + value.length, 0); + } + outgoing.writeHead(res.status, resHeaderRecord); + values.forEach((value) => { + outgoing.write(value); + }); + if (done) outgoing.end(); + else { + if (values.length === 0) flushHeaders(outgoing); + await writeFromReadableStreamDefaultReader(reader, outgoing, currentReadPromise); + } + } else if (resHeaderRecord[require_constants.X_ALREADY_SENT]) {} else { + outgoing.writeHead(res.status, resHeaderRecord); + outgoing.end(); + } + outgoing[outgoingEnded]?.(); +}; +const getRequestListener = (fetchCallback, options = {}) => { + const autoCleanupIncoming = options.autoCleanupIncoming ?? true; + if (options.overrideGlobalObjects !== false && global.Request !== Request$1) { + Object.defineProperty(global, "Request", { value: Request$1 }); + Object.defineProperty(global, "Response", { value: Response$1 }); + } + return async (incoming, outgoing) => { + let res, req; + let needsBodyCleanup = false; + let closeHandlerAttached = false; + const ensureCloseHandler = () => { + if (!req || closeHandlerAttached) return; + closeHandlerAttached = true; + outgoing.on("close", makeCloseHandler(req, incoming, outgoing, needsBodyCleanup)); + }; + try { + req = newRequest(incoming, options.hostname); + needsBodyCleanup = autoCleanupIncoming && !(incoming.method === "GET" || incoming.method === "HEAD"); + if (needsBodyCleanup) { + incoming[wrapBodyStream] = true; + if (incoming instanceof node_http2.Http2ServerRequest) outgoing[outgoingEnded] = () => { + if (!incoming.readableEnded) setTimeout(() => { + if (!incoming.readableEnded) setTimeout(() => { + incoming.destroy(); + outgoing.destroy(); + }); + }); + }; + } + res = fetchCallback(req, { + incoming, + outgoing + }); + if (!isPromise(res) && isImmediateCacheableResponse(res)) { + if (needsBodyCleanup && !incoming.readableEnded) outgoing.once("finish", () => { + if (!incoming.readableEnded) drainIncoming(incoming); + }); + return responseViaCache(res, outgoing); + } + ensureCloseHandler(); + } catch (e) { + if (!res) if (options.errorHandler) { + ensureCloseHandler(); + res = await options.errorHandler(req ? e : toRequestError(e)); + if (!res) return; + } else if (!req) res = handleRequestError(); + else res = handleFetchError(e); + else return handleResponseError(e, outgoing); + } + try { + return await responseViaResponseObject(res, outgoing, options); + } catch (e) { + return handleResponseError(e, outgoing); + } + }; +}; + +//#endregion +//#region src/websocket.ts +/** +* @link https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent +*/ +const CloseEvent = globalThis.CloseEvent ?? class extends Event { + #eventInitDict; + constructor(type, eventInitDict = {}) { + super(type, eventInitDict); + this.#eventInitDict = eventInitDict; + } + get wasClean() { + return this.#eventInitDict.wasClean ?? false; + } + get code() { + return this.#eventInitDict.code ?? 0; + } + get reason() { + return this.#eventInitDict.reason ?? ""; + } +}; +/** +* Node.js has no global `ErrorEvent`, unlike `Event`/`MessageEvent`/`CloseEvent`. +* @link https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent +*/ +const ErrorEvent = globalThis.ErrorEvent ?? class extends Event { + #eventInitDict; + constructor(type, eventInitDict = {}) { + super(type, eventInitDict); + this.#eventInitDict = eventInitDict; + } + get message() { + return this.#eventInitDict.message ?? ""; + } + get filename() { + return this.#eventInitDict.filename ?? ""; + } + get lineno() { + return this.#eventInitDict.lineno ?? 0; + } + get colno() { + return this.#eventInitDict.colno ?? 0; + } + get error() { + return this.#eventInitDict.error ?? null; + } +}; +const generateConnectionSymbol = () => Symbol("connection"); +const CONNECTION_SYMBOL_KEY = Symbol("CONNECTION_SYMBOL_KEY"); +const WAIT_FOR_WEBSOCKET_SYMBOL = Symbol("WAIT_FOR_WEBSOCKET_SYMBOL"); +const responseHeadersToSkip = new Set([ + "connection", + "content-length", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailer", + "transfer-encoding", + "upgrade", + "sec-websocket-accept", + "sec-websocket-extensions", + "sec-websocket-protocol" +]); +const appendResponseHeaders = (headers, responseHeaders) => { + if (!responseHeaders) return; + responseHeaders.forEach((value, key) => { + if (responseHeadersToSkip.has(key.toLowerCase())) return; + headers.push(`${key}: ${value}`); + }); +}; +const rejectUpgradeRequest = (socket, status, responseHeaders) => { + const responseLines = ["Connection: close", "Content-Length: 0"]; + appendResponseHeaders(responseLines, responseHeaders); + socket.end(`HTTP/1.1 ${status.toString()} ${node_http.STATUS_CODES[status] ?? ""}\r\n${responseLines.join("\r\n")}\r\n\r +`); +}; +const createUpgradeRequest = (request) => { + const protocol = request.socket.encrypted ? "https" : "http"; + const url = new URL(request.url ?? "/", `${protocol}://${request.headers.host ?? "localhost"}`); + const headers = new Headers(); + for (const key in request.headers) { + const value = request.headers[key]; + if (!value) continue; + headers.append(key, Array.isArray(value) ? value[0] : value); + } + return new Request(url, { headers }); +}; +const setupWebSocket = (options) => { + const { server, fetchCallback, wss } = options; + const waiterMap = /* @__PURE__ */ new Map(); + wss.on("connection", (ws, request) => { + const waiter = waiterMap.get(request); + if (waiter) { + waiter.resolve(ws); + waiterMap.delete(request); + } + }); + const rejectWaiter = (request) => { + const waiter = waiterMap.get(request); + if (waiter) { + waiterMap.delete(request); + waiter.reject(/* @__PURE__ */ new Error("WebSocket handshake aborted")); + } + }; + const waitForWebSocket = (request, connectionSymbol) => { + return new Promise((resolve, reject) => { + waiterMap.set(request, { + resolve, + reject, + connectionSymbol + }); + }); + }; + server.on("upgrade", async (request, socket, head) => { + if (request.headers.upgrade?.toLowerCase() !== "websocket") return; + const env = { + incoming: request, + outgoing: void 0, + wss, + [WAIT_FOR_WEBSOCKET_SYMBOL]: waitForWebSocket + }; + let status = 400; + let responseHeaders; + try { + const response = await fetchCallback(createUpgradeRequest(request), env); + if (response instanceof Response) { + status = response.status; + responseHeaders = response.headers; + } + } catch { + if (server.listenerCount("upgrade") === 1) rejectUpgradeRequest(socket, 500); + return; + } + const waiter = waiterMap.get(request); + if (!waiter || waiter.connectionSymbol !== env[CONNECTION_SYMBOL_KEY]) { + rejectWaiter(request); + if (server.listenerCount("upgrade") === 1) rejectUpgradeRequest(socket, status, responseHeaders); + return; + } + const addResponseHeaders = (headers) => { + appendResponseHeaders(headers, responseHeaders); + }; + const reclaimWaiterOnClose = () => rejectWaiter(request); + socket.once("close", reclaimWaiterOnClose); + wss.on("headers", addResponseHeaders); + try { + wss.handleUpgrade(request, socket, head, (ws) => { + socket.off("close", reclaimWaiterOnClose); + wss.emit("connection", ws, request); + }); + } finally { + wss.off("headers", addResponseHeaders); + } + }); + server.on("close", () => { + wss.close(); + }); +}; +const upgradeWebSocket = (0, hono_ws.defineWebSocketHelper)(async (c, events, options) => { + if (c.req.header("upgrade")?.toLowerCase() !== "websocket") return; + const env = c.env; + const waitForWebSocket = env[WAIT_FOR_WEBSOCKET_SYMBOL]; + if (!waitForWebSocket || !env.incoming) return new Response(null, { status: 500 }); + const connectionSymbol = generateConnectionSymbol(); + env[CONNECTION_SYMBOL_KEY] = connectionSymbol; + (async () => { + let ws; + try { + ws = await waitForWebSocket(env.incoming, connectionSymbol); + } catch { + return; + } + const messagesReceivedInStarting = []; + const bufferMessage = (data, isBinary) => { + messagesReceivedInStarting.push([data, isBinary]); + }; + ws.on("message", bufferMessage); + const ctx = { + binaryType: "arraybuffer", + close(code, reason) { + ws.close(code, reason); + }, + protocol: ws.protocol, + raw: ws, + get readyState() { + return ws.readyState; + }, + send(source, opts) { + ws.send(source, { compress: opts?.compress }); + }, + url: new URL(c.req.url) + }; + try { + events?.onOpen?.(new Event("open"), ctx); + } catch (e) { + (options?.onError ?? console.error)(e); + } + const handleMessage = (data, isBinary) => { + const datas = Array.isArray(data) ? data : [data]; + for (const data of datas) try { + events?.onMessage?.(new MessageEvent("message", { data: isBinary ? data instanceof ArrayBuffer ? data : data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) : typeof data === "string" ? data : Buffer.from(data).toString("utf-8") }), ctx); + } catch (e) { + (options?.onError ?? console.error)(e); + } + }; + ws.off("message", bufferMessage); + for (const message of messagesReceivedInStarting) handleMessage(...message); + ws.on("message", (data, isBinary) => { + handleMessage(data, isBinary); + }); + ws.on("close", (code, reason) => { + try { + events?.onClose?.(new CloseEvent("close", { + code, + reason: reason.toString() + }), ctx); + } catch (e) { + (options?.onError ?? console.error)(e); + } + }); + ws.on("error", (error) => { + try { + events?.onError?.(new ErrorEvent("error", { error }), ctx); + } catch (e) { + (options?.onError ?? console.error)(e); + } + }); + })(); + return new Response(); +}); + +//#endregion +//#region src/server.ts +const createAdaptorServer = (options) => { + const fetchCallback = options.fetch; + const requestListener = getRequestListener(fetchCallback, { + hostname: options.hostname, + overrideGlobalObjects: options.overrideGlobalObjects, + autoCleanupIncoming: options.autoCleanupIncoming + }); + const server = (options.createServer || node_http.createServer)(options.serverOptions || {}, requestListener); + if (options.websocket && options.websocket.server) { + if (options.websocket.server.options.noServer !== true) throw new Error("WebSocket server must be created with { noServer: true } option"); + setupWebSocket({ + server, + fetchCallback, + wss: options.websocket.server + }); + } + return server; +}; +const serve = (options, listeningListener) => { + const server = createAdaptorServer(options); + server.listen(options?.port ?? 3e3, options.hostname, () => { + const serverInfo = server.address(); + listeningListener && listeningListener(serverInfo); + }); + return server; +}; + +//#endregion +exports.RequestError = RequestError; +exports.createAdaptorServer = createAdaptorServer; +exports.getRequestListener = getRequestListener; +exports.serve = serve; +exports.upgradeWebSocket = upgradeWebSocket; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/index.d.cts b/tests/fixtures/npm/node_modules/@hono/node-server/dist/index.d.cts new file mode 100644 index 000000000..d3c55ce99 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/index.d.cts @@ -0,0 +1,101 @@ +import { AddressInfo } from "node:net"; +import { IncomingMessage, Server, ServerOptions, ServerResponse, createServer } from "node:http"; +import { Http2SecureServer, Http2Server, Http2ServerRequest, Http2ServerResponse, SecureServerOptions, ServerOptions as ServerOptions$1, createSecureServer, createServer as createServer$1 } from "node:http2"; +import { ServerOptions as ServerOptions$2, createServer as createServer$2 } from "node:https"; +import { Duplex } from "node:stream"; +import { UpgradeWebSocket } from "hono/ws"; + +//#region src/websocket-types.d.ts +type WSReadyState = 0 | 1 | 2 | 3; +type WebSocketData = string | ArrayBuffer | Uint8Array | readonly Uint8Array[]; +type WebSocketSendOptions = { + compress?: boolean; +}; +interface WebSocketLike { + protocol: string; + readyState: WSReadyState; + close(code?: number, reason?: string): void; + send(data: string | ArrayBuffer | ArrayBufferView, options?: WebSocketSendOptions): void; + on(event: 'message', listener: (data: WebSocketData, isBinary: boolean) => void): this; + on(event: 'close', listener: (code: number, reason: Uint8Array) => void): this; + on(event: 'error', listener: (error: unknown) => void): this; + off(event: 'message', listener: (data: WebSocketData, isBinary: boolean) => void): this; +} +interface WebSocketServerLike { + options: { + noServer?: boolean; + }; + on(event: 'connection', listener: (ws: WebSocketLike, request: IncomingMessage) => void): this; + on(event: 'headers', listener: (headers: string[]) => void): this; + off(event: 'headers', listener: (headers: string[]) => void): this; + emit(event: 'connection', ws: WebSocketLike, request: IncomingMessage): boolean; + handleUpgrade(request: IncomingMessage, socket: Duplex, head: Buffer, callback: (ws: WebSocketLike) => void): void; + close(): void; +} +//#endregion +//#region src/types.d.ts +type HttpBindings = { + incoming: IncomingMessage; + outgoing: ServerResponse; +}; +type Http2Bindings = { + incoming: Http2ServerRequest; + outgoing: Http2ServerResponse; +}; +type FetchCallback = (request: Request, env: HttpBindings | Http2Bindings) => Promise | unknown; +type ServerType = Server | Http2Server | Http2SecureServer; +type createHttpOptions = { + serverOptions?: ServerOptions; + createServer?: typeof createServer; +}; +type createHttpsOptions = { + serverOptions?: ServerOptions$2; + createServer?: typeof createServer$2; +}; +type createHttp2Options = { + serverOptions?: ServerOptions$1; + createServer?: typeof createServer$1; +}; +type createSecureHttp2Options = { + serverOptions?: SecureServerOptions; + createServer?: typeof createSecureServer; +}; +type ServerOptions$3 = createHttpOptions | createHttpsOptions | createHttp2Options | createSecureHttp2Options; +type Options = { + fetch: FetchCallback; + overrideGlobalObjects?: boolean; + autoCleanupIncoming?: boolean; + port?: number; + hostname?: string; + websocket?: { + server: WebSocketServerLike; + }; +} & ServerOptions$3; +type CustomErrorHandler = (err: unknown) => void | Response | Promise; +//#endregion +//#region src/server.d.ts +declare const createAdaptorServer: (options: Options) => ServerType; +declare const serve: (options: Options, listeningListener?: (info: AddressInfo) => void) => ServerType; +//#endregion +//#region src/websocket.d.ts +type UpgradeWebSocketOptions = { + onError: (err: unknown) => void; +}; +declare const upgradeWebSocket: UpgradeWebSocket; +//#endregion +//#region src/listener.d.ts +declare const getRequestListener: (fetchCallback: FetchCallback, options?: { + hostname?: string; + errorHandler?: CustomErrorHandler; + overrideGlobalObjects?: boolean; + autoCleanupIncoming?: boolean; +}) => (incoming: IncomingMessage | Http2ServerRequest, outgoing: ServerResponse | Http2ServerResponse) => Promise; +//#endregion +//#region src/error.d.ts +declare class RequestError extends Error { + constructor(message: string, options?: { + cause?: unknown; + }); +} +//#endregion +export { type Http2Bindings, type HttpBindings, RequestError, type ServerType, type WebSocketData, type WebSocketLike, type WebSocketServerLike, createAdaptorServer, getRequestListener, serve, upgradeWebSocket }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/index.d.mts b/tests/fixtures/npm/node_modules/@hono/node-server/dist/index.d.mts new file mode 100644 index 000000000..a815b86f0 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/index.d.mts @@ -0,0 +1,101 @@ +import { IncomingMessage, Server, ServerOptions, ServerResponse, createServer } from "node:http"; +import { Http2SecureServer, Http2Server, Http2ServerRequest, Http2ServerResponse, SecureServerOptions, ServerOptions as ServerOptions$1, createSecureServer, createServer as createServer$1 } from "node:http2"; +import { Duplex } from "node:stream"; +import { UpgradeWebSocket } from "hono/ws"; +import { AddressInfo } from "node:net"; +import { ServerOptions as ServerOptions$2, createServer as createServer$2 } from "node:https"; + +//#region src/websocket-types.d.ts +type WSReadyState = 0 | 1 | 2 | 3; +type WebSocketData = string | ArrayBuffer | Uint8Array | readonly Uint8Array[]; +type WebSocketSendOptions = { + compress?: boolean; +}; +interface WebSocketLike { + protocol: string; + readyState: WSReadyState; + close(code?: number, reason?: string): void; + send(data: string | ArrayBuffer | ArrayBufferView, options?: WebSocketSendOptions): void; + on(event: 'message', listener: (data: WebSocketData, isBinary: boolean) => void): this; + on(event: 'close', listener: (code: number, reason: Uint8Array) => void): this; + on(event: 'error', listener: (error: unknown) => void): this; + off(event: 'message', listener: (data: WebSocketData, isBinary: boolean) => void): this; +} +interface WebSocketServerLike { + options: { + noServer?: boolean; + }; + on(event: 'connection', listener: (ws: WebSocketLike, request: IncomingMessage) => void): this; + on(event: 'headers', listener: (headers: string[]) => void): this; + off(event: 'headers', listener: (headers: string[]) => void): this; + emit(event: 'connection', ws: WebSocketLike, request: IncomingMessage): boolean; + handleUpgrade(request: IncomingMessage, socket: Duplex, head: Buffer, callback: (ws: WebSocketLike) => void): void; + close(): void; +} +//#endregion +//#region src/types.d.ts +type HttpBindings = { + incoming: IncomingMessage; + outgoing: ServerResponse; +}; +type Http2Bindings = { + incoming: Http2ServerRequest; + outgoing: Http2ServerResponse; +}; +type FetchCallback = (request: Request, env: HttpBindings | Http2Bindings) => Promise | unknown; +type ServerType = Server | Http2Server | Http2SecureServer; +type createHttpOptions = { + serverOptions?: ServerOptions; + createServer?: typeof createServer; +}; +type createHttpsOptions = { + serverOptions?: ServerOptions$2; + createServer?: typeof createServer$2; +}; +type createHttp2Options = { + serverOptions?: ServerOptions$1; + createServer?: typeof createServer$1; +}; +type createSecureHttp2Options = { + serverOptions?: SecureServerOptions; + createServer?: typeof createSecureServer; +}; +type ServerOptions$3 = createHttpOptions | createHttpsOptions | createHttp2Options | createSecureHttp2Options; +type Options = { + fetch: FetchCallback; + overrideGlobalObjects?: boolean; + autoCleanupIncoming?: boolean; + port?: number; + hostname?: string; + websocket?: { + server: WebSocketServerLike; + }; +} & ServerOptions$3; +type CustomErrorHandler = (err: unknown) => void | Response | Promise; +//#endregion +//#region src/server.d.ts +declare const createAdaptorServer: (options: Options) => ServerType; +declare const serve: (options: Options, listeningListener?: (info: AddressInfo) => void) => ServerType; +//#endregion +//#region src/websocket.d.ts +type UpgradeWebSocketOptions = { + onError: (err: unknown) => void; +}; +declare const upgradeWebSocket: UpgradeWebSocket; +//#endregion +//#region src/listener.d.ts +declare const getRequestListener: (fetchCallback: FetchCallback, options?: { + hostname?: string; + errorHandler?: CustomErrorHandler; + overrideGlobalObjects?: boolean; + autoCleanupIncoming?: boolean; +}) => (incoming: IncomingMessage | Http2ServerRequest, outgoing: ServerResponse | Http2ServerResponse) => Promise; +//#endregion +//#region src/error.d.ts +declare class RequestError extends Error { + constructor(message: string, options?: { + cause?: unknown; + }); +} +//#endregion +export { type Http2Bindings, type HttpBindings, RequestError, type ServerType, type WebSocketData, type WebSocketLike, type WebSocketServerLike, createAdaptorServer, getRequestListener, serve, upgradeWebSocket }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/index.mjs b/tests/fixtures/npm/node_modules/@hono/node-server/dist/index.mjs new file mode 100644 index 000000000..4f5954d70 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/index.mjs @@ -0,0 +1,1335 @@ +import { t as X_ALREADY_SENT } from "./constants-BLSFu_RU.mjs"; +import { STATUS_CODES, ServerResponse, createServer } from "node:http"; +import { Http2ServerRequest, constants } from "node:http2"; +import { Readable } from "node:stream"; +import { defineWebSocketHelper } from "hono/ws"; + +//#region src/error.ts +var RequestError = class extends Error { + constructor(message, options) { + super(message, options); + this.name = "RequestError"; + } +}; + +//#endregion +//#region src/headers.ts +const nonJoinedHeaders = new Set([ + "age", + "authorization", + "content-length", + "content-type", + "etag", + "expires", + "from", + "host", + "if-modified-since", + "if-unmodified-since", + "last-modified", + "location", + "max-forwards", + "proxy-authorization", + "referer", + "retry-after", + "server", + "user-agent" +]); +const validHeaderName = /^[!#$%&'*+\-.^_`|~\dA-Za-z]+$/; +const isHttpWhitespace = (code) => code === 9 || code === 10 || code === 13 || code === 32; +const normalizeHeaderValue = (value) => { + if (!isHttpWhitespace(value.charCodeAt(0)) && !isHttpWhitespace(value.charCodeAt(value.length - 1))) return value; + let start = 0; + let end = value.length; + while (start < end && isHttpWhitespace(value.charCodeAt(start))) start++; + while (end > start && isHttpWhitespace(value.charCodeAt(end - 1))) end--; + return value.slice(start, end); +}; +const forbiddenHeaderValue = /[\0\r\n]/; +const GlobalHeaders = globalThis.Headers; +const materializeHeaders = (rawHeaders, HeadersCtor = GlobalHeaders) => { + const headers = new HeadersCtor(); + for (let i = 0; i < rawHeaders.length; i += 2) { + const name = rawHeaders[i]; + if (!name.startsWith(":")) headers.append(name, rawHeaders[i + 1]); + } + return headers; +}; +var RequestHeaders = class { + #incoming; + #rawHeaders; + #headers; + #invalidValue; + constructor(incoming) { + this.#incoming = incoming; + if (incoming instanceof Http2ServerRequest) this.#rawHeaders = incoming.rawHeaders.slice(); + } + get #lazyRawHeaders() { + return this.#rawHeaders ??= this.#incoming.rawHeaders.slice(); + } + get #native() { + if (!this.#headers) { + this.#headers = materializeHeaders(this.#lazyRawHeaders); + this.#rawHeaders = void 0; + } + return this.#headers; + } + #normalizedName(name) { + if (typeof name !== "string") return; + if (!validHeaderName.test(name)) throw new TypeError(`Invalid header name: ${name}`); + return name.toLowerCase(); + } + #lookupHttp1(lowerName) { + const headers = this.#incoming instanceof Http2ServerRequest ? void 0 : this.#incoming.headers; + if (!headers || nonJoinedHeaders.has(lowerName) || lowerName === "set-cookie" || lowerName === "__proto__") return; + if (!Object.hasOwn(headers, lowerName)) return null; + const rawValue = headers[lowerName]; + if (typeof rawValue === "string") { + const value = normalizeHeaderValue(rawValue); + return forbiddenHeaderValue.test(value) ? void 0 : value; + } + } + #lookup(rawHeaders, lowerName) { + const separator = lowerName === "cookie" ? "; " : ", "; + let value = null; + for (let i = 0; i < rawHeaders.length; i += 2) { + const rawName = rawHeaders[i]; + if (rawName.length === lowerName.length && rawName.toLowerCase() === lowerName) { + const rawValue = normalizeHeaderValue(rawHeaders[i + 1]); + if (forbiddenHeaderValue.test(rawValue)) { + this.#invalidValue = true; + return; + } + value = value === null ? rawValue : value + separator + rawValue; + } + } + return value; + } + append(name, value) { + this.#native.append(name, value); + } + delete(name) { + this.#native.delete(name); + } + get(name) { + const lowerName = this.#normalizedName(name); + if (lowerName && !this.#headers && !this.#invalidValue) { + const http1Value = this.#lookupHttp1(lowerName); + if (http1Value !== void 0) return http1Value; + const value = this.#lookup(this.#lazyRawHeaders, lowerName); + if (value !== void 0) return value; + } + return this.#native.get(name); + } + has(name) { + const lowerName = this.#normalizedName(name); + if (lowerName && !this.#headers && !this.#invalidValue) { + const http1Value = this.#lookupHttp1(lowerName); + if (http1Value !== void 0) return http1Value !== null; + const value = this.#lookup(this.#lazyRawHeaders, lowerName); + if (value !== void 0) return value !== null; + } + return this.#native.has(name); + } + set(name, value) { + this.#native.set(name, value); + } + getSetCookie() { + return this.#native.getSetCookie(); + } + keys() { + return this.#native.keys(); + } + values() { + return this.#native.values(); + } + entries() { + return this.#native.entries(); + } + forEach(callback, thisArg) { + this.#native.forEach((value, key) => { + callback.call(thisArg, value, key, this); + }); + } + [Symbol.iterator]() { + return this.entries(); + } +}; +Object.defineProperty(RequestHeaders.prototype, Symbol.for("nodejs.util.inspect.custom"), { value: function(depth, options, inspectFn) { + return `Headers (lightweight) ${inspectFn(Object.fromEntries(this), { + ...options, + depth: depth == null ? null : depth - 1 + })}`; +} }); +Object.setPrototypeOf(RequestHeaders.prototype, GlobalHeaders.prototype); +const newHeadersFromIncoming = (incoming) => globalThis.Headers === GlobalHeaders ? new RequestHeaders(incoming) : materializeHeaders(incoming.rawHeaders, globalThis.Headers); + +//#endregion +//#region src/url.ts +const reValidRequestUrl = /^\/[!#$&-;=?-\[\]_a-z~]*$/; +const reDotSegment = /\/\.\.?(?:[/?#]|$)/; +const reValidHost = /^[a-z0-9._-]+(?::(?:[1-5]\d{3,4}|[6-9]\d{3}))?$/; +const buildUrl = (scheme, host, incomingUrl) => { + const url = `${scheme}://${host}${incomingUrl}`; + if (!reValidHost.test(host)) { + const urlObj = new URL(url); + if (urlObj.hostname.length !== host.length && urlObj.hostname !== (host.includes(":") ? host.replace(/:\d+$/, "") : host).toLowerCase()) throw new RequestError("Invalid host header"); + return urlObj.href; + } else if (incomingUrl.length === 0) return url + "/"; + else { + if (incomingUrl.charCodeAt(0) !== 47) throw new RequestError("Invalid URL"); + if (!reValidRequestUrl.test(incomingUrl) || reDotSegment.test(incomingUrl)) return new URL(url).href; + return url; + } +}; + +//#endregion +//#region src/request.ts +const toRequestError = (e) => { + if (e instanceof RequestError) return e; + return new RequestError(e.message, { cause: e }); +}; +const GlobalRequest = global.Request; +var Request$1 = class extends GlobalRequest { + constructor(input, options) { + if (typeof input === "object" && getRequestCache in input) { + const hasReplacementBody = options !== void 0 && "body" in options && options.body != null; + if (input[bodyConsumedDirectlyKey] && !hasReplacementBody) throw new TypeError("Cannot construct a Request with a Request object that has already been used."); + input = input[getRequestCache](); + } + if (typeof (options?.body)?.getReader !== "undefined") options.duplex ??= "half"; + super(input, options); + } +}; +const wrapBodyStream = Symbol("wrapBodyStream"); +const byteExactEncodings = new Set([ + "latin1", + "binary", + "hex", + "base64", + "base64url" +]); +const isByteExactEncoding = (encoding) => encoding === null || byteExactEncodings.has(encoding); +const bodyBufferedBeforeDisconnectKey = Symbol("bodyBufferedBeforeDisconnect"); +const bodyBufferedLengthBeforeDisconnectKey = Symbol("bodyBufferedLengthBeforeDisconnect"); +const toBufferChunk = (chunk, encoding) => Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding ?? "utf8"); +const isRecoverableDisconnectedIncoming = (incoming) => !(incoming instanceof Http2ServerRequest) && !!incoming.complete && !!incoming.readableAborted && typeof incoming.read === "function" && isByteExactEncoding(incoming.readableEncoding); +const recordBodyBufferedBeforeDisconnect = (incoming) => { + if (incoming.readableDidRead || !isRecoverableDisconnectedIncoming(incoming)) return; + const incomingWithRecovery = incoming; + incomingWithRecovery[bodyBufferedLengthBeforeDisconnectKey] ??= incoming.readableLength; +}; +const readBodyBufferedBeforeDisconnect = (incoming, chunks) => { + if (incoming.readableDidRead && !chunks || !isRecoverableDisconnectedIncoming(incoming)) return; + const incomingWithRecovery = incoming; + if (incomingWithRecovery[bodyBufferedBeforeDisconnectKey] !== void 0) return incomingWithRecovery[bodyBufferedBeforeDisconnectKey]; + let result; + const errored = incoming.errored; + if (errored && errored.code !== "ECONNRESET") result = errored; + else if (incomingWithRecovery[bodyBufferedLengthBeforeDisconnectKey] !== void 0 && incoming.readableLength !== incomingWithRecovery[bodyBufferedLengthBeforeDisconnectKey]) result = newBodyUnusableError(); + else { + const bodyChunks = chunks ?? []; + const chunk = incoming.read(); + if (chunk !== null) bodyChunks.push(toBufferChunk(chunk, incoming.readableEncoding)); + const buffer = bodyChunks.length === 1 ? bodyChunks[0] : Buffer.concat(bodyChunks); + result = buffer; + const contentLength = incoming.headers["content-length"]; + if (typeof contentLength === "string" && /^\d+$/.test(contentLength)) { + const expectedLength = Number(contentLength); + if (Number.isSafeInteger(expectedLength) && buffer.length !== expectedLength) result = newBodyUnusableError(); + } + } + incomingWithRecovery[bodyBufferedBeforeDisconnectKey] = result; + return result; +}; +const enqueueBufferedBody = (controller, buffered) => { + if (buffered instanceof Error) { + controller.error(buffered); + return; + } + if (buffered.length > 0) controller.enqueue(buffered); + controller.close(); +}; +const newRequestFromIncoming = (method, url, headers, incoming, abortController) => { + const init = { + method, + headers, + signal: abortController.signal + }; + if (method === "TRACE") { + init.method = "GET"; + const req = new Request$1(url, init); + Object.defineProperty(req, "method", { get() { + return "TRACE"; + } }); + return req; + } + if (!(method === "GET" || method === "HEAD")) if ("rawBody" in incoming && incoming.rawBody instanceof Buffer) init.body = new ReadableStream({ start(controller) { + controller.enqueue(incoming.rawBody); + controller.close(); + } }); + else if (incoming[wrapBodyStream]) { + let reader; + init.body = new ReadableStream({ async pull(controller) { + try { + if (!reader) { + const buffered = readBodyBufferedBeforeDisconnect(incoming); + if (buffered !== void 0) { + enqueueBufferedBody(controller, buffered); + return; + } + } + reader ||= Readable.toWeb(incoming).getReader(); + const { done, value } = await reader.read(); + if (done) controller.close(); + else controller.enqueue(value); + } catch (error) { + controller.error(error); + } + } }); + } else { + const buffered = readBodyBufferedBeforeDisconnect(incoming); + if (buffered !== void 0) init.body = new ReadableStream({ start(controller) { + enqueueBufferedBody(controller, buffered); + } }); + else init.body = Readable.toWeb(incoming); + } + return new Request$1(url, init); +}; +const getRequestCache = Symbol("getRequestCache"); +const requestCache = Symbol("requestCache"); +const incomingKey = Symbol("incomingKey"); +const urlKey = Symbol("urlKey"); +const methodKey = Symbol("methodKey"); +const headersKey = Symbol("headersKey"); +const abortControllerKey = Symbol("abortControllerKey"); +const getAbortController = Symbol("getAbortController"); +const abortRequest = Symbol("abortRequest"); +const bodyBufferKey = Symbol("bodyBuffer"); +const bodyReadPromiseKey = Symbol("bodyReadPromise"); +const bodyConsumedDirectlyKey = Symbol("bodyConsumedDirectly"); +const bodyLockReaderKey = Symbol("bodyLockReader"); +const abortReasonKey = Symbol("abortReason"); +const newBodyUnusableError = () => { + return /* @__PURE__ */ new TypeError("Body is unusable"); +}; +const rejectBodyUnusable = () => { + return Promise.reject(newBodyUnusableError()); +}; +const textDecoder = new TextDecoder(); +const consumeBodyDirectOnce = (request) => { + if (request[bodyConsumedDirectlyKey]) return rejectBodyUnusable(); + request[bodyConsumedDirectlyKey] = true; +}; +const toArrayBuffer = (buf) => { + return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); +}; +const contentType = (request) => { + return (request[headersKey] ||= newHeadersFromIncoming(request[incomingKey])).get("content-type") || ""; +}; +const methodTokenRegExp = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/; +const normalizeIncomingMethod = (method) => { + if (typeof method !== "string" || method.length === 0) return "GET"; + switch (method) { + case "DELETE": + case "GET": + case "HEAD": + case "OPTIONS": + case "PATCH": + case "POST": + case "PUT": + case "QUERY": return method; + } + const upper = method.toUpperCase(); + switch (upper) { + case "DELETE": + case "GET": + case "HEAD": + case "OPTIONS": + case "POST": + case "PUT": return upper; + default: return method; + } +}; +const validateDirectReadMethod = (method) => { + if (!methodTokenRegExp.test(method)) return /* @__PURE__ */ new TypeError(`'${method}' is not a valid HTTP method.`); + const normalized = method.toUpperCase(); + if (normalized === "CONNECT" || normalized === "TRACK" || normalized === "TRACE" && method !== "TRACE") return /* @__PURE__ */ new TypeError(`'${method}' HTTP method is unsupported.`); +}; +const readBodyWithFastPath = (request, method, fromBuffer) => { + if (request[bodyConsumedDirectlyKey]) return rejectBodyUnusable(); + const methodName = request.method; + if (methodName === "GET" || methodName === "HEAD") return request[getRequestCache]()[method](); + const methodValidationError = validateDirectReadMethod(methodName); + if (methodValidationError) return Promise.reject(methodValidationError); + if (request[requestCache]) { + if (methodName !== "TRACE") return request[requestCache][method](); + } + const alreadyUsedError = consumeBodyDirectOnce(request); + if (alreadyUsedError) return alreadyUsedError; + const raw = readRawBodyIfAvailable(request); + if (raw) { + const result = Promise.resolve(fromBuffer(raw, request)); + request[bodyBufferKey] = void 0; + return result; + } + return readBodyDirect(request).then((buf) => { + const result = fromBuffer(buf, request); + request[bodyBufferKey] = void 0; + return result; + }); +}; +const readRawBodyIfAvailable = (request) => { + const incoming = request[incomingKey]; + if ("rawBody" in incoming && incoming.rawBody instanceof Buffer) return incoming.rawBody; +}; +const normalizeAbortError = (request, incoming) => { + if (incoming.errored) return incoming.errored; + const reason = request[abortReasonKey]; + if (reason !== void 0) return reason instanceof Error ? reason : new Error(String(reason)); + return /* @__PURE__ */ new Error("Client connection prematurely closed."); +}; +const readBodyDirect = (request) => { + if (request[bodyBufferKey]) return Promise.resolve(request[bodyBufferKey]); + if (request[bodyReadPromiseKey]) return request[bodyReadPromiseKey]; + const incoming = request[incomingKey]; + if (incoming.readableDidRead) return rejectBodyUnusable(); + const buffered = readBodyBufferedBeforeDisconnect(incoming); + if (buffered !== void 0) { + if (buffered instanceof Error) return Promise.reject(buffered); + request[bodyBufferKey] = buffered; + return Promise.resolve(buffered); + } + const promise = new Promise((resolve, reject) => { + const chunks = []; + let settled = false; + const finish = (callback) => { + if (settled) return; + settled = true; + cleanup(); + callback(); + }; + const recoverCompleteBodyAfterDisconnect = (error) => { + const streamError = incoming.errored ?? error; + if (!isRecoverableDisconnectedIncoming(incoming) || streamError && streamError.code !== "ECONNRESET") return false; + finish(() => { + const recovered = readBodyBufferedBeforeDisconnect(incoming, chunks); + if (recovered instanceof Error) reject(recovered); + else if (recovered === void 0) reject(error ?? normalizeAbortError(request, incoming)); + else { + request[bodyBufferKey] = recovered; + resolve(recovered); + } + }); + return true; + }; + const onData = (chunk) => { + chunks.push(toBufferChunk(chunk, incoming.readableEncoding)); + }; + const onEnd = () => { + finish(() => { + const buffer = chunks.length === 1 ? chunks[0] : Buffer.concat(chunks); + request[bodyBufferKey] = buffer; + resolve(buffer); + }); + }; + const onError = (error) => { + if (recoverCompleteBodyAfterDisconnect(error)) return; + finish(() => { + reject(error); + }); + }; + const onClose = () => { + if (incoming.readableEnded) { + onEnd(); + return; + } + if (recoverCompleteBodyAfterDisconnect()) return; + finish(() => { + reject(normalizeAbortError(request, incoming)); + }); + }; + const cleanup = () => { + incoming.off("data", onData); + incoming.off("end", onEnd); + incoming.off("error", onError); + incoming.off("close", onClose); + request[bodyReadPromiseKey] = void 0; + }; + incoming.on("data", onData); + incoming.on("end", onEnd); + incoming.on("error", onError); + incoming.on("close", onClose); + queueMicrotask(() => { + if (settled) return; + if (incoming.readableEnded) onEnd(); + else if (incoming.errored) onError(incoming.errored); + else if (incoming.destroyed) onClose(); + }); + }); + request[bodyReadPromiseKey] = promise; + return promise; +}; +const requestPrototype = { + get method() { + return this[methodKey]; + }, + get url() { + return this[urlKey]; + }, + get headers() { + return this[headersKey] ||= newHeadersFromIncoming(this[incomingKey]); + }, + [abortRequest](reason) { + if (this[abortReasonKey] === void 0) this[abortReasonKey] = reason; + const abortController = this[abortControllerKey]; + if (abortController && !abortController.signal.aborted) abortController.abort(reason); + }, + [getAbortController]() { + this[abortControllerKey] ||= new AbortController(); + if (this[abortReasonKey] !== void 0 && !this[abortControllerKey].signal.aborted) this[abortControllerKey].abort(this[abortReasonKey]); + return this[abortControllerKey]; + }, + [getRequestCache]() { + const abortController = this[getAbortController](); + if (this[requestCache]) return this[requestCache]; + const method = this.method; + if (this[bodyConsumedDirectlyKey] && !(method === "GET" || method === "HEAD")) { + this[bodyBufferKey] = void 0; + const init = { + method: method === "TRACE" ? "GET" : method, + headers: this.headers, + signal: abortController.signal + }; + if (method !== "TRACE") { + init.body = new ReadableStream({ start(c) { + c.close(); + } }); + init.duplex = "half"; + } + const req = new Request$1(this[urlKey], init); + if (method === "TRACE") Object.defineProperty(req, "method", { get() { + return "TRACE"; + } }); + return this[requestCache] = req; + } + return this[requestCache] = newRequestFromIncoming(this.method, this[urlKey], this.headers, this[incomingKey], abortController); + }, + get body() { + if (!this[bodyConsumedDirectlyKey]) return this[getRequestCache]().body; + const request = this[getRequestCache](); + if (!this[bodyLockReaderKey] && request.body) this[bodyLockReaderKey] = request.body.getReader(); + return request.body; + }, + get bodyUsed() { + if (this[bodyConsumedDirectlyKey]) return true; + if (this[requestCache]) return this[requestCache].bodyUsed; + return false; + } +}; +Object.defineProperty(requestPrototype, "signal", { get() { + return this[getAbortController]().signal; +} }); +[ + "cache", + "credentials", + "destination", + "integrity", + "mode", + "redirect", + "referrer", + "referrerPolicy", + "keepalive" +].forEach((k) => { + Object.defineProperty(requestPrototype, k, { get() { + return this[getRequestCache]()[k]; + } }); +}); +["clone", "formData"].forEach((k) => { + Object.defineProperty(requestPrototype, k, { value: function() { + if (this[bodyConsumedDirectlyKey]) { + if (k === "clone") throw newBodyUnusableError(); + return rejectBodyUnusable(); + } + return this[getRequestCache]()[k](); + } }); +}); +Object.defineProperty(requestPrototype, "text", { value: function() { + return readBodyWithFastPath(this, "text", (buf) => textDecoder.decode(buf)); +} }); +Object.defineProperty(requestPrototype, "arrayBuffer", { value: function() { + return readBodyWithFastPath(this, "arrayBuffer", (buf) => toArrayBuffer(buf)); +} }); +Object.defineProperty(requestPrototype, "blob", { value: function() { + return readBodyWithFastPath(this, "blob", (buf, request) => { + const type = contentType(request); + const init = type ? { headers: { "content-type": type } } : void 0; + return new Response(buf, init).blob(); + }); +} }); +Object.defineProperty(requestPrototype, "json", { value: function() { + if (this[bodyConsumedDirectlyKey]) return rejectBodyUnusable(); + return this.text().then(JSON.parse); +} }); +Object.defineProperty(requestPrototype, Symbol.for("nodejs.util.inspect.custom"), { value: function(depth, options, inspectFn) { + return `Request (lightweight) ${inspectFn({ + method: this.method, + url: this.url, + headers: this.headers, + nativeRequest: this[requestCache] + }, { + ...options, + depth: depth == null ? null : depth - 1 + })}`; +} }); +Object.setPrototypeOf(requestPrototype, Request$1.prototype); +const newRequest = (incoming, defaultHostname) => { + const req = Object.create(requestPrototype); + req[incomingKey] = incoming; + req[methodKey] = normalizeIncomingMethod(incoming.method); + const incomingUrl = incoming.url || ""; + if (incomingUrl[0] !== "/" && (incomingUrl.startsWith("http://") || incomingUrl.startsWith("https://"))) { + if (incoming instanceof Http2ServerRequest) throw new RequestError("Absolute URL for :path is not allowed in HTTP/2"); + try { + req[urlKey] = new URL(incomingUrl).href; + } catch (e) { + throw new RequestError("Invalid absolute URL", { cause: e }); + } + return req; + } + const host = (incoming instanceof Http2ServerRequest ? incoming.authority : incoming.headers.host) || defaultHostname; + if (!host) throw new RequestError("Missing host header"); + let scheme; + if (incoming instanceof Http2ServerRequest) { + scheme = incoming.scheme; + if (!(scheme === "http" || scheme === "https")) throw new RequestError("Unsupported scheme"); + } else scheme = incoming.socket && incoming.socket.encrypted ? "https" : "http"; + try { + req[urlKey] = buildUrl(scheme, host, incomingUrl); + } catch (e) { + if (e instanceof RequestError) throw e; + else throw new RequestError("Invalid URL", { cause: e }); + } + return req; +}; + +//#endregion +//#region src/response.ts +const defaultContentType = "text/plain; charset=UTF-8"; +const responseCache = Symbol("responseCache"); +const getResponseCache = Symbol("getResponseCache"); +const cacheKey = Symbol("cache"); +const GlobalResponse = global.Response; +var Response$1 = class Response$1 { + #body; + #init; + [getResponseCache]() { + const cache = this[cacheKey]; + const liveHeaders = cache && cache[2] instanceof Headers ? cache[2] : void 0; + delete this[cacheKey]; + return this[responseCache] ||= new GlobalResponse(this.#body, liveHeaders ? { + status: this.#init?.status, + statusText: this.#init?.statusText, + headers: liveHeaders + } : this.#init); + } + constructor(body, init) { + let headers; + this.#body = body; + if (init instanceof GlobalResponse) { + const cachedGlobalResponse = init[responseCache]; + if (cachedGlobalResponse) { + this.#init = cachedGlobalResponse; + this[getResponseCache](); + return; + } + this.#init = init instanceof Response$1 ? init.#init : init; + headers = new Headers(init.headers); + } else this.#init = init; + if (body == null || typeof body === "string" || typeof body?.getReader !== "undefined" || body instanceof Blob || body instanceof Uint8Array) this[cacheKey] = [ + init?.status || 200, + body ?? null, + headers || init?.headers + ]; + } + get headers() { + const cache = this[cacheKey]; + if (cache) { + if (!(cache[2] instanceof Headers)) cache[2] = new Headers(cache[2] || (cache[1] === null ? void 0 : { "content-type": defaultContentType })); + return cache[2]; + } + return this[getResponseCache]().headers; + } + get status() { + return this[cacheKey]?.[0] ?? this[getResponseCache]().status; + } + get ok() { + const status = this.status; + return status >= 200 && status < 300; + } +}; +[ + "body", + "bodyUsed", + "redirected", + "statusText", + "trailers", + "type", + "url" +].forEach((k) => { + Object.defineProperty(Response$1.prototype, k, { get() { + return this[getResponseCache]()[k]; + } }); +}); +[ + "arrayBuffer", + "blob", + "clone", + "formData", + "json", + "text" +].forEach((k) => { + Object.defineProperty(Response$1.prototype, k, { value: function() { + return this[getResponseCache]()[k](); + } }); +}); +Object.defineProperty(Response$1.prototype, Symbol.for("nodejs.util.inspect.custom"), { value: function(depth, options, inspectFn) { + return `Response (lightweight) ${inspectFn({ + status: this.status, + headers: this.headers, + ok: this.ok, + nativeResponse: this[responseCache] + }, { + ...options, + depth: depth == null ? null : depth - 1 + })}`; +} }); +Object.setPrototypeOf(Response$1, GlobalResponse); +Object.setPrototypeOf(Response$1.prototype, GlobalResponse.prototype); +const validRedirectUrl = /^https?:\/\/[!#-;=?-[\]_a-z~A-Z]+$/; +const parseRedirectUrl = (url) => { + if (url instanceof URL) return url.href; + if (validRedirectUrl.test(url)) return url; + return new URL(url).href; +}; +const validRedirectStatuses = new Set([ + 301, + 302, + 303, + 307, + 308 +]); +Object.defineProperty(Response$1, "redirect", { + value: function redirect(url, status = 302) { + if (!validRedirectStatuses.has(status)) throw new RangeError("Invalid status code"); + return new Response$1(null, { + status, + headers: { location: parseRedirectUrl(url) } + }); + }, + writable: true, + configurable: true +}); +Object.defineProperty(Response$1, "json", { + value: function json(data, init) { + const body = JSON.stringify(data); + if (body === void 0) throw new TypeError("The data is not JSON serializable"); + const initHeaders = init?.headers; + let headers; + if (initHeaders) { + headers = new Headers(initHeaders); + if (!headers.has("content-type")) headers.set("content-type", "application/json"); + } else headers = { "content-type": "application/json" }; + return new Response$1(body, { + status: init?.status ?? 200, + statusText: init?.statusText, + headers + }); + }, + writable: true, + configurable: true +}); + +//#endregion +//#region src/utils.ts +async function readWithoutBlocking(readPromise) { + return Promise.race([readPromise, Promise.resolve().then(() => Promise.resolve(void 0))]); +} +function writeFromReadableStreamDefaultReader(reader, writable, currentReadPromise) { + const cancel = (error) => { + reader.cancel(error).catch(() => {}); + }; + writable.on("close", cancel); + writable.on("error", cancel); + (currentReadPromise ?? reader.read()).then(flow, handleStreamError); + return reader.closed.finally(() => { + writable.off("close", cancel); + writable.off("error", cancel); + }); + function handleStreamError(error) { + if (error) writable.destroy(error); + } + function onDrain() { + reader.read().then(flow, handleStreamError); + } + function flow({ done, value }) { + try { + if (done) writable.end(); + else if (!writable.write(value)) writable.once("drain", onDrain); + else return reader.read().then(flow, handleStreamError); + } catch (e) { + handleStreamError(e); + } + } +} +function writeFromReadableStream(stream, writable) { + if (stream.locked) throw new TypeError("ReadableStream is locked."); + else if (writable.destroyed) return; + return writeFromReadableStreamDefaultReader(stream.getReader(), writable); +} +const buildOutgoingHttpHeaders = (headers, defaultContentType) => { + const res = {}; + if (!(headers instanceof Headers)) headers = new Headers(headers ?? void 0); + if (headers.has("set-cookie")) { + const cookies = []; + for (const [k, v] of headers) if (k === "set-cookie") cookies.push(v); + else res[k] = v; + if (cookies.length > 0) res["set-cookie"] = cookies; + } else for (const [k, v] of headers) res[k] = v; + if (defaultContentType) res["content-type"] ??= defaultContentType; + return res; +}; + +//#endregion +//#region src/listener.ts +const outgoingEnded = Symbol("outgoingEnded"); +const incomingDraining = Symbol("incomingDraining"); +const DRAIN_TIMEOUT_MS = 500; +const MAX_DRAIN_BYTES = 64 * 1024 * 1024; +const drainIncoming = (incoming) => { + const incomingWithDrainState = incoming; + if (incoming.destroyed || incomingWithDrainState[incomingDraining]) return; + incomingWithDrainState[incomingDraining] = true; + if (incoming instanceof Http2ServerRequest) { + try { + incoming.stream?.close?.(constants.NGHTTP2_NO_ERROR); + } catch {} + return; + } + let bytesRead = 0; + const cleanup = () => { + clearTimeout(timer); + incoming.off("data", onData); + incoming.off("end", cleanup); + incoming.off("error", cleanup); + }; + const forceClose = () => { + cleanup(); + const socket = incoming.socket; + if (socket && !socket.destroyed) { + if (typeof socket.destroySoon === "function") socket.destroySoon(); + else if (typeof socket.destroy === "function") socket.destroy(); + } + }; + const timer = setTimeout(forceClose, DRAIN_TIMEOUT_MS); + timer.unref?.(); + const onData = (chunk) => { + bytesRead += chunk.length; + if (bytesRead > MAX_DRAIN_BYTES) forceClose(); + }; + incoming.on("data", onData); + incoming.on("end", cleanup); + incoming.on("error", cleanup); + incoming.resume(); +}; +const makeCloseHandler = (req, incoming, outgoing, needsBodyCleanup) => () => { + if (incoming.errored) { + recordBodyBufferedBeforeDisconnect(incoming); + req[abortRequest](incoming.errored.toString()); + } else if (!outgoing.writableFinished || incoming instanceof Http2ServerRequest && incoming.aborted) { + recordBodyBufferedBeforeDisconnect(incoming); + req[abortRequest]("Client connection prematurely closed."); + } + if (needsBodyCleanup && !incoming.readableEnded) setTimeout(() => { + if (!incoming.readableEnded) setTimeout(() => { + drainIncoming(incoming); + }); + }); +}; +const isImmediateCacheableResponse = (res) => { + if (!(cacheKey in res)) return false; + const body = res[cacheKey][1]; + return body === null || typeof body === "string" || body instanceof Uint8Array; +}; +const handleRequestError = () => new Response(null, { status: 400 }); +const handleFetchError = (e) => new Response(null, { status: e instanceof Error && (e.name === "TimeoutError" || e.constructor.name === "TimeoutError") ? 504 : 500 }); +const handleResponseError = (e, outgoing) => { + const err = e instanceof Error ? e : new Error("unknown error", { cause: e }); + if (err.code === "ERR_STREAM_PREMATURE_CLOSE") console.info("The user aborted a request."); + else { + console.error(e); + if (!outgoing.headersSent) { + if (outgoing instanceof ServerResponse) outgoing._contentLength = null; + outgoing.writeHead(500, { "Content-Type": "text/plain" }); + } + outgoing.end(`Error: ${err.message}`); + outgoing.destroy(err); + } +}; +const flushHeaders = (outgoing) => { + if ("flushHeaders" in outgoing && outgoing.writable) outgoing.flushHeaders(); +}; +const trySetContentLength = (outgoing, status, length) => { + const http1 = outgoing; + if (http1._contentLength === null && http1._hasBody && http1.useChunkedEncodingByDefault && !http1._removedContLen && status >= 200 && status !== 204 && status !== 304 && !outgoing.hasHeader("content-length") && !outgoing.hasHeader("transfer-encoding") && !outgoing.hasHeader("trailer")) { + http1._contentLength = length; + return true; + } + return false; +}; +const writeDefaultHeaders = (outgoing, status, length) => { + if (trySetContentLength(outgoing, status, length)) outgoing.writeHead(status, { "Content-Type": defaultContentType }); + else outgoing.writeHead(status, { + "Content-Type": defaultContentType, + "Content-Length": length + }); +}; +const responseViaCache = async (res, outgoing) => { + let [status, body, header] = res[cacheKey]; + if (!header) { + if (body === null) { + outgoing.writeHead(status); + outgoing.end(); + } else if (typeof body === "string") { + writeDefaultHeaders(outgoing, status, Buffer.byteLength(body)); + outgoing.end(body); + } else if (body instanceof Uint8Array) { + writeDefaultHeaders(outgoing, status, body.byteLength); + outgoing.end(body); + } else if (body instanceof Blob) { + writeDefaultHeaders(outgoing, status, body.size); + outgoing.end(new Uint8Array(await body.arrayBuffer())); + } else { + outgoing.writeHead(status, { "Content-Type": defaultContentType }); + flushHeaders(outgoing); + await writeFromReadableStream(body, outgoing)?.catch((e) => handleResponseError(e, outgoing)); + } + outgoing[outgoingEnded]?.(); + return; + } + let hasContentLength = false; + let plainHeaders = false; + let canAutoLength = true; + if (header instanceof Headers) { + hasContentLength = header.has("content-length"); + header = buildOutgoingHttpHeaders(header, body === null ? void 0 : defaultContentType); + } else if (Array.isArray(header)) { + const headerObj = new Headers(header); + hasContentLength = headerObj.has("content-length"); + header = buildOutgoingHttpHeaders(headerObj, body === null ? void 0 : defaultContentType); + } else { + plainHeaders = true; + for (const key in header) { + if (key.length === 14 && key.toLowerCase() === "content-length") { + hasContentLength = true; + break; + } + if (key.length === 17 && key.toLowerCase() === "transfer-encoding" || key.length === 7 && key.toLowerCase() === "trailer") canAutoLength = false; + } + } + if (!hasContentLength) { + let length; + if (typeof body === "string") length = Buffer.byteLength(body); + else if (body instanceof Uint8Array) length = body.byteLength; + else if (body instanceof Blob) length = body.size; + if (length !== void 0 && (!plainHeaders || !canAutoLength || !trySetContentLength(outgoing, status, length))) { + if (plainHeaders) header = { ...header }; + header["Content-Length"] = length; + } + } + outgoing.writeHead(status, header); + if (body == null) outgoing.end(); + else if (typeof body === "string" || body instanceof Uint8Array) outgoing.end(body); + else if (body instanceof Blob) outgoing.end(new Uint8Array(await body.arrayBuffer())); + else { + flushHeaders(outgoing); + await writeFromReadableStream(body, outgoing)?.catch((e) => handleResponseError(e, outgoing)); + } + outgoing[outgoingEnded]?.(); +}; +const isPromise = (res) => typeof res.then === "function"; +const responseViaResponseObject = async (res, outgoing, options = {}) => { + if (isPromise(res)) if (options.errorHandler) try { + res = await res; + } catch (err) { + const errRes = await options.errorHandler(err); + if (!errRes) return; + res = errRes; + } + else res = await res.catch(handleFetchError); + if (cacheKey in res) return responseViaCache(res, outgoing); + const resHeaderRecord = buildOutgoingHttpHeaders(res.headers, res.body === null ? void 0 : defaultContentType); + if (res.body) { + const reader = res.body.getReader(); + const values = []; + let done = false; + let currentReadPromise = void 0; + if (resHeaderRecord["transfer-encoding"] !== "chunked") { + let maxReadCount = 2; + for (let i = 0; i < maxReadCount; i++) { + currentReadPromise ||= reader.read(); + const chunk = await readWithoutBlocking(currentReadPromise).catch((e) => { + console.error(e); + done = true; + }); + if (!chunk) { + if (i === 1) { + await new Promise((resolve) => setTimeout(resolve)); + maxReadCount = 3; + continue; + } + break; + } + currentReadPromise = void 0; + if (chunk.value) values.push(chunk.value); + if (chunk.done) { + done = true; + break; + } + } + if (done && !("content-length" in resHeaderRecord)) resHeaderRecord["content-length"] = values.reduce((acc, value) => acc + value.length, 0); + } + outgoing.writeHead(res.status, resHeaderRecord); + values.forEach((value) => { + outgoing.write(value); + }); + if (done) outgoing.end(); + else { + if (values.length === 0) flushHeaders(outgoing); + await writeFromReadableStreamDefaultReader(reader, outgoing, currentReadPromise); + } + } else if (resHeaderRecord[X_ALREADY_SENT]) {} else { + outgoing.writeHead(res.status, resHeaderRecord); + outgoing.end(); + } + outgoing[outgoingEnded]?.(); +}; +const getRequestListener = (fetchCallback, options = {}) => { + const autoCleanupIncoming = options.autoCleanupIncoming ?? true; + if (options.overrideGlobalObjects !== false && global.Request !== Request$1) { + Object.defineProperty(global, "Request", { value: Request$1 }); + Object.defineProperty(global, "Response", { value: Response$1 }); + } + return async (incoming, outgoing) => { + let res, req; + let needsBodyCleanup = false; + let closeHandlerAttached = false; + const ensureCloseHandler = () => { + if (!req || closeHandlerAttached) return; + closeHandlerAttached = true; + outgoing.on("close", makeCloseHandler(req, incoming, outgoing, needsBodyCleanup)); + }; + try { + req = newRequest(incoming, options.hostname); + needsBodyCleanup = autoCleanupIncoming && !(incoming.method === "GET" || incoming.method === "HEAD"); + if (needsBodyCleanup) { + incoming[wrapBodyStream] = true; + if (incoming instanceof Http2ServerRequest) outgoing[outgoingEnded] = () => { + if (!incoming.readableEnded) setTimeout(() => { + if (!incoming.readableEnded) setTimeout(() => { + incoming.destroy(); + outgoing.destroy(); + }); + }); + }; + } + res = fetchCallback(req, { + incoming, + outgoing + }); + if (!isPromise(res) && isImmediateCacheableResponse(res)) { + if (needsBodyCleanup && !incoming.readableEnded) outgoing.once("finish", () => { + if (!incoming.readableEnded) drainIncoming(incoming); + }); + return responseViaCache(res, outgoing); + } + ensureCloseHandler(); + } catch (e) { + if (!res) if (options.errorHandler) { + ensureCloseHandler(); + res = await options.errorHandler(req ? e : toRequestError(e)); + if (!res) return; + } else if (!req) res = handleRequestError(); + else res = handleFetchError(e); + else return handleResponseError(e, outgoing); + } + try { + return await responseViaResponseObject(res, outgoing, options); + } catch (e) { + return handleResponseError(e, outgoing); + } + }; +}; + +//#endregion +//#region src/websocket.ts +/** +* @link https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent +*/ +const CloseEvent = globalThis.CloseEvent ?? class extends Event { + #eventInitDict; + constructor(type, eventInitDict = {}) { + super(type, eventInitDict); + this.#eventInitDict = eventInitDict; + } + get wasClean() { + return this.#eventInitDict.wasClean ?? false; + } + get code() { + return this.#eventInitDict.code ?? 0; + } + get reason() { + return this.#eventInitDict.reason ?? ""; + } +}; +/** +* Node.js has no global `ErrorEvent`, unlike `Event`/`MessageEvent`/`CloseEvent`. +* @link https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent +*/ +const ErrorEvent = globalThis.ErrorEvent ?? class extends Event { + #eventInitDict; + constructor(type, eventInitDict = {}) { + super(type, eventInitDict); + this.#eventInitDict = eventInitDict; + } + get message() { + return this.#eventInitDict.message ?? ""; + } + get filename() { + return this.#eventInitDict.filename ?? ""; + } + get lineno() { + return this.#eventInitDict.lineno ?? 0; + } + get colno() { + return this.#eventInitDict.colno ?? 0; + } + get error() { + return this.#eventInitDict.error ?? null; + } +}; +const generateConnectionSymbol = () => Symbol("connection"); +const CONNECTION_SYMBOL_KEY = Symbol("CONNECTION_SYMBOL_KEY"); +const WAIT_FOR_WEBSOCKET_SYMBOL = Symbol("WAIT_FOR_WEBSOCKET_SYMBOL"); +const responseHeadersToSkip = new Set([ + "connection", + "content-length", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailer", + "transfer-encoding", + "upgrade", + "sec-websocket-accept", + "sec-websocket-extensions", + "sec-websocket-protocol" +]); +const appendResponseHeaders = (headers, responseHeaders) => { + if (!responseHeaders) return; + responseHeaders.forEach((value, key) => { + if (responseHeadersToSkip.has(key.toLowerCase())) return; + headers.push(`${key}: ${value}`); + }); +}; +const rejectUpgradeRequest = (socket, status, responseHeaders) => { + const responseLines = ["Connection: close", "Content-Length: 0"]; + appendResponseHeaders(responseLines, responseHeaders); + socket.end(`HTTP/1.1 ${status.toString()} ${STATUS_CODES[status] ?? ""}\r\n${responseLines.join("\r\n")}\r\n\r +`); +}; +const createUpgradeRequest = (request) => { + const protocol = request.socket.encrypted ? "https" : "http"; + const url = new URL(request.url ?? "/", `${protocol}://${request.headers.host ?? "localhost"}`); + const headers = new Headers(); + for (const key in request.headers) { + const value = request.headers[key]; + if (!value) continue; + headers.append(key, Array.isArray(value) ? value[0] : value); + } + return new Request(url, { headers }); +}; +const setupWebSocket = (options) => { + const { server, fetchCallback, wss } = options; + const waiterMap = /* @__PURE__ */ new Map(); + wss.on("connection", (ws, request) => { + const waiter = waiterMap.get(request); + if (waiter) { + waiter.resolve(ws); + waiterMap.delete(request); + } + }); + const rejectWaiter = (request) => { + const waiter = waiterMap.get(request); + if (waiter) { + waiterMap.delete(request); + waiter.reject(/* @__PURE__ */ new Error("WebSocket handshake aborted")); + } + }; + const waitForWebSocket = (request, connectionSymbol) => { + return new Promise((resolve, reject) => { + waiterMap.set(request, { + resolve, + reject, + connectionSymbol + }); + }); + }; + server.on("upgrade", async (request, socket, head) => { + if (request.headers.upgrade?.toLowerCase() !== "websocket") return; + const env = { + incoming: request, + outgoing: void 0, + wss, + [WAIT_FOR_WEBSOCKET_SYMBOL]: waitForWebSocket + }; + let status = 400; + let responseHeaders; + try { + const response = await fetchCallback(createUpgradeRequest(request), env); + if (response instanceof Response) { + status = response.status; + responseHeaders = response.headers; + } + } catch { + if (server.listenerCount("upgrade") === 1) rejectUpgradeRequest(socket, 500); + return; + } + const waiter = waiterMap.get(request); + if (!waiter || waiter.connectionSymbol !== env[CONNECTION_SYMBOL_KEY]) { + rejectWaiter(request); + if (server.listenerCount("upgrade") === 1) rejectUpgradeRequest(socket, status, responseHeaders); + return; + } + const addResponseHeaders = (headers) => { + appendResponseHeaders(headers, responseHeaders); + }; + const reclaimWaiterOnClose = () => rejectWaiter(request); + socket.once("close", reclaimWaiterOnClose); + wss.on("headers", addResponseHeaders); + try { + wss.handleUpgrade(request, socket, head, (ws) => { + socket.off("close", reclaimWaiterOnClose); + wss.emit("connection", ws, request); + }); + } finally { + wss.off("headers", addResponseHeaders); + } + }); + server.on("close", () => { + wss.close(); + }); +}; +const upgradeWebSocket = defineWebSocketHelper(async (c, events, options) => { + if (c.req.header("upgrade")?.toLowerCase() !== "websocket") return; + const env = c.env; + const waitForWebSocket = env[WAIT_FOR_WEBSOCKET_SYMBOL]; + if (!waitForWebSocket || !env.incoming) return new Response(null, { status: 500 }); + const connectionSymbol = generateConnectionSymbol(); + env[CONNECTION_SYMBOL_KEY] = connectionSymbol; + (async () => { + let ws; + try { + ws = await waitForWebSocket(env.incoming, connectionSymbol); + } catch { + return; + } + const messagesReceivedInStarting = []; + const bufferMessage = (data, isBinary) => { + messagesReceivedInStarting.push([data, isBinary]); + }; + ws.on("message", bufferMessage); + const ctx = { + binaryType: "arraybuffer", + close(code, reason) { + ws.close(code, reason); + }, + protocol: ws.protocol, + raw: ws, + get readyState() { + return ws.readyState; + }, + send(source, opts) { + ws.send(source, { compress: opts?.compress }); + }, + url: new URL(c.req.url) + }; + try { + events?.onOpen?.(new Event("open"), ctx); + } catch (e) { + (options?.onError ?? console.error)(e); + } + const handleMessage = (data, isBinary) => { + const datas = Array.isArray(data) ? data : [data]; + for (const data of datas) try { + events?.onMessage?.(new MessageEvent("message", { data: isBinary ? data instanceof ArrayBuffer ? data : data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) : typeof data === "string" ? data : Buffer.from(data).toString("utf-8") }), ctx); + } catch (e) { + (options?.onError ?? console.error)(e); + } + }; + ws.off("message", bufferMessage); + for (const message of messagesReceivedInStarting) handleMessage(...message); + ws.on("message", (data, isBinary) => { + handleMessage(data, isBinary); + }); + ws.on("close", (code, reason) => { + try { + events?.onClose?.(new CloseEvent("close", { + code, + reason: reason.toString() + }), ctx); + } catch (e) { + (options?.onError ?? console.error)(e); + } + }); + ws.on("error", (error) => { + try { + events?.onError?.(new ErrorEvent("error", { error }), ctx); + } catch (e) { + (options?.onError ?? console.error)(e); + } + }); + })(); + return new Response(); +}); + +//#endregion +//#region src/server.ts +const createAdaptorServer = (options) => { + const fetchCallback = options.fetch; + const requestListener = getRequestListener(fetchCallback, { + hostname: options.hostname, + overrideGlobalObjects: options.overrideGlobalObjects, + autoCleanupIncoming: options.autoCleanupIncoming + }); + const server = (options.createServer || createServer)(options.serverOptions || {}, requestListener); + if (options.websocket && options.websocket.server) { + if (options.websocket.server.options.noServer !== true) throw new Error("WebSocket server must be created with { noServer: true } option"); + setupWebSocket({ + server, + fetchCallback, + wss: options.websocket.server + }); + } + return server; +}; +const serve = (options, listeningListener) => { + const server = createAdaptorServer(options); + server.listen(options?.port ?? 3e3, options.hostname, () => { + const serverInfo = server.address(); + listeningListener && listeningListener(serverInfo); + }); + return server; +}; + +//#endregion +export { RequestError, createAdaptorServer, getRequestListener, serve, upgradeWebSocket }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.cjs b/tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.cjs new file mode 100644 index 000000000..bcfb0e52e --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.cjs @@ -0,0 +1,153 @@ +Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +const require_utils_stream = require('./utils/stream.cjs'); +let hono_utils_mime = require("hono/utils/mime"); +let node_fs = require("node:fs"); +let node_path = require("node:path"); + +//#region src/serve-static.ts +const COMPRESSIBLE_CONTENT_TYPE_REGEX = /^\s*(?:text\/[^;\s]+|application\/(?:javascript|json|xml|xml-dtd|ecmascript|dart|postscript|rtf|tar|toml|vnd\.dart|vnd\.ms-fontobject|vnd\.ms-opentype|wasm|x-httpd-php|x-javascript|x-ns-proxy-autoconfig|x-sh|x-tar|x-virtualbox-hdd|x-virtualbox-ova|x-virtualbox-ovf|x-virtualbox-vbox|x-virtualbox-vdi|x-virtualbox-vhd|x-virtualbox-vmdk|x-www-form-urlencoded)|font\/(?:otf|ttf)|image\/(?:bmp|vnd\.adobe\.photoshop|vnd\.microsoft\.icon|vnd\.ms-dds|x-icon|x-ms-bmp)|message\/rfc822|model\/gltf-binary|x-shader\/x-fragment|x-shader\/x-vertex|[^;\s]+?\+(?:json|text|xml|yaml))(?:[;\s]|$)/i; +const ENCODINGS = { + br: ".br", + zstd: ".zst", + gzip: ".gz" +}; +const ENCODINGS_ORDERED_KEYS = Object.keys(ENCODINGS); +const getStats = (path) => { + let stats; + try { + stats = (0, node_fs.statSync)(path); + } catch {} + return stats; +}; +const BYTE_RANGE_PATTERN = /^(?:bytes=)?(?!-$)(\d*)-(\d*)$/; +const parseByteRange = (range) => { + const match = range.match(BYTE_RANGE_PATTERN); + if (!match) return; + const [, start, end] = match; + if (start === "") return { + type: "suffix", + length: Number(end) + }; + if (end === "") return { + type: "open-ended", + start: Number(start) + }; + return { + type: "bounded", + start: Number(start), + end: Number(end) + }; +}; +const resolveByteRange = (spec, size) => { + if (size === 0) return; + if (spec.type === "suffix") { + if (spec.length === 0) return; + return { + start: Math.max(size - spec.length, 0), + end: size - 1 + }; + } + const end = spec.type === "bounded" ? Math.min(spec.end, size - 1) : size - 1; + if (spec.start >= size || spec.start > end) return; + return { + start: spec.start, + end + }; +}; +const tryDecode = (str, decoder) => { + try { + return decoder(str); + } catch { + return str.replace(/(?:%[0-9A-Fa-f]{2})+/g, (match) => { + try { + return decoder(match); + } catch { + return match; + } + }); + } +}; +const tryDecodeURI = (str) => tryDecode(str, decodeURI); +const serveStatic = (options = { root: "" }) => { + const root = options.root || ""; + const optionPath = options.path; + if (root !== "" && !(0, node_fs.existsSync)(root)) console.error(`serveStatic: root path '${root}' is not found, are you sure it's correct?`); + return async (c, next) => { + if (c.finalized) return next(); + if (c.req.method !== "GET" && c.req.method !== "HEAD") return next(); + let filename; + if (optionPath) filename = optionPath; + else try { + if (!options.allowPercentInPath && c.req.path.includes("%")) throw new Error(); + filename = tryDecodeURI(c.req.path); + if (/(?:^|[\/\\])\.{1,2}(?:$|[\/\\])|[\/\\]{2,}|\\/.test(filename)) throw new Error(); + } catch { + await options.onNotFound?.(c.req.path, c); + return next(); + } + let path = (0, node_path.join)(root, !optionPath && options.rewriteRequestPath ? options.rewriteRequestPath(filename, c) : filename); + let stats = getStats(path); + if (stats && stats.isDirectory()) { + const indexFile = options.index ?? "index.html"; + path = (0, node_path.join)(path, indexFile); + stats = getStats(path); + } + if (!stats) { + await options.onNotFound?.(path, c); + return next(); + } + const mimeType = (0, hono_utils_mime.getMimeType)(path); + c.header("Content-Type", mimeType || "application/octet-stream"); + if (options.precompressed && (!mimeType || mimeType === "application/octet-stream" || COMPRESSIBLE_CONTENT_TYPE_REGEX.test(mimeType))) { + const acceptEncodingSet = new Set(c.req.header("Accept-Encoding")?.split(",").map((encoding) => encoding.trim())); + for (const encoding of ENCODINGS_ORDERED_KEYS) { + if (!acceptEncodingSet.has(encoding)) continue; + const precompressedStats = getStats(path + ENCODINGS[encoding]); + if (precompressedStats) { + c.header("Content-Encoding", encoding); + c.header("Vary", "Accept-Encoding", { append: true }); + stats = precompressedStats; + path = path + ENCODINGS[encoding]; + break; + } + } + } + let result; + const size = stats.size; + const range = c.req.header("range") || ""; + c.header("Last-Modified", stats.mtime.toUTCString()); + if (c.req.method === "HEAD") { + c.header("Content-Length", size.toString()); + c.status(200); + result = c.body(null); + } else if (!range) { + c.header("Content-Length", size.toString()); + result = c.body(require_utils_stream.createStreamBody((0, node_fs.createReadStream)(path)), 200); + } else { + c.header("Accept-Ranges", "bytes"); + const resolvedRange = resolveByteRange(parseByteRange(range) ?? { + type: "open-ended", + start: 0 + }, size); + if (!resolvedRange) { + c.header("Content-Range", `bytes */${size}`); + result = c.body(null, 416); + } else { + const { start, end } = resolvedRange; + const chunkSize = end - start + 1; + const stream = (0, node_fs.createReadStream)(path, { + start, + end + }); + c.header("Content-Length", chunkSize.toString()); + c.header("Content-Range", `bytes ${start}-${end}/${size}`); + result = c.body(require_utils_stream.createStreamBody(stream), 206); + } + } + await options.onFound?.(path, c); + return result; + }; +}; + +//#endregion +exports.serveStatic = serveStatic; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.d.cts b/tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.d.cts new file mode 100644 index 000000000..f4dae1201 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.d.cts @@ -0,0 +1,19 @@ +import { Context, Env, MiddlewareHandler } from "hono"; + +//#region src/serve-static.d.ts +type ServeStaticOptions = { + /** + * Root path. Relative path is based on current working directory from which the app was started. + */ + root?: string; + path?: string; + index?: string; + precompressed?: boolean; /** Allow percent signs in the routed request path. Defaults to false. */ + allowPercentInPath?: boolean; + rewriteRequestPath?: (path: string, c: Context) => string; + onFound?: (path: string, c: Context) => void | Promise; + onNotFound?: (path: string, c: Context) => void | Promise; +}; +declare const serveStatic: (options?: ServeStaticOptions) => MiddlewareHandler; +//#endregion +export { ServeStaticOptions, serveStatic }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.d.mts b/tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.d.mts new file mode 100644 index 000000000..f4dae1201 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.d.mts @@ -0,0 +1,19 @@ +import { Context, Env, MiddlewareHandler } from "hono"; + +//#region src/serve-static.d.ts +type ServeStaticOptions = { + /** + * Root path. Relative path is based on current working directory from which the app was started. + */ + root?: string; + path?: string; + index?: string; + precompressed?: boolean; /** Allow percent signs in the routed request path. Defaults to false. */ + allowPercentInPath?: boolean; + rewriteRequestPath?: (path: string, c: Context) => string; + onFound?: (path: string, c: Context) => void | Promise; + onNotFound?: (path: string, c: Context) => void | Promise; +}; +declare const serveStatic: (options?: ServeStaticOptions) => MiddlewareHandler; +//#endregion +export { ServeStaticOptions, serveStatic }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.mjs b/tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.mjs new file mode 100644 index 000000000..97505a187 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.mjs @@ -0,0 +1,152 @@ +import { createStreamBody } from "./utils/stream.mjs"; +import { getMimeType } from "hono/utils/mime"; +import { createReadStream, existsSync, statSync } from "node:fs"; +import { join } from "node:path"; + +//#region src/serve-static.ts +const COMPRESSIBLE_CONTENT_TYPE_REGEX = /^\s*(?:text\/[^;\s]+|application\/(?:javascript|json|xml|xml-dtd|ecmascript|dart|postscript|rtf|tar|toml|vnd\.dart|vnd\.ms-fontobject|vnd\.ms-opentype|wasm|x-httpd-php|x-javascript|x-ns-proxy-autoconfig|x-sh|x-tar|x-virtualbox-hdd|x-virtualbox-ova|x-virtualbox-ovf|x-virtualbox-vbox|x-virtualbox-vdi|x-virtualbox-vhd|x-virtualbox-vmdk|x-www-form-urlencoded)|font\/(?:otf|ttf)|image\/(?:bmp|vnd\.adobe\.photoshop|vnd\.microsoft\.icon|vnd\.ms-dds|x-icon|x-ms-bmp)|message\/rfc822|model\/gltf-binary|x-shader\/x-fragment|x-shader\/x-vertex|[^;\s]+?\+(?:json|text|xml|yaml))(?:[;\s]|$)/i; +const ENCODINGS = { + br: ".br", + zstd: ".zst", + gzip: ".gz" +}; +const ENCODINGS_ORDERED_KEYS = Object.keys(ENCODINGS); +const getStats = (path) => { + let stats; + try { + stats = statSync(path); + } catch {} + return stats; +}; +const BYTE_RANGE_PATTERN = /^(?:bytes=)?(?!-$)(\d*)-(\d*)$/; +const parseByteRange = (range) => { + const match = range.match(BYTE_RANGE_PATTERN); + if (!match) return; + const [, start, end] = match; + if (start === "") return { + type: "suffix", + length: Number(end) + }; + if (end === "") return { + type: "open-ended", + start: Number(start) + }; + return { + type: "bounded", + start: Number(start), + end: Number(end) + }; +}; +const resolveByteRange = (spec, size) => { + if (size === 0) return; + if (spec.type === "suffix") { + if (spec.length === 0) return; + return { + start: Math.max(size - spec.length, 0), + end: size - 1 + }; + } + const end = spec.type === "bounded" ? Math.min(spec.end, size - 1) : size - 1; + if (spec.start >= size || spec.start > end) return; + return { + start: spec.start, + end + }; +}; +const tryDecode = (str, decoder) => { + try { + return decoder(str); + } catch { + return str.replace(/(?:%[0-9A-Fa-f]{2})+/g, (match) => { + try { + return decoder(match); + } catch { + return match; + } + }); + } +}; +const tryDecodeURI = (str) => tryDecode(str, decodeURI); +const serveStatic = (options = { root: "" }) => { + const root = options.root || ""; + const optionPath = options.path; + if (root !== "" && !existsSync(root)) console.error(`serveStatic: root path '${root}' is not found, are you sure it's correct?`); + return async (c, next) => { + if (c.finalized) return next(); + if (c.req.method !== "GET" && c.req.method !== "HEAD") return next(); + let filename; + if (optionPath) filename = optionPath; + else try { + if (!options.allowPercentInPath && c.req.path.includes("%")) throw new Error(); + filename = tryDecodeURI(c.req.path); + if (/(?:^|[\/\\])\.{1,2}(?:$|[\/\\])|[\/\\]{2,}|\\/.test(filename)) throw new Error(); + } catch { + await options.onNotFound?.(c.req.path, c); + return next(); + } + let path = join(root, !optionPath && options.rewriteRequestPath ? options.rewriteRequestPath(filename, c) : filename); + let stats = getStats(path); + if (stats && stats.isDirectory()) { + const indexFile = options.index ?? "index.html"; + path = join(path, indexFile); + stats = getStats(path); + } + if (!stats) { + await options.onNotFound?.(path, c); + return next(); + } + const mimeType = getMimeType(path); + c.header("Content-Type", mimeType || "application/octet-stream"); + if (options.precompressed && (!mimeType || mimeType === "application/octet-stream" || COMPRESSIBLE_CONTENT_TYPE_REGEX.test(mimeType))) { + const acceptEncodingSet = new Set(c.req.header("Accept-Encoding")?.split(",").map((encoding) => encoding.trim())); + for (const encoding of ENCODINGS_ORDERED_KEYS) { + if (!acceptEncodingSet.has(encoding)) continue; + const precompressedStats = getStats(path + ENCODINGS[encoding]); + if (precompressedStats) { + c.header("Content-Encoding", encoding); + c.header("Vary", "Accept-Encoding", { append: true }); + stats = precompressedStats; + path = path + ENCODINGS[encoding]; + break; + } + } + } + let result; + const size = stats.size; + const range = c.req.header("range") || ""; + c.header("Last-Modified", stats.mtime.toUTCString()); + if (c.req.method === "HEAD") { + c.header("Content-Length", size.toString()); + c.status(200); + result = c.body(null); + } else if (!range) { + c.header("Content-Length", size.toString()); + result = c.body(createStreamBody(createReadStream(path)), 200); + } else { + c.header("Accept-Ranges", "bytes"); + const resolvedRange = resolveByteRange(parseByteRange(range) ?? { + type: "open-ended", + start: 0 + }, size); + if (!resolvedRange) { + c.header("Content-Range", `bytes */${size}`); + result = c.body(null, 416); + } else { + const { start, end } = resolvedRange; + const chunkSize = end - start + 1; + const stream = createReadStream(path, { + start, + end + }); + c.header("Content-Length", chunkSize.toString()); + c.header("Content-Range", `bytes ${start}-${end}/${size}`); + result = c.body(createStreamBody(stream), 206); + } + } + await options.onFound?.(path, c); + return result; + }; +}; + +//#endregion +export { serveStatic }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.cjs b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.cjs new file mode 100644 index 000000000..87b8e95ab --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.cjs @@ -0,0 +1,8 @@ +Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +const require_constants = require('../constants-BXAKTxRC.cjs'); + +//#region src/utils/response.ts +const RESPONSE_ALREADY_SENT = new Response(null, { headers: { [require_constants.X_ALREADY_SENT]: "true" } }); + +//#endregion +exports.RESPONSE_ALREADY_SENT = RESPONSE_ALREADY_SENT; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.d.cts b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.d.cts new file mode 100644 index 000000000..169a3b551 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.d.cts @@ -0,0 +1,4 @@ +//#region src/utils/response.d.ts +declare const RESPONSE_ALREADY_SENT: Response; +//#endregion +export { RESPONSE_ALREADY_SENT }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.d.mts b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.d.mts new file mode 100644 index 000000000..169a3b551 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.d.mts @@ -0,0 +1,4 @@ +//#region src/utils/response.d.ts +declare const RESPONSE_ALREADY_SENT: Response; +//#endregion +export { RESPONSE_ALREADY_SENT }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.mjs b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.mjs new file mode 100644 index 000000000..92495c1f5 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.mjs @@ -0,0 +1,7 @@ +import { t as X_ALREADY_SENT } from "../constants-BLSFu_RU.mjs"; + +//#region src/utils/response.ts +const RESPONSE_ALREADY_SENT = new Response(null, { headers: { [X_ALREADY_SENT]: "true" } }); + +//#endregion +export { RESPONSE_ALREADY_SENT }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.cjs b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.cjs new file mode 100644 index 000000000..b4f4f7d42 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.cjs @@ -0,0 +1,65 @@ +Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +let node_stream = require("node:stream"); +let node_process = require("node:process"); + +//#region src/utils/stream.ts +const pr54206Applied = () => { + const [major, minor] = node_process.versions.node.split(".").map((component) => parseInt(component)); + return major >= 23 || major === 22 && minor >= 7 || major === 20 && minor >= 18; +}; +const useReadableToWeb = pr54206Applied(); +const createStreamBody = (stream, useNativeReadableToWeb = useReadableToWeb) => { + if (useNativeReadableToWeb) return node_stream.Readable.toWeb(stream); + let controller; + let settled = false; + const cleanup = () => { + stream.off("data", onData); + stream.off("error", onError); + stream.off("end", onTerminate); + stream.off("close", onTerminate); + }; + const settle = (callback) => { + if (settled) return; + settled = true; + cleanup(); + callback?.(); + }; + const onData = (chunk) => { + if (settled || !controller) return; + controller.enqueue(chunk); + if ((controller.desiredSize ?? 0) <= 0) stream.pause(); + }; + const onError = (error) => { + settle(() => { + controller?.error(error); + }); + }; + const onTerminate = () => { + settle(() => { + controller?.close(); + }); + }; + return new ReadableStream({ + start(streamController) { + controller = streamController; + stream.on("data", onData); + stream.on("error", onError); + stream.on("end", onTerminate); + stream.on("close", onTerminate); + stream.pause(); + }, + pull() { + if (!settled) stream.resume(); + }, + cancel() { + settle(); + const ignoreError = () => {}; + stream.on("error", ignoreError); + stream.once("close", () => stream.off("error", ignoreError)); + stream.destroy(); + } + }); +}; + +//#endregion +exports.createStreamBody = createStreamBody; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.d.cts b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.d.cts new file mode 100644 index 000000000..5f92c4f79 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.d.cts @@ -0,0 +1,6 @@ +import { ReadStream } from "node:fs"; + +//#region src/utils/stream.d.ts +declare const createStreamBody: (stream: ReadStream, useNativeReadableToWeb?: boolean) => ReadableStream; +//#endregion +export { createStreamBody }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.d.mts b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.d.mts new file mode 100644 index 000000000..5f92c4f79 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.d.mts @@ -0,0 +1,6 @@ +import { ReadStream } from "node:fs"; + +//#region src/utils/stream.d.ts +declare const createStreamBody: (stream: ReadStream, useNativeReadableToWeb?: boolean) => ReadableStream; +//#endregion +export { createStreamBody }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.mjs b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.mjs new file mode 100644 index 000000000..8d18a9a2e --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.mjs @@ -0,0 +1,64 @@ +import { Readable } from "node:stream"; +import { versions } from "node:process"; + +//#region src/utils/stream.ts +const pr54206Applied = () => { + const [major, minor] = versions.node.split(".").map((component) => parseInt(component)); + return major >= 23 || major === 22 && minor >= 7 || major === 20 && minor >= 18; +}; +const useReadableToWeb = pr54206Applied(); +const createStreamBody = (stream, useNativeReadableToWeb = useReadableToWeb) => { + if (useNativeReadableToWeb) return Readable.toWeb(stream); + let controller; + let settled = false; + const cleanup = () => { + stream.off("data", onData); + stream.off("error", onError); + stream.off("end", onTerminate); + stream.off("close", onTerminate); + }; + const settle = (callback) => { + if (settled) return; + settled = true; + cleanup(); + callback?.(); + }; + const onData = (chunk) => { + if (settled || !controller) return; + controller.enqueue(chunk); + if ((controller.desiredSize ?? 0) <= 0) stream.pause(); + }; + const onError = (error) => { + settle(() => { + controller?.error(error); + }); + }; + const onTerminate = () => { + settle(() => { + controller?.close(); + }); + }; + return new ReadableStream({ + start(streamController) { + controller = streamController; + stream.on("data", onData); + stream.on("error", onError); + stream.on("end", onTerminate); + stream.on("close", onTerminate); + stream.pause(); + }, + pull() { + if (!settled) stream.resume(); + }, + cancel() { + settle(); + const ignoreError = () => {}; + stream.on("error", ignoreError); + stream.once("close", () => stream.off("error", ignoreError)); + stream.destroy(); + } + }); +}; + +//#endregion +export { createStreamBody }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/package.json b/tests/fixtures/npm/node_modules/@hono/node-server/package.json new file mode 100644 index 000000000..b3ab47d33 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/package.json @@ -0,0 +1,128 @@ +{ + "name": "@hono/node-server", + "version": "2.1.5", + "description": "Node.js Adapter for Hono", + "main": "dist/index.mjs", + "type": "module", + "types": "dist/index.d.mts", + "files": [ + "dist" + ], + "exports": { + ".": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + }, + "./serve-static": { + "import": { + "types": "./dist/serve-static.d.mts", + "default": "./dist/serve-static.mjs" + }, + "require": { + "types": "./dist/serve-static.d.cts", + "default": "./dist/serve-static.cjs" + } + }, + "./utils/*": { + "import": { + "types": "./dist/utils/*.d.mts", + "default": "./dist/utils/*.mjs" + }, + "require": { + "types": "./dist/utils/*.d.cts", + "default": "./dist/utils/*.cjs" + } + }, + "./conninfo": { + "import": { + "types": "./dist/conninfo.d.mts", + "default": "./dist/conninfo.mjs" + }, + "require": { + "types": "./dist/conninfo.d.cts", + "default": "./dist/conninfo.cjs" + } + }, + "./early-hints": { + "import": { + "types": "./dist/early-hints.d.mts", + "default": "./dist/early-hints.mjs" + }, + "require": { + "types": "./dist/early-hints.d.cts", + "default": "./dist/early-hints.cjs" + } + } + }, + "typesVersions": { + "*": { + ".": [ + "./dist/index.d.mts" + ], + "serve-static": [ + "./dist/serve-static.d.mts" + ], + "utils/*": [ + "./dist/utils/*.d.mts" + ], + "conninfo": [ + "./dist/conninfo.d.mts" + ], + "early-hints": [ + "./dist/early-hints.d.mts" + ] + } + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/honojs/node-server.git" + }, + "homepage": "https://github.com/honojs/node-server", + "author": "Yusuke Wada (https://github.com/yusukebe)", + "publishConfig": { + "registry": "https://registry.npmjs.org", + "access": "public", + "provenance": true + }, + "engines": { + "node": ">=20" + }, + "devDependencies": { + "@hono/eslint-config": "^1.0.1", + "@types/node": "^20.10.0", + "@types/ws": "^8.18.1", + "@whatwg-node/fetch": "^0.9.14", + "eslint": "^9.10.0", + "hono": "^4.13.9", + "np": "^11.2.1", + "prettier": "^3.2.4", + "publint": "^0.3.18", + "tsdown": "^0.20.3", + "typescript": "^5.3.2", + "vitest": "^4.0.18", + "ws": "^8.19.0" + }, + "peerDependencies": { + "hono": "^4 || ^5.0.0-0" + }, + "scripts": { + "test": "vitest", + "build": "tsdown --external hono", + "watch": "tsdown --watch", + "postbuild": "publint", + "prerelease": "pnpm run build && pnpm run test --run", + "release": "np --no-publish", + "typecheck": "tsc --noEmit", + "lint": "eslint src test", + "lint:fix": "eslint src test --fix", + "format": "prettier --check \"src/**/*.{js,ts}\" \"test/**/*.{js,ts}\"", + "format:fix": "prettier --write \"src/**/*.{js,ts}\" \"test/**/*.{js,ts}\"" + } +} \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/gtminicache/dist/index.d.ts b/tests/fixtures/npm/node_modules/gtminicache/dist/index.d.ts new file mode 100644 index 000000000..5d12cad7e --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtminicache/dist/index.d.ts @@ -0,0 +1,7 @@ +export declare class Cache { + constructor(max: number); + set(key: K, value: V): this; + get(key: K): V | undefined; + keys(): K[]; + describe(label: string): string; +} diff --git a/tests/fixtures/npm/node_modules/gtminicache/dist/index.js b/tests/fixtures/npm/node_modules/gtminicache/dist/index.js new file mode 100644 index 000000000..31c6deef9 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtminicache/dist/index.js @@ -0,0 +1 @@ +"use strict";var n=(s,e)=>()=>(e||s((e={exports:{}}).exports,e),e.exports);var u=n(i=>{"use strict";Object.defineProperty(i,"__esModule",{value:!0});i.metrics=void 0;var o=require("node:diagnostics_channel");i.metrics=(0,o.channel)("gtminicache:metrics")});var d=n(r=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});r.describe=a;var l=require("node:util");function a(s,e){return(0,l.format)("%s holds %d",s,e)}});Object.defineProperty(exports,"__esModule",{value:!0});exports.Cache=void 0;var h=u(),_=d(),c=class{#e=new Map;#t;constructor(e){this.#t=e}set(e,t){for(this.#e.delete(e),this.#e.set(e,t);this.#e.size>this.#t;)this.#e.delete(this.#e.keys().next().value);return h.metrics.hasSubscribers&&h.metrics.publish({key:e}),this}get(e){let t=this.#e.get(e);return t!==void 0&&(this.#e.delete(e),this.#e.set(e,t)),t}keys(){return[...this.#e.keys()]}describe(e){return(0,_.describe)(e,this.#e.size)}};exports.Cache=c; diff --git a/tests/fixtures/npm/node_modules/gtminicache/package.json b/tests/fixtures/npm/node_modules/gtminicache/package.json new file mode 100644 index 000000000..3f57e1ae3 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtminicache/package.json @@ -0,0 +1,7 @@ +{ + "name": "gtminicache", + "version": "1.0.0", + "description": "tsc-style CommonJS modules bundled by esbuild 0.27.0 with --bundle --platform=node --format=cjs --minify: the __commonJS helper and the chunk parameters lose their names, the __esModule stamps read { value: !0 }, and calls into node:util and node:diagnostics_channel use the (0, ns.member)(...) form", + "main": "./dist/index.js", + "types": "./dist/index.d.ts" +} diff --git a/tests/fixtures/npm/node_modules/gtminimark/dist/index.d.ts b/tests/fixtures/npm/node_modules/gtminimark/dist/index.d.ts new file mode 100644 index 000000000..295430b86 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtminimark/dist/index.d.ts @@ -0,0 +1 @@ +export declare function render(src: string, options?: { pedantic?: boolean }): string; diff --git a/tests/fixtures/npm/node_modules/gtminimark/dist/index.js b/tests/fixtures/npm/node_modules/gtminimark/dist/index.js new file mode 100644 index 000000000..c5e1daa31 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtminimark/dist/index.js @@ -0,0 +1 @@ +var m = { heading: /^(#{1,6}) +(.*)$/, link: /^\[([^\]]*)\]\(([^ )]*)(?: "([^"]*)")?\)/, ref: /^\[([^\]]*)\]$/, lead: /^( {1,4}| {0,3}\t)/, nonSpace: /[^ ]/, punct: /\\([\\*_\[\]])/g, word: /\w+/g, def: /^\[([^\]]*)\]: (\S+)(?: "([^"]*)")?$/ },B = { ...m, emph: /\*([^*]+)\*/g },P = { ...B, word: /[a-z]+/g },K = { normal: { other: m, inline: B }, pedantic: { other: m, inline: P, strict: !0 } };function X(a, e) { let t = 0, n = ""; for (let i of a) if (i === "\t") { let r = 4 - t % 4; n += " ".repeat(r), t += r } else n += i, t++; return n + e; }function Se(a, e, t, n) { let r = e.href, o = e.title || null; n.links++; return { type: "link", raw: t, href: r, title: o, text: a[1] }; }class T {constructor() { this.state = { links: 0 }; }heading(s) { let c = s.match(this.rules.other.heading); return c ? "h" + c[1].length + ":" + c[2].replace(this.rules.other.punct, "$1") : null; }indent(s, w) { return s.replace(this.rules.other.lead, W => X(W, w)); }first(s) { return s.search(this.rules.other.nonSpace); }emph(s) { return s.replace(this.rules.inline.emph, (W, x) => "" + x + ""); }words(s) { let c = s.match(this.rules.inline.word); return c ? c.length : 0; }link(s) { let c = this.rules.other.link.exec(s); if (!c) return null; let r = c[2], o = ""; o = c[3] ? c[3] : ""; return Se(c, { href: r && r.replace(this.rules.other.punct, "$1"), title: o && o.replace(this.rules.other.punct, "$1") }, c[0], this.state); }ref(s, l) { let c = this.rules.other.ref.exec(s); if (!c) return null; let o = l[c[1].toLowerCase()]; return o ? Se(c, o, c[0], this.state) : { type: "text", raw: c[0] }; }}class L {constructor(e) { this.options = e || {}; this.links = {}; this.tokenizer = new T(); this.tokenizer.rules = this.options.pedantic ? K.pedantic : K.normal; }define(s) { let c = s.match(this.tokenizer.rules.other.def); if (c) this.links[c[1].toLowerCase()] = { href: c[2], title: c[3] || "" }; return !!c; }line(s) {let t = this.tokenizer, h = t.heading(s);if (h) return h;let k = t.link(s);if (k) return JSON.stringify(k);let f = t.ref(s, this.links);if (f) return JSON.stringify(f);return t.first(s) + "|" + JSON.stringify(t.indent(s, t.first(s))) + "|" + t.emph(s) + "|" + t.words(s);}}export function render(src, options) {let l = new L(options), out = [];for (let s of src.split("\n")) if (!l.define(s)) out.push(l.line(s));return out.join("\n") + "\nlinks " + l.tokenizer.state.links;} diff --git a/tests/fixtures/npm/node_modules/gtminimark/package.json b/tests/fixtures/npm/node_modules/gtminimark/package.json new file mode 100644 index 000000000..ff188c682 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtminimark/package.json @@ -0,0 +1,8 @@ +{ + "name": "gtminimark", + "version": "1.0.0", + "description": "A one-line minified ES module: a module-level var statement whose later declarators spread earlier ones, regexes read through an untyped rules table, and a function called with typed and untyped link objects", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts" +} diff --git a/tests/fixtures/npm/node_modules/hono/LICENSE b/tests/fixtures/npm/node_modules/hono/LICENSE new file mode 100644 index 000000000..bd174facc --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 - present, Yusuke Wada and Hono contributors + +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/node_modules/hono/dist/helper/websocket/index.js b/tests/fixtures/npm/node_modules/hono/dist/helper/websocket/index.js new file mode 100644 index 000000000..620de4105 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/helper/websocket/index.js @@ -0,0 +1,53 @@ +//#region src/helper/websocket/index.ts +/** +* A context for controlling WebSockets +*/ +var WSContext = class { + #init; + constructor(init) { + this.#init = init; + this.raw = init.raw; + this.url = init.url ? new URL(init.url) : null; + this.protocol = init.protocol ?? null; + } + send(source, options) { + this.#init.send(source, options ?? {}); + } + raw; + binaryType = "arraybuffer"; + get readyState() { + return this.#init.readyState; + } + url; + protocol; + close(code, reason) { + this.#init.close(code, reason); + } +}; +const createWSMessageEvent = (source) => { + return new MessageEvent("message", { data: source }); +}; +/** +* Create a WebSocket adapter/helper +*/ +const defineWebSocketHelper = (handler) => { + return ((...args) => { + if (typeof args[0] === "function") { + const [createEvents, options] = args; + return async function upgradeWebSocket(c, next) { + const result = await handler(c, await createEvents(c), options); + if (result) return result; + await next(); + }; + } else { + const [c, events, options] = args; + return (async () => { + const upgraded = await handler(c, events, options); + if (!upgraded) throw new Error("Failed to upgrade WebSocket"); + return upgraded; + })(); + } + }); +}; +//#endregion +export { WSContext, createWSMessageEvent, defineWebSocketHelper }; diff --git a/tests/fixtures/npm/node_modules/hono/package.json b/tests/fixtures/npm/node_modules/hono/package.json new file mode 100644 index 000000000..6264c824a --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/package.json @@ -0,0 +1,12 @@ +{ + "name": "hono", + "version": "4.13.13", + "description": "Test fixture: only the hono/ws helper that @hono/node-server's index imports.", + "type": "module", + "license": "MIT", + "exports": { + "./ws": { + "import": "./dist/helper/websocket/index.js" + } + } +} 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__/node-types-fenced.txt b/tests/harness/__snapshots__/node-types-fenced.txt index cabce49c4..562733f53 100644 --- a/tests/harness/__snapshots__/node-types-fenced.txt +++ b/tests/harness/__snapshots__/node-types-fenced.txt @@ -120,22 +120,22 @@ fenced.ts:76:1 - error SC2020: 'crypto.publicEncrypt' is typed by @types/node bu 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 }); + 77 | createCipheriv("aes-128-ocb", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); hint: RSA encryption has no lowering — the asymmetric crypto surface is RSA and EC KeyObjects (generateKeyPair, createPrivateKey, createPublicKey), sign, verify, createSign, createVerify, createECDH, and createDiffieHellman -fenced.ts:77:16 - error SC2020: 'crypto.createCipheriv with cipher 'aes-128-ccm'' is part of the standard library types but has no scriptc lowering yet +fenced.ts:77:16 - error SC2020: 'crypto.createCipheriv with cipher 'aes-128-ocb'' is part of the standard library types but has no scriptc lowering yet 76 | publicEncrypt("key", Buffer.alloc(1)); - 77 | createCipheriv("aes-128-ccm", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); + 77 | createCipheriv("aes-128-ocb", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); | ^~~~~~~~~~~~~ 78 | secureHeapUsed(); - 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 + hint: the supported ciphers are aes-128, aes-192 and aes-256 in cbc, ctr, gcm, ccm, ecb, cfb, cfb8, cfb1 and ofb modes and their wrap, wrap-pad, wrap-inv and wrap-pad-inv key wraps, 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.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 }); + 77 | createCipheriv("aes-128-ocb", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); 78 | secureHeapUsed(); | ^~~~~~~~~~~~~~~~ 79 | setFips(false); diff --git a/tests/harness/__snapshots__/split-cjs-require-cycle/main.js.txt b/tests/harness/__snapshots__/split-cjs-require-cycle/main.js.txt new file mode 100644 index 000000000..6c39c35d6 --- /dev/null +++ b/tests/harness/__snapshots__/split-cjs-require-cycle/main.js.txt @@ -0,0 +1,7 @@ +split-cjs-require-cycle/main.js:1:1 - error SC1016: circular imports (split-cjs-require-cycle/main.js → split-cjs-require-cycle/b.js → split-cjs-require-cycle/main.js; the cycle closes through a require() edge: split-cjs-require-cycle/main.js uses its exports at split-cjs-require-cycle/main.js:10 outside the export statements that precede its first require, so a module of the cycle can observe them before they settle) are not supported yet + + 1 | // A split require binding (`var b; b = require("./b.js")`) whose require + | ^ + 2 | // closes a CommonJS cycle that top-level code can observe: b.js reads a.js's + + hint: ES-module cycles of let/const/function bindings compile as-is; rewrite the named construct (a named import instead of a namespace, a const instead of a default-export expression or var, a class declared before the code that can reach it) or break the named edge \ No newline at end of file diff --git a/tests/harness/__snapshots__/split-cjs-require-early/main.js.txt b/tests/harness/__snapshots__/split-cjs-require-early/main.js.txt new file mode 100644 index 000000000..2269f906e --- /dev/null +++ b/tests/harness/__snapshots__/split-cjs-require-early/main.js.txt @@ -0,0 +1,6 @@ +split-cjs-require-early/main.js:10:7 - error SC1013: require() binding 'lib' assigned below code that can reach it (Node reads undefined before the assignment runs, which is not modeled — move the assignment above the code that can run first) is not supported yet + + 9 | console.log('boot', helper()); + 10 | lib = require('./lib.js'); + | ^~~~~~~~~~~~~~~~~~~ + 11 | \ No newline at end of file diff --git a/tests/harness/__snapshots__/stream-runtime-option-order.ts.txt b/tests/harness/__snapshots__/stream-runtime-option-order.ts.txt new file mode 100644 index 000000000..ff7f10be1 --- /dev/null +++ b/tests/harness/__snapshots__/stream-runtime-option-order.ts.txt @@ -0,0 +1,26 @@ +stream-runtime-option-order.ts:16:3 - error SC2020: 'the Writable option 'defaultEncoding' with a runtime value that an earlier option value could observe or change' is part of the standard library types but has no scriptc lowering yet + + 15 | highWaterMark: bump(), + 16 | defaultEncoding: encoding, + | ^~~~~~~~~~~~~~~~~~~~~~~~~ + 17 | write(_chunk, _encoding, done) { + + hint: compute the option values into consts before the options object (the runtime value is read before the other options run) + +stream-runtime-option-order.ts:23:3 - error SC2020: 'the Writable option 'decodeStrings' with a runtime value that an earlier option value could observe or change' is part of the standard library types but has no scriptc lowering yet + + 22 | highWaterMark: bump(), + 23 | decodeStrings: decode, + | ^~~~~~~~~~~~~~~~~~~~~ + 24 | write(_chunk, _encoding, done) { + + hint: compute the option values into consts before the options object (the runtime value is read before the other options run) + +stream-runtime-option-order.ts:35:3 - error SC2020: 'the Writable option 'defaultEncoding' with a runtime value that an earlier option value could observe or change' is part of the standard library types but has no scriptc lowering yet + + 34 | highWaterMark: sizes.hwm, + 35 | defaultEncoding: pickEncoding(), + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 36 | write(_chunk, _encoding, done) { + + hint: compute the option values into consts before the options object (the runtime value is read before the other options run) \ No newline at end of file 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/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/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/7154-crypto-cipher-info.cjs.json b/tests/harness/corpus-costs/7154-crypto-cipher-info.cjs.json new file mode 100644 index 000000000..4eec93aec --- /dev/null +++ b/tests/harness/corpus-costs/7154-crypto-cipher-info.cjs.json @@ -0,0 +1 @@ +{ "plain": 6115, "san": 62881 } diff --git a/tests/harness/corpus-costs/7155-cipher-aes-ccm.cjs.json b/tests/harness/corpus-costs/7155-cipher-aes-ccm.cjs.json new file mode 100644 index 000000000..4968524bd --- /dev/null +++ b/tests/harness/corpus-costs/7155-cipher-aes-ccm.cjs.json @@ -0,0 +1 @@ +{ "plain": 8462, "san": 75851 } diff --git a/tests/harness/corpus-costs/7156-cipher-key-wrap.cjs.json b/tests/harness/corpus-costs/7156-cipher-key-wrap.cjs.json new file mode 100644 index 000000000..b5e45dae6 --- /dev/null +++ b/tests/harness/corpus-costs/7156-cipher-key-wrap.cjs.json @@ -0,0 +1 @@ +{ "plain": 5548, "san": 52523 } diff --git a/tests/harness/corpus-costs/7157-crypto-cipher-info-typed.ts.json b/tests/harness/corpus-costs/7157-crypto-cipher-info-typed.ts.json new file mode 100644 index 000000000..425ec1dc2 --- /dev/null +++ b/tests/harness/corpus-costs/7157-crypto-cipher-info-typed.ts.json @@ -0,0 +1 @@ +{ "plain": 3930, "san": 6119 } 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/7264-array-dense-paired-loops.ts.json b/tests/harness/corpus-costs/7264-array-dense-paired-loops.ts.json new file mode 100644 index 000000000..f591a71f3 --- /dev/null +++ b/tests/harness/corpus-costs/7264-array-dense-paired-loops.ts.json @@ -0,0 +1 @@ +{ "plain": 2900, "san": 4200 } diff --git a/tests/harness/corpus-costs/7281-http-server-connect-tunnel.ts.json b/tests/harness/corpus-costs/7281-http-server-connect-tunnel.ts.json new file mode 100644 index 000000000..c35274f8c --- /dev/null +++ b/tests/harness/corpus-costs/7281-http-server-connect-tunnel.ts.json @@ -0,0 +1 @@ +{ "plain": 3700, "san": 3100 } diff --git a/tests/harness/corpus-costs/7282-http2-once-add-listener.ts.json b/tests/harness/corpus-costs/7282-http2-once-add-listener.ts.json new file mode 100644 index 000000000..9e5407833 --- /dev/null +++ b/tests/harness/corpus-costs/7282-http2-once-add-listener.ts.json @@ -0,0 +1 @@ +{ "plain": 4000, "san": 5100 } diff --git a/tests/harness/corpus-costs/7287-http-held-request-backpressure.ts.json b/tests/harness/corpus-costs/7287-http-held-request-backpressure.ts.json new file mode 100644 index 000000000..9622e52d1 --- /dev/null +++ b/tests/harness/corpus-costs/7287-http-held-request-backpressure.ts.json @@ -0,0 +1 @@ +{ "plain": 5600, "san": 5600 } 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-cycle-deferred-folded-key/main.ts.json b/tests/harness/corpus-costs/7380-cycle-deferred-folded-key/main.ts.json new file mode 100644 index 000000000..d5d1c5f65 --- /dev/null +++ b/tests/harness/corpus-costs/7380-cycle-deferred-folded-key/main.ts.json @@ -0,0 +1 @@ +{ "plain": 2500, "san": 3500 } diff --git a/tests/harness/corpus-costs/7381-cycle-deferred-reached-tdz/main.ts.json b/tests/harness/corpus-costs/7381-cycle-deferred-reached-tdz/main.ts.json new file mode 100644 index 000000000..b24c0c970 --- /dev/null +++ b/tests/harness/corpus-costs/7381-cycle-deferred-reached-tdz/main.ts.json @@ -0,0 +1 @@ +{ "plain": 1800, "san": 2600 } diff --git a/tests/harness/corpus-costs/7382-cycle-static-this-tdz/main.ts.json b/tests/harness/corpus-costs/7382-cycle-static-this-tdz/main.ts.json new file mode 100644 index 000000000..b24c0c970 --- /dev/null +++ b/tests/harness/corpus-costs/7382-cycle-static-this-tdz/main.ts.json @@ -0,0 +1 @@ +{ "plain": 1800, "san": 2600 } diff --git a/tests/harness/corpus-costs/7383-cycle-class-expression-name-tdz/main.ts.json b/tests/harness/corpus-costs/7383-cycle-class-expression-name-tdz/main.ts.json new file mode 100644 index 000000000..b24c0c970 --- /dev/null +++ b/tests/harness/corpus-costs/7383-cycle-class-expression-name-tdz/main.ts.json @@ -0,0 +1 @@ +{ "plain": 1800, "san": 2600 } diff --git a/tests/harness/corpus-costs/7390-record-key-read-presence.ts.json b/tests/harness/corpus-costs/7390-record-key-read-presence.ts.json new file mode 100644 index 000000000..3f7384f20 --- /dev/null +++ b/tests/harness/corpus-costs/7390-record-key-read-presence.ts.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 6000 } 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/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/7447-union-record-arm-width.mjs.json b/tests/harness/corpus-costs/7447-union-record-arm-width.mjs.json new file mode 100644 index 000000000..5648bd255 --- /dev/null +++ b/tests/harness/corpus-costs/7447-union-record-arm-width.mjs.json @@ -0,0 +1 @@ +{ "plain": 5785, "san": 5722 } diff --git a/tests/harness/corpus-costs/7448-union-record-arm-width.ts.json b/tests/harness/corpus-costs/7448-union-record-arm-width.ts.json new file mode 100644 index 000000000..c60a813c2 --- /dev/null +++ b/tests/harness/corpus-costs/7448-union-record-arm-width.ts.json @@ -0,0 +1 @@ +{ "plain": 3204, "san": 4052 } diff --git a/tests/harness/corpus-costs/7460-transform-writev.mjs.json b/tests/harness/corpus-costs/7460-transform-writev.mjs.json new file mode 100644 index 000000000..f6c21fd3d --- /dev/null +++ b/tests/harness/corpus-costs/7460-transform-writev.mjs.json @@ -0,0 +1 @@ +{ "plain": 2266, "san": 2991 } diff --git a/tests/harness/corpus-costs/7461-transform-writev.cjs.json b/tests/harness/corpus-costs/7461-transform-writev.cjs.json new file mode 100644 index 000000000..b5868c90b --- /dev/null +++ b/tests/harness/corpus-costs/7461-transform-writev.cjs.json @@ -0,0 +1 @@ +{ "plain": 1828, "san": 2413 } diff --git a/tests/harness/corpus-costs/7462-transform-writev.ts.json b/tests/harness/corpus-costs/7462-transform-writev.ts.json new file mode 100644 index 000000000..f8fd66c2a --- /dev/null +++ b/tests/harness/corpus-costs/7462-transform-writev.ts.json @@ -0,0 +1 @@ +{ "plain": 2178, "san": 2875 } diff --git a/tests/harness/corpus-costs/7463-stream-option-values.ts.json b/tests/harness/corpus-costs/7463-stream-option-values.ts.json new file mode 100644 index 000000000..0f66a4266 --- /dev/null +++ b/tests/harness/corpus-costs/7463-stream-option-values.ts.json @@ -0,0 +1 @@ +{ "plain": 1786, "san": 4192 } diff --git a/tests/harness/corpus-costs/7464-stream-option-values.mjs.json b/tests/harness/corpus-costs/7464-stream-option-values.mjs.json new file mode 100644 index 000000000..b609b128f --- /dev/null +++ b/tests/harness/corpus-costs/7464-stream-option-values.mjs.json @@ -0,0 +1 @@ +{ "plain": 1678, "san": 4502 } diff --git a/tests/harness/corpus-costs/7465-transform-callback-values.ts.json b/tests/harness/corpus-costs/7465-transform-callback-values.ts.json new file mode 100644 index 000000000..1c67bb43f --- /dev/null +++ b/tests/harness/corpus-costs/7465-transform-callback-values.ts.json @@ -0,0 +1 @@ +{ "plain": 1571, "san": 2074 } diff --git a/tests/harness/corpus-costs/7466-stream-construct-assign.mjs.json b/tests/harness/corpus-costs/7466-stream-construct-assign.mjs.json new file mode 100644 index 000000000..1c67bb43f --- /dev/null +++ b/tests/harness/corpus-costs/7466-stream-construct-assign.mjs.json @@ -0,0 +1 @@ +{ "plain": 1571, "san": 2074 } diff --git a/tests/harness/corpus-costs/7500-pipeline-callback-undefined.ts.json b/tests/harness/corpus-costs/7500-pipeline-callback-undefined.ts.json new file mode 100644 index 000000000..122c66b27 --- /dev/null +++ b/tests/harness/corpus-costs/7500-pipeline-callback-undefined.ts.json @@ -0,0 +1 @@ +{ "plain": 3783, "san": 4994 } diff --git a/tests/harness/corpus-costs/7501-pipeline-callback-undefined.mjs.json b/tests/harness/corpus-costs/7501-pipeline-callback-undefined.mjs.json new file mode 100644 index 000000000..d9c00a7de --- /dev/null +++ b/tests/harness/corpus-costs/7501-pipeline-callback-undefined.mjs.json @@ -0,0 +1 @@ +{ "plain": 2457, "san": 3243 } diff --git a/tests/harness/corpus-costs/7502-readline-cursor-undefined.ts.json b/tests/harness/corpus-costs/7502-readline-cursor-undefined.ts.json new file mode 100644 index 000000000..7a36db8f3 --- /dev/null +++ b/tests/harness/corpus-costs/7502-readline-cursor-undefined.ts.json @@ -0,0 +1 @@ +{ "plain": 2592, "san": 3421 } 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/7543-var-list-declarator-order.js.json b/tests/harness/corpus-costs/7543-var-list-declarator-order.js.json new file mode 100644 index 000000000..cc47657bd --- /dev/null +++ b/tests/harness/corpus-costs/7543-var-list-declarator-order.js.json @@ -0,0 +1 @@ +{ "plain": 4300, "san": 5900 } 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/7545-string-regex-checked-arguments.js.json b/tests/harness/corpus-costs/7545-string-regex-checked-arguments.js.json new file mode 100644 index 000000000..83b82ee18 --- /dev/null +++ b/tests/harness/corpus-costs/7545-string-regex-checked-arguments.js.json @@ -0,0 +1 @@ +{ "plain": 4900, "san": 4600 } diff --git a/tests/harness/corpus-costs/7548-cjs-receiverless-builtin-calls/main.cjs.json b/tests/harness/corpus-costs/7548-cjs-receiverless-builtin-calls/main.cjs.json new file mode 100644 index 000000000..8a603b2f4 --- /dev/null +++ b/tests/harness/corpus-costs/7548-cjs-receiverless-builtin-calls/main.cjs.json @@ -0,0 +1 @@ +{ "plain": 12300, "san": 13600 } diff --git a/tests/harness/corpus-costs/7549-cjs-minified-esmodule-stamp/main.cjs.json b/tests/harness/corpus-costs/7549-cjs-minified-esmodule-stamp/main.cjs.json new file mode 100644 index 000000000..d00a49ec8 --- /dev/null +++ b/tests/harness/corpus-costs/7549-cjs-minified-esmodule-stamp/main.cjs.json @@ -0,0 +1 @@ +{ "plain": 16000, "san": 21200 } diff --git a/tests/harness/corpus-costs/7553-var-for-in-of-zero-iterations.js.json b/tests/harness/corpus-costs/7553-var-for-in-of-zero-iterations.js.json new file mode 100644 index 000000000..566096d24 --- /dev/null +++ b/tests/harness/corpus-costs/7553-var-for-in-of-zero-iterations.js.json @@ -0,0 +1 @@ +{ "plain": 19400, "san": 25900 } 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/7580-int32-arrays-across-calls.ts.json b/tests/harness/corpus-costs/7580-int32-arrays-across-calls.ts.json new file mode 100644 index 000000000..7c9d20632 --- /dev/null +++ b/tests/harness/corpus-costs/7580-int32-arrays-across-calls.ts.json @@ -0,0 +1 @@ +{ "plain": 3100, "san": 3500 } 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/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/7620-js-class-view-descriptors.js.json b/tests/harness/corpus-costs/7620-js-class-view-descriptors.js.json new file mode 100644 index 000000000..ef439d096 --- /dev/null +++ b/tests/harness/corpus-costs/7620-js-class-view-descriptors.js.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 4500 } 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/7640-child-spawn-before-fast-exit.ts.json b/tests/harness/corpus-costs/7640-child-spawn-before-fast-exit.ts.json new file mode 100644 index 000000000..b56ad904d --- /dev/null +++ b/tests/harness/corpus-costs/7640-child-spawn-before-fast-exit.ts.json @@ -0,0 +1 @@ +{ "plain": 2600, "san": 4000 } 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/corpus-costs/7740-handle-method-values.js.json b/tests/harness/corpus-costs/7740-handle-method-values.js.json new file mode 100644 index 000000000..d18e4dddd --- /dev/null +++ b/tests/harness/corpus-costs/7740-handle-method-values.js.json @@ -0,0 +1 @@ +{ "plain": 9800, "san": 12000 } diff --git a/tests/harness/corpus-costs/7741-abort-signal-listener-value.js.json b/tests/harness/corpus-costs/7741-abort-signal-listener-value.js.json new file mode 100644 index 000000000..e028a5bb1 --- /dev/null +++ b/tests/harness/corpus-costs/7741-abort-signal-listener-value.js.json @@ -0,0 +1 @@ +{ "plain": 4000, "san": 7500 } diff --git a/tests/harness/corpus-costs/7790-js-class-integrity-layouts.js.json b/tests/harness/corpus-costs/7790-js-class-integrity-layouts.js.json new file mode 100644 index 000000000..af44b31af --- /dev/null +++ b/tests/harness/corpus-costs/7790-js-class-integrity-layouts.js.json @@ -0,0 +1 @@ +{ "plain": 4400, "san": 5200 } diff --git a/tests/harness/corpus-costs/7791-js-class-accessor-layouts.js.json b/tests/harness/corpus-costs/7791-js-class-accessor-layouts.js.json new file mode 100644 index 000000000..eb36dcefe --- /dev/null +++ b/tests/harness/corpus-costs/7791-js-class-accessor-layouts.js.json @@ -0,0 +1 @@ +{ "plain": 5100, "san": 4700 } diff --git a/tests/harness/corpus-costs/class-brand-probe-keys.ts.json b/tests/harness/corpus-costs/class-brand-probe-keys.ts.json new file mode 100644 index 000000000..9f464b312 --- /dev/null +++ b/tests/harness/corpus-costs/class-brand-probe-keys.ts.json @@ -0,0 +1 @@ +{ "plain": 8500, "san": 30000 } diff --git a/tests/harness/corpus-costs/fetch-headers-copy-cache.ts.json b/tests/harness/corpus-costs/fetch-headers-copy-cache.ts.json new file mode 100644 index 000000000..6057503d9 --- /dev/null +++ b/tests/harness/corpus-costs/fetch-headers-copy-cache.ts.json @@ -0,0 +1 @@ +{ "plain": 6400, "san": 9200 } diff --git a/tests/harness/corpus-costs/split-cjs-require-cycle/main.cjs.json b/tests/harness/corpus-costs/split-cjs-require-cycle/main.cjs.json new file mode 100644 index 000000000..9619c105e --- /dev/null +++ b/tests/harness/corpus-costs/split-cjs-require-cycle/main.cjs.json @@ -0,0 +1 @@ +{ "plain": 2700, "san": 3200 } diff --git a/tests/harness/corpus-costs/string-last-index-of-ascii.ts.json b/tests/harness/corpus-costs/string-last-index-of-ascii.ts.json new file mode 100644 index 000000000..a970c2f16 --- /dev/null +++ b/tests/harness/corpus-costs/string-last-index-of-ascii.ts.json @@ -0,0 +1 @@ +{ "plain": 6000, "san": 9000 } diff --git a/tests/harness/coverage.test.ts b/tests/harness/coverage.test.ts index 3ed562aea..e1831c609 100644 --- a/tests/harness/coverage.test.ts +++ b/tests/harness/coverage.test.ts @@ -561,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 f379973b3..c49c6f07b 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -325,13 +325,14 @@ console.log(cells.join(",")); ); }); - test("a split require closing a CommonJS cycle throws the partial-exports fence", async () => { + test("a split require closing a CommonJS cycle keeps the const form's SC1016", async () => { // Node answers the closing require with the dependency's partial exports // ({ fromA: 1 } at this point), which the static export model cannot - // represent. The split assignment must hit the same fence as an inline - // require in expression position instead of reading uninitialized - // export storage. - const r = await compileAndRun( + // represent. Preflight treats the split assignment like `const a = + // require("./a.cjs")`, so the cycle keeps that form's SC1016 instead of + // reading uninitialized export storage. A cycle whose reads are all deferred + // compiles (tests/corpus/split-cjs-require-cycle). + const r = compileAndRun( "split-require-cycle", `require("./a.cjs"); `, @@ -348,10 +349,8 @@ console.log(a.fromA, a.fromB); `, }, ); - expect(r.exitCode).toBe(1); - expect(r.stdout).toBe(""); - expect(r.stderr).toMatch( - /^Uncaught Error: require\(\) of '\.\/a\.cjs' while it is still evaluating \(a require cycle: Node answers its partial exports, which the static export table does not model\) is not supported yet \[SC1090 at .*b\.cjs:3\]\n$/, + await expect(r).rejects.toThrow( + /SC1016: circular imports \(.*a\.cjs → .*b\.cjs → .*a\.cjs; the cycle closes through a require\(\) edge/, ); }); @@ -879,7 +878,7 @@ describe("unimplemented cipher names", () => { const r = await compileAndRun( "cipher-computed-name", `import { createCipheriv } from "node:crypto"; -const mode = ["c", "c", "m"].join(""); +const mode = ["o", "c", "b"].join(""); try { createCipheriv("aes-128-" + mode, Buffer.alloc(16), Buffer.alloc(12)); } catch (error) { @@ -896,16 +895,43 @@ 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, 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 SC2020 The cipher 'aes-128-ocb' has no native implementation; the supported ciphers are " + + "aes-128, aes-192 and aes-256 in cbc, ctr, gcm, ccm, ecb, cfb, cfb8, cfb1 and ofb modes and their " + + "wrap, wrap-pad, wrap-inv and wrap-pad-inv key wraps, 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(""); }); }); +// getCipherInfo's result is Node's info object; CipherInfo declares nid as +// required, and Node reports the SIV ciphers without one. +describe("getCipherInfo through CipherInfo", () => { + test("a typed read of a missing nid throws a TypeError", async () => { + const r = await compileAndRun( + "cipher-info-no-nid", + `import { getCipherInfo } from "node:crypto"; +const siv = getCipherInfo("aes-128-siv")!; +console.log(JSON.stringify(siv), getCipherInfo("aes-128-gcm")?.nid); +try { + const nid: number = siv.nid; + console.log(nid); +} catch (error) { + const e = error as Error; + console.log(e.name, e.message); +} +`, + ); + expect(r.exitCode).toBe(0); + expect(r.stdout).toBe( + '{"mode":"siv","name":"aes-128-siv","blockSize":1,"keyLength":32} 895\n' + + "TypeError expected number at $, got undefined\n", + ); + expect(r.stderr).toBe(""); + }); +}); + // new tls.TLSSocket reads a runtime options record: the client mode, whose // handshake Node never starts, and members without a lowering throw the // fence, as do sockets it cannot take over (a second wrap, and @@ -1154,6 +1180,41 @@ try { }); }); +// getCipherInfo reads keyLength and ivLength through getters, which a +// class instance's accessors are not visible to from the runtime, so a +// class that defines either throws a coded SC2020 error. +describe("getCipherInfo options whose class defines keyLength or ivLength", () => { + test("throw SC2020 after the argument checks", async () => { + const r = await compileAndRun( + "cipher-info-class-options", + `import { getCipherInfo } from "node:crypto"; +class Options { + get keyLength(): number { + return 17; + } +} +const options: any = new Options(); +const names: any[] = ["aes-128-cbc", 1.5]; +for (const name of names) { + try { + getCipherInfo(name, options); + } catch (error) { + const e = error as Error & { code?: string }; + console.log(e.name, e.code, e.message); + } +} +`, + ); + expect(r.exitCode).toBe(0); + expect(r.stdout).toBe( + "Error SC2020 getCipherInfo options whose class defines keyLength or ivLength have no lowering; " + + "pass an object with those properties\n" + + 'RangeError ERR_OUT_OF_RANGE The value of "nameOrNid" is out of range. It must be an integer. Received 1.5\n', + ); + expect(r.stderr).toBe(""); + }); +}); + // After a Hash's stream flushes its digest, Node copies the finished // OpenSSL context, whose later updates and digests fail with OpenSSL // error-queue codes. That copy throws a coded SC2020 error. diff --git a/tests/harness/library-mode.test.ts b/tests/harness/library-mode.test.ts index 0d4052952..db4fabb3e 100644 --- a/tests/harness/library-mode.test.ts +++ b/tests/harness/library-mode.test.ts @@ -50,8 +50,8 @@ * no-teachings default is pinned by K5/K7 over * the teachings-free traps profile * K13 npm-posture bare npm specifiers are static-or-refuse: an - * eligible package (own .d.ts, unminified JS, - * no transform markers) compiles statically + * eligible package (own .d.ts, no transform + * markers) compiles statically * into the library graph, its own npm deps * judged by the same bar; every ineligible one * refuses SC4020 naming the failed bar — never @@ -746,11 +746,11 @@ describe("K9: library-mode refusals", () => { * synthesized per test like K9's. Emission never engages — SC4020 is a * frontend verdict — so one lane suffices. */ let npmRefusalCounter = 0; -async function npmRefusal( +async function compileNpmFixture( entryFile: string, exports: unknown[], profilePatch: Record = {}, -): Promise<{ code: string; message: string; hint?: string; note?: string; file: string }[]> { +): Promise>> { const outDir = join(cacheDir, `npm-refusal-${npmRefusalCounter++}`); mkdirSync(outDir, { recursive: true }); const profile = { @@ -770,7 +770,15 @@ async function npmRefusal( }; const profilePath = join(outDir, "profile.json"); writeFileSync(profilePath, JSON.stringify(profile)); - const result = await compileLibrary({ profilePath, outDir }); + return compileLibrary({ profilePath, outDir }); +} + +async function npmRefusal( + entryFile: string, + exports: unknown[], + profilePatch: Record = {}, +): Promise<{ code: string; message: string; hint?: string; note?: string; file: string }[]> { + const result = await compileNpmFixture(entryFile, exports, profilePatch); expect(result.ok).toBe(false); if (result.ok) throw new Error("unreachable"); return result.diagnostics.map((d) => ({ @@ -795,13 +803,11 @@ describe("K13: library-mode npm refusals (SC4020, static-or-refuse)", () => { expect(diags[0]!.file.endsWith("lib-markers.ts")).toBe(true); }); - test("minified shipped JS refuses", async () => { - const diags = await npmRefusal("lib-minified.ts", [ + test("minified shipped JS compiles", async () => { + const result = await compileNpmFixture("lib-minified.ts", [ { export: "f", symbol: "kx_f", params: ["f64", "f64"], returns: "f64" }, ]); - expect(diags.map((d) => d.code)).toEqual(["SC4020"]); - expect(diags[0]!.message).toContain("'slimmed'"); - expect(diags[0]!.message).toContain("looks minified"); + expect(result.ok ? [] : result.diagnostics.map((d) => d.message)).toEqual([]); }); test("no own .d.ts refuses by name — never the generic import fence", async () => { diff --git a/tests/harness/library-static-types.test.ts b/tests/harness/library-static-types.test.ts index dfbd70bf9..36e2befac 100644 --- a/tests/harness/library-static-types.test.ts +++ b/tests/harness/library-static-types.test.ts @@ -81,7 +81,8 @@ test.each(["sample", "@scope/sample"])( expect(actual).toBe(reference); expect(actual).toBe("22\n"); } - // Opting in to one runtime package does not admit its untyped dependency. + // A dependency the package's JavaScript imports compiles without + // declarations of its own: no declaration checks that import. const dependency = join(dir, "node_modules/unchecked-dependency"); await mkdir(dependency, { recursive: true }); await writeFile( @@ -96,14 +97,18 @@ test.each(["sample", "@scope/sample"])( join(pkg, "index.js"), "import { adjust } from 'unchecked-dependency'; export function calculate(value) { return adjust(value) * 3 + 1; }\n", ); - const refused = await compileLibrary({ profilePath, outDir: join(dir, "refused") }); - expect(refused.ok).toBe(false); - if (!refused.ok) - expect( - refused.diagnostics.some( - (d) => d.code === "SC4020" && d.message.includes("unchecked-dependency"), - ), - ).toBe(true); + const withDependency = await compileLibrary({ profilePath, outDir: join(dir, "dependency") }); + if (!withDependency.ok) throw new Error(JSON.stringify(withDependency.diagnostics)); + if (process.env["SCRIPTC_PORTABLE_ONLY"] !== "1") { + const source = join(dir, "dependency-host.c"), + binary = join(dir, "dependency-host"); + await writeFile( + source, + '#include \nextern void st_init(void); extern double st_result(void);\nint main(void) { st_init(); printf("%.0f\\n", st_result()); }\n', + ); + execFileSync("clang", [source, withDependency.archivePath, "-lm", "-o", binary]); + expect(execFileSync(binary, { encoding: "utf8" })).toBe("22\n"); + } expect(await readFile(join(types, "index.d.ts"), "utf8")).toBe(declaration); } finally { await rm(dir, { recursive: true, force: true }); diff --git a/tests/harness/npm-static.test.ts b/tests/harness/npm-static.test.ts index 7ee76307f..388da2138 100644 --- a/tests/harness/npm-static.test.ts +++ b/tests/harness/npm-static.test.ts @@ -1490,6 +1490,24 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => { ]); }, 180_000); + // 7546: a one-line minified ES module: a var statement whose later + // declarators spread earlier ones, regexes read through an untyped rules + // table, and a function instantiated with typed and untyped arguments. + test("the minified module 7546-minified-esm-rules compiles statically and matches Node", async () => { + await expectNodeParity(join(fixturesRoot, "npm/cases/7546-minified-esm-rules/main.ts"), [ + "gtminimark", + ]); + }, 180_000); + + // 7547: an identifier-minified esbuild CommonJS bundle: the __commonJS + // helper and chunk parameters renamed, { value: !0 } stamps, and + // (0, ns.member)(...) calls of builtin module functions. + test("the identifier-minified bundle 7547-minified-cjs-chunks compiles statically and matches Node", async () => { + await expectNodeParity(join(fixturesRoot, "npm/cases/7547-minified-cjs-chunks/main.ts"), [ + "gtminicache", + ]); + }, 180_000); + // A whitespace/syntax-minified bundle whose helpers keep their names is // judged by the recognizers, not refused up front as minified. test("--npm-static=auto opts a minified bundle with named helpers in", () => { diff --git a/tests/harness/npm.test.ts b/tests/harness/npm.test.ts index 9a13c0968..461b7fbcd 100644 --- a/tests/harness/npm.test.ts +++ b/tests/harness/npm.test.ts @@ -112,13 +112,16 @@ 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, and zod3 and zod3-infer - * compile zod 3's 3,700-line types module. A sanitized build of either - * takes minutes on a CI runner. */ + * 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, }; diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index 4d5546ab6..fb15e80a4 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: 33, marker: "scr_runtime_abi_v33" }, + runtime_abi: { version: 37, marker: "scr_runtime_abi_v37" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: [], sanitizer: "external-toolchain-required" }, flavors: { diff --git a/tests/library-mode/npm-refuse/lib-minified.ts b/tests/library-mode/npm-refuse/lib-minified.ts index 9e5c16e75..492cd5369 100644 --- a/tests/library-mode/npm-refuse/lib-minified.ts +++ b/tests/library-mode/npm-refuse/lib-minified.ts @@ -1,5 +1,5 @@ -// SC4020 fixture: slimmed ships a one-long-line dist — minified JS fails -// the eligibility bar. +// slimmed ships a one-long-line dist: minified JS compiles like any other +// eligible package. import { pick } from "slimmed"; export function f(a: number, b: number): number { diff --git a/tests/library-mode/npm-static/lib.ts b/tests/library-mode/npm-static/lib.ts index 27107f29d..15bad17a3 100644 --- a/tests/library-mode/npm-static/lib.ts +++ b/tests/library-mode/npm-static/lib.ts @@ -1,5 +1,5 @@ // Library-mode npm posture fixture (static-or-refuse): a bare specifier -// naming an ELIGIBLE package (own .d.ts, unminified shipped JS, no +// naming an ELIGIBLE package (own .d.ts, no // build-transform markers) compiles statically as part of the library // graph — no flag, no island — and the package's own top-level require // ("mathdep") joins through the same bar. The node:path import alongside diff --git a/tests/library-mode/npm-static/node_modules/mathkit/index.js b/tests/library-mode/npm-static/node_modules/mathkit/index.js index f9f9242c3..eadfefba9 100644 --- a/tests/library-mode/npm-static/node_modules/mathkit/index.js +++ b/tests/library-mode/npm-static/node_modules/mathkit/index.js @@ -1,6 +1,6 @@ "use strict"; // A vendored fake package for the library-mode npm posture: eligible by -// every bar (own .d.ts, unminified CJS, no build-transform markers), with +// every bar (own .d.ts, no build-transform markers), with // its OWN npm dep edge (mathdep) so the transitive fixpoint is exercised. const steps = require("mathdep");