diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bbc9ddb10..5a100e374 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -364,6 +364,20 @@ jobs: target: wasm32-wasi helper: "false" - run: pnpm build + # The sanitized seed compiles the compiler's 700 MB module with one clang + # process under AddressSanitizer. It peaks near 14 GB while the Node + # process that emitted the module still holds about 6 GB. Together they + # exceed the runner's 16 GB of memory, so back them with swap. + - name: Enlarge swap for the sanitized seed + if: matrix.flavor == 'san' + run: | + sudo swapoff -a + sudo rm -f /mnt/swapfile + sudo fallocate -l 24G /mnt/swapfile + sudo chmod 600 /mnt/swapfile + sudo mkswap /mnt/swapfile + sudo swapon /mnt/swapfile + free -h - name: Prepare bootstrap seed run: node scripts/bootstrap-seed.mjs env: diff --git a/CHANGELOG.md b/CHANGELOG.md index 31d34e8db..ec0b27dbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,161 @@ All notable changes to scriptc will be documented in this file. +## 0.4.2 + +### Highlights + +- **Faster builds of large programs:** + - scriptc checks with tsc-ts 0.0.7, which answers contextual types and resolved signatures in batches, so lowering asks for them in one request per chunk instead of one per node. A native build of tsc-ts is 7% faster (110.5 → 103.5 s on 8 vCPUs, lowering 12% faster), with an identical executable. + - The native compiler is built with profile-guided optimization, so tsc-ts builds 6.9% faster cold and 10.7% faster after an edit. + - The native compiler's optimization profile is refreshed, so a native build of tsc-ts is a further 10% faster (124.7 → 111.7 s on 16 vCPUs), with lowering 17% faster. + - The class dispatch passes index their receivers by name instead of testing each one, and they reuse their per-function scans while a function's body is unchanged. + - Checker replies resolve objects the compiler already holds from their ids without decoding them. +- **Leaner builds:** + - The frontend of an optimized executable's first build runs in a child process, so the native code generator starts after its memory is released. The cold build of tsc-ts peaks at 2.21 GB instead of 3.30 GB. + - The compiler collects its heap as lowering and LLVM emission grow it. +- **WebCrypto:** + - The `crypto` global, `node:crypto`'s `webcrypto` and `crypto.subtle` are one object. + - `generateKey`, `importKey`, `exportKey`, `encrypt`, `decrypt`, `deriveBits`, `deriveKey`, `wrapKey` and `unwrapKey` cover HMAC, AES-GCM, AES-CBC, AES-CTR, AES-KW, PBKDF2, HKDF and JSON Web Keys. + - `crypto.subtle` signs and verifies with ECDSA, RSASSA-PKCS1-v1_5, RSA-PSS and Ed25519. + - All of it follows Node 24's argument conversion, algorithm normalization and `DOMException`s. jose's symmetric JWE paths run as in Node. +- **JSX:** `.tsx`, `.jsx` and JavaScript files with JSX compile under the project's `jsx` options. + - `react`, `react-jsx` and `react-jsxdev` lower to the factory calls TypeScript emits, with the same keys, fallbacks and source positions. + - `preserve` and `react-native` are refused with a named diagnostic. +- **Faster worker messages:** + - A message is encoded into one block sized to its contents. + - A typed record is posted from its fields. + - A `'message'` listener with a typed parameter builds it from the message, without a checked-dynamic copy. + - Messages between `MessageChannel` ports take 90 ms where they took 250 ms in 0.4.1 (Node: 337 ms), with a peak RSS of 45 MB instead of 115 MB. +- **Loops over typed arrays vectorize:** a loop to a typed array's length keeps one bounds test for every typed array it indexes. + - Index temporaries such as `const j = i * 4` are computed as integers. + - Uint8ClampedArray stores convert inline. + - Row-major index terms such as `r * cols + j` are tested once before the loop. + - Typed-array callback methods call a literal callback directly on an integer counter. + - An 896 × 896 matrix-vector product with four accumulators takes 55 ms instead of 396 ms (Node: 275 ms). `Uint8Array.map` is 4.5 times as fast and `Int32Array.some` 7.8 times as fast as in 0.4.1. + +### Features + +- **Asymmetric keys and signatures in `node:crypto`:** + - `generateKeyPair`, `generateKeyPairSync`, `createPrivateKey` and `createPublicKey` handle RSA, EC and Ed25519 keys in PEM, DER and JWK. + - `sign`, `verify`, `createSign` and `createVerify` sign and verify with those keys, with Node's output and errors. + - Ed25519 verification matches Node on 2,083 test vectors, including small-order and mixed-order keys. +- **`crypto.createDiffieHellman`, `crypto.getDiffieHellman` and `crypto.createDiffieHellmanGroup`:** + - The modp1, modp2 and modp5 to modp18 groups, explicit primes and generators, and generated safe primes are supported, with Node 24's key widths, `verifyError` bits, key range checks and errors. + - Secrets of 0, 1 and p − 1 are rejected, and so are peer keys outside the RFC 5114 subgroups. + - Primes longer than 8192 bits are refused. +- **TLS:** + - `tls.connect` and `tls.createServer` negotiate `ALPNProtocols`, and both sockets report `alpnProtocol`. `minVersion` and `maxVersion` bound the handshake, and `getProtocol()` reports the version it picked. + - `tls.createServer` takes an `SNICallback` that answers a `SecureContext` synchronously or later. + - `new tls.TLSSocket(socket, { isServer: true, ... })` and `tls.connect({ socket })` run the handshake on an open socket, such as an HTTP `CONNECT` tunnel. + - A client whose peer closes before the handshake completes fails with Node's `ECONNRESET`. +- **Streams:** + - Readable, Writable, Duplex, Transform and PassThrough streams take `objectMode`, `readableObjectMode` and `writableObjectMode`, and an object-mode side carries any value through `push`, `write`, `pipe`, `pipeline`, `for await` and `'data'` listeners. + - `_write` and `_transform` receive the encoding argument as Node passes it. + - `Cipheriv` and `Decipheriv` are Transforms: they can be piped, used as `pipeline()` stages, read and iterated. + - `pipeline()` accepts function stages anywhere among its streams, and an array, a string or a synchronous generator as its source. +- **Packages compiled from their JavaScript:** + - js-yaml and smol-toml compile statically and match Node: ES5 error constructors (`Object.create(Error.prototype)`, `Error.call(this)`, `Error.captureStackTrace`) and classes extending `Date` behave as in Node. + - TypeScript's UMD module wrapper compiles as the module body, and tsc's `__importDefault` and `__importStar` helpers around builtin modules compile as plain `require()` calls. + - Interfaces, type aliases and enums that only a package's `.d.ts` declares keep their types. +- **JavaScript sources:** + - Error constructors can be read as values, so `err.constructor === TypeError` works. + - `workerData` can be destructured. + - Unary `+` converts unions of strings, numbers, booleans, `null` and `undefined` as `Number()` does, so `+process.argv[2]` compiles. + - These compile and behave as in Node: + - chained readline listener registration; + - `Number.parseInt` without a radix; + - `match` and `search` with a possibly undefined regular expression; + - `Buffer.from` over an untyped `ArrayBuffer`, with its byte offset; + - Atomics on untyped arrays. + - Buffer methods run on Buffers held as untyped values. +- **`fs`:** + - The synchronous functions (`readFileSync`, `writeFileSync`, `statSync`, `readdirSync`, `mkdirSync`, `rmSync`, `openSync` and the others) accept `file:` URLs wherever they accept a path, with Node's conversion errors. + - `fs.promises.rm` takes `recursive`, `force`, `maxRetries` and `retryDelay`, and removing a directory without `recursive` throws Node's `ERR_FS_EISDIR`. +- **`TypedArray.of`** creates a typed array of its arguments, and a dynamic iterable spread into `Array.of`, `push` or `TypedArray.of` is drained through the iterator protocol. +- **Global `isFinite` converts its argument with ToNumber,** as `isNaN` does. The Math functions and `String.fromCharCode` evaluate every argument before converting any. +- **`Object.freeze`, `Object.seal` and `Object.preventExtensions` of a function** act on its own properties, and `Object.isFrozen`, `Object.isSealed` and `Object.isExtensible` answer for functions. @noble/hashes' frozen hashers work. +- **EventEmitters used through both typed and untyped references** share one listener list. An untyped `emit` reaches typed listeners, an untyped `emit('error')` follows Node's contract, and `listenerCount`, `eventNames`, `listeners`, `rawListeners`, `removeAllListeners`, `setMaxListeners` and `getMaxListeners` work on untyped values. +- **Template literals, `join`, `Array.prototype.toString` and the default `sort()`** call an object's own or inherited `toString`, as `String()` does, instead of printing `[object Object]`. +- **Classes and generics:** + - A generic factory declared to return a constructor type can return one of its local classes, including an anonymous class expression, and `class X extends factory(...)` extends it. + - Static methods of a function-local class compile. + - Constructor types declared in the program (`new () => unknown`, types that also name static members, construct overloads) hold class values. + - Members typed by a conditional type over a literal class type argument (`Field`) resolve. + - Object literals under an erased generic member may hold callbacks with optional or defaulted parameters. +- **Assignment and update targets wrapped in parentheses or non-null assertions** (`(x) = 1`, `h.sums[k]! += 10`) compile. +- **Untyped values keep what JavaScript code adds:** + - Properties that untyped code adds to a typed object literal stay on it. + - Named properties written to an array through an `unknown` view stay on the array, and `for...in` visits them after the indices. + - An `unknown` value asserted to a structural type with methods keeps its class's methods. + - The type inferred from an empty object literal holds any non-nullish value, as TypeScript's `{}` does. +- **A WebAssembly module the build finds but does not translate** rejects with a `CompileError` that names the reason, for example a proposal outside the translated feature set. +- **SC6101 names the functions that likely run often and use untyped values:** + - It shows up to five, most likely hot first, with why their values are untyped: an `any`, an untyped `JSON.parse` result or an untyped JavaScript file. + - `scriptc coverage --fail-on=dynamic` fails on it, and `--no-dynamic-tier-warning` leaves it out. + +### Performance + +- **Regular expressions skip start positions that their leading character sets rule out,** testing 16 positions at once. Searching 50 MB of text for nine case-insensitive alternations takes 0.83 s instead of 3.88 s (Node: 4.38 s). +- **Loop counters stay integers in more loops:** + - Counters that step beside a counted loop's counter (`for (let i = 0, p = 0; i < n; i++, p += 4)`) stay integers: a pixel-stride loop takes 114 ms instead of 340 ms (Node: 190 ms). + - So do two counters that close in on each other (`lo++, hi--`), 17% faster on a permutation-reversal loop. + - So do counters of loops bounded by a string's length or by an invariant limit other than an array's length, and while-loop counters that nested scans advance. A non-ASCII lexer is 9.5% faster. +- **Loops that call a known callback** test its closure once before the loop and call the function directly, so captured `let` bindings stay in registers. `forEach` sums with a captured accumulator are 25 to 55% faster. +- **A speculative integer loop whose check fails** finishes the iteration outside the ordinary loop, which stays optimizable, and a loop site that keeps failing stops trying. With the hole-free-prefix test that element reads now take first, a pairwise loop over an array of class instances with number fields is about 1.25 times as fast as in 0.4.1. +- **Typed arrays over a SharedArrayBuffer** read and write elements inline as relaxed atomic accesses. A numeric kernel over shared `Float64Array`s takes 0.78 s instead of 1.56 s (Node: 0.84 s). +- **`Math.pow(2, n)` for integer exponents** builds the result from its bits. A counter that masks with `Math.pow(2, length)` takes 11.0 s instead of 20.4 s. +- **Async calls that never suspend run in place,** and so does `await Promise.all` of such calls, with Node's microtask turns and rejection handling. An async pipeline takes 5.5 ms instead of 9.7 ms. +- **Strings:** + - `Number()` of a short integer string returns the integer directly. + - `split` builds its result in one inline block, and `indexOf` and `slice` on ASCII strings skip the UTF-16 index map. A log summary is 20% faster. + - `charAt` and `s[i]` on ASCII strings read the character from a table. + - Chains of one-character `replace` calls (the HTML-escape idiom) replace a block of 16 bytes at a time, 12.8% faster on a template renderer. +- **`JSON.parse` and `JSON.stringify`** scan whole words and append parsed typed arrays in bulk, 22% faster to parse and 10% faster to stringify records. +- **Smaller arrays, maps and program-lifetime instances:** + - The array header is 32 bytes instead of 48. + - Arrays of up to 16 known elements keep their slots in their own block. + - Arrays of a known final length get exactly that capacity. + - Small map entry arrays grow in finer steps. + - On Linux, `ProgramLifetime` instances take a 32-bit header and are referenced in 4 bytes. + - They lower the peak memory of a large TypeScript type checker: the 4-byte references alone take a large project's check from 4.13 GB to 3.75 GB, and the 32-bit header saves a further 5%. +- **Worker messages queued together are delivered in one batch** before immediates run. 20,000 messages interleaved with a worker's output drain in 0.5 s instead of 5.6 s (Node: 0.57 s). +- **The cycle collector** never buffers instances of classes whose fields hold nothing cycle-capable, and its per-object paths are shorter. A workload of cyclic graphs is about 5% faster. +- **Array helpers specialized for a callback that cannot throw** skip the pending-exception test, 4.5% faster on a functional pipeline. +- **Compile time:** + - A file's function declarations are walked once for their references, so lowering a 10 MB single-module bundle takes 69 s instead of 219 s. + - LLVM emission computes its module facts once, 6.9% faster on tsc-ts. + - AST walks visit subtrees by their node ranges and read node words through a `Uint32Array`. + - Program builds no longer import runtime bitcode, so builds through Node and through the native compiler produce identical executables. + +### Fixes + +- **Member reads and method calls on an array hole** held in a binding narrowed to one arm of a wide union throw Node's `Cannot read properties of undefined` TypeError. +- **`AsyncLocalStorage`:** + - A store entered before an async function's first await, or in a body that never suspends, is the caller's store after the call. + - Each synchronous generator step runs in its resumer's context, as in Node. +- **Generic arrow and function-expression bindings used before their declarations** behave as in Node, including the `ReferenceError` of a `let` or `const`. +- **Assignment expressions into a module `let` declared without an initializer** (`new Promise((r) => (resolve = r))`) store the value instead of throwing a `ReferenceError`. +- **The `extends` expression of a class defined inside a function** is evaluated each time the definition runs. +- **A `super()` call that leads an `if` condition's comma chain** compiles. +- **A catch binding tests `instanceof`** correctly for values thrown as `unknown`, rejected promises and runtime errors. +- **zlib callbacks receive `undefined` as their result when the operation fails,** as in Node, and a callback may assign a fallback to it. +- **`Readable.push` and `Writable.write` with `string | null` or `Buffer | null` chunks** no longer crash. +- **Object literals keep their key order** through compiler rewrites and on shapes a typed `JSON.parse` also builds, so `JSON.stringify`, `Object.keys` and `console.log` match Node. +- **`console.log` prints class instances with their class name** in programs that test `instanceof` on unknown values. +- **`String`, `Number` and `Boolean` passed to array methods** convert as their direct calls do, and nested array literals in a recursive JSON-style union build. +- **A dynamic method call that the program also reads through a prototype** finds the method when the instance's property bag does not hold it. +- **Storing an element on `Array.prototype`** fails with SC2020, because holes in native arrays do not read inherited elements. +- **A recursive type alias in a generic signature** no longer crashes the compiler. +- **A `Buffer` view of an untyped typed array** compiles. +- **JSX programs checked with tsgo** compile as they do with tsc-ts. +- **Two programs whose entries share a name and build into one directory** no longer share a cached translation unit, so a later cache hit always relinks the right program. +- **A warm build whose cached LLVM exceeds the engine's string limit** reads it in parts instead of failing with `Invalid string length`. +- **The same program compiles to the same executable wherever it sits on disk** and whatever order its functions lower in, and edits outside a declaration do not rename the helpers and closures inside it. +- **Libraries that use untyped byte arrays** link. + + + ## 0.4.1 ### Highlights @@ -77,8 +232,6 @@ All notable changes to scriptc will be documented in this file. - **TypeScript enum exports in compiled packages stay `undefined` until they are assigned,** as in Node. - **Element writes accept a possibly undefined index** (typed-array writes, compound and update writes, element assignments used as values and destructuring targets) and convert it with ToNumber, as element reads already did. - - ## 0.4.0 ### Breaking changes diff --git a/benchmarks/runtime/README.md b/benchmarks/runtime/README.md index 2653ec51a..4f152e4bb 100644 --- a/benchmarks/runtime/README.md +++ b/benchmarks/runtime/README.md @@ -11,6 +11,8 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea | `channel-messages` | cpu | 200,000 MessageChannel messages in one thread, half numbers and half small objects, all posted before the port delivers any | | `csv-numbers` | cpu | `split`, `Number`, `parseInt`, `toFixed` | | `cyclic-graphs` | cpu | object graphs with back references built and dropped: trees with parent and sibling links, rings of records sharing a long-lived registry, closures capturing their owner | +| `dictionary-walks` | cpu | `Record` dictionaries walked with for-in and `Object.keys`/`values`/`entries` while entries are added, updated and deleted | +| `dynamic-methods` | cpu | untyped JavaScript calling builtin methods on values it does not type: string `slice`/`indexOf`/`toUpperCase`/`padStart`, array `push`/`pop`/`includes`, `Map` and `Set`, and an object's own methods | | `functional-pipeline` | cpu | `filter`/`map`/`reduce`/`flatMap` chains, closures, small records | | `intern-table`, `intern-table-map`, `intern-table-1`, `intern-table-map-1` | cpu | worker threads interning the same type-like objects under string keys and records under number keys, then hits and misses: one shared `InternTable` per kind of key, or a module-level `Map` per thread (the `-map` entries), with 8 threads or 1 (the `-1` entries); under Node.js every thread has its own tables | | `json-records` | cpu | `JSON.stringify`, typed `JSON.parse`, Map aggregation | diff --git a/benchmarks/runtime/workloads/dictionary-walks.json b/benchmarks/runtime/workloads/dictionary-walks.json new file mode 100644 index 000000000..ebfb6ea3f --- /dev/null +++ b/benchmarks/runtime/workloads/dictionary-walks.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/dictionary-walks.ts", + "args": [ + "1" + ] +} diff --git a/benchmarks/runtime/workloads/dictionary-walks.ts b/benchmarks/runtime/workloads/dictionary-walks.ts new file mode 100644 index 000000000..30b18be63 --- /dev/null +++ b/benchmarks/runtime/workloads/dictionary-walks.ts @@ -0,0 +1,53 @@ +// String-keyed dictionaries (`Record`) walked with for-in, +// Object.keys, Object.values and Object.entries while entries are added, +// updated and deleted: per-category totals, a pruning pass and a merge. +let seed = 777; +function random(): number { + seed = (Math.imul(seed, 1103515245) + 12345) >>> 0; + return seed / 4294967296; +} + +const scale = Number(process.argv[2] ?? "1"); +const categories = 4000; +const counts: Record = {}; +for (let i = 0; i < Math.floor(200000 * scale); i++) { + const key = "c" + Math.floor(random() * categories); + counts[key] = (counts[key] ?? 0) + 1; +} + +let walked = 0; +let sum = 0; +for (let pass = 0; pass < 40; pass++) { + for (const key in counts) { + walked++; + sum += counts[key]!; + } +} + +// Prune: delete the small categories during the walk, including keys the +// walk has not reached yet. +let pruned = 0; +for (const key in counts) { + const n = counts[key]!; + if (n < 45) { + delete counts[key]; + pruned++; + } + const ahead = "c" + ((Number(key.slice(1)) + 1) % categories); + if (n > 60 && ahead in counts && counts[ahead]! < 45) { + delete counts[ahead]; + pruned++; + } +} + +// Merge into a second dictionary and read it back every way. +const merged: Record = {}; +for (const key of Object.keys(counts)) merged[key.toUpperCase()] = counts[key]! * 2; +let values = 0; +for (const v of Object.values(merged)) values += v; +let entries = 0; +for (const [key, v] of Object.entries(merged)) entries += key.length + v; + +console.log( + `walked ${walked}, sum ${sum}, pruned ${pruned}, kept ${Object.keys(counts).length}, values ${values}, entries ${entries}`, +); diff --git a/benchmarks/runtime/workloads/dynamic-methods.json b/benchmarks/runtime/workloads/dynamic-methods.json new file mode 100644 index 000000000..fa27f4ea8 --- /dev/null +++ b/benchmarks/runtime/workloads/dynamic-methods.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/dynamic-methods.mjs", + "args": [ + "300000" + ] +} diff --git a/benchmarks/runtime/workloads/dynamic-methods.mjs b/benchmarks/runtime/workloads/dynamic-methods.mjs new file mode 100644 index 000000000..bbf217701 --- /dev/null +++ b/benchmarks/runtime/workloads/dynamic-methods.mjs @@ -0,0 +1,36 @@ +// Untyped JavaScript calling builtin methods on values it never declares +// types for: string slice/indexOf/toUpperCase/startsWith/padStart, array +// push/pop/at/join/includes/indexOf, Map and Set get/set/has/add, number +// toFixed, and calls of an object's own methods. Argument: the number of +// iterations. +const rounds = Number(process.argv[2] ?? "300000"); +const words = ["alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta"]; +const counts = new Map(); +const seen = new Set(); +const stack = []; +const recent = []; +const tally = { + total: 0, + add(value) { + this.total += value; + return this.total; + }, +}; +let checksum = 0; +for (let i = 0; i < rounds; i++) { + const word = words[i % words.length]; + const head = word.slice(0, 3).toUpperCase(); + const key = head.padStart(4, "-"); + if (word.indexOf("e") >= 0 && !word.startsWith("z")) checksum += 1; + counts.set(key, (counts.get(key) ?? 0) + 1); + seen.add(i % 97); + if (seen.has(i % 89)) checksum += 2; + stack.push(i); + if (stack.length > 32) checksum += stack.pop() % 3; + recent.push(word); + if (recent.length > 4) recent.shift(); + if (recent.includes("gamma")) checksum += recent.indexOf("gamma"); + checksum += tally.add(i % 5) % 11; + if (i % 1000 === 0) checksum += Number((i / 7).toFixed(2)) | 0; +} +console.log(checksum, counts.size, seen.size, stack.at(-1), recent.join(","), tally.total); diff --git a/docs/content/docs/contracts.mdx b/docs/content/docs/contracts.mdx index 5af1cad2f..d5949c4cf 100644 --- a/docs/content/docs/contracts.mdx +++ b/docs/content/docs/contracts.mdx @@ -349,7 +349,7 @@ class Identifier extends SyntaxNode { } ``` -On Linux, except in builds with sanitizers, the instances also take less memory. An executable keeps them in one address range below 32 GiB that it reserves at startup (only the pages in use take memory), and a field typed `C`, `C | undefined` or `C | null`, where `C` is a program-lifetime class, stores the instance's address in 4 bytes instead of 8. In the example, `parent` takes 4 bytes. +On Linux, except in builds with sanitizers, the instances also take less memory. An executable keeps them in one address range below 32 GiB that it reserves at startup (only the pages in use take memory), and a field typed `C`, `C | undefined` or `C | null`, where `C` is a program-lifetime class, stores the instance's address in 4 bytes instead of 8, as does each element of an array of such a type and each value of a `Map` with `string` keys and such a value type. In the example, `parent` takes 4 bytes. scriptc checks that the promise cannot change what the program computes (SC7008): diff --git a/docs/content/docs/coverage.mdx b/docs/content/docs/coverage.mdx index ae36f736d..954b040db 100644 --- a/docs/content/docs/coverage.mdx +++ b/docs/content/docs/coverage.mdx @@ -38,6 +38,8 @@ When a program has unsupported operations, the report adds a group for each root compile staticallyStatements that compile to native instructions. in dynamic tierOf those, the statements in functions that operate on values typed only at runtime, such as any values and untyped JavaScript. Shown when there are any. See Dynamic tier. blockersUnsupported operations with counts, diagnostic codes, reasons, locations, and hints. + runs at startupDeferred operations in a JavaScript module's top-level code, which runs when the module loads: at startup for a module the program imports. + deferred to runtimeOther deferred operations in JavaScript code. Each throws only if it runs. in unreached codeBlockers in code the entry never reaches. A build never compiles that code, so these cannot fail it. differs from NodeSites that compile but can behave differently from Node.js. See Node divergences. npm package compilationNative package compilation outcomes and refusal reasons. @@ -46,7 +48,7 @@ When a program has unsupported operations, the report adds a group for each root Blockers in unreached code and deferred operations in JavaScript packages are listed separately because they don't fail a build. A deferred operation throws a runtime error if it runs, so a successful build doesn't mean every API of a package works; see [unsupported code inside a package](/docs/dependencies#unsupported-code-inside-a-package). -If a function's signature can't compile, the statements in its body count as not compiling, so the percentage reflects how much of the program compiles. +If a function's signature can't compile, the statements in its body count as not compiling, so the percentage reflects how much of the program compiles. The report still lists the diagnostics of those statements; a use of a parameter whose type can't compile reports `SC2004` back to the signature. @@ -211,6 +213,10 @@ For editors, CI annotations, and other tools, `scriptc coverage --print=diagnost group The id of the root-cause group the diagnostic belongs to. A group's primary site is its first in source order. + + startup + true on a deferred diagnostic in a module's top-level code, which runs when the module loads. Absent otherwise. + diff --git a/docs/content/docs/dependencies.mdx b/docs/content/docs/dependencies.mdx index 85a232bb2..409b9cb30 100644 --- a/docs/content/docs/dependencies.mdx +++ b/docs/content/docs/dependencies.mdx @@ -132,7 +132,7 @@ CommonJS packages compile with Node.js's module semantics. Each `require()` eval ## Unsupported code inside a package -A JavaScript package can contain operations scriptc can't compile, often in functions your program never calls. These don't fail the build: each one compiles to code that throws a runtime error if it ever runs. Coverage reports list them as deferred sites, so review the list and test the parts of the package your program uses. +A JavaScript package can contain operations scriptc can't compile, often in functions your program never calls. These don't fail the build: each one compiles to code that throws a runtime error if it ever runs. Coverage reports list them as deferred sites, so review the list and test the parts of the package your program uses. Sites in a module's top-level code are listed under `runs at startup`: that code runs when the module loads, at startup for a module the program imports. ## Provenance sources (experimental) diff --git a/docs/content/limitations/builtin-apis/crypto.mdx b/docs/content/limitations/builtin-apis/crypto.mdx index c84ce3e52..971065408 100644 --- a/docs/content/limitations/builtin-apis/crypto.mdx +++ b/docs/content/limitations/builtin-apis/crypto.mdx @@ -8,7 +8,7 @@ `randomFill`, `randomFillSync` (every view kind and ArrayBuffer), `randomInt`, `randomUUID`, and `randomUUIDv7` follow Node 24.22, including callback timing; a `randomInt` callback whose error parameter is typed `Error | null` receives `null` where Node passes `undefined`. Scrypt supports its default parameters, numeric options, aliases, and memory limit. Both KDFs accept strings, Buffers, numeric typed arrays, DataViews, and fixed-length ArrayBuffers. - `createCipheriv` and `createDecipheriv` support aes-128, aes-192, and aes-256 in CBC, CTR, and GCM modes and chacha20-poly1305, on native targets, through `update()`, `final()`, `setAAD()`, `setAutoPadding()`, `getAuthTag()`, and `setAuthTag()`; other ciphers and wasm32-wasi builds are refused. + `createCipheriv` and `createDecipheriv` support aes-128, aes-192, and aes-256 in CBC, CTR, GCM, ECB, CFB, CFB8, CFB1, and OFB modes, des-ede3 in CBC, ECB, CFB, CFB8, CFB1, and OFB modes, des-ede in CBC, ECB, CFB, and OFB modes, chacha20, and chacha20-poly1305, on native targets, through `update()`, `final()`, `setAAD()`, `setAutoPadding()`, `getAuthTag()`, and `setAuthTag()`; other ciphers and wasm32-wasi builds are refused. Cipheriv and Decipheriv values are also Transforms: they can be written, piped into, used as `pipeline()` stages, read, iterated with `for await`, and listened to, and their stream properties read as in Node; written chunks go through `update()`, and the end pushes `final()` or reports its error through `'error'`. Through `unknown` and `any` slots and untyped JavaScript, in a program that uses streams, their stream properties and their `on`, `once`, `off`, `pipe`, `unpipe`, `read`, `pause`, `resume`, `setEncoding`, `write`, `end`, `destroy`, `cork`, and `uncork` members work; `write` and `end` take no encoding or callback there, and the other stream members throw a coded SC2020 error. Hash, Hmac, Cipheriv, and Decipheriv streams count a written string's bytes in `writableLength`, where Node counts the string's length. @@ -25,6 +25,9 @@ `Object.keys()` and `in` do not see a DiffieHellman's own `verifyError` property, and `JSON.stringify()` and `instanceof` are refused. After `createDiffieHellman(primeLength)` throws `Invalid DH parameters`, the next OpenSSL error Node throws can list the failed generation's errors in `opensslErrorStack`; errors in a compiled binary list only their own. `crypto.diffieHellman`, which takes DH, EC, or X25519 KeyObjects, and the `DiffieHellman` and `DiffieHellmanGroup` constructors are not available. + `checkPrime`, `checkPrimeSync`, `generatePrime`, and `generatePrimeSync` support bigint, ArrayBuffer, typed-array, Buffer, and DataView values and the `checks`, `safe`, `add`, `rem`, and `bigint` options on native targets, with Node's errors; the callback forms compute before they return and call back in a later macrotask. + Candidates and sizes longer than 262144 bits throw a coded SC2020 error, and wasm32-wasi builds are refused. + Node calls their callbacks with `undefined` as the error, which a callback whose error parameter is typed `Error | null`, as @types/node declares it, receives as `null`. RSA, EC, and Ed25519 private and public KeyObjects come from `generateKeyPairSync`, `generateKeyPair`, `createPrivateKey`, and `createPublicKey`, on native targets. 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. diff --git a/docs/content/limitations/builtin-apis/map-and-set.mdx b/docs/content/limitations/builtin-apis/map-and-set.mdx index bf77001ee..ee7435b2d 100644 --- a/docs/content/limitations/builtin-apis/map-and-set.mdx +++ b/docs/content/limitations/builtin-apis/map-and-set.mdx @@ -7,4 +7,6 @@ They support `.next()`, `for...of`, array spreads, and mapper-less `Array.from`. They also support the iterator helper methods, such as `.map()` and `.take()`. Detached collection methods are unsupported. + Constructing a Map, Set, WeakMap or WeakSet, or calling `Object.fromEntries`, with a checked-dynamic iterable (a native iterator, a generator, or an object with a `Symbol.iterator` method) takes all of its values before adding the first one. + When an entry of a Map seed is not an object, the TypeError comes after the iterable is exhausted; Node closes the iterator at that entry. The Set composition methods (`union`, `intersection`, `difference`, `symmetricDifference`, `isSubsetOf`, `isSupersetOf`, `isDisjointFrom`) accept a Set of the same element type or a Map keyed by it; other set-like arguments are unsupported. diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index 574c49ff4..c341da852 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -11085,9 +11085,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -11108,9 +11108,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -11614,9 +11614,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -11637,9 +11637,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 31ce5da02..9393fcaa8 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -19328,6 +19328,10 @@ "tests/corpus/7084-crypto-key-pairs.cjs", "tests/corpus/7122-webcrypto-node-crypto.ts", "tests/corpus/7123-webcrypto-node-crypto.cjs", + "tests/corpus/7150-cipher-legacy-modes.ts", + "tests/corpus/7151-cipher-legacy-errors.cjs", + "tests/corpus/7152-crypto-prime.ts", + "tests/corpus/7153-crypto-prime-errors.cjs", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/node24-crypto-kdf.ts", @@ -19658,8 +19662,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "surface-manifest:node-builtin.crypto.checkPrime", + "tests": [ + "tests/corpus/7152-crypto-prime.ts", + "tests/corpus/7153-crypto-prime-errors.cjs" + ] }, "anchorSource": "exact" }, @@ -19681,8 +19689,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "surface-manifest:node-builtin.crypto.checkPrimeSync", + "tests": [ + "tests/corpus/7152-crypto-prime.ts", + "tests/corpus/7153-crypto-prime-errors.cjs" + ] }, "anchorSource": "exact" }, @@ -19707,6 +19719,8 @@ "status": "partial", "evidence": "surface-manifest:node-builtin.crypto.createCipheriv", "tests": [ + "tests/corpus/7150-cipher-legacy-modes.ts", + "tests/corpus/7151-cipher-legacy-errors.cjs", "tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", @@ -19746,6 +19760,8 @@ "status": "partial", "evidence": "surface-manifest:node-builtin.crypto.createDecipheriv", "tests": [ + "tests/corpus/7150-cipher-legacy-modes.ts", + "tests/corpus/7151-cipher-legacy-errors.cjs", "tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", @@ -20364,8 +20380,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "surface-manifest:node-builtin.crypto.generatePrime", + "tests": [ + "tests/corpus/7152-crypto-prime.ts", + "tests/corpus/7153-crypto-prime-errors.cjs" + ] }, "anchorSource": "exact" }, @@ -20387,8 +20407,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "surface-manifest:node-builtin.crypto.generatePrimeSync", + "tests": [ + "tests/corpus/7152-crypto-prime.ts", + "tests/corpus/7153-crypto-prime-errors.cjs" + ] }, "anchorSource": "exact" }, diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index a59d6365a..06aad26cf 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -42,8 +42,10 @@ "node-builtin.child_process": ["tests/corpus/1361-spawn-events.ts", "tests/corpus/1565-spawn-pipe-streams.ts", "tests/corpus/2911-execfile-callback.ts", "tests/corpus/2939-child-shell-text.ts", "tests/corpus/2946-child-fork-ipc/main.ts", "tests/corpus/3127-spawn-runtime-options.ts", "tests/corpus/3130-child-generic-streams.ts", "tests/corpus/3131-child-rest-listeners.js", "tests/corpus/3132-child-generic-stream-completion.ts"], "node-builtin.cluster": ["tests/corpus/957-builtins-namespace.ts"], "node-builtin.console": ["tests/corpus/console-imports.cjs", "tests/corpus/2440-console-inspect-args.ts", "tests/corpus/console-native-value.js", "tests/harness/npm-static.test.ts"], - "node-builtin.crypto.createCipheriv": ["tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/corpus/7055-cipher-streams.ts", "tests/corpus/7056-cipher-stream-errors.ts", "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts"], - "node-builtin.crypto.createDecipheriv": ["tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/corpus/7055-cipher-streams.ts", "tests/corpus/7056-cipher-stream-errors.ts", "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts"], + "node-builtin.crypto.checkPrime": ["tests/corpus/7152-crypto-prime.ts", "tests/corpus/7153-crypto-prime-errors.cjs"], + "node-builtin.crypto.checkPrimeSync": ["tests/corpus/7152-crypto-prime.ts", "tests/corpus/7153-crypto-prime-errors.cjs"], + "node-builtin.crypto.createCipheriv": ["tests/corpus/7150-cipher-legacy-modes.ts", "tests/corpus/7151-cipher-legacy-errors.cjs", "tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/corpus/7055-cipher-streams.ts", "tests/corpus/7056-cipher-stream-errors.ts", "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts"], + "node-builtin.crypto.createDecipheriv": ["tests/corpus/7150-cipher-legacy-modes.ts", "tests/corpus/7151-cipher-legacy-errors.cjs", "tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/corpus/7055-cipher-streams.ts", "tests/corpus/7056-cipher-stream-errors.ts", "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts"], "node-builtin.crypto.createDiffieHellman": ["tests/corpus/6706-dh-groups.ts", "tests/corpus/6707-dh-explicit-params.ts", "tests/corpus/6708-dh-errors.js", "tests/corpus/6709-dh-generate.ts", "tests/corpus/6710-dh-encodings.cjs", "tests/corpus/6711-dh-handles.ts", "tests/fixtures/node-types/dh.ts"], "node-builtin.crypto.createDiffieHellmanGroup": ["tests/corpus/6706-dh-groups.ts", "tests/corpus/6707-dh-explicit-params.ts", "tests/corpus/6708-dh-errors.js", "tests/corpus/6709-dh-generate.ts", "tests/corpus/6710-dh-encodings.cjs", "tests/corpus/6711-dh-handles.ts", "tests/fixtures/node-types/dh.ts"], "node-builtin.crypto.createECDH": ["tests/corpus/6700-ecdh-shared-secret.ts", "tests/corpus/6701-ecdh-formats.ts", "tests/corpus/6702-ecdh-errors.js", "tests/corpus/6703-ecdh-private-keys.ts", "tests/corpus/6704-ecdh-set-public-key.cjs", "tests/corpus/6705-ecdh-handles.ts", "tests/fixtures/node-types/ecdh.ts"], @@ -120,6 +122,8 @@ "node-builtin.os.version": ["tests/corpus/5140-os-host-facts.ts"], "node-builtin.os.constants": ["tests/corpus/5141-os-constants.ts"], "node-builtin.fs.constants": ["tests/corpus/6207-fs-constants-object.ts", "tests/corpus/6208-fs-constants-require.cjs"], + "node-builtin.crypto.generatePrime": ["tests/corpus/7152-crypto-prime.ts", "tests/corpus/7153-crypto-prime-errors.cjs"], + "node-builtin.crypto.generatePrimeSync": ["tests/corpus/7152-crypto-prime.ts", "tests/corpus/7153-crypto-prime-errors.cjs"], "node-builtin.crypto.getDiffieHellman": ["tests/corpus/6706-dh-groups.ts", "tests/corpus/6707-dh-explicit-params.ts", "tests/corpus/6708-dh-errors.js", "tests/corpus/6709-dh-generate.ts", "tests/corpus/6710-dh-encodings.cjs", "tests/corpus/6711-dh-handles.ts", "tests/fixtures/node-types/dh.ts"], "node-builtin.crypto.getRandomValues": ["tests/corpus/6204-crypto-get-random-values.ts", "tests/corpus/6205-crypto-get-random-values.js"], "node-builtin.url.parse": ["tests/corpus/5220-url-parse-warnings.ts", "tests/corpus/5221-url-parse-shapes.ts", "tests/corpus/5222-url-parse-hosts.ts", "tests/corpus/5223-url-parse-query.ts", "tests/corpus/5227-url-legacy-dependency/main.ts", "tests/corpus/5229-url-legacy-errors.cjs", "packages/runtime/test/url-legacy.test.ts", "tests/fixtures/node-types/url-legacy.ts"], diff --git a/packages/cli-darwin-arm64/package.json b/packages/cli-darwin-arm64/package.json index 482e5f1e5..23584b60a 100644 --- a/packages/cli-darwin-arm64/package.json +++ b/packages/cli-darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/cli-darwin-arm64", - "version": "0.4.1", + "version": "0.4.2", "description": "Native scriptc compiler for darwin-arm64", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/cli-darwin-x64/package.json b/packages/cli-darwin-x64/package.json index b1ced2c03..d9e6dafc6 100644 --- a/packages/cli-darwin-x64/package.json +++ b/packages/cli-darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/cli-darwin-x64", - "version": "0.4.1", + "version": "0.4.2", "description": "Native scriptc compiler for darwin-x64", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/cli-linux-arm64-gnu/package.json b/packages/cli-linux-arm64-gnu/package.json index 0aa213279..5b8cb4db2 100644 --- a/packages/cli-linux-arm64-gnu/package.json +++ b/packages/cli-linux-arm64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/cli-linux-arm64-gnu", - "version": "0.4.1", + "version": "0.4.2", "description": "Native scriptc compiler for linux-arm64-gnu", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/cli-linux-arm64-musl/package.json b/packages/cli-linux-arm64-musl/package.json index 4ac5434ae..0901602ec 100644 --- a/packages/cli-linux-arm64-musl/package.json +++ b/packages/cli-linux-arm64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/cli-linux-arm64-musl", - "version": "0.4.1", + "version": "0.4.2", "description": "Native scriptc compiler for linux-arm64-musl", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/cli-linux-x64-gnu/package.json b/packages/cli-linux-x64-gnu/package.json index 7473c36bc..f01478aad 100644 --- a/packages/cli-linux-x64-gnu/package.json +++ b/packages/cli-linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/cli-linux-x64-gnu", - "version": "0.4.1", + "version": "0.4.2", "description": "Native scriptc compiler for linux-x64-gnu", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/cli-linux-x64-musl/package.json b/packages/cli-linux-x64-musl/package.json index 08e048cd1..a6986a66a 100644 --- a/packages/cli-linux-x64-musl/package.json +++ b/packages/cli-linux-x64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/cli-linux-x64-musl", - "version": "0.4.1", + "version": "0.4.2", "description": "Native scriptc compiler for linux-x64-musl", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/cli-win32-x64-msvc/package.json b/packages/cli-win32-x64-msvc/package.json index 54e323839..6b03811b8 100644 --- a/packages/cli-win32-x64-msvc/package.json +++ b/packages/cli-win32-x64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/cli-win32-x64-msvc", - "version": "0.4.1", + "version": "0.4.2", "description": "Native scriptc compiler for win32-x64-msvc", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/cli/package.json b/packages/cli/package.json index d66ba3247..034747c29 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "scriptc", - "version": "0.4.1", + "version": "0.4.2", "description": "Compile ordinary TypeScript and JavaScript to small, fast native executables and WebAssembly modules that run without Node.js", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/cli/test/native-link-info.test.ts b/packages/cli/test/native-link-info.test.ts index a7a5e718a..a1b673630 100644 --- a/packages/cli/test/native-link-info.test.ts +++ b/packages/cli/test/native-link-info.test.ts @@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => { minimum_os: "14.0", }, program: { object, entry_symbol: "main" }, - runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" }, + runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" }, runtime_pack: { kind: "precompiled", package: "@scriptc/runtime-darwin-arm64", @@ -165,6 +165,6 @@ describe.runIf(supported)("macOS arm64 native link info", () => { () => null, (failure: { stderr?: string }) => failure, ); - expect(error?.stderr).toContain("scr_runtime_abi_v27"); + expect(error?.stderr).toContain("scr_runtime_abi_v28"); }); }); diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 2a3f11be9..5f3805813 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -3171,7 +3171,8 @@ declare module "crypto" { callback: (error: Error | null, buffer: Buffer) => void, ): void; export type BinaryLike = string | ArrayBuffer | ArrayBufferView; - /* Cipheriv/Decipheriv: aes-128/192/256 in cbc, ctr, and gcm modes and + /* Cipheriv/Decipheriv: aes-128/192/256 in cbc, ctr, gcm, ecb, cfb, cfb8, + * cfb1, and ofb modes, des-ede3 and des-ede, chacha20, and * chacha20-poly1305. Both are Transforms: written chunks go through * update(), and the end pushes final(). */ export type CipherGCMTypes = "aes-128-gcm" | "aes-192-gcm" | "aes-256-gcm"; @@ -3351,6 +3352,61 @@ declare module "crypto" { ): DiffieHellman; export function getDiffieHellman(groupName: string): DiffieHellmanGroup; export function createDiffieHellmanGroup(name: string): DiffieHellmanGroup; + export type LargeNumberLike = ArrayBufferView | SharedArrayBuffer | ArrayBuffer | bigint; + export interface GeneratePrimeOptions { + add?: LargeNumberLike | undefined; + rem?: LargeNumberLike | undefined; + safe?: boolean | undefined; + bigint?: boolean | undefined; + } + export interface GeneratePrimeOptionsBigInt extends GeneratePrimeOptions { + bigint: true; + } + export interface GeneratePrimeOptionsArrayBuffer extends GeneratePrimeOptions { + bigint?: false | undefined; + } + export function generatePrime( + size: number, + callback: (err: Error | null, prime: ArrayBuffer) => void, + ): void; + export function generatePrime( + size: number, + options: GeneratePrimeOptionsBigInt, + callback: (err: Error | null, prime: bigint) => void, + ): void; + export function generatePrime( + size: number, + options: GeneratePrimeOptionsArrayBuffer, + callback: (err: Error | null, prime: ArrayBuffer) => void, + ): void; + export function generatePrime( + size: number, + options: GeneratePrimeOptions, + callback: (err: Error | null, prime: ArrayBuffer | bigint) => void, + ): void; + export function generatePrimeSync(size: number): ArrayBuffer; + export function generatePrimeSync(size: number, options: GeneratePrimeOptionsBigInt): bigint; + export function generatePrimeSync( + size: number, + options: GeneratePrimeOptionsArrayBuffer, + ): ArrayBuffer; + export function generatePrimeSync( + size: number, + options: GeneratePrimeOptions, + ): ArrayBuffer | bigint; + export interface CheckPrimeOptions { + checks?: number | undefined; + } + export function checkPrime( + value: LargeNumberLike, + callback: (err: Error | null, result: boolean) => void, + ): void; + export function checkPrime( + value: LargeNumberLike, + options: CheckPrimeOptions, + callback: (err: Error | null, result: boolean) => void, + ): void; + export function checkPrimeSync(candidate: LargeNumberLike, options?: CheckPrimeOptions): boolean; export interface ScryptOptions { N?: number; r?: number; @@ -3512,7 +3568,8 @@ declare module "child_process" { * killSignal at the deadline and the result carries error: ETIMEDOUT + * the signal, never a throw), killSignal (a signal NAME literal), * stdio ("ignore"/"inherit"/"pipe" or a 3-tuple of those — captures - * read "" for non-piped streams), and windowsHide (a POSIX no-op). */ + * read "" for non-piped streams), env (replaces the child's + * environment), and windowsHide (a POSIX no-op). */ export function spawnSync( command: string, args?: string[], @@ -3521,6 +3578,7 @@ declare module "child_process" { timeout?: number; killSignal?: string; stdio?: "pipe" | "ignore" | "inherit" | ("pipe" | "ignore" | "inherit")[]; + env?: { [k: string]: string | undefined }; windowsHide?: boolean; }, ): SpawnSyncReturns; diff --git a/packages/compiler/native/cli.profdata.gz b/packages/compiler/native/cli.profdata.gz index 55523e765..aa0e9c36b 100644 Binary files a/packages/compiler/native/cli.profdata.gz and b/packages/compiler/native/cli.profdata.gz differ diff --git a/packages/compiler/native/host.c b/packages/compiler/native/host.c index 6aa1b2a28..eeead7eff 100644 --- a/packages/compiler/native/host.c +++ b/packages/compiler/native/host.c @@ -56,3 +56,78 @@ void scr_collect_cycles_phase(void); void scriptc_native_collect_cycles(void) { scr_collect_cycles_phase(); } + +/* Forked lowering workers (frontend/process-fork.ts). A worker shares the + * parent's heap copy-on-write, writes its results to a file and leaves with + * _exit, so no buffered output or exit handler of the parent runs twice. + * Checker requests from workers hold an exclusive flock on a file each + * worker opens after the fork, so every worker has its own open file + * description and the lock excludes the other workers. */ +#ifdef _WIN32 +int32_t scriptc_native_fork(void) { return -1; } +void scriptc_native_exit(int32_t code) { ExitProcess((UINT)code); } +int32_t scriptc_native_wait(int32_t pid) { (void)pid; return -1; } +uint8_t scriptc_native_lock_open(const uint8_t *bytes, size_t size) { (void)bytes; (void)size; return 0; } +void scriptc_native_lock(void) {} +void scriptc_native_unlock(void) {} +#else +#include +#include +#include +#include + +int32_t scriptc_native_fork(void) { return (int32_t)fork(); } + +void scriptc_native_exit(int32_t code) { _exit(code); } + +/* The worker's exit status, 128 plus the signal for a worker a signal + * ended, or -1 when waitpid fails. */ +int32_t scriptc_native_wait(int32_t pid) { + int status = 0; + while (waitpid((pid_t)pid, &status, 0) < 0) { + if (errno != EINTR) return -1; + } + if (WIFEXITED(status)) return WEXITSTATUS(status); + if (WIFSIGNALED(status)) return 128 + WTERMSIG(status); + return -1; +} + +static int scriptc_native_lock_fd = -1; + +uint8_t scriptc_native_lock_open(const uint8_t *bytes, size_t size) { + if (size == 0 || memchr(bytes, 0, size) != NULL) return 0; + char *path = malloc(size + 1); + if (!path) return 0; + memcpy(path, bytes, size); + path[size] = 0; + if (scriptc_native_lock_fd >= 0) close(scriptc_native_lock_fd); + scriptc_native_lock_fd = open(path, O_CREAT | O_RDWR | O_CLOEXEC, 0600); + free(path); + return scriptc_native_lock_fd >= 0; +} + +void scriptc_native_lock(void) { + if (scriptc_native_lock_fd < 0) return; + while (flock(scriptc_native_lock_fd, LOCK_EX) < 0 && errno == EINTR) { + } +} + +void scriptc_native_unlock(void) { + if (scriptc_native_lock_fd >= 0) flock(scriptc_native_lock_fd, LOCK_UN); +} +#endif + +/* The runtime's growth cap for full cycle passes (scr_rc.h). */ +void scr_cyc_set_growth_cap(long cap); + +/* Bounds the cyclic garbage a build can hold to twice its live heap. + * Lowering a large program leaves hundreds of megabytes of cyclic garbage, + * and whether a scheduled full pass reclaims it before the build's peak + * depends on where the growth rule fires: with the default cap of 16, small + * changes to the compiler moved the peak of one build by 390 MB. With a cap + * of 2 a full pass runs once the heap has grown to three times its size + * after the last one; a cap of 1 bounds the garbage further but runs more + * passes over the live heap. SCR_CYCLE_GROWTH_CAP still overrides it. */ +void scriptc_native_bound_cycle_growth(void) { + scr_cyc_set_growth_cap(2); +} diff --git a/packages/compiler/native/host.ffi.json b/packages/compiler/native/host.ffi.json index d7e0e92aa..e7ecbca60 100644 --- a/packages/compiler/native/host.ffi.json +++ b/packages/compiler/native/host.ffi.json @@ -2,6 +2,13 @@ "ffi_format": 6, "functions": [ { "name": "compilerNativePrivateDirectory", "symbol": "scriptc_native_private_directory", "params": ["string", "bool"], "returns": "bool" }, - { "name": "compilerNativeCollectCycles", "symbol": "scriptc_native_collect_cycles", "params": [], "returns": "void" } + { "name": "compilerNativeCollectCycles", "symbol": "scriptc_native_collect_cycles", "params": [], "returns": "void" }, + { "name": "compilerNativeFork", "symbol": "scriptc_native_fork", "params": [], "returns": "i32" }, + { "name": "compilerNativeExit", "symbol": "scriptc_native_exit", "params": ["i32"], "returns": "void" }, + { "name": "compilerNativeWait", "symbol": "scriptc_native_wait", "params": ["i32"], "returns": "i32" }, + { "name": "compilerNativeLockOpen", "symbol": "scriptc_native_lock_open", "params": ["string"], "returns": "bool" }, + { "name": "compilerNativeLock", "symbol": "scriptc_native_lock", "params": [], "returns": "void" }, + { "name": "compilerNativeUnlock", "symbol": "scriptc_native_unlock", "params": [], "returns": "void" }, + { "name": "compilerNativeBoundCycleGrowth", "symbol": "scriptc_native_bound_cycle_growth", "params": [], "returns": "void" } ] } diff --git a/packages/compiler/package.json b/packages/compiler/package.json index 98d984d4f..a1d66963d 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/compiler", - "version": "0.4.1", + "version": "0.4.2", "description": "The scriptc compiler — TypeScript/JavaScript frontend, typed IR, LLVM backend", "license": "Apache-2.0", "homepage": "https://scriptc.dev", @@ -40,7 +40,7 @@ }, "dependencies": { "@scriptc/runtime": "workspace:*", - "tsc-ts": "0.0.5", + "tsc-ts": "0.0.7", "typescript": "7.0.2", "typescript5": "npm:typescript@5.9.3" }, diff --git a/packages/compiler/shims/chalk/v5.ts b/packages/compiler/shims/chalk/v5.ts new file mode 100644 index 000000000..2d34c5c53 --- /dev/null +++ b/packages/compiler/shims/chalk/v5.ts @@ -0,0 +1,688 @@ +// scriptc's built-in implementation of the `chalk` package, 5.x API. +// +// Programs whose installed chalk is 5.x compile this module in its place +// (packages/compiler/src/frontend/package-shims.ts). chalk builds its +// styles as functions whose prototypes it swaps at run time; here a style +// chain is an instance of one class whose style members are cached +// getters, and the instance is callable through its `%call` field (the +// merged interface below gives it chalk's call signature; the lowerer calls +// the field). The output, color-level detection (supports-color 10: +// FORCE_COLOR, the --color/--no-color flags, TTY, TERM, CI variables) and +// the shared, settable `level` follow chalk 5.6 byte for byte. +import * as os from "node:os"; +import * as tty from "node:tty"; + +export type ColorSupportLevel = 0 | 1 | 2 | 3; + +export interface ColorSupport { + level: ColorSupportLevel; + hasBasic: boolean; + has256: boolean; + has16m: boolean; +} + +export type ColorInfo = ColorSupport | false; + +export interface Options { + readonly level?: ColorSupportLevel; +} + +export type ModifierName = + | "reset" + | "bold" + | "dim" + | "italic" + | "underline" + | "overline" + | "inverse" + | "hidden" + | "strikethrough"; + +export type ForegroundColorName = + | "black" + | "red" + | "green" + | "yellow" + | "blue" + | "cyan" + | "magenta" + | "white" + | "gray" + | "grey" + | "blackBright" + | "redBright" + | "greenBright" + | "yellowBright" + | "blueBright" + | "cyanBright" + | "magentaBright" + | "whiteBright"; + +export type BackgroundColorName = + | "bgBlack" + | "bgRed" + | "bgGreen" + | "bgYellow" + | "bgBlue" + | "bgCyan" + | "bgMagenta" + | "bgWhite" + | "bgGray" + | "bgGrey" + | "bgBlackBright" + | "bgRedBright" + | "bgGreenBright" + | "bgYellowBright" + | "bgBlueBright" + | "bgCyanBright" + | "bgMagentaBright" + | "bgWhiteBright"; + +export type ColorName = ForegroundColorName | BackgroundColorName; + +export type Modifiers = ModifierName; +export type ForegroundColor = ForegroundColorName; +export type BackgroundColor = BackgroundColorName; +export type Color = ColorName; + +// ── ansi-styles ────────────────────────────────────────────────────────── + +export const modifierNames: ModifierName[] = [ + "reset", + "bold", + "dim", + "italic", + "underline", + "overline", + "inverse", + "hidden", + "strikethrough", +]; +export const foregroundColorNames: ForegroundColorName[] = [ + "black", + "red", + "green", + "yellow", + "blue", + "magenta", + "cyan", + "white", + "blackBright", + "gray", + "grey", + "redBright", + "greenBright", + "yellowBright", + "blueBright", + "magentaBright", + "cyanBright", + "whiteBright", +]; +export const backgroundColorNames: BackgroundColorName[] = [ + "bgBlack", + "bgRed", + "bgGreen", + "bgYellow", + "bgBlue", + "bgMagenta", + "bgCyan", + "bgWhite", + "bgBlackBright", + "bgGray", + "bgGrey", + "bgRedBright", + "bgGreenBright", + "bgYellowBright", + "bgBlueBright", + "bgMagentaBright", + "bgCyanBright", + "bgWhiteBright", +]; +export const colorNames: ColorName[] = [...foregroundColorNames, ...backgroundColorNames]; +export const modifiers: Modifiers[] = modifierNames; +export const foregroundColors: ForegroundColor[] = foregroundColorNames; +export const backgroundColors: BackgroundColor[] = backgroundColorNames; +export const colors: Color[] = colorNames; + +const ESC = "\u001B["; + +/** Open and close codes, indexed by style id (the order of the getters). */ +const OPEN_CODES: number[] = [ + 0, 1, 2, 3, 4, 53, 7, 8, 9, 30, 31, 32, 33, 34, 35, 36, 37, 90, 90, 90, 91, 92, 93, 94, 95, 96, + 97, 40, 41, 42, 43, 44, 45, 46, 47, 100, 100, 100, 101, 102, 103, 104, 105, 106, 107, +]; +const CLOSE_CODES: number[] = [ + 0, 22, 22, 23, 24, 55, 27, 28, 29, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, + 39, 39, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, +]; +const VISIBLE = 45; + +function rgbToAnsi256(red: number, green: number, blue: number): number { + if (red === green && green === blue) { + if (red < 8) return 16; + if (red > 248) return 231; + return Math.round(((red - 8) / 247) * 24) + 232; + } + return ( + 16 + + 36 * Math.round((red / 255) * 5) + + 6 * Math.round((green / 255) * 5) + + Math.round((blue / 255) * 5) + ); +} + +function hexToRgb(hex: string): number[] { + const matches = /[a-f\d]{6}|[a-f\d]{3}/i.exec(hex); + if (matches === null) return [0, 0, 0]; + let colorString = matches[0]; + if (colorString.length === 3) { + let doubled = ""; + for (const character of colorString) doubled += character + character; + colorString = doubled; + } + const integer = Number.parseInt(colorString, 16); + return [(integer >> 16) & 0xff, (integer >> 8) & 0xff, integer & 0xff]; +} + +function ansi256ToAnsi(code: number): number { + if (code < 8) return 30 + code; + if (code < 16) return 90 + (code - 8); + let red: number; + let green: number; + let blue: number; + if (code >= 232) { + red = ((code - 232) * 10 + 8) / 255; + green = red; + blue = red; + } else { + code -= 16; + const remainder = code % 36; + red = Math.floor(code / 36) / 5; + green = Math.floor(remainder / 6) / 5; + blue = (remainder % 6) / 5; + } + const value = Math.max(red, green, blue) * 2; + if (value === 0) return 30; + let result = 30 + ((Math.round(blue) << 2) | (Math.round(green) << 1) | Math.round(red)); + if (value === 2) result += 60; + return result; +} + +/** The open code of a color model at a level: offset 0 is the foreground, + * 10 the background (ansi-styles' wrapAnsi16/256/16m). */ +function rgbOpen(level: number, offset: number, red: number, green: number, blue: number): string { + if (level === 3) return `${ESC}${38 + offset};2;${red};${green};${blue}m`; + if (level === 2) return `${ESC}${38 + offset};5;${rgbToAnsi256(red, green, blue)}m`; + return `${ESC}${ansi256ToAnsi(rgbToAnsi256(red, green, blue)) + offset}m`; +} + +/** chalk's ansi256 model emits the 256-color code at every level (only + * the rgb and hex models downsample). */ +function ansi256Open(offset: number, code: number): string { + return `${ESC}${38 + offset};5;${code}m`; +} + +// ── supports-color ─────────────────────────────────────────────────────── + +function hasFlag(flag: string): boolean { + const argv = process.argv; + const prefix = flag.startsWith("-") ? "" : flag.length === 1 ? "-" : "--"; + const position = argv.indexOf(prefix + flag); + const terminatorPosition = argv.indexOf("--"); + return position !== -1 && (terminatorPosition === -1 || position < terminatorPosition); +} + +let flagForceColor: number | undefined = undefined; +if ( + hasFlag("no-color") || + hasFlag("no-colors") || + hasFlag("color=false") || + hasFlag("color=never") +) { + flagForceColor = 0; +} else if ( + hasFlag("color") || + hasFlag("colors") || + hasFlag("color=true") || + hasFlag("color=always") +) { + flagForceColor = 1; +} + +function envForceColor(): number | undefined { + const force = process.env["FORCE_COLOR"]; + if (force !== undefined) { + if (force === "true") return 1; + if (force === "false") return 0; + return force.length === 0 ? 1 : Math.min(Number.parseInt(force, 10), 3); + } + return undefined; +} + +function translateLevel(level: number): ColorInfo { + if (level === 0) return false; + return { + level: level as ColorSupportLevel, + hasBasic: true, + has256: level >= 2, + has16m: level >= 3, + }; +} + +function inEnv(name: string): boolean { + return process.env[name] !== undefined; +} + +function detectLevel(streamIsTTY: boolean): number { + const noFlagForceColor = envForceColor(); + if (noFlagForceColor !== undefined) flagForceColor = noFlagForceColor; + const forceColor = flagForceColor; + if (forceColor === 0) return 0; + if (hasFlag("color=16m") || hasFlag("color=full") || hasFlag("color=truecolor")) return 3; + if (hasFlag("color=256")) return 2; + if (inEnv("TF_BUILD") && inEnv("AGENT_NAME")) return 1; + if (!streamIsTTY && forceColor === undefined) return 0; + const min = + forceColor === undefined || forceColor === 0 || Number.isNaN(forceColor) ? 0 : forceColor; + const env = process.env; + const term = env["TERM"]; + if (term === "dumb") return min; + if (process.platform === "win32") { + const osRelease = os.release().split("."); + if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10_586) { + return Number(osRelease[2]) >= 14_931 ? 3 : 2; + } + return 1; + } + if (inEnv("CI")) { + if (inEnv("GITHUB_ACTIONS") || inEnv("GITEA_ACTIONS") || inEnv("CIRCLECI")) return 3; + if ( + inEnv("TRAVIS") || + inEnv("APPVEYOR") || + inEnv("GITLAB_CI") || + inEnv("BUILDKITE") || + inEnv("DRONE") || + env["CI_NAME"] === "codeship" + ) { + return 1; + } + return min; + } + const teamcity = env["TEAMCITY_VERSION"]; + if (teamcity !== undefined) { + return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(teamcity) ? 1 : 0; + } + if (env["COLORTERM"] === "truecolor") return 3; + if (term === "xterm-kitty") return 3; + if (term === "xterm-ghostty") return 3; + if (term === "wezterm") return 3; + const termProgram = env["TERM_PROGRAM"]; + if (termProgram !== undefined) { + const version = Number.parseInt((env["TERM_PROGRAM_VERSION"] || "").split(".")[0]!, 10); + if (termProgram === "iTerm.app") return version >= 3 ? 3 : 2; + if (termProgram === "Apple_Terminal") return 2; + } + // supports-color tests these against `env.TERM`, which is the string + // "undefined" when TERM is unset. + const termText = term === undefined ? "undefined" : term; + if (/-256(color)?$/i.test(termText)) return 2; + if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(termText)) return 1; + if (inEnv("COLORTERM")) return 1; + return min; +} + +const stdoutColor: ColorInfo = translateLevel(detectLevel(tty.isatty(1))); +const stderrColor: ColorInfo = translateLevel(detectLevel(tty.isatty(2))); + +export const supportsColor: ColorInfo = stdoutColor; +export const supportsColorStderr: ColorInfo = stderrColor; + +// ── the builder ────────────────────────────────────────────────────────── + +/** One link of a style chain (chalk's styler). */ +class Styler { + open: string; + close: string; + openAll: string; + closeAll: string; + parent: Styler | undefined; + constructor(open: string, close: string, parent: Styler | undefined) { + this.open = open; + this.close = close; + this.parent = parent; + if (parent === undefined) { + this.openAll = open; + this.closeAll = close; + } else { + this.openAll = parent.openAll + open; + this.closeAll = close + parent.closeAll; + } + } +} + +/** The level every builder of one chalk instance shares. */ +class LevelBox { + level: number; + constructor(level: number) { + this.level = level; + } +} + +function stringReplaceAll(string: string, substring: string, replacer: string): string { + let index = string.indexOf(substring); + if (index === -1) return string; + const substringLength = substring.length; + let endIndex = 0; + let returnValue = ""; + do { + returnValue += string.slice(endIndex, index) + substring + replacer; + endIndex = index + substringLength; + index = string.indexOf(substring, endIndex); + } while (index !== -1); + returnValue += string.slice(endIndex); + return returnValue; +} + +function stringEncaseCRLFWithFirstIndex( + string: string, + prefix: string, + postfix: string, + index: number, +): string { + let endIndex = 0; + let returnValue = ""; + do { + const gotCR = string[index - 1] === "\r"; + returnValue += + string.slice(endIndex, gotCR ? index - 1 : index) + + prefix + + (gotCR ? "\r\n" : "\n") + + postfix; + endIndex = index + 1; + index = string.indexOf("\n", endIndex); + } while (index !== -1); + returnValue += string.slice(endIndex); + return returnValue; +} + +function applyStyle(self: ChalkInstance, string: string): string { + if (self.box.level <= 0 || !string) return self.isEmpty ? "" : string; + let styler = self.styler; + if (styler === undefined) return string; + const openAll = styler.openAll; + const closeAll = styler.closeAll; + if (string.includes("\u001B")) { + while (styler !== undefined) { + string = stringReplaceAll(string, styler.close, styler.open); + styler = styler.parent; + } + } + const lfIndex = string.indexOf("\n"); + if (lfIndex !== -1) string = stringEncaseCRLFWithFirstIndex(string, closeAll, openAll, lfIndex); + return openAll + string + closeAll; +} + +function joinArguments(args: unknown[]): string { + let out = ""; + for (let i = 0; i < args.length; i++) { + if (i > 0) out += " "; + const value = args[i]; + if (value !== undefined && value !== null) out += String(value); + } + return out; +} + +/** A chalk instance or one of its style chains: callable (chalk's call + * signature merges in from the interface), with every style a cached + * getter. */ +// The merge is the point: the interface gives instances chalk's call +// signature, and the class's `%call` field implements it. +// oxlint-disable-next-line typescript/no-unsafe-declaration-merging +export interface ChalkInstance { + (...text: unknown[]): string; +} + +// oxlint-disable-next-line typescript/no-unsafe-declaration-merging +export class ChalkInstance { + "%call": (...text: unknown[]) => string = (...text: unknown[]): string => + this.root + ? joinArguments(text) + : applyStyle(this, text.length === 1 ? "" + String(text[0]) : joinArguments(text)); + box: LevelBox; + styler: Styler | undefined; + isEmpty: boolean; + private readonly root: boolean; + private readonly cache: (ChalkInstance | undefined)[] = []; + + constructor(box: LevelBox, styler: Styler | undefined, isEmpty: boolean, root: boolean) { + this.box = box; + this.styler = styler; + this.isEmpty = isEmpty; + this.root = root; + } + + /** One string argument: the compiler calls this instead of `%call` + * (no variadic arguments to box). */ + "%call1"(text: string): string { + return this.root ? text : applyStyle(this, text); + } + + get level(): ColorSupportLevel { + return this.box.level as ColorSupportLevel; + } + + set level(level: ColorSupportLevel) { + this.box.level = level; + } + + private style(id: number): ChalkInstance { + const cached = this.cache[id]; + if (cached !== undefined) return cached; + let builder: ChalkInstance; + if (id === VISIBLE) { + builder = new ChalkInstance(this.box, this.styler, true, false); + } else { + const styler = new Styler( + `${ESC}${OPEN_CODES[id]!}m`, + `${ESC}${CLOSE_CODES[id]!}m`, + this.styler, + ); + builder = new ChalkInstance(this.box, styler, this.isEmpty, false); + } + while (this.cache.length <= id) this.cache.push(undefined); + this.cache[id] = builder; + return builder; + } + + private model(open: string, background: boolean): ChalkInstance { + const styler = new Styler(open, background ? `${ESC}49m` : `${ESC}39m`, this.styler); + return new ChalkInstance(this.box, styler, this.isEmpty, false); + } + + rgb(red: number, green: number, blue: number): ChalkInstance { + return this.model(rgbOpen(this.box.level, 0, red, green, blue), false); + } + hex(color: string): ChalkInstance { + const [red, green, blue] = hexToRgb(color); + return this.model(rgbOpen(this.box.level, 0, red!, green!, blue!), false); + } + ansi256(index: number): ChalkInstance { + return this.model(ansi256Open(0, index), false); + } + bgRgb(red: number, green: number, blue: number): ChalkInstance { + return this.model(rgbOpen(this.box.level, 10, red, green, blue), true); + } + bgHex(color: string): ChalkInstance { + const [red, green, blue] = hexToRgb(color); + return this.model(rgbOpen(this.box.level, 10, red!, green!, blue!), true); + } + bgAnsi256(index: number): ChalkInstance { + return this.model(ansi256Open(10, index), true); + } + + get reset(): ChalkInstance { + return this.style(0); + } + get bold(): ChalkInstance { + return this.style(1); + } + get dim(): ChalkInstance { + return this.style(2); + } + get italic(): ChalkInstance { + return this.style(3); + } + get underline(): ChalkInstance { + return this.style(4); + } + get overline(): ChalkInstance { + return this.style(5); + } + get inverse(): ChalkInstance { + return this.style(6); + } + get hidden(): ChalkInstance { + return this.style(7); + } + get strikethrough(): ChalkInstance { + return this.style(8); + } + get visible(): ChalkInstance { + return this.style(VISIBLE); + } + get black(): ChalkInstance { + return this.style(9); + } + get red(): ChalkInstance { + return this.style(10); + } + get green(): ChalkInstance { + return this.style(11); + } + get yellow(): ChalkInstance { + return this.style(12); + } + get blue(): ChalkInstance { + return this.style(13); + } + get magenta(): ChalkInstance { + return this.style(14); + } + get cyan(): ChalkInstance { + return this.style(15); + } + get white(): ChalkInstance { + return this.style(16); + } + get blackBright(): ChalkInstance { + return this.style(17); + } + get gray(): ChalkInstance { + return this.style(18); + } + get grey(): ChalkInstance { + return this.style(19); + } + get redBright(): ChalkInstance { + return this.style(20); + } + get greenBright(): ChalkInstance { + return this.style(21); + } + get yellowBright(): ChalkInstance { + return this.style(22); + } + get blueBright(): ChalkInstance { + return this.style(23); + } + get magentaBright(): ChalkInstance { + return this.style(24); + } + get cyanBright(): ChalkInstance { + return this.style(25); + } + get whiteBright(): ChalkInstance { + return this.style(26); + } + get bgBlack(): ChalkInstance { + return this.style(27); + } + get bgRed(): ChalkInstance { + return this.style(28); + } + get bgGreen(): ChalkInstance { + return this.style(29); + } + get bgYellow(): ChalkInstance { + return this.style(30); + } + get bgBlue(): ChalkInstance { + return this.style(31); + } + get bgMagenta(): ChalkInstance { + return this.style(32); + } + get bgCyan(): ChalkInstance { + return this.style(33); + } + get bgWhite(): ChalkInstance { + return this.style(34); + } + get bgBlackBright(): ChalkInstance { + return this.style(35); + } + get bgGray(): ChalkInstance { + return this.style(36); + } + get bgGrey(): ChalkInstance { + return this.style(37); + } + get bgRedBright(): ChalkInstance { + return this.style(38); + } + get bgGreenBright(): ChalkInstance { + return this.style(39); + } + get bgYellowBright(): ChalkInstance { + return this.style(40); + } + get bgBlueBright(): ChalkInstance { + return this.style(41); + } + get bgMagentaBright(): ChalkInstance { + return this.style(42); + } + get bgCyanBright(): ChalkInstance { + return this.style(43); + } + get bgWhiteBright(): ChalkInstance { + return this.style(44); + } +} + +function optionsLevel(options: Options | undefined): number { + const level = options === undefined ? undefined : options.level; + if ( + level !== undefined && + level !== 0 && + !(Number.isInteger(level) && level >= 0 && level <= 3) + ) { + throw new Error("The `level` option should be an integer from 0 to 3"); + } + if (level === undefined) return stdoutColor === false ? 0 : stdoutColor.level; + return level; +} + +/** `new Chalk(options)`: a fresh chalk instance with its own level. */ +export class Chalk extends ChalkInstance { + constructor(options?: Options) { + super(new LevelBox(optionsLevel(options)), undefined, false, true); + } +} + +const chalk: ChalkInstance = new Chalk(); +export const chalkStderr: ChalkInstance = new Chalk({ + level: stderrColor === false ? 0 : stderrColor.level, +}); + +export default chalk; diff --git a/packages/compiler/shims/hot-shots/client.ts b/packages/compiler/shims/hot-shots/client.ts new file mode 100644 index 000000000..354744e7b --- /dev/null +++ b/packages/compiler/shims/hot-shots/client.ts @@ -0,0 +1,1126 @@ +// scriptc's built-in implementation of the `hot-shots` package, 8.x API: +// a typed port of hot-shots 8.5's StatsD client with the same datagrams, +// callbacks, error messages and child-client sharing, over node:dgram +// (udp), node:net (tcp) and a native Unix datagram socket (uds). The +// entry modules (v8.ts, v8-unix-dgram.ts) say whether the optional +// unix-dgram addon that hot-shots' uds protocol loads is installed. +import * as dgram from "node:dgram"; +import * as dns from "node:dns"; +import * as net from "node:net"; +import * as os from "node:os"; + +export type Tags = { [key: string]: string } | string[]; + +export interface ClientOptions { + bufferFlushInterval?: number; + bufferHolder?: { buffer: string }; + cacheDns?: boolean; + cacheDnsTtl?: number; + errorHandler?: (err: Error) => void; + globalTags?: Tags; + globalize?: boolean; + host?: string; + isChild?: boolean; + maxBufferSize?: number; + mock?: boolean; + path?: string; + port?: number; + prefix?: string; + protocol?: "tcp" | "udp" | "uds"; + sampleRate?: number; + suffix?: string; + telegraf?: boolean; + useDefaultRoute?: boolean; + tagPrefix?: string; + tagSeparator?: string; + udsGracefulErrorHandling?: boolean; + udsGracefulRestartRateLimit?: number; + closingFlushInterval?: number; +} + +export interface ChildClientOptions { + globalTags?: Tags; + prefix?: string; + suffix?: string; + errorHandler?: (err: Error) => void; +} + +export interface CheckOptions { + /** A Unix timestamp; hot-shots also takes a Date, which this shim's + * option records cannot hold yet. */ + date_happened?: number; + hostname?: string; + message?: string; +} + +export interface DatadogChecks { + OK: 0; + WARNING: 1; + CRITICAL: 2; + UNKNOWN: 3; +} + +export type DatadogChecksValues = 0 | 1 | 2 | 3; + +export interface EventOptions { + aggregation_key?: string; + alert_type?: "info" | "warning" | "success" | "error"; + /** A Unix timestamp; hot-shots also takes a Date, which this shim's + * option records cannot hold yet. */ + date_happened?: number; + hostname?: string; + priority?: "low" | "normal"; + source_type_name?: string; +} + +/** A send's completion: hot-shots passes `null` on success. */ +export type StatsCb = (error: Error | null | undefined, bytes?: any) => void; + +type SampleArg = number | Tags | StatsCb | undefined; +type TagsArg = Tags | StatsCb | undefined; + +const PROTOCOL_TCP = "tcp"; +const PROTOCOL_UDS = "uds"; +const PROTOCOL_UDP = "udp"; +const UDS_DEFAULT_GRACEFUL_RESTART_LIMIT = 1000; +const CACHE_DNS_TTL_DEFAULT = 60000; + +function udsErrorCodes(): number[] { + if (process.platform === "linux") { + return [os.constants.errno.ENOTCONN, os.constants.errno.ECONNREFUSED]; + } + if (process.platform === "darwin") { + return [os.constants.errno.EDESTADDRREQ, os.constants.errno.ECONNRESET]; + } + return []; +} +const UDS_ERROR_CODES = udsErrorCodes(); + +// ── helpers ────────────────────────────────────────────────────────────── + +function sanitizeTags(value: string, telegraf: boolean): string { + const blacklist = telegraf ? /:|\||,/g : /:|\||@|,/g; + return String(value).replace(blacklist, "_"); +} + +function formatTags(tags: Tags, telegraf: boolean): string[] { + if (Array.isArray(tags)) return tags; + const out: string[] = []; + for (const key of Object.keys(tags)) { + out.push(`${sanitizeTags(key, telegraf)}:${sanitizeTags(tags[key]!, telegraf)}`); + } + return out; +} + +function isArrayIndexKey(key: string): boolean { + return /^(0|[1-9]\d*)$/.test(key) && Number(key) < 4294967295; +} + +function overrideTags(parent: string[], child: Tags, telegraf: boolean): string[] { + const childKeys: string[] = []; + const childValues = new Map(); + const toAppend: string[] = []; + for (const tag of formatTags(child, telegraf)) { + const idx = tag.indexOf(":"); + if (idx < 1) { + toAppend.push(tag); + } else { + const key = tag.substring(0, idx); + const value = tag.substring(idx + 1); + let values = childValues.get(key); + if (values === undefined) { + values = []; + childValues.set(key, values); + childKeys.push(key); + } + values.push(value); + } + } + const result = parent.filter((tag) => { + const idx = tag.indexOf(":"); + if (idx < 1) return true; + return !childValues.has(tag.substring(0, idx)); + }); + // hot-shots walks an object's keys: array-index keys ascending first, + // then the rest in insertion order. + const indexKeys = childKeys.filter(isArrayIndexKey).sort((a, b) => Number(a) - Number(b)); + const ordered = indexKeys.concat(childKeys.filter((key) => !isArrayIndexKey(key))); + for (const key of ordered) { + for (const value of childValues.get(key)!) result.push(`${key}:${value}`); + } + return result.concat(toAppend); +} + +function formatDate(date: number): number { + return Math.round(date); +} + +/** console.error of an arbitrary value, as hot-shots logs errors. The + * value crosses as unknown: the typed Error path has no inspect lowering + * in programs whose Error subclasses define accessors. */ +function logError(value: unknown): void { + console.error(value); +} + +function errorWithCode(message: string, code: string | undefined): Error { + const error: NodeJS.ErrnoException = new Error(message); + error.code = code; + return error; +} + +// ── transports ─────────────────────────────────────────────────────────── + +type SendCallback = (err: Error | null, bytes?: number) => void; + +/** The socket a client sends through (hot-shots' transport interface). */ +interface Transport { + createdAt: number; + send(buf: Buffer, callback: SendCallback): void; + close(): void; + onError(listener: (err: Error) => void): void; + offError(listener: (err: Error) => void): void; + /** Emits 'error' on the socket, as hot-shots' `socket.emit('error')`. */ + emitError(err: Error): void; + onClose(listener: () => void): void; +} + +/** A socket's 'error' listeners, kept by the transport (the typed + * sockets have no removeListener): one socket listener dispatches to a + * snapshot, and an error nobody listens for throws, as an unhandled + * 'error' event does. */ +class ErrorListeners { + private listeners: ((err: Error) => void)[] = []; + add(listener: (err: Error) => void): void { + this.listeners.push(listener); + } + remove(listener: (err: Error) => void): void { + const index = this.listeners.indexOf(listener); + if (index !== -1) this.listeners.splice(index, 1); + } + emit(err: Error): void { + const snapshot = this.listeners.slice(); + if (snapshot.length === 0) throw err; + for (const listener of snapshot) listener(err); + } +} + +function addEol(buf: Buffer): string { + let msg = buf.toString(); + if (msg.length > 0 && msg[msg.length - 1] !== "\n") msg += "\n"; + return msg; +} + +class UdpTransport implements Transport { + createdAt = 0; + private readonly socket: dgram.Socket; + private readonly errors = new ErrorListeners(); + private resolvedAddress: string | undefined = undefined; + private resolvedAt = 0; + private readonly host: string | undefined; + private readonly port: number; + private readonly cacheDns: boolean; + private readonly cacheDnsTtl: number; + constructor(host: string | undefined, port: number, cacheDns: boolean, cacheDnsTtl: number) { + this.host = host; + this.port = port; + this.cacheDns = cacheDns; + this.cacheDnsTtl = cacheDnsTtl; + this.socket = dgram.createSocket("udp4"); + this.socket.unref(); + // The listener captures the list, not the transport: a socket that is + // never closed keeps its listener, and a capture of `this` would make + // a cycle the reference counts cannot free at exit. + const errors = this.errors; + this.socket.on("error", (err) => errors.emit(err)); + } + send(buf: Buffer, callback: SendCallback): void { + if (!this.cacheDns) { + this.socket.send(buf, this.port, this.host || "127.0.0.1", callback); + return; + } + const now = Date.now(); + const cached = this.resolvedAddress; + if (cached === undefined || now - this.resolvedAt > this.cacheDnsTtl) { + dns.lookup(this.host ?? "localhost", (error, address) => { + if (error) { + callback(error); + return; + } + this.resolvedAddress = address; + this.resolvedAt = now; + this.socket.send(buf, this.port, address, callback); + }); + } else { + this.socket.send(buf, this.port, cached, callback); + } + } + close(): void { + this.socket.close(); + } + onError(listener: (err: Error) => void): void { + this.errors.add(listener); + } + offError(listener: (err: Error) => void): void { + this.errors.remove(listener); + } + emitError(err: Error): void { + this.errors.emit(err); + } + onClose(listener: () => void): void { + this.socket.on("close", listener); + } +} + +class TcpTransport implements Transport { + createdAt = 0; + private readonly socket: net.Socket; + private readonly errors = new ErrorListeners(); + constructor(host: string | undefined, port: number) { + this.socket = net.connect(port, host ?? "localhost"); + this.socket.setKeepAlive(true); + this.socket.unref(); + // The listener captures the list, not the transport: a socket that is + // never closed keeps its listener, and a capture of `this` would make + // a cycle the reference counts cannot free at exit. + const errors = this.errors; + this.socket.on("error", (err) => errors.emit(err)); + } + send(buf: Buffer, callback: SendCallback): void { + // hot-shots writes the line with the 'ascii' encoding, which encodes + // strings as 'latin1' does. The callback runs once the data is written, + // so a message held back by a full socket buffer stays in flight. + this.socket.write(Buffer.from(addEol(buf), "latin1"), (err) => callback(err ?? null)); + } + close(): void { + this.socket.destroy(); + } + onError(listener: (err: Error) => void): void { + this.errors.add(listener); + } + offError(listener: (err: Error) => void): void { + this.errors.remove(listener); + } + emitError(err: Error): void { + this.errors.emit(err); + } + onClose(listener: () => void): void { + this.socket.on("close", () => listener()); + } +} + +// The runtime's AF_UNIX datagram calls (the unix-dgram addon's binding): +// results >= 0, or a negative errno; send answers 1 when it would block. +declare function scriptcUnixDgramSocket(): number; +declare function scriptcUnixDgramConnect(fd: number, path: string): number; +declare function scriptcUnixDgramSend(fd: number, data: Buffer): number; +declare function scriptcUnixDgramClose(fd: number): number; + +let unixDgramInstalled = false; + +/** Whether the unix-dgram addon is installed beside hot-shots (set by the + * entry module). */ +export function configureUnixDgram(installed: boolean): void { + unixDgramInstalled = installed; +} + +const UDS_PATH_DEFAULT = "/var/run/datadog/dsd.socket"; + +/** unix-dgram's errnoException: " " (the code is the + * negative errno; this Error's code field holds its decimal string). */ +function errnoException(errorno: number, syscall: string): Error { + const error: NodeJS.ErrnoException = new Error(`${syscall} ${errorno}`); + error.errno = errorno; + error.code = String(errorno); + error.syscall = syscall; + return error; +} + +/** hot-shots' uds transport over unix-dgram: a connected, non-blocking + * AF_UNIX datagram socket. Sends complete synchronously, as the addon's + * do. The addon polls the socket for reads, which holds the event loop + * until close; a ref'd timer stands in for that poll. */ +class UdsTransport implements Transport { + createdAt = 0; + private fd: number; + private readonly errors = new ErrorListeners(); + private readonly closeListeners: (() => void)[] = []; + private readonly stopHold: () => void; + constructor(path: string) { + const fd = scriptcUnixDgramSocket(); + if (fd < 0) throw errnoException(fd, "socket"); + this.fd = fd; + const hold = setInterval(() => {}, 2147483647); + this.stopHold = () => clearInterval(hold); + const err = scriptcUnixDgramConnect(fd, path); + if (err < 0) { + // The addon emits the connect failure as 'error' before anyone + // listens, which throws; hot-shots closes the socket and rethrows. + this.close(); + throw errnoException(err, "connect"); + } + } + send(buf: Buffer, callback: SendCallback): void { + const result = scriptcUnixDgramSend(this.fd, buf); + if (result < 0) { + callback(errnoException(result, "send")); + } else if (result === 1) { + const congestion: NodeJS.ErrnoException = new Error("congestion"); + congestion.code = "1"; + callback(congestion); + } else { + callback(null); + } + } + close(): void { + const result = scriptcUnixDgramClose(this.fd); + if (result < 0) throw errnoException(result, "close"); + this.fd = -1; + this.stopHold(); + for (const listener of this.closeListeners.slice()) listener(); + } + onError(listener: (err: Error) => void): void { + this.errors.add(listener); + } + offError(listener: (err: Error) => void): void { + this.errors.remove(listener); + } + emitError(err: Error): void { + this.errors.emit(err); + } + onClose(listener: () => void): void { + this.closeListeners.push(listener); + } +} + +/** hot-shots' createUdsTransport: without the unix-dgram addon the + * protocol reports it missing. */ +function createUdsTransport(path: string | undefined): Transport { + if (!unixDgramInstalled) { + throw new Error( + "The library `unix_dgram`, needed for the uds protocol to work, is not installed. " + + "You need to pick another protocol to use hot-shots. " + + "See the hot-shots README for additional details.", + ); + } + return new UdsTransport(path ? path : UDS_PATH_DEFAULT); +} + +function createTransport(client: StatsD): Transport | null { + let transport: Transport | null = null; + const protocol = client.protocol; + try { + if (protocol === PROTOCOL_TCP) { + transport = new TcpTransport(client.host, client.port); + } else if (protocol === PROTOCOL_UDS) { + transport = createUdsTransport(client.path); + } else if (protocol === PROTOCOL_UDP) { + transport = new UdpTransport(client.host, client.port, client.cacheDns, client.cacheDnsTtl); + } else { + throw new Error(`Unsupported protocol '${protocol}'`); + } + transport.createdAt = Date.now(); + } catch (e) { + const handler = client.errorHandler; + if (handler) { + handler(e as Error); + } else { + console.error(e); + } + } + return transport; +} + +// ── the client ─────────────────────────────────────────────────────────── + +class BufferHolder { + buffer = ""; +} + +/** What a child client inherits from its parent. */ +class ChildInit { + socket: Transport | null; + bufferHolder: BufferHolder; + dnsError: Error | undefined; + constructor(socket: Transport | null, bufferHolder: BufferHolder, dnsError: Error | undefined) { + this.socket = socket; + this.bufferHolder = bufferHolder; + this.dnsError = dnsError; + } +} + +export class StatsD { + protocol: string; + cacheDns: boolean; + cacheDnsTtl: number; + host: string | undefined; + port: number; + path: string | undefined; + prefix: string; + suffix: string; + tagPrefix: string; + tagSeparator: string; + mock: boolean; + globalTags: string[]; + telegraf: boolean; + maxBufferSize: number; + sampleRate: number; + bufferFlushInterval: number; + errorHandler: ((err: Error) => void) | undefined; + udsGracefulErrorHandling: boolean; + udsGracefulRestartRateLimit: number; + isChild: boolean; + closingFlushInterval: number; + mockBuffer: string[] | undefined = undefined; + messagesInFlight = 0; + CHECKS: DatadogChecks = { OK: 0, WARNING: 1, CRITICAL: 2, UNKNOWN: 3 }; + private bufferHolder: BufferHolder; + private transport: Transport | null = null; + private dnsError: Error | undefined = undefined; + private stopFlushInterval: (() => void) | undefined = undefined; + + constructor(options?: ClientOptions, child?: ChildInit) { + const o: ClientOptions = options ?? {}; + const protocol = o.protocol ? o.protocol.toLowerCase() : ""; + this.protocol = protocol ? protocol : PROTOCOL_UDP; + this.cacheDns = o.cacheDns === true; + this.cacheDnsTtl = o.cacheDnsTtl || CACHE_DNS_TTL_DEFAULT; + this.host = o.host || process.env["DD_AGENT_HOST"]; + this.port = o.port || parseInt(process.env["DD_DOGSTATSD_PORT"] ?? "", 10) || 8125; + this.path = o.path; + this.prefix = o.prefix || ""; + this.suffix = o.suffix || ""; + this.tagPrefix = o.tagPrefix || "#"; + this.tagSeparator = o.tagSeparator || ","; + this.mock = o.mock === true; + this.telegraf = o.telegraf || false; + const globalTags = o.globalTags; + this.globalTags = globalTags !== undefined ? formatTags(globalTags, this.telegraf) : []; + const entityId = process.env["DD_ENTITY_ID"]; + if (entityId) { + this.globalTags = this.globalTags.filter( + (item) => item.indexOf("dd.internal.entity_id:") !== 0, + ); + this.globalTags.push("dd.internal.entity_id:".concat(sanitizeTags(entityId, false))); + } + this.maxBufferSize = o.maxBufferSize || 0; + this.sampleRate = o.sampleRate || 1; + this.bufferFlushInterval = o.bufferFlushInterval || 1000; + this.bufferHolder = child !== undefined ? child.bufferHolder : new BufferHolder(); + this.errorHandler = o.errorHandler; + const graceful = o.udsGracefulErrorHandling; + this.udsGracefulErrorHandling = graceful === undefined ? true : graceful; + this.udsGracefulRestartRateLimit = + o.udsGracefulRestartRateLimit || UDS_DEFAULT_GRACEFUL_RESTART_LIMIT; + this.isChild = child !== undefined; + this.closingFlushInterval = o.closingFlushInterval || 50; + if (this.mock) this.mockBuffer = []; + if (child === undefined && this.maxBufferSize > 0) { + const handle = setInterval(() => this.onBufferFlushInterval(), this.bufferFlushInterval); + handle.unref(); + this.stopFlushInterval = () => clearInterval(handle); + } + if (child !== undefined) { + if (child.dnsError) this.dnsError = child.dnsError; + this.transport = child.socket; + } + if (!this.transport) this.transport = createTransport(this); + const handler = o.errorHandler; + const transport = this.transport; + if (transport !== null && child === undefined && handler) transport.onError(handler); + if (transport !== null && o.protocol === PROTOCOL_UDS) this.maybeAddUDSErrorHandler(); + } + + private maybeAddUDSErrorHandler(): void { + if (this.isChild || !this.udsGracefulErrorHandling) return; + const transport = this.transport; + if (transport !== null) transport.onError((err) => this.udsErrorHandler(err)); + } + + private udsErrorHandler(err: Error): void { + const code = Number((err as NodeJS.ErrnoException).code); + if (!UDS_ERROR_CODES.includes(-code)) return; + const socket = this.transport; + if (socket === null) return; + if (!socket.createdAt || Date.now() - socket.createdAt < this.udsGracefulRestartRateLimit) { + return; + } + const handler = this.errorHandler; + if (handler) socket.offError(handler); + const newSocket = createTransport(this); + if (newSocket === null) { + const errorMessage = "Could not replace UDS connection with new socket"; + if (handler) { + handler(new Error(errorMessage)); + } else { + console.error(errorMessage); + } + return; + } + socket.close(); + this.transport = newSocket; + this.maybeAddUDSErrorHandler(); + if (handler) { + newSocket.onError(handler); + } else { + newSocket.onError((error) => console.error(`hot-shots UDS error: ${String(error)}`)); + } + } + + // ── stats functions ── + + timing( + stat: string | string[], + time: number, + sampleRate?: number, + tags?: Tags, + callback?: StatsCb, + ): void; + timing(stat: string | string[], time: number, tags?: Tags, callback?: StatsCb): void; + timing(stat: string | string[], time: number, callback?: StatsCb): void; + timing(stat: string | string[], time: number, sampleRate?: number, callback?: StatsCb): void; + timing( + stat: string | string[], + time: number, + sampleRate?: SampleArg, + tags?: TagsArg, + callback?: StatsCb, + ): void { + this.sendAll(stat, time, "ms", sampleRate, tags, callback); + } + + increment(stat: string, tags?: Tags): void; + increment( + stat: string | string[], + value: number, + sampleRate?: number, + tags?: Tags, + callback?: StatsCb, + ): void; + increment(stat: string | string[], value: number, tags?: Tags, callback?: StatsCb): void; + increment(stat: string | string[], value: number, callback?: StatsCb): void; + increment(stat: string | string[], value: number, sampleRate?: number, callback?: StatsCb): void; + increment( + stat: string | string[], + value?: number | Tags, + sampleRate?: SampleArg, + tags?: TagsArg, + callback?: StatsCb, + ): void { + // A non-number second argument of a two-argument call is the tags. + let amount = 1; + let tagArg: TagsArg = tags; + if (typeof value === "number") { + amount = value; + } else if (sampleRate === undefined && tags === undefined && callback === undefined) { + tagArg = value; + } + this.sendAll(stat, amount, "c", sampleRate, tagArg, callback); + } + + decrement(stat: string, tags?: Tags): void; + decrement( + stat: string | string[], + value: number, + sampleRate?: number, + tags?: Tags, + callback?: StatsCb, + ): void; + decrement(stat: string | string[], value: number, tags?: Tags, callback?: StatsCb): void; + decrement(stat: string | string[], value: number, callback?: StatsCb): void; + decrement(stat: string | string[], value: number, sampleRate?: number, callback?: StatsCb): void; + decrement( + stat: string | string[], + value?: number | Tags, + sampleRate?: SampleArg, + tags?: TagsArg, + callback?: StatsCb, + ): void { + let amount = 1; + let tagArg: TagsArg = tags; + if (typeof value === "number") { + amount = value; + } else if (sampleRate === undefined && tags === undefined && callback === undefined) { + tagArg = value; + } + this.sendAll(stat, -amount, "c", sampleRate, tagArg, callback); + } + + histogram( + stat: string | string[], + value: number, + sampleRate?: number, + tags?: Tags, + callback?: StatsCb, + ): void; + histogram(stat: string | string[], value: number, tags?: Tags, callback?: StatsCb): void; + histogram(stat: string | string[], value: number, callback?: StatsCb): void; + histogram(stat: string | string[], value: number, sampleRate?: number, callback?: StatsCb): void; + histogram( + stat: string | string[], + value: number, + sampleRate?: SampleArg, + tags?: TagsArg, + callback?: StatsCb, + ): void { + this.sendAll(stat, value, "h", sampleRate, tags, callback); + } + + distribution( + stat: string | string[], + value: number, + sampleRate?: number, + tags?: Tags, + callback?: StatsCb, + ): void; + distribution(stat: string | string[], value: number, tags?: Tags, callback?: StatsCb): void; + distribution(stat: string | string[], value: number, callback?: StatsCb): void; + distribution( + stat: string | string[], + value: number, + sampleRate?: number, + callback?: StatsCb, + ): void; + distribution( + stat: string | string[], + value: number, + sampleRate?: SampleArg, + tags?: TagsArg, + callback?: StatsCb, + ): void { + this.sendAll(stat, value, "d", sampleRate, tags, callback); + } + + gauge( + stat: string | string[], + value: number, + sampleRate?: number, + tags?: Tags, + callback?: StatsCb, + ): void; + gauge(stat: string | string[], value: number, tags?: Tags, callback?: StatsCb): void; + gauge(stat: string | string[], value: number, callback?: StatsCb): void; + gauge(stat: string | string[], value: number, sampleRate?: number, callback?: StatsCb): void; + gauge( + stat: string | string[], + value: number, + sampleRate?: SampleArg, + tags?: TagsArg, + callback?: StatsCb, + ): void { + this.sendAll(stat, value, "g", sampleRate, tags, callback); + } + + set( + stat: string | string[], + value: number | string, + sampleRate?: number, + tags?: Tags, + callback?: StatsCb, + ): void; + set(stat: string | string[], value: number | string, tags?: Tags, callback?: StatsCb): void; + set(stat: string | string[], value: number | string, callback?: StatsCb): void; + set( + stat: string | string[], + value: number | string, + sampleRate?: number, + callback?: StatsCb, + ): void; + set( + stat: string | string[], + value: number | string, + sampleRate?: SampleArg, + tags?: TagsArg, + callback?: StatsCb, + ): void { + this.sendAll(stat, value, "s", sampleRate, tags, callback); + } + + unique( + stat: string | string[], + value: number | string, + sampleRate?: number, + tags?: Tags, + callback?: StatsCb, + ): void; + unique(stat: string | string[], value: number | string, tags?: Tags, callback?: StatsCb): void; + unique(stat: string | string[], value: number | string, callback?: StatsCb): void; + unique( + stat: string | string[], + value: number | string, + sampleRate?: number, + callback?: StatsCb, + ): void; + unique( + stat: string | string[], + value: number | string, + sampleRate?: SampleArg, + tags?: TagsArg, + callback?: StatsCb, + ): void { + this.sendAll(stat, value, "s", sampleRate, tags, callback); + } + + check( + name: string, + status: DatadogChecksValues, + options?: CheckOptions, + tags?: Tags | StatsCb, + callback?: StatsCb, + ): void { + if (this.telegraf) { + const err = new Error("Not supported by Telegraf / InfluxDB"); + if (callback) return callback(err); + const handler = this.errorHandler; + if (handler) return handler(err); + throw err; + } + const check: string[] = ["_sc", this.prefix + name + this.suffix, String(status)]; + const metadata: CheckOptions = options ?? {}; + const happened = metadata.date_happened; + if (happened) { + const timestamp = formatDate(happened); + if (timestamp) check.push(`d:${timestamp}`); + } + if (metadata.hostname) check.push(`h:${metadata.hostname}`); + let mergedTags = this.globalTags; + if (tags !== undefined && typeof tags !== "function") { + mergedTags = overrideTags(mergedTags, tags, this.telegraf); + } + if (mergedTags.length > 0) check.push(`#${mergedTags.join(",")}`); + if (metadata.message) check.push(`m:${metadata.message}`); + let cb = callback; + if (typeof tags === "function" && callback === undefined) cb = tags; + this.sendRaw(check.join("|"), cb); + } + + event( + title: string, + text?: string, + options?: EventOptions, + tags?: Tags | StatsCb, + callback?: StatsCb, + ): void { + if (this.telegraf) { + const err = new Error("Not supported by Telegraf / InfluxDB"); + if (callback) return callback(err); + const handler = this.errorHandler; + if (handler) return handler(err); + throw err; + } + const msgTitle = String(title ? title : ""); + let msgText = String(text ? text : msgTitle); + msgText = msgText.replace(/\n/g, "\\n"); + let message = `_e{${msgTitle.length},${msgText.length}}:${msgTitle}|${msgText}`; + if (options) { + const happened = options.date_happened; + if (happened) { + const timestamp = formatDate(happened); + if (timestamp) message += `|d:${timestamp}`; + } + if (options.hostname) message += `|h:${options.hostname}`; + if (options.aggregation_key) message += `|k:${options.aggregation_key}`; + if (options.priority) message += `|p:${options.priority}`; + if (options.source_type_name) message += `|s:${options.source_type_name}`; + if (options.alert_type) message += `|t:${options.alert_type}`; + } + let cb = callback; + let tagList: Tags | undefined = undefined; + if (typeof tags === "function") { + if (callback === undefined) cb = tags; + } else { + tagList = tags; + } + this.send(message, tagList, cb); + } + + // ── sending ── + + private sendAll( + stat: string | string[], + value: number | string, + type: string, + sampleRateArg: SampleArg, + tagsArg: TagsArg, + callbackArg: StatsCb | undefined, + ): void { + let sampleRate: number | undefined = undefined; + let tags: Tags | undefined = undefined; + let callback: StatsCb | undefined = callbackArg; + if (typeof sampleRateArg === "number") { + sampleRate = sampleRateArg; + if (typeof tagsArg === "function") { + callback = tagsArg; + } else { + tags = tagsArg; + } + } else if (sampleRateArg !== undefined) { + // A non-number sample rate shifts into the tags, and the tags + // argument into the callback. + if (typeof sampleRateArg === "function") { + callback = sampleRateArg; + } else { + tags = sampleRateArg; + callback = typeof tagsArg === "function" ? tagsArg : undefined; + } + } else if (typeof tagsArg === "function") { + callback = tagsArg; + } else { + tags = tagsArg; + } + + if (!Array.isArray(stat)) { + this.sendStat(stat, value, type, sampleRate, tags, callback); + return; + } + let completed = 0; + let calledback = false; + let sentBytes = 0; + const count = stat.length; + const onSend: StatsCb = (error, bytes) => { + completed += 1; + if (calledback) return; + if (error) { + if (callback !== undefined) { + calledback = true; + callback(error); + } else { + const handler = this.errorHandler; + if (handler) { + calledback = true; + handler(error); + } + } + return; + } + if (bytes) sentBytes += bytes as number; + if (completed === count && callback !== undefined) callback(null, sentBytes); + }; + for (const item of stat) this.sendStat(item, value, type, sampleRate, tags, onSend); + } + + private sendStat( + stat: string, + value: number | string, + type: string, + sampleRateArg: number | undefined, + tags: Tags | undefined, + callback: StatsCb | undefined, + ): void { + let message = `${this.prefix + stat + this.suffix}:${value}|${type}`; + const sampleRate = sampleRateArg || this.sampleRate; + if (sampleRate && sampleRate < 1) { + if (Math.random() < sampleRate) { + message += `|@${sampleRate}`; + } else { + if (callback) callback(undefined); + return; + } + } + this.send(message, tags, callback); + } + + private send(message: string, tags: Tags | undefined, callback: StatsCb | undefined): void { + let mergedTags = this.globalTags; + if (tags !== undefined) mergedTags = overrideTags(mergedTags, tags, this.telegraf); + if (mergedTags.length > 0) { + if (this.telegraf) { + const parts = message.split(":"); + message = `${parts[0]!},${mergedTags.join(",").replace(/:/g, "=")}:${parts.slice(1).join(":")}`; + } else { + message += `|${this.tagPrefix}${mergedTags.join(this.tagSeparator)}`; + } + } + this.sendRaw(message, callback); + } + + /** hot-shots' `_send`: the message with its tags. */ + private sendRaw(message: string, callback: StatsCb | undefined): void { + const dnsError = this.dnsError; + if (dnsError) { + if (callback) return callback(dnsError); + const handler = this.errorHandler; + if (handler) return handler(dnsError); + throw dnsError; + } + if (!this.mock) { + if (this.maxBufferSize === 0) { + this.sendMessage(message, callback); + } else { + this.enqueue(message, callback); + } + } else { + const buffer = this.mockBuffer; + if (buffer !== undefined) buffer.push(message); + if (callback) callback(null, 0); + } + } + + private enqueue(message: string, callback: StatsCb | undefined): void { + message += "\n"; + if (this.bufferHolder.buffer.length + message.length > this.maxBufferSize) { + this.flushQueue(callback); + this.bufferHolder.buffer += message; + } else { + this.bufferHolder.buffer += message; + if (callback) callback(null); + } + } + + private flushQueue(callback?: StatsCb): void { + this.sendMessage(this.bufferHolder.buffer, callback); + this.bufferHolder.buffer = ""; + } + + private sendMessage(message: string, callback: StatsCb | undefined): void { + if (message === "") { + if (callback) callback(null); + return; + } + const socketWasMissing = this.transport === null; + if (socketWasMissing && this.protocol === PROTOCOL_UDS) { + this.transport = createTransport(this); + if (this.transport !== null) this.maybeAddUDSErrorHandler(); + } + const socket = this.transport; + if (socketWasMissing || socket === null) { + const error = new Error("Socket not created properly. Check previous errors for details."); + if (callback) return callback(error); + const handler = this.errorHandler; + if (handler) return handler(error); + console.error(String(error)); + return; + } + const handleCallback = (err: Error | null, bytes?: number): void => { + this.messagesInFlight--; + let errFormatted: Error | null = null; + if (err) { + errFormatted = errorWithCode( + `Error sending hot-shots message: ${String(err)}`, + (err as NodeJS.ErrnoException).code, + ); + if (this.protocol === PROTOCOL_UDS && (callback || this.errorHandler)) { + this.udsErrorHandler(err); + } + } + if (callback) { + callback(errFormatted, bytes); + } else if (errFormatted) { + const handler = this.errorHandler; + if (handler) { + handler(errFormatted); + } else { + console.error(String(errFormatted)); + // hot-shots also emits the error on the current socket, where the + // uds reconnection handler listens; with no listener it throws. + const current = this.transport; + if (current !== null) current.emitError(errFormatted); + } + } + }; + try { + this.messagesInFlight++; + socket.send(Buffer.from(message), handleCallback); + } catch (err) { + handleCallback(err as Error); + } + } + + private onBufferFlushInterval(): void { + this.flushQueue(); + } + + close(callback?: (error?: Error) => void): void { + const stop = this.stopFlushInterval; + if (stop !== undefined) stop(); + this.flushQueue((err) => { + if (err) { + if (callback) return callback(err); + logError(err); + return; + } + let intervalAttempts = 0; + const waitForMessages = setInterval(() => { + intervalAttempts++; + if (intervalAttempts > 10) { + console.log("hot-shots could not clear out messages in flight but closing anyways"); + this.messagesInFlight = 0; + } + if (this.messagesInFlight <= 0) { + clearInterval(waitForMessages); + this.closeSocket(callback); + } + }, this.closingFlushInterval); + }); + } + + private closeSocket(callback: ((error?: Error) => void) | undefined): void { + let handledError = false; + const handleErr = (err: unknown): void => { + const errMessage = `Error closing hot-shots socket: ${String(err)}`; + if (handledError) { + console.error(errMessage); + } else { + handledError = true; + if (callback) { + callback(new Error(errMessage)); + } else { + const handler = this.errorHandler; + if (handler) { + handler(new Error(errMessage)); + } else { + console.error(errMessage); + } + } + } + }; + const socket = this.transport; + if (socket === null) { + handleErr(new TypeError("Cannot read properties of null (reading 'removeListener')")); + return; + } + const handler = this.errorHandler; + if (handler) socket.offError(handler); + socket.onError(handleErr); + if (callback) { + socket.onClose(() => { + if (!handledError) callback(undefined); + }); + } + try { + socket.close(); + } catch (err) { + handleErr(err); + } + } + + childClient(options?: ChildClientOptions): StatsD { + const o: ChildClientOptions = options ?? {}; + const childTags = o.globalTags; + return new StatsD( + { + isChild: true, + errorHandler: o.errorHandler || this.errorHandler, + host: this.host, + port: this.port, + tagPrefix: this.tagPrefix, + tagSeparator: this.tagSeparator, + prefix: (o.prefix || "") + this.prefix, + suffix: this.suffix + (o.suffix || ""), + globalize: false, + mock: this.mock, + globalTags: + childTags !== undefined + ? overrideTags(this.globalTags, childTags, this.telegraf) + : this.globalTags, + maxBufferSize: this.maxBufferSize, + bufferFlushInterval: this.bufferFlushInterval, + telegraf: this.telegraf, + protocol: this.protocol === "tcp" ? "tcp" : this.protocol === "uds" ? "uds" : "udp", + closingFlushInterval: this.closingFlushInterval, + }, + new ChildInit(this.transport, this.bufferHolder, this.dnsError), + ); + } +} + +export default StatsD; diff --git a/packages/compiler/shims/hot-shots/v8-unix-dgram.ts b/packages/compiler/shims/hot-shots/v8-unix-dgram.ts new file mode 100644 index 000000000..f4bfb180e --- /dev/null +++ b/packages/compiler/shims/hot-shots/v8-unix-dgram.ts @@ -0,0 +1,8 @@ +// The hot-shots 8.x shim for installations with the optional unix-dgram +// addon: the uds protocol sends through a native Unix datagram socket. +import { configureUnixDgram } from "./client.ts"; + +configureUnixDgram(true); + +export * from "./client.ts"; +export { default } from "./client.ts"; diff --git a/packages/compiler/shims/hot-shots/v8.ts b/packages/compiler/shims/hot-shots/v8.ts new file mode 100644 index 000000000..0d1871170 --- /dev/null +++ b/packages/compiler/shims/hot-shots/v8.ts @@ -0,0 +1,8 @@ +// The hot-shots 8.x shim for installations without the optional unix-dgram +// addon: the uds protocol reports it missing, as hot-shots does under Node. +import { configureUnixDgram } from "./client.ts"; + +configureUnixDgram(false); + +export * from "./client.ts"; +export { default } from "./client.ts"; diff --git a/packages/compiler/src/backend/cycle-analysis.test.ts b/packages/compiler/src/backend/cycle-analysis.test.ts index c78420f73..340ffa8f9 100644 --- a/packages/compiler/src/backend/cycle-analysis.test.ts +++ b/packages/compiler/src/backend/cycle-analysis.test.ts @@ -17,6 +17,7 @@ import { } from "../ir/ir.js"; import { computeTraced, explainCycle } from "./cycle-analysis.js"; import { computeTraced as llvmTraced } from "./llvm/shapes.js"; +import { addRecordResultEntries } from "./llvm/record-results.js"; const loc = { file: "cycles.ts", start: 0, end: 1 }; function module(): IrModule { @@ -404,6 +405,61 @@ test("a later write through any alias makes the edge mutable", () => { checkExact(record, ["record:list"]); }); +test("a result-slot entry's stores into its destination are not writes", () => { + const next: IrType = { kind: "union", unionId: "next" }; + const listModule = (): IrModule => { + const mod = module(); + mod.records = [shape("list", [next, F64])]; + mod.unions = [{ id: "next", arms: [ref("list"), { kind: "nullT" }] }]; + mod.functions.push({ + name: "cell", + params: [{ localId: "rest", name: "rest", type: next }], + locals: [], + returnType: ref("list"), + body: [ + { + kind: "return", + value: { + kind: "recordLit", + fields: [ + { name: "field0", value: { kind: "varRef", localId: "rest", type: next, loc } }, + { name: "field1", value: { kind: "numLit", value: 1, type: F64, loc } }, + ], + type: ref("list"), + loc, + }, + loc, + }, + ], + loc, + }); + return addRecordResultEntries(mod); + }; + // The entry stores the literal's fields into caller frame storage that + // nothing can point to: the list stays acyclic. + const entries = listModule(); + expect(entries.functions.map((fn) => fn.name)).toContain("%into.cell"); + checkExact(entries, []); + // The same store into the destination's local id outside an entry, and a + // store into any other binding of the shape inside one, are writes. + const store = (localId: string): IrStmt => ({ + kind: "recordSet", + obj: { kind: "varRef", localId, type: ref("list"), loc }, + shapeId: "list", + field: "field0", + value: value(next), + loc, + }); + for (const [fn, localId] of [ + ["main", "%into.dst"], + ["%into.cell", "v"], + ] as const) { + const written = listModule(); + written.functions.find((f) => f.name === fn)!.body.unshift(store(localId)); + checkExact(written, ["record:list"], ["next"]); + } +}); + test("field increments count as writes", () => { const mod = tree(); mod.functions[0]!.body.push({ diff --git a/packages/compiler/src/backend/cycle-analysis.ts b/packages/compiler/src/backend/cycle-analysis.ts index aa624579f..fbd96e854 100644 --- a/packages/compiler/src/backend/cycle-analysis.ts +++ b/packages/compiler/src/backend/cycle-analysis.ts @@ -11,7 +11,7 @@ import { STRING, VOID, } from "../ir/ir.js"; -import { computeCycleMutability, fieldKey } from "./cycle-mutability.js"; +import { type CycleMutability, computeCycleMutability, fieldKey } from "./cycle-mutability.js"; /** Cycle capability for native code generation. * Nodes are shape units and unions; edges are the references their fields @@ -38,8 +38,10 @@ import { computeCycleMutability, fieldKey } from "./cycle-mutability.js"; export function computeTraced( mod: IrModule, stores?: DynamicFieldStores, + /** computeCycleMutability's answer for this module, when the caller has it. */ + precomputed?: CycleMutability, ): { shapes: Set; unions: Set } { - const { shapes, unions } = analyzeCycles(mod, false, stores); + const { shapes, unions } = analyzeCycles(mod, false, stores, precomputed); return { shapes, unions }; } @@ -93,6 +95,7 @@ function analyzeCycles( mod: IrModule, labels: boolean, stores?: DynamicFieldStores, + precomputed?: CycleMutability, ): { shapes: Set; unions: Set; @@ -125,7 +128,7 @@ function analyzeCycles( const lifetimeRoots = new Set(mod.contracts?.programLifetime ?? []); const programLifetime = (className: string): boolean => lifetimeRoots.size > 0 && lifetimeRoots.has(rootOf(className)); - const mutability = computeCycleMutability(mod, unitOfClass, stores); + const mutability = precomputed ?? computeCycleMutability(mod, unitOfClass, stores); const shapeDefs = [ ...classes.map((c) => ({ key: `object:${c.name}`, diff --git a/packages/compiler/src/backend/cycle-mutability.ts b/packages/compiler/src/backend/cycle-mutability.ts index 78daa6204..541a1312f 100644 --- a/packages/compiler/src/backend/cycle-mutability.ts +++ b/packages/compiler/src/backend/cycle-mutability.ts @@ -2,6 +2,7 @@ import type { IrExpr, IrFunction, IrModule, IrStmt, IrType } from "../ir/ir.js"; import { RUNTIME_EMITTER_CLASS, RUNTIME_STREAM_CLASSES, typeKey } from "../ir/ir.js"; import { dynamicFieldStores, type DynamicFieldStores } from "../ir/int32-slots.js"; import { everyExprChild, everyStmtChild, everyTypeChild } from "../ir/traverse.js"; +import { resultSlotStore } from "./result-slots.js"; /** Which shape fields can be written after their object escapes. * @@ -17,6 +18,10 @@ import { everyExprChild, everyStmtChild, everyTypeChild } from "../ir/traverse.j * - every `fieldSet` outside a constructor's own `this`, `fieldIncDec`, and * `recordSet` makes its (unit, field) mutable; `recordKeySet` on declared * fields and `recordKeyDelete` make the whole record mutable; + * - a result-slot entry's stores into its destination (result-slots.ts) are + * not writes: the destination is frame storage of a caller that only + * projects the value, so no object can point to it and its fields cannot + * lie on a cycle; * - a constructor's stores into its own `this` are construction-only up to * the first top-level statement in which `this` escapes (any use other * than a field store target, a field read, or the receiver of the direct @@ -69,6 +74,22 @@ export function fieldKey(unit: string, field: string): string { return `${unit}\0${field}`; } +/** The cycle unit of a class: `object:`, the root of its hierarchy, + * as computeImmutableFields and computeTraced group classes. */ +export function hierarchyUnitOfClass(mod: IrModule): (className: string) => string { + const baseOf = new Map((mod.classes ?? []).map((c) => [c.name, c.base ?? null] as const)); + const roots = new Map(); + return (name) => { + const known = roots.get(name); + if (known !== undefined) return `object:${known}`; + let cur = name; + for (let base = baseOf.get(cur); base !== null && base !== undefined; base = baseOf.get(cur)) + cur = base; + roots.set(name, cur); + return `object:${cur}`; + }; +} + export function computeCycleMutability( mod: IrModule, unitOfClass: (className: string) => string, @@ -296,7 +317,8 @@ export function computeCycleMutability( result.fields.add(fieldKey(unitOfClass(s.className), s.field)); break; case "recordSet": - result.fields.add(fieldKey(`record:${s.shapeId}`, s.field)); + if (!resultSlotStore(fn.name, s)) + result.fields.add(fieldKey(`record:${s.shapeId}`, s.field)); break; case "recordKeySet": if (s.overflowOnly !== true) result.units.add(`record:${s.shapeId}`); diff --git a/packages/compiler/src/backend/immutability.ts b/packages/compiler/src/backend/immutability.ts index 6c37731e3..21380de8f 100644 --- a/packages/compiler/src/backend/immutability.ts +++ b/packages/compiler/src/backend/immutability.ts @@ -48,6 +48,8 @@ export class ImmutableFields { export function computeImmutableFields( mod: IrModule, stores?: DynamicFieldStores, + /** computeCycleMutability's answer for this module, when the caller has it. */ + precomputed?: CycleMutability, ): ImmutableFields | null { if (mod.lib !== undefined) return null; const baseOf = new Map((mod.classes ?? []).map((c) => [c.name, c.base ?? null] as const)); @@ -62,7 +64,7 @@ export function computeImmutableFields( return cur; }; const unitOfClass = (name: string): string => `object:${rootOf(name)}`; - const mutability = computeCycleMutability(mod, unitOfClass, stores); + const mutability = precomputed ?? computeCycleMutability(mod, unitOfClass, stores); return mutability.all ? null : new ImmutableFields(mutability, unitOfClass); } @@ -213,6 +215,9 @@ export function stableGlobals(mod: IrModule): Set { } return everyStmtChild(s, expr, stmt); }; - for (const fn of mod.functions) fn.body.every(stmt); + for (const fn of mod.functions) { + if (candidates.size === 0) break; + fn.body.every(stmt); + } return candidates; } diff --git a/packages/compiler/src/backend/llvm/appended-array-literals.ts b/packages/compiler/src/backend/llvm/appended-array-literals.ts index 1d4577ae6..09a2976de 100644 --- a/packages/compiler/src/backend/llvm/appended-array-literals.ts +++ b/packages/compiler/src/backend/llvm/appended-array-literals.ts @@ -85,6 +85,7 @@ export function findAppendedArrayLiterals(module: IrModule): ReadonlySet for (const field of fieldAliases.get(binding) ?? []) appendedFields.add(field); const result = new Set(); + if (appendedBindings.size === 0 && appendedFields.size === 0) return result; const consider = (value: IrExpr | null, appended: boolean): void => { if (value && appended && isEmptyLiteral(value)) result.add(value); }; diff --git a/packages/compiler/src/backend/llvm/array-search.ts b/packages/compiler/src/backend/llvm/array-search.ts index d61d91e5c..41b64d418 100644 --- a/packages/compiler/src/backend/llvm/array-search.ts +++ b/packages/compiler/src/backend/llvm/array-search.ts @@ -10,6 +10,8 @@ * SameValueZero are both pointer identity (class instances, records, * arrays, maps, sets, typed arrays): 64-bit compares, four slots per * branch. + * - sc_arr_index_of_cref, the same over the 4-byte slots of compact class + * references (SCR_ELEM_CREF), against the needle's 32-bit reference. * - sc_arr_index_of_str, for strings: pointer identity, then the length, * then the bytes. Strings of up to 16 bytes compare as two words; a short * string's word is the eight bytes ending at its last byte, which start @@ -26,10 +28,12 @@ import type { IrType } from "../../ir/ir.js"; import { SCR_ARR_FIELD } from "./common.js"; import { FN_ATTRS } from "./shapes.js"; -export type ArraySearchKind = "ptr" | "str"; +export type ArraySearchKind = "ptr" | "cref" | "str"; -/** The inline search for arrays of `elem`, or null for the runtime search. */ -export function arraySearchKind(elem: IrType): ArraySearchKind | null { +/** The inline search for arrays of `elem`, or null for the runtime search. + * `compact`: the arrays hold compact class references (SCR_ELEM_CREF). */ +export function arraySearchKind(elem: IrType, compact: boolean): ArraySearchKind | null { + if (compact) return elem.kind === "object" ? "cref" : null; switch (elem.kind) { case "object": case "record": @@ -46,7 +50,7 @@ export function arraySearchKind(elem: IrType): ArraySearchKind | null { } export function arraySearchHelperName(kind: ArraySearchKind): string { - return kind === "ptr" ? "sc_arr_index_of_ptr" : "sc_arr_index_of_str"; + return `sc_arr_index_of_${kind}`; } /** The definitions of the helpers in `kinds`; they declare their slow paths @@ -76,9 +80,18 @@ export function emitArraySearchHelpers( ` %dp = getelementptr inbounds %ScrArr, ptr %a, i32 0, i32 ${SCR_ARR_FIELD.data}`, ` %data = load ptr, ptr %dp`, ); - if (kind === "ptr") { + if (kind === "ptr" || kind === "cref") { + // A compact reference is the address over 8 (classes.ts + // compactRefLoadLines). + const slot = kind === "ptr" ? "i64" : "i32"; defs.push( - ` %needle = ptrtoint ptr %v to i64`, + ...(kind === "ptr" + ? [` %needle = ptrtoint ptr %v to i64`] + : [ + ` %vaddr = ptrtoint ptr %v to i64`, + ` %vref = lshr i64 %vaddr, 3`, + ` %needle = trunc i64 %vref to i32`, + ]), // Four slots per step with one branch, then one at a time. ` br label %quad`, `quad:`, @@ -89,9 +102,9 @@ export function emitArraySearchHelpers( `quadbody:`, ...[0, 1, 2, 3].flatMap((k) => [ ` %qi${k} = add ${sz} %q, ${k}`, - ` %qp${k} = getelementptr inbounds i64, ptr %data, ${sz} %qi${k}`, - ` %qs${k} = load i64, ptr %qp${k}`, - ` %qh${k} = icmp eq i64 %qs${k}, %needle`, + ` %qp${k} = getelementptr inbounds ${slot}, ptr %data, ${sz} %qi${k}`, + ` %qs${k} = load ${slot}, ptr %qp${k}`, + ` %qh${k} = icmp eq ${slot} %qs${k}, %needle`, ]), ` %qh01 = or i1 %qh0, %qh1`, ` %qh23 = or i1 %qh2, %qh3`, @@ -107,9 +120,9 @@ export function emitArraySearchHelpers( ` %done = icmp eq ${sz} %i, %len`, ` br i1 %done, label %miss, label %body`, `body:`, - ` %sp = getelementptr inbounds i64, ptr %data, ${sz} %i`, - ` %s = load i64, ptr %sp`, - ` %hit = icmp eq i64 %s, %needle`, + ` %sp = getelementptr inbounds ${slot}, ptr %data, ${sz} %i`, + ` %s = load ${slot}, ptr %sp`, + ` %hit = icmp eq ${slot} %s, %needle`, ` br i1 %hit, label %found, label %cont`, ); } else { diff --git a/packages/compiler/src/backend/llvm/blocks.ts b/packages/compiler/src/backend/llvm/blocks.ts index 6d22bf3bf..354e631c6 100644 --- a/packages/compiler/src/backend/llvm/blocks.ts +++ b/packages/compiler/src/backend/llvm/blocks.ts @@ -95,8 +95,8 @@ export class BlockBuilder { this.terminate(`br label %${label}`); } - condBr(v: string, t: string, f: string): void { - this.terminate(`br i1 ${v}, label %${t}, label %${f}`); + condBr(v: string, t: string, f: string, attachment = ""): void { + this.terminate(`br i1 ${v}, label %${t}, label %${f}${attachment}`); } /** Emit a double-indexed `for (i = 0; i < len; i++)` loop. `next` is diff --git a/packages/compiler/src/backend/llvm/byte-windows.ts b/packages/compiler/src/backend/llvm/byte-windows.ts index 5fffa1428..9741cc547 100644 --- a/packages/compiler/src/backend/llvm/byte-windows.ts +++ b/packages/compiler/src/backend/llvm/byte-windows.ts @@ -43,6 +43,7 @@ export function findInitializedByteLoopBindings( if (stmt.init) list([stmt.init], inner); list(stmt.body, inner); } + if (stmt.kind === "while") entries.set(stmt, new Set(known)); if (stmt.kind === "while" || stmt.kind === "doWhile") list(stmt.body, new Set(known)); } } diff --git a/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts b/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts index ace8f294c..2d70e9014 100644 --- a/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts @@ -177,6 +177,11 @@ test("statically ranged indices compare one constant with the length", () => { ]); expect(version.affine).toEqual([]); expect(version.counted).toBeNull(); + // A uint32 index (an array counter's range) gets no guard: no typed array + // in practice has 2^31 elements. + const wide = bin(">>>", bin("^", ref("i"), ref("k")), num(0)); + const unbounded = counted([set(ref("out", f64s), num(0), get(ref("table", i32s), wide))]); + expect(match(fn([unbounded]), unbounded)!.fixed).toEqual([{ receiver: 0, end: 0 }]); }); test("scaled counters and body temps resolve to their receiver and index", () => { @@ -358,6 +363,63 @@ test("index temps of a version's counter are integers in the fast copy", () => { expect(fast).not.toContain("bytes.index"); }); +test("while loop counters below a bound version their accesses", () => { + // while (i < n) { out[i] = src[i + 1]; i = i + 1 } + const step: IrStmt = { kind: "assign", localId: "i", value: bin("+", ref("i"), num(1)), loc }; + const whileLoop = (body: IrStmt[]): IrStmt & { kind: "while" } => ({ + kind: "while", + cond: { kind: "bin", op: "<", left: ref("i"), right: ref("n"), type: BOOL, loc }, + body, + loc, + }); + const copy = whileLoop([ + set(ref("out", f64s), ref("i"), get(ref("src", f64s), bin("+", ref("i"), num(1)))), + step, + ]); + const declared = (loop: IrStmt): IrFunction => + fn([{ kind: "varDecl", localId: "i", init: ref("k"), loc }, loop]); + const versionOf = (loop: IrStmt & { kind: "while" }): BytesLoopVersion | null => { + const f = declared(loop); + const ranges = analyzeIntegerRanges(f); + return matchBytesLoopVersion(loop, null, { + locals: new Map(f.locals.map((l) => [l.id, l])), + invariantGlobals: new Set(), + captured: new Set(), + ranges, + initialized: findInitializedByteLoopBindings(f).get(loop) ?? new Set(), + proven: new Set(), + workers: false, + owned: new Set(), + }); + }; + const version = versionOf(copy)!; + expect(version.bounds.size).toBe(2); + expect(version.bounded.map((c) => [version.receivers[c.receiver]!.ref, c.slack])).toEqual([ + [ref("out", f64s), 0], + [ref("src", f64s), 1], + ]); + // `out[i]` needs `i` to start at 0 or above (`src[i + 1]` only at -1). + expect(version.counters.map((c) => [c.ref.kind === "varRef" && c.ref.localId, c.min])).toEqual([ + ["i", 0], + ]); + // An access after the counter advances in the same pass stays checked. + const advanced = whileLoop([step, set(ref("out", f64s), ref("i"), num(0))]); + expect(versionOf(advanced)).toBeNull(); + const mod: IrModule = { + irVersion: 17, + sourceFile: loc.file, + entry: "__main", + functions: [ + { name: "__main", params: [], returnType: VOID, locals: [], body: [], loc }, + declared(copy), + ], + }; + const body = /^define internal [^\n]*@sc_bf_kernel[^]*?^}/m.exec(emitLlvmModule(mod))![0]; + const fast = /^bytes\.version\.fast\d+:$[^]*?^bytes\.version\.done\d+:$/m.exec(body)![0]; + expect(fast).not.toContain("bytes.index"); + expect(body.slice(0, body.indexOf(fast))).toContain("bytes.index.invalid"); +}); + test("Float16Array accesses version in elements and share the element tag", () => { const f16s: IrType = { kind: "bytes", elem: "f16" }; const withHalves = (body: IrStmt[]): IrFunction => { diff --git a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts index c3cee3feb..e797bcbb9 100644 --- a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts @@ -1,4 +1,4 @@ -import type { IrExpr, IrLocal, IrStmt } from "../../ir/ir.js"; +import { F64, type IrExpr, type IrLocal, type IrStmt } from "../../ir/ir.js"; import type { IntegerRange, IntegerRanges } from "../../ir/integer-ranges.js"; import type { IntegerCountedForLoop } from "../../ir/integer-loops.js"; import type { LlvmEmitterContext } from "./expr-context.js"; @@ -23,6 +23,24 @@ interface KeyedTerm extends Term { key: string; } +/** Every access of `receiver` at a while loop counter `c + d` happens while + * `c < bound` (`c <= bound` when `inclusive`) holds and before the counter + * advances, so it is in bounds when `bound + slack <= length`. */ +interface BoundedCheck { + receiver: number; + bound: IrExpr; + slack: number; +} + +/** A while loop counter whose entry value the guard tests: an exact + * integer of at least `min`. A counter that `| 0` wraps also needs every + * bound of its loops at most `wrapLimit`, so that it never reaches 2^31. */ +interface CounterCheck { + ref: VarRef; + min: number; + wrapBounds: { bound: IrExpr; limit: number }[]; +} + /** Every access of `receiver` at a static index range ends below `end + 1`. */ interface FixedCheck { receiver: number; @@ -51,6 +69,9 @@ export interface BytesLoopVersion { ownedReceivers: number[]; fixed: FixedCheck[]; affine: AffineCheck[]; + /** While loops: accesses at a counter below a loop-invariant bound. */ + bounded: BoundedCheck[]; + counters: CounterCheck[]; /** Keys of affine terms the guard tests at run time: exact integers * within GUARDED_TERM. */ guarded: ReadonlySet; @@ -83,7 +104,7 @@ export interface BytesLoopFacts { } const MAX_COST = 400; -const MAX_FIXED_END = 2 ** 32; +const MAX_FIXED_END = 2 ** 31; const MAX_CHECKS = 8; const MAX_OFFSET = 2 ** 31; const SAFE = Number.MAX_SAFE_INTEGER; @@ -116,7 +137,7 @@ const INVARIANT_OPS = new Set(["+", "-", "*", "&", "|", "^", "<<", ">>", ">>>"]) * whenever the guard fails, and keeps every out-of-bounds read, ignored * write and NaN-index behavior. */ export function matchBytesLoopVersion( - loop: IrStmt & { kind: "for" }, + loop: IrStmt & { kind: "for" | "while" }, counted: IntegerCountedForLoop | null, facts: BytesLoopFacts, ): BytesLoopVersion | null { @@ -155,10 +176,10 @@ export function matchBytesLoopVersion( // writes is not invariant either: `for (let i = (a = b, 0); ...)` must // not keep the bounds the guard proved for the old `a`. if ( - (loop.init !== null && !scanStmt(loop.init)) || + (loop.kind === "for" && loop.init !== null && !scanStmt(loop.init)) || !loop.body.every(scanStmt) || (loop.cond !== null && !scanExpr(loop.cond)) || - (loop.update !== null && !scanStmt(loop.update)) + (loop.kind === "for" && loop.update !== null && !scanStmt(loop.update)) ) return null; // A body temp stands for its initializer only when it is a constant @@ -337,6 +358,44 @@ export function matchBytesLoopVersion( return sum + Math.max(Math.abs(r.min), Math.abs(r.max)); }, 0); + const whileAccesses = loop.kind === "while" ? whileCounterAccesses(loop) : null; + function whileCounterAccesses(loop: IrStmt & { kind: "while" }) { + return findWhileCounterAccesses(loop, { + counterLocal: (id) => { + const local = facts.locals.get(id); + return ( + local?.type.kind === "f64" && + !local.boxed && + !local.tdz && + !facts.captured.has(id) && + facts.initialized.has(id) + ); + }, + boundKey: (e) => { + if (e.type.kind !== "f64") return null; + // The guard evaluates the bound before the loop: a typed array's + // length needs the binding itself, not a temp the body declares. + if ( + e.kind === "bytesIntrinsic" && + e.method === "length" && + e.receiver.kind === "varRef" && + e.receiver.type.kind === "bytes" && + invariant(e.receiver.localId) + ) + return `bytes-length:${e.receiver.localId}`; + if ( + e.kind === "strIntrinsic" && + e.method === "length" && + e.args.length === 0 && + e.receiver.kind === "varRef" && + e.receiver.type.kind === "string" && + invariant(e.receiver.localId) + ) + return `string-length:${e.receiver.localId}`; + return pureKey(e); + }, + }); + } const receivers: { ref: VarRef; global: boolean }[] = []; const receiverIndex = new Map(); const fixed = new Map(); @@ -356,6 +415,11 @@ export function matchBytesLoopVersion( // Worker programs: accesses through invariant receivers, proven or not, // become thread-owned when the guard finds no shared storage. const owned = new Map(); + // While loops: accesses at a counter below a bound, by receiver and bound. + const boundedChecks = new Map< + string, + { check: BoundedCheck; nodes: { node: Node; access: WhileAccess }[] } + >(); const access = (node: Node, array: IrExpr, index: IrExpr, width: number): void => { const ref = root(array); if (!ref) return; @@ -390,11 +454,30 @@ export function matchBytesLoopVersion( }); return; } + const bounded = whileAccesses?.accesses.get(node); + const loopCounter = bounded && whileAccesses!.counters.get(bounded.counter); + if (bounded && loopCounter && (!loopCounter.wraps || loopCounter.bounds !== null)) { + const receiver = receiverOf(ref); + const key = `${receiver}|${bounded.key}|${bounded.adj}`; + const slack = bounded.offset + width - 1 + bounded.adj; + const group = boundedChecks.get(key); + if (group) { + group.check.slack = Math.max(group.check.slack, slack); + group.nodes.push({ node, access: bounded }); + } else + boundedChecks.set(key, { + check: { receiver, bound: bounded.bound, slack }, + nodes: [{ node, access: bounded }], + }); + return; + } const range = facts.ranges.get(index); if (!range) return; const end = range.max + width - 1; - // Only a length a buffer can have: an outer loop must not pay for a - // guard that bounds an inner loop's unbounded counter. + // Only an end below 2^31: an outer loop must not pay for a guard that + // bounds an inner loop's unbounded counter, and the counter of a loop to + // an array's length reaches 2^32 - 2, a length no typed array in + // practice has. if (range.min < 0 || end >= MAX_FIXED_END) return; const receiver = receiverOf(ref); const group = fixed.get(receiver); @@ -416,6 +499,7 @@ export function matchBytesLoopVersion( return everyStmtChild(s, visitExpr, visitStmt); }; loop.body.every(visitStmt); + if (loop.kind === "while") visitExpr(loop.cond); // Bound the counter so every counter operand stays a safe integer. let bound = SAFE; const affineGroups = [...affineChecks.values()].filter((group) => { @@ -431,8 +515,18 @@ export function matchBytesLoopVersion( }); const fixedKept = [...fixed.values()].slice(0, MAX_CHECKS); const affineKept = affineGroups.slice(0, MAX_CHECKS - fixedKept.length); + const boundedKept = [...boundedChecks.values()].slice( + 0, + Math.max(0, MAX_CHECKS - fixedKept.length - affineKept.length), + ); const ownedKept = [...owned.entries()].slice(0, MAX_CHECKS); - if (fixedKept.length === 0 && affineKept.length === 0 && ownedKept.length === 0) return null; + if ( + fixedKept.length === 0 && + affineKept.length === 0 && + boundedKept.length === 0 && + ownedKept.length === 0 + ) + return null; const bounds = new Set(); const ownedNodes = new Set(); const used = new Set(); @@ -477,6 +571,39 @@ export function matchBytesLoopVersion( for (const group of affineKept) for (const term of group.check.terms) if (guardedRanges.has(term.expr)) guarded.add(term.key); for (const [node, key] of guardedNodes) if (guarded.has(key)) ranges.set(node, GUARDED_TERM); + // While loop counters of the kept checks: each entry value is an exact + // integer that no access offset takes below 0. In the fast copy each + // access's index is below its length, and its counter read at least the + // most negative offset. + const narrow = (e: IrExpr, range: IntegerRange): void => { + const known = ranges.get(e) ?? facts.ranges.get(e); + ranges.set( + e, + known ? { min: Math.max(known.min, range.min), max: Math.min(known.max, range.max) } : range, + ); + }; + const counterMins = new Map(); + for (const group of boundedKept) { + used.add(group.check.receiver); + for (const { node, access } of group.nodes) { + bounds.add(node); + const min = 0 - access.offset; + counterMins.set(access.counter, Math.max(counterMins.get(access.counter) ?? -SAFE, min)); + narrow(access.ref, { min: -MAX_OFFSET, max: SAFE }); + if (access.index !== access.ref) narrow(access.index, { min: 0, max: SAFE }); + } + } + const counterChecks: CounterCheck[] = []; + for (const [id, min] of counterMins) { + const info = whileAccesses!.counters.get(id)!; + counterChecks.push({ + ref: { kind: "varRef", localId: id, type: F64, loc: loop.loc }, + min, + wrapBounds: info.wraps + ? info.bounds!.map((b) => ({ bound: b.bound, limit: 2 ** 31 - info.sum - b.adj })) + : [], + }); + } // Renumber the receivers the kept checks use. const order = [...used].sort((a, b) => a - b); const renumber = new Map(order.map((old, index) => [old, index])); @@ -500,6 +627,12 @@ export function matchBytesLoopVersion( minOffset: g.check.minOffset, maxEnd: g.check.maxEnd, })), + bounded: boundedKept.map((g) => ({ + receiver: renumber.get(g.check.receiver)!, + bound: g.check.bound, + slack: g.check.slack, + })), + counters: counterChecks, guarded, counted: affineKept.length > 0 ? counted : null, counterBound: needsBound ? bound : null, @@ -507,6 +640,307 @@ export function matchBytesLoopVersion( }; } +/** A typed-array access at a while loop counter: `counter + offset`, read + * or written while `counter < bound` (`<=` when `adj` is 1) holds. */ +interface WhileAccess { + counter: string; + /** The index, and the counter's read in it. */ + index: IrExpr; + ref: IrExpr; + bound: IrExpr; + key: string; + adj: number; + offset: number; +} + +interface WhileCounter { + /** Some increment is `c = (c + k) | 0`. */ + wraps: boolean; + /** The sum of every increment literal in the loop. */ + sum: number; + /** One bound from the condition of the loop and of every nested while + * loop that advances the counter; null when one of them has none. */ + bounds: { bound: IrExpr; adj: number }[] | null; +} + +/** `counter = counter + k`, `counter = (counter + k) | 0` or `counter++`. */ +function counterStep(s: IrStmt): { id: string; k: number; wraps: boolean } | null { + if (s.kind === "exprStmt" && s.expr.kind === "incDec" && s.expr.op === "+") + return { id: s.expr.localId, k: 1, wraps: false }; + if (s.kind !== "assign") return null; + let value = s.value, + wraps = false; + if ( + value.kind === "bin" && + value.op === "|" && + value.right.kind === "numLit" && + value.right.value === 0 && + value.left.kind === "bin" + ) { + value = value.left; + wraps = true; + } + if ( + value.kind !== "bin" || + value.op !== "+" || + value.left.kind !== "varRef" || + value.left.localId !== s.localId || + value.right.kind !== "numLit" || + !Number.isSafeInteger(value.right.value) || + value.right.value <= 0 || + value.right.value > MAX_OFFSET + ) + return null; + return { id: s.localId, k: value.right.value, wraps }; +} + +/** Typed-array accesses of a while loop that happen at a counter below a + * loop-invariant bound. A counter is a number local that the loop changes + * only by positive integer increments, written as statements of the body, + * of its blocks and branches, or of nested while loops. The condition of + * the loop, an `if` or a `&&` chain that compares the counter below a bound + * (`while (pos < end && bytes[pos] === 32) pos++`) makes it below that + * bound until the next increment on the path. The counter starts at its + * entry value and only grows (a `| 0` increment never wraps when every + * bound is at most 2^31 minus the increments), so an access at `counter + + * d` is in bounds for the whole loop when the entry value is at least `-d` + * and `bound + d < length` (`+ 1` for `<=`). */ +function findWhileCounterAccesses( + loop: IrStmt & { kind: "while" }, + host: { + counterLocal: (id: string) => boolean; + /** A key for a bound expression whose value the loop never changes. */ + boundKey: (e: IrExpr) => string | null; + }, +): { accesses: Map; counters: Map } { + const steps = new Map(); + const refused = new Set(); + // Increments count only as statements of the loop's straight-line + // structure; any other write refuses the binding. + const writes = (list: readonly IrStmt[], direct: boolean): void => { + for (const s of list) { + const step = counterStep(s); + if (step && direct) { + const prior = steps.get(step.id); + steps.set(step.id, { + sum: (prior?.sum ?? 0) + step.k, + wraps: (prior?.wraps ?? false) || step.wraps, + }); + continue; + } + if (step) refused.add(step.id); + const nested = + direct && + (s.kind === "if" || s.kind === "while" || (s.kind === "block" && !s.labels?.length)); + if (s.kind === "assign" || s.kind === "varDecl" || s.kind === "forOf") refused.add(s.localId); + if (s.kind === "tryCatch" && s.catchLocalId) refused.add(s.catchLocalId); + everyStmtChild( + s, + (e) => (exprWrites(e), true), + (c) => (writes([c], nested), true), + ); + } + }; + const exprWrites = (e: IrExpr): void => { + if (e.kind === "assignExpr" || e.kind === "incDec") refused.add(e.localId); + everyExprChild( + e, + (c) => (exprWrites(c), true), + (c) => (writes([c], false), true), + ); + }; + exprWrites(loop.cond); + writes(loop.body, true); + const counter = (id: string): boolean => + steps.has(id) && !refused.has(id) && host.counterLocal(id); + + type Fact = { bound: IrExpr; key: string; adj: number }; + type Clean = ReadonlyMap; + // The bounds a condition puts on counters while it holds. + const facts = (cond: IrExpr, into: Map): void => { + if (cond.kind === "toBool") return facts(cond.operand, into); + if (cond.kind === "logical" && cond.op === "&&") { + facts(cond.left, into); + facts(cond.right, into); + return; + } + if (cond.kind !== "bin") return; + const below = cond.op === "<" || cond.op === "<=", + above = cond.op === ">" || cond.op === ">="; + const c = below ? cond.left : above ? cond.right : null, + bound = below ? cond.right : cond.left; + if (c?.kind !== "varRef" || !counter(c.localId) || into.has(c.localId)) return; + const key = host.boundKey(bound); + if (key === null) return; + into.set(c.localId, { bound, key, adj: cond.op === "<=" || cond.op === ">=" ? 1 : 0 }); + }; + // The facts that hold after either of two paths. + const meet = (a: Clean | null, b: Clean | null): Clean | null => { + if (a === null) return b; + if (b === null) return a; + const out = new Map(); + for (const [id, fact] of a) if (b.get(id) === fact) out.set(id, fact); + return out; + }; + const withFacts = (clean: Clean, cond: IrExpr): Clean => { + const out = new Map(clean); + facts(cond, out); + return out; + }; + const accesses = new Map(); + const record = (node: Node, index: IrExpr, clean: Clean): void => { + let ref: IrExpr = index, + offset = 0; + if (index.kind === "bin" && (index.op === "+" || index.op === "-")) { + const literal = + index.right.kind === "numLit" ? index.right : index.op === "+" ? index.left : null; + const other = literal === index.right ? index.left : index.right; + if (literal?.kind !== "numLit" || !Number.isSafeInteger(literal.value)) return; + if (Math.abs(literal.value) > MAX_OFFSET || Object.is(literal.value, -0)) return; + offset = index.op === "-" ? -literal.value : literal.value; + ref = other; + } + if (ref.kind !== "varRef") return; + const fact = clean.get(ref.localId); + if (fact) accesses.set(node, { counter: ref.localId, index, ref, ...fact, offset }); + }; + const expr = (e: IrExpr, clean: Clean): void => { + if (e.kind === "bytesIntrinsic") { + const numeric = byteNumberAccess(e); + const index = numeric + ? e.args[numeric.offsetArg] + : e.method === "get" + ? e.args[0] + : undefined; + if (index) record(e, index, clean); + } + if (e.kind === "logical" && e.op === "&&") { + expr(e.left, clean); + expr(e.right, withFacts(clean, e.left)); + return; + } + if (e.kind === "ternary") { + expr(e.cond, clean); + expr(e.then, withFacts(clean, e.cond)); + expr(e.else_, clean); + return; + } + everyExprChild( + e, + (c) => (expr(c, clean), true), + (c) => (stmts([c], clean), true), + ); + }; + // Walks statements in order; the result is the clean state where the + // list falls through, or null when it never does. + const stmts = (list: readonly IrStmt[], clean: Clean): Clean | null => { + let current: Clean | null = clean; + for (const s of list) { + if (current === null) return null; + const step = counterStep(s); + if (step) { + const next: Map = new Map(current); + next.delete(step.id); + current = next; + continue; + } + switch (s.kind) { + case "if": { + expr(s.cond, current); + const then = stmts(s.then, withFacts(current, s.cond)); + const otherwise = stmts(s.else_ ?? [], current); + current = meet(then, otherwise); + break; + } + case "while": { + // A pass of the nested loop may follow its own increments. + const advanced: Map = new Map(current); + for (const id of writtenCounters(s.body)) advanced.delete(id); + expr(s.cond, advanced); + stmts(s.body, withFacts(advanced, s.cond)); + current = advanced; + break; + } + case "block": + if (!s.labels?.length) { + current = stmts(s.body, current); + break; + } + everyStmtChild( + s, + (e) => (expr(e, current!), true), + (c) => (stmts([c], current!), true), + ); + break; + case "bytesSet": + record(s, s.index, current); + everyStmtChild( + s, + (e) => (expr(e, current!), true), + (c) => (stmts([c], current!), true), + ); + break; + case "break": + case "continue": + case "return": + case "throw": + everyStmtChild( + s, + (e) => (expr(e, current!), true), + (c) => (stmts([c], current!), true), + ); + current = null; + break; + default: + everyStmtChild( + s, + (e) => (expr(e, current!), true), + (c) => (stmts([c], current!), true), + ); + break; + } + } + return current; + }; + const writtenCounters = (list: readonly IrStmt[]): Set => { + const out = new Set(); + const visit = (s: IrStmt): boolean => { + const step = counterStep(s); + if (step) out.add(step.id); + return everyStmtChild(s, () => true, visit); + }; + list.every(visit); + return out; + }; + expr(loop.cond, new Map()); + stmts(loop.body, withFacts(new Map(), loop.cond)); + + // Each counter's bounds: the loop's and those of nested while loops that + // advance it. + const counters = new Map(); + const loops: (IrStmt & { kind: "while" })[] = [loop]; + const collectLoops = (s: IrStmt): boolean => { + if (s.kind === "while") loops.push(s); + return everyStmtChild(s, () => true, collectLoops); + }; + loop.body.every(collectLoops); + for (const [id, step] of steps) { + if (!counter(id)) continue; + let bounds: WhileCounter["bounds"] = []; + for (const l of loops) { + if (l !== loop && !writtenCounters(l.body).has(id)) continue; + const fact = withFacts(new Map(), l.cond).get(id); + if (!fact) { + bounds = null; + break; + } + bounds.push({ bound: fact.bound, adj: fact.adj }); + } + counters.set(id, { ...step, bounds }); + } + return { accesses, counters }; +} + /** Branches to `fast` when every access the version proves is in bounds for * every iteration and every view it proves thread-owned has no shared * storage, and to `slow` otherwise (including a counted loop whose @@ -553,6 +987,39 @@ export function emitBytesLoopVersionGuard( B.line(`${ok} = icmp ugt i64 ${lengths[check.receiver]}, ${check.end}`); conditions.push(ok); } + for (const counter of version.counters) { + const value = host.emitExpr(counter.ref); + // A counter held as an integer (an int32 slot) is exact already and + // needs at most its lower bound tested. + const integer = exactInteger(host, value, counter.ref); + if (integer && integer.range.max <= SAFE) { + if (integer.range.min < counter.min) { + const ok = B.tmp(); + B.line(`${ok} = icmp sge i64 ${widenInteger(host, integer)}, ${counter.min}`); + conditions.push(ok); + } + } else conditions.push(emitExactIntegerTest(host, value.name, counter.min, SAFE).ok); + for (const wrap of counter.wrapBounds) { + const bound = host.emitExpr(wrap.bound); + const boundInteger = exactInteger(host, bound, wrap.bound); + if (boundInteger && boundInteger.range.max <= wrap.limit) continue; + const ok = B.tmp(); + B.line(`${ok} = fcmp ole double ${bound.name}, ${f64Lit(wrap.limit)}`); + conditions.push(ok); + } + } + for (const check of version.bounded) { + // `bound <= length - slack` in doubles: both sides are exact (a length + // is below 2^53 and a slack below 2^32), and NaN fails. + const bound = host.emitExpr(check.bound).name; + const length = B.tmp(), + room = B.tmp(), + ok = B.tmp(); + B.line(`${length} = uitofp i64 ${lengths[check.receiver]} to double`); + B.line(`${room} = fsub double ${length}, ${f64Lit(check.slack)}`); + B.line(`${ok} = fcmp ole double ${bound}, ${room}`); + conditions.push(ok); + } if (counted && version.affine.length > 0) { // The first and last counter values, in ascending order. The last value // of a non-unit step is exact: `start + ⌊(limit ∓ 1 - start) / step⌋ * @@ -599,7 +1066,7 @@ export function emitBytesLoopVersionGuard( for (const term of check.terms) { const value = host.emitExpr(term.expr); if (version.guarded.has(term.key)) { - const tested = emitGuardedTerm(host, value.name); + const tested = emitExactIntegerTest(host, value.name, GUARDED_TERM.min, GUARDED_TERM.max); conditions.push(tested.ok); const sum = B.tmp(); B.line(`${sum} = ${term.sign > 0 ? "add" : "sub"} i64 ${base}, ${tested.integer}`); @@ -653,10 +1120,15 @@ export function emitBytesLoopVersionGuard( else B.condBr(valid, fast, slow); } -/** Tests that a guarded term is an exact integer within GUARDED_TERM (not - * -0; NaN fails the ordered comparisons) and gives its i64. The integer is - * frozen, so it is a fixed value even when the test fails. */ -function emitGuardedTerm(host: LlvmEmitterContext, value: string): { ok: string; integer: string } { +/** Tests that a double is an exact integer within [min, max] (not -0; NaN + * fails the ordered comparisons) and gives its i64. The integer is frozen, + * so it is a fixed value even when the test fails. */ +function emitExactIntegerTest( + host: LlvmEmitterContext, + value: string, + min: number, + max: number, +): { ok: string; integer: string } { const B = host.B; const low = B.tmp(), high = B.tmp(), @@ -668,8 +1140,8 @@ function emitGuardedTerm(host: LlvmEmitterContext, value: string): { ok: string; backBits = B.tmp(), exact = B.tmp(), ok = B.tmp(); - B.line(`${low} = fcmp oge double ${value}, ${f64Lit(GUARDED_TERM.min)}`); - B.line(`${high} = fcmp ole double ${value}, ${f64Lit(GUARDED_TERM.max)}`); + B.line(`${low} = fcmp oge double ${value}, ${f64Lit(min)}`); + B.line(`${high} = fcmp ole double ${value}, ${f64Lit(max)}`); B.line(`${inRange} = and i1 ${low}, ${high}`); B.line(`${raw} = fptosi double ${value} to i64`); B.line(`${integer} = freeze i64 ${raw}`); diff --git a/packages/compiler/src/backend/llvm/classes.test.ts b/packages/compiler/src/backend/llvm/classes.test.ts index 2d95e942c..f921900a3 100644 --- a/packages/compiler/src/backend/llvm/classes.test.ts +++ b/packages/compiler/src/backend/llvm/classes.test.ts @@ -1,5 +1,14 @@ import { expect, test } from "vitest"; -import { DYN, F64, STRING, VOID, type IrModule } from "../../ir/ir.js"; +import { + DYN, + F64, + STRING, + VOID, + type IrExpr, + type IrFunction, + type IrModule, + type IrType, +} from "../../ir/ir.js"; import { assignCompactLayouts, buildClassGraph, @@ -222,12 +231,13 @@ test("ProgramLifetime classes pack their fields after a 32-bit header", () => { contracts: { programLifetime: ["Node"] }, }; const llvm = emitLlvmModule(mod, { pointerBits: 64 }); - // Fields go widest first, each at the first offset that fits it. Node's - // weight (an int32 slot: nothing stores a fraction) takes the word the - // header shares (24 bytes, not 32); Leaf keeps Node's offsets and puts its - // bool next to Node's (32 bytes, not 48). - expect(llvm).toContain(`%${mangleClassStruct("Node")} = type { i32, i32, ptr, i8 }`); - expect(llvm).toContain(`%${mangleClassStruct("Leaf")} = type { i32, i32, ptr, i8, i8, ptr }`); + // Scalars narrower than 8 bytes go first, then the rest, widest first, + // each at the first offset that fits it. Node's weight (an int32 slot: + // nothing stores a fraction) takes the word the header shares and its + // flag the next one (24 bytes, not 32); Leaf keeps Node's offsets and puts + // its bool next to Node's (32 bytes, not 48). + expect(llvm).toContain(`%${mangleClassStruct("Node")} = type { i32, i32, i8, ptr }`); + expect(llvm).toContain(`%${mangleClassStruct("Leaf")} = type { i32, i32, i8, i8, ptr, ptr }`); // Without the contract, the layout is the declared one. const mortal = emitLlvmModule({ ...mod, contracts: {} }, { pointerBits: 64 }); expect(mortal).toContain(`%${mangleClassStruct("Leaf")} = type { i64, ptr, i8, i32, ptr, i8 }`); @@ -332,6 +342,99 @@ test("references between ProgramLifetime instances are 32-bit indexes into their expect(classFieldIndex(leafMeta, "label").compressed).toBe(false); }); +test("arrays of ProgramLifetime instances keep 32-bit references in 4-byte slots", () => { + const node = { kind: "object" as const, className: "Node" }; + const nodes = { kind: "array" as const, elem: node }; + const ref = (localId: string, type: IrType): IrExpr => ({ kind: "varRef", localId, type, loc }); + const fn = (name: string, body: IrExpr, returnType: IrType): IrFunction => ({ + name, + params: [ + { name: "a", localId: "a", type: nodes }, + { name: "v", localId: "v", type: node }, + ], + returnType, + locals: [ + { id: "a", name: "a", type: nodes, mutable: false }, + { id: "v", name: "v", type: node, mutable: false }, + ], + body: [{ kind: "return", value: body, loc }], + loc, + }); + const mod: IrModule = { + irVersion: 17, + sourceFile: loc.file, + entry: "main", + functions: [ + { name: "main", params: [], returnType: VOID, locals: [], body: [], loc }, + fn( + "first", + { + kind: "arrayGet", + arr: ref("a", nodes), + index: { kind: "numLit", value: 0, type: F64, loc }, + type: node, + loc, + }, + node, + ), + fn( + "add", + { + kind: "arrIntrinsic", + method: "push", + receiver: ref("a", nodes), + args: [ref("v", node)], + type: F64, + loc, + }, + F64, + ), + fn("make", { kind: "arrayLit", elems: [ref("v", node)], type: nodes, loc }, nodes), + fn( + "find", + { + kind: "arrIntrinsic", + method: "indexOf", + receiver: ref("a", nodes), + args: [ref("v", node)], + type: F64, + loc, + }, + F64, + ), + ], + classes: [{ name: "Node", fields: [{ name: "kids", type: nodes }], methods: [], loc }], + contracts: { programLifetime: ["Node"] }, + }; + const body = (llvm: string, name: string): string => + llvm.split(/^define /m).find((f) => f.includes(`; ${name}\n`)) ?? ""; + const llvm = emitLlvmModule(mod, { pointerBits: 64, objectAudit: false, lowRegion: true }); + // The element type's ops say so (SCR_ARR_OPS_COMPACT), so the runtime makes + // the arrays SCR_ELEM_CREF. + expect(llvm).toMatch( + /^@sc_arrops\.\S+\.c = internal global \{ ptr, ptr, ptr, i32, i32 \} \{ [^}]*, i32 0, i32 1 \}$/m, + ); + // A read loads the 32-bit reference and shifts it; an append stores one. + const first = body(llvm, "first"); + expect(first).toMatch(/getelementptr inbounds i32, ptr %\S+, i64 \S+\n\s+%\S+ = load i32/); + // The states follow 4-byte slots. + expect(first).toContain("getelementptr inbounds [5 x i8]"); + expect(first).toMatch(/= inttoptr i64 %\S+ to ptr/); + expect(body(llvm, "add")).toMatch( + /= lshr i64 %\S+, 3\n\s+%\S+ = trunc i64 %\S+ to i32\n\s+store i32/, + ); + // indexOf scans the 4-byte slots for the needle's reference. + expect(llvm).toMatch(/call double @sc_arr_index_of_cref\(ptr %\S+, ptr %\S+\)/); + expect(/^define internal double @sc_arr_index_of_cref[^]*?^}/m.exec(llvm)?.[0]).toContain( + "load i32, ptr %qp0", + ); + // Elsewhere the arrays keep 8-byte slots. + const elsewhere = emitLlvmModule(mod, { pointerBits: 64, objectAudit: false }); + expect(elsewhere).not.toMatch(/sc_arrops\.\S+\.c /); + expect(body(elsewhere, "first")).not.toContain("inttoptr"); + expect(elsewhere).not.toContain("sc_arr_index_of_cref"); +}); + test("a recycled block is cleared from the end of a ProgramLifetime instance's 32-bit header", () => { const BOOL = { kind: "bool" as const }; const mod: IrModule = { diff --git a/packages/compiler/src/backend/llvm/classes.ts b/packages/compiler/src/backend/llvm/classes.ts index 3073bd1bd..8f179828d 100644 --- a/packages/compiler/src/backend/llvm/classes.ts +++ b/packages/compiler/src/backend/llvm/classes.ts @@ -703,11 +703,13 @@ export function classFieldTypes(host: ShapeHost, cls: IrClassDef, int32?: Int32S /** Gives the classes `compact` admits (whole hierarchies: the test is the * root's) the 32-bit header and packed fields. A class keeps its base's - * field offsets, and places each of its own fields, widest first, at the - * first offset after the header that fits it: in a hole another field's - * alignment left, in the base's tail padding, or at the end. So 32-bit and - * smaller fields share 8-byte words. The struct lists the header and the - * fields by offset, which LLVM's layout of the type reproduces (checked). */ + * field offsets, and places each of its own fields at the first offset + * after the header that fits it: in a hole another field's alignment left, + * in the base's tail padding, or at the end. Scalars narrower than 8 bytes + * (flags, ids, counts) go first, widest first, so they share the header's + * cache line; then the other fields, widest first. So 32-bit and smaller + * fields share 8-byte words. The struct lists the header and the fields by + * offset, which LLVM's layout of the type reproduces (checked). */ export function assignCompactLayouts( metaMap: ReadonlyMap, compact: (root: LlClassMeta) => boolean, @@ -751,10 +753,12 @@ export function assignCompactLayouts( ); return COMPACT_REF; }); + const smallScalar = (f: { type: string; field: number; size: number }): number => + f.size < 8 && f.type !== "ptr" && !compressed[f.field] ? 0 : 1; const own = types .map((type, field) => ({ type, field, ...fieldStorage(type) })) .slice(baseCount) - .sort((a, b) => b.size - a.size || a.field - b.field); + .sort((a, b) => smallScalar(a) - smallScalar(b) || b.size - a.size || a.field - b.field); for (const f of own) { const sorted = [...elems].sort((a, b) => a.offset - b.offset); let at = -1; @@ -860,9 +864,9 @@ export function emitClassShapes( nullable?: NullableRefFields, int32?: Int32Slots, ): { typeDefs: string[]; defs: string[] } { - // Whether compact instances live in the program-lifetime region (any - // field holds a 32-bit reference to one; emitter.ts assignCompactLayouts). - const regionRefs = [...metaMap.values()].some((m) => m.compressed.some(Boolean)); + // Whether compact instances live in the program-lifetime region, where + // fields and arrays can refer to them in 32 bits (LlEmitter.compactRegion). + const regionRefs = host.compactRegion?.() === true; const typeDefs: string[] = []; const defs: string[] = []; const emitted = (mod.classes ?? []).filter((c) => !c.runtime); diff --git a/packages/compiler/src/backend/llvm/constant-callbacks.ts b/packages/compiler/src/backend/llvm/constant-callbacks.ts index 8a898d6a0..cf5c1b062 100644 --- a/packages/compiler/src/backend/llvm/constant-callbacks.ts +++ b/packages/compiler/src/backend/llvm/constant-callbacks.ts @@ -252,11 +252,13 @@ export function findConstantCallbacks( // entries actually used as indirect callees in each body. A module-wide // global/function cross product would dominate large-source emission. const globalTargets = new Map(); + const result = new Map>(); + // Only indirect callees get targets: without one, nothing resolves. + if (![...indirectCallees.values()].some((callees) => callees.size > 0)) return result; for (const [id, value] of globals) { const target = resolve(null, value); if (target) globalTargets.set(id, target); } - const result = new Map>(); for (const fn of mod.functions) { const needed = indirectCallees.get(fn.name)!; if (!needed.size) continue; diff --git a/packages/compiler/src/backend/llvm/dense-array-access.ts b/packages/compiler/src/backend/llvm/dense-array-access.ts index 4488f90cb..5d7994d42 100644 --- a/packages/compiler/src/backend/llvm/dense-array-access.ts +++ b/packages/compiler/src/backend/llvm/dense-array-access.ts @@ -27,7 +27,10 @@ import { storeArrCount, } from "./common.js"; import type { LlValue, LlvmEmitterContext } from "./expr-context.js"; +import { compactRefLoadLines, compactRefStoreLines } from "./classes.js"; +import { elemAccess } from "./shapes.js"; import { exactInteger, widenInteger } from "./integer-values.js"; +import { integerOrNaNIndex } from "./wrapping-sums.js"; const SCR_ARR_VALUE = 1; @@ -75,12 +78,17 @@ function emitDenseGuard( const B = host.B; const size = host.sizeType; const integer = exactInteger(host, index, indexExpr); + const nanIndex = integer ? null : integerOrNaNIndex(host, index, indexExpr); let wide: string; let integral: string | null = null; if (integer) { // A proven integer needs no double round-trip. Negative signed values // compare above any length or capacity as unsigned 64-bit integers. wide = widenInteger(host, integer); + } else if (nanIndex) { + // An exact integer or NaN is an index exactly when it is ordered. + wide = nanIndex.wide; + integral = nanIndex.ordered; } else { // freeze keeps NaN/out-of-range conversions from producing poison; the // exact round-trip comparison then rejects every value that is not an @@ -209,26 +217,40 @@ function statePointer( return statePtr; } -/** The dense slot type: boolean arrays store one byte per element, every - * other element kind an 8-byte slot. */ -function slotType(acc: ArrayAccess): "i8" | "i64" { - return acc === "bool" ? "i8" : "i64"; +/** How an array's dense slots hold its elements: the access kind, or + * `cref` for references to compact class instances (SCR_ELEM_CREF: a 4-byte + * slot holding the address over 8; LlEmitter.compactArrayElem). */ +type SlotKind = ArrayAccess | "cref"; + +function slotKind(host: LlvmEmitterContext, acc: ArrayAccess, element: IrType): SlotKind { + return acc === "ref" && host.compactArrayElem(element) ? "cref" : acc; +} + +/** The dense slot type: boolean arrays store one byte per element, compact + * references four, every other element kind an 8-byte slot. */ +function slotType(kind: SlotKind): "i8" | "i32" | "i64" { + return kind === "bool" ? "i8" : kind === "cref" ? "i32" : "i64"; } /** Bytes per dense slot (scr_arr_slot_width). */ -export function slotWidth(acc: ArrayAccess): number { - return acc === "bool" ? 1 : 8; +function slotWidth(kind: SlotKind): number { + return kind === "bool" ? 1 : kind === "cref" ? 4 : 8; +} + +/** Bytes per dense slot of an array of `element` (scr_arr_slot_width). */ +export function denseSlotWidth(host: LlvmEmitterContext, element: IrType): number { + return slotWidth(slotKind(host, elemAccess(element), element)); } function valuePointer( host: LlvmEmitterContext, guard: Pick, - acc: ArrayAccess, + kind: SlotKind, ): string { const B = host.B; const valuePtr = B.tmp(); B.line( - `${valuePtr} = getelementptr inbounds ${slotType(acc)}, ptr ${guard.data}, ${host.sizeType} ${guard.offset}`, + `${valuePtr} = getelementptr inbounds ${slotType(kind)}, ptr ${guard.data}, ${host.sizeType} ${guard.offset}`, ); host.markMemoryPointer(valuePtr, "array:elements"); return valuePtr; @@ -300,6 +322,7 @@ export function emitDenseArrayGet( const B = host.B; const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr"; const ret = acc === "bool" ? "zeroext i1" : ty; + const kind = slotKind(host, acc, element); host.declare(`declare ${ret} @${getter}(ptr, double)`); if (acc === "ref" && retain && !isRefCounted(element)) { const t = B.tmp(); @@ -322,7 +345,7 @@ export function emitDenseArrayGet( slow, true, valueLabel, - slotWidth(acc), + slotWidth(kind), proven, induction, ); @@ -333,7 +356,7 @@ export function emitDenseArrayGet( B.condBr(present, valueLabel, slow); } B.startBlock(valueLabel); - const ptr = valuePointer(host, guard, acc); + const ptr = valuePointer(host, guard, kind); let fast: string; let fastLabel = valueLabel; if (acc === "f64") { @@ -347,7 +370,13 @@ export function emitDenseArrayGet( } else { const raw = B.tmp(), nonnull = B.tmp(); - B.line(`${raw} = load ptr, ptr ${ptr}${host.fieldAliasAttachment(ptr)}`); + if (kind === "cref") { + const ref = B.tmp(); + B.line(`${ref} = load i32, ptr ${ptr}${host.fieldAliasAttachment(ptr)}`); + for (const line of compactRefLoadLines(ref, raw, `${raw}.r`)) B.line(line.trimStart()); + } else { + B.line(`${raw} = load ptr, ptr ${ptr}${host.fieldAliasAttachment(ptr)}`); + } B.line(`${nonnull} = icmp ne ptr ${raw}, null`); fastLabel = B.newLabel("arr.get.ref"); B.condBr(nonnull, fastLabel, slow); @@ -448,6 +477,7 @@ export function emitDenseArraySet( ): void { const B = host.B; const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr"; + const kind = slotKind(host, acc, element); const setter = `scr_arr_set_${acc}`; host.declare(`declare void @${setter}(ptr, double, ${acc === "bool" ? "i1 zeroext" : ty})`); const call = (attrs: string): void => @@ -467,7 +497,7 @@ export function emitDenseArraySet( slow, false, filledLabel, - slotWidth(acc), + slotWidth(kind), proven, ); const { offset, len } = guard; @@ -478,15 +508,20 @@ export function emitDenseArraySet( const bits = B.tmp(); B.line(`${bits} = zext i1 ${value} to i8`); B.line(`store i8 ${bits}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); + } else if (kind === "cref") { + const ref = B.tmp(); + for (const line of compactRefStoreLines(value, ref, `${ref}.r`)) B.line(line.trimStart()); + B.line(`store i32 ${ref}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); } else { B.line(`store ptr ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); } }; const statePtr = statePointer(host, guard); - const valuePtr = valuePointer(host, guard, acc); + const valuePtr = valuePointer(host, guard, kind); let oldState: string | null = null; let old: string | null = null; - if (acc === "ref") { + // Compact instances are never released: a replaced one needs nothing. + if (kind === "ref") { oldState = B.tmp(); old = B.tmp(); B.line(`${oldState} = load i8, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`); @@ -537,7 +572,7 @@ export function emitDenseArraySet( storeArrCount(host, nextLen, lenPtr, host.fieldAliasAttachment(lenPtr)); B.br(storedLabel); B.startBlock(storedLabel); - if (acc === "ref" && oldState && old) { + if (kind === "ref" && oldState && old) { const hadValue = B.tmp(); B.line(`${hadValue} = icmp eq i8 ${oldState}, ${SCR_ARR_VALUE}`); const releaseLabel = B.newLabel("arr.set.release"); @@ -549,9 +584,9 @@ export function emitDenseArraySet( // Below the bound the slot holds a present value: replace it, leaving // the state alone, and release a replaced reference after the store. B.startBlock(filledLabel); - const filledValuePtr = valuePointer(host, guard, acc); + const filledValuePtr = valuePointer(host, guard, kind); let filledOld: string | null = null; - if (acc === "ref") { + if (kind === "ref") { filledOld = B.tmp(); B.line( `${filledOld} = load ptr, ptr ${filledValuePtr}${host.fieldAliasAttachment(filledValuePtr)}`, @@ -578,10 +613,12 @@ export function emitDenseArrayPop( host: LlvmEmitterContext, array: string, acc: ArrayAccess, + element: IrType, empty: string, slow: string, ): string { const B = host.B; + const kind = slotKind(host, acc, element); const size = host.sizeType; const header = (field: number): string => { const ptr = B.tmp(); @@ -615,16 +652,23 @@ export function emitDenseArrayPop( states = B.tmp(); B.line(`${data} = load ptr, ptr ${dataPtr}${host.fieldAliasAttachment(dataPtr)}`); loadArrCount(host, cap, capPtr, host.fieldAliasAttachment(capPtr)); - scrArrStates(host, states, data, cap, slotWidth(acc)); - const valuePtr = valuePointer(host, { offset: last, data }, acc); + scrArrStates(host, states, data, cap, slotWidth(kind)); + const valuePtr = valuePointer(host, { offset: last, data }, kind); const statePtr = statePointer(host, { offset: last, states }); - const slotTy = slotType(acc); + const slotTy = slotType(kind); let raw = B.tmp(); B.line(`${raw} = load ${slotTy}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); if (slotTy === "i8") { const wide = B.tmp(); B.line(`${wide} = zext i8 ${raw} to i64`); raw = wide; + } else if (slotTy === "i32") { + // The 8-byte slot of a compact reference is its address. + const wide = B.tmp(), + address = B.tmp(); + B.line(`${wide} = zext i32 ${raw} to i64`); + B.line(`${address} = shl nuw nsw i64 ${wide}, 3`); + raw = address; } B.line(`store ${slotTy} 0, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); B.line(`store i8 0, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`); @@ -648,10 +692,12 @@ export function emitDenseArrayPush( host: LlvmEmitterContext, array: string, acc: ArrayAccess, + element: IrType | null, value: string, ): string { const B = host.B; const size = host.sizeType; + const kind = element === null ? acc : slotKind(host, acc, element); const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr"; const pusher = `scr_arr_push_${acc}`; host.declare(`declare double @${pusher}(ptr, ${acc === "bool" ? "i1 zeroext" : ty})`); @@ -685,16 +731,20 @@ export function emitDenseArrayPush( B.line(`${spare} = sub nuw ${size} ${cap}, ${len}`); B.line(`${near} = getelementptr inbounds i8, ptr ${data}, ${size} ${spare}`); B.line( - `${statePtr} = getelementptr inbounds [${slotWidth(acc)} x i8], ptr ${near}, ${size} ${cap}, ${size} -1`, + `${statePtr} = getelementptr inbounds [${slotWidth(kind)} x i8], ptr ${near}, ${size} ${cap}, ${size} -1`, ); host.markMemoryPointer(statePtr, "array:present"); - const valuePtr = valuePointer(host, { offset: len, data }, acc); + const valuePtr = valuePointer(host, { offset: len, data }, kind); if (acc === "f64") { B.line(`store double ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); } else if (acc === "bool") { const bits = B.tmp(); B.line(`${bits} = zext i1 ${value} to i8`); B.line(`store i8 ${bits}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); + } else if (kind === "cref") { + const ref = B.tmp(); + for (const line of compactRefStoreLines(value, ref, `${ref}.r`)) B.line(line.trimStart()); + B.line(`store i32 ${ref}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); } else { B.line(`store ptr ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); } diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index 5c9d8e60f..594634893 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -196,6 +196,55 @@ export class LlDyn { constructor(private readonly host: DynHost) {} + /** Journal replay (emit-journal.ts): builds or finds the helper a chunk + * emitter recorded, and answers its name. */ + replayHelper(op: string, args: readonly unknown[]): string { + switch (op) { + case "dyn.collectionDynOps": + return this.collectionDynOps(args[0] as IrType & { kind: "map" | "set" }); + case "dyn.dynMatchHelper": + return this.dynMatchHelper(args[0] as IrType, args[1] as boolean); + case "dyn.jsonSchema": + return this.jsonSchema(args[0] as IrType); + case "dyn.recordNameTable": { + const shapeId = args[0] as string; + return this.recordNameTable(shapeId, this.host.recordsById.get(shapeId)!).names; + } + case "dyn.jsonParseHelper": + return this.jsonParseHelper(args[0] as IrType)!; + case "dyn.dynCheckHelper": + return this.dynCheckHelper(args[0] as IrType, args[1] as boolean); + case "dyn.toDynHelper": + return this.toDynHelper(args[0] as IrType); + case "dyn.promiseDynAdapterHelper": + return this.promiseDynAdapterHelper(args[0] as IrType); + case "dyn.dynFuncThunkHelper": + return this.dynFuncThunkHelper(args[0] as IrType & { kind: "func" }); + case "dyn.dynFuncBoxHelper": + return this.dynFuncBoxHelper(args[0] as IrType & { kind: "func" }); + case "dyn.dynFuncAdapterHelper": + return this.dynFuncAdapterHelper(args[0] as IrType & { kind: "func" }); + case "dyn.dynToStrHelper": + return this.dynToStrHelper(); + case "dyn.dynIterNHelper": + return this.dynIterNHelper(); + case "dyn.dynDestrCheckHelper": + return this.dynDestrCheckHelper(); + case "dyn.dynKeyGetHelper": + return this.dynKeyGetHelper(); + case "dyn.dynComputedKeyGetHelper": + return this.dynComputedKeyGetHelper(); + case "dyn.caughtToDynHelper": + return this.caughtToDynHelper(); + case "dyn.messageListenerHelper": + return this.messageListenerHelper(args[0] as IrType & { kind: "func" }) ?? ""; + case "dyn.messageReaderHelper": + return this.messageReaderHelper(args[0] as IrType); + default: + throw new InternalCompilerError(`llvm emitter bug: no dyn helper replay ${op}`); + } + } + /** The types a dyn matcher or builder for `t` checks structurally: record * fields and index values, array elements and union arms. */ private structuralChildren(t: IrType): readonly IrType[] { @@ -385,6 +434,7 @@ export class LlDyn { if (prior) return prior; const name = `sc_map_dyn_${this.mapDynOps.size}`; this.mapDynOps.set(key, name); + this.host.journalAllocated?.(name, "dyn.collectionDynOps", [t]); const keyType = t.kind === "map" ? t.key : t.elem; const valueType: IrType = t.kind === "map" ? t.value : { kind: "f64" }; const emitSlot = (type: IrType, role: string): void => { @@ -442,6 +492,7 @@ export class LlDyn { `@${name} = internal constant { ptr, ptr, ptr, ptr, ptr, ptr } { ptr ${this.host.cstr(key)}, ptr @${name}_key_box, ptr @${name}_key_unbox, ptr @${this.dynMatchHelper(keyType)}, ptr @${name}_value_box, ptr @${name}_value_unbox }`, "", ); + this.host.journalBuilt?.(); return name; } @@ -840,6 +891,7 @@ export class LlDyn { if (existing) return existing; const name = `sc_dm_${this.dynMatchers.size}`; this.dynMatchers.set(matcherKey, name); + this.host.journalAllocated?.(name, "dyn.dynMatchHelper", [t, preserveRefs]); const recursive = this.isRecursiveShape(t); const wrapped = recursive || this.holdsRecursiveShape(t); const B = new BlockBuilder(); @@ -1200,6 +1252,7 @@ export class LlDyn { ``, ); if (wrapped) this.emitVisitWrapper(name, t, recursive ? "match" : "visit"); + this.host.journalBuilt?.(); return name; } @@ -1293,6 +1346,7 @@ export class LlDyn { const S = host.sizeType; const name = `sc_jsch_${this.jsonSchemas.size}`; this.jsonSchemas.set(key, name); + this.host.journalAllocated?.(name, "dyn.jsonSchema", [t]); let body: string; switch (t.kind) { case "f64": @@ -1364,6 +1418,7 @@ export class LlDyn { `@${name} = internal constant { ${S}, ${S}, ptr, ptr, ptr, ptr, ptr, ptr, ${S} } { ${body} }`, ``, ); + this.host.journalBuilt?.(); return name; } @@ -1453,6 +1508,7 @@ export class LlDyn { const base = `sc_rnames_${this.recordNameTables.size}`; const table = { names: `@${base}`, lens: `@${base}_lens` }; this.recordNameTables.set(shapeId, table); + this.host.journalAllocated?.(base, "dyn.recordNameTable", [shapeId]); const n = shape.fields.length; const names = recordSourceFieldOrder(shape); this.defs.push( @@ -1460,6 +1516,7 @@ export class LlDyn { `@${base}_lens = internal constant [${n} x ${S}] [${names.map((name) => `${S} ${Buffer.byteLength(name, "utf8")}`).join(", ")}]`, ``, ); + this.host.journalBuilt?.(); return table; } @@ -1478,6 +1535,7 @@ export class LlDyn { const host = this.host; const name = `sc_jp_${this.jsonParsers.size}`; this.jsonParsers.set(key, name); + this.host.journalAllocated?.(name, "dyn.jsonParseHelper", [t]); const schema = this.jsonSchema(t); const check = this.dynCheckHelper(t); host.declare(`declare ptr @scr_json_parse_schema(ptr, ptr)`); @@ -1504,6 +1562,7 @@ export class LlDyn { `}`, ``, ); + this.host.journalBuilt?.(); return name; } @@ -1519,6 +1578,7 @@ export class LlDyn { if (existing) return existing; const name = `sc_dc_${this.dynBuilders.size}`; this.dynBuilders.set(builderKey, name); + this.host.journalAllocated?.(name, "dyn.dynCheckHelper", [t, preserveRefs]); const recursive = this.isRecursiveShape(t); const memo = !recursive && this.holdsRecursiveShape(t) && this.valTy(t) === "ptr"; const host = this.host; @@ -1541,6 +1601,7 @@ export class LlDyn { `}`, ``, ); + this.host.journalBuilt?.(); return name; } if (isRefCounted(t) && t.kind !== "dyn") { @@ -2537,6 +2598,7 @@ export class LlDyn { ``, ); if (recursive || memo) this.emitVisitWrapper(name, t, recursive ? "build" : "memo"); + this.host.journalBuilt?.(); return name; } @@ -2551,6 +2613,7 @@ export class LlDyn { if (existing) return existing; const name = `sc_td_${this.toDynFns.size}`; this.toDynFns.set(key, name); + this.host.journalAllocated?.(name, "dyn.toDynHelper", [t]); const host = this.host; const B = new BlockBuilder(); let sourceAccessor: { name: string; release: string } | null = null; @@ -3157,6 +3220,7 @@ export class LlDyn { ``, ); } + this.host.journalBuilt?.(); return name; } @@ -3173,6 +3237,7 @@ export class LlDyn { if (existing) return existing; const name = `sc_pda_${this.promiseDynAdapters.size}`; this.promiseDynAdapters.set(key, name); + this.host.journalAllocated?.(name, "dyn.promiseDynAdapterHelper", [inner]); const host = this.host; const B = new BlockBuilder(); host.declare(`declare void @scr_promise_fulfill_ref(ptr, ptr, ptr, ptr, ptr)`); @@ -3250,6 +3315,7 @@ export class LlDyn { `}`, ``, ); + this.host.journalBuilt?.(); return name; } @@ -3262,6 +3328,7 @@ export class LlDyn { if (this.dynToStrFn) return this.dynToStrFn; const name = "sc_ds"; this.dynToStrFn = name; + this.host.journalAllocated?.(name, "dyn.dynToStrHelper", []); const host = this.host; host.declare(`declare void @scr_jb_init(ptr)`); host.declare(`declare ptr @scr_jb_finish(ptr)`); @@ -3566,6 +3633,7 @@ export class LlDyn { ``, ); } + this.host.journalBuilt?.(); return name; } @@ -3577,6 +3645,7 @@ export class LlDyn { if (this.caughtToDynFn) return this.caughtToDynFn; const name = "sc_cd"; this.caughtToDynFn = name; + this.host.journalAllocated?.(name, "dyn.caughtToDynHelper", []); const host = this.host; const B = new BlockBuilder(); const kp = B.tmp(); @@ -3682,6 +3751,7 @@ export class LlDyn { `}`, ``, ); + this.host.journalBuilt?.(); return name; } @@ -3695,6 +3765,7 @@ export class LlDyn { this.helperMemo.set(name, name); const B = new BlockBuilder(); const host = this.host; + this.host.journalAllocated?.(name, "dyn.dynComputedKeyGetHelper", []); const stringHelper = this.dynKeyGetHelper(); host.declare(`declare ptr @scr_dyn_property_key_value(ptr)`); host.declare(`declare ptr @scr_dyn_symbol_key_get(ptr, ptr, i1 zeroext)`); @@ -3732,6 +3803,7 @@ export class LlDyn { "}", "", ); + this.host.journalBuilt?.(); return name; } @@ -3741,6 +3813,7 @@ export class LlDyn { if (existing) return existing; const name = "sc_dyn_key_get"; this.dynBuilders.set(memoKey, name); + this.host.journalAllocated?.(name, "dyn.dynKeyGetHelper", []); const host = this.host; const B = new BlockBuilder(); const kd = this.kindOf(B, "%d"); @@ -4134,6 +4207,7 @@ export class LlDyn { `}`, ``, ); + this.host.journalBuilt?.(); return name; } @@ -4145,6 +4219,7 @@ export class LlDyn { if (existing) return existing; const name = "sc_dyn_destr_check"; this.dynBuilders.set(memoKey, name); + this.host.journalAllocated?.(name, "dyn.dynDestrCheckHelper", []); const host = this.host; host.declare(`declare void @scr_jb_init(ptr)`); host.declare(`declare ptr @scr_jb_finish(ptr)`); @@ -4199,6 +4274,7 @@ export class LlDyn { `}`, ``, ); + this.host.journalBuilt?.(); return name; } @@ -4210,6 +4286,7 @@ export class LlDyn { if (existing) return existing; const name = "sc_dyn_iter_n"; this.dynBuilders.set(memoKey, name); + this.host.journalAllocated?.(name, "dyn.dynIterNHelper", []); const host = this.host; host.declare(`declare ptr @scr_dyn_new_arr()`); host.declare(`declare void @scr_dyn_arr_push(ptr, ptr)`); @@ -4414,6 +4491,7 @@ export class LlDyn { `}`, ``, ); + this.host.journalBuilt?.(); return name; } @@ -4445,6 +4523,7 @@ export class LlDyn { if (existing) return existing; const name = `sc_dfk_${this.dynFuncThunks.size}`; this.dynFuncThunks.set(key, name); + this.host.journalAllocated?.(name, "dyn.dynFuncThunkHelper", [t]); const host = this.host; const B = new BlockBuilder(); const argNames: string[] = []; @@ -4594,6 +4673,7 @@ export class LlDyn { `}`, ``, ); + this.host.journalBuilt?.(); return name; } @@ -4604,6 +4684,7 @@ export class LlDyn { if (existing) return existing; const name = `sc_dfb_${this.dynFuncBoxes.size}`; this.dynFuncBoxes.set(key, name); + this.host.journalAllocated?.(name, "dyn.dynFuncBoxHelper", [t]); const host = this.host; const thunk = this.dynFuncThunkHelper(t); host.declare(`declare ptr @scr_closure_retain_v(ptr)`); @@ -4618,6 +4699,7 @@ export class LlDyn { `}`, ``, ); + this.host.journalBuilt?.(); return name; } @@ -4640,11 +4722,15 @@ export class LlDyn { (t.ret.kind === "void" || t.ret.kind === "f64" || t.ret.kind === "bool") && this.messageReadable(param, new Set()); if (!supported) { + // A signature without an entry still takes a number. this.messageListeners.set(key, null); + this.host.journalAllocated?.("", "dyn.messageListenerHelper", [t]); + this.host.journalBuilt?.(); return null; } const name = `sc_dml_${this.messageListeners.size}`; this.messageListeners.set(key, name); + this.host.journalAllocated?.(name, "dyn.messageListenerHelper", [t]); const B = new BlockBuilder(); B.entryAllocas.push(`%fail = alloca i1`); B.line(`store i1 false, ptr %fail`); @@ -4676,6 +4762,7 @@ export class LlDyn { `}`, ``, ); + this.host.journalBuilt?.(); return name; } @@ -4737,6 +4824,7 @@ export class LlDyn { if (existing) return existing; const name = `sc_dmr_${this.messageReaders.size}`; this.messageReaders.set(key, name); + this.host.journalAllocated?.(name, "dyn.messageReaderHelper", [t]); const host = this.host; const S = host.sizeType; if (t.kind !== "record") host.declare(`declare i32 @scr_message_kind(ptr, ${S})`); @@ -4909,6 +4997,7 @@ export class LlDyn { `}`, ``, ); + this.host.journalBuilt?.(); return name; } @@ -4920,6 +5009,7 @@ export class LlDyn { if (existing) return existing; const name = `sc_dfa_${this.dynFuncAdapters.size}`; this.dynFuncAdapters.set(key, name); + this.host.journalAllocated?.(name, "dyn.dynFuncAdapterHelper", [t]); const host = this.host; const B = new BlockBuilder(); host.declare(`declare ptr @scr_box_get_ref(ptr)`); @@ -5022,6 +5112,7 @@ export class LlDyn { `}`, ``, ); + this.host.journalBuilt?.(); return name; } } diff --git a/packages/compiler/src/backend/llvm/emit-function.ts b/packages/compiler/src/backend/llvm/emit-function.ts index 55ce97894..16e654393 100644 --- a/packages/compiler/src/backend/llvm/emit-function.ts +++ b/packages/compiler/src/backend/llvm/emit-function.ts @@ -54,6 +54,8 @@ import { f64Lit } from "./common.js"; import { RESULT_ENTRY_PREFIX } from "./record-results.js"; import { emitIntegerClone, emitIntegerDispatcher } from "./integer-clones.js"; import { beginFunctionLoops, LOOPS } from "./loop-state.js"; +import { runningExtremumUpdates } from "./running-extrema.js"; +import { findIntegerOrNaNLocals } from "./wrapping-sums.js"; export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { const B = new BlockBuilder(); @@ -80,6 +82,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { emitter.currentLocals.set(id, plain); emitter.lazyCaptureBoxes.set(id, ""); } + emitter.runningExtrema = emitter.debug === null ? runningExtremumUpdates(fn) : new Set(); emitter.currentConstantCallbacks = emitter.debug === null ? (emitter.constantCallbacks.get(fn.name) ?? new Map()) : new Map(); emitter.knownCallbacks.clear(); @@ -215,6 +218,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { emitter.integerRanges = rangeFacts.ranges; emitter.integerLoopRanges = rangeFacts.loopRanges; emitter.int32Elements = slotFacts.elements ?? null; + emitter.integerOrNaNLocals = findIntegerOrNaNLocals(emitter, numericFn); emitter.bytesBounds = findBytesBounds(numericFn, emitter.integerRanges); emitter.bytesOwned = new Set(); // Debug builds keep every number local a double for the debugger's diff --git a/packages/compiler/src/backend/llvm/emit-journal.ts b/packages/compiler/src/backend/llvm/emit-journal.ts new file mode 100644 index 000000000..ad89d2671 --- /dev/null +++ b/packages/compiler/src/backend/llvm/emit-journal.ts @@ -0,0 +1,237 @@ +/* Chunked function emission (emit-module.ts). An emitter that emits one + * contiguous range of a module's functions records here the helpers and + * sites it numbers from a counter (`sc_td_${size}` and the like). The + * emitter that merges the ranges replays each range's records in order: + * replay calls the same entry point with the same arguments, so every + * helper gets the number and the definition the serial emission gives it, + * and the range's text is renamed to those numbers. */ +import type { LlEmitter } from "./emitter.js"; + +/** One event of a range's emission, in order. */ +export type JournalEntry = + /** A helper or site the range's emitter numbered: the name it gave it + * (a counter prefix and a number), and the replay entry point + * (registerReplay) with its arguments, which are plain IR values. */ + | { + readonly kind: "op"; + readonly name: string; + readonly op: string; + readonly args: readonly unknown[]; + } + /** The first request of a key of a module-level table made outside any + * replayable definition, with the value the range's emitter stored. */ + | { + readonly kind: "entry"; + readonly table: string; + readonly key: string; + readonly value: unknown; + }; + +export class EmitJournal { + readonly entries: JournalEntry[] = []; + /** The replayable definitions being built, innermost last. */ + private readonly open: string[] = []; + + /** The definitions still open (empty between functions). */ + get unclosed(): readonly string[] { + return this.open; + } + /** Keys already recorded per table. */ + private readonly recorded = new Map>(); + + /** A keyed helper, recorded when its number is taken, before anything its + * definition creates in turn: that keeps the serial numbering order. + * Replaying a helper that exists answers its name, so helpers created + * inside another helper's definition are recorded too. Opens the + * definition, which the creating entry point closes with `built`. */ + allocated(name: string, op: string, args: readonly unknown[]): void { + this.entries.push({ kind: "op", name, op, args }); + this.open.push(op); + } + + /** Opens a definition whose helper is not replayed itself (its + * entry point records the request with `used`). */ + opened(op: string): void { + this.open.push(op); + } + + /** A request, made outside any definition, of the helper `name` that an + * entry point answered; recorded once per name. Replay makes the same + * request. */ + used(name: string, op: string, args: readonly unknown[]): void { + if (this.open.length !== 0) return; + if (this.usedNames.has(name)) return; + this.usedNames.add(name); + this.entries.push({ kind: "op", name, op, args }); + } + private readonly usedNames = new Set(); + + /** Closes the definition `allocated` or `opened` opened. */ + built(): void { + if (this.open.pop() === undefined) this.overclosed = true; + } + + /** A `built` closed a definition no `allocated` opened. */ + overclosed = false; + + /** An unkeyed site (one per use). Sites taken while a definition is built + * are taken again when the merging emitter replays that definition. */ + site(name: string, op: string, args: readonly unknown[] = []): void { + if (this.open.length === 0) this.entries.push({ kind: "op", name, op, args }); + } + + /** A request of `key` in a module-level table. Only the first request + * made outside any definition is recorded: requests made while a + * definition is built happen again when it is replayed. */ + entry(table: string, key: string, value: unknown): void { + if (this.open.length !== 0) return; + let keys = this.recorded.get(table); + if (keys === undefined) this.recorded.set(table, (keys = new Set())); + if (keys.has(key)) return; + keys.add(key); + this.entries.push({ kind: "entry", table, key, value }); + } +} + +/** A range emitter's result (LlEmitter.chunkResult): plain data, so a + * worker process can write it. */ +export interface ChunkResult { + readonly entries: readonly JournalEntry[]; + readonly flags: { + readonly usesCoroutines: boolean; + readonly usesTimers: boolean; + readonly usesGenSlots: boolean; + readonly needsUnionNone: boolean; + readonly needsNullableAbsent: boolean; + readonly needsOom: boolean; + readonly needsBadTag: boolean; + readonly needsBadKey: boolean; + readonly needsRetainBox: boolean; + }; + readonly publishedNamesSym: string | null; + readonly rcHelpers: readonly string[]; + readonly immortalValues: readonly string[]; + readonly fnValues: readonly string[]; + readonly arraySearchKinds: readonly string[]; + readonly recordCloneShapes: readonly string[]; + readonly awaitCallees: readonly string[]; + readonly adoptCallees: readonly string[]; + readonly directAwaitCallees: readonly string[]; + readonly inlineAwaitCallees: readonly string[]; +} + +type Replay = (emitter: LlEmitter, args: readonly unknown[]) => string; +const replays = new Map(); + +/** Registers the entry point that creates (or finds) a numbered helper or + * site from its recorded arguments and answers its name. */ +export function registerReplay(op: string, replay: Replay): void { + replays.set(op, replay); +} + +/** Replays a range's entries on the merging emitter, in record order, and + * records the renames from the range's names to its own in `names`. */ +export function replayJournal( + emitter: LlEmitter, + entries: readonly JournalEntry[], + names: Map, + tags: Map, +): void { + for (const entry of entries) { + if (entry.kind === "entry") { + emitter.applyJournalEntry(entry.table, entry.key, entry.value, names, tags); + continue; + } + const replay = replays.get(entry.op); + if (replay === undefined) + throw new Error(`llvm emitter bug: no replay for journal entry ${entry.op}`); + const own = stripSigil(replay(emitter, entry.args)); + const local = stripSigil(entry.name); + if (own !== local) names.set(local, own); + } +} + +/** The rename of a range's text: numbered names (`names`, without the + * sigil) and field alias tags (`tags`). One scan for each kind of name: + * a numbered name starts `sc_` after a character that cannot be part of a + * name, and ends at the last digit after its counter prefix. */ +export function renameChunk( + names: ReadonlyMap, + tags: ReadonlyMap, +): (text: string) => string { + const prefixes = new Set(); + for (const name of names.keys()) prefixes.add(name.replace(/\d+$/, "")); + if (prefixes.size === 0 && tags.size === 0) return (text) => text; + return (text) => { + let out = prefixes.size === 0 ? text : renameNames(text, names, prefixes); + if (tags.size > 0) out = renameTags(out, tags); + return out; + }; +} + +function renameNames( + text: string, + names: ReadonlyMap, + prefixes: ReadonlySet, +): string { + const parts: string[] = []; + let copied = 0; + for (let at = text.indexOf("sc_"); at >= 0; at = text.indexOf("sc_", at + 3)) { + if (at > 0 && isNameChar(text.charCodeAt(at - 1))) continue; + // The counter prefix ends where the digits start; the number ends at + // the last digit. + let end = at + 3; + while (end < text.length && isNameChar(text.charCodeAt(end)) && !isDigit(text.charCodeAt(end))) + end++; + if (end === text.length || !isDigit(text.charCodeAt(end))) continue; + if (!prefixes.has(text.slice(at, end))) continue; + let last = end; + while (last < text.length && isDigit(text.charCodeAt(last))) last++; + const own = names.get(text.slice(at, last)); + if (own === undefined) continue; + parts.push(text.slice(copied, at), own); + copied = last; + } + if (copied === 0) return text; + parts.push(text.slice(copied)); + return parts.join(""); +} + +function renameTags(text: string, tags: ReadonlyMap): string { + const marker = "!tbaa !"; + const parts: string[] = []; + let copied = 0; + for (let at = text.indexOf(marker); at >= 0; at = text.indexOf(marker, at + marker.length)) { + const start = at + marker.length; + let end = start; + while (end < text.length && isDigit(text.charCodeAt(end))) end++; + const own = tags.get(Number(text.slice(start, end))); + if (own === undefined) continue; + parts.push(text.slice(copied, start), String(own)); + copied = end; + } + if (copied === 0) return text; + parts.push(text.slice(copied)); + return parts.join(""); +} + +/** `[-$._A-Za-z0-9]`: a character of an LLVM identifier. */ +function isNameChar(code: number): boolean { + return ( + (code >= 48 && code <= 57) || + (code >= 65 && code <= 90) || + (code >= 97 && code <= 122) || + code === 95 || + code === 36 || + code === 45 || + code === 46 + ); +} + +function isDigit(code: number): boolean { + return code >= 48 && code <= 57; +} + +function stripSigil(name: string): string { + return name.startsWith("@") ? name.slice(1) : name; +} diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts index 3d9d41072..fa5df1f38 100644 --- a/packages/compiler/src/backend/llvm/emit-module.ts +++ b/packages/compiler/src/backend/llvm/emit-module.ts @@ -2,6 +2,10 @@ import { relieveHeapPressure } from "../../heap-pressure.js"; import { emitWasmModules } from "./wasm-modules.js"; import { type LlEmitter, ffiCallbackDummyLl, llStrBytes } from "./emitter.js"; +import { type ChunkResult, EmitJournal } from "./emit-journal.js"; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { JS_IC_WORDS } from "./js-ic.js"; import { mangleAsyncSpawn, @@ -35,9 +39,11 @@ import { emitClassMembershipHelper } from "./classes.js"; import { type RegexMatcherCode, compileRegexMatcher } from "./regex-matchers.js"; import { MAP_ITER_DECL, + MAP_SIZE_DECL, MAP_ITER_NUM_DECL, SCR_MAP_NUM_DEAD, NUMBER_MAP_ENTRY_DECL, + STRING_MAP_ENTRY_C_DECL, STRING_MAP_ENTRY_DECL, } from "./map-number-lookup.js"; import { RUNTIME_ABI_MARKER } from "../runtime-abi.js"; @@ -59,16 +65,150 @@ import { STRING_READ_HELPERS, stringScanDecl } from "./string-reads.js"; import { emitLibraryIdentityLines } from "../library-identity-markers.js"; import { InternalCompilerError } from "../../errors.js"; +/** The function bodies emitted in `chunks` contiguous ranges, each by its + * own emitter (LlEmitter.chunkEmitter), then merged in function order: the + * text one emitter gives. */ +function emitFunctionsInChunks(emitter: LlEmitter, chunks: number): string[] { + const functions = emitter.mod.functions; + const size = Math.ceil(functions.length / chunks); + const ranges: { chunk: LlEmitter; texts: string[] }[] = []; + for (let start = 0; start < functions.length; start += size) { + const chunk = emitter.chunkEmitter(); + const texts = functions.slice(start, start + size).map((fn) => { + const text = chunk.emitFunction(fn); + if (chunk.journal!.overclosed) + throw new Error(`llvm emitter bug: journal definition closed twice in ${fn.name}`); + if (chunk.journal!.unclosed.length > 0) + throw new Error( + `llvm emitter bug: unclosed journal definitions after ${fn.name}: ${chunk.journal!.unclosed.join(", ")}`, + ); + return text; + }); + ranges.push({ chunk, texts }); + } + const fnDefs: string[] = []; + for (const { chunk, texts } of ranges) { + // The plain-data result a worker process would write, round-tripped. + const result = JSON.parse(JSON.stringify(chunk.chunkResult())) as ChunkResult; + const rename = emitter.mergeChunk(result); + for (const text of texts) { + const def = rename(text); + emitter.fnDefText.set(functions[fnDefs.length]!.name, def); + fnDefs.push(def); + } + } + return fnDefs; +} + +/** Modules with fewer functions emit in one process: forking and merging + * would cost more than the workers save (SCRIPTC_EMIT_WORKER_FUNCTIONS + * lowers it, for tests). */ +function workerMinFunctions(): number { + const value = Number(process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"] ?? "2000"); + return Number.isFinite(value) && value > 0 ? value : 2000; +} + +/** The function bodies emitted on forked worker processes, `workers` at a + * time: the function list splits into contiguous ranges, each emitted in + * its own process, which inherits this emitter as it is at the fork and + * records a journal (emit-journal.ts). Ranges merge in function order, so + * the text is the serial text. A range whose worker fails is emitted here + * when its turn comes. Null without fork primitives (Node, Windows, inside + * a worker) or when the module is small. */ +function emitFunctionsOnWorkers(emitter: LlEmitter, workers: number): string[] | null { + const fork = emitter.targetOptions.forkPrimitives; + const functions = emitter.mod.functions; + if (fork === undefined || workers < 2) return null; + if (functions.length < workerMinFunctions()) return null; + // More ranges than workers: a range's merge overlaps the later ranges' + // emission, and a slow range holds back less. + const count = Math.min(functions.length, workers * 3); + const size = Math.ceil(functions.length / count); + const dir = mkdtempSync(join(tmpdir(), "scriptc-emit-")); + const pids: number[] = []; + const start = (range: number): void => { + const pid = fork.fork(); + if (pid === 0) { + let code = 1; + try { + emitter.journal = new EmitJournal(); + const texts: string[] = []; + for (const fn of functions.slice(range * size, (range + 1) * size)) { + texts.push(emitter.emitFunction(fn)); + relieveHeapPressure(); + } + writeFileSync(join(dir, `${range}.json`), JSON.stringify(emitter.chunkResult())); + // Function text never holds NUL, so it separates the functions. + writeFileSync(join(dir, `${range}.ll`), texts.join("\0")); + code = 0; + } catch { + // The parent emits the range itself. + } + fork.exit(code); + } + pids.push(pid); + }; + const fnDefs: string[] = []; + try { + for (let range = 0; range < Math.min(workers, count); range++) start(range); + for (let range = 0; range < count; range++) { + const pid = pids[range]!; + const status = pid > 0 ? fork.wait(pid) : -1; + // The next worker starts from this emitter as merged so far, which + // the journal allows: it records requests, not table contents. + const merged = status === 0 ? readRange(dir, range) : null; + const slice = functions.slice(range * size, (range + 1) * size); + if (merged !== null && merged.texts.length === slice.length) { + const rename = emitter.mergeChunk(merged.result); + slice.forEach((fn, index) => { + const def = rename(merged.texts[index]!); + emitter.fnDefText.set(fn.name, def); + fnDefs.push(def); + }); + } else + for (const fn of slice) { + const def = emitter.emitFunction(fn); + emitter.fnDefText.set(fn.name, def); + fnDefs.push(def); + relieveHeapPressure(); + } + if (pids.length < count) start(pids.length); + } + } finally { + rmSync(dir, { recursive: true, force: true }); + } + return fnDefs; +} + +/** A worker's range result, or null when its files are missing. */ +function readRange(dir: string, range: number): { result: ChunkResult; texts: string[] } | null { + try { + const result = JSON.parse(readFileSync(join(dir, `${range}.json`), "utf8")) as ChunkResult; + const texts = readFileSync(join(dir, `${range}.ll`), "utf8").split("\0"); + return { result, texts }; + } catch { + return null; + } +} + export function emitModuleParts(emitter: LlEmitter): string[] { // Function bodies first (the literal/unit/fn-value tables fill as they // emit), then the file assembles around them — the runtime ABI’s order. - const fnDefs: string[] = []; - for (const fn of emitter.mod.functions) { - const def = emitter.emitFunction(fn); - fnDefs.push(def); - emitter.fnDefText.set(fn.name, def); - relieveHeapPressure(); - } + // Debug metadata is numbered per emitter and not journaled, so a debug + // build emits every body here. + const ranges = emitter.debug === null; + const chunks = emitter.targetOptions.functionChunks ?? 1; + const fnDefs = + (ranges && chunks > 1 ? emitFunctionsInChunks(emitter, chunks) : null) ?? + (ranges ? emitFunctionsOnWorkers(emitter, emitter.targetOptions.functionWorkers ?? 1) : null) ?? + []; + if (fnDefs.length === 0) + for (const fn of emitter.mod.functions) { + const def = emitter.emitFunction(fn); + fnDefs.push(def); + emitter.fnDefText.set(fn.name, def); + relieveHeapPressure(); + } emitter.selectStacklessAsync(fnDefs); const errorViews: string[] = []; for (const property of ["name", "message"]) @@ -456,15 +596,18 @@ export function emitModuleParts(emitter: LlEmitter): string[] { // RC fast paths test the traced byte (shapes.ts). ...(emitter.rcHelpers === null ? [] : [`%ScrMapRc = type { ${emitter.sizeType}, i8, i8, i8 }`]), // The complete ScrMap and its entries, read by the inline number-key - // lookup (sc_map_entry_f64) and the inline iteration steps - // (sc_map_iter_entry, sc_map_iter_entry_num): field 8 is the i32 entry + // lookup (sc_map_entry_f64), the inline iteration steps + // (sc_map_iter_entry, sc_map_iter_entry_num) and Map.size + // (sc_map_size): field 8 is the i32 entry count, field 9 the i32 live // count, field 10 the i64 absent-key filter, entries is field 11, // nbuckets and buckets fields 13 and 14, the direct index and its i32 // length fields 15 and 16. ...(emitter.decls.has(NUMBER_MAP_ENTRY_DECL) || emitter.decls.has(STRING_MAP_ENTRY_DECL) || + emitter.decls.has(STRING_MAP_ENTRY_C_DECL) || emitter.decls.has(MAP_ITER_DECL) || - emitter.decls.has(MAP_ITER_NUM_DECL) + emitter.decls.has(MAP_ITER_NUM_DECL) || + emitter.decls.has(MAP_SIZE_DECL) ? [ `%ScrMapIx = type { ${emitter.sizeType}, i8, i8, i8, i8, i32, ptr, ptr, i32, i32, i64, ptr, i32, i32, ptr, ptr, i32, i32 }`, ] @@ -479,6 +622,11 @@ export function emitModuleParts(emitter: LlEmitter): string[] { emitter.decls.has(MAP_ITER_NUM_DECL) ? [`%ScrMapEntry = type { i64, i64, i64 }`, `%ScrMapNumEntry = type { i64, i64 }`] : []), + // A compact-value map's { key, hash, val } entry (scr_map.h ScrMapCEntry): + // a 32-bit hash and the value as an address over 8. + ...(emitter.decls.has(STRING_MAP_ENTRY_C_DECL) + ? [`%ScrMapCEntry = type { i64, i32, i32 }`] + : []), // The runtime error prefix { rc, vt, name, message, code, cause } and the // class-object shape { rc, pre, post, ctor, name } — field reads on // builtin errors and classval loads GEP through these. @@ -545,7 +693,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] { ``, `@scr_error_vts = external ${tl}global [${RUNTIME_ERROR_CLASSES.size} x %ScrVt]`, `declare void @scr_init()`, - ...(emitter.usesCompactRefs ? [`declare void @scr_plr_init()`] : []), + ...(emitter.compactRegion ? [`declare void @scr_plr_init()`] : []), `declare void @scr_lib_init(i32, ptr)`, ...(emitter.mod.workers ? [`declare void @scr_runtime_workers_v8()`] : []), ...(emitter.stackChecks?.size ? [`declare void @scr_stack_guard_init()`] : []), @@ -727,6 +875,8 @@ export function emitModuleParts(emitter: LlEmitter): string[] { out.push(`@${sym} = internal constant { ptr, ptr, ptr, ptr, ptr, ptr, ptr } { ${body} }`); } if (emitter.mapOpsConsts.size > 0) out.push(``); + for (const sym of emitter.methodIdSlots.values()) out.push(`@${sym} = internal global i32 -1`); + if (emitter.methodIdSlots.size > 0) out.push(``); for (const line of ffiCallbacks.globals) out.push(line); if (ffiCallbacks.globals.length > 0) out.push(``); @@ -840,7 +990,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] { ` call void @scr_init()`, // Compressed fields hold addresses in the program-lifetime region, which // is reserved below 32 GiB before compiled code runs. - ...(emitter.usesCompactRefs ? [` call void @scr_plr_init()`] : []), + ...(emitter.compactRegion ? [` call void @scr_plr_init()`] : []), // Each context (the main thread, every worker) installs its stack // guard before running compiled code that checks it. ...(emitter.stackChecks?.size ? [` call void @scr_stack_guard_init()`] : []), @@ -1293,6 +1443,18 @@ export function helperDefs(emitter: LlEmitter): string[] { ``, ); } + // Map.size and Set.size read the live entry count (scr_map_size). + if (emitter.decls.has(MAP_SIZE_DECL)) + defs.push( + `define internal double @sc_map_size(ptr %m) ${FN_ATTRS} {`, + `entry:`, + ` %np = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 9`, + ` %n = load i32, ptr %np`, + ` %nf = uitofp i32 %n to double`, + ` ret double %nf`, + `}`, + ``, + ); // String-key Map/Set lookups test the map's absent-key filter inline // (scr_map.c scr_map_filter_bits): a key whose two filter bits are not // both set is absent, so most misses on small and medium maps return @@ -1301,36 +1463,69 @@ export function helperDefs(emitter: LlEmitter): string[] { // and the filter reads its bits 44..55, which are the capacity word's // own. A key without a cached hash, and every possible hit, take the // runtime lookup. - if (emitter.decls.has(STRING_MAP_ENTRY_DECL)) { + // A compact-value map's lookup (sc_map_entry_str_c) tests the same filter + // and calls scr_map_entry_str_c; its iteration step (sc_map_iter_entry_c) + // reads 16-byte entries and their 32-bit hash. + const stringMapEntry = (helper: string, runtime: string): string[] => [ + `define internal ptr @${helper}(ptr %m, ptr %k) ${FN_ATTRS} {`, + `entry:`, + ...(emitter.sizeType === "i64" + ? [ + ` %fp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 10`, + ` %filter = load i64, ptr %fp`, + ` %capp = getelementptr inbounds %ScrStr, ptr %k, i32 0, i32 2`, + ` %cap = load i64, ptr %capp`, + ` %cached = icmp uge i64 %cap, 4294967296`, + ` br i1 %cached, label %test, label %slow`, + `test:`, + ` %s1 = lshr i64 %cap, 44`, + ` %i1 = and i64 %s1, 63`, + ` %b1 = shl i64 1, %i1`, + ` %s2 = lshr i64 %cap, 50`, + ` %i2 = and i64 %s2, 63`, + ` %b2 = shl i64 1, %i2`, + ` %bits = or i64 %b1, %b2`, + ` %have = and i64 %filter, %bits`, + ` %maybe = icmp eq i64 %have, %bits`, + ` br i1 %maybe, label %slow, label %absent`, + `absent:`, + ` ret ptr null`, + ] + : [` br label %slow`]), + `slow:`, + ` %r = call ptr @${runtime}(ptr %m, ptr %k)`, + ` ret ptr %r`, + `}`, + ``, + ]; + if (emitter.decls.has(STRING_MAP_ENTRY_DECL)) + defs.push(...stringMapEntry("sc_map_entry_str", "scr_map_entry_str")); + if (emitter.decls.has(STRING_MAP_ENTRY_C_DECL)) { + const sz = emitter.sizeType; defs.push( - `define internal ptr @sc_map_entry_str(ptr %m, ptr %k) ${FN_ATTRS} {`, + ...stringMapEntry("sc_map_entry_str_c", "scr_map_entry_str_c"), + `define internal ptr @sc_map_iter_entry_c(ptr %m, double %i) ${FN_ATTRS} {`, `entry:`, - ...(emitter.sizeType === "i64" - ? [ - ` %fp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 10`, - ` %filter = load i64, ptr %fp`, - ` %capp = getelementptr inbounds %ScrStr, ptr %k, i32 0, i32 2`, - ` %cap = load i64, ptr %capp`, - ` %cached = icmp uge i64 %cap, 4294967296`, - ` br i1 %cached, label %test, label %slow`, - `test:`, - ` %s1 = lshr i64 %cap, 44`, - ` %i1 = and i64 %s1, 63`, - ` %b1 = shl i64 1, %i1`, - ` %s2 = lshr i64 %cap, 50`, - ` %i2 = and i64 %s2, 63`, - ` %b2 = shl i64 1, %i2`, - ` %bits = or i64 %b1, %b2`, - ` %have = and i64 %filter, %bits`, - ` %maybe = icmp eq i64 %have, %bits`, - ` br i1 %maybe, label %slow, label %absent`, - `absent:`, - ` ret ptr null`, - ] - : [` br label %slow`]), - `slow:`, - ` %r = call ptr @scr_map_entry_str(ptr %m, ptr %k)`, - ` ret ptr %r`, + ` %np = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 8`, + ` %n = load i32, ptr %np`, + ` %nf = uitofp i32 %n to double`, + ` %lo = fcmp oge double %i, 0.0`, + ` %hi = fcmp olt double %i, %nf`, + ` %in = and i1 %lo, %hi`, + ` br i1 %in, label %load, label %none`, + `load:`, + ` %ix = fptoui double %i to ${sz}`, + ` %ep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 11`, + ` %entries = load ptr, ptr %ep`, + ` %e = getelementptr inbounds %ScrMapCEntry, ptr %entries, ${sz} %ix`, + ` %hp = getelementptr inbounds %ScrMapCEntry, ptr %e, i32 0, i32 1`, + ` %h = load i32, ptr %hp`, + ` %live = icmp ne i32 %h, 0`, + ` br i1 %live, label %yes, label %none`, + `yes:`, + ` ret ptr %e`, + `none:`, + ` ret ptr null`, `}`, ``, ); diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index b57e11e29..74736cd1a 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -37,7 +37,7 @@ import { InternalCompilerError } from "../../errors.js"; import { emitDenseArrayGet, emitDenseArrayPop, emitDenseArraySet } from "./dense-array-access.js"; import { emitBorrowedInput } from "./borrowed-inputs.js"; import { f64Lit, llvmCommentText, loadArrCount, sizeToDouble } from "./common.js"; -import { RESULT_DST_ID, RESULT_ENTRY_PREFIX } from "./record-results.js"; +import { resultSlotStore } from "../result-slots.js"; import { matchIntegerArrayForLoop, matchIntegerBytesForLoop, @@ -51,7 +51,7 @@ import { emitCountedLoopSafety, emitPairedLoopSafety, } from "./counted-loops.js"; -import { emitBytesLoopVersionGuard } from "./bytes-loop-versions.js"; +import { emitBytesLoopVersionGuard, type BytesLoopVersion } from "./bytes-loop-versions.js"; import { mangleLocal } from "../mangle.js"; import { LlvmUnsupportedError } from "./unsupported.js"; import type { LlValue } from "./expr-context.js"; @@ -474,7 +474,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { B.startBlock(pop); const elem = s.arr.type.elem; const acc = elemAccess(elem); - const raw = emitDenseArrayPop(emitter, arr.name, acc, call, call); + const raw = emitDenseArrayPop(emitter, arr.name, acc, elem, call, call); if (acc === "ref" && isRefCounted(elem)) { const old = B.tmp(); B.line(`${old} = inttoptr i64 ${raw} to ptr`); @@ -574,13 +574,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { ? emitter.classFieldPtr(obj.name, s.className, s.field, "compressed") : emitter.recordFieldPtr(obj.name, s.shapeId, s.field); // A result-slot entry fills zeroed caller storage, each field once. - if ( - s.kind === "recordSet" && - s.obj.kind === "varRef" && - s.obj.localId === RESULT_DST_ID && - emitter.currentFunctionName.startsWith(RESULT_ENTRY_PREFIX) - ) - emitter.initField(ptr, type, inline, v); + if (resultSlotStore(emitter.currentFunctionName, s)) emitter.initField(ptr, type, inline, v); else emitter.replaceField(ptr, type, inline, v); if ( s.kind === "fieldSet" && @@ -787,7 +781,12 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { const trueLabel = B.newLabel("if.t"); const joinLabel = B.newLabel("if.j"); const falseLabel = s.else_ ? B.newLabel("if.f") : joinLabel; - B.condBr(cond, trueLabel, falseLabel); + // A running maximum or minimum is rarely updated; the weights keep + // the branch, where a select would make each element wait on the last. + const weights = emitter.runningExtrema.has(s) + ? `, !prof !{!"branch_weights", i32 1, i32 1000}` + : ""; + B.condBr(cond, trueLabel, falseLabel, weights); B.startBlock(trueLabel); emitter.emitBlock(s.then); B.br(joinLabel); @@ -805,6 +804,16 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { emitCallbackVersionedLoop(emitter, s, callbacks); break; } + const version = + LOOPS.of(emitter).bytesLoopVersionsEnabled && + LOOPS.of(emitter).bytesLoopVersioning.size < MAX_BYTES_LOOP_VERSION_DEPTH && + !LOOPS.of(emitter).bytesLoopVersioning.has(s) + ? emitter.matchBytesLoopVersion(s, null) + : null; + if (version) { + emitVersionedLoop(emitter, s, version); + break; + } const workerBudget = emitter.workerLoopBudget(s); const le = B.newLabel("loop.e"); emitter.emitLoopCopies(s, true, le, (exit) => { @@ -1002,35 +1011,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { : countedLoop; const version = versionable ? emitter.matchBytesLoopVersion(s, versionCounted) : null; if (version) { - const fast = B.newLabel("bytes.version.fast"), - slow = B.newLabel("bytes.version.slow"), - done = B.newLabel("bytes.version.done"); - emitBytesLoopVersionGuard(emitter, version, fast, slow); - const originalBounds = emitter.bytesBounds, - originalOwned = emitter.bytesOwned, - originalRanges = emitter.integerRanges; - LOOPS.of(emitter).bytesLoopVersioning.add(s); - try { - B.startBlock(slow); - emitter.emitStmt(s); - B.br(done); - B.startBlock(fast); - emitter.bytesBounds = new Set([...originalBounds, ...version.bounds]); - if (version.owned.size > 0) - emitter.bytesOwned = new Set([...originalOwned, ...version.owned]); - if (version.ranges.size > 0) - emitter.integerRanges = new Map([...originalRanges, ...version.ranges]); - if (version.counted?.guarded) LOOPS.of(emitter).provenCountedGuards.add(s); - emitter.emitStmt(s); - B.br(done); - } finally { - emitter.bytesBounds = originalBounds; - emitter.bytesOwned = originalOwned; - emitter.integerRanges = originalRanges; - LOOPS.of(emitter).provenCountedGuards.delete(s); - LOOPS.of(emitter).bytesLoopVersioning.delete(s); - } - B.startBlock(done); + emitVersionedLoop(emitter, s, version); break; } let countedJoin: string | null = null; @@ -2044,6 +2025,45 @@ const SPECULATION_BAILS = 8; * when speculative-integers.ts plans a fast integer copy. Both copies * share the loop's setup and exit; the ordinary copy comes first so the * fast copy is only emitted when every resume point exists. */ +/** A typed-array loop version: the guard, then the checked copy, which + * runs whenever the guard fails, and the copy whose accesses the guard + * proves in bounds (or thread-owned). */ +function emitVersionedLoop( + emitter: LlEmitter, + s: IrStmt & { kind: "for" | "while" }, + version: BytesLoopVersion, +): void { + const B = emitter.B; + const fast = B.newLabel("bytes.version.fast"), + slow = B.newLabel("bytes.version.slow"), + done = B.newLabel("bytes.version.done"); + emitBytesLoopVersionGuard(emitter, version, fast, slow); + const originalBounds = emitter.bytesBounds, + originalOwned = emitter.bytesOwned, + originalRanges = emitter.integerRanges; + LOOPS.of(emitter).bytesLoopVersioning.add(s); + try { + B.startBlock(slow); + emitter.emitStmt(s); + B.br(done); + B.startBlock(fast); + emitter.bytesBounds = new Set([...originalBounds, ...version.bounds]); + if (version.owned.size > 0) emitter.bytesOwned = new Set([...originalOwned, ...version.owned]); + if (version.ranges.size > 0) + emitter.integerRanges = new Map([...originalRanges, ...version.ranges]); + if (version.counted?.guarded) LOOPS.of(emitter).provenCountedGuards.add(s); + emitter.emitStmt(s); + B.br(done); + } finally { + emitter.bytesBounds = originalBounds; + emitter.bytesOwned = originalOwned; + emitter.integerRanges = originalRanges; + LOOPS.of(emitter).provenCountedGuards.delete(s); + LOOPS.of(emitter).bytesLoopVersioning.delete(s); + } + B.startBlock(done); +} + export function emitLoopCopies( emitter: LlEmitter, loop: Extract, @@ -2099,8 +2119,7 @@ export function emitLoopCopies( // count. After SPECULATION_BAILS bailing entries in a row, the site runs // the ordinary copy only. The count is a hint: both copies give the same // results, and a race between workers loses at most an update. - const bails = `@scr.spec.bails.${emitter.speculativeLoopSites++}`; - emitter.declare(`${bails} = internal global i8 0`); + const bails = emitter.speculativeLoopSite(); const count = B.tmp(), trying = B.tmp(); B.line(`${count} = load atomic i8, ptr ${bails} unordered, align 1`); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 911b03ed2..de7999dd0 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -70,7 +70,15 @@ import { import { scalarizeNumericRecords } from "../../ir/scalar-records.js"; import { addRecordResultEntries } from "./record-results.js"; import { specializeCallbackCalls } from "../../ir/callback-specialization.js"; +import { + type ChunkResult, + EmitJournal, + registerReplay, + renameChunk, + replayJournal, +} from "./emit-journal.js"; import { specializeNumericCalls } from "../../ir/numeric-call-specialization.js"; +import { versionLoopBoundParameters } from "../../ir/parameter-versions.js"; import { everyExpr, everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse.js"; import { INT32_RANGE, type IntegerRange, type IntegerRanges } from "../../ir/integer-ranges.js"; import { unshareNodes } from "../../ir/unshare.js"; @@ -174,6 +182,7 @@ import { emitDynamicExpr } from "./expr-dynamic.js"; import { emitIntrinsicExpr, emitSerializationExpr, emitAsyncExpr } from "./expr-async.js"; import { PublishWalkers } from "./publish.js"; import { computeImmutableFields, stableGlobals, type ImmutableFields } from "../immutability.js"; +import { computeCycleMutability, hierarchyUnitOfClass } from "../cycle-mutability.js"; import { computePublishedTypes, type PublishedTypes } from "../publication.js"; import { emitExpr } from "./expr-dispatch.js"; import { LOOPS } from "./loop-state.js"; @@ -392,9 +401,32 @@ export interface LlScopeEntry { lazy?: boolean; } +/** Process primitives for forked emission workers. */ +export interface ForkPrimitives { + /** 0 in the worker, the worker's pid in the parent, -1 on failure. */ + fork(): number; + /** Ends the worker without running exit handlers. */ + exit(code: number): void; + /** The worker's exit status, 128 plus a signal number, or -1. */ + wait(pid: number): number; +} + export interface LlvmTargetOptions { /** C ABI target; omitted direct emission uses the host ABI. */ targetTriple?: string; + /** Emit the function bodies in this many contiguous ranges, each by its + * own emitter, and merge them in function order (emit-journal.ts). The + * output is the same as one emitter's. Debug builds (`debugSources`) + * emit in one emitter. */ + functionChunks?: number; + /** Emit the function bodies of a large module on this many forked worker + * processes at a time (emit-module.ts), forked with `forkPrimitives`. The + * output is the same as one emitter's. Debug builds (`debugSources`) + * emit in one emitter. */ + functionWorkers?: number; + /** The host's process primitives, where it can fork (the native + * compiler's). */ + forkPrimitives?: ForkPrimitives | undefined; /** Exact frontend sources for dev-build locations and variable metadata. */ debugSources?: ReadonlyMap; /** Pointer width of the target C ABI. Native targets are 64-bit today. */ @@ -442,6 +474,14 @@ export function lowRegionTarget(platform: string, sanitize: boolean): boolean { return platform === "linux" && !sanitize; } +/** Function versions on loop-bounding number parameters + * (ir/parameter-versions.ts). Library builds have no int32 entries, and a + * module that observes call stacks could see the version's frame. */ +function versionParameters(mod: IrModule): IrModule { + if (mod.lib !== undefined || observesStacks(mod)) return mod; + return versionLoopBoundParameters(mod, mangleFunction); +} + export function emitLlvmModule(input: IrModule, options: LlvmTargetOptions = {}): string { // Facts are keyed by node identity: emit the same module whether it was // lowered in memory or read back from its serialized form. @@ -449,20 +489,22 @@ export function emitLlvmModule(input: IrModule, options: LlvmTargetOptions = {}) // Keep source storage intact for debugger inspection in dev builds. return new LlEmitter( options.debugSources === undefined - ? addRecordResultEntries( - scalarizeNumericRecords( - specializeCallbackCalls( - specializeNumericCalls( - foldConstantGlobals( - copyStableCaptures( - resolveSelfCaptures( - hoistFinalFieldReads(freezeInitOnlyGlobals(dropUnwrittenPropertyBags(mod))), + ? versionParameters( + addRecordResultEntries( + scalarizeNumericRecords( + specializeCallbackCalls( + specializeNumericCalls( + foldConstantGlobals( + copyStableCaptures( + resolveSelfCaptures( + hoistFinalFieldReads(freezeInitOnlyGlobals(dropUnwrittenPropertyBags(mod))), + ), ), ), + mangleFunction, ), mangleFunction, ), - mangleFunction, ), ), ) @@ -481,20 +523,22 @@ export function emitLlvmModuleSource( const mod = unshareNodes(input); const parts = new LlEmitter( options.debugSources === undefined - ? addRecordResultEntries( - scalarizeNumericRecords( - specializeCallbackCalls( - specializeNumericCalls( - foldConstantGlobals( - copyStableCaptures( - resolveSelfCaptures( - hoistFinalFieldReads(freezeInitOnlyGlobals(dropUnwrittenPropertyBags(mod))), + ? versionParameters( + addRecordResultEntries( + scalarizeNumericRecords( + specializeCallbackCalls( + specializeNumericCalls( + foldConstantGlobals( + copyStableCaptures( + resolveSelfCaptures( + hoistFinalFieldReads(freezeInitOnlyGlobals(dropUnwrittenPropertyBags(mod))), + ), ), ), + mangleFunction, ), mangleFunction, ), - mangleFunction, ), ), ) @@ -643,9 +687,11 @@ export class LlEmitter { * 32-bit references to compact class instances (LlClassMeta.compressed): * loadField/loadRef decode them and storeField/storeRef encode them. */ readonly compressedFieldPtrs = new Set(); - /** Whether a class has compressed fields: main reserves the region before - * compiled code runs (scr_plr_init). */ - usesCompactRefs = false; + /** Compact class instances live in the program-lifetime region below + * 32 GiB (LlvmTargetOptions.lowRegion, and the program has compact + * classes): main reserves it before compiled code runs (scr_plr_init), and + * fields and arrays of compactRefType hold 32-bit references. */ + compactRegion = false; /** Native class fields with different property names cannot occupy the * same slot of one object. Inherited views of the same property share a @@ -662,6 +708,7 @@ export class LlEmitter { tag = 2 + this.fieldAliasTags.size * 2; this.fieldAliasTags.set(field, tag); } + this.journal?.entry("tag", field, tag); this.fieldPointerTags.set(ptr, tag); } @@ -674,6 +721,7 @@ export class LlEmitter { tag = 2 + this.fieldAliasTags.size * 2; this.fieldAliasTags.set(category, tag); } + this.journal?.entry("tag", category, tag); return `, !tbaa !${tag}`; } @@ -784,6 +832,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. */ @@ -833,6 +885,9 @@ export class LlEmitter { readonly callLifetimes: CallLifetimes; readonly constantCallbacks: Map>; currentConstantCallbacks: ReadonlyMap = new Map(); + /** The current function's running maximum and minimum updates, whose + * branches are weighted as rarely taken (running-extrema.ts). */ + runningExtrema: ReadonlySet = new Set(); /** Callback bindings whose closures an enclosing callback loop version * proved to hold their known function: calls take the direct entry with * no fallback (emitCallbackVersionedLoop). */ @@ -1132,6 +1187,9 @@ export class LlEmitter { int32Elements: ((arr: IrExpr) => boolean) | null = null; /** Sums whose result only feeds 32-bit conversions (wrapping-sums.ts). */ wrappingSums = new Set(); + /** Number locals of the current function that hold exact integers or NaN + * (findIntegerOrNaNLocals in wrapping-sums.ts). */ + integerOrNaNLocals: ReadonlyMap = new Map(); bytesBounds: ReadonlySet = new Set(); /** Worker programs: typed-array accesses whose views an enclosing version * guard proved thread-owned. */ @@ -1203,13 +1261,29 @@ export class LlEmitter { readonly chainSlots = new Map(); logArgSlots = 0; readonly stackTraces: boolean; + /** The options this emitter was built with (chunk emitters reuse them). */ + readonly targetOptions: LlvmTargetOptions; + /** Records the module-level effects of emitting a range of functions, + * which the emitter that merges the range replays (emit-journal.ts). Null + * when this emitter emits the whole module itself. */ + journal: EmitJournal | null; + /** The emitter's hosts forward to `journal` (journal set, or workers). */ + private readonly journaled: boolean; /** Stack-trace mode: functions no capture can include (no frame push). */ readonly unframedFunctions: ReadonlySet; constructor( readonly mod: IrModule, options: LlvmTargetOptions, + journal: EmitJournal | null = null, ) { + this.targetOptions = options; + this.journal = journal; + this.journaled = + journal !== null || + ((options.functionWorkers ?? 1) > 1 && + options.forkPrimitives !== undefined && + options.debugSources === undefined); this.stackTraces = observesStacks(mod); this.nullableUnions = new NullableUnions(mod, options.debugSources === undefined); this.mockTimers = this.runtimeFeatures.mockTimers; @@ -1292,8 +1366,10 @@ export class LlEmitter { // and per async fiber; WASI, Windows and library builds have none. const triple = (options.targetTriple ?? "").toLowerCase(); const windows = triple === "" ? process.platform === "win32" : triple.includes("windows"); + this.publishedTypes = computePublishedTypes(mod); const mt = computeMayThrow(mod, { stackChecks: !this.wasi && mod.lib === undefined && !windows, + published: this.publishedTypes, }); this.mayThrow = mt.fns; this.workerEntryPolls = mt.workerEntryPolls ?? null; @@ -1302,11 +1378,12 @@ export class LlEmitter { // runtime moves deep prefix nests to a fresh stack using the guard, // which Windows lacks. this.stacklessAsyncAllowed = this.stacklessGenerators && mod.lib === undefined && !windows; - this.publishedTypes = computePublishedTypes(mod); this.unframedFunctions = this.stackTraces ? captureFreeFunctions(mod, this.stackChecks, this.publishedTypes) : new Set(); - this.immutableFields = computeImmutableFields(mod, dynamicStores); + // One field-mutability answer serves immutable fields and traced shapes. + const cycleMutability = computeCycleMutability(mod, hierarchyUnitOfClass(mod), dynamicStores); + this.immutableFields = computeImmutableFields(mod, dynamicStores, cycleMutability); this.constructorFunctions = new Set((mod.classes ?? []).map((c) => `%${c.name}.constructor`)); this.indirectMayThrow = mt.indirect || mod.workers === true; for (const cls of mod.classes ?? []) { @@ -1431,7 +1508,7 @@ export class LlEmitter { this.receiverReaders = new AmbientReceiverReaders(this.fnByName); this.stackCaptures = new StackCaptures(this.fnByName, this.callLifetimes, this.stackCallbacks); this.lazyCaptures = new LazyCaptures(this.fnByName); - const traced = computeTraced(mod, dynamicStores); + const traced = computeTraced(mod, dynamicStores, cycleMutability); this.tracedShapes = traced.shapes; this.tracedUnions = traced.unions; // A shape union's value is whichever record it holds, and the collector @@ -1456,6 +1533,22 @@ export class LlEmitter { } } this.shapeHost = { + // Journal hooks only on emitters that can emit ranges (chunk + // emitters, and emitters whose workers take a journal after the + // fork), so the host's optional calls cost nothing otherwise. + journalAllocated: !this.journaled + ? undefined + : (name, op, args) => this.journal?.allocated(name, op, args), + journalBuilt: !this.journaled ? undefined : () => this.journal?.built(), + journalOpened: !this.journaled ? undefined : (op) => this.journal?.opened(op), + journalUsed: !this.journaled + ? undefined + : (name, op, args) => this.journal?.used(name, op, args), + journalEntry: !this.journaled + ? undefined + : (table, key, value) => this.journal?.entry(table, key, value), + compactArrayElem: (elem) => this.compactArrayElem(elem), + compactRegion: () => this.compactRegion, sizeType: this.sizeType, cycleBufOffset: this.cycleBufOffset, tracedShapes: this.tracedShapes, @@ -1585,22 +1678,19 @@ export class LlEmitter { ? (cls, i, isCompact) => { const f = cls.fields[i]!; if (this.inlineUnions.classField(cls.name, f.name)) return false; - const t = f.type; - if (t.kind === "object") return isCompact(t.className); - if (t.kind !== "union") return false; - const single = this.nullableUnions.single(t.unionId); - if (single !== null) - return ( - single.nullTag < 0 && - single.arm.kind === "object" && - isCompact(single.arm.className) - ); - const nullable = this.nullableFields.get(cls.name, f.name); + if (this.compactRefType(f.type, isCompact)) return true; + const nullable = + f.type.kind === "union" && this.nullableUnions.single(f.type.unionId) === null + ? this.nullableFields.get(cls.name, f.name) + : null; return nullable !== null && isCompact(nullable.arm.className); } : undefined, ); - this.usesCompactRefs = [...this.classMeta.values()].some((m) => m.compressed.some(Boolean)); + this.compactRegion = + mod.lib === undefined && + options.lowRegion === true && + [...this.classMeta.values()].some((m) => m.compact); for (const [name, rec] of RUNTIME_ERROR_CLASSES) { const meta = this.classMeta.get(name); if (!meta) break; // hand-written IR without the builtin defs: no stamps @@ -2212,7 +2302,7 @@ export class LlEmitter { B.condBr(thrown, rejected, clean); B.startBlock(clean); if (acc === "ref") this.toHeap(value); - this.arrPush(values, acc, value.name); + this.arrPush(values, acc, value.name, elem); B.br(next); B.startBlock(rejected); const promise = B.tmp(); @@ -2328,6 +2418,7 @@ export class LlEmitter { }; this.literals.set(text, lit); } + this.journal?.entry("literal", text, lit); const name = `@${lit.sym}`; this.immortalValues.add(name); return name; @@ -2338,6 +2429,7 @@ export class LlEmitter { literalEquals(text: string): string { const literal = this.internLiteral(text); const name = `@sc_streq_${literal.slice("@sc_lit_".length)}`; + this.journal?.entry("literalEquality", text, name); if (!this.literalEqualities.has(text)) { this.literalEqualities.set(text, name); if (Buffer.byteLength(text, "utf8") > INLINE_LITERAL_BYTES) { @@ -2364,6 +2456,7 @@ export class LlEmitter { lit = { sym: this.contentSymbol("sc_bigint_", key), value }; this.bigintLiterals.set(key, lit); } + this.journal?.entry("bigint", key, { sym: lit.sym, value: lit.value.toString() }); const name = `@${lit.sym}`; this.immortalValues.add(name); return name; @@ -2377,9 +2470,20 @@ export class LlEmitter { c = { sym: this.contentSymbol("sc_cs_", text), len: Buffer.byteLength(text, "utf8") }; this.cstrs.set(text, c); } + this.journal?.entry("cstr", text, c); return `@${c.sym}`; } + /** The method-id slot for a constant method name (methodIdSlots). */ + methodIdSlot(name: string): string { + let sym = this.methodIdSlots.get(name); + if (!sym) { + sym = this.contentSymbol("sc_mid_", name); + this.methodIdSlots.set(name, sym); + } + return `@${sym}`; + } + needBadTag(): void { this.needsBadTag = true; } @@ -2393,6 +2497,7 @@ export class LlEmitter { sym = this.contentSymbol("sc_map_ops_", body); this.mapOpsConsts.set(body, sym); } + this.journal?.entry("mapOps", body, sym); return `@${sym}`; } @@ -2419,6 +2524,7 @@ export class LlEmitter { sym = `sc_unit_${this.unitInstances.size}`; this.unitInstances.set(key, sym); } + this.journal?.entry("unit", key, sym); const name = `@${sym}`; this.immortalValues.add(name); return name; @@ -2432,6 +2538,7 @@ export class LlEmitter { sym = `sc_retype_guard_${this.retypeGuards.size}`; this.retypeGuards.set(unionId, sym); } + this.journal?.entry("retype", unionId, sym); return `@${sym}`; } @@ -2453,6 +2560,7 @@ export class LlEmitter { sym = `sc_absent_${this.absentInstances.size}`; this.absentInstances.set(unionId, sym); } + this.journal?.entry("absent", unionId, sym); const name = `@${sym}`; this.immortalValues.add(name); return name; @@ -2500,6 +2608,7 @@ export class LlEmitter { } declare(decl: string): void { + this.journal?.entry("decl", decl, null); this.decls.add(decl); } @@ -3565,6 +3674,30 @@ export class LlEmitter { return { ptr: p, type, inline: this.inlineUnions.classField(className, field), compressed }; } + /** Whether values of `t` are NULL or a compact class instance: `C`, + * `C | undefined` or `C | null` for a compact class `C`. `C | null | + * undefined` is not: its null is a sentinel outside the region. */ + compactRefType( + t: IrType, + isCompact: (className: string) => boolean = (name) => + this.classMeta.get(name)?.compact === true, + ): boolean { + if (t.kind === "object") return isCompact(t.className); + if (t.kind !== "union") return false; + const single = this.nullableUnions.single(t.unionId); + return ( + single !== null && + single.nullTag < 0 && + single.arm.kind === "object" && + isCompact(single.arm.className) + ); + } + + /** Whether arrays of `elem` hold 32-bit references (SCR_ELEM_CREF). */ + compactArrayElem(elem: IrType): boolean { + return this.compactRegion && this.compactRefType(elem); + } + /** Loads the reference a field pointer addresses: a pointer, or the * decoded 32-bit reference of a compressed field (compressedFieldPtrs). * `tagged`: the load carries the field's alias tags. */ @@ -3620,6 +3753,7 @@ export class LlEmitter { const meta = this.classMetaOf(className); this.classObjs.set(className, { nameSym: this.internLiteral(classJsName(meta.def)) }); } + this.journal?.entry("classObj", className, this.classObjs.get(className)); return mangleClassObj(className); } @@ -3829,7 +3963,7 @@ export class LlEmitter { /** A binding's storage: a module global, a plain local slot, or a boxed * local (the slot holds the capture BOX; access goes through it). */ matchBytesLoopVersion( - s: IrStmt & { kind: "for" }, + s: IrStmt & { kind: "for" | "while" }, counted: IntegerCountedForLoop | null, ): ReturnType { const fn = LOOPS.of(this).bytesLoopFunction; @@ -4012,7 +4146,7 @@ export class LlEmitter { const acc = boxAccess(t); const cell = B.tmp(); B.line(`${cell} = ${arrNewCall(this.shapeHost, t, "1")} ; TDZ cell`); - this.arrPush(cell, acc === "bool" ? "bool" : "f64", value); + this.arrPush(cell, acc === "bool" ? "bool" : "f64", value, null); this.declare(`declare void @scr_box_set_ref(ptr, ptr)`); B.line(`call void @scr_box_set_ref(ptr ${box}, ptr ${cell})`); } @@ -4327,6 +4461,189 @@ export class LlEmitter { return emitFunction(this, fn); } + /** What this range emitter (chunkEmitter) hands the merging emitter, as + * plain data a worker process can write: its journal and the sets and + * flags its functions set. */ + chunkResult(): ChunkResult { + return { + entries: this.journal!.entries, + flags: { + usesCoroutines: this.usesCoroutines, + usesTimers: this.usesTimers, + usesGenSlots: this.usesGenSlots, + needsUnionNone: this.needsUnionNone, + needsNullableAbsent: this.needsNullableAbsent, + needsOom: this.needsOom, + needsBadTag: this.needsBadTag, + needsBadKey: this.needsBadKey, + needsRetainBox: this.needsRetainBox, + }, + publishedNamesSym: this.publishedNamesSym, + rcHelpers: this.rcHelpers === null ? [] : [...this.rcHelpers], + immortalValues: [...this.immortalValues], + fnValues: [...this.fnValues], + arraySearchKinds: [...this.arraySearchKinds], + recordCloneShapes: [...this.recordCloneShapes], + awaitCallees: [...this.awaitCallees], + adoptCallees: [...this.adoptCallees], + directAwaitCallees: [...this.directAwaitCallees], + inlineAwaitCallees: [...this.inlineAwaitCallees], + }; + } + + /** Merges a range emitter's result (chunkResult) into this emitter, in + * the range's order, and answers the rename of the range's text to this + * emitter's numbered helpers. Ranges merge in function order. */ + mergeChunk(chunk: ChunkResult): (text: string) => string { + const names = new Map(); + const tags = new Map(); + // The range's helpers and table entries first, in its order; then the + // sets and flags, which replayed definitions must find as serial + // emission left them (a helper set guards the declarations its first + // request adds). + replayJournal(this, chunk.entries, names, tags); + const flags = chunk.flags; + this.usesCoroutines ||= flags.usesCoroutines; + this.usesTimers ||= flags.usesTimers; + this.usesGenSlots ||= flags.usesGenSlots; + this.needsUnionNone ||= flags.needsUnionNone; + this.needsNullableAbsent ||= flags.needsNullableAbsent; + this.needsOom ||= flags.needsOom; + this.needsBadTag ||= flags.needsBadTag; + this.needsBadKey ||= flags.needsBadKey; + this.needsRetainBox ||= flags.needsRetainBox; + this.publishedNamesSym ??= chunk.publishedNamesSym; + if (this.rcHelpers !== null) for (const key of chunk.rcHelpers) this.rcHelpers.add(key); + for (const name of chunk.immortalValues) this.immortalValues.add(name); + for (const name of chunk.fnValues) this.fnValues.add(name); + for (const kind of chunk.arraySearchKinds) this.arraySearchKinds.add(kind as ArraySearchKind); + for (const shapeId of chunk.recordCloneShapes) this.recordCloneShapes.add(shapeId); + for (const name of chunk.awaitCallees) this.awaitCallees.add(name); + for (const name of chunk.adoptCallees) this.adoptCallees.add(name); + for (const name of chunk.directAwaitCallees) this.directAwaitCallees.add(name); + for (const name of chunk.inlineAwaitCallees) this.inlineAwaitCallees.add(name); + return renameChunk(names, tags); + } + + /** Adds a content-named constant a chunk interned. Its symbol is this + * emitter's own: a digest collision retries in first-use order, which a + * chunk alone does not know. */ + private addContentEntry( + into: Map, + key: string, + entry: V, + prefix: string, + names: Map, + ): void { + let own = into.get(key); + if (own === undefined) { + own = { ...entry, sym: this.contentSymbol(prefix, key) }; + into.set(key, own); + } + if (own.sym !== entry.sym) names.set(entry.sym, own.sym); + } + + /** Applies a chunk's table entry (EmitJournal.entry) at its place in the + * chunk's order: a key this emitter lacks is added, numbered after its + * own entries where the table numbers them, and the chunk's name for it + * is renamed to this emitter's. */ + applyJournalEntry( + table: string, + key: string, + value: unknown, + names: Map, + tags: Map, + ): void { + switch (table) { + case "decl": + this.decls.add(key); + return; + case "literal": + this.addContentEntry( + this.literals, + key, + value as { sym: string; len: number }, + "sc_lit_", + names, + ); + return; + case "literalEquality": + addEntry(this.literalEqualities, key, value as string); + return; + case "bigint": + { + const entry = value as { sym: string; value: string }; + this.addContentEntry( + this.bigintLiterals, + key, + { sym: entry.sym, value: BigInt(entry.value) }, + "sc_bigint_", + names, + ); + } + return; + case "cstr": + this.addContentEntry( + this.cstrs, + key, + value as { sym: string; len: number }, + "sc_cs_", + names, + ); + return; + case "mapOps": { + let own = this.mapOpsConsts.get(key); + if (own === undefined) { + own = this.contentSymbol("sc_map_ops_", key); + this.mapOpsConsts.set(key, own); + } + if (own !== value) names.set(value as string, own); + return; + } + case "classObj": + addEntry(this.classObjs, key, value as { nameSym: string }); + return; + case "retainWrapper": + addEntry(this.nullableUnions.retainWrappers, key, value as string); + return; + case "unit": + return addNumbered(this.unitInstances, key, value as string, "sc_unit_", names); + case "absent": + return addNumbered(this.absentInstances, key, value as string, "sc_absent_", names); + case "retype": + return addNumbered(this.retypeGuards, key, value as string, "sc_retype_guard_", names); + case "slots": + return addNumbered(this.threadSlotsBlocks, key, value as string, "@sc_slots_", names); + case "regex": + return addNumberedSymbol(this.regexInstances, key, value as never, "sc_re_", names); + case "templateStrings": + return addNumberedSymbol( + this.templateStringsInstances, + key, + value as never, + "sc_tsa_", + names, + ); + case "tag": { + let own = this.fieldAliasTags.get(key); + if (own === undefined) { + own = 2 + this.fieldAliasTags.size * 2; + this.fieldAliasTags.set(key, own); + } + if (own !== value) tags.set(value as number, own); + return; + } + default: + throw new InternalCompilerError(`llvm emitter bug: no journal table ${table}`); + } + } + + /** An emitter for a range of this module's functions: the same facts, + * empty module-level tables, and a journal of what the range adds. */ + chunkEmitter(): LlEmitter { + return new LlEmitter(this.mod, { ...this.targetOptions, functionChunks: 1 }, new EmitJournal()); + } + /** The double-ABI entry of a function with an i32 entry: every caller * that is not a direct call (closures, dispatch, adapters, the runtime) * enters here. A pending exception passes through to its caller. */ @@ -5409,6 +5726,7 @@ export class LlEmitter { sym = `@sc_slots_${this.threadSlotsBlocks.size}`; this.threadSlotsBlocks.set(name, sym); } + this.journal?.entry("slots", name, sym); return sym; } @@ -5558,6 +5876,20 @@ export class LlEmitter { if (this.publishWalkersCache !== null) return this.publishWalkersCache; const byPre = [...this.classMeta.values()].sort((a, b) => a.pre - b.pre); this.publishWalkersCache = new PublishWalkers({ + // Journal hooks only on emitters that can emit ranges (chunk + // emitters, and emitters whose workers take a journal after the + // fork), so the host's optional calls cost nothing otherwise. + journalAllocated: !this.journaled + ? undefined + : (name, op, args) => this.journal?.allocated(name, op, args), + journalBuilt: !this.journaled ? undefined : () => this.journal?.built(), + journalOpened: !this.journaled ? undefined : (op) => this.journal?.opened(op), + journalUsed: !this.journaled + ? undefined + : (name, op, args) => this.journal?.used(name, op, args), + journalEntry: !this.journaled + ? undefined + : (table, key, value) => this.journal?.entry(table, key, value), declare: (decl) => this.declare(decl), cstr: (text) => this.cstr(text), sizeType: this.sizeType, @@ -6044,12 +6376,27 @@ export class LlEmitter { } jsIcSite(): string { - return `@sc_js_ic_${this.jsIcSiteCount++}`; + const name = `@sc_js_ic_${this.jsIcSiteCount++}`; + this.journal?.site(name, "jsIcSite"); + return name; } /** A fresh read-site cache for one constant-key dyn read. */ dynReadIcSite(): string { - return `@sc_dyn_read_ic_${this.dynReadIcSites++}`; + const name = `@sc_dyn_read_ic_${this.dynReadIcSites++}`; + this.journal?.site(name, "dynReadIcSite"); + return name; + } + + /** A fresh speculative loop site: the global that counts its bailing + * entries in a row. */ + speculativeLoopSite(): string { + const name = `@sc_spec_bails_${this.speculativeLoopSites++}`; + this.journal?.site(name, "speculativeLoopSite"); + this.journal?.opened("speculativeLoopSite"); + this.declare(`${name} = internal global i8 0`); + this.journal?.built(); + return name; } dynKind(d: string): string { @@ -6162,8 +6509,8 @@ export class LlEmitter { return tagInSet(this, uName, unionId, tags); } - arrPush(arr: string, acc: "f64" | "bool" | "ref", value: string): string { - return arrPush(this, arr, acc, value); + arrPush(arr: string, acc: "f64" | "bool" | "ref", value: string, element: IrType | null): string { + return arrPush(this, arr, acc, value, element); } emitArrayCopyLoop(dst: string, src: string, acc: "f64" | "bool" | "ref"): void { @@ -6451,3 +6798,76 @@ export function adoptingAwaitOf(value: IrExpr | null): ExprOf<"awaitExpr"> | nul }); return found; } + +/** Adds `key` to `into` when it lacks it. */ +function addEntry(into: Map, key: string, value: V): void { + if (!into.has(key)) into.set(key, value); +} + +/** Adds `key` to a numbered table when it lacks it, numbering it after the + * table's own entries (`prefix` and its index), and records the rename of + * the chunk's `name` for it. */ +function addNumbered( + into: Map, + key: string, + name: string, + prefix: string, + names: Map, +): void { + let own = into.get(key); + if (own === undefined) { + own = `${prefix}${into.size}`; + into.set(key, own); + } + if (own !== name) names.set(name.replace(/^@/, ""), own.replace(/^@/, "")); +} + +/** addNumbered for entries that keep their numbered name in `sym`. */ +function addNumberedSymbol( + into: Map, + key: string, + entry: V, + prefix: string, + names: Map, +): void { + let own = into.get(key); + if (own === undefined) { + own = { ...entry, sym: `${prefix}${into.size}` }; + into.set(key, own); + } + if (own.sym !== entry.sym) names.set(entry.sym, own.sym); +} + +registerReplay("jsIcSite", (emitter) => emitter.jsIcSite()); +registerReplay("dynReadIcSite", (emitter) => emitter.dynReadIcSite()); +registerReplay("speculativeLoopSite", (emitter) => emitter.speculativeLoopSite()); +for (const op of [ + "dyn.collectionDynOps", + "dyn.dynMatchHelper", + "dyn.jsonSchema", + "dyn.recordNameTable", + "dyn.jsonParseHelper", + "dyn.dynCheckHelper", + "dyn.toDynHelper", + "dyn.promiseDynAdapterHelper", + "dyn.dynFuncThunkHelper", + "dyn.dynFuncBoxHelper", + "dyn.dynFuncAdapterHelper", + "dyn.dynToStrHelper", + "dyn.dynIterNHelper", + "dyn.dynDestrCheckHelper", + "dyn.dynKeyGetHelper", + "dyn.dynComputedKeyGetHelper", + "dyn.caughtToDynHelper", + "dyn.messageListenerHelper", + "dyn.messageReaderHelper", +]) + registerReplay(op, (emitter, args) => emitter.dyn.replayHelper(op, args)); +for (const op of [ + "walkers.jsonWriteHelper", + "walkers.recordFieldWriter", + "walkers.unionJoinHelper", +]) + registerReplay(op, (emitter, args) => emitter.walkers.replayHelper(op, args)); +for (const op of ["publish.walker", "publish.refusal"]) + registerReplay(op, (emitter, args) => emitter.publishWalkers.replayHelper(op, args)); diff --git a/packages/compiler/src/backend/llvm/expr-bytes.ts b/packages/compiler/src/backend/llvm/expr-bytes.ts index 062bfdfbe..c16ae856a 100644 --- a/packages/compiler/src/backend/llvm/expr-bytes.ts +++ b/packages/compiler/src/backend/llvm/expr-bytes.ts @@ -7,7 +7,7 @@ import { F64_INF, exactIndexLines, f64Lit, sizeToDouble } from "./common.js"; import { exactInteger, widenInteger } from "./integer-values.js"; import { byteNumberAccess } from "../../ir/byte-numbers.js"; import { emitByteNumber } from "./byte-numbers.js"; -import { integerOrNaNRange } from "./wrapping-sums.js"; +import { integerOrNaNIndex, integerOrNaNRange } from "./wrapping-sums.js"; import { bytesElementAlias, bytesHeaderAlias } from "./bytes-alias.js"; const BYTES_NUM_KIND: Record = { @@ -119,12 +119,28 @@ export function emitBytesIndex( B.line(`${narrow} = trunc i64 ${wideIndex} to ${host.sizeType}`); return narrow; } - const exact = exactIndexLines(index, len, host.sizeType, () => B.tmp()); - for (const line of exact.lines) B.line(line); + let ok: string; + let wide: string; + const nanIndex = integerOrNaNIndex(host, value, expr); + if (nanIndex) { + // An exact integer or NaN: in range when ordered and below the length. + const wideLength = host.sizeType === "i64" ? len : B.tmp(); + if (host.sizeType !== "i64") B.line(`${wideLength} = zext ${host.sizeType} ${len} to i64`); + const below = B.tmp(); + ok = B.tmp(); + B.line(`${below} = icmp ult i64 ${nanIndex.wide}, ${wideLength}`); + B.line(`${ok} = and i1 ${nanIndex.ordered}, ${below}`); + wide = nanIndex.wide; + } else { + const exact = exactIndexLines(index, len, host.sizeType, () => B.tmp()); + for (const line of exact.lines) B.line(line); + ok = exact.ok; + wide = exact.wide; + } const invalid = B.newLabel("bytes.index.invalid"); const valid = B.newLabel("bytes.index.valid"); - B.condBr(exact.ok, valid, invalid); - let idx = exact.wide; + B.condBr(ok, valid, invalid); + let idx = wide; B.startBlock(invalid); if (skipInvalid) B.br(skipInvalid); @@ -138,7 +154,7 @@ export function emitBytesIndex( B.startBlock(valid); if (host.sizeType !== "i64") { idx = B.tmp(); - B.line(`${idx} = trunc i64 ${exact.wide} to ${host.sizeType}`); + B.line(`${idx} = trunc i64 ${wide} to ${host.sizeType}`); } return idx; } @@ -455,11 +471,13 @@ export function emitToUint32( B.line(`${out} = select i1 ${ordered}, i32 ${low}, i32 0`); return out; } + // Below 2^63 in magnitude, the i64 conversion truncates exactly (every + // double above 2^53 is an integer), and its low 32 bits are ToUint32. const aboveMin = B.tmp(); const belowMax = B.tmp(); const fast = B.tmp(); - B.line(`${aboveMin} = fcmp oge double ${value}, ${f64Lit(-9007199254740992)}`); - B.line(`${belowMax} = fcmp ole double ${value}, ${f64Lit(9007199254740992)}`); + B.line(`${aboveMin} = fcmp oge double ${value}, ${f64Lit(-9223372036854775808)}`); + B.line(`${belowMax} = fcmp olt double ${value}, ${f64Lit(9223372036854775808)}`); B.line(`${fast} = and i1 ${aboveMin}, ${belowMax}`); const fastLabel = B.newLabel("uint32.coerce.fast"); @@ -475,8 +493,8 @@ export function emitToUint32( B.br(done); B.startBlock(slowLabel); - // Only NaN, the infinities and magnitudes above 2^53 reach this block. - // Such a finite double is the integer mantissa * 2^shift with shift >= 1, + // Only NaN, the infinities and magnitudes from 2^63 reach this block. + // Such a finite double is the integer mantissa * 2^shift with shift >= 11, // so its low 32 bits are the shifted mantissa's (none once shift >= 64), // negated for a negative value. NaN and the infinities have the maximum // exponent, a shift of 972, and convert to 0 like ToUint32. Integer diff --git a/packages/compiler/src/backend/llvm/expr-callbacks.ts b/packages/compiler/src/backend/llvm/expr-callbacks.ts index b91cdc543..398aebb21 100644 --- a/packages/compiler/src/backend/llvm/expr-callbacks.ts +++ b/packages/compiler/src/backend/llvm/expr-callbacks.ts @@ -14,6 +14,7 @@ import { LlvmUnsupportedError } from "./unsupported.js"; import type { LlvmEmitterContext } from "./expr-context.js"; import { unionPeekLines, unionTagLines } from "./union-repr.js"; import { inlineUnionCopyLines } from "./inline-unions.js"; +import { registerReplay } from "./emit-journal.js"; /** Text-template union construction around an owned (+1) reference payload: * a fresh box, or the payload itself for a nullable union. */ @@ -55,6 +56,7 @@ export function raceAdapterFor(host: LlvmEmitterContext, from: IrType, to: IrTyp if (sym) return sym; sym = `sc_race_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "raceAdapterFor", [from, to]); const toArms = to.kind === "union" ? (host.unionsById.get(to.unionId)?.arms ?? []) : []; if (from.kind === "void" && !toArms.some((a) => a.kind === "undefinedT")) { // A Promise entry (the frontend admits void entries without an @@ -70,6 +72,7 @@ export function raceAdapterFor(host: LlvmEmitterContext, from: IrType, to: IrTyp `}`, ``, ); + host.journal?.built(); return sym; } if (to.kind !== "union") @@ -98,6 +101,7 @@ export function raceAdapterFor(host: LlvmEmitterContext, from: IrType, to: IrTyp const tag = tagOf(UNDEFINED_T); d.push(` ${fulfill(host.unitInstanceRef(to.unionId, tag))}`, ` ret void`, `}`, ``); host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } if (from.kind !== "union") { @@ -138,6 +142,7 @@ export function raceAdapterFor(host: LlvmEmitterContext, from: IrType, to: IrTyp } d.push(` ${fulfill("%u")}`, ` ret void`, `}`, ``); host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } // Sub-union re-tag: switch over the entry's arms, rebuild under the @@ -200,6 +205,7 @@ export function raceAdapterFor(host: LlvmEmitterContext, from: IrType, to: IrTyp ``, ); host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } @@ -213,6 +219,7 @@ export function genResultThunkFor( if (sym) return sym; sym = mangleGenResThunk(host.resolveThunks.size); host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "genResultThunkFor", [genT, recT]); const shape = host.recordsById.get(recT.shapeId); const base = host.nullableUnions.fieldBase(recT.shapeId); const valueT = shape?.fields.find((f) => f.name === "value")?.type; @@ -299,6 +306,7 @@ export function genResultThunkFor( ``, ]; host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } if (valueT.kind !== "union") throw new LlvmUnsupportedError(`genResume:${valueT.kind}`); @@ -456,6 +464,7 @@ export function genResultThunkFor( ``, ); host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } @@ -467,6 +476,7 @@ export function childExitThunkFor(host: LlvmEmitterContext, param: IrType): stri if (sym) return sym; sym = `sc_cx_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "childExitThunkFor", [param]); const def = host.unionsById.get(param.unionId); const f64Tag = def ? def.arms.findIndex((a) => a.kind === "f64") : -1; const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1; @@ -494,6 +504,7 @@ export function childExitThunkFor(host: LlvmEmitterContext, param: IrType): stri `}`, ``, ); + host.journal?.built(); return sym; } @@ -510,6 +521,7 @@ export function childExitSignalThunkFor( if (sym) return sym; sym = `sc_cx_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "childExitSignalThunkFor", [codeParam, sigParam]); const codeDef = host.unionsById.get(codeParam.unionId); const f64Tag = codeDef ? codeDef.arms.findIndex((a) => a.kind === "f64") : -1; const codeNullTag = codeDef ? codeDef.arms.findIndex((a) => a.kind === "nullT") : -1; @@ -560,6 +572,7 @@ export function childExitSignalThunkFor( `}`, ``, ); + host.journal?.built(); return sym; } @@ -569,6 +582,7 @@ export function execFileThunkFor(host: LlvmEmitterContext, cbT: IrType & { kind: if (sym) return sym; sym = `sc_exec_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "execFileThunkFor", [cbT]); const errorParam = cbT.params[0]; let errorTag = -1; let nullTag = -1; @@ -627,6 +641,7 @@ export function execFileThunkFor(host: LlvmEmitterContext, cbT: IrType & { kind: ``, ); host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } @@ -639,6 +654,7 @@ export function ipcMessageThunkFor( if (sym) return sym; sym = `sc_ipc_msg_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "ipcMessageThunkFor", [cbT]); const param = cbT.params[0]; const d: string[] = [ `define internal void @${sym}(ptr %cb, ptr %message) ${FN_ATTRS} { ; IPC message ${typeKey(cbT)}`, @@ -679,6 +695,7 @@ export function ipcMessageThunkFor( } d.push(` ret void`, `}`, ``); host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } @@ -688,6 +705,7 @@ export function ipcSendThunkFor(host: LlvmEmitterContext, cbT: IrType & { kind: if (sym) return sym; sym = `sc_ipc_send_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "ipcSendThunkFor", [cbT]); const param = cbT.params[0]; const d: string[] = [ `define internal void @${sym}(ptr %cb, ptr %error) ${FN_ATTRS} { ; IPC send callback ${typeKey(cbT)}`, @@ -736,6 +754,7 @@ export function ipcSendThunkFor(host: LlvmEmitterContext, cbT: IrType & { kind: } d.push(` ret void`, `}`, ``); host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } @@ -747,6 +766,7 @@ export function childDataThunkFor(host: LlvmEmitterContext, param: IrType): stri if (sym) return sym; sym = `sc_cd_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "childDataThunkFor", [param]); const def = host.unionsById.get(param.unionId); const bytesTag = def ? def.arms.findIndex((a) => a.kind === "bytes" && a.elem === "u8") : -1; if (bytesTag < 0) @@ -768,6 +788,7 @@ export function childDataThunkFor(host: LlvmEmitterContext, param: IrType): stri `}`, ``, ); + host.journal?.built(); return sym; } @@ -780,6 +801,7 @@ export function childDataDynThunkFor(host: LlvmEmitterContext, chunk: "bytes" | if (sym) return sym; sym = `sc_cdd_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "childDataDynThunkFor", [chunk]); const box = chunk === "bytes" ? "scr_dyn_new_buffer" : "scr_dyn_new_str"; host.declare(`declare ptr @${box}(ptr)`); host.resolveThunkDefs.push( @@ -793,6 +815,7 @@ export function childDataDynThunkFor(host: LlvmEmitterContext, chunk: "bytes" | `}`, ``, ); + host.journal?.built(); return sym; } @@ -813,6 +836,7 @@ export function emitterFixedAdapter( if (sym) return { fn: sym, shim }; sym = `sc_ee_ad_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "emitterFixedAdapter", [cbT]); host.declare(`declare ptr @scr_box_get_ref(ptr)`); host.declare(`declare void @scr_closure_release(ptr)`); const params = cursor ? "ptr %args" : cbT.params.map((_, i) => `ptr %a${i}`).join(", "); @@ -857,6 +881,7 @@ export function emitterFixedAdapter( } d.push(` call void @scr_closure_release(ptr %orig)`, ` ret void`, `}`, ``); host.resolveThunkDefs.push(...d); + host.journal?.built(); return { fn: sym, shim }; } @@ -882,6 +907,7 @@ export function emitterOnceAdapter( if (sym) return { fn: sym, shim }; sym = `sc_ee_once_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "emitterOnceAdapter", [boxerT, fire]); host.declare(`declare ptr @scr_box_get_ref(ptr)`); host.declare(`declare void @scr_closure_release(ptr)`); host.declare(`declare void @${fire}(ptr, ptr)`); @@ -921,6 +947,7 @@ export function emitterOnceAdapter( ``, ); host.resolveThunkDefs.push(...d); + host.journal?.built(); return { fn: sym, shim }; } @@ -990,6 +1017,7 @@ export function closeBindThunkFor( if (sym) return sym; sym = `sc_ncb_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "closeBindThunkFor", [cbUnion, retServer]); const def = host.unionsById.get(cbUnion.unionId); const funcTag = def ? def.arms.findIndex((a) => a.kind === "func") : -1; const funcArm = funcTag >= 0 ? (def!.arms[funcTag] as IrType & { kind: "func" }) : null; @@ -1079,6 +1107,7 @@ export function closeBindThunkFor( } d.push(`}`, ``); host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } @@ -1094,6 +1123,7 @@ export function closeOverrideWrapFor( if (sym) return sym; sym = `sc_ncw_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "closeOverrideWrapFor", [cbUnion, retServer]); const def = host.unionsById.get(cbUnion.unionId); const undefTag = def ? def.arms.findIndex((a) => a.kind === "undefinedT") : -1; if (undefTag < 0) @@ -1122,6 +1152,7 @@ export function closeOverrideWrapFor( `}`, ``, ); + host.journal?.built(); return sym; } @@ -1140,6 +1171,7 @@ export function settledStoreFor( if (sym) return sym; sym = `sc_settled_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "settledStoreFor", [from, to]); const def = host.unionsById.get(to.unionId); if (!def) throw new InternalCompilerError("llvm emitter bug: settled store to an unknown union"); const armOf = (field: string): { tag: number; arm: IrType & { kind: "record" } } => { @@ -1230,5 +1262,52 @@ export function settledStoreFor( `}`, ``, ); + host.journal?.built(); return sym; } + +registerReplay("raceAdapterFor", (host, args) => + raceAdapterFor(host, args[0] as IrType, args[1] as IrType), +); +registerReplay("genResultThunkFor", (host, args) => + genResultThunkFor( + host, + args[0] as IrType & { kind: "generator" }, + args[1] as IrType & { kind: "record" }, + ), +); +registerReplay("childExitThunkFor", (host, args) => childExitThunkFor(host, args[0] as IrType)); +registerReplay("childExitSignalThunkFor", (host, args) => + childExitSignalThunkFor(host, args[0] as IrType, args[1] as IrType), +); +registerReplay("execFileThunkFor", (host, args) => + execFileThunkFor(host, args[0] as IrType & { kind: "func" }), +); +registerReplay("ipcMessageThunkFor", (host, args) => + ipcMessageThunkFor(host, args[0] as IrType & { kind: "func" }), +); +registerReplay("ipcSendThunkFor", (host, args) => + ipcSendThunkFor(host, args[0] as IrType & { kind: "func" }), +); +registerReplay("childDataThunkFor", (host, args) => childDataThunkFor(host, args[0] as IrType)); +registerReplay("childDataDynThunkFor", (host, args) => + childDataDynThunkFor(host, args[0] as "bytes" | "str"), +); +registerReplay("closeBindThunkFor", (host, args) => + closeBindThunkFor(host, args[0] as IrType, args[1] as boolean), +); +registerReplay("closeOverrideWrapFor", (host, args) => + closeOverrideWrapFor(host, args[0] as IrType, args[1] as boolean), +); +registerReplay("settledStoreFor", (host, args) => + settledStoreFor(host, args[0] as IrType, args[1] as IrType & { kind: "union" }), +); +registerReplay( + "emitterFixedAdapter", + (host, args) => emitterFixedAdapter(host, args[0] as IrType & { kind: "func" }).fn, +); +registerReplay( + "emitterOnceAdapter", + (host, args) => + emitterOnceAdapter(host, args[0] as IrType & { kind: "func" }, args[1] as string).fn, +); diff --git a/packages/compiler/src/backend/llvm/expr-calls.ts b/packages/compiler/src/backend/llvm/expr-calls.ts index 4bcf2a3d5..a4e7cde2c 100644 --- a/packages/compiler/src/backend/llvm/expr-calls.ts +++ b/packages/compiler/src/backend/llvm/expr-calls.ts @@ -11,6 +11,7 @@ import { type IrExpr, type IrFunction, classValueConstructor, + type IrType, } from "../../ir/ir.js"; import { liveRefView } from "./expr-stream-bridges.js"; import { typedRefConstructor, vAdapters } from "./shapes.js"; @@ -55,6 +56,7 @@ import { import type { Writes } from "./reference-writes.js"; import { emitJsIcCall, jsIcSiteOf } from "./js-ic.js"; import { emitClassMembership, emitDowncastGuard } from "./narrow-guards.js"; +import { registerReplay } from "./emit-journal.js"; export function emitCallExpr( host: LlvmEmitterContext, @@ -815,22 +817,7 @@ export function emitCallExpr( const pushReceiver = (): void => { if (lazyReceiver) { const t = lazyReceiver.type; - const key = typeKey(t); - let recipe = host.typedRefRecipes.get(key); - if (recipe === undefined) { - const adapter = host.liveDynRefAdapter(t); - const rc = vAdapters(host.shapeHost, t); - const ctor = typedRefConstructor(host.shapeHost, t); - const kind = ctor.endsWith("_traced") ? 2 : ctor.endsWith("_observed") ? 1 : 0; - // The box binds the same live view eager boxing attaches, so - // `in`, for-in and Object.keys see the class members. - const view = liveRefView(adapter); - recipe = `@sc_trr_${host.typedRefRecipes.size}`; - host.typedRefRecipes.set(key, recipe); - host.resolveThunkDefs.push( - `${recipe} = internal constant { ptr, ptr, ptr, ${host.sizeType}, ptr, ptr, i32, i32, ptr } { ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit}, i32 ${kind}, i32 ${view?.code ?? 0}, ptr ${view?.ops ?? "null"} }`, - ); - } + const recipe = typedRefRecipe(host, t); host.declare("declare void @scr_dyn_this_push_typed(ptr, ptr)"); B.line(`call void @scr_dyn_this_push_typed(ptr ${lazyReceiver.name}, ptr ${recipe})`); return; @@ -1493,3 +1480,28 @@ function emitNumericIndexRead( B.line(`${result} = phi ptr [ ${fast}, %${fastLabel} ], [ ${read.name}, %${slowLabel} ]`); return host.own({ name: result, type: e.type }); } + +/** The recipe a lazily boxed receiver of type `t` is built from: its RC + * adapters, type key and live view, one per type. */ +function typedRefRecipe(host: LlvmEmitterContext, t: IrType): string { + const key = typeKey(t); + let recipe = host.typedRefRecipes.get(key); + if (recipe !== undefined) return recipe; + const adapter = host.liveDynRefAdapter(t); + const rc = vAdapters(host.shapeHost, t); + const ctor = typedRefConstructor(host.shapeHost, t); + const kind = ctor.endsWith("_traced") ? 2 : ctor.endsWith("_observed") ? 1 : 0; + // The box binds the same live view eager boxing attaches, so `in`, + // for-in and Object.keys see the class members. + const view = liveRefView(adapter); + recipe = `@sc_trr_${host.typedRefRecipes.size}`; + host.typedRefRecipes.set(key, recipe); + host.journal?.allocated(recipe, "typedRefRecipe", [t]); + host.resolveThunkDefs.push( + `${recipe} = internal constant { ptr, ptr, ptr, ${host.sizeType}, ptr, ptr, i32, i32, ptr } { ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit}, i32 ${kind}, i32 ${view?.code ?? 0}, ptr ${view?.ops ?? "null"} }`, + ); + host.journal?.built(); + return recipe; +} + +registerReplay("typedRefRecipe", (host, args) => typedRefRecipe(host, args[0] as IrType)); diff --git a/packages/compiler/src/backend/llvm/expr-containers.ts b/packages/compiler/src/backend/llvm/expr-containers.ts index 8970aa3be..f870d513c 100644 --- a/packages/compiler/src/backend/llvm/expr-containers.ts +++ b/packages/compiler/src/backend/llvm/expr-containers.ts @@ -31,8 +31,10 @@ import { emitMapIterCount, emitMapIterEntry, emitMapIterSlot, + compactMapValues, mapEntryLayout, emitMapIterStart, + emitMapSize, emitNumberMapGetRef, emitNumberMapGetScalar, emitNumberMapHas, @@ -58,6 +60,7 @@ import { emitDenseArrayPush, } from "./dense-array-access.js"; import { emitUnionWiden } from "./expr-records.js"; +import { registerReplay } from "./emit-journal.js"; export function resolveThunkFor(host: LlvmEmitterContext, inner: IrType): string { const key = typeKey(inner); @@ -65,6 +68,7 @@ export function resolveThunkFor(host: LlvmEmitterContext, inner: IrType): string if (!sym) { sym = mangleResolveThunk(host.resolveThunks.size); host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "resolveThunkFor", [inner]); const v = vAdapters(host.shapeHost, inner); host.declare(`declare void @scr_resolve_ref_impl(ptr, ptr, ptr, ptr, ptr)`); host.resolveThunkDefs.push( @@ -75,6 +79,7 @@ export function resolveThunkFor(host: LlvmEmitterContext, inner: IrType): string `}`, ``, ); + host.journal?.built(); } return sym; } @@ -102,13 +107,17 @@ export function tagInSet( return acc; } +/** `element`: the array's element type, or null for arrays whose + * elements are not compact class references (dense-array-access.ts + * slotKind). */ export function arrPush( host: LlvmEmitterContext, arr: string, acc: "f64" | "bool" | "ref", value: string, + element: IrType | null, ): string { - return emitDenseArrayPush(host, arr, acc, value); + return emitDenseArrayPush(host, arr, acc, element, value); } /** Move evaluated values through one bounded slot buffer per call site. */ @@ -116,6 +125,7 @@ export function emitArrayValues( host: LlvmEmitterContext, arr: string, acc: "f64" | "bool" | "ref", + element: IrType, values: LlValue[], prepend = false, sharedBuffer?: string, @@ -129,7 +139,7 @@ export function emitArrayValues( return result; } if (values.length === 1) { - if (!prepend) return arrPush(host, arr, acc, values[0]!.name); + if (!prepend) return arrPush(host, arr, acc, values[0]!.name, element); const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr"; host.declare( `declare double @scr_arr_unshift_${acc}(ptr, ${ty}${acc === "bool" ? " zeroext" : ""})`, @@ -628,7 +638,7 @@ export function emitArrIntrinsic( // no-op zero-argument call. const vs = e.args.map((a) => host.emitExpr(a)); host.emitPublishedArrayGuard(r.name, e.receiver.type, 1, f64Lit(0), f64Lit(vs.length)); - return { name: emitArrayValues(host, r.name, acc, vs), type: e.type }; + return { name: emitArrayValues(host, r.name, acc, elem, vs), type: e.type }; } case "pushSpread": { // `a.push(...src)`: append src's elements in order (borrowed src, @@ -660,7 +670,7 @@ export function emitArrIntrinsic( // from right to left so the final front order is source order. const vs = e.args.map((a) => host.emitExpr(a)); host.emitPublishedArrayGuard(r.name, e.receiver.type, 4, f64Lit(0), f64Lit(vs.length)); - return { name: emitArrayValues(host, r.name, acc, vs, true), type: e.type }; + return { name: emitArrayValues(host, r.name, acc, elem, vs, true), type: e.type }; } case "unshiftSpread": { // The runtime snapshots the borrowed source and handles self-spread. @@ -702,7 +712,7 @@ export function emitArrIntrinsic( let slowLabel = ""; if (method === "pop") { slowLabel = B.newLabel("remove.slow"); - denseRaw = emitDenseArrayPop(host, r.name, acc, la, slowLabel); + denseRaw = emitDenseArrayPop(host, r.name, acc, elem, la, slowLabel); denseLabel = B.currentLabel(); B.br(lp); B.startBlock(slowLabel); @@ -771,7 +781,7 @@ export function emitArrIntrinsic( // by content, everything else by pointer). Strict equality. Arrays of // objects and strings scan inline (array-search.ts). const v = host.emitExpr(e.args[0]!); - const search = acc === "ref" ? arraySearchKind(elem) : null; + const search = acc === "ref" ? arraySearchKind(elem, host.compactArrayElem(elem)) : null; if (search !== null) { host.arraySearchKinds.add(search); const t = B.tmp(); @@ -789,7 +799,7 @@ export function emitArrIntrinsic( // Borrowed needle, SameValueZero (NaN matches NaN). For the inline // searches SameValueZero is strict equality. const v = host.emitExpr(e.args[0]!); - const search = acc === "ref" ? arraySearchKind(elem) : null; + const search = acc === "ref" ? arraySearchKind(elem, host.compactArrayElem(elem)) : null; if (search !== null) { host.arraySearchKinds.add(search); const at = B.tmp(), @@ -1060,8 +1070,10 @@ function newCollection(host: LlvmEmitterContext, key: IrType, value: IrType): st const kind = mapKeyKindNum(key, arms); const m = B.tmp(); host.declare(`declare ptr @scr_map_new(i32, i32, ptr)`); + // SCR_MAP_VAL_CREF (3): string keys and compact values (compactMapValues). + const valKind = compactMapValues(host, key, value) ? 3 : mapValKindNum(value); B.line( - `${m} = call ptr @scr_map_new(i32 ${kind}, i32 ${mapValKindNum(value)}, ptr ${collectionOps(host, key, value)})`, + `${m} = call ptr @scr_map_new(i32 ${kind}, i32 ${valKind}, ptr ${collectionOps(host, key, value)})`, ); return m; } @@ -1124,6 +1136,9 @@ export function emitMapLikeIntrinsic( const key = receiverType.kind === "map" ? receiverType.key : receiverType.elem; const kAcc = mapKeyAccess(key); const kTy = mapKeyParamType(kAcc); + // The map keeps compact values in 16-byte entries (SCR_MAP_VAL_CREF). + const compact = receiverType.kind === "map" && compactMapValues(host, key, receiverType.value); + const layout = mapEntryLayout(key, compact); const method = e.method; // A published Map or Set rejects mutation (the guard precedes the // operands, which each method evaluates itself). @@ -1287,7 +1302,7 @@ export function emitMapLikeIntrinsic( if (numberKey !== null) { raw = emitNumberMapGetRef(host, r.name, numberKey, value); } else if (stringKey !== null) { - raw = emitStringMapGetRef(host, r.name, stringKey, value); + raw = emitStringMapGetRef(host, r.name, stringKey, value, compact); } else { host.declare(`declare ptr @scr_map_get_${kAcc}_ref(ptr, ${kTy})`); raw = B.tmp(); @@ -1365,7 +1380,7 @@ export function emitMapLikeIntrinsic( return host.wrapNullable(raw, raw, value, valueTag, e.type, undefTag); } if (stringKey !== null && isRefCounted(value)) { - const raw = emitStringMapGetRef(host, r.name, stringKey, value); + const raw = emitStringMapGetRef(host, r.name, stringKey, value, compact); return host.wrapNullable(raw, raw, value, valueTag, e.type, undefTag); } host.declare(`declare ptr @scr_map_get_${kAcc}_ref(ptr, ${kTy})`); @@ -1413,7 +1428,8 @@ export function emitMapLikeIntrinsic( case "has": { const k = borrowInputs ? host.emitReadReceiver(e.args[0]!) : host.emitExpr(e.args[0]!); if (kAcc === "f64") return { name: emitNumberMapHas(host, r.name, k.name), type: e.type }; - if (kAcc === "str") return { name: emitStringMapHas(host, r.name, k.name), type: e.type }; + if (kAcc === "str") + return { name: emitStringMapHas(host, r.name, k.name, compact), type: e.type }; host.declare(`declare zeroext i1 @scr_map_has_${kAcc}(ptr, ${kTy})`); const t = B.tmp(); B.line(`${t} = call zeroext i1 @scr_map_has_${kAcc}(ptr ${r.name}, ${kTy} ${k.name})`); @@ -1426,12 +1442,8 @@ export function emitMapLikeIntrinsic( B.line(`${t} = call zeroext i1 @scr_map_delete_${kAcc}(ptr ${r.name}, ${kTy} ${k.name})`); return { name: t, type: e.type }; } - case "size": { - host.declare(`declare double @scr_map_size(ptr)`); - const t = B.tmp(); - B.line(`${t} = call double @scr_map_size(ptr ${r.name})`); - return { name: t, type: e.type }; - } + case "size": + return { name: emitMapSize(host, r.name), type: e.type }; case "clear": host.declare(`declare void @scr_map_clear(ptr)`); B.line(`call void @scr_map_clear(ptr ${r.name})`); @@ -1442,7 +1454,7 @@ export function emitMapLikeIntrinsic( return { name: emitMapIterStart(host, r.name), type: e.type }; case "iterLive": { const i = host.emitExpr(e.args[0]!); - const entry = emitMapIterEntry(host, r.name, i.name, mapEntryLayout(key)); + const entry = emitMapIterEntry(host, r.name, i.name, layout); const t = B.tmp(); B.line(`${t} = icmp ne ptr ${entry}, null`); return { name: t, type: e.type }; @@ -1461,7 +1473,7 @@ export function emitMapLikeIntrinsic( kAcc === "str" ? "ref" : kAcc, key, { decl: `declare ${retTy} @${symbol}(ptr, double)`, symbol }, - mapEntryLayout(key), + layout, ); return host.own({ name: t, type: e.type }); } @@ -1488,7 +1500,7 @@ export function emitMapLikeIntrinsic( decl: `declare ${vAcc === "bool" ? "zeroext i1" : retTy} @${symbol}(ptr, double)`, symbol, }, - mapEntryLayout(key), + layout, ); return host.own({ name: t, type: e.type }); } @@ -1539,3 +1551,5 @@ export function emitSetNew(host: LlvmEmitterContext, e: IrExpr & { kind: "setNew } return out; } + +registerReplay("resolveThunkFor", (host, args) => resolveThunkFor(host, args[0] as IrType)); diff --git a/packages/compiler/src/backend/llvm/expr-dynamic.ts b/packages/compiler/src/backend/llvm/expr-dynamic.ts index 58bb90569..434435eae 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(); @@ -1296,9 +1298,7 @@ function emitUnionEqWrappedScalar( B.startBlock(same); const eq = B.tmp(); if (arm.kind === "f64") { - host.declare(`declare double @scr_union_get_f64(ptr)`); - const v = B.tmp(); - B.line(`${v} = call double @scr_union_get_f64(ptr ${union.name})`); + const v = host.unionGetF64(union.name); if (e.sameValue) { host.declare(`declare zeroext i1 @scr_num_same_value(double, double)`); B.line(`${eq} = call zeroext i1 @scr_num_same_value(double ${v}, double ${plain.name})`); @@ -1306,9 +1306,7 @@ function emitUnionEqWrappedScalar( B.line(`${eq} = fcmp oeq double ${v}, ${plain.name}`); } } else { - host.declare(`declare zeroext i1 @scr_union_get_bool(ptr)`); - const v = B.tmp(); - B.line(`${v} = call zeroext i1 @scr_union_get_bool(ptr ${union.name})`); + const v = host.unionGetBool(union.name); B.line(`${eq} = icmp eq i1 ${v}, ${plain.name}`); } B.line(`store i1 ${eq}, ptr ${slot}`); diff --git a/packages/compiler/src/backend/llvm/expr-primitives.test.ts b/packages/compiler/src/backend/llvm/expr-primitives.test.ts index 41ecd189e..6eb9be8f7 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.test.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.test.ts @@ -200,6 +200,10 @@ test("identity bitwise forms over a proven int32 return the operand's number", ( } // Unproven operands keep the full ToInt32; other operators still compute. expect(body("unknown_or")).toContain("uint32.coerce.slow"); + // The i64 conversion covers every magnitude below 2^63; only NaN, the + // infinities and larger magnitudes take the slow block. + expect(body("unknown_or")).toMatch(/ = fcmp oge double %\S+, 0xC3E0000000000000$/m); + expect(body("unknown_or")).toMatch(/ = fcmp olt double %\S+, 0x43E0000000000000$/m); expect(body("length_and")).toMatch(/ = and i32 .*, 255$/m); }); diff --git a/packages/compiler/src/backend/llvm/expr-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts index 354004e9e..02ffd3c82 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.ts @@ -32,7 +32,7 @@ import { emitPowerOfTwoRemainder, emitSignedIntegerRemainder, } from "./integer-remainder.js"; -import { emitDenseArrayGet, emitDenseArrayState, slotWidth } from "./dense-array-access.js"; +import { denseSlotWidth, emitDenseArrayGet, emitDenseArrayState } from "./dense-array-access.js"; import { emitWrappingSum, markWrappingOperands } from "./wrapping-sums.js"; export function emitLiteralExpr( @@ -563,6 +563,7 @@ export function regexLiteralTemplate( }; host.regexInstances.set(key, re); } + host.journal?.entry("regex", key, re); return `@${re.sym}`; } @@ -823,6 +824,7 @@ export function emitStringExpr( }; host.templateStringsInstances.set(e.key, inst); } + host.journal?.entry("templateStrings", e.key, inst); return host.own({ name: host.retainFrame(`@${inst.sym}`, e.type), type: e.type }); } case "regexIntrinsic": @@ -879,7 +881,7 @@ export function emitContainerExpr( buffer = B.tmp(); B.entryAllocas.push(`${buffer} = alloca [${Math.min(e.elems.length, 64)} x i64]`); } - if (pending.length > 0) emitArrayValues(host, arr, acc, pending, false, buffer); + if (pending.length > 0) emitArrayValues(host, arr, acc, elem, pending, false, buffer); pending = []; }; e.elems.forEach((el, i) => { @@ -924,7 +926,7 @@ export function emitContainerExpr( } const bound = B.tmp(); B.line(`${bound} = fsub double ${n.name}, ${f64Lit(1)}`); - B.countedLoop(bound, () => host.arrPush(arr, acc, fill), "ole"); + B.countedLoop(bound, () => host.arrPush(arr, acc, fill, elem), "ole"); return out; } case "arrayGet": { @@ -962,7 +964,7 @@ export function emitContainerExpr( idx, e.index, "has", - slotWidth(elemAccess(e.arr.type.elem)), + denseSlotWidth(host, e.arr.type.elem), ); return { name: t, type: e.type }; } @@ -977,7 +979,7 @@ export function emitContainerExpr( idx, e.index, "state", - slotWidth(elemAccess(e.arr.type.elem)), + denseSlotWidth(host, e.arr.type.elem), ); return { name: t, type: e.type }; } diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index a0a0cf24a..7cb0fa772 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -44,6 +44,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 { registerReplay } from "./emit-journal.js"; /** An owned (+1) union value for a nullable-pointer class field slot `p` * (the unit arm's immortal instance, or a fresh box around a retained @@ -91,6 +92,7 @@ export function dynPromiseAdapter(host: LlvmEmitterContext, inner: IrType): stri if (existing) return existing; const sym = `sc_dpa_${host.dynPromiseAdapters.size}`; host.dynPromiseAdapters.set(key, sym); + host.journal?.allocated(sym, "dynPromiseAdapter", [inner]); host.declare(`declare ptr @scr_promise_payload_ref(ptr)`); host.declare(`declare void @scr_dyn_release_v(ptr)`); host.declare(`declare void @scr_promise_reject_pending(ptr)`); @@ -146,6 +148,7 @@ export function dynPromiseAdapter(host: LlvmEmitterContext, inner: IrType): stri `}`, ``, ); + host.journal?.built(); return sym; } @@ -602,6 +605,7 @@ export function liveDynUnionRefAdapter( const sym = `sc_ldu_${host.liveDynUnionRefAdapters.size}`; host.liveDynUnionRefAdapters.set(key, sym); + host.journal?.allocated(sym, "liveDynUnionRefAdapter", [t]); const adapters = new Map(); for (const { arm, tag } of mutableArms) { @@ -684,6 +688,7 @@ export function liveDynUnionRefAdapter( `}`, ``, ); + host.journal?.built(); return sym; } @@ -732,6 +737,11 @@ export function streamTypedRefMaterializeAdapter( const snapshot = `sc_ldr_${host.liveDynRefAdapters.size}_materialize`; const adapter: LlStreamTypedRefAdapter = { snapshot, commit: "null" }; host.liveDynRefAdapters.set(key, adapter); + host.journal?.allocated( + snapshot.replace(/_materialize$/, ""), + "streamTypedRefMaterializeAdapter", + [t], + ); if (isDynTypedRefType(t)) { const fields = host.classMeta.get(t.className)?.def.fields; if ( @@ -744,6 +754,7 @@ export function streamTypedRefMaterializeAdapter( ) { host.declare(`declare ptr @scr_dyn_class_view_unavailable(ptr)`); adapter.snapshot = "scr_dyn_class_view_unavailable"; + host.journal?.built(); return adapter; } } @@ -1185,6 +1196,7 @@ export function streamTypedRefMaterializeAdapter( `}`, ``, ); + host.journal?.built(); return adapter; } @@ -1469,6 +1481,7 @@ export function streamFromArrayAdapter( if (existing) return existing; const sym = `sc_sfa_${host.streamFromArrayAdapters.size}`; host.streamFromArrayAdapters.set(key, sym); + host.journal?.allocated(sym, "streamFromArrayAdapter", [t]); const B = new BlockBuilder(); const unionDef = elem.kind === "union" ? host.unionsById.get(elem.unionId) : undefined; if (elem.kind === "union" && !unionDef) { @@ -1585,5 +1598,17 @@ export function streamFromArrayAdapter( `}`, ``, ); + host.journal?.built(); return sym; } + +registerReplay("dynPromiseAdapter", (host, args) => dynPromiseAdapter(host, args[0] as IrType)); +registerReplay("liveDynUnionRefAdapter", (host, args) => + liveDynUnionRefAdapter(host, args[0] as IrType & { kind: "union" }), +); +registerReplay("streamTypedRefMaterializeAdapter", (host, args) => + streamTypedRefMaterializeAdapter(host, args[0] as IrType).snapshot.replace(/_materialize$/, ""), +); +registerReplay("streamFromArrayAdapter", (host, args) => + streamFromArrayAdapter(host, args[0] as IrType & { kind: "array" }), +); diff --git a/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts b/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts index 0570d0ea7..42c803697 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts @@ -4,6 +4,7 @@ import { type IrType, isRefCounted, typeKey } from "../../ir/ir.js"; import { FN_ATTRS, releaseSym, retainSym, traceArg } from "./shapes.js"; import type { LlvmEmitterContext } from "./expr-context.js"; import { unionPeekLines, unionTagLines } from "./union-repr.js"; +import { registerReplay } from "./emit-journal.js"; export function streamDataAdapter( host: LlvmEmitterContext, @@ -14,6 +15,7 @@ export function streamDataAdapter( if (sym) return sym; sym = `sc_ee_dad_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "streamDataAdapter", [cbT]); const p = cbT.params[0]; if (cbT.params.length > 1) { throw new InternalCompilerError( @@ -207,6 +209,7 @@ export function streamDataAdapter( d.push(`}`, ``); } host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } @@ -220,6 +223,7 @@ export function streamDoneFnFor( if (sym) return sym; sym = `sc_sd_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "streamDoneFnFor", [kind, doneT]); const errT = doneT.params[0]; let errTag = -1; if (errT !== undefined) { @@ -347,6 +351,7 @@ export function streamDoneFnFor( d.push(` call void ${releaseSym(host.shapeHost, dataT)}(ptr %d)`); d.push(` ret void`, `}`, ``); host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } @@ -360,6 +365,7 @@ export function fsRenameThunkFor(host: LlvmEmitterContext, cbT: IrType & { kind: if (sym) return sym; sym = `sc_fsren_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "fsRenameThunkFor", [cbT]); const param = cbT.params[0]!; const d: string[] = [ `define internal void @${sym}(ptr %cb, ptr %err) ${FN_ATTRS} { ; fs.rename callback ${typeKey(cbT)}`, @@ -415,6 +421,7 @@ export function fsRenameThunkFor(host: LlvmEmitterContext, cbT: IrType & { kind: ``, ); host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } @@ -447,6 +454,7 @@ export function cryptoIntThunkFor( if (sym) return sym; sym = `sc_cryptoint_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "cryptoIntThunkFor", [cbT]); const d: string[] = [ `define internal void @${sym}(ptr %cb, double %value) ${FN_ATTRS} { ; randomInt callback ${typeKey(cbT)}`, `entry:`, @@ -489,6 +497,7 @@ export function cryptoIntThunkFor( ``, ); host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } @@ -507,6 +516,7 @@ export function zlibBytesThunkFor( if (sym) return sym; sym = `sc_${family}bytes_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "zlibBytesThunkFor", [cbT, arrayBuffer, family, valueMode]); const d: string[] = [ `define internal void @${sym}(ptr %cb, ptr %err, ptr %value) ${FN_ATTRS} { ; zlib bytes callback ${typeKey(cbT)}`, `entry:`, @@ -618,6 +628,7 @@ export function zlibBytesThunkFor( ``, ); host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } @@ -633,6 +644,7 @@ export function streamCbThunkFor( if (sym) return sym; sym = `sc_scb_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "streamCbThunkFor", [kind, cbT]); const runtimeParams = kind === "r" ? ["ptr %s", "double %size"] @@ -901,5 +913,35 @@ export function streamCbThunkFor( } d.push(` ret void`, `}`, ``); host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } + +registerReplay("streamDataAdapter", (host, args) => + streamDataAdapter(host, args[0] as IrType & { kind: "func" }), +); +registerReplay("streamDoneFnFor", (host, args) => + streamDoneFnFor( + host, + args[0] as "w" | "f" | "d" | "t" | "l", + args[1] as IrType & { kind: "func" }, + ), +); +registerReplay("fsRenameThunkFor", (host, args) => + fsRenameThunkFor(host, args[0] as IrType & { kind: "func" }), +); +registerReplay("cryptoIntThunkFor", (host, args) => + cryptoIntThunkFor(host, args[0] as IrType & { kind: "func" }), +); +registerReplay("zlibBytesThunkFor", (host, args) => + zlibBytesThunkFor( + host, + args[0] as IrType & { kind: "func" }, + args[1] as boolean, + args[2] as string, + args[3] as BytesThunkValue, + ), +); +registerReplay("streamCbThunkFor", (host, args) => + streamCbThunkFor(host, args[0] as "r" | "w" | "f" | "d" | "t" | "l" | "e", args[1] as IrType), +); diff --git a/packages/compiler/src/backend/llvm/integer-or-nan-indexes.test.ts b/packages/compiler/src/backend/llvm/integer-or-nan-indexes.test.ts new file mode 100644 index 000000000..93a0a73ed --- /dev/null +++ b/packages/compiler/src/backend/llvm/integer-or-nan-indexes.test.ts @@ -0,0 +1,130 @@ +import { expect, test } from "vitest"; +import { F64, arrayOf, bytesOf, type IrExpr, type IrModule, type IrStmt } from "../../ir/ir.js"; +import { emitLlvmModule } from "./emitter.js"; + +const loc = { file: "integer-or-nan-indexes.ts", start: 0, end: 0 }; +const nums = arrayOf(F64); +const u8 = bytesOf("u8"); +const ref = (localId: string, type: IrExpr["type"]): IrExpr => ({ + kind: "varRef", + localId, + type, + loc, +}); +const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc }); +/** `t[i]!`: the element, or NaN for an index that misses. */ +const byteRead = (index: IrExpr): IrExpr => ({ + kind: "bytesIntrinsic", + method: "get", + receiver: ref("t", u8), + args: [index], + type: F64, + loc, + invalidNaN: true, +}); + +function module(body: IrStmt[]): IrModule { + return { + irVersion: 17, + sourceFile: loc.file, + entry: "__main", + unions: [], + functions: [ + { name: "__main", params: [], returnType: F64, locals: [], body: [], loc }, + { + name: "read", + params: [ + { name: "a", localId: "a", type: nums }, + { name: "t", localId: "t", type: u8 }, + { name: "i", localId: "i", type: F64 }, + ], + returnType: F64, + locals: [ + { id: "a", name: "a", type: nums, mutable: false }, + { id: "t", name: "t", type: u8, mutable: false }, + { id: "i", name: "i", type: F64, mutable: false }, + { id: "c", name: "c", type: F64, mutable: true }, + ], + body, + loc, + }, + ], + }; +} + +function body(llvm: string): string { + return /^define internal [^\n]*@sc_bf_read(?:\.i32)?\([^]*?^}/m.exec(llvm)![0]; +} + +const count = (text: string, pattern: RegExp): number => text.match(pattern)?.length ?? 0; + +test("an index bound to a byte read that may miss checks only that it is ordered", () => { + const llvm = emitLlvmModule( + module([ + { kind: "varDecl", localId: "c", init: byteRead(ref("i", F64)), loc }, + { + kind: "return", + value: { + kind: "bin", + op: "+", + left: { kind: "arrayGet", arr: ref("a", nums), index: ref("c", F64), type: F64, loc }, + right: { + kind: "bytesIntrinsic", + method: "get", + receiver: ref("t", u8), + args: [{ kind: "bin", op: "+", left: ref("c", F64), right: num(1), type: F64, loc }], + type: F64, + loc, + invalidNaN: true, + }, + type: F64, + loc, + }, + loc, + }, + ]), + ); + const fn = body(llvm); + // Only the parameter index `i` round-trips through a double. + expect(count(fn, /fcmp oeq double/g)).toBe(1); + expect(count(fn, /fcmp ord double/g)).toBe(2); +}); + +test("a local also written an ordinary number keeps the exact index check", () => { + const llvm = emitLlvmModule( + module([ + { kind: "varDecl", localId: "c", init: byteRead(ref("i", F64)), loc }, + { kind: "assign", localId: "c", value: ref("i", F64), loc }, + { + kind: "return", + value: { kind: "arrayGet", arr: ref("a", nums), index: ref("c", F64), type: F64, loc }, + loc, + }, + ]), + ); + const fn = body(llvm); + expect(count(fn, /fcmp oeq double/g)).toBe(2); + expect(fn).not.toContain("fcmp ord double"); +}); + +test("a local whose writes grow from its own value keeps the exact index check", () => { + const llvm = emitLlvmModule( + module([ + { kind: "varDecl", localId: "c", init: byteRead(ref("i", F64)), loc }, + { + kind: "assign", + localId: "c", + value: { kind: "bin", op: "+", left: ref("c", F64), right: num(1), type: F64, loc }, + loc, + }, + { + kind: "return", + value: { kind: "arrayGet", arr: ref("a", nums), index: ref("c", F64), type: F64, loc }, + loc, + }, + ]), + ); + const fn = body(llvm); + expect(count(fn, /fcmp oeq double/g)).toBe(2); + expect(fn).not.toContain("fcmp ord double"); +}); diff --git a/packages/compiler/src/backend/llvm/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/lib-connect.ts b/packages/compiler/src/backend/llvm/lib-connect.ts index ce9c40e21..4d151fc7e 100644 --- a/packages/compiler/src/backend/llvm/lib-connect.ts +++ b/packages/compiler/src/backend/llvm/lib-connect.ts @@ -3,6 +3,7 @@ import { typeKey } from "../../ir/ir.js"; import type { LibCallExpr, LlValue, LlvmEmitterContext } from "./expr-context.js"; import { abiValue, callbackType, finishRuntimeCall, ptr } from "./lib-abi.js"; import { FN_ATTRS } from "./shapes.js"; +import { registerReplay } from "./emit-journal.js"; /** CONNECT transfers owned request/socket/response values to the listener. */ function connectAdapter(host: LlvmEmitterContext, cb: LlValue, h2: boolean): string { @@ -18,6 +19,7 @@ function connectAdapter(host: LlvmEmitterContext, cb: LlValue, h2: boolean): str if (previous !== undefined) return previous; const symbol = `sc_connect_${host.resolveThunks.size}`; host.resolveThunks.set(key, symbol); + host.journal?.allocated(symbol, "connectAdapter", [cb, h2]); const retain = h2 ? "scr_http_res_retain_v" : "scr_net_sock_retain_v"; const release = h2 ? "scr_http_res_release_v" : "scr_net_sock_release_v"; host.declare(`declare ptr @${retain}(ptr)`); @@ -37,6 +39,7 @@ function connectAdapter(host: LlvmEmitterContext, cb: LlValue, h2: boolean): str "}", "", ); + host.journal?.built(); return symbol; } @@ -70,3 +73,7 @@ export function emitConnectListener(host: LlvmEmitterContext, e: LibCallExpr): L a[2]!, ]); } + +registerReplay("connectAdapter", (host, args) => + connectAdapter(host, args[0] as LlValue, args[1] as boolean), +); diff --git a/packages/compiler/src/backend/llvm/lib-datagram.ts b/packages/compiler/src/backend/llvm/lib-datagram.ts index 184fdb99e..07de4e54e 100644 --- a/packages/compiler/src/backend/llvm/lib-datagram.ts +++ b/packages/compiler/src/backend/llvm/lib-datagram.ts @@ -11,6 +11,7 @@ import { ptr, } from "./lib-abi.js"; import { FN_ATTRS } from "./shapes.js"; +import { registerReplay } from "./emit-journal.js"; function messageAdapter(host: LlvmEmitterContext, type: IrType): string { if (type.kind !== "record") @@ -20,6 +21,7 @@ function messageAdapter(host: LlvmEmitterContext, type: IrType): string { if (previous !== undefined) return previous; const symbol = `sc_dgram_message_${host.resolveThunks.size}`; host.resolveThunks.set(key, symbol); + host.journal?.allocated(symbol, "messageAdapter", [type]); const shape = host.recordsById.get(type.shapeId); if (shape === undefined) throw new InternalCompilerError("missing datagram remote info shape"); const lines: string[] = []; @@ -48,6 +50,7 @@ function messageAdapter(host: LlvmEmitterContext, type: IrType): string { "}", "", ); + host.journal?.built(); return symbol; } @@ -67,6 +70,7 @@ function lookupAdapter(host: LlvmEmitterContext, cb: LlValue): string { if (previous !== undefined) return previous; const symbol = `sc_dns_lookup_${host.resolveThunks.size}`; host.resolveThunks.set(key, symbol); + host.journal?.allocated(symbol, "lookupAdapter", [cb]); host.declare("declare ptr @scr_error_new(i32, ptr)"); host.declare("declare ptr @scr_error_retain_v(ptr)"); host.declare("declare void @scr_error_release_v(ptr)"); @@ -93,6 +97,7 @@ function lookupAdapter(host: LlvmEmitterContext, cb: LlValue): string { "}", "", ); + host.journal?.built(); return symbol; } @@ -105,6 +110,10 @@ export function emitDatagramLibCall(host: LlvmEmitterContext, e: LibCallExpr): L "dgram.sendStr": "scr_dgram_send_str", "dgram.sendBytes": "scr_dgram_send_bytes", "dgram.sendChk": "scr_dgram_send_chk", + "dgram.unixSocket": "scr_dgram_unix_socket", + "dgram.unixConnect": "scr_dgram_unix_connect", + "dgram.unixSend": "scr_dgram_unix_send", + "dgram.unixClose": "scr_dgram_unix_close", "dgram.ref": "scr_dgram_ref", "dgram.unref": "scr_dgram_unref", "dgram.setBroadcast": "scr_dgram_set_broadcast", @@ -250,3 +259,6 @@ export function emitDatagramLibCall(host: LlvmEmitterContext, e: LibCallExpr): L throw new InternalCompilerError(`unhandled datagram call ${e.fn}`); } } + +registerReplay("messageAdapter", (host, args) => messageAdapter(host, args[0] as IrType)); +registerReplay("lookupAdapter", (host, args) => lookupAdapter(host, args[0] as LlValue)); diff --git a/packages/compiler/src/backend/llvm/lib-filesystem.ts b/packages/compiler/src/backend/llvm/lib-filesystem.ts index 6d5fe652d..7d9be632d 100644 --- a/packages/compiler/src/backend/llvm/lib-filesystem.ts +++ b/packages/compiler/src/backend/llvm/lib-filesystem.ts @@ -163,7 +163,7 @@ function emitObjEntryPairs(host: LlvmEmitterContext, e: LibCallExpr): LlValue { B.line(`${row} = call ptr @${mangleRecordNew(rowT.shapeId)}()`); host.storeField(host.recordFieldPtr(row, rowT.shapeId, "0").ptr, STRING, key, false); host.storeField(host.recordFieldPtr(row, rowT.shapeId, "1").ptr, DYN, item, false); - host.arrPush(arr, "ref", row); // push takes ownership of the row + host.arrPush(arr, "ref", row, null); // push takes ownership of the row const next = B.tmp(); B.line(`${next} = add ${size} ${i}, 1`); B.line(`store ${size} ${next}, ptr ${iSlot}`); @@ -536,7 +536,7 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr): const parent = B.tmp(); B.line(`${parent} = call ptr @scr_fs_glob_parent(ptr ${snap}, ${host.sizeType} ${i}) ; +1`); B.line(`store ptr ${parent}, ptr ${host.recordFieldPtr(row, recT.shapeId, "parentPath").ptr}`); - host.arrPush(arr, "ref", row); // push takes ownership of the row + host.arrPush(arr, "ref", row, null); // push takes ownership of the row const i2 = B.tmp(); B.line(`${i2} = add ${host.sizeType} ${i}, 1`); B.line(`store ${host.sizeType} ${i2}, ptr ${iSlot}`); @@ -620,7 +620,7 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr): B.line( `store ptr ${host.retainHeap(path.name, STRING)}, ptr ${host.recordFieldPtr(row, recT.shapeId, "parentPath").ptr}`, ); - host.arrPush(arr, "ref", row); // push takes ownership of the row + host.arrPush(arr, "ref", row, null); // push takes ownership of the row const i2 = B.tmp(); B.line(`${i2} = add ${host.sizeType} ${i}, 1`); B.line(`store ${host.sizeType} ${i2}, ptr ${iSlot}`); @@ -955,7 +955,7 @@ export function emitPathUrlLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll B.startBlock(lj); const rows = B.tmp(); B.line(`${rows} = load ptr, ptr ${rowsSlot}`); - host.arrPush(rows, "ref", row); // push takes ownership of the row + host.arrPush(rows, "ref", row, null); // push takes ownership of the row B.line(`call void @scr_arr_release(ptr ${rows})`); B.line(`call void @scr_str_release(ptr ${nm})`); const i2 = B.tmp(); diff --git a/packages/compiler/src/backend/llvm/lib-http2.ts b/packages/compiler/src/backend/llvm/lib-http2.ts index 791987f86..321f1d005 100644 --- a/packages/compiler/src/backend/llvm/lib-http2.ts +++ b/packages/compiler/src/backend/llvm/lib-http2.ts @@ -13,6 +13,7 @@ import { requestHandler, } from "./lib-abi.js"; import { FN_ATTRS } from "./shapes.js"; +import { registerReplay } from "./emit-journal.js"; /** The runtime owns the answer closure; its arguments arrive owned from the caller. */ function sniAnswer(host: LlvmEmitterContext, type: IrType): string { @@ -22,6 +23,7 @@ function sniAnswer(host: LlvmEmitterContext, type: IrType): string { if (previous !== undefined) return previous; const symbol = `sc_sni_answer_${host.resolveThunks.size}`; host.resolveThunks.set(key, symbol); + host.journal?.allocated(symbol, "sniAnswer", [type]); const params = ["ptr %self"], body: string[] = []; let hasError = "false", @@ -82,6 +84,7 @@ function sniAnswer(host: LlvmEmitterContext, type: IrType): string { "}", "", ); + host.journal?.built(); return symbol; } @@ -318,3 +321,5 @@ export function emitHttp2LibCall(host: LlvmEmitterContext, e: LibCallExpr): LlVa const handler = thunk === null ? [a[1]!] : callbackAdapter(host, cb, thunk, params); return finishRuntimeCall(host, e, runtime, [a[0]!, ...handler, ...a.slice(2)]); } + +registerReplay("sniAnswer", (host, args) => sniAnswer(host, args[0] as IrType)); diff --git a/packages/compiler/src/backend/llvm/lib-lookup.ts b/packages/compiler/src/backend/llvm/lib-lookup.ts index c6bc4d97b..56e94461f 100644 --- a/packages/compiler/src/backend/llvm/lib-lookup.ts +++ b/packages/compiler/src/backend/llvm/lib-lookup.ts @@ -1,9 +1,10 @@ import { InternalCompilerError } from "../../errors.js"; -import { typeKey } from "../../ir/ir.js"; +import { type IrType, typeKey } from "../../ir/ir.js"; import { mangleRecordRelease, mangleRecordStruct } from "../mangle.js"; import type { LibCallExpr, LlValue, LlvmEmitterContext } from "./expr-context.js"; import { abiValue, callbackType, finishRuntimeCall, ptr } from "./lib-abi.js"; import { FN_ATTRS } from "./shapes.js"; +import { registerReplay } from "./emit-journal.js"; export function emitLookupConnect(host: LlvmEmitterContext, e: LibCallExpr): LlValue { const args = e.args.map((arg) => host.emitExpr(arg)), @@ -12,6 +13,18 @@ export function emitLookupConnect(host: LlvmEmitterContext, e: LibCallExpr): LlV answer = lookup.params[2]; if (answer?.kind !== "func" || answer.params.length !== 2) throw new InternalCompilerError("lookup answer must have two parameters"); + const symbol = lookupAnswer(host, answer); + host.usesTimers = true; + host.toHeap(cb); + return finishRuntimeCall(host, e, "scr_net_connect_lookup", [ + ...args.map((arg) => abiValue(host, arg)), + ptr("@" + symbol), + ]); +} + +/** The thunk that hands a lookup's answer (error, addresses) to the + * runtime, one per answer signature. */ +function lookupAnswer(host: LlvmEmitterContext, answer: IrType & { kind: "func" }): string { const error = answer.params[0]!, addresses = answer.params[1]!; if (error.kind !== "union" || addresses.kind !== "array" || addresses.elem.kind !== "record") @@ -26,6 +39,7 @@ export function emitLookupConnect(host: LlvmEmitterContext, e: LibCallExpr): LlV if (symbol === undefined) { symbol = `sc_lookup_answer_${host.resolveThunks.size}`; host.resolveThunks.set(key, symbol); + host.journal?.allocated(symbol, "lookupAnswer", [answer]); for (const declaration of [ "declare ptr @scr_str_retain_v(ptr)", "declare void @scr_str_release(ptr)", @@ -84,11 +98,11 @@ export function emitLookupConnect(host: LlvmEmitterContext, e: LibCallExpr): LlV "}", "", ); + host.journal?.built(); } - host.usesTimers = true; - host.toHeap(cb); - return finishRuntimeCall(host, e, "scr_net_connect_lookup", [ - ...args.map((arg) => abiValue(host, arg)), - ptr("@" + symbol), - ]); + return symbol; } + +registerReplay("lookupAnswer", (host, args) => + lookupAnswer(host, args[0] as IrType & { kind: "func" }), +); diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 9a65d9a24..d0888ccbe 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -47,7 +47,7 @@ export const LIB_FN_SYMS: Record = { "dyn.fromEntries": "scr_dyn_from_entries", "bytes.constructor": "scr_bytes_constructor", "bytes.construct": "scr_bytes_construct", - "bytes.instanceOf": "scr_bytes_instanceof", + "bytes.instanceOf": "scr_bytes_instanceof_at", "arrayBuffer.constructor": "scr_array_buffer_constructor", "sharedArrayBuffer.new": "scr_shared_array_buffer_new", "sharedArrayBuffer.is": "scr_shared_array_buffer_is", @@ -579,6 +579,10 @@ export const LIB_FN_SYMS: Record = { "crypto.ecdhNew": "scr_crypto_ecdh_new", "crypto.ecdhConvertKey": "scr_crypto_ecdh_convert_key", "crypto.dhNew": "scr_crypto_dh_new", + "crypto.checkPrimeSync": "scr_crypto_check_prime_sync", + "crypto.checkPrimeCb": "scr_crypto_check_prime_async", + "crypto.generatePrimeSync": "scr_crypto_generate_prime_sync", + "crypto.generatePrimeCb": "scr_crypto_generate_prime_async", "crypto.dhGroupNew": "scr_crypto_dh_group_new", "crypto.hmacNewStr": "scr_crypto_hmac_new_str", "crypto.hmacNewBytes": "scr_crypto_hmac_new_bytes", @@ -754,6 +758,7 @@ export const LIB_FN_SYMS: Record = { "cp.spawnSync": "scr_spawn_sync", "cp.spawnSyncOpts": "scr_spawn_sync_opts", "cp.spawnSyncStdioStr": "scr_spawn_sync_stdio_str", + "cp.spawnSyncEnv": "scr_spawn_sync_env", "cp.execSync": "scr_exec_sync", "cp.execCapture": "scr_exec_capture", "spawnRes.stdout": "scr_spawn_res_stdout", @@ -1600,6 +1605,8 @@ export function mockTimerSym( export const USES_TIMERS_LIB_FNS = new Set([ "crypto.generateKeyPairCb", + "crypto.checkPrimeCb", + "crypto.generatePrimeCb", "process.emitWarning", // Constructor-function streams defer their events on the stream ticks. "dyn.builtinStreamClass", diff --git a/packages/compiler/src/backend/llvm/local-array-reads.test.ts b/packages/compiler/src/backend/llvm/local-array-reads.test.ts index b09029865..7af495d42 100644 --- a/packages/compiler/src/backend/llvm/local-array-reads.test.ts +++ b/packages/compiler/src/backend/llvm/local-array-reads.test.ts @@ -940,3 +940,17 @@ test("a dense read tests the hole-free prefix before the capacity", () => { expect(ir.lastIndexOf("local.array.range", firstCap)).toBeGreaterThan(firstFilled); } }); + +test("an optional read at a proven integer index skips the double round trip", () => { + const module = fixture(); + const call = module.functions[2]!.body[0]!; + if (call.kind !== "varDecl" || call.init?.kind !== "call") throw new Error("missing call"); + call.init.args[1] = { kind: "bin", op: "|", left: ref("i", F64), right: num(0), type: F64, loc }; + expect(candidates(module).get("value")?.borrow).toBe(true); + expect(validateModule(module)).toEqual([]); + // The int32 index is the dense offset on 64-bit targets; a 32-bit target + // converts the double, as a borrowed read does. + expect(workBody(module, 64)).not.toContain("local.array.exact"); + expect(workBody(module, 64)).toContain("local.array.value"); + expect(workBody(module, 32)).toContain("local.array.exact"); +}); diff --git a/packages/compiler/src/backend/llvm/local-array-reads.ts b/packages/compiler/src/backend/llvm/local-array-reads.ts index cdd00c4e7..8df187736 100644 --- a/packages/compiler/src/backend/llvm/local-array-reads.ts +++ b/packages/compiler/src/backend/llvm/local-array-reads.ts @@ -15,6 +15,13 @@ import { ReferenceEffects } from "./reference-effects.js"; import { SCR_ARR_FIELD, loadArrCount, scrArrStateAt } from "./common.js"; import { exactInteger, widenInteger } from "./integer-values.js"; import { READ_FALLBACK } from "./dense-array-access.js"; +import { compactRefLoadLines } from "./classes.js"; + +/** The dense slot load of a reference read from an array of type + * `arrayType`: a pointer, or a compact class reference (SCR_ELEM_CREF). */ +function referenceLoad(host: LlvmEmitterContext, arrayType: IrType): "ptr" | "cref" { + return arrayType.kind === "array" && host.compactArrayElem(arrayType.elem) ? "cref" : "ptr"; +} export interface LocalArrayRead { type: IrType; @@ -469,7 +476,7 @@ function emitDenseReferenceArrayRead( no: string, slow: string, join: string, - loadType: "ptr" | "i64" | "i8" = "ptr", + loadType: "ptr" | "cref" | "i64" | "i8" = "ptr", ): void { const B = host.B; const header = (field: number): string => { @@ -555,7 +562,14 @@ function emitDenseReferenceArrayRead( const statePtr = B.tmp(), denseState = B.tmp(), densePresent = B.tmp(); - scrArrStateAt(host, statePtr, data, cap, loadType === "i8" ? 1 : 8, offset); + scrArrStateAt( + host, + statePtr, + data, + cap, + loadType === "i8" ? 1 : loadType === "cref" ? 4 : 8, + offset, + ); host.markMemoryPointer(statePtr, "array:present"); B.line(`${denseState} = load i8, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`); B.line(`${densePresent} = icmp eq i8 ${denseState}, 1`); @@ -564,13 +578,20 @@ function emitDenseReferenceArrayRead( B.startBlock(valueLabel); const valuePtr = B.tmp(), raw = B.tmp(); - // Boolean arrays store one byte per element; every other kind an 8-byte slot. - const slotType = loadType === "i8" ? "i8" : "i64"; + // Boolean arrays store one byte per element, compact references four, + // every other kind an 8-byte slot. + const slotType = loadType === "i8" ? "i8" : loadType === "cref" ? "i32" : "i64"; B.line( `${valuePtr} = getelementptr inbounds ${slotType}, ptr ${data}, ${host.sizeType} ${offset}`, ); host.markMemoryPointer(valuePtr, "array:elements"); - B.line(`${raw} = load ${loadType}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); + if (loadType === "cref") { + const ref = B.tmp(); + B.line(`${ref} = load i32, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); + for (const line of compactRefLoadLines(ref, raw, `${raw}.r`)) B.line(line.trimStart()); + } else { + B.line(`${raw} = load ${loadType}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); + } present(raw); B.br(join); } @@ -598,6 +619,18 @@ export function emitBorrowedArrayRead( ); host.markMemoryPointer(dataPtr, "array:header"); B.line(`${data} = load ptr, ptr ${dataPtr}${host.fieldAliasAttachment(dataPtr)}`); + if (referenceLoad(host, read.arr.type) === "cref") { + // Compact references: 4-byte slots holding the address over 8. + const ref = B.tmp(); + B.line( + `${valuePtr} = getelementptr inbounds i32, ptr ${data}, ${host.sizeType} ${loopIndex}`, + ); + host.markMemoryPointer(valuePtr, "array:elements"); + B.line(`${ref} = load i32, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); + for (const line of compactRefLoadLines(ref, value, `${value}.r`)) B.line(line.trimStart()); + B.line(`store ptr ${value}, ptr ${slot}`); + return; + } B.line(`${valuePtr} = getelementptr inbounds i64, ptr ${data}, ${host.sizeType} ${loopIndex}`); host.markMemoryPointer(valuePtr, "array:elements"); B.line(`${value} = load ptr, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); @@ -622,6 +655,7 @@ export function emitBorrowedArrayRead( slow, slow, join, + referenceLoad(host, read.arr.type), ); B.startBlock(slow); host.declare("declare ptr @scr_arr_borrow_ref(ptr, double)"); @@ -644,8 +678,15 @@ export function emitLocalArrayRead( ): { slot: string; type: IrType } | null { const B = host.B; const array = host.emitStableReceiver(read.array, [read.index]); - const integerIndex = inline ? host.emitIntegerLoopIndex(read.index) : null; + const loopIndex = inline ? host.emitIntegerLoopIndex(read.index) : null; const index = host.emitExpr(read.index); + // A proven integer index (a counter with a known range, say) needs no + // double round trip, as in a borrowed read. + const exact = + inline && loopIndex === null && host.sizeType === "i64" + ? exactInteger(host, index, read.index) + : null; + const integerIndex = loopIndex ?? (exact ? widenInteger(host, exact) : null); const nullable = read.type.kind === "union" ? host.nullableUnions.get(read.type.unionId) : null; if (nullable) { // A nullable union is the element pointer itself: borrowed from an @@ -665,12 +706,12 @@ export function emitLocalArrayRead( array, index, integerIndex, - integerIndex !== null, + loopIndex !== null, store, no, slow, join, - "ptr", + referenceLoad(host, read.array.type), ); else B.br(slow); B.startBlock(slow); @@ -726,12 +767,12 @@ export function emitLocalArrayRead( array, index, integerIndex, - integerIndex !== null, + loopIndex !== null, storeValue, no, slow, join, - !scalar ? "ptr" : read.element.kind === "bool" ? "i8" : "i64", + !scalar ? referenceLoad(host, read.array.type) : read.element.kind === "bool" ? "i8" : "i64", ); else B.br(slow); B.startBlock(slow); diff --git a/packages/compiler/src/backend/llvm/map-number-lookup.test.ts b/packages/compiler/src/backend/llvm/map-number-lookup.test.ts index 5dc3589e4..989b8b3b0 100644 --- a/packages/compiler/src/backend/llvm/map-number-lookup.test.ts +++ b/packages/compiler/src/backend/llvm/map-number-lookup.test.ts @@ -181,3 +181,63 @@ test("hashed number-key lookups probe inline and call the runtime only for wide ); } }); + +/** work(map): map.size, and keep(map): map returned as an owned value. */ +function sizeFixture(): IrModule { + const map: IrType = { kind: "map", key: F64, value: F64 }; + const params = [{ localId: "map", name: "map", type: map }]; + const locals = params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: true })); + const size: IrExpr = { + kind: "mapIntrinsic", + method: "size", + receiver: ref("map", map), + args: [], + type: F64, + loc, + }; + return { + irVersion: 17, + sourceFile: loc.file, + entry: "main", + records: [], + unions: [], + functions: [ + { name: "main", params: [], locals: [], returnType: VOID, body: [], loc }, + { + name: "work", + params, + returnType: F64, + loc, + locals, + body: [{ kind: "return", value: size, loc }], + }, + { + name: "keep", + params, + returnType: map, + loc, + locals, + body: [{ kind: "return", value: ref("map", map), loc }], + }, + ], + }; +} + +test("Map.size and Map retains need no runtime call in release builds", () => { + const mod = sizeFixture(); + expect(validateModule(mod)).toEqual([]); + for (const bits of [32, 64] as const) { + const module = emitLlvmModule(mod, { pointerBits: bits, inlineRc: "release" }); + const fn = (name: string) => + new RegExp(`^define internal [^\\n]*@sc_(?:b)?f_${name}(?:\\.i32)?\\([^]*?^}`, "m").exec( + module, + )![0]; + // Field 9 is the live entry count (scr_map.h). + expect(module).toMatch(/^define internal double @sc_map_size\(ptr %m\)/m); + expect(module).toContain(`getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 9`); + expect(fn("work")).toContain("call double @sc_map_size("); + expect(module).not.toContain("call double @scr_map_size("); + expect(fn("keep")).toContain("@sc_rc_retain_map("); + expect(module).not.toContain("call ptr @scr_map_retain_v("); + } +}); diff --git a/packages/compiler/src/backend/llvm/map-number-lookup.ts b/packages/compiler/src/backend/llvm/map-number-lookup.ts index 1244bd9c3..6d219b50a 100644 --- a/packages/compiler/src/backend/llvm/map-number-lookup.ts +++ b/packages/compiler/src/backend/llvm/map-number-lookup.ts @@ -15,6 +15,12 @@ * retain inline rather than through the runtime's indirect adapter call, * and scalar reads and has() share the one lookup. * + * A string-key map whose values are compact class instances + * (SCR_MAP_VAL_CREF, LlEmitter.compactArrayElem of the value type) keeps + * 16-byte { key, hash, val } entries with the value as an address over 8: + * its reads go through sc_map_entry_str_c and sc_map_iter_entry_c, and + * decode the 4-byte value. + * * The iteration steps behind for-of, forEach and the other entry walks read * the entries array through the internal sc_map_iter_entry and * sc_map_iter_count helpers: a live entry's key or value loads in place, @@ -24,6 +30,7 @@ */ import type { IrType } from "../../ir/ir.js"; import type { LlvmEmitterContext } from "./expr-context.js"; +import { mapKeyAccess } from "./shapes.js"; export const NUMBER_MAP_ENTRY_DECL = "declare ptr @scr_map_entry_f64(ptr, double)"; @@ -47,18 +54,59 @@ export function emitNumberMapSlot(host: LlvmEmitterContext, entry: string, type: } export const STRING_MAP_ENTRY_DECL = "declare ptr @scr_map_entry_str(ptr, ptr)"; +/** Declared by reads of a compact-value map; its presence makes the emitter + * define sc_map_entry_str_c and sc_map_iter_entry_c (emit-module.ts). */ +export const STRING_MAP_ENTRY_C_DECL = "declare ptr @scr_map_entry_str_c(ptr, ptr)"; -/** The entry for a string `key`, or null when absent. Valid until the next mutation. */ -export function emitStringMapEntry(host: LlvmEmitterContext, map: string, key: string): string { - host.declare(STRING_MAP_ENTRY_DECL); +/** Whether a Map with key type `key` and value type `value` keeps compact + * values (SCR_MAP_VAL_CREF): string keys and a value type whose instances + * are compact (LlEmitter.compactArrayElem). */ +export function compactMapValues(host: LlvmEmitterContext, key: IrType, value: IrType): boolean { + // SCR_MAP_KEY_STR (shapes.ts mapKeyKindNum 1): string access, not a union. + return key.kind !== "union" && mapKeyAccess(key) === "str" && host.compactArrayElem(value); +} + +/** The entry for a string `key`, or null when absent. Valid until the next + * mutation. `compact`: the map keeps compact values (a %ScrMapCEntry). */ +export function emitStringMapEntry( + host: LlvmEmitterContext, + map: string, + key: string, + compact = false, +): string { + host.declare(compact ? STRING_MAP_ENTRY_C_DECL : STRING_MAP_ENTRY_DECL); const entry = host.B.tmp(); - host.B.line(`${entry} = call ptr @sc_map_entry_str(ptr ${map}, ptr ${key})`); + host.B.line( + `${entry} = call ptr @${compact ? "sc_map_entry_str_c" : "sc_map_entry_str"}(ptr ${map}, ptr ${key})`, + ); return entry; } +/** A found compact-value entry's value as a pointer: its 4-byte form, an + * address over 8 (scr_map.h ScrMapCEntry). */ +export function emitCompactMapSlot(host: LlvmEmitterContext, entry: string): string { + const B = host.B; + const slot = B.tmp(), + ref = B.tmp(), + wide = B.tmp(), + addr = B.tmp(), + value = B.tmp(); + B.line(`${slot} = getelementptr inbounds %ScrMapCEntry, ptr ${entry}, i32 0, i32 2`); + B.line(`${ref} = load i32, ptr ${slot}`); + B.line(`${wide} = zext i32 ${ref} to i64`); + B.line(`${addr} = shl nuw nsw i64 ${wide}, 3`); + B.line(`${value} = inttoptr i64 ${addr} to ptr`); + return value; +} + /** Map.has / Set.has under a string key. */ -export function emitStringMapHas(host: LlvmEmitterContext, map: string, key: string): string { - const entry = emitStringMapEntry(host, map, key); +export function emitStringMapHas( + host: LlvmEmitterContext, + map: string, + key: string, + compact = false, +): string { + const entry = emitStringMapEntry(host, map, key, compact); const found = host.B.tmp(); host.B.line(`${found} = icmp ne ptr ${entry}, null`); return found; @@ -92,12 +140,18 @@ export function emitStringMapGetRef( map: string, key: string, value: IrType, + compact = false, ): string { - return emitMapEntryGetRef(host, emitStringMapEntry(host, map, key), value); + return emitMapEntryGetRef(host, emitStringMapEntry(host, map, key, compact), value, compact); } /** The +1 value of a found entry, or null for a missing (null) entry. */ -function emitMapEntryGetRef(host: LlvmEmitterContext, entry: string, value: IrType): string { +function emitMapEntryGetRef( + host: LlvmEmitterContext, + entry: string, + value: IrType, + compact = false, +): string { const B = host.B; const found = B.tmp(); B.line(`${found} = icmp ne ptr ${entry}, null`); @@ -106,7 +160,10 @@ function emitMapEntryGetRef(host: LlvmEmitterContext, entry: string, value: IrTy join = B.newLabel("mapn.join"); B.condBr(found, hit, miss); B.startBlock(hit); - const retained = host.retainFrame(emitNumberMapSlot(host, entry, "ptr"), value); + const retained = host.retainFrame( + compact ? emitCompactMapSlot(host, entry) : emitNumberMapSlot(host, entry, "ptr"), + value, + ); B.br(join); B.startBlock(miss); B.br(join); @@ -199,20 +256,25 @@ export const SCR_MAP_NUM_DEAD = "9218868437227405313"; /** The entry layout a Map or Set of key type `key` stores: * - "num": number keys, whose entries are { key, val } (scr_map.h ScrMapNumEntry); + * - "centry": string keys with compact values (compactMapValues), whose + * entries are { key, hash, val } (ScrMapCEntry); * - "entry": every other typed key, whose entries are { key, val, hash }; * - "either": checked (`unknown`) keys. A `Map` or * `Set` cast from a checked value keeps the collection's own - * storage (scr_dyn_native_collection_check), which can have number keys. */ -export type MapEntryLayout = "num" | "entry" | "either"; + * storage (scr_dyn_native_collection_check), which can have number keys + * or compact values. */ +export type MapEntryLayout = "num" | "centry" | "entry" | "either"; -export function mapEntryLayout(key: IrType): MapEntryLayout { +export function mapEntryLayout(key: IrType, compact = false): MapEntryLayout { + if (compact) return "centry"; return key.kind === "f64" ? "num" : key.kind === "dyn" ? "either" : "entry"; } /** The entry at an iteration index, or null for a tombstone or an index at * or past the entry count (the scr_map_iter_live test). With the "either" * layout, the map's key kind (%ScrMapIx field 1, SCR_MAP_KEY_F64 = 0) picks - * the step at run time. */ + * the step at run time; in a program with compact class instances, so does + * its value kind (field 2, SCR_MAP_VAL_CREF = 3). */ export function emitMapIterEntry( host: LlvmEmitterContext, map: string, @@ -220,14 +282,20 @@ export function emitMapIterEntry( layout: MapEntryLayout, ): string { const B = host.B; - const step = (num: boolean): string => { - host.declare(num ? MAP_ITER_NUM_DECL : MAP_ITER_DECL); + const step = (kind: "num" | "centry" | "entry"): string => { + host.declare(kind === "num" ? MAP_ITER_NUM_DECL : MAP_ITER_DECL); + if (kind === "centry") host.declare(STRING_MAP_ENTRY_C_DECL); const entry = B.tmp(); - const helper = num ? "sc_map_iter_entry_num" : "sc_map_iter_entry"; + const helper = + kind === "num" + ? "sc_map_iter_entry_num" + : kind === "centry" + ? "sc_map_iter_entry_c" + : "sc_map_iter_entry"; B.line(`${entry} = call ptr @${helper}(ptr ${map}, double ${index})`); return entry; }; - if (layout !== "either") return step(layout === "num"); + if (layout !== "either") return step(layout); host.declare(MAP_ITER_DECL); const kindPtr = B.tmp(), kind = B.tmp(), @@ -240,16 +308,31 @@ export function emitMapIterEntry( join = B.newLabel("mapit.layout"); B.condBr(isNum, numLabel, entryLabel); B.startBlock(numLabel); - const numEntry = step(true); + const numEntry = step("num"); const numEnd = B.currentLabel(); B.br(join); B.startBlock(entryLabel); - const otherEntry = step(false); - const entryEnd = B.currentLabel(); + const arms: { value: string; label: string }[] = [{ value: numEntry, label: numEnd }]; + if (host.shapeHost.compactRegion?.() === true) { + const valPtr = B.tmp(), + valKind = B.tmp(), + isCompact = B.tmp(); + B.line(`${valPtr} = getelementptr inbounds %ScrMapIx, ptr ${map}, i32 0, i32 2`); + B.line(`${valKind} = load i8, ptr ${valPtr}`); + B.line(`${isCompact} = icmp eq i8 ${valKind}, 3`); + const compactLabel = B.newLabel("mapit.centry"), + wideLabel = B.newLabel("mapit.wide"); + B.condBr(isCompact, compactLabel, wideLabel); + B.startBlock(compactLabel); + arms.push({ value: step("centry"), label: B.currentLabel() }); + B.br(join); + B.startBlock(wideLabel); + } + arms.push({ value: step("entry"), label: B.currentLabel() }); B.br(join); B.startBlock(join); const entry = B.tmp(); - B.line(`${entry} = phi ptr [ ${numEntry}, %${numEnd} ], [ ${otherEntry}, %${entryEnd} ]`); + B.line(`${entry} = phi ptr ${arms.map((a) => `[ ${a.value}, %${a.label} ]`).join(", ")}`); return entry; } @@ -270,6 +353,16 @@ export function emitMapIterCount(host: LlvmEmitterContext, map: string): string return count; } +export const MAP_SIZE_DECL = "declare double @scr_map_size(ptr)"; + +/** Map.size / Set.size: the live entry count, read inline (sc_map_size). */ +export function emitMapSize(host: LlvmEmitterContext, map: string): string { + host.declare(MAP_SIZE_DECL); + const size = host.B.tmp(); + host.B.line(`${size} = call double @sc_map_size(ptr ${map})`); + return size; +} + /** The key (field 0) or value (field 1) of the live entry at an iteration * index, read inline: a number as double, a boolean as i1, and a string or * reference retained with `type`'s retain (+1), the adapter the collection @@ -294,18 +387,24 @@ export function emitMapIterSlot( join = B.newLabel("mapit.join"); B.condBr(found, hit, miss); B.startBlock(hit); - const slot = B.tmp(); - B.line(`${slot} = getelementptr inbounds %ScrMapEntry, ptr ${entry}, i32 0, i32 ${field}`); const llTy = access === "f64" ? "double" : access === "bool" ? "i1" : "ptr"; - let loaded = B.tmp(); - if (access === "bool") { - const bits = loaded; - B.line(`${bits} = load i64, ptr ${slot}`); - loaded = B.tmp(); - B.line(`${loaded} = icmp ne i64 ${bits}, 0`); + let loaded: string; + if (layout === "centry" && field === 1) { + // A compact value; its type's retain emits no counting. + loaded = host.retainFrame(emitCompactMapSlot(host, entry), type); } else { - B.line(`${loaded} = load ${llTy}, ptr ${slot}`); - if (access === "ref") loaded = host.retainFrame(loaded, type); + const slot = B.tmp(); + B.line(`${slot} = getelementptr inbounds %ScrMapEntry, ptr ${entry}, i32 0, i32 ${field}`); + loaded = B.tmp(); + if (access === "bool") { + const bits = loaded; + B.line(`${bits} = load i64, ptr ${slot}`); + loaded = B.tmp(); + B.line(`${loaded} = icmp ne i64 ${bits}, 0`); + } else { + B.line(`${loaded} = load ${llTy}, ptr ${slot}`); + if (access === "ref") loaded = host.retainFrame(loaded, type); + } } const hitEnd = B.currentLabel(); B.br(join); diff --git a/packages/compiler/src/backend/llvm/map-read-lifetimes.ts b/packages/compiler/src/backend/llvm/map-read-lifetimes.ts index 005f88f13..6b58d314a 100644 --- a/packages/compiler/src/backend/llvm/map-read-lifetimes.ts +++ b/packages/compiler/src/backend/llvm/map-read-lifetimes.ts @@ -15,6 +15,7 @@ import { emitNumberMapGetScalar, emitStringMapGetScalar, emitStringMapGetRef, + compactMapValues, } from "./map-number-lookup.js"; import { mapKeyAccess, mapKeyParamType } from "./shapes.js"; import { rememberMapEntry } from "./map-entry-fusion.js"; @@ -323,7 +324,10 @@ export function emitStackMapRead(host: LlvmEmitterContext, read: LocalMapRead): } else if (isRefCounted(read.value)) { const stringKey = entryStringKey(key); let raw: string; - if (stringKey !== null) raw = emitStringMapGetRef(host, receiver.name, stringKey, read.value); + const mapType = read.receiver.type; + const compact = mapType.kind === "map" && compactMapValues(host, mapType.key, mapType.value); + if (stringKey !== null) + raw = emitStringMapGetRef(host, receiver.name, stringKey, read.value, compact); else { raw = B.tmp(); host.declare(`declare ptr @scr_map_get_${access}_ref(ptr, ${keyType})`); diff --git a/packages/compiler/src/backend/llvm/nullable-unions.ts b/packages/compiler/src/backend/llvm/nullable-unions.ts index 8729e9649..2c355f551 100644 --- a/packages/compiler/src/backend/llvm/nullable-unions.ts +++ b/packages/compiler/src/backend/llvm/nullable-unions.ts @@ -279,6 +279,8 @@ function shapeUnion( * tag and payload inside the C map. */ function boxedUnionIds(mod: IrModule): Set { const boxed = new Set(); + // Only the module's union definitions consult the result. + if (!mod.unions?.length) return boxed; const mark = (t: IrType): void => { if (t.kind === "union") boxed.add(t.unionId); }; diff --git a/packages/compiler/src/backend/llvm/publish.ts b/packages/compiler/src/backend/llvm/publish.ts index 2f45c3cd2..60270adda 100644 --- a/packages/compiler/src/backend/llvm/publish.ts +++ b/packages/compiler/src/backend/llvm/publish.ts @@ -31,6 +31,13 @@ import type { InlineUnionFields } from "./inline-unions.js"; import { LlvmUnsupportedError } from "./unsupported.js"; export interface PublishHost { + /** Chunk emitters only (emit-journal.ts): EmitJournal's allocated, built, + * opened, used and entry. */ + journalAllocated?: ((name: string, op: string, args: readonly unknown[]) => void) | undefined; + journalBuilt?: (() => void) | undefined; + journalOpened?: ((op: string) => void) | undefined; + journalUsed?: ((name: string, op: string, args: readonly unknown[]) => void) | undefined; + journalEntry?: ((table: string, key: string, value: unknown) => void) | undefined; declare(decl: string): void; /** An interned NUL-terminated constant. */ cstr(text: string): string; @@ -103,6 +110,14 @@ export class PublishWalkers { constructor(private readonly host: PublishHost) {} + /** Journal replay (emit-journal.ts): builds or finds the walker a chunk + * emitter recorded, and answers its name. */ + replayHelper(op: string, args: readonly unknown[]): string { + if (op === "publish.walker") + return this.walker(args[0] as IrType, args[1] as string, args[2] as SrcLoc)!; + return this.refusal(args[0] as string, args[1] as string); + } + /** The definitions emitted so far (module-level LLVM text). */ defs(): string[] { return this.defsOut; @@ -119,6 +134,7 @@ export class PublishWalkers { } const fn = this.walker(t, "the published value", loc); if (fn === null) return []; + this.host.journalUsed?.(fn, "publish.walker", [t, "the published value", loc]); this.host.declare(`declare void @scr_publish(ptr, ptr)`); return [` call void @scr_publish(ptr ${v}, ptr ${fn})`]; } @@ -156,8 +172,10 @@ export class PublishWalkers { if (known !== undefined) return known; const sym = `@sc_pub_${this.counter++}`; this.walkers.set(key, sym); + this.host.journalOpened?.("publish.walker"); const body = this.body(t, sym, path, loc); this.defsOut.push(...body, ``); + this.host.journalBuilt?.(); return sym; } @@ -172,6 +190,7 @@ export class PublishWalkers { if (known !== undefined) return known; const sym = `@sc_pub_${this.counter++}`; this.refusals.set(key, sym); + this.host.journalOpened?.("publish.refusal"); this.host.declare(`declare void @scr_pub_refuse_at(ptr, ptr, ptr)`); this.defsOut.push( `define internal void ${sym}(ptr %o, ptr %ctx) ${FN_ATTRS} { ; refuse ${llvmComment(detail)}`, @@ -181,6 +200,7 @@ export class PublishWalkers { `}`, ``, ); + this.host.journalBuilt?.(); return sym; } diff --git a/packages/compiler/src/backend/llvm/record-results.ts b/packages/compiler/src/backend/llvm/record-results.ts index 47a7983d2..db25016cd 100644 --- a/packages/compiler/src/backend/llvm/record-results.ts +++ b/packages/compiler/src/backend/llvm/record-results.ts @@ -12,20 +12,9 @@ import { import { dynamicFieldStores } from "../../ir/int32-slots.js"; import { everyExpr, everyStmtList, transformStmtList } from "../../ir/traverse.js"; import { InlineUnionFields } from "./inline-unions.js"; +import { RESULT_DST_ID, resultEntryName } from "../result-slots.js"; -/** Prefix of a function's result-slot entry: `%into.(...params, dst)` - * writes the function's fresh result object into `dst` and returns nothing. */ -export const RESULT_ENTRY_PREFIX = "%into."; - -export function resultEntryName(name: string): string { - return `${RESULT_ENTRY_PREFIX}${name}`; -} - -/** The local id of a result-slot entry's destination parameter. The - * caller passes zeroed frame storage, and on every path the entry stores - * each of its fields at most once, so a store into it is an - * initialization: there is no old value to release. */ -export const RESULT_DST_ID = "%into.dst"; +export { RESULT_DST_ID, RESULT_ENTRY_PREFIX, resultEntryName } from "../result-slots.js"; function distinctNames(names: readonly string[]): boolean { return new Set(names).size === names.length; diff --git a/packages/compiler/src/backend/llvm/reference-writes.ts b/packages/compiler/src/backend/llvm/reference-writes.ts index d31f14d12..6d1593a55 100644 --- a/packages/compiler/src/backend/llvm/reference-writes.ts +++ b/packages/compiler/src/backend/llvm/reference-writes.ts @@ -118,6 +118,7 @@ const INERT_LIB_CALLS = new Set([ "worker.parentPort", "worker.root", "cp.spawnSyncOpts", + "cp.spawnSyncEnv", ]); /** jsval conversions (ir/jsval.ts): no user code, no stores. */ diff --git a/packages/compiler/src/backend/llvm/running-extrema.test.ts b/packages/compiler/src/backend/llvm/running-extrema.test.ts new file mode 100644 index 000000000..4ce307c5c --- /dev/null +++ b/packages/compiler/src/backend/llvm/running-extrema.test.ts @@ -0,0 +1,97 @@ +import { expect, test } from "vitest"; +import { BOOL, F64, type IrExpr, type IrFunction, type IrStmt } from "../../ir/ir.js"; +import { runningExtremumUpdates } from "./running-extrema.js"; + +const loc = { file: "extrema.ts", start: 0, end: 0 }; +const ref = (localId: string): IrExpr => ({ kind: "varRef", localId, type: F64, loc }); +const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc }); +const assign = (localId: string, value: IrExpr): IrStmt => ({ + kind: "assign", + localId, + value, + loc, +}); +const decl = (localId: string): IrStmt => ({ kind: "varDecl", localId, init: num(0), loc }); +const compare = (op: "<" | ">", left: IrExpr, right: IrExpr): IrExpr => ({ + kind: "bin", + op, + left, + right, + type: BOOL, + loc, +}); +/** `if (v > max) max = v;` */ +const update = (): IrStmt & { kind: "if" } => ({ + kind: "if", + cond: compare(">", ref("v"), ref("max")), + then: [assign("max", ref("v"))], + else_: null, + loc, +}); +const loop = (body: IrStmt[]): IrStmt => ({ + kind: "while", + cond: compare(">", ref("n"), num(0)), + body, + loc, +}); +const fn = (body: IrStmt[], captures: string[] = []): IrFunction => ({ + name: "scan", + params: [], + locals: ["v", "max", "n"].map((id) => ({ id, name: id, type: F64, mutable: true })), + captures: captures.map((localId) => ({ localId, name: localId, type: F64 })), + returnType: F64, + body, + loc, +}); + +test("a loop's update of an accumulator declared outside it qualifies", () => { + const s = update(); + expect(runningExtremumUpdates(fn([decl("max"), loop([decl("v"), s])])).has(s)).toBe(true); +}); + +test("the local may be on either side of the comparison", () => { + const s = update(); + s.cond = compare("<", ref("max"), ref("v")); + expect(runningExtremumUpdates(fn([decl("max"), loop([s])])).has(s)).toBe(true); +}); + +test("a local declared in the loop is not an accumulator", () => { + const s = update(); + expect(runningExtremumUpdates(fn([loop([decl("max"), s])])).size).toBe(0); +}); + +test("another write in the loop disqualifies the update", () => { + const s = update(); + const body = [decl("max"), loop([assign("max", num(1)), s])]; + expect(runningExtremumUpdates(fn(body)).size).toBe(0); +}); + +test("the assigned value must be the compared expression", () => { + const s = update(); + s.then = [assign("max", num(5))]; + expect(runningExtremumUpdates(fn([decl("max"), loop([s])])).size).toBe(0); +}); + +test("outside a loop only a captured binding qualifies", () => { + const captured = update(); + expect(runningExtremumUpdates(fn([captured], ["max"])).has(captured)).toBe(true); + expect(runningExtremumUpdates(fn([update()])).size).toBe(0); +}); + +test("an else branch disqualifies the update", () => { + const s = update(); + s.else_ = [assign("v", num(0))]; + expect(runningExtremumUpdates(fn([decl("max"), loop([s])])).size).toBe(0); +}); + +test("an update nested in another branch does not qualify", () => { + const s = update(); + const guard: IrStmt = { + kind: "if", + cond: compare(">", ref("v"), num(0)), + then: [s], + else_: null, + loc, + }; + expect(runningExtremumUpdates(fn([decl("max"), loop([guard])])).size).toBe(0); +}); diff --git a/packages/compiler/src/backend/llvm/running-extrema.ts b/packages/compiler/src/backend/llvm/running-extrema.ts new file mode 100644 index 000000000..1ddea603d --- /dev/null +++ b/packages/compiler/src/backend/llvm/running-extrema.ts @@ -0,0 +1,110 @@ +// Running maximum and minimum updates, `if (v > max) { max = v; ... }`. +// With the accumulator in a register LLVM turns such an update into a +// select (`maxsd`, or a compare and mask), so every element waits on the +// previous one's. The update gets rarer as a scan goes on, so a branch +// predicts well; emitStmtBody gives these `if`s branch weights that say the +// update is rarely taken, and LLVM keeps the branch. A clamp of a value read +// in the same iteration is not an accumulator and keeps its select: its +// branch would go whichever way the data goes. An update nested in another +// branch keeps its select too: kept, its branch can stop LLVM from turning +// the enclosing branch into selects, and that branch may follow the data. + +import type { IrExpr, IrFunction, IrStmt } from "../../ir/ir.js"; +import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; + +type Loop = IrStmt & { kind: "for" | "while" | "doWhile" | "forOf" }; + +const RELATIONAL = new Set(["<", "<=", ">", ">="]); + +/** The `if` statements of `fn` that update a running maximum or minimum: + * no `else`, a numeric `<`, `<=`, `>` or + * `>=` between a local and another expression, and a then-block that + * assigns that expression to the local. The local must carry across + * iterations: inside a loop the `if` is a statement of the innermost loop's + * body, the local is declared outside that loop, and the loop writes it only + * in this `if`; outside any loop the `if` is a statement of the function + * body and the local is a binding the function captures and writes only in + * this `if` (a callback that a loop elsewhere calls). */ +export function runningExtremumUpdates(fn: IrFunction): Set { + const found = new Set(); + const captured = new Set((fn.captures ?? []).map((c) => c.localId)); + const loops: Loop[] = []; + const candidates: { s: IrStmt; local: string; loop: Loop | undefined }[] = []; + const expr = (e: IrExpr): boolean => everyExprChild(e, expr, stmt); + const stmt = (s: IrStmt): boolean => { + if (s.kind === "if") { + const local = updatedExtremum(s); + if (local !== undefined) candidates.push({ s, local, loop: loops[loops.length - 1] }); + } + const loop = + s.kind === "for" || s.kind === "while" || s.kind === "doWhile" || s.kind === "forOf"; + if (loop) loops.push(s); + everyStmtChild(s, expr, stmt); + if (loop) loops.pop(); + return true; + }; + for (const s of fn.body) stmt(s); + for (const { s, local, loop } of candidates) { + const scope = loop ?? (captured.has(local) ? fn.body : undefined); + const body = loop ? loop.body : fn.body; + if (scope !== undefined && body.includes(s) && writes(scope, local) === 1) found.add(s); + } + return found; +} + +/** The local an `if` updates to a new maximum or minimum, if it is one. */ +function updatedExtremum(s: IrStmt & { kind: "if" }): string | undefined { + if (s.else_ !== null && s.else_.length > 0) return undefined; + const c = s.cond; + if (c.kind !== "bin" || !RELATIONAL.has(c.op)) return undefined; + if (c.left.type.kind !== "f64" || c.right.type.kind !== "f64") return undefined; + for (const [local, other] of [ + [c.left, c.right], + [c.right, c.left], + ]) { + if (local!.kind !== "varRef") continue; + const id = local!.localId; + const assigns = s.then.some( + (x) => x.kind === "assign" && x.localId === id && sameExpr(x.value, other!), + ); + if (assigns) return id; + } + return undefined; +} + +/** Declarations and writes of `local` under `scope`. A declaration counts + * as a write too, so a local declared inside a loop does not qualify. */ +function writes(scope: IrStmt | IrStmt[], local: string): number { + let n = 0; + const expr = (e: IrExpr): boolean => { + if ((e.kind === "assignExpr" || e.kind === "incDec") && e.localId === local) n++; + return everyExprChild(e, expr, stmt); + }; + const stmt = (s: IrStmt): boolean => { + if ((s.kind === "assign" || s.kind === "varDecl" || s.kind === "forOf") && s.localId === local) + n++; + else if (s.kind === "tryCatch" && s.catchLocalId === local) n++; + return everyStmtChild(s, expr, stmt); + }; + if (Array.isArray(scope)) for (const s of scope) stmt(s); + else stmt(scope); + return n; +} + +/** Structural equality of two small expressions (`v`, `a[i]`), ignoring + * source locations and types; larger expressions do not compare equal. */ +function sameExpr(a: IrExpr, b: IrExpr): boolean { + let budget = 64; + const same = (x: unknown, y: unknown): boolean => { + if (--budget < 0) return false; + if (typeof x !== "object" || x === null || typeof y !== "object" || y === null) return x === y; + if (Array.isArray(x) !== Array.isArray(y)) return false; + const keys = Object.keys(x).filter((k) => k !== "loc" && k !== "type"); + if (keys.length !== Object.keys(y).filter((k) => k !== "loc" && k !== "type").length) + return false; + return keys.every((k) => + same((x as Record)[k], (y as Record)[k]), + ); + }; + return same(a, b); +} diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index 6de92f667..382d2aa34 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -68,9 +68,22 @@ const CYC_MAX_TYPES = 32768; /** What the tables need from the emitter: the extern-declaration ledger * and the module-wide cycle/shape indexes. */ export interface ShapeHost { + /** Chunk emitters only (emit-journal.ts): EmitJournal's allocated, built, + * opened, used and entry. */ + journalAllocated?: ((name: string, op: string, args: readonly unknown[]) => void) | undefined; + journalBuilt?: (() => void) | undefined; + journalOpened?: ((op: string) => void) | undefined; + journalUsed?: ((name: string, op: string, args: readonly unknown[]) => void) | undefined; + journalEntry?: ((table: string, key: string, value: unknown) => void) | undefined; /** The hierarchy root of a class (a program class or a runtime base), * or undefined for classes outside the class graph. */ classRootOf?(className: string): string | undefined; + /** Whether arrays of `elem` hold 32-bit references to compact class + * instances (SCR_ELEM_CREF; LlEmitter.compactArrayElem). */ + compactArrayElem?(elem: IrType): boolean; + /** Whether compact class instances live in the program-lifetime region + * (LlEmitter.compactRegion). */ + compactRegion?(): boolean; /** Byte offset from an object pointer back to ScrCycHdr.buf. */ readonly cycleBufOffset: number; declare(decl: string): void; @@ -386,6 +399,7 @@ function nullableRetainSym(host: ShapeHost, arm: IrType): string { const sym = `@sc_nretain_${arm.kind}`; if (!host.nullableUnions.retainWrappers.has(sym)) host.nullableUnions.retainWrappers.set(sym, retainSym(host, arm)); + host.journalEntry?.("retainWrapper", sym, host.nullableUnions.retainWrappers.get(sym)); return sym; } @@ -564,14 +578,16 @@ export function inlineRcFamily(host: ShapeHost, stem: string, op: RcOp): InlineR } /** Which RC fast paths an emission inlines. `speed` mirrors every family - * with a fast path. `release` mirrors only the string retain and the dyn - * retain and release: the operations that dominate dynamic-value and string - * code, at almost no size cost. String releases stay calls in `release` - * because string-heavy programs have many release sites. */ + * with a fast path. `release` mirrors only the string and Map retains and + * the dyn retain and release: the operations that dominate dynamic-value, + * string and Map code (a Map read from an array or another Map), at almost + * no size cost. String releases stay calls in `release` because + * string-heavy programs have many release sites. */ export type InlineRcMode = "release" | "speed"; const RELEASE_RC_OPS = new Set([ "str:retain", + "map:retain", "dyn:retain", "dyn:release", "js:retain", @@ -1076,18 +1092,30 @@ export function arrNewCall( /** The %ScrArrOps descriptor (retain, release, trace, registry id) of a REF * element type, defined once per module and shared by all its arrays. It is - * writable: the runtime stores its registry id on the first array. */ + * writable: the runtime stores its registry id on the first array. Its tail + * padding is the runtime's flags word (0); arrays of compact class + * references (ShapeHost.compactArrayElem) spell the word out as + * SCR_ARR_OPS_COMPACT. */ export function arrOps(host: ShapeHost, elem: IrType): string { const v = vAdapters(host, elem); const trace = traceArg(host, elem); const part = (sym: string): string => sym.replace(/^@/, "").replace(/[^A-Za-z0-9_.]/g, "_"); const name = `@sc_arrops.${part(v.retain)}.${part(v.release)}.${part(trace)}`; + if (host.compactArrayElem?.(elem) === true) { + host.declare( + `${name}.c = internal global { ptr, ptr, ptr, i32, i32 } { ptr ${v.retain}, ptr ${v.release}, ptr ${trace}, i32 0, i32 ${SCR_ARR_OPS_COMPACT} }`, + ); + return `${name}.c`; + } host.declare( `${name} = internal global %ScrArrOps { ptr ${v.retain}, ptr ${v.release}, ptr ${trace}, i32 0 }`, ); return name; } +/** scr_array.h SCR_ARR_OPS_COMPACT. */ +const SCR_ARR_OPS_COMPACT = 1; + /* ── capture boxes ────────────────────────────────────────────────────── */ /** Box construction call text (boxNewC's dispatch): plain-kind boxes for diff --git a/packages/compiler/src/backend/llvm/walkers.ts b/packages/compiler/src/backend/llvm/walkers.ts index 46dbc86e1..044cb79ae 100644 --- a/packages/compiler/src/backend/llvm/walkers.ts +++ b/packages/compiler/src/backend/llvm/walkers.ts @@ -62,6 +62,21 @@ export class LlWalkers { constructor(private readonly host: WalkerHost) {} + /** Journal replay (emit-journal.ts): builds or finds the helper a chunk + * emitter recorded, and answers its name. */ + replayHelper(op: string, args: readonly unknown[]): string { + switch (op) { + case "walkers.jsonWriteHelper": + return this.jsonWriteHelper(args[0] as IrType); + case "walkers.recordFieldWriter": + return this.recordFieldWriter(args[0] as string); + case "walkers.unionJoinHelper": + return this.unionJoinHelper(args[0] as string); + default: + throw new InternalCompilerError(`llvm emitter bug: no walker replay ${op}`); + } + } + private get S(): "i32" | "i64" { return this.host.sizeType; } @@ -191,6 +206,7 @@ export class LlWalkers { if (existing) return existing; const name = `sc_jw_${this.jsonWriters.size}`; this.jsonWriters.set(key, name); + this.host.journalAllocated?.(name, "walkers.jsonWriteHelper", [t]); const B = new BlockBuilder(); switch (t.kind) { case "f64": @@ -244,6 +260,7 @@ export class LlWalkers { `}`, ``, ); + this.host.journalBuilt?.(); return name; } @@ -457,6 +474,7 @@ export class LlWalkers { throw new InternalCompilerError(`llvm emitter bug: field writer of unknown shape ${shapeId}`); const name = `sc_jwf_${this.recordFieldWriters.size}`; this.recordFieldWriters.set(shapeId, name); + this.host.journalAllocated?.(name, "walkers.recordFieldWriter", [shapeId]); const fieldBase = this.host.nullableUnions.fieldBase(shapeId); const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + fieldBase])); const byName = new Map(shape.fields.map((f) => [f.name, f])); @@ -508,6 +526,7 @@ export class LlWalkers { `}`, ``, ); + this.host.journalBuilt?.(); return name; } @@ -852,6 +871,7 @@ export class LlWalkers { if (!def) throw new InternalCompilerError(`llvm emitter bug: join of unknown union ${unionId}`); const name = `sc_uj_${this.unionJoinFns.size}`; this.unionJoinFns.set(unionId, name); + this.host.journalAllocated?.(name, "walkers.unionJoinHelper", [unionId]); const host = this.host; host.declare(`declare void @scr_jb_init(ptr)`); @@ -946,6 +966,7 @@ export class LlWalkers { `}`, ``, ); + this.host.journalBuilt?.(); return name; } } diff --git a/packages/compiler/src/backend/llvm/wrapping-sums.ts b/packages/compiler/src/backend/llvm/wrapping-sums.ts index b00b61cbb..c736fcf72 100644 --- a/packages/compiler/src/backend/llvm/wrapping-sums.ts +++ b/packages/compiler/src/backend/llvm/wrapping-sums.ts @@ -1,4 +1,5 @@ -import type { IrExpr } from "../../ir/ir.js"; +import type { IrExpr, IrFunction, IrStmt } from "../../ir/ir.js"; +import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; import { integerArithmeticRange, type IntegerRange } from "../../ir/integer-ranges.js"; import type { LlValue, LlvmEmitterContext } from "./expr-context.js"; import type { LlInteger } from "./integer-values.js"; @@ -47,6 +48,7 @@ export function integerOrNaNRange(host: LlvmEmitterContext, e: IrExpr): IntegerR return ELEMENT_RANGES.i32!; // A UTF-16 code unit, or NaN past either end. if (e.kind === "strIntrinsic" && e.method === "charCodeAt") return { min: 0, max: 65535 }; + if (e.kind === "varRef") return host.integerOrNaNLocals.get(e.localId) ?? null; if (e.kind === "bin" && (e.op === "+" || e.op === "-")) { const left = integerOrNaNRange(host, e.left); const right = left && integerOrNaNRange(host, e.right); @@ -55,6 +57,34 @@ export function integerOrNaNRange(host: LlvmEmitterContext, e: IrExpr): IntegerR return null; } +/** An index that is an exact integer or NaN (`integerOrNaNRange`): the integer as an i64, and an + * i1 that is false when the index is NaN. An index check then needs no round trip through a double: + * the integer is the value's ToUint32 view where the range proves it holds the whole value, or one + * conversion, which is exact for a safe integer and frozen, since the conversion of NaN is poison. + * Null for any other index. */ +export function integerOrNaNIndex( + host: LlvmEmitterContext, + value: LlValue, + expr: IrExpr | undefined, +): { wide: string; ordered: string } | null { + const range = expr ? integerOrNaNRange(host, expr) : null; + if (!range) return null; + const B = host.B; + const ordered = B.tmp(); + const wide = B.tmp(); + B.line(`${ordered} = fcmp ord double ${value.name}, 0.0`); + if (value.uint32 !== undefined && range.min >= 0 && range.max <= 4294967295) { + B.line(`${wide} = zext i32 ${value.uint32} to i64`); + } else if (value.uint32 !== undefined && range.min >= -2147483648 && range.max <= 2147483647) { + B.line(`${wide} = sext i32 ${value.uint32} to i64`); + } else { + const raw = B.tmp(); + B.line(`${raw} = fptosi double ${value.name} to i64`); + B.line(`${wide} = freeze i64 ${raw}`); + } + return { wide, ordered }; +} + const ELEMENT_RANGES: Partial> = { u8: { min: 0, max: 255 }, u8c: { min: 0, max: 255 }, @@ -131,3 +161,85 @@ export function emitWrappingSum( B.line(`${out} = select i1 ${valid}, i32 ${sum}, i32 0`); return out; } + +/** The number locals of `fn` that only hold exact integers (never -0) or NaN, with the range of + * those integers: locals declared once with an initializer, neither parameters nor captured or + * boxed, whose initializer and every later write `integerOrNaNRange` covers. A local joins once + * the values it is written depend only on locals that have joined, so `x = x + 1` keeps `x` out: + * its values have no bound. Uses the function's `integerRanges` and `int32Elements`. */ +export function findIntegerOrNaNLocals( + host: LlvmEmitterContext, + fn: IrFunction, +): ReadonlyMap { + const found = new Map(); + if (fn.async || fn.generator) return found; + const excluded = new Set(); + for (const param of fn.params) excluded.add(param.localId); + for (const capture of fn.captures ?? []) excluded.add(capture.localId); + for (const capture of fn.classCaptures ?? []) excluded.add(capture.localId); + const candidates = new Set(); + for (const local of fn.locals) + if (local.type.kind === "f64" && !local.boxed && !local.tdz && !excluded.has(local.id)) + candidates.add(local.id); + if (candidates.size === 0) return found; + const declared = new Set(); + const writes = new Map(); + const drop = (id: string): void => { + candidates.delete(id); + writes.delete(id); + }; + const write = (id: string, value: IrExpr): void => { + if (!candidates.has(id)) return; + const list = writes.get(id); + if (list) list.push(value); + else writes.set(id, [value]); + }; + function walkExpr(e: IrExpr): boolean { + if (e.kind === "assignExpr") write(e.localId, e.value); + else if (e.kind === "incDec") drop(e.localId); + return everyExprChild(e, walkExpr, walkStmt); + } + function walkStmt(s: IrStmt): boolean { + if (s.kind === "assign") write(s.localId, s.value); + else if (s.kind === "varDecl") { + if (s.init === null || declared.has(s.localId)) drop(s.localId); + else { + declared.add(s.localId); + write(s.localId, s.init); + } + } else if (s.kind === "forOf") drop(s.localId); + else if (s.kind === "tryCatch" && s.catchLocalId) drop(s.catchLocalId); + return everyStmtChild(s, walkExpr, walkStmt); + } + for (const s of fn.body) walkStmt(s); + // `integerOrNaNRange` reads the joined locals while the next ones are tested. + const previous = host.integerOrNaNLocals; + host.integerOrNaNLocals = found; + try { + let joined = true; + while (joined) { + joined = false; + for (const [id, values] of writes) { + if (found.has(id) || !declared.has(id)) continue; + let range: IntegerRange | null = null; + for (const value of values) { + const next = integerOrNaNRange(host, value); + if (next === null) { + range = null; + break; + } + range = + range === null + ? next + : { min: Math.min(range.min, next.min), max: Math.max(range.max, next.max) }; + } + if (range === null) continue; + found.set(id, range); + joined = true; + } + } + } finally { + host.integerOrNaNLocals = previous; + } + return found; +} diff --git a/packages/compiler/src/backend/may-throw.ts b/packages/compiler/src/backend/may-throw.ts index b72dc545d..a04cce856 100644 --- a/packages/compiler/src/backend/may-throw.ts +++ b/packages/compiler/src/backend/may-throw.ts @@ -35,7 +35,12 @@ export type MayThrowReason = * and a generator's `.return()` unwind through `catch`. */ export function computeMayThrow( mod: IrModule, - options: { stackChecks?: boolean; explain?: boolean } = {}, + options: { + stackChecks?: boolean; + explain?: boolean; + /** computePublishedTypes(mod), when the caller already has it. */ + published?: ReturnType; + } = {}, ): { fns: Set; /** With `explain`: the reason each may-throw function was marked. */ @@ -50,7 +55,8 @@ export function computeMayThrow( const terminations = mod.workers === true ? workerTerminationPolls(mod) : null; const stackChecks = options.stackChecks === true ? stackCheckedFunctions(mod) : null; // Stores that can reach a published object throw the frozen TypeError. - const published = computePublishedTypes(mod); + const published = + options.published !== undefined ? options.published : computePublishedTypes(mod); interface Facts { throws: boolean; callees: string[]; diff --git a/packages/compiler/src/backend/result-slots.ts b/packages/compiler/src/backend/result-slots.ts new file mode 100644 index 000000000..608877f06 --- /dev/null +++ b/packages/compiler/src/backend/result-slots.ts @@ -0,0 +1,29 @@ +import type { IrStmt } from "../ir/ir.js"; + +/** Prefix of a function's result-slot entry: `%into.(...params, dst)` + * writes the function's fresh result object into `dst` and returns nothing + * (llvm/record-results.ts). */ +export const RESULT_ENTRY_PREFIX = "%into."; + +export function resultEntryName(name: string): string { + return `${RESULT_ENTRY_PREFIX}${name}`; +} + +/** The local id of a result-slot entry's destination parameter. The + * caller passes zeroed frame storage, and on every path the entry stores + * each of its fields at most once, so a store into it is an + * initialization: there is no old value to release. */ +export const RESULT_DST_ID = "%into.dst"; + +/** Whether `s`, a statement of the function `fnName`, is a result-slot + * entry's store into its destination. The destination is frame storage of + * a caller that only projects the value (llvm/stack-objects.ts), so the + * store initializes a field of an object no other object can point to. */ +export function resultSlotStore(fnName: string, s: IrStmt): boolean { + return ( + s.kind === "recordSet" && + s.obj.kind === "varRef" && + s.obj.localId === RESULT_DST_ID && + fnName.startsWith(RESULT_ENTRY_PREFIX) + ); +} diff --git a/packages/compiler/src/backend/runtime-abi.ts b/packages/compiler/src/backend/runtime-abi.ts index 3a41d9176..a616dbec2 100644 --- a/packages/compiler/src/backend/runtime-abi.ts +++ b/packages/compiler/src/backend/runtime-abi.ts @@ -2,8 +2,8 @@ * of the link contract: a program object keeps this symbol undefined and the * matching runtime defines it, so incompatible manual links fail before the * program can start. */ -export const RUNTIME_ABI_VERSION = 27 as const; -export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v27" as const; +export const RUNTIME_ABI_VERSION = 28 as const; +export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v28" 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-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts index af61e5229..c2536ede8 100644 --- a/packages/compiler/src/backend/runtime-pack-targets.test.ts +++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts @@ -52,7 +52,7 @@ async function fixture(target: NativeTargetSpec, workers = false) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" }, + runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" }, flavors: { diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts index d09509911..33b1ca38e 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -127,7 +127,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" }, + runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" }, compiler: { command: "clang", identity: "fixture clang", diff --git a/packages/compiler/src/backend/stack-observations.ts b/packages/compiler/src/backend/stack-observations.ts index fa67c071f..c448897f1 100644 --- a/packages/compiler/src/backend/stack-observations.ts +++ b/packages/compiler/src/backend/stack-observations.ts @@ -29,35 +29,50 @@ const NON_STACK_KEY_TYPES: ReadonlySet = new Set(["f64", "bigint * containers or calls; a key the program assembles from other strings or * from data it reads keeps the static answer. */ class StackKeys { - private readonly spelled: boolean; - private readonly calls = new Map(); - private readonly named: Set; + private spelled: boolean | undefined; + private census: + | { calls: Map; named: Set } + | undefined; private readonly inits = new Map>(); /** Settled answers by function and local: true where a search reached a * key that can be "stack", false where a search that found none passed. */ private readonly answers = new Map(); - constructor(mod: IrModule) { - this.spelled = spellsStack(mod); - this.named = new Set(moduleFunctionReferences(mod)); + // The module walks run on the first question that needs them: a program + // without a dynamic key read asks none. + constructor(private readonly mod: IrModule) {} + + private spellsStack(): boolean { + return (this.spelled ??= spellsStack(this.mod)); + } + + private callCensus(): { + calls: Map; + named: Set; + } { + if (this.census !== undefined) return this.census; + const mod = this.mod; + const calls = new Map(); + const named = new Set(moduleFunctionReferences(mod)); // Class methods, accessors and constructors are called through dispatch. const classNames = new Set((mod.classes ?? []).map((cls) => cls.name)); for (const fn of mod.functions) { - if (isClassMember(fn.name, classNames)) this.named.add(fn.name); + if (isClassMember(fn.name, classNames)) named.add(fn.name); everyStmtList(fn.body, { stmt: () => true, expr: (expr) => { if (expr.kind === "call") { - const sites = this.calls.get(expr.callee) ?? []; + const sites = calls.get(expr.callee) ?? []; sites.push({ caller: fn, args: expr.args }); - this.calls.set(expr.callee, sites); + calls.set(expr.callee, sites); } const referenced = exprFunctionReference(expr); - if (referenced !== undefined) this.named.add(referenced); + if (referenced !== undefined) named.add(referenced); return true; }, }); } + return (this.census = { calls, named }); } mayBeStack(fn: IrFunction, key: IrExpr): boolean { @@ -71,7 +86,7 @@ class StackKeys { private keyMayBeStack(fn: IrFunction, key: IrExpr, visited: Set): boolean { if (key.kind === "strLit") return key.value === "stack"; - if (!this.spelled) return false; + if (!this.spellsStack()) return false; if (NON_STACK_KEY_TYPES.has(key.type.kind)) return false; if (key.kind === "toString" && NON_STACK_KEY_TYPES.has(key.operand.type.kind)) return false; if (key.kind === "dynFrom" && NON_STACK_KEY_TYPES.has(key.value.type.kind)) return false; @@ -93,8 +108,9 @@ class StackKeys { if (local === undefined || local.mutable) return true; const index = fn.params.findIndex((param) => param.localId === localId); if (index !== -1) { - const sites = this.calls.get(fn.name); - if (this.named.has(fn.name) || sites === undefined) return true; + const { calls, named } = this.callCensus(); + const sites = calls.get(fn.name); + if (named.has(fn.name) || sites === undefined) return true; return sites.some( (site) => site.args.length !== fn.params.length || diff --git a/packages/compiler/src/backend/target-diagnostics.ts b/packages/compiler/src/backend/target-diagnostics.ts index 3e97d0c10..236ce12cf 100644 --- a/packages/compiler/src/backend/target-diagnostics.ts +++ b/packages/compiler/src/backend/target-diagnostics.ts @@ -3,6 +3,7 @@ import type { LlvmUnsupportedError } from "./llvm/emitter.js"; import type { ScrDiagnostic } from "../diagnostics/diagnostic.js"; import { type IrModule, type SrcLoc } from "../ir/ir.js"; import { + DH_LIB_FNS, PKI_LIB_FNS, moduleUsesFetch, moduleUsesWebCryptoAsymmetric, @@ -232,9 +233,12 @@ export function moduleWasiUnavailableSurface( }; return false; } - if (node.fn === "crypto.dhNew" || node.fn === "crypto.dhGroupNew") { + if (DH_LIB_FNS.has(node.fn)) { found = { - surface: "DiffieHellman (the wasm32-wasi runtime does not include the mbedTLS library)", + surface: + node.fn === "crypto.dhNew" || node.fn === "crypto.dhGroupNew" + ? "DiffieHellman (the wasm32-wasi runtime does not include the mbedTLS library)" + : "checkPrime and generatePrime (the wasm32-wasi runtime does not include the mbedTLS library)", loc, }; return false; diff --git a/packages/compiler/src/coverage/report.ts b/packages/compiler/src/coverage/report.ts index 37f77f4c6..79017c97a 100644 --- a/packages/compiler/src/coverage/report.ts +++ b/packages/compiler/src/coverage/report.ts @@ -318,12 +318,23 @@ export function renderCoverage(input: CoverageInput, opts: CoverageRenderOptions // statements the module carries): they compile, but executing one // throws its SC-coded error — the report lists them so a "compiling" // JS program never hides its dynamic gaps. + // Fences in a module's top-level code run when the module initializes: + // they are listed first, as sites that run at startup. const fences = input.runtimeFences ?? []; - if (fences.length > 0) { + const startup = fences.filter((d) => d.startup === true); + const later = fences.filter((d) => d.startup !== true); + if (startup.length > 0) { out.push( - ` ${c(YELLOW, "deferred to runtime")} ${fences.length} site${fences.length === 1 ? "" : "s"} ${c(DIM, "(JS statements that throw their fence if executed)")}`, + ` ${c(YELLOW, "runs at startup")} ${startup.length} site${startup.length === 1 ? "" : "s"} ${c(DIM, "(JS statements in module top-level code, which runs when the module loads)")}`, ); - renderRows(groupBlockers(fences, "deferred"), YELLOW); + renderRows(groupBlockers(startup, "deferred"), YELLOW); + out.push(""); + } + if (later.length > 0) { + out.push( + ` ${c(YELLOW, "deferred to runtime")} ${later.length} site${later.length === 1 ? "" : "s"} ${c(DIM, "(JS statements that throw their fence if executed)")}`, + ); + renderRows(groupBlockers(later, "deferred"), YELLOW); out.push(""); } diff --git a/packages/compiler/src/diagnostics/diagnostic.ts b/packages/compiler/src/diagnostics/diagnostic.ts index 17cb2efc0..625ee93af 100644 --- a/packages/compiler/src/diagnostics/diagnostic.ts +++ b/packages/compiler/src/diagnostics/diagnostic.ts @@ -92,6 +92,10 @@ export interface ScrDiagnostic { * the error may not reproduce under the project's tsc. Machine-readable * output reports these under the "environment" category. */ typeWorld?: boolean; + /** A runtime fence in a module's top-level code, outside every function + * and class member body: it throws when the module initializes, which is + * program startup for a module the entry imports. */ + startup?: true; } /** SC3002–SC3004 — native-helper target, installation, and execution diff --git a/packages/compiler/src/diagnostics/envelope.ts b/packages/compiler/src/diagnostics/envelope.ts index 59c285ead..b4de8db77 100644 --- a/packages/compiler/src/diagnostics/envelope.ts +++ b/packages/compiler/src/diagnostics/envelope.ts @@ -67,6 +67,9 @@ export interface LocatedDiagnostic extends SourcePosition { hint: string; /** The id of the root-cause group this site belongs to. */ group: string; + /** Deferred scope only: the statement is module top-level code, so it + * runs when its module loads. */ + startup?: true; } export interface DiagnosticGroup { @@ -290,6 +293,7 @@ export function locateDiagnostic( message: diag.message, hint: hintOf(diag), group: groupId(diag, scope), + ...(scope === "deferred" && diag.startup === true ? { startup: true as const } : {}), }; } diff --git a/packages/compiler/src/frontend/lowering/builtins/child-process.ts b/packages/compiler/src/frontend/lowering/builtins/child-process.ts index ea2dd379d..9096316a3 100644 --- a/packages/compiler/src/frontend/lowering/builtins/child-process.ts +++ b/packages/compiler/src/frontend/lowering/builtins/child-process.ts @@ -115,6 +115,8 @@ export function lowerSpawnSyncCall(lowerer: Lowerer, expr: ts.CallExpression, lo // "pipe"`) — accepted when its TYPE proves every arm is a supported // literal; the runtime maps the value to the modes at the call. let stdioStr: IrExpr | null = null; + // The `env` option's [k,v,...] pairs, which replace the child's environment. + let envPairs: IrExpr | null = null; let plain = true; // no behavior-changing option: the historical libCall if (optsNode) { @@ -227,11 +229,16 @@ export function lowerSpawnSyncCall(lowerer: Lowerer, expr: ts.CallExpression, lo case "windowsHide": lowerer.lowerExpr(m.value); // Node no-op on POSIX break; + case "env": + // A REPLACEMENT environment, as for spawn and execFileSync. + envPairs = recordToEnvPairs(lowerer, m.value); + plain = false; + break; default: lowerer.noLowering( `spawnSync option '${m.name}'`, p, - "encoding, timeout, killSignal, stdio, and windowsHide are the supported options", + "encoding, timeout, killSignal, stdio, env, and windowsHide are the supported options", ); } } @@ -239,6 +246,13 @@ export function lowerSpawnSyncCall(lowerer: Lowerer, expr: ts.CallExpression, lo if (plain) { return { kind: "libCall", fn: "cp.spawnSync", args: [cmd, argv], type: SPAWNRES_T, loc }; } + if (stdioStr !== null && envPairs !== null) { + lowerer.noLowering( + "spawnSync with a runtime stdio value and env", + optsNode!, + 'pass stdio as a "pipe"/"ignore"/"inherit" literal when env is set', + ); + } if (stdioStr !== null) { return { kind: "libCall", @@ -250,7 +264,7 @@ export function lowerSpawnSyncCall(lowerer: Lowerer, expr: ts.CallExpression, lo } return { kind: "libCall", - fn: "cp.spawnSyncOpts", + fn: envPairs !== null ? "cp.spawnSyncEnv" : "cp.spawnSyncOpts", args: [ cmd, argv, @@ -259,6 +273,7 @@ export function lowerSpawnSyncCall(lowerer: Lowerer, expr: ts.CallExpression, lo numLit(inMode, loc), numLit(outMode, loc), numLit(errMode, loc), + ...(envPairs !== null ? [envPairs] : []), ], type: SPAWNRES_T, loc, diff --git a/packages/compiler/src/frontend/lowering/builtins/crypto.ts b/packages/compiler/src/frontend/lowering/builtins/crypto.ts index 7a1791197..47e66f9b2 100644 --- a/packages/compiler/src/frontend/lowering/builtins/crypto.ts +++ b/packages/compiler/src/frontend/lowering/builtins/crypto.ts @@ -702,6 +702,8 @@ export function lowerCryptoModuleCall( } const pki = lowerPkiCall(lowerer, expr, bi.member, args, loc); if (pki) return pki; + const prime = lowerPrimeCall(lowerer, expr, bi.member, args, loc); + if (prime) return prime; if (bi.member === "createSecretKey") { if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement)) lowerer.noLowering( @@ -867,11 +869,29 @@ const IMPLEMENTED_CIPHERS = new Set([ "id-aes192-gcm", "id-aes256-gcm", "chacha20-poly1305", + ...["128", "192", "256"].flatMap((bits) => + ["ecb", "cfb", "cfb8", "cfb1", "ofb"].map((mode) => `aes-${bits}-${mode}`), + ), + "des-ede3-cbc", + "des3", + "des-ede3", + "des-ede3-ecb", + "des-ede3-cfb", + "des-ede3-cfb8", + "des-ede3-cfb1", + "des-ede3-ofb", + "des-ede-cbc", + "des-ede", + "des-ede-ecb", + "des-ede-cfb", + "des-ede-ofb", + "chacha20", ]); const SUPPORTED_CIPHERS_HINT = - "the supported ciphers are aes-128-cbc, aes-192-cbc, aes-256-cbc, aes-128-ctr, aes-192-ctr, " + - "aes-256-ctr, aes-128-gcm, aes-192-gcm, aes-256-gcm, and chacha20-poly1305"; + "the supported ciphers are aes-128, aes-192 and aes-256 in cbc, ctr, gcm, ecb, cfb, cfb8, cfb1 " + + "and ofb modes, des-ede3 in cbc, ecb, cfb, cfb8, cfb1 and ofb modes, des-ede in cbc, ecb, cfb " + + "and ofb modes, chacha20, and chacha20-poly1305"; /** A literal cipher name that Node knows but the runtime does not * implement is refused at compile time. Unknown literal names keep Node's @@ -1346,6 +1366,59 @@ export function isWebCryptoReceiver(lowerer: Lowerer, receiver: ts.Expression): return false; } +/** Node calls checkPrime's and generatePrime's callbacks with undefined as + * the error, where @types/node declares Error | null: whether this + * callback's error parameter can hold undefined. One that cannot receives + * null instead. */ +function takesUndefinedError(lowerer: Lowerer, callback: ts.Expression): boolean { + const type = lowerer.irTypeOf(callback); + if (type.kind !== "func") return true; + const error = type.params[0]; + if (error === undefined || error.kind === "dyn" || error.kind === "undefinedT") return true; + if (error.kind !== "union") return false; + return !!lowerer.unions.get(error.unionId)?.arms.some((arm) => arm.kind === "undefinedT"); +} + +/** checkPrime, checkPrimeSync, generatePrime and generatePrimeSync + * (scr_dh.c): checked-dynamic arguments, which the runtime checks as Node + * does (a function in the options position is the callback), and the + * sync forms' boolean, ArrayBuffer or bigint converted to the call site's + * type. Null for other members. */ +function lowerPrimeCall( + lowerer: Lowerer, + expr: ts.CallExpression, + member: string, + args: readonly ts.Expression[], + loc: SrcLoc, +): IrExpr | null { + const forms: Readonly> = { + checkPrimeSync: ["crypto.checkPrimeSync", 1, 2, "checkPrimeSync(candidate[, options])"], + checkPrime: ["crypto.checkPrimeCb", 1, 3, "checkPrime(candidate[, options], callback)"], + generatePrimeSync: ["crypto.generatePrimeSync", 1, 2, "generatePrimeSync(size[, options])"], + generatePrime: ["crypto.generatePrimeCb", 1, 3, "generatePrime(size[, options], callback)"], + }; + const form = forms[member]; + if (!form) return null; + const [fn, min, max, supported] = form; + if (args.length < min || args.length > max || args.some(ts.isSpreadElement)) + lowerer.noLowering( + `crypto.${member} with ${args.length} arguments`, + expr, + `${supported} is supported`, + ); + const values = Array.from({ length: max }, (_, i) => dynArg(lowerer, args[i], loc)); + if (fn === "crypto.checkPrimeCb" || fn === "crypto.generatePrimeCb") { + const callback = args[args.length - 1]; + const nullError = callback !== undefined && !takesUndefinedError(lowerer, callback); + values.push({ kind: "boolLit", value: nullError, type: BOOL, loc }); + return { kind: "libCall", fn, args: values, type: VOID, loc }; + } + const call: IrExpr = { kind: "libCall", fn, args: values, type: DYN, loc }; + const result = lowerer.mapTypeOf(lowerer.typeOf(expr)); + if (result === null || result.kind === "dyn") return call; + return lowerer.coerceInto(expr, call, result); +} + /** The asymmetric-key and signature members (scr_pki.c): checked-dynamic * arguments, and the result converted to the call site's type * (KeyObject, Sign and Verify are checked-dynamic handles; key pairs, diff --git a/packages/compiler/src/frontend/lowering/class-construction.ts b/packages/compiler/src/frontend/lowering/class-construction.ts index 66ec547bc..7ee53d04e 100644 --- a/packages/compiler/src/frontend/lowering/class-construction.ts +++ b/packages/compiler/src/frontend/lowering/class-construction.ts @@ -25,16 +25,21 @@ const PREFIX = "%class.construct:"; const PACKED = `${PREFIX}packed`; const INSTANCEOF = "%class.instanceof:dynamic"; +/** `value instanceof callee` for an untyped constructor value, through one + * shared helper. `refusal` is the site's message for a runtime + * constructor with no instance test. */ export function checkedClassInstanceOf( lowerer: Lowerer, value: IrExpr, callee: IrExpr, + refusal: string, loc: SrcLoc, ): IrExpr { if (!lowerer.liftedFns.some((fn) => fn.name === INSTANCEOF)) { const params = [ { localId: "value", name: "value", type: DYN }, { localId: "callee", name: "callee", type: DYN }, + { localId: "refusal", name: "refusal", type: STRING }, ]; lowerer.liftedFns.push({ name: INSTANCEOF, @@ -47,7 +52,11 @@ export function checkedClassInstanceOf( value: { kind: "libCall", fn: "bytes.instanceOf", - args: [varRef("value", DYN, loc), varRef("callee", DYN, loc)], + args: [ + varRef("value", DYN, loc), + varRef("callee", DYN, loc), + varRef("refusal", STRING, loc), + ], type: BOOL, loc, }, @@ -57,7 +66,13 @@ export function checkedClassInstanceOf( loc, }); } - return { kind: "call", callee: INSTANCEOF, args: [value, callee], type: BOOL, loc }; + return { + kind: "call", + callee: INSTANCEOF, + args: [value, callee, { kind: "strLit", value: refusal, type: STRING, loc }], + type: BOOL, + loc, + }; } /** Checked construction dispatches to emitted native constructor thunks. */ diff --git a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts index 612c47ea0..dcadf3bf3 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts @@ -59,6 +59,7 @@ import { } from "./array-iteration.js"; import { dynamicReceiverThrows } from "./dynamic-receivers.js"; import { defaultAfterUndefined, lowerStaticallyUndefinedArgument } from "../optional-arguments.js"; +import { checkedFilterNarrowing } from "./filter-predicates.js"; /** `a.map(fn)` / `a.filter(fn)` / `a.forEach(fn)` desugar to a direct * call of a synthetic module function — one per method + element/result @@ -123,10 +124,11 @@ export function lowerArrayHofCall( } lowerer.badType(argNode, lowerer.typeOf(argNode)); } - const outElem = + const filtered = method === "filter" ? filterResultElem(lowerer, call, argNode, elem, valueT, booleanFilter) - : callbackArrayElement(lowerer, call, fnRet); + : null; + const outElem = filtered !== null ? filtered.elem : callbackArrayElement(lowerer, call, fnRet); // A filter that re-tags its elements keeps the single union-typed visit. const fast = present || booleanFilter || (method === "filter" && !typeEquals(outElem, elem)) @@ -145,19 +147,33 @@ export function lowerArrayHofCall( present, ); const resultType: IrType = method === "map" || method === "filter" ? arrayOf(outElem) : VOID; - return { + const result: IrExpr = { kind: "call", callee: helper, args: [receiver, fnArg, ...(fast ? [fast] : [])], type: resultType, loc, }; + const narrowing = filtered?.narrowing ?? null; + if (narrowing === null) return result; + return { + kind: "call", + callee: narrowing.helper, + args: [result], + type: arrayOf(narrowing.elem), + loc, + }; } -/** The checker-selected element of a filter result, with the same trust - * boundary as the historical narrowing filter: an inferred inline type - * predicate may prove one source arm; a written predicate annotation may - * not. Boolean is safe when every discarded arm is a nullish unit. */ +/** The element a filter keeps, and the checked conversion of the result + * to the checker's narrower element when the predicate does not prove it. + * An inferred inline type predicate proves one source arm, or a class + * view, so the loop keeps that type directly. A written predicate (`(p): p + * is Cat => ...`, a guard function passed as a value) is an assertion + * TypeScript does not check, and an inferred predicate that keeps several + * arms has no single arm to re-tag: the loop keeps the receiver's element + * and `narrowing` (checkedFilterNarrowing) converts each kept element. + * Boolean is safe when every discarded arm is a nullish unit. */ function filterResultElem( lowerer: Lowerer, call: ts.CallExpression, @@ -165,7 +181,37 @@ function filterResultElem( elem: IrType, valueT: IrType, booleanFilter: boolean, -): IrType { +): { elem: IrType; narrowing: { helper: string; elem: IrType } | null } { + const kept = filterKeptElem(lowerer, call, argNode, elem, valueT, booleanFilter); + if (typeof kept !== "string") return { elem: kept, narrowing: null }; + return { elem, narrowing: { helper: kept, elem: filterCheckerElem(lowerer, call, elem) } }; +} + +/** The checker's spelling of a filter result's element type. */ +function filterTypeText(lowerer: Lowerer, call: ts.CallExpression): string { + const result = lowerer.typeOf(call); + const element = lowerer.checker.isArrayType(result) + ? lowerer.checker.getTypeArguments(result as ts.TypeReference)[0] + : undefined; + return element === undefined ? "" : lowerer.checker.typeToString(element); +} + +/** The checker's element of a filter result, or the receiver's when the + * filter introduces no refinement. */ +function filterCheckerElem(lowerer: Lowerer, call: ts.CallExpression, elem: IrType): IrType { + const result = lowerer.mapTypeOf(lowerer.typeOf(call)); + return result?.kind === "array" ? result.elem : elem; +} + +/** filterResultElem's element, or the name of the checked narrowing helper. */ +function filterKeptElem( + lowerer: Lowerer, + call: ts.CallExpression, + argNode: ts.Expression, + elem: IrType, + valueT: IrType, + booleanFilter: boolean, +): IrType | string { const callT = lowerer.typeOf(call); const result = lowerer.mapTypeOf(callT); let outElem = result?.kind === "array" ? result.elem : elem; @@ -191,23 +237,31 @@ function filterResultElem( "keep the receiver's element type instead — annotate the callback's return ': boolean' " + "(the checker then skips the predicate) or annotate the result with the receiver's own " + "element type — and narrow the elements after"; - if (!booleanFilter) { + const written = + !booleanFilter && + ((!ts.isArrowFunction(argNode) && !ts.isFunctionExpression(argNode)) || + argNode.type !== undefined); + const narrowing = booleanFilter + ? null + : (): string | null => + checkedFilterNarrowing(lowerer, elem, outElem, filterTypeText(lowerer, call), locOf(call)); + if (written) { + const helper = narrowing!(); + if (helper !== null) return helper; if (!ts.isArrowFunction(argNode) && !ts.isFunctionExpression(argNode)) { lowerer.unsupported( "SC1090", argNode, - `narrowing '.filter' through a callback VALUE (only an inline callback whose predicate ` + - `the checker inferred can re-tag — ${annotateEscape})`, - ); - } - if (argNode.type) { - lowerer.unsupported( - "SC1090", - argNode, - `narrowing '.filter' with a hand-written type predicate (a written 'x is T' is an ` + - `unchecked assertion nothing validates at runtime — ${annotateEscape})`, + `narrowing '.filter' to '${lowerer.fmt(outElem)}' through a callback VALUE (no checked ` + + `conversion from '${lowerer.fmt(elem)}' — ${annotateEscape})`, ); } + lowerer.unsupported( + "SC1090", + argNode, + `narrowing '.filter' to '${lowerer.fmt(outElem)}' with a hand-written type predicate (no ` + + `checked conversion from '${lowerer.fmt(elem)}' — ${annotateEscape})`, + ); } const classRefinement = !booleanFilter && outElem.kind === "object" && canAssertClassValue(lowerer, valueT, outElem); @@ -215,6 +269,8 @@ function filterResultElem( !classRefinement && (valueT.kind !== "union" || lowerer.armTag(valueT.unionId, outElem) < 0) ) { + const helper = narrowing?.() ?? null; + if (helper !== null) return helper; const multiArm = outElem.kind === "union"; lowerer.unsupported( "SC1090", @@ -2650,6 +2706,7 @@ export function lowerTuplePredicateCall( // the value type an array find narrows from. let foundT: IrType = elem; let retag: string | null = null; + let narrowing: { helper: string; elem: IrType } | null = null; if (method === "filter") { // The element union is built in position order, so the checker's // result element can list the same arms in another order. @@ -2660,9 +2717,11 @@ export function lowerTuplePredicateCall( checkerResult?.kind === "array" && arms(checkerResult.elem).length === arms(elem).length && arms(checkerResult.elem).every((arm) => arms(elem).some((other) => typeEquals(arm, other))); - outElem = unchanged - ? elem - : filterResultElem(lowerer, call, argNode, elem, elem, booleanFilter); + if (!unchanged) { + const filtered = filterResultElem(lowerer, call, argNode, elem, elem, booleanFilter); + outElem = filtered.elem; + narrowing = filtered.narrowing; + } resultT = arrayOf(outElem); } else if (method === "find") { resultT = lowerer.irTypeOf(call); @@ -2795,5 +2854,19 @@ export function lowerTuplePredicateCall( loc, }); } - return { kind: "call", callee: helper, args: [receiver, fnArg], type: resultT, loc }; + const result: IrExpr = { + kind: "call", + callee: helper, + args: [receiver, fnArg], + type: resultT, + loc, + }; + if (narrowing === null) return result; + return { + kind: "call", + callee: narrowing.helper, + args: [result], + type: arrayOf(narrowing.elem), + loc, + }; } diff --git a/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts b/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts index 9db04e95b..d95640978 100644 --- a/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts +++ b/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts @@ -53,6 +53,8 @@ export function lowerArrayCallback( bindUntyped = false, expectedReturn?: IrType, ): { fnArg: IrExpr & { type: IrType & { kind: "func" } }; arity: number } { + // `xs.map(((x) => x))`: a parenthesized callback is the function itself. + while (ts.isParenthesizedExpression(argNode)) argNode = argNode.expression; // The ambient `Boolean` as the callback (`flags.every(Boolean)`, // `xs.some(Boolean)`, `xs.map(Boolean)`): Boolean(value) answers the // ToBoolean of its FIRST argument and ignores the rest, so the callback diff --git a/packages/compiler/src/frontend/lowering/containers/filter-predicates.ts b/packages/compiler/src/frontend/lowering/containers/filter-predicates.ts new file mode 100644 index 000000000..3934edbf0 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/containers/filter-predicates.ts @@ -0,0 +1,261 @@ +/* Checked narrowing for `.filter` with a written type predicate. + * + * A written predicate (`(p): p is Cat => ...`, or a guard function passed + * as a value) is an assertion TypeScript does not check, so the filter loop + * keeps the receiver's element type and the result converts to the + * predicate's type afterwards, element by element: + * + * - a union element to one of its arms, or to a union of some of its + * arms, by its tag; + * - a checked-dynamic element by dynCheck; + * - a class instance to a subclass by its class; + * - a tuple or record to one with the same fields, field by field. + * + * Each conversion throws the catchable TypeError of a checked extraction + * when a kept element is not of the predicate's type, which is when the + * predicate returned true for a value it does not describe. */ +import { varRef } from "../../../ir/build.js"; +import { + BOOL, + F64, + STRING, + isUnitType, + typeEquals, + type IrExpr, + type IrStmt, + type IrType, + type SrcLoc, +} from "../../../ir/ir.js"; +import { checkedClassAssertion } from "../class-assertions.js"; +import { buildArrayConversion } from "../coercions/builders.js"; +import type { Lowerer } from "../lowerer.js"; +import { keyedHelperName } from "../stable-names.js"; +import { typeKey } from "../../type-mapper.js"; + +/** The interned helper converting a `from[]` filter result to `to[]`, or + * null when an element of `from` has no checked conversion to `to`. + * `typeText` names the predicate's type in the TypeError. */ +export function checkedFilterNarrowing( + lowerer: Lowerer, + from: IrType, + to: IrType, + typeText: string, + loc: SrcLoc, +): string | null { + const element = elementHelper(lowerer, from, to, loc, typeText); + if (element === null) return null; + const key = `filter.narrow:${typeKey(from)}:${typeKey(to)}:${typeText}`; + const existing = lowerer.valueHelpers.get(key); + if (existing) return existing; + const name = keyedHelperName(lowerer, "%arr.narrow", key); + lowerer.valueHelpers.set(key, name); + const outT: IrType = { kind: "array", elem: to }; + lowerer.liftedFns.push( + buildArrayConversion( + name, + { kind: "array", elem: from }, + { kind: "arrayLit", elems: [], type: outT, loc }, + (value, _index, result) => ({ + kind: "arrIntrinsic", + method: "push", + receiver: result, + args: [{ kind: "call", callee: element, args: [value], type: to, loc }], + type: F64, + loc, + }), + loc, + ), + ); + return name; +} + +/** The interned helper converting one `from` value to `to`, or null. */ +function elementHelper( + lowerer: Lowerer, + from: IrType, + to: IrType, + loc: SrcLoc, + typeText = lowerer.fmt(to), +): string | null { + const key = `filter.narrow.value:${typeKey(from)}:${typeKey(to)}:${typeText}`; + const existing = lowerer.valueHelpers.get(key); + if (existing) return existing; + // A recursive type pair converts through the helper being built. + if (lowerer.coercions.planning.has(key)) return null; + lowerer.coercions.planning.add(key); + const input = varRef("v.0", from, loc); + let body: IrStmt[] | null; + try { + body = conversionBody(lowerer, input, from, to, typeText, loc); + } finally { + lowerer.coercions.planning.delete(key); + } + if (body === null) return null; + const name = keyedHelperName(lowerer, "%narrow.checked", key); + lowerer.valueHelpers.set(key, name); + lowerer.liftedFns.push({ + name, + params: [{ localId: "v.0", name: "v", type: from }], + returnType: to, + locals: [{ id: "v.0", name: "v", type: from, mutable: false }], + body, + loc, + }); + return name; +} + +/** A converted value: `value` itself when the types are equal, or a call of + * the conversion helper. */ +function converted( + lowerer: Lowerer, + value: IrExpr, + from: IrType, + to: IrType, + loc: SrcLoc, +): IrExpr | null { + if (typeEquals(from, to)) return value; + const helper = elementHelper(lowerer, from, to, loc); + return helper === null ? null : { kind: "call", callee: helper, args: [value], type: to, loc }; +} + +/** The TypeError a kept element of another type throws. */ +function typeError(message: string, loc: SrcLoc): IrStmt { + return { + kind: "throw", + value: { + kind: "libCall", + fn: "error.new", + args: [{ kind: "strLit", value: message, type: STRING, loc }], + type: { kind: "object", className: "%TypeError" }, + loc, + }, + loc, + }; +} + +function conversionBody( + lowerer: Lowerer, + input: IrExpr, + from: IrType, + to: IrType, + typeText: string, + loc: SrcLoc, +): IrStmt[] | null { + if (typeEquals(from, to)) return [{ kind: "return", value: input, loc }]; + if (from.kind === "dyn") + return [{ kind: "return", value: { kind: "dynCheck", value: input, type: to, loc }, loc }]; + if (from.kind === "object" && to.kind === "object") { + const asserted = checkedClassAssertion(lowerer, input, to, loc); + return asserted === null ? null : [{ kind: "return", value: asserted, loc }]; + } + if (from.kind === "union") + return unionConversion(lowerer, input, from.unionId, to, typeText, loc); + if (from.kind === "record" && to.kind === "record") { + const source = lowerer.shapes.get(from.shapeId); + const target = lowerer.shapes.get(to.shapeId); + if ( + !source || + !target || + source.tuple !== target.tuple || + source.indexValue || + target.indexValue + ) + return null; + if (source.fields.length !== target.fields.length) return null; + const fields: { name: string; value: IrExpr }[] = []; + for (const field of target.fields) { + const own = source.fields.find((f) => f.name === field.name); + if (!own) return null; + const read: IrExpr = { + kind: "recordGet", + obj: input, + shapeId: from.shapeId, + field: field.name, + type: own.type, + loc, + }; + const value = converted(lowerer, read, own.type, field.type, loc); + if (value === null) return null; + fields.push({ + name: field.name, + value: keepAbsence(lowerer, input, from.shapeId, field, value, loc), + }); + } + // The copy keeps the source's own keys in the source's order, as a width + // copy does (key-order-witness.ts). + const copy: IrExpr = { kind: "recordLit", fields, type: to, loc }; + return [{ kind: "return", value: lowerer.keyOrder.copy(copy, from.shapeId), loc }]; + } + return null; +} + +/** A converted field value that keeps the source field's absence: an + * optional field the source object lacks stays absent in the copy rather + * than becoming a property that holds undefined. */ +function keepAbsence( + lowerer: Lowerer, + input: IrExpr, + shapeId: string, + field: { name: string; type: IrType }, + value: IrExpr, + loc: SrcLoc, +): IrExpr { + if (field.type.kind !== "union") return value; + if (!lowerer.unions.get(field.type.unionId)?.arms.some((arm) => arm.kind === "undefinedT")) + return value; + return { + kind: "ternary", + cond: { kind: "recordHas", obj: input, shapeId, field: field.name, type: BOOL, loc }, + then: value, + else_: { kind: "fieldAbsent", unionId: field.type.unionId, type: field.type, loc }, + type: field.type, + loc, + }; +} + +/** A union value to one of its arms, or to a union of some of its arms: + * each arm the target holds returns, any other arm throws. */ +function unionConversion( + lowerer: Lowerer, + input: IrExpr, + unionId: string, + to: IrType, + typeText: string, + loc: SrcLoc, +): IrStmt[] | null { + const source = lowerer.unions.get(unionId); + if (!source) return null; + const targetArms = to.kind === "union" ? (lowerer.unions.get(to.unionId)?.arms ?? null) : [to]; + if (targetArms === null) return null; + const body: IrStmt[] = []; + let kept = 0; + for (let tag = 0; tag < source.arms.length; tag++) { + const arm = source.arms[tag]!; + const targetTag = targetArms.findIndex((t) => typeEquals(t, arm)); + if (targetTag < 0) continue; + kept++; + const payload: IrExpr = isUnitType(arm) + ? { kind: "unitLit", unit: arm.kind === "undefinedT" ? "undefined" : "null", type: arm, loc } + : { kind: "unionNarrow", unionId, tag, value: input, type: arm, loc }; + const value: IrExpr = + to.kind === "union" + ? { kind: "unionWrap", unionId: to.unionId, tag: targetTag, value: payload, type: to, loc } + : payload; + body.push({ + kind: "if", + cond: { kind: "unionIsTag", unionId, tag, negated: false, value: input, type: BOOL, loc }, + then: [{ kind: "return", value, loc }], + else_: null, + loc, + }); + } + // Every arm the target names must be one of the union's own arms. + if (kept === 0 || kept !== targetArms.length) return null; + body.push( + typeError( + `a kept element is not a '${typeText}' (a type predicate returned true for a value of another type)`, + loc, + ), + ); + return body; +} diff --git a/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts b/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts index b3faf649e..668603d47 100644 --- a/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts +++ b/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts @@ -1388,6 +1388,15 @@ export type IndexMergeContributor = * JS own-key order. Explicit values arrive pre-coerced to the target's * value slot. Null when a spread source is outside that matrix (the * caller keeps its fence). */ +/** A value of an index slot's type that holds a key's place until a later + * write replaces it, or null for slot types without a literal. */ +function indexSlotPlaceholder(slot: IrType, loc: SrcLoc): IrExpr | null { + if (slot.kind === "string") return { kind: "strLit", value: "", type: STRING, loc }; + if (slot.kind === "f64") return { kind: "numLit", value: 0, type: slot, loc }; + if (slot.kind === "bool") return { kind: "boolLit", value: false, type: slot, loc }; + return null; +} + export function lowerIndexMergeHelper( lowerer: Lowerer, toId: string, @@ -1410,9 +1419,9 @@ export function lowerIndexMergeHelper( shapeId: string; wrapTag: number; retag: string | null; - fields: Map | null; + fields: Map | null; }[] = []; - for (const c of contributors) { + for (const [ci, c] of contributors.entries()) { // A conditional spread needs the undefined arm as its "absent" // value — a target slot without one can't carry the empty arm. if ( @@ -1426,15 +1435,26 @@ export function lowerIndexMergeHelper( if (!from || from.tuple) return null; if (!from.indexValue) { if (from.fields.some((f) => f.name.startsWith("%"))) return null; - const fields = new Map(); + const fields = new Map(); for (const ff of from.fields) { const lift = typeEquals(ff.type, tIv) ? ({ how: "copy" } as WidthLift) : tIv.kind === "dyn" && (ff.type.kind === "dyn" || lowerer.dynConvertible(ff.type)) ? "dyn" : lowerer.widthLiftPlan(ff.type, tIv); - if (lift === null) return null; - fields.set(ff.name, lift); + if (lift !== null) { + fields.set(ff.name, lift); + continue; + } + // A field a later plain property overrides (`{ ...tags, status: + // String(tags.status) }`): its value never survives, but its key + // keeps the spread's position, so a placeholder of the slot's + // type holds the place until the override writes. + const overridden = contributors + .slice(ci + 1) + .some((later) => later.kind === "field" && later.name === ff.name); + if (!overridden || indexSlotPlaceholder(tIv, loc) === null) return null; + fields.set(ff.name, "placeholder"); } srcPlans.push({ shapeId: c.shapeId, wrapTag: -1, retag: null, fields }); continue; @@ -1569,6 +1589,11 @@ export function lowerIndexMergeHelper( // own planned lift (absent optionals skip — the unset convention); // no overflow exists to walk. if (plan.fields !== null) { + // Keys enter in the source's enumeration order, as its own keys do. + // That order settles only after lowering (key-order witnesses), so + // the writes go into one block that a finalizer orders. + lowerer.keyOrder.observed.add(from.id); + const fieldStmts = new Map(); for (const ff of from.fields) { const lift = plan.fields.get(ff.name)!; const raw: IrExpr = { @@ -1586,13 +1611,16 @@ export function lowerIndexMergeHelper( shapeId: toId, key: { kind: "strLit", value: ff.name, type: STRING, loc }, value: - lift === "dyn" - ? { kind: "dynFrom", value: raw, type: DYN, loc } - : lowerer.applyWidthLift(lift, raw, tIv, loc), + lift === "placeholder" + ? indexSlotPlaceholder(tIv, loc)! + : lift === "dyn" + ? { kind: "dynFrom", value: raw, type: DYN, loc } + : lowerer.applyWidthLift(lift, raw, tIv, loc), overflowOnly: true, loc, }; - body.push( + fieldStmts.set( + ff.name, utag >= 0 && ff.type.kind === "union" ? { kind: "if", @@ -1604,6 +1632,17 @@ export function lowerIndexMergeHelper( : write, ); } + // declaredOrder omits internal '%'-fields, which never copy. + const ordered = (shape: IrRecordShape): IrStmt[] => + (shape.declaredOrder ?? shape.fields.map((f) => f.name)).flatMap((field) => { + const stmt = fieldStmts.get(field); + return stmt ? [stmt] : []; + }); + const group: IrStmt & { kind: "block" } = { kind: "block", body: ordered(from), loc }; + lowerer.shapeOrderHelperFinalizers.push(() => { + group.body = ordered(lowerer.shapes.get(plan.shapeId) ?? from); + }); + body.push(group); return; } const fIv = from.indexValue!; diff --git a/packages/compiler/src/frontend/lowering/dispatch-sites.ts b/packages/compiler/src/frontend/lowering/dispatch-sites.ts index 2618dacf9..c8a8d6ba6 100644 --- a/packages/compiler/src/frontend/lowering/dispatch-sites.ts +++ b/packages/compiler/src/frontend/lowering/dispatch-sites.ts @@ -11,6 +11,18 @@ import { everyStmtList } from "../../ir/traverse.js"; import { toPrimitiveHint } from "./class-dynamic-dispatch.js"; import { GENERATOR_STEP_METHODS, HELPER_METHODS } from "./generator-dynamic-dispatch.js"; +/** Runtime entry points that step a checked-dynamic iterable's iterator: + * destructuring rest, and the collection constructors' and + * Object.fromEntries' seeds. */ +const RUNTIME_STEP_LIB_FNS: ReadonlySet = new Set([ + "dyn.destructureRest", + "dyn.nativeSetNew", + "dyn.mapSeedEntries", + "dyn.fromEntries", + "weakMap.new", + "weakSet.new", +]); + /** What one function's body holds for the class dispatch pass and its * caught-value, generator and class construction sub-passes, collected in * one walk. Lists keep the order the walk meets their entries. */ @@ -186,7 +198,7 @@ function scanSites(statements: IrStmt[]): FunctionSites { expr.key.kind === "strLit" && HELPER_METHODS.has(expr.key.value)) || expr.kind === "dynIterN" || - (expr.kind === "libCall" && expr.fn === "dyn.destructureRest") + (expr.kind === "libCall" && RUNTIME_STEP_LIB_FNS.has(expr.fn)) ) sites.runtimeSteps = true; switch (expr.kind) { diff --git a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts index 8f119504c..cb2d86804 100644 --- a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts @@ -58,6 +58,22 @@ export function jsHoldsDynObject(lowerer: Lowerer, expr: ts.Expression): boolean return false; } +/** An empty TypeScript literal whose own type is `never[]`: tsc's type for + * `[]` with no contextual array type (`return []` in an async function, + * whose contextual type is `string[] | PromiseLike`; `{ files: [] }`). + * It holds no element, as the empty tuple does, so it builds as the + * unit-only element array, and an array slot receives it through the + * checked empty-array lift. never's f64 element mapping would make it a + * `number[]`, which no `string[]` slot accepts. */ +function emptyNeverLiteral(lowerer: Lowerer, expr: ts.ArrayLiteralExpression): boolean { + if (expr.elements.nodes.length > 0 || isJsSourceFile(ts.getSourceFileOfNode(expr))) return false; + const own = lowerer.typeOf(expr); + return ( + lowerer.checker.isArrayType(own) && + (lowerer.checker.getTypeArguments(own as ts.TypeReference)[0]!.flags & ts.TypeFlags.Never) !== 0 + ); +} + /** `[a, b, c]`. The element type comes from the contextual type when tsc * has one (`const a: number[] = []`, arguments, nested literals) and from * the literal's own inferred type otherwise. A bare `[]` with no context @@ -80,6 +96,8 @@ export function lowerArrayLiteral( ? lowerer.typeOf(expr) : (ctxType ?? lowerer.typeOf(expr)); let mapped = expected ?? lowerer.mapTypeOf(tsType); + if (expected === undefined && ctxType === undefined && emptyNeverLiteral(lowerer, expr)) + mapped = arrayOf(unitOnlyUnion(lowerer.unions)); // Inferred JavaScript arrays can contain unrelated constructors with // structurally equal checker types. Their native class ABIs still differ. if ( @@ -131,11 +149,14 @@ export function lowerArrayLiteral( // the literal's own inferred type is the real tuple. if (!mapped || mapped.kind === "union" || mapped.kind === "dyn") { const ctxUnion = mapped?.kind === "union" ? mapped : null; + const ctxUnmapped = mapped === null; mapped = lowerer.mapTypeOf(lowerer.typeOf(expr)); // The never-tainted own type carries no element information here // either (the JS story above) — only a contextual union's single // array arm below may still supply a static home. if (neverTaintedOwnJs) mapped = null; + if (ctxUnmapped && emptyNeverLiteral(lowerer, expr)) + mapped = arrayOf(unitOnlyUnion(lowerer.unions)); // An EMPTY literal in a union slot (`const t: string[] | undefined = // []`, the `[]` default argument against `string[] | undefined`) has // no useful own type — tsc infers `never[]`, whose f64-element diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index 7f3910c15..5df3c7fe0 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -666,14 +666,20 @@ export function lowerPropertyAccessExpr( ); if (fixedArity) { // A `this` parameter is type-only: callers never pass it. - return { - kind: "numLit", - value: (fn as ts.FunctionLikeDeclaration).parameters.nodes.filter( - (p) => !isThisParameter(p), - ).length, - type: F64, - loc, - }; + const declared = (fn as ts.FunctionLikeDeclaration).parameters.nodes.filter( + (p) => !isThisParameter(p), + ).length; + const count = Math.max(declared, contextualArgumentCount(lowerer, fn) ?? 0); + // Calls through the expression's own name pass the declared count, + // calls through the wider slot pass more: no constant answers both. + if (count > declared && referencesOwnName(lowerer, fn)) { + lowerer.unsupported( + "SC1090", + expr, + "'arguments.length' in a function expression that is called both through its own name and through a function type with more parameters (the count varies per call)", + ); + } + return { kind: "numLit", value: count, type: F64, loc }; } lowerer.unsupported( "SC1090", @@ -4440,3 +4446,47 @@ function lowerPromiseConstructorRead( } return null; } + +/** Whether a named function expression refers to itself by name in its + * body (a recursive call, or the function escaping under its own name). */ +function referencesOwnName(lowerer: Lowerer, fn: ts.Node): boolean { + if (!ts.isFunctionExpression(fn) || fn.name === undefined || fn.body === undefined) return false; + const self = lowerer.checker.getSymbolAtLocation(fn.name); + if (self === undefined) return false; + const name = fn.name.text; + let found = false; + ts.walkPreorder(fn.body, (node) => { + if (found) return "stop"; + if ( + ts.isIdentifier(node) && + node.text === name && + lowerer.checker.getSymbolAtLocation(node) === self + ) + found = true; + return undefined; + }); + return found; +} + +/** The argument count every caller passes a function expression whose + * value flows into one fixed-arity function type, an array method's + * callback or an annotated slot: that type's parameter count. Null when + * there is no such type or its arity varies. */ +function contextualArgumentCount(lowerer: Lowerer, fn: ts.Node): number | null { + if (!ts.isFunctionExpression(fn)) return null; + const contextual = lowerer.checker.getContextualType(fn); + if (contextual === undefined) return null; + const signatures = lowerer.checker.getCallSignatures( + lowerer.checker.getNonNullableType(contextual), + ); + if (signatures.length !== 1) return null; + const declaration = signatures[0]!.declaration; + if (declaration === undefined) return null; + let count = 0; + for (const parameter of (declaration.parameters?.nodes ?? []) as ts.ParameterDeclaration[]) { + if (isThisParameter(parameter)) continue; + if (parameter.questionToken || parameter.initializer || parameter.dotDotDotToken) return null; + count++; + } + return count; +} diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts index 61558f008..759687ff0 100644 --- a/packages/compiler/src/frontend/lowering/expressions/relational.ts +++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts @@ -49,6 +49,7 @@ import { InternalCompilerError } from "../../../errors.js"; import { tryLowerExpression } from "./try-lower-expression.js"; import { isJsSourceFile, locOf } from "../../program.js"; import { checkedClassInstanceOf } from "../class-construction.js"; +import { unsupportedDiag } from "../../../diagnostics/diagnostic.js"; import { globalSymbolKey } from "./global-symbols.js"; import { foldedStringKeyOf } from "./object-literals.js"; import { NARROW_FIRST, sideEffectFreeOptionValue } from "../surfaces.js"; @@ -490,7 +491,13 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc const constructor = tryLowerExpression(lowerer, expr.right); if (constructor?.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr))) { const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN); - return checkedClassInstanceOf(lowerer, value, constructor, loc); + return checkedClassInstanceOf( + lowerer, + value, + constructor, + dynInstanceOfRefusal(lowerer, expr), + loc, + ); } // `this instanceof Glob` in a JavaScript constructor function: the // function's prototype against the value's prototype chain. @@ -2000,3 +2007,25 @@ function lowerRuntimeKeyIn( } return { kind: "call", callee: helper, args: [keyIr, recv], type: BOOL, loc }; } + +/** Standard-library constructors the untyped instanceof helper has no + * instance test for: an untyped `x instanceof Object` always refuses. */ +const DYN_INSTANCEOF_REFUSED: ReadonlySet = new Set(["Object", "Function"]); + +/** The SC1090 message `x instanceof C` throws, with the site's position, + * when `C` is a runtime constructor with no instance test. A right-hand + * side that names one of DYN_INSTANCEOF_REFUSED always reaches that + * refusal, so the site is listed with the runtime fences. */ +function dynInstanceOfRefusal(lowerer: Lowerer, expr: ts.BinaryExpression): string { + const diag = unsupportedDiag("SC1090", locOf(expr), "instanceof against this native constructor"); + const symbol = lowerer.checker.getSymbolAtLocation(expr.right); + if ( + symbol !== undefined && + lowerer.isStdlibSymbol(symbol) && + DYN_INSTANCEOF_REFUSED.has(ts.symbolName(symbol)) + ) + lowerer.runtimeFences.push(diag); + const sf = ts.getSourceFileOfNode(expr); + const line = ts.getLineAndCharacterOfPosition(sf, diag.loc.start).line + 1; + return `${diag.message} [${diag.code} at ${diag.loc.file}:${line}]`; +} diff --git a/packages/compiler/src/frontend/lowering/key-order-witness.test.ts b/packages/compiler/src/frontend/lowering/key-order-witness.test.ts index de16e5d61..8632bad47 100644 --- a/packages/compiler/src/frontend/lowering/key-order-witness.test.ts +++ b/packages/compiler/src/frontend/lowering/key-order-witness.test.ts @@ -157,6 +157,36 @@ test("a synthesized construction pins today's table", () => { expect(f.order(ab)).toEqual(["a", "b"]); }); +test("a runtime builder typed by its use site pins today's table", () => { + const f = fixture(); + const ab = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + // net.serverAddress's signature result is VOID: the call takes its record + // type from its use site, and the runtime builds the record in that order. + const built: IrExpr = { kind: "libCall", fn: "net.serverAddress", args: [], type: ab, loc }; + f.run([observe(f.literal(ab, ["b", "a"])), observe(built)]); + expect(f.order(ab)).toEqual(["a", "b"]); +}); + +test("an always-throwing call typed as the record leaves its literals' order", () => { + const f = fixture(); + const ab = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + const thrown: IrExpr = { kind: "libCall", fn: "error.nodeThrow", args: [], type: ab, loc }; + f.run([observe(f.literal(ab, ["b", "a"])), observe(thrown)]); + expect(f.order(ab)).toEqual(["b", "a"]); +}); + test("a checked conversion alone pins today's table", () => { const f = fixture(); const ab = f.record( diff --git a/packages/compiler/src/frontend/lowering/key-order-witness.ts b/packages/compiler/src/frontend/lowering/key-order-witness.ts index 2fc9a6cbd..4adfb87ab 100644 --- a/packages/compiler/src/frontend/lowering/key-order-witness.ts +++ b/packages/compiler/src/frontend/lowering/key-order-witness.ts @@ -45,6 +45,7 @@ import type { IrExpr, IrFunction, IrRecordShape, IrStmt, IrType } from "../../ir import { shapeHasAccessorSlots, typeEquals, UNDEFINED_T } from "../../ir/ir.js"; import { everyExprChild, everyStmtChild, everyTypeChild } from "../../ir/traverse.js"; import type { Lowerer } from "./lowerer.js"; +import { ALWAYS_THROW_LIB_FNS } from "../../ir/builtin-effects.js"; /** One property of an object literal, in source order. */ export type KeyOrderSegment = @@ -312,7 +313,11 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio // Runtime surfaces (console, util.format, workers) that take a // record may walk its keys; runtime builders make new records. for (const arg of e.args) observedRoots.push(arg.type); - producedBy(e.type, true); + // An always-throwing call stands in for a value (`error.nodeThrow` + // in a narrowing ternary) and never produces one. Other calls + // whose signature result is VOID are typed by their use site and + // build the record at run time (`server.address()`). + if (!ALWAYS_THROW_LIB_FNS.has(e.fn)) producedBy(e.type, true); return; case "intrinsic": // Intrinsics hand back values built elsewhere, except allSettled's diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 39b7207c0..2666e106d 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -45,6 +45,7 @@ import { errorToStringCall } from "./error-methods.js"; import { lowerClassPrototypeAssign, lowerClassPrototypeDescriptors } from "./class-prototypes.js"; import { BYTES_ELEMENT_NAME, + BYTES_U8, BIGINT_T, BOOL, CAUGHT, @@ -53,6 +54,7 @@ import { F64, type IrExpr, type IrFunction, + type IrLibFn, type IrLocal, type IrParam, type IrStmt, @@ -76,6 +78,7 @@ import { } from "../../ir/ir.js"; import { lowerProtocolMethodCall } from "./class-protocol-dispatch.js"; import { isJsSourceFile, locOf, requireSpecOf } from "../program.js"; +import { isPackageShimFile, isShimIntrinsicName } from "../package-shims.js"; import { lowerJsonRequireCall } from "./json-require.js"; import { lowerAssertInstanceCall } from "./assert-instances.js"; import { @@ -292,6 +295,12 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const codeGeneration = codeGenerationCalleeOf(lowerer, expr.expression); if (codeGeneration !== null) return lowerCodeGenerationCall(lowerer, expr, codeGeneration); } + // A package shim's runtime intrinsic (a bodiless `declare function` + // named scriptcUnixDgram* in a shipped shim): a direct runtime call. + if (ts.isIdentifier(expr.expression) && isShimIntrinsicName(expr.expression.text)) { + const intrinsic = lowerShimIntrinsicCall(lowerer, expr, expr.expression, loc); + if (intrinsic) return intrinsic; + } // Inline require() of a program module (cjs-inline-require.ts): a // member call over its export table resolves like a binding's member // call after the module's %init at this position; the require itself @@ -1619,7 +1628,16 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { } const accessorTarget = lowerer.fieldTarget(expr.expression); if (accessorTarget?.container === "accessor") { - const callee = lowerer.fieldGetExpr(accessorTarget, locOf(expr.expression), expr.expression); + let callee = lowerer.fieldGetExpr(accessorTarget, locOf(expr.expression), expr.expression); + // A getter returning a callable class instance (chalk's + // `chalk.red("x")`): call through its `%call` field. + if (callee.type.kind === "object") { + const direct = callableInstanceStringCall(lowerer, expr, callee, locOf(expr)); + if (direct) return direct; + callee = lowerer.hybridCallUnwrap(callee); + if (callee.type.kind === "func") + return lowerCallThroughValue(lowerer, expr, callee, locOf(expr)); + } if (callee.type.kind === "func") { const args = completeFuncValueArgs(lowerer, expr, callee.type, locOf(expr)); return { kind: "callValue", callee, args, type: callee.type.ret, loc: locOf(expr) }; @@ -1871,7 +1889,55 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // a non-func IR type was already rejected while lowering the callee. // HYBRID (function-with-properties) values call through their %call slot. let callee = lowerer.lowerExpr(expr.expression); - if (callee.type.kind === "record") callee = lowerer.hybridCallUnwrap(callee); + { + const direct = callableInstanceStringCall(lowerer, expr, callee, loc); + if (direct) return direct; + } + if (callee.type.kind === "record" || callee.type.kind === "object") + callee = lowerer.hybridCallUnwrap(callee); + return lowerCallThroughValue(lowerer, expr, callee, loc); +} + +/** A callable class instance called with one string: its `%call1` method + * when the class has one (the chalk shim's typed fast path), skipping the + * variadic closure's checked-dynamic arguments. */ +function callableInstanceStringCall( + lowerer: Lowerer, + expr: ts.CallExpression, + callee: IrExpr, + loc: SrcLoc, +): IrExpr | null { + if (callee.type.kind !== "object" || expr.arguments.nodes.length !== 1) return null; + const arg = expr.arguments.nodes[0]!; + if (ts.isSpreadElement(arg)) return null; + const info = lowerer.classes.get(callee.type.className); + if (info === undefined || !info.fields.has("%call") || !lowerer.findMethodOn(info, "%call1")) + return null; + const value = lowerer.lowerExpr(arg); + if (value.type.kind === "string") + return lowerer.accessorCall(callee.type.className, "%call1", callee, [value], STRING, loc); + // Any other argument (a number behind a cast): the variadic closure, + // through the checked-dynamic call as lowerCallThroughValue would. + const closure = lowerer.hybridCallUnwrap(callee); + if (closure.type.kind !== "func") return null; + return { + kind: "dynCall", + callee: { kind: "dynFrom", value: closure, type: DYN, loc }, + calleeName: ts.isIdentifier(expr.expression) ? expr.expression.text : "value", + args: [lowerer.coerceToExpected(value, DYN)], + type: DYN, + loc, + }; +} + +/** A call through an evaluated callee value: checked-dynamic callees, + * function unions, typed-rest and runtime-arity closures. */ +function lowerCallThroughValue( + lowerer: Lowerer, + expr: ts.CallExpression, + callee: IrExpr, + loc: SrcLoc, +): IrExpr { // A CHECKED-DYNAMIC callee — `fn(a, b)` where fn is an implicit-any // JS binding (the mustCall body's `fn(...args)`), a dyn capture, or a // keyed read off a dyn value: the dynCall boundary. Arguments convert @@ -6668,6 +6734,15 @@ export function lowerFilterNarrowCall( } const narrowed = outElem !== null && !typeEquals(outElem, elem); if (!narrowed && !isBooleanArg) return null; + // A written predicate (`(p): p is Cat => ...`, a guard function value) + // proves nothing at run time: the generic array HOF path filters the + // receiver's elements and converts the kept ones checked + // (filterResultElem). + if ( + !isBooleanArg && + ((!ts.isArrowFunction(argNode) && !ts.isFunctionExpression(argNode)) || argNode.type) + ) + return null; const annotateEscape = "keep the receiver's element type instead — annotate the callback's return ': boolean' " + @@ -6679,6 +6754,9 @@ export function lowerFilterNarrowCall( lowerer.badType(call, callT); // defensive: a narrowed non-union receiver } tag = lowerer.armTag(elem.unionId, outElem); + // Several kept arms: the generic path converts the kept elements to + // the narrower union (filterResultElem). + if (tag < 0 && !isBooleanArg) return null; if (tag < 0) { lowerer.unsupported( "SC1090", @@ -6700,25 +6778,8 @@ export function lowerFilterNarrowCall( return { kind: "call", callee: helper, args: [receiver], type: arrayOf(outElem ?? elem), loc }; } - // Inferred type predicate: inline function literal, NO return - // annotation (a written one is an unchecked assertion), and the - // checker reports a predicate over parameter 0. - if (!ts.isArrowFunction(argNode) && !ts.isFunctionExpression(argNode)) { - lowerer.unsupported( - "SC1090", - argNode, - `narrowing '.filter' through a callback VALUE ` + - `(only an inline callback whose predicate the checker inferred can re-tag — ${annotateEscape})`, - ); - } - if (argNode.type) { - lowerer.unsupported( - "SC1090", - argNode, - `narrowing '.filter' with a hand-written type predicate ` + - `(a written 'x is T' is an unchecked assertion nothing validates at runtime — ${annotateEscape})`, - ); - } + // An inferred type predicate: an inline function literal with no return + // annotation, over parameter 0 (written predicates returned above). // The receiver evaluates FIRST, in the enclosing function, like JS. const receiver = lowerer.lowerExpr(access.expression); const fnArg = lowerer.lowerExpr(argNode); @@ -9181,7 +9242,16 @@ export function lowerRecordFieldCall( // A HYBRID (function-with-properties) field is callable through its // reserved %call slot — `colors.blue("x")` where blue also carries // `.bold` (the chalk shape). - if (callee.type.kind === "record") callee = lowerer.hybridCallUnwrap(callee); + if (callee.type.kind === "object" && !init) { + const direct = callableInstanceStringCall(lowerer, call, callee, loc); + if (direct) return direct; + // Other argument counts call the %call closure through its value, as + // a call on the builder itself does (`theme.error("a", "b")`). + const closure = lowerer.hybridCallUnwrap(callee); + if (closure !== callee) return lowerCallThroughValue(lowerer, call, closure, loc); + } + if (callee.type.kind === "record" || callee.type.kind === "object") + callee = lowerer.hybridCallUnwrap(callee); // Inferred JavaScript option records can retain checked callables in // fields even when the checker only describes those members as any. // TypeScript reaches here for callable library types that are checked @@ -10939,3 +11009,37 @@ function fenceClassCopyIntoFreshObject( `Object.assign copying a '${lowerer.fmt(rawSource.type)}' instance into a fresh object typed as the class (the copy is a plain object holding the instance's own fields, not an instance — build the object literal from the fields you need)`, ); } + +/** The runtime calls package shims declare as bodiless functions. */ +const SHIM_INTRINSICS: ReadonlyMap = new Map< + string, + { fn: IrLibFn; args: IrType[]; ret: IrType } +>([ + ["scriptcUnixDgramSocket", { fn: "dgram.unixSocket", args: [], ret: F64 }], + ["scriptcUnixDgramConnect", { fn: "dgram.unixConnect", args: [F64, STRING], ret: F64 }], + ["scriptcUnixDgramSend", { fn: "dgram.unixSend", args: [F64, BYTES_U8], ret: F64 }], + ["scriptcUnixDgramClose", { fn: "dgram.unixClose", args: [F64], ret: F64 }], +]); + +function lowerShimIntrinsicCall( + lowerer: Lowerer, + expr: ts.CallExpression, + callee: ts.Identifier, + loc: SrcLoc, +): IrExpr | null { + const intrinsic = SHIM_INTRINSICS.get(callee.text); + if (intrinsic === undefined) return null; + const decl = lowerer.checker.getSymbolAtLocation(callee)?.valueDeclaration; + if ( + decl === undefined || + !ts.isFunctionDeclaration(decl) || + decl.body || + !isPackageShimFile(ts.getSourceFileOfNode(decl).fileName) || + expr.arguments.nodes.length !== intrinsic.args.length + ) + return null; + const args = expr.arguments.nodes.map((arg, i) => + lowerer.lowerExprExpecting(arg, intrinsic.args[i]!), + ); + return { kind: "libCall", fn: intrinsic.fn, args, type: intrinsic.ret, loc }; +} diff --git a/packages/compiler/src/frontend/lowering/lower-dgram.ts b/packages/compiler/src/frontend/lowering/lower-dgram.ts index aa90fc7c7..0e0ddf975 100644 --- a/packages/compiler/src/frontend/lowering/lower-dgram.ts +++ b/packages/compiler/src/frontend/lowering/lower-dgram.ts @@ -612,8 +612,10 @@ export function lowerDgramMethodCall( const fn: IrLibFn = data.type.kind === "string" ? "dgram.sendStr" : "dgram.sendBytes"; return { kind: "libCall", fn, args: [receiver, data, port, host], type: VOID, loc }; } + // TypeScript reaches the ladder for the completion-callback form + // send(msg, port, address, callback) (the hot-shots package shim). if ( - isJsSourceFile(ts.getSourceFileOfNode(call)) && + (isJsSourceFile(ts.getSourceFileOfNode(call)) || args.length === 4) && args.length <= 6 && !args.some(ts.isSpreadElement) ) { diff --git a/packages/compiler/src/frontend/lowering/lower-functions.ts b/packages/compiler/src/frontend/lowering/lower-functions.ts index 653c0d837..c1cca9c64 100644 --- a/packages/compiler/src/frontend/lowering/lower-functions.ts +++ b/packages/compiler/src/frontend/lowering/lower-functions.ts @@ -1,3 +1,4 @@ +import type { ScrDiagnostic } from "../../diagnostics/diagnostic.js"; import { declareTypedArguments } from "./typed-arguments.js"; import { dynUndefinedExpr, varRef } from "../../ir/build.js"; import { everyStmtList, transformStmtList } from "../../ir/traverse.js"; @@ -17,6 +18,7 @@ import { type IrLocal, type IrParam, type IrStmt, + type IrType, type SrcLoc, VOID, canBoxFuncIntoDyn, @@ -858,3 +860,79 @@ function isBufferCheckOf(e: IrExpr, slotId: string): boolean { e.type.elem === "u8" ); } + +/** The diagnostics of a TypeScript function whose signature did not + * compile, for the coverage remainder: its statements lower for their + * diagnostics only (the remainder's output is discarded), so the report + * lists them with the signature's blocker instead of hiding them behind it. + * Each parameter whose type maps is declared, and a use of any other + * parameter reports SC2004 back to the declaration. The statements already + * count as failed (countSkippedBody), so the pass leaves the stats alone. + * The pass adds diagnostics only: an internal error while it lowers drops + * what it found, and the signature's blocker stands alone as before. */ +export function blockedBodyDiagnostics( + lowerer: Lowerer, + decl: ts.FunctionDeclaration, +): ScrDiagnostic[] { + const body = decl.body; + if (!body || decl.typeParameters || decl.asteriskToken) return []; + if (isJsSourceFile(ts.getSourceFileOfNode(decl))) return []; + const symbol = declSymbolOf(lowerer, decl); + if (!symbol || lowerer.fnSigsBySymbol.has(symbol)) return []; + const diagsBefore = lowerer.diags.length; + const fencesBefore = lowerer.runtimeFences.length; + const suppressStats = lowerer.suppressStats; + lowerer.suppressStats = true; + let declared: IrType = DYN; + lowersCleanly(lowerer, () => { + declared = lowerer.declaredReturnType(decl, decl); + }); + const isAsync = decl.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) ?? false; + const ctx = newFnCtx(false, null, null, lowerer.bodyReturnType(isAsync, declared)); + ctx.isAsync = isAsync; + lowerer.fnStack.push(ctx); + let internal = false; + try { + // A declared `this` parameter gives the body its receiver, as + // lowerFunction declares it. + if (decl.parameters.nodes.some(isThisParameter)) declareCallReceiver(lowerer, decl, false); + for (const param of decl.parameters.nodes) { + if (isThisParameter(param)) continue; + const declaredParam = lowersCleanly(lowerer, () => { + lowerer.declareParams([param], lowerer.paramShapes([param])); + }); + if (declaredParam) continue; + const symbol = ts.isIdentifier(param.name) + ? lowerer.checker.getSymbolAtLocation(param.name) + : undefined; + if (symbol) lowerer.blockedBindings.add(symbol); + } + lowersCleanly(lowerer, () => { + lowerer.lowerStmts(body.statements.nodes); + }); + } catch (error) { + if (!(error instanceof InternalCompilerError)) throw error; + internal = true; + } finally { + lowerer.fnStack.pop(); + lowerer.suppressStats = suppressStats; + } + lowerer.runtimeFences.splice(fencesBefore); + const diags = lowerer.diags.splice(diagsBefore); + return internal || diags.some((d) => d.code === "SC9001") ? [] : diags; +} + +/** Runs `lower`, for blockedBodyDiagnostics: false when it poisons, with the + * diagnostics it recorded dropped (a failed mapping reports what the + * signature's blocker already reports). */ +function lowersCleanly(lowerer: Lowerer, lower: () => void): boolean { + const before = lowerer.diags.length; + try { + lower(); + return true; + } catch (error) { + if (!(error instanceof PoisonError)) throw error; + lowerer.diags.splice(before); + return false; + } +} diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index 5b78c7278..5238333ad 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -2929,6 +2929,38 @@ export function lowerDefaultExport(lowerer: Lowerer, stmt: ts.ExportAssignment): return { kind: "assign", localId: g.id, value, loc: locOf(stmt) }; } +/** Marks the runtime fences recorded since `fencesBefore` that lie in + * `sf`'s top-level code: they run when the module initializes. A fence + * inside a function, or inside a class member that runs later (a method, + * accessor, constructor or instance field initializer), runs when that + * code is called. */ +function markModuleInitFences(lowerer: Lowerer, sf: ts.SourceFile, fencesBefore: number): void { + for (let i = fencesBefore; i < lowerer.runtimeFences.length; i++) { + const fence = lowerer.runtimeFences[i]!; + if (fence.loc.file !== sf.fileName || runsAfterModuleInit(sf, fence.loc.start)) continue; + lowerer.runtimeFences[i] = { ...fence, startup: true }; + } +} + +/** Whether the code at `pos` sits in a function or a later-running class + * member body. */ +function runsAfterModuleInit(sf: ts.SourceFile, pos: number): boolean { + let later = false; + ts.walkPreorder(sf, (node) => { + if (node.pos > pos || node.end <= pos) return "skip"; + if ( + ts.isFunctionLike(node) || + (ts.isPropertyDeclaration(node) && + !node.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.StaticKeyword)) + ) { + later = true; + return "stop"; + } + return undefined; + }); + return later; +} + export function lowerFileInit( lowerer: Lowerer, sf: ts.SourceFile, @@ -2938,6 +2970,7 @@ export function lowerFileInit( const isAsync = lowerer.asyncInitFiles.has(sf); const ctx = newFnCtx(false, null, null, VOID); ctx.isAsync = isAsync; + const fencesBefore = lowerer.runtimeFences.length; lowerer.fnStack.push(ctx); try { const loc0: SrcLoc = { file: sf.fileName, start: 0, end: 0 }; @@ -3288,6 +3321,7 @@ export function lowerFileInit( }; } finally { lowerer.fnStack.pop(); + markModuleInitFences(lowerer, sf, fencesBefore); } } diff --git a/packages/compiler/src/frontend/lowering/lower-namespaces.ts b/packages/compiler/src/frontend/lowering/lower-namespaces.ts index 19123dae3..ea844ab1e 100644 --- a/packages/compiler/src/frontend/lowering/lower-namespaces.ts +++ b/packages/compiler/src/frontend/lowering/lower-namespaces.ts @@ -38,7 +38,7 @@ * calls). */ import { positionTag, type PositionTagOwner } from "./structural-positions.js"; import * as ts from "../ts7/adapter.js"; -import { packageShimFor } from "../package-shims.js"; +import { isPackageShimFile, isShimIntrinsicName, packageShimFor } from "../package-shims.js"; import type { Lowerer } from "./lowerer.js"; import { boundIdentifiersOf } from "./lowerer.js"; import type { FileParts } from "./lower-modules.js"; @@ -654,6 +654,12 @@ export function ambientUndefinedFnSymbolOf( const sym = lowerer.resolveValueSymbol(ident); if (!sym || !(sym.flags & ts.SymbolFlags.Function)) return null; if (lowerer.isStdlibSymbol(sym)) return null; + // A package shim's runtime intrinsic (lower-calls.ts) is defined. + if ( + isShimIntrinsicName(ident.text) && + sym.declarations.some((d) => isPackageShimFile(ts.getSourceFileOfNode(d).fileName)) + ) + return null; let sawFn = false; for (const d of sym.declarations) { if (ts.isInterfaceDeclaration(d) || ts.isTypeAliasDeclaration(d)) continue; // type-world merge partners diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 213abfec6..02e585aaf 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -4799,9 +4799,10 @@ export function lowerVarDeclList( * `never[]` or `any[]` and each later read by the elements written before * it, and reads inside closures as the unsettled array. When every read * with a settled element type maps to the same array type, the binding has - * that type. When reads disagree or the binding is reassigned, it is - * checked-dynamic, as the same binding is at module scope. Null for other - * bindings. */ + * that type. When reads disagree, a write (`xs.push(v)`, `xs.unshift(v)`, + * `xs.push(...values)`, `xs[i] = v`) stores a value that type's element does + * not hold, or the binding is reassigned, it is checked-dynamic, as the same + * binding is at module scope. Null for other bindings. */ function evolvingArrayType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrType | null { if (!ts.isIdentifier(decl.name) || decl.initializer === undefined) return null; let initializer: ts.Expression = decl.initializer; @@ -4823,6 +4824,9 @@ function evolvingArrayType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrTy }; const scope = owner(decl); let result: IrType | null = null; + // The values the binding's writes store, checked against the element + // type once the reads settle it. + const written: ts.Expression[] = []; // The file's identifiers are indexed by name once (identifiersNamed), so // each binding resolves only the uses spelled like it. for (const node of identifiersNamed(lowerer, ts.getSourceFileOfNode(decl), decl.name.text)) { @@ -4842,6 +4846,22 @@ function evolvingArrayType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrTy parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment; if (reassigned) return DYN; + if ( + ts.isPropertyAccessExpression(parent) && + parent.expression === target && + (parent.name.text === "push" || parent.name.text === "unshift") && + ts.isCallExpression(parent.parent) && + parent.parent.expression === parent + ) + written.push(...parent.parent.arguments.nodes); + else if ( + ts.isElementAccessExpression(parent) && + parent.expression === target && + ts.isBinaryExpression(parent.parent) && + parent.parent.left === parent && + parent.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken + ) + written.push(parent.parent.right); const t = lowerer.typeOf(node); if (unsettled(t)) continue; const mapped = lowerer.mapTypeOf(t); @@ -4849,7 +4869,49 @@ function evolvingArrayType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrTy return DYN; result = mapped; } - return result ?? DYN; + if (result === null || result.kind !== "array") return DYN; + // The element types a write stores: its value's, or for a spread + // (`xs.push(...values)`) the source array's element or tuple's elements. + // Another spread source stores elements this check cannot see. + const storedTypes = (value: ts.Expression): ts.Type[] | null => { + if (!ts.isSpreadElement(value)) return [lowerer.typeOf(value)]; + const source = lowerer.typeOf(value.expression); + if (lowerer.checker.isArrayType(source) || lowerer.checker.isTupleType(source)) + return [...lowerer.checker.getTypeArguments(source as ts.TypeReference)]; + return null; + }; + for (const value of written) { + const stored = storedTypes(value); + if (stored === null) return DYN; + for (const type of stored) { + const t = lowerer.mapTypeOf(lowerer.checker.getBaseTypeOfLiteralType(type)); + if (!t || (!typeEquals(t, result.elem) && lowerer.widthLiftPlan(t, result.elem) === null)) + return DYN; + } + } + return result; +} + +/** An unannotated alias of an evolving array binding whose reads disagree + * (`const alias = xs` between `xs.push(1)` and `xs.push("two")`): the + * checker types the alias by the elements written before it, but both names + * hold one array, so the alias takes the binding's checked-dynamic type and + * sees the later writes. Aliases of such an alias follow it. Null for other + * bindings. */ +function evolvingArrayAliasType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrType | null { + let current = decl; + for (let depth = 0; depth < 8; depth++) { + let initializer = current.initializer; + while (initializer !== undefined && ts.isParenthesizedExpression(initializer)) + initializer = initializer.expression; + if (initializer === undefined || !ts.isIdentifier(initializer)) return null; + const source = lowerer.checker.getSymbolAtLocation(initializer)?.valueDeclaration; + if (source === undefined || !ts.isVariableDeclaration(source) || source.type) return null; + const evolved = evolvingArrayType(lowerer, source); + if (evolved !== null) return evolved.kind === "dyn" ? evolved : null; + current = source; + } + return null; } /** A single static type for an initializer-less JavaScript `let` whose @@ -5565,7 +5627,7 @@ export function lowerVarDecl( !decl.type && !isJsSourceFile(ts.getSourceFileOfNode(decl)) ) { - const evolved = evolvingArrayType(lowerer, decl); + const evolved = evolvingArrayType(lowerer, decl) ?? evolvingArrayAliasType(lowerer, decl); if (evolved !== null) { type = evolved; init = lowerer.lowerExprExpecting(decl.initializer, evolved); @@ -11484,11 +11546,13 @@ function lowerForIn(lowerer: Lowerer, stmt: ts.ForInStatement): IrStmt { keysT, loc, ); + // The overflow map's hashed key set answers presence directly; a + // fresh key list per visit would make the walk quadratic. const guard = (kRef: IrExpr): IrExpr => ({ - kind: "arrIntrinsic", - method: "includes", - receiver: { kind: "recordOvfKeys", obj: recvRef(), shapeId, type: keysT, loc }, - args: [kRef], + kind: "recordOvfHas", + obj: recvRef(), + shapeId, + key: kRef, type: BOOL, loc, }); diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 747e396af..34b345c94 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -230,6 +230,7 @@ import { arrayOf, BOOL, canAdaptDynFuncTo, + canBoxFuncIntoDyn, canDynCheckTo, canConvertToDyn, DYN, @@ -445,6 +446,7 @@ import { lowerNestedFunctionDecl, lowerLambda, lowerFunction, + blockedBodyDiagnostics, } from "./lower-functions.js"; import { lowerArrayMethodCall } from "./containers/array-methods.js"; import { type ArrayHofInfo, fuseArrayPipelines } from "./containers/array-pipelines.js"; @@ -1347,6 +1349,7 @@ export function lowerToIr( // travel with them: a failed statement is never left unexplained. const covered: LowerResult = { ...result, + diagnostics: [...result.diagnostics, ...remainder.reachedBlockedBodyDiagnostics], stats: { ...result.stats, statementsDynamic }, unreached: { diagnostics: rem.diagnostics, @@ -6621,7 +6624,10 @@ export class Lowerer { node.expression.name.text, node.arguments.nodes.length >= 2, ); - const callback = node.arguments.nodes[0]; + // `xs.map(((x) => x))`: the parenthesized callback is the arrow. + let callback = node.arguments.nodes[0]; + while (callback !== undefined && ts.isParenthesizedExpression(callback)) + callback = callback.expression; if (callbackIndices !== null && callback !== undefined && !ts.isSpreadElement(callback)) { const receiverNode = node.expression.expression; const receiverTs = this.typeOf(receiverNode); @@ -6680,13 +6686,26 @@ export class Lowerer { presentSites.set(node, elementsPresent && !reduce); if (elementsPresent && reduce && retypeInlineCallback(callback, callbackIndices)) changed = true; - if ( - (method === "reduce" || method === "reduceRight") && - ((!elementsPresent && node.arguments.nodes.length < 2) || - callbackReturnsOptional(callback)) - ) { - if (promoteHofCallback(callback, [0, 1])) changed = true; - const result = this.runtimeOptionalType(this.irTypeOf(node)); + // The accumulator holds undefined when the callback can return + // it, when an initial value can be missing, or when elements can + // be absent and there is no initial value. It holds another + // primitive when the callback's result can be one at run time + // (`missing[0] ?? "fallback"`). + const initial = node.arguments.nodes[1]; + const optional = + (!elementsPresent && initial === undefined) || + (initial !== undefined && !ts.isSpreadElement(initial) && mayBeOptional(initial)) || + callbackReturnsOptional(callback); + const arithmetic = + reduce && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) + ? lambdaArithmeticReturn(callback) + : null; + if (reduce && (optional || arithmetic !== null)) { + if (optional && promoteHofCallback(callback, [0, 1])) changed = true; + let result = this.irTypeOf(node); + if (arithmetic !== null) + result = joinPrimitiveTypes([result, arithmetic]) ?? result; + if (optional) result = this.runtimeOptionalType(result); siteDependencies?.read(node); const previous = this.runtimeOptionalReduceTypes.get(node); if (!previous || !typeEquals(previous, result)) { @@ -7446,7 +7465,10 @@ export class Lowerer { if (sig && !this.wantBody(sig.name)) continue; const fn = this.lowerFunction(decl); if (fn) functions.push(fn); - else if (this.countsSkips()) this.countSkippedBody(decl); + else if (this.countsSkips()) { + this.countSkippedBody(decl); + if (this.remainder) this.reportBlockedBody(decl); + } } for (const decl of fp.classDecls) { const info = this.classes.get(this.classNamer(decl)); @@ -7777,6 +7799,19 @@ export class Lowerer { } } + /** Coverage remainder: the body diagnostics of a function whose + * signature blocked (blockedBodyDiagnostics). A function reachable emit + * reached (its signature's diagnostics flushed there) reports them with + * the reached diagnostics; any other reports them as unreached. */ + readonly reachedBlockedBodyDiagnostics: ScrDiagnostic[] = []; + private reportBlockedBody(decl: ts.FunctionDeclaration): void { + const diags = blockedBodyDiagnostics(this, decl); + const symbol = declSymbolOf(this, decl); + if (symbol && this.alreadyFlushed.has(symbol)) + this.reachedBlockedBodyDiagnostics.push(...diags); + else for (const d of diags) this.pushDiag(d); + } + /** Whether run() counts a signature-blocked declaration in * stats.functionsSkipped: whole-program passes and the coverage * remainder do; an externally-gated emit pass leaves the counting to the @@ -9300,6 +9335,21 @@ export class Lowerer { * type-mapper.ts's chalk-shape mapping — call paths and func-slot coercions * share it. */ hybridCallUnwrap(expr: IrExpr): IrExpr { + // A callable CLASS instance: a class whose merged interface carries a + // call signature stores its call behavior in a `%call` field (the + // chalk package shim's builder). Calls read the field. + if (expr.type.kind === "object") { + const callType = this.classes.get(expr.type.className)?.fields.get("%call"); + if (callType === undefined || callType.kind !== "func") return expr; + return { + kind: "fieldGet", + obj: expr, + className: expr.type.className, + field: "%call", + type: callType, + loc: expr.loc, + }; + } if (expr.type.kind !== "record") return expr; const shape = this.shapes.get(expr.type.shapeId); const call = shape?.fields.find((f) => f.name === "%call"); @@ -9630,9 +9680,27 @@ export class Lowerer { // A HYBRID (function-with-properties) record flowing into a plain // func slot extracts its reserved %call field — the chalk shape's // "the value IS callable" half (type-mapper.ts's hybrid mapping). - if (expected.kind === "func" && expr.type.kind === "record") { + if (expected.kind === "func" && (expr.type.kind === "record" || expr.type.kind === "object")) { const un = this.hybridCallUnwrap(expr); if (un !== expr && typeEquals(un.type, expected)) return un; + // A callable class instance (the chalk shim's variadic builder) in a + // narrower function slot: its call closure crosses the checked + // function boundary, which adapts the arity. + if ( + un !== expr && + expr.type.kind === "object" && + un.type.kind === "func" && + canBoxFuncIntoDyn( + un.type, + (id) => this.shapes.get(id), + (id) => this.unions.get(id), + ) + ) { + return this.coerceToExpected( + { kind: "dynFrom", value: un, type: DYN, loc: un.loc }, + expected, + ); + } } // A zero-param function whose RETURN is a wider record array than the // slot's (`getRoutes: () => cachedRoutes` against `() => Narrow[]`): diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 0167adf12..cb16fd257 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -1590,6 +1590,12 @@ export const BUILTIN_MODULE_FNS: Record< createDiffieHellman: { fn: "crypto.dhNew", params: [DYN, DYN, DYN, DYN], result: DYN }, getDiffieHellman: { fn: "crypto.dhGroupNew", params: [DYN], result: DYN }, createDiffieHellmanGroup: { fn: "crypto.dhGroupNew", params: [DYN], result: DYN }, + // Prime checks and generation (lowerPrimeCall): checked-dynamic + // arguments, results converted to the call site's type. + checkPrimeSync: { fn: "crypto.checkPrimeSync", params: [], result: DYN }, + checkPrime: { fn: "crypto.checkPrimeCb", params: [], result: VOID }, + generatePrimeSync: { fn: "crypto.generatePrimeSync", params: [], result: DYN }, + generatePrime: { fn: "crypto.generatePrimeCb", params: [], result: VOID }, }, zlib: { // The default-options convenience family is special-cased in @@ -3099,7 +3105,8 @@ export const BUILTIN_MODULE_FENCE_HINTS: Record< ["getCipherInfo"].map((m) => [ m, "cipher metadata has no lowering — createCipheriv and createDecipheriv support " + - "aes-128/192/256 in cbc, ctr, and gcm modes and chacha20-poly1305", + "aes-128/192/256 in cbc, ctr, gcm, ecb, cfb, cfb8, cfb1, and ofb modes, des-ede3 and " + + "des-ede, chacha20, and chacha20-poly1305", ]), ), setFips: diff --git a/packages/compiler/src/frontend/package-shims.ts b/packages/compiler/src/frontend/package-shims.ts index 86a41d3a6..a0c2e4cb9 100644 --- a/packages/compiler/src/frontend/package-shims.ts +++ b/packages/compiler/src/frontend/package-shims.ts @@ -62,9 +62,20 @@ interface ImplementationShim { * of it (a __commonJS chunk whose key names the package, its version * and this file) is the package itself. */ mainFile: string; + /** The package's declaration entry relative to its directory, when it + * is not `index.d.ts`. */ + typesFile?: string; + /** An alternative entry for this installation (relative to the shim + * root), or undefined for the version's own entry. */ + variant?: (pkgDir: string) => string | undefined; + /** Every variant entry, for the guard test that compiles each one. */ + variants?: readonly string[]; } -const IMPLEMENTATION_SHIMS: ReadonlyMap = new Map([ +const IMPLEMENTATION_SHIMS: ReadonlyMap = new Map< + string, + ImplementationShim +>([ [ "fs-extra", { @@ -79,6 +90,33 @@ const IMPLEMENTATION_SHIMS: ReadonlyMap = new Map([ ]), }, ], + [ + "chalk", + { + name: "chalk", + defaultExport: true, + mainFile: "source/index.js", + typesFile: "source/index.d.ts", + versions: new Map([[5, "chalk/v5.ts"]]), + }, + ], + [ + "hot-shots", + { + name: "hot-shots", + defaultExport: true, + mainFile: "index.js", + typesFile: "types.d.ts", + versions: new Map([[8, "hot-shots/v8.ts"]]), + // hot-shots' uds protocol loads the optional unix-dgram addon: where + // it is installed, the entry with the native Unix datagram transport. + variant: (pkgDir: string) => + installedPackageDir(join(pkgDir, "index.js"), "unix-dgram") === null + ? undefined + : "hot-shots/v8-unix-dgram.ts", + variants: ["hot-shots/v8-unix-dgram.ts"], + }, + ], [ "undici", { @@ -141,8 +179,10 @@ export function throughBundledShimForwarder(resolved: string): string { * the guard test that compiles each one. */ export function packageShimEntries(): string[] { const entries = new Set(); - for (const shim of IMPLEMENTATION_SHIMS.values()) + for (const shim of IMPLEMENTATION_SHIMS.values()) { for (const entry of shim.versions.values()) entries.add(join(packageShimRoot(), entry)); + for (const entry of shim.variants ?? []) entries.add(join(packageShimRoot(), entry)); + } return [...entries].sort(); } @@ -240,7 +280,8 @@ function shimEntryForPackageDir(pkgDir: string, expectedName: string | null): st if (shim === undefined) return null; const major = Number(/^(\d+)\./.exec(pkg.version)?.[1] ?? NaN); const entry = shim.versions.get(major); - return entry === undefined ? null : join(packageShimRoot(), entry); + if (entry === undefined) return null; + return join(packageShimRoot(), shim.variant?.(pkgDir) ?? entry); } /** The shim module `specifier` resolves to from `fromFile`, or null. Only @@ -280,16 +321,33 @@ export function isShimmedImport(fromFile: string, specifier: string): boolean { return isUnloadedPackage(specifier) || packageShimFor(fromFile, specifier) !== null; } +/** The shimmed package whose declaration entry `path` is, with the + * package directory: `/index.d.ts`, or the entry's own `typesFile`. */ +function shimOfDeclarationPath(path: string): { pkgDir: string; shim: ImplementationShim } | null { + if (!path.endsWith(".d.ts")) return null; + if (path.endsWith("/index.d.ts")) { + const pkgDir = dirname(path); + const shim = IMPLEMENTATION_SHIMS.get(pkgDir.slice(pkgDir.lastIndexOf("/") + 1)); + if (shim !== undefined && shim.typesFile === undefined) return { pkgDir, shim }; + } + for (const shim of IMPLEMENTATION_SHIMS.values()) { + if (shim.typesFile === undefined) continue; + const suffix = `/${shim.name}/${shim.typesFile}`; + if (path.endsWith(suffix)) + return { pkgDir: path.slice(0, path.length - suffix.length + shim.name.length + 1), shim }; + } + return null; +} + /** The fs-shadow answer for a shimmed package's declaration entry: the * installed `index.d.ts` reads as a re-export of the version's shim module, * so the checker types each importer against its own version. Undefined * for every other path. */ export function packageShimDeclarationText(path: string): string | undefined { - if (!path.endsWith("/index.d.ts")) return undefined; - const pkgDir = dirname(path); - const name = pkgDir.slice(pkgDir.lastIndexOf("/") + 1); - const shim = IMPLEMENTATION_SHIMS.get(name); - if (shim === undefined) return undefined; + const located = shimOfDeclarationPath(path); + if (located === null) return undefined; + const { pkgDir, shim } = located; + const name = shim.name; // A DefinitelyTyped twin (`@types/fs-extra`) types a package that ships no // declarations. Nothing at its path names the installed runtime version, // so it reads as the shim only when every supported version shares one @@ -316,6 +374,12 @@ export function isPackageShimFile(fileName: string): boolean { return fileName.startsWith(root.endsWith("/") ? root : `${root}/`); } +/** True for a name a shipped shim may declare as a bodiless runtime + * intrinsic (lower-calls.ts lowers its calls to runtime calls). */ +export function isShimIntrinsicName(name: string): boolean { + return name.startsWith("scriptcUnixDgram"); +} + /** Resets per-compile caches. */ export function clearPackageShimCaches(): void { installedCache.clear(); diff --git a/packages/compiler/src/frontend/process-fork.ts b/packages/compiler/src/frontend/process-fork.ts new file mode 100644 index 000000000..744d57aa5 --- /dev/null +++ b/packages/compiler/src/frontend/process-fork.ts @@ -0,0 +1,26 @@ +/** Process primitives for forked lowering workers (unit-journal.ts). The + * native compiler fills these in; under Node they stay unset and reachable + * emit lowers every wave in-process. */ +export interface ProcessFork { + /** fork(): 0 in the worker, the worker's pid in the parent, -1 on failure. */ + fork(): number; + /** Ends the worker without running exit handlers. */ + exit(code: number): void; + /** The worker's exit status, 128 plus a signal number, or -1. */ + wait(pid: number): number; + /** Opens the lock file checker requests from workers take. */ + openLock(path: string): boolean; + lock(): void; + unlock(): void; +} + +export const processFork: { + primitives: ProcessFork | null; + /** True in a worker: checker requests take the lock (rpc-client.ts). */ + worker: boolean; + /** A worker's checker requests and the time it waited for the lock and + * for responses (SCRIPTC_TIMING). */ + requests: number; + lockWaitMs: number; + requestMs: number; +} = { primitives: null, worker: false, requests: 0, lockWaitMs: 0, requestMs: 0 }; diff --git a/packages/compiler/src/frontend/ts7/ast-node.test.ts b/packages/compiler/src/frontend/ts7/ast-node.test.ts index 6b2725b93..8b391e622 100644 --- a/packages/compiler/src/frontend/ts7/ast-node.test.ts +++ b/packages/compiler/src/frontend/ts7/ast-node.test.ts @@ -169,6 +169,30 @@ function ids( return nodes?.map((n) => (n as OracleNode).index); } +test("generated child getters select the child their name selects", () => { + // These getters adapt their raw child; their slot functions are compared instead. + const adapted = ["body", "initializer", "left", "modifiers", "operand", "questionToken"]; + const names = [...new Set(Object.values(childProperties).flat())]; + const lists = (value: unknown): unknown => (value instanceof AstNodeList ? value.nodes : value); + let compared = 0; + for (const { file } of decoded.values()) { + walkPreorder(file.root, (node) => { + const view = node as unknown as Record; + for (const name of names) { + if (adapted.includes(name)) { + const slot = (view[`${name}Slot`] as () => number).call(node); + expect(slot, `${node.kind}.${name}`).toBe(astChildOrder(node.kind, name)); + } else { + expect(lists(view[name]), `${node.kind}.${name}`).toBe(lists(node.child(name))); + } + compared++; + } + return undefined; + }); + } + expect(compared).toBeGreaterThan(100_000); +}); + test("the writeTargets option answers write targets without their wrappers", () => { const { bytes, file: plain } = decoded.get("targets.ts")!; const file = new AstFile(bytes, undefined, { writeTargets: true }); diff --git a/packages/compiler/src/frontend/ts7/ast-node.ts b/packages/compiler/src/frontend/ts7/ast-node.ts index be2745616..92c71aeba 100644 --- a/packages/compiler/src/frontend/ts7/ast-node.ts +++ b/packages/compiler/src/frontend/ts7/ast-node.ts @@ -85,6 +85,8 @@ export class AstFile { private readonly strings = new Map(); private lines: number[] | undefined; private childIndices: Uint32Array | undefined; + /** The root source file's name (AstNode.fileName). */ + private rootFileName: string | undefined; /** Whether nodes answer write targets without their wrappers (see * AstFileOptions.writeTargets). */ @@ -113,7 +115,8 @@ export class AstFile { } node(index: number): AstNode { - if (Object.hasOwn(this.nodes, index)) return this.nodes[index]!; + const existing: AstNode | undefined = this.nodes[index]; + if (existing !== undefined) return existing; if (index === 0 || this.wire.kind(index) === KIND_NODE_LIST) throw new AstDecodeError("expected a node index"); const node = new AstNode(this, index); @@ -140,8 +143,13 @@ export class AstFile { } namedChild(node: AstNode, name: string): number { + return this.namedChildAt(node, astChildOrder(node.kind, name)); + } + + /** The child in wire slot `order` (astChildOrder), or 0 when the slot is + * negative or empty. */ + namedChildAt(node: AstNode, order: number): number { if (node.file !== this) throw new AstDecodeError("named child belongs to another source file"); - const order = astChildOrder(node.kind, name); if (order < 0) return 0; const mask = node.data >>> 30 === 0 ? node.data & 0xff : 0xff; if ((mask & (1 << order)) === 0) return 0; @@ -202,6 +210,15 @@ export class AstFile { return result; } + /** The file name of the source file node at `index`; the root's is + * read once. */ + sourceFileName(index: number): string { + if (index === 1 && this.rootFileName !== undefined) return this.rootFileName; + const name = this.wire.string(this.wire.extendedWord(index, 4)); + if (index === 1) this.rootFileName = name; + return name; + } + lineStarts(): number[] { if (this.lines === undefined) this.lines = astLineStarts(this.root.text ?? ""); return this.lines; @@ -327,6 +344,22 @@ export class AstNode { const index = this.file.namedChild(this, name); return index === 0 ? undefined : this.file.node(index); } + /** childNode by wire slot, as the generated getters select it. */ + childNodeAt(order: number): AstNode | undefined { + const index = this.file.namedChildAt(this, order); + return index === 0 ? undefined : this.file.node(index); + } + childListAt(order: number): AstNodeList | undefined { + const index = this.file.namedChildAt(this, order); + return index === 0 ? undefined : this.file.list(index); + } + childAt(order: number): AstNode | AstNodeList | undefined { + const index = this.file.namedChildAt(this, order); + if (index === 0) return undefined; + return this.file.wire.kind(index) === KIND_NODE_LIST + ? this.file.list(index) + : this.file.node(index); + } childList(name: string): AstNodeList | undefined { const index = this.file.namedChild(this, name); return index === 0 ? undefined : this.file.list(index); @@ -564,18 +597,18 @@ export class AstNode { } get modifierFlags(): number { let result = 0; - for (const modifier of this.childList("modifiers")?.nodes ?? []) + for (const modifier of this.childListAt(this.modifiersSlot())?.nodes ?? []) result |= astModifierFlag(modifier.kind); return result; } get modifiers(): AstModifierList | undefined { - const index = this.file.namedChild(this, "modifiers"); + const index = this.file.namedChildAt(this, this.modifiersSlot()); return index === 0 ? undefined : this.file.modifierList(index); } get body(): AstNode | undefined { - const body = this.childNode("body"); + const body = this.childNodeAt(this.bodySlot()); // TS7 can serialize a reparsed @type callable before a method's // body without setting the type presence bit. The linked Block is // still the real body; keep the JSDoc node out of this view. @@ -597,11 +630,11 @@ export class AstNode { } get questionToken(): AstNode | undefined { - const direct = this.childNode("questionToken"); + const direct = this.childNodeAt(this.questionTokenSlot()); if (direct !== undefined) return direct; // TypeScript 7 shares one postfix slot for optional and definite- // assignment declarations. The familiar helper surface keeps them apart. - const token = this.childNode("postfixToken"); + const token = this.postfixToken; return token?.kind === SyntaxKind.QuestionToken ? token : undefined; } @@ -609,21 +642,21 @@ export class AstNode { // tree; with AstFileOptions.writeTargets these views return the target // without them. get initializer(): AstNode | undefined { - const initializer = this.childNode("initializer"); + const initializer = this.childNodeAt(this.initializerSlot()); return initializer !== undefined && this.file.writeTargets && writesTarget(this, "initializer") ? unwrapTarget(initializer) : initializer; } get left(): AstNode | undefined { - const left = this.childNode("left"); + const left = this.childNodeAt(this.leftSlot()); return left !== undefined && this.file.writeTargets && writesTarget(this, "left") ? unwrapTarget(left) : left; } get operand(): AstNode | undefined { - const operand = this.childNode("operand"); + const operand = this.childNodeAt(this.operandSlot()); return operand !== undefined && this.file.writeTargets && writesTarget(this, "operand") ? unwrapTarget(operand) : operand; @@ -632,281 +665,1173 @@ export class AstNode { // BEGIN GENERATED CHILD GETTERS // Generated from typescript@7.0.2; run scripts/generate-ts7-ast-schema.mjs. get argument(): AstNode | undefined { - return this.childNode("argument"); + switch (this.kind) { + case 206: + return this.childNodeAt(0); + default: + return undefined; + } } get argumentExpression(): AstNode | undefined { - return this.childNode("argumentExpression"); + switch (this.kind) { + case 213: + return this.childNodeAt(2); + default: + return undefined; + } } get arguments(): AstNodeList | undefined { - return this.childList("arguments"); + switch (this.kind) { + case 215: + return this.childListAt(2); + case 214: + return this.childListAt(3); + default: + return undefined; + } } get assertsModifier(): AstNode | undefined { - return this.childNode("assertsModifier"); + switch (this.kind) { + case 183: + return this.childNodeAt(0); + default: + return undefined; + } } get asteriskToken(): AstNode | undefined { - return this.childNode("asteriskToken"); + switch (this.kind) { + case 230: + return this.childNodeAt(0); + case 175: + case 219: + case 263: + return this.childNodeAt(1); + default: + return undefined; + } } get attributes(): AstNode | AstNodeList | undefined { - return this.child("attributes"); + switch (this.kind) { + case 301: + return this.childAt(0); + case 206: + return this.childAt(1); + case 286: + case 287: + return this.childAt(2); + case 273: + case 279: + case 343: + case 346: + return this.childAt(3); + default: + return undefined; + } } get awaitModifier(): AstNode | undefined { - return this.childNode("awaitModifier"); + switch (this.kind) { + case 250: + case 251: + return this.childNodeAt(0); + default: + return undefined; + } } get block(): AstNode | undefined { - return this.childNode("block"); + switch (this.kind) { + case 300: + return this.childNodeAt(1); + default: + return undefined; + } } get caseBlock(): AstNode | undefined { - return this.childNode("caseBlock"); + switch (this.kind) { + case 256: + return this.childNodeAt(1); + default: + return undefined; + } } get catchClause(): AstNode | undefined { - return this.childNode("catchClause"); + switch (this.kind) { + case 259: + return this.childNodeAt(1); + default: + return undefined; + } } get checkType(): AstNode | undefined { - return this.childNode("checkType"); + switch (this.kind) { + case 195: + return this.childNodeAt(0); + default: + return undefined; + } } get children(): AstNode | AstNodeList | undefined { - return this.child("children"); + switch (this.kind) { + case 344: + return this.childAt(0); + case 285: + case 289: + return this.childAt(1); + default: + return undefined; + } } get className(): AstNode | undefined { - return this.childNode("className"); + switch (this.kind) { + case 323: + case 324: + return this.childNodeAt(1); + default: + return undefined; + } } get clauses(): AstNodeList | undefined { - return this.childList("clauses"); + switch (this.kind) { + case 270: + return this.childListAt(0); + default: + return undefined; + } } get closingElement(): AstNode | undefined { - return this.childNode("closingElement"); + switch (this.kind) { + case 285: + return this.childNodeAt(2); + default: + return undefined; + } } get closingFragment(): AstNode | undefined { - return this.childNode("closingFragment"); + switch (this.kind) { + case 289: + return this.childNodeAt(2); + default: + return undefined; + } } get colonToken(): AstNode | undefined { - return this.childNode("colonToken"); + switch (this.kind) { + case 228: + return this.childNodeAt(3); + default: + return undefined; + } } get comment(): AstNodeList | undefined { - return this.childList("comment"); + switch (this.kind) { + case 315: + return this.childListAt(0); + case 322: + case 325: + case 326: + case 327: + case 328: + case 329: + case 330: + return this.childListAt(1); + case 323: + case 324: + case 332: + case 334: + case 335: + case 336: + case 339: + case 341: + case 342: + return this.childListAt(2); + case 331: + case 333: + case 337: + case 338: + case 340: + return this.childListAt(3); + case 343: + return this.childListAt(4); + default: + return undefined; + } } get condition(): AstNode | undefined { - return this.childNode("condition"); + switch (this.kind) { + case 228: + return this.childNodeAt(0); + case 249: + return this.childNodeAt(1); + default: + return undefined; + } } get constraint(): AstNode | undefined { - return this.childNode("constraint"); + switch (this.kind) { + case 337: + return this.childNodeAt(1); + case 169: + return this.childNodeAt(2); + default: + return undefined; + } } get declarationList(): AstNode | undefined { - return this.childNode("declarationList"); + switch (this.kind) { + case 244: + return this.childNodeAt(1); + default: + return undefined; + } } get declarations(): AstNodeList | undefined { - return this.childList("declarations"); + switch (this.kind) { + case 262: + return this.childListAt(0); + default: + return undefined; + } } get defaultType(): AstNode | undefined { - return this.childNode("defaultType"); + switch (this.kind) { + case 169: + return this.childNodeAt(4); + default: + return undefined; + } } get dotDotDotToken(): AstNode | undefined { - return this.childNode("dotDotDotToken"); + switch (this.kind) { + case 203: + case 209: + case 295: + return this.childNodeAt(0); + case 170: + return this.childNodeAt(1); + default: + return undefined; + } } get elementType(): AstNode | undefined { - return this.childNode("elementType"); + switch (this.kind) { + case 189: + return this.childNodeAt(0); + default: + return undefined; + } } get elements(): AstNodeList | undefined { - return this.childList("elements"); + switch (this.kind) { + case 190: + case 207: + case 208: + case 210: + case 276: + case 280: + return this.childListAt(0); + default: + return undefined; + } } get elseStatement(): AstNode | undefined { - return this.childNode("elseStatement"); + switch (this.kind) { + case 246: + return this.childNodeAt(2); + default: + return undefined; + } } get endOfFileToken(): AstNode | undefined { - return this.childNode("endOfFileToken"); + switch (this.kind) { + case 307: + return this.childNodeAt(1); + default: + return undefined; + } } get equalsGreaterThanToken(): AstNode | undefined { - return this.childNode("equalsGreaterThanToken"); + switch (this.kind) { + case 220: + return this.childNodeAt(4); + default: + return undefined; + } } get equalsToken(): AstNode | undefined { - return this.childNode("equalsToken"); + switch (this.kind) { + case 304: + return this.childNodeAt(4); + default: + return undefined; + } } get exclamationToken(): AstNode | undefined { - return this.childNode("exclamationToken"); + switch (this.kind) { + case 261: + return this.childNodeAt(1); + default: + return undefined; + } } get exportClause(): AstNode | undefined { - return this.childNode("exportClause"); + switch (this.kind) { + case 279: + return this.childNodeAt(1); + default: + return undefined; + } } get exprName(): AstNode | undefined { - return this.childNode("exprName"); + switch (this.kind) { + case 187: + return this.childNodeAt(0); + default: + return undefined; + } } get expression(): AstNode | undefined { - return this.childNode("expression"); + switch (this.kind) { + case 168: + case 171: + case 212: + case 213: + case 214: + case 215: + case 218: + case 221: + case 222: + case 223: + case 224: + case 231: + case 234: + case 235: + case 236: + case 239: + case 240: + case 245: + case 246: + case 248: + case 254: + case 255: + case 256: + case 258: + case 284: + case 294: + case 297: + case 298: + case 305: + case 348: + case 349: + return this.childNodeAt(0); + case 217: + case 230: + case 247: + case 295: + return this.childNodeAt(1); + case 250: + case 251: + case 278: + return this.childNodeAt(2); + case 169: + return this.childNodeAt(3); + default: + return undefined; + } } get extendsType(): AstNode | undefined { - return this.childNode("extendsType"); + switch (this.kind) { + case 195: + return this.childNodeAt(1); + default: + return undefined; + } } get falseType(): AstNode | undefined { - return this.childNode("falseType"); + switch (this.kind) { + case 195: + return this.childNodeAt(3); + default: + return undefined; + } } get finallyBlock(): AstNode | undefined { - return this.childNode("finallyBlock"); + switch (this.kind) { + case 259: + return this.childNodeAt(2); + default: + return undefined; + } } get head(): AstNode | undefined { - return this.childNode("head"); + switch (this.kind) { + case 204: + case 229: + return this.childNodeAt(0); + default: + return undefined; + } } get heritageClauses(): AstNodeList | undefined { - return this.childList("heritageClauses"); + switch (this.kind) { + case 232: + case 264: + case 265: + return this.childListAt(3); + default: + return undefined; + } } get importClause(): AstNode | undefined { - return this.childNode("importClause"); + switch (this.kind) { + case 273: + case 343: + case 346: + return this.childNodeAt(1); + default: + return undefined; + } } get incrementor(): AstNode | undefined { - return this.childNode("incrementor"); + switch (this.kind) { + case 249: + return this.childNodeAt(2); + default: + return undefined; + } } get indexType(): AstNode | undefined { - return this.childNode("indexType"); + switch (this.kind) { + case 200: + return this.childNodeAt(1); + default: + return undefined; + } } get jsdocPropertyTags(): AstNode | undefined { - return this.childNode("jsdocPropertyTags"); + switch (this.kind) { + case 317: + return this.childNodeAt(0); + default: + return undefined; + } } get label(): AstNode | undefined { - return this.childNode("label"); + switch (this.kind) { + case 252: + case 253: + case 257: + return this.childNodeAt(0); + default: + return undefined; + } } get literal(): AstNode | undefined { - return this.childNode("literal"); + switch (this.kind) { + case 202: + return this.childNodeAt(0); + case 205: + case 240: + return this.childNodeAt(1); + default: + return undefined; + } } get members(): AstNodeList | undefined { - return this.childList("members"); + switch (this.kind) { + case 188: + return this.childListAt(0); + case 267: + return this.childListAt(2); + case 232: + case 264: + case 265: + return this.childListAt(4); + case 201: + return this.childListAt(5); + default: + return undefined; + } } get moduleReference(): AstNode | undefined { - return this.childNode("moduleReference"); + switch (this.kind) { + case 272: + return this.childNodeAt(2); + default: + return undefined; + } } get moduleSpecifier(): AstNode | undefined { - return this.childNode("moduleSpecifier"); + switch (this.kind) { + case 273: + case 279: + case 343: + case 346: + return this.childNodeAt(2); + default: + return undefined; + } } get name(): AstNode | undefined { - return this.childNode("name"); + switch (this.kind) { + case 237: + case 261: + case 274: + case 275: + case 281: + case 292: + case 302: + case 306: + case 309: + case 319: + case 320: + case 321: + return this.childNodeAt(0); + case 169: + case 172: + case 173: + case 174: + case 178: + case 179: + case 203: + case 232: + case 264: + case 265: + case 266: + case 267: + case 268: + case 271: + case 272: + case 277: + case 282: + case 296: + case 303: + case 304: + case 333: + case 340: + case 345: + return this.childNodeAt(1); + case 170: + case 175: + case 209: + case 212: + case 219: + case 263: + case 331: + case 338: + return this.childNodeAt(2); + default: + return undefined; + } } get nameExpression(): AstNode | undefined { - return this.childNode("nameExpression"); + switch (this.kind) { + case 339: + return this.childNodeAt(1); + default: + return undefined; + } } get nameType(): AstNode | undefined { - return this.childNode("nameType"); + switch (this.kind) { + case 201: + return this.childNodeAt(2); + default: + return undefined; + } } get namedBindings(): AstNode | undefined { - return this.childNode("namedBindings"); + switch (this.kind) { + case 274: + return this.childNodeAt(1); + default: + return undefined; + } } get namespace(): AstNode | undefined { - return this.childNode("namespace"); + switch (this.kind) { + case 296: + return this.childNodeAt(0); + default: + return undefined; + } } get objectAssignmentInitializer(): AstNode | undefined { - return this.childNode("objectAssignmentInitializer"); + switch (this.kind) { + case 304: + return this.childNodeAt(5); + default: + return undefined; + } } get objectType(): AstNode | undefined { - return this.childNode("objectType"); + switch (this.kind) { + case 200: + return this.childNodeAt(0); + default: + return undefined; + } } get openingElement(): AstNode | undefined { - return this.childNode("openingElement"); + switch (this.kind) { + case 285: + return this.childNodeAt(0); + default: + return undefined; + } } get openingFragment(): AstNode | undefined { - return this.childNode("openingFragment"); + switch (this.kind) { + case 289: + return this.childNodeAt(0); + default: + return undefined; + } } get operatorToken(): AstNode | undefined { - return this.childNode("operatorToken"); + switch (this.kind) { + case 227: + return this.childNodeAt(3); + default: + return undefined; + } } get parameterName(): AstNode | undefined { - return this.childNode("parameterName"); + switch (this.kind) { + case 183: + return this.childNodeAt(1); + default: + return undefined; + } } get parameters(): AstNodeList | undefined { - return this.childList("parameters"); + switch (this.kind) { + case 180: + case 181: + case 182: + case 185: + case 318: + return this.childListAt(1); + case 177: + case 186: + case 220: + return this.childListAt(2); + case 178: + case 179: + return this.childListAt(3); + case 174: + case 219: + case 263: + return this.childListAt(4); + case 175: + return this.childListAt(5); + default: + return undefined; + } } get postfixToken(): AstNode | undefined { - return this.childNode("postfixToken"); + switch (this.kind) { + case 172: + case 173: + case 174: + case 303: + case 304: + return this.childNodeAt(2); + case 175: + return this.childNodeAt(3); + default: + return undefined; + } } get properties(): AstNodeList | undefined { - return this.childList("properties"); + switch (this.kind) { + case 211: + case 293: + return this.childListAt(0); + default: + return undefined; + } } get propertyName(): AstNode | undefined { - return this.childNode("propertyName"); + switch (this.kind) { + case 277: + case 282: + return this.childNodeAt(0); + case 209: + return this.childNodeAt(1); + default: + return undefined; + } } get qualifier(): AstNode | undefined { - return this.childNode("qualifier"); + switch (this.kind) { + case 206: + return this.childNodeAt(2); + default: + return undefined; + } } get questionDotToken(): AstNode | undefined { - return this.childNode("questionDotToken"); + switch (this.kind) { + case 212: + case 213: + case 214: + case 216: + return this.childNodeAt(1); + default: + return undefined; + } } get readonlyToken(): AstNode | undefined { - return this.childNode("readonlyToken"); + switch (this.kind) { + case 201: + return this.childNodeAt(0); + default: + return undefined; + } } get right(): AstNode | undefined { - return this.childNode("right"); + switch (this.kind) { + case 167: + return this.childNodeAt(1); + case 227: + return this.childNodeAt(4); + default: + return undefined; + } } get statement(): AstNode | undefined { - return this.childNode("statement"); + switch (this.kind) { + case 247: + return this.childNodeAt(0); + case 248: + case 255: + case 257: + return this.childNodeAt(1); + case 249: + case 250: + case 251: + return this.childNodeAt(3); + default: + return undefined; + } } get statements(): AstNodeList | undefined { - return this.childList("statements"); + switch (this.kind) { + case 242: + case 269: + case 307: + return this.childListAt(0); + case 297: + case 298: + return this.childListAt(1); + default: + return undefined; + } } get tag(): AstNode | undefined { - return this.childNode("tag"); + switch (this.kind) { + case 216: + return this.childNodeAt(0); + default: + return undefined; + } } get tagName(): AstNode | undefined { - return this.childNode("tagName"); + switch (this.kind) { + case 286: + case 287: + case 288: + case 322: + case 323: + case 324: + case 325: + case 326: + case 327: + case 328: + case 329: + case 330: + case 331: + case 332: + case 333: + case 334: + case 335: + case 336: + case 337: + case 338: + case 339: + case 340: + case 341: + case 342: + case 343: + return this.childNodeAt(0); + default: + return undefined; + } } get tags(): AstNodeList | undefined { - return this.childList("tags"); + switch (this.kind) { + case 315: + return this.childListAt(1); + default: + return undefined; + } } get template(): AstNode | undefined { - return this.childNode("template"); + switch (this.kind) { + case 216: + return this.childNodeAt(3); + default: + return undefined; + } } get templateSpans(): AstNodeList | undefined { - return this.childList("templateSpans"); + switch (this.kind) { + case 204: + case 229: + return this.childListAt(1); + default: + return undefined; + } } get thenStatement(): AstNode | undefined { - return this.childNode("thenStatement"); + switch (this.kind) { + case 246: + return this.childNodeAt(1); + default: + return undefined; + } } get thisArg(): AstNode | undefined { - return this.childNode("thisArg"); + switch (this.kind) { + case 349: + return this.childNodeAt(1); + default: + return undefined; + } } get trueType(): AstNode | undefined { - return this.childNode("trueType"); + switch (this.kind) { + case 195: + return this.childNodeAt(2); + default: + return undefined; + } } get tryBlock(): AstNode | undefined { - return this.childNode("tryBlock"); + switch (this.kind) { + case 259: + return this.childNodeAt(0); + default: + return undefined; + } } get tupleNameSource(): AstNode | undefined { - return this.childNode("tupleNameSource"); + switch (this.kind) { + case 238: + return this.childNodeAt(0); + default: + return undefined; + } } get type(): AstNode | undefined { - return this.childNode("type"); + switch (this.kind) { + case 191: + case 192: + case 197: + case 199: + case 205: + case 217: + case 308: + case 311: + case 312: + case 313: + case 314: + return this.childNodeAt(0); + case 235: + case 239: + case 278: + return this.childNodeAt(1); + case 180: + case 181: + case 182: + case 183: + case 185: + case 227: + case 261: + case 318: + return this.childNodeAt(2); + case 172: + case 173: + case 177: + case 186: + case 203: + case 220: + case 266: + case 303: + case 304: + case 345: + return this.childNodeAt(3); + case 170: + case 178: + case 179: + case 201: + return this.childNodeAt(4); + case 174: + case 219: + case 263: + return this.childNodeAt(5); + case 175: + return this.childNodeAt(6); + default: + return undefined; + } } get typeArguments(): AstNodeList | undefined { - return this.childList("typeArguments"); + switch (this.kind) { + case 184: + case 187: + case 215: + case 234: + case 286: + case 287: + return this.childListAt(1); + case 214: + case 216: + return this.childListAt(2); + case 206: + return this.childListAt(3); + default: + return undefined; + } } get typeExpression(): AstNode | undefined { - return this.childNode("typeExpression"); + switch (this.kind) { + case 331: + case 332: + case 334: + case 335: + case 336: + case 338: + case 341: + case 342: + return this.childNodeAt(1); + case 333: + case 340: + return this.childNodeAt(2); + default: + return undefined; + } } get typeName(): AstNode | undefined { - return this.childNode("typeName"); + switch (this.kind) { + case 184: + return this.childNodeAt(0); + default: + return undefined; + } } get typeParameter(): AstNode | undefined { - return this.childNode("typeParameter"); + switch (this.kind) { + case 196: + return this.childNodeAt(0); + case 201: + return this.childNodeAt(1); + default: + return undefined; + } } get typeParameters(): AstNodeList | undefined { - return this.childList("typeParameters"); + switch (this.kind) { + case 180: + case 181: + case 185: + case 318: + return this.childListAt(0); + case 177: + case 186: + case 220: + return this.childListAt(1); + case 178: + case 179: + case 232: + case 264: + case 265: + case 266: + case 337: + case 345: + return this.childListAt(2); + case 174: + case 219: + case 263: + return this.childListAt(3); + case 175: + return this.childListAt(4); + default: + return undefined; + } } get types(): AstNodeList | undefined { - return this.childList("types"); + switch (this.kind) { + case 193: + case 194: + case 299: + return this.childListAt(0); + default: + return undefined; + } } get value(): AstNode | undefined { - return this.childNode("value"); + switch (this.kind) { + case 302: + return this.childNodeAt(1); + default: + return undefined; + } } get variableDeclaration(): AstNode | undefined { - return this.childNode("variableDeclaration"); + switch (this.kind) { + case 300: + return this.childNodeAt(0); + default: + return undefined; + } } get whenFalse(): AstNode | undefined { - return this.childNode("whenFalse"); + switch (this.kind) { + case 228: + return this.childNodeAt(4); + default: + return undefined; + } } get whenTrue(): AstNode | undefined { - return this.childNode("whenTrue"); + switch (this.kind) { + case 228: + return this.childNodeAt(2); + default: + return undefined; + } + } + private bodySlot(): number { + switch (this.kind) { + case 176: + return 1; + case 268: + return 2; + case 177: + return 4; + case 178: + case 179: + case 220: + return 5; + case 219: + case 263: + return 6; + case 175: + return 7; + default: + return -1; + } + } + private initializerSlot(): number { + switch (this.kind) { + case 249: + return 0; + case 250: + case 251: + case 292: + case 306: + return 1; + case 209: + case 261: + return 3; + case 172: + case 173: + case 303: + return 4; + case 170: + return 5; + default: + return -1; + } + } + private leftSlot(): number { + switch (this.kind) { + case 167: + return 0; + case 227: + return 1; + default: + return -1; + } + } + private modifiersSlot(): number { + switch (this.kind) { + case 169: + case 170: + case 172: + case 173: + case 174: + case 175: + case 176: + case 177: + case 178: + case 179: + case 182: + case 186: + case 219: + case 220: + case 227: + case 232: + case 244: + case 263: + case 264: + case 265: + case 266: + case 267: + case 268: + case 271: + case 272: + case 273: + case 278: + case 279: + case 283: + case 303: + case 304: + case 345: + case 346: + return 0; + default: + return -1; + } + } + private operandSlot(): number { + switch (this.kind) { + case 225: + case 226: + return 0; + default: + return -1; + } + } + private questionTokenSlot(): number { + switch (this.kind) { + case 228: + return 1; + case 203: + return 2; + case 170: + case 201: + return 3; + default: + return -1; + } } // END GENERATED CHILD GETTERS // Source-file fields stay on the same nominal class, matching the wire's // single node representation. Their extended-data accesses are checked. get fileName(): string { - return this.file.wire.string(this.file.wire.extendedWord(this.index, 4)); + return this.file.sourceFileName(this.index); } get path(): string { return this.file.wire.string(this.file.wire.extendedWord(this.index, 8)); diff --git a/packages/compiler/src/frontend/ts7/ast-wire.ts b/packages/compiler/src/frontend/ts7/ast-wire.ts index ff70148df..9dd8c9f5c 100644 --- a/packages/compiler/src/frontend/ts7/ast-wire.ts +++ b/packages/compiler/src/frontend/ts7/ast-wire.ts @@ -240,12 +240,12 @@ export class AstWireFile { string(index: number): string { if (index >>> 0 !== index || index % 2 !== 0 || index >= this.stringCount * 2) throw new AstDecodeError(`invalid string index ${index}`); - const cached = this.stringCache[index / 2]; + const cached = this.stringCache[index >>> 1]; if (cached !== undefined) return cached; const start = astU32(this.bytes, this.stringOffsets + index * 4); const end = astU32(this.bytes, this.stringOffsets + (index + 1) * 4); const text = decodeAstString(this.bytes, this.strings + start, end - start); - this.stringCache[index / 2] = text; + this.stringCache[index >>> 1] = text; return text; } diff --git a/packages/compiler/src/frontend/ts7/checker.ts b/packages/compiler/src/frontend/ts7/checker.ts index cd43636af..9550d1dc2 100644 --- a/packages/compiler/src/frontend/ts7/checker.ts +++ b/packages/compiler/src/frontend/ts7/checker.ts @@ -86,12 +86,19 @@ function batchAnswers( items: readonly I[], fetch: (chunk: readonly I[]) => readonly (O | undefined)[], store: (item: I, answer: O | undefined) => void, -): void { + unknown: (error: unknown) => boolean = () => false, +): boolean { + let known = true; const answer = (chunk: readonly I[]): void => { + if (!known) return; let answers: readonly (O | undefined)[]; try { answers = fetch(chunk); - } catch { + } catch (error) { + if (unknown(error)) { + known = false; + return; + } if (chunk.length === 1) return; const mid = chunk.length >> 1; answer(chunk.slice(0, mid)); @@ -101,6 +108,13 @@ function batchAnswers( chunk.forEach((item, index) => store(item, answers[index])); }; for (let i = 0; i < items.length; i += BATCH_CHUNK) answer(items.slice(i, i + BATCH_CHUNK)); + return known; +} + +/** Whether a request failed because the server has no such method: tsgo, + * and tsc-ts before 0.0.7, answer the batch methods this way. */ +function unknownMethod(error: unknown, method: string): boolean { + return error instanceof Error && error.message.includes(`unknown API method "${method}"`); } function chunked(items: readonly T[], fetch: (chunk: readonly T[]) => readonly R[]): R[] { @@ -239,6 +253,9 @@ function collectNodes(roots: readonly Node[], walk: PrefetchWalk = "all"): Node[ export class CheckerFacade { private readonly cache = new CheckerCache(); + /** Batch methods the server answered as unknown. Lowering asks those + * node by node from then on, without a failed request per prefetch. */ + private readonly unknownBatchMethods = new Set(); constructor( /** The underlying 7.0.2 sync checker — exposed for methods the facade @@ -374,7 +391,8 @@ export class CheckerFacade { if (!NODE_BATCH_QUERIES) return; this.ensureActive(); const missing = [...new Set(nodes)].filter((node) => !this.cache.contextualType.has(node)); - batchAnswers( + this.batchNodes( + "getContextualTypes", missing, (chunk) => this.raw.getContextualTypes(chunk), (node, type) => this.cache.contextualType.set(node, type), @@ -386,13 +404,27 @@ export class CheckerFacade { if (!NODE_BATCH_QUERIES) return; this.ensureActive(); const missing = [...new Set(nodes)].filter((node) => !this.cache.resolvedSignature.has(node)); - batchAnswers( + this.batchNodes( + "getResolvedSignatures", missing, (chunk) => this.raw.getResolvedSignatures(chunk), (node, signature) => this.cache.resolvedSignature.set(node, signature), ); } + /** Answers a node batch method's chunks (batchAnswers), unless the server + * has answered the method as unknown. */ + private batchNodes( + method: string, + nodes: readonly Node[], + fetch: (chunk: readonly Node[]) => readonly (O | undefined)[], + store: (node: Node, answer: O | undefined) => void, + ): void { + if (nodes.length === 0 || this.unknownBatchMethods.has(method)) return; + if (!batchAnswers(nodes, fetch, store, (error) => unknownMethod(error, method))) + this.unknownBatchMethods.add(method); + } + /** Batches symbol queries for every identifier under roots without the * usual companion getTypeOfSymbol batch. Preflight uses this for AST * analyses that themselves inspect deferred bodies for binding identity: diff --git a/packages/compiler/src/frontend/ts7/semantic-checker.ts b/packages/compiler/src/frontend/ts7/semantic-checker.ts index d1d42c6e1..c19f93598 100644 --- a/packages/compiler/src/frontend/ts7/semantic-checker.ts +++ b/packages/compiler/src/frontend/ts7/semantic-checker.ts @@ -97,11 +97,11 @@ export interface SemanticCompletion { } /** Whether lowering asks the checker for contextual types and resolved - * signatures in batches (`getContextualTypes`, `getResolvedSignatures`). - * Off until scriptc depends on a tsc-ts release that answers them: the - * batch prefetches (checker.ts) then do nothing and lowering asks node by - * node. */ -export const NODE_BATCH_QUERIES = false; + * signatures in batches (`getContextualTypes`, `getResolvedSignatures`, + * answered from tsc-ts 0.0.7 on). Off, the batch prefetches (checker.ts) do + * nothing and lowering asks node by node. A server without a batch method + * (tsgo) answers it as unknown once, and the facade stops asking. */ +export const NODE_BATCH_QUERIES = true; /** The pinned checker's request surface over native objects. Every query * converges on the same snapshot/project registries, including batch and diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index bd2a32c37..14379f30f 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -5844,6 +5844,41 @@ function constAssertedEmptyArrayProp(p: ts.Symbol): boolean { return underConstAssertion(d); } +/** A TypeScript property assignment `name: []` whose type is `never[]`. */ +function emptyNeverArrayProp(p: ts.Symbol, fieldTs: ts.Type, checker: ts.TypeChecker): boolean { + const d = p.valueDeclaration; + if (d === undefined || !ts.isPropertyAssignment(d)) return false; + if (isJsSourceFile(ts.getSourceFileOfNode(d))) return false; + if (!ts.isArrayLiteralExpression(d.initializer) || d.initializer.elements.nodes.length > 0) + return false; + return ( + checker.isArrayType(fieldTs) && + (checker.getTypeArguments(fieldTs as ts.TypeReference)[0]!.flags & ts.TypeFlags.Never) !== 0 + ); +} + +/** A JavaScript object literal's property (`{ lastComponent: undefined }`, + * `{ user: null }`). Nothing checks a JavaScript file's later writes, which + * can store any value in it (`path.lastComponent = { ... }`), so a + * property whose only initializer is a unit holds the checked-dynamic value + * rather than the unit-only union. */ +function isUnitOnlyIr(t: IrType, ctx: TypeMapperCtx): boolean { + if (t.kind !== "union") return false; + const def = ctx.unions.get(t.unionId); + return def !== undefined && def.arms.every((arm) => isUnitType(arm)); +} + +function jsObjectLiteralProperty(p: ts.Symbol): boolean { + const d = p.valueDeclaration; + // `__proto__: null` sets the literal's prototype; it declares no property. + return ( + d !== undefined && + (ts.isPropertyAssignment(d) || ts.isShorthandPropertyAssignment(d)) && + ts.symbolName(p) !== "__proto__" && + isJsSourceFile(ts.getSourceFileOfNode(d)) + ); +} + function mapGenericAwaitedAlias(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const { resolveTypeParam } = ctx; if (!resolveTypeParam) return null; @@ -7880,6 +7915,14 @@ function mapRecordTypeInner( ) { pt = arrayOf(unitOnlyUnion(ctx.unions)); } + // A TypeScript property initialized with an empty literal and typed + // `never[]` (`const out = { files: [] }`) holds no element, so it maps + // like the empty tuple: the unit-only element array, which a + // destination's typed array field (`files: string[]`) receives through + // the checked empty-array lift. never's f64 element would make it a + // `number[]` field. + if (pt?.kind === "array" && emptyNeverArrayProp(p, fieldTs, checker)) + pt = arrayOf(unitOnlyUnion(ctx.unions)); if (pt === null && (fieldTs.flags & ts.TypeFlags.Any) !== 0) pt = DYN; // An `any[]` field (`list: any[]` in an SDK's message type, also // optional or nullable) is the checked-dynamic array, exactly as @@ -7903,6 +7946,7 @@ function mapRecordTypeInner( // omits the undefined arm exactly like an omitted optional. if (pt?.kind === "void" && isUnitOnlyTsType(fieldTs, ctx.resolveTypeParam)) pt = unitOnlyUnion(ctx.unions); + if (pt !== null && isUnitOnlyIr(pt, ctx) && jsObjectLiteralProperty(p)) pt = DYN; // dyn FIELDS map now (`{ v: unknown }`, `[string, unknown]` tuples): // the slot carries a dyn value exactly like an `unknown`-valued // overflow entry — same RC adapters, same dynFrom conversion on the diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 16db45b6c..7d01bb83d 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -16,6 +16,12 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ // Node's prime, generator, size, encoding and group-name errors. "crypto.dhNew", "crypto.dhGroupNew", + // Node's candidate, size, options, add and rem errors, and OpenSSL's + // generation errors. + "crypto.checkPrimeSync", + "crypto.checkPrimeCb", + "crypto.generatePrimeSync", + "crypto.generatePrimeCb", // node:test's { timeout } and { concurrency } range checks. "test.register", "test.registerEmpty", @@ -1001,3 +1007,25 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "crypto.createSign", "crypto.createVerify", ]); + +/** libCall members that always throw. Their result type is the replaced + * expression's own and is never materialized: global.undefRead's is the + * declared type of the undefined global; the others stand in for a value + * of the expression they replace. The fs Chk ladders qualify: every + * validation failure throws Node's typed error, and a full pass throws the + * trailing compiler-rendered fence. */ +export const ALWAYS_THROW_LIB_FNS: ReadonlySet = new Set([ + "global.undefRead", + "error.nodeThrow", + "error.argTypeThrow", + "error.propTypeThrow", + "fs.mkdtempChk", + "fs.readFileChk", + "fs.opendirChk", + "fs.watchFileChk", + "fs.lchmodChk", + "fs.readChk", + "fs.streamOptsChk", + "net.connectOptsChk", + "tls.caCertsChk", +]); diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index fcdbe8694..6bf864da6 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -1318,6 +1318,12 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ argTypes: [STRING, arrayOf(STRING), F64, STRING, F64, F64, F64], result: SPAWNRES_T, }, + /** cp.spawnSyncOpts with the `env` option: the trailing [k,v,...] + * pairs replace the child's environment (recordToEnvPairs). */ + "cp.spawnSyncEnv": { + argTypes: [STRING, arrayOf(STRING), F64, STRING, F64, F64, F64, arrayOf(STRING)], + result: SPAWNRES_T, + }, /** cp.spawnSyncOpts with the stdio carried as a RUNTIME string — * "pipe" | "ignore" | "inherit", proven by the call site's TYPE (the * defaultRunner idiom `stdio: options?.stdio ?? "pipe"`); the runtime @@ -1528,6 +1534,13 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ }, "dgram.sendStr": { argTypes: [DGRAMSOCK_T, STRING, F64, STRING], result: VOID }, "dgram.sendBytes": { argTypes: [DGRAMSOCK_T, BYTES_U8, F64, STRING], result: VOID }, + /** Connected AF_UNIX datagram sockets for the hot-shots package shim + * (the unix-dgram addon's surface): fd or result >= 0, -errno on + * failure; send answers 1 when the socket would block. */ + "dgram.unixSocket": { argTypes: [], result: F64 }, + "dgram.unixConnect": { argTypes: [F64, STRING], result: F64 }, + "dgram.unixSend": { argTypes: [F64, BYTES_U8], result: F64 }, + "dgram.unixClose": { argTypes: [F64], result: F64 }, /** The send argument-validation ladder over dyn arguments (Node's * signature shuffle: slice bounds, list/type contracts, port/address * validation, connected-state errors), then the send: lists, connected @@ -2820,6 +2833,16 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ /** getDiffieHellman(groupName) and createDiffieHellmanGroup(name): a * native DiffieHellmanGroup handle. */ "crypto.dhGroupNew": { argTypes: [DYN], result: DYN }, + /** checkPrimeSync(candidate, options) and generatePrimeSync(size, + * options) over checked-dynamic arguments (scr_dh.c): a boolean, and an + * ArrayBuffer or bigint, as checked-dynamic results. The Cb forms take + * the callback and whether its error parameter takes null for Node's + * undefined, and call it in a later macrotask; Node's argument errors + * throw (may-throw). */ + "crypto.checkPrimeSync": { argTypes: [DYN, DYN], result: DYN }, + "crypto.checkPrimeCb": { argTypes: [DYN, DYN, DYN, BOOL], result: VOID }, + "crypto.generatePrimeSync": { argTypes: [DYN, DYN], result: DYN }, + "crypto.generatePrimeCb": { argTypes: [DYN, DYN, DYN, BOOL], result: VOID }, "crypto.hmacNewStr": { argTypes: [STRING, STRING], result: CRYPTOHMAC_T }, "crypto.hmacNewBytes": { argTypes: [STRING, BYTES_U8], result: CRYPTOHMAC_T }, "crypto.hashUpdateStr": { argTypes: [CRYPTOHASH_T, STRING], result: CRYPTOHASH_T }, @@ -3593,7 +3616,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "weakSet.new": { argTypes: [DYN], result: DYN }, "dyn.fromEntries": { argTypes: [DYN], result: DYN }, "bytes.constructor": { argTypes: [STRING], result: DYN }, - "bytes.instanceOf": { argTypes: [DYN, DYN], result: BOOL }, + "bytes.instanceOf": { argTypes: [DYN, DYN, STRING], result: BOOL }, "bytes.construct": { argTypes: [DYN, DYN, STRING], result: DYN }, /** The ambient receiver — JS `this` in a plain (non-method) function * body: the innermost binding the current firing/dispatch window diff --git a/packages/compiler/src/ir/int32-slots.ts b/packages/compiler/src/ir/int32-slots.ts index 21223862a..ac0747276 100644 --- a/packages/compiler/src/ir/int32-slots.ts +++ b/packages/compiler/src/ir/int32-slots.ts @@ -508,7 +508,10 @@ export function dynamicFieldStores( }; const dynWritten = new Set(); let dynWritesAll = false; - for (const fn of mod.functions) + // A store whose key is not a literal names every field: later stores add + // nothing, so the walk stops there. + for (const fn of mod.functions) { + if (dynWritesAll) break; walkFunction(fn, { expr: (e) => { if (dynWritesAll) return; @@ -524,6 +527,7 @@ export function dynamicFieldStores( } }, }); + } return { all: dynWritesAll, names: dynWritten }; } @@ -536,6 +540,22 @@ export function dynamicallyStoredFamilies( slots: Int32Slots, stores?: DynamicFieldStores, ): Set { + const records = new Map((mod.records ?? []).map((r) => [r.id, r])); + const unions = new Map((mod.unions ?? []).map((u) => [u.id, u])); + // A live dyn view (typed-ref capsule) of an instance writes back into + // its fields only after a runtime store into the capsule; the commit then + // writes every field from the view, which holds the exact snapshot value + // of any property no store named. So reaching dynamic code disqualifies + // exactly the field names some dynamic store in the program can name. + const { all: dynWritesAll, names: dynWritten } = + stores ?? dynamicFieldStores(mod, records, unions); + const storable = (field: { name: string; type: IrType }): boolean => + field.type.kind === "f64" && + !field.name.startsWith("%") && + (dynWritesAll || dynWritten.has(field.name)); + const families = new Set(); + // Without a field some store can name, no class is worth exposing. + if (!(mod.classes ?? []).some((cls) => cls.fields.some(storable))) return families; const classes = new Map((mod.classes ?? []).map((c) => [c.name, c])); const children = new Map(); for (const cls of classes.values()) @@ -554,8 +574,6 @@ export function dynamicallyStoredFamilies( } return out; }; - const records = new Map((mod.records ?? []).map((r) => [r.id, r])); - const unions = new Map((mod.unions ?? []).map((u) => [u.id, u])); const exposedClasses = new Set(); const seenNamed = new Set(); let exposeAll = false; @@ -596,13 +614,6 @@ export function dynamicallyStoredFamilies( }); } }; - // A live dyn view (typed-ref capsule) of an instance writes back into - // its fields only after a runtime store into the capsule; the commit then - // writes every field from the view, which holds the exact snapshot value - // of any property no store named. So reaching dynamic code disqualifies - // exactly the field names some dynamic store in the program can name. - const { all: dynWritesAll, names: dynWritten } = - stores ?? dynamicFieldStores(mod, records, unions); for (const fn of mod.functions) walkFunction(fn, { expr: (e) => { @@ -628,16 +639,10 @@ export function dynamicallyStoredFamilies( if (s.kind === "throw") expose(s.value.type); }, }); - const families = new Set(); for (const cls of classes.values()) { if (!exposeAll && !exposedClasses.has(cls.name)) continue; for (const field of cls.fields) - if ( - field.type.kind === "f64" && - !field.name.startsWith("%") && - (dynWritesAll || dynWritten.has(field.name)) - ) - families.add(slots.family(cls.name, field.name)!); + if (storable(field)) families.add(slots.family(cls.name, field.name)!); } return families; } @@ -1058,12 +1063,18 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): if (params) for (const id of params.keys()) reads(`param:${fn.name}\0${id}`, fn.name); // Range facts are consulted only at writes into candidate slots; slots // are only ever dropped, so a body without such a write needs none. - const ranges: IntegerRanges = hasCandidateWrite(fn) + const candidate = hasCandidateWrite(fn); + const ranges: IntegerRanges = candidate ? analyzeIntegerRanges(numeric(fn), slots.facts(fn.name)) : new Map(); if (ranges.size > 0) lastWrites.set(fn.name, slotWrites(fn, ranges, functions)); else lastWrites.delete(fn.name); const proven: IrExpr[] = []; + // The walk below acts only at writes into candidate slots. + if (!candidate) { + provenByFunction.set(fn.name, proven); + continue; + } const int32 = (e: IrExpr): boolean => withinInt32(ranges.get(e)); // An int32 written into a slot still assumed non-negative: widen the // assumption when the value may be negative, and reanalyze its readers. diff --git a/packages/compiler/src/ir/parameter-versions.ts b/packages/compiler/src/ir/parameter-versions.ts new file mode 100644 index 000000000..88232251c --- /dev/null +++ b/packages/compiler/src/ir/parameter-versions.ts @@ -0,0 +1,307 @@ +import { analyzeInt32Slots } from "./int32-slots.js"; +import { BOOL, F64, type IrExpr, type IrFunction, type IrModule, type IrStmt } from "./ir.js"; +import { reparseStmt } from "./serialize.js"; +import { everyExprChild, everyStmtChild, everyStmtList } from "./traverse.js"; + +const MAX_FUNCTION_NODES = 2048; +const MAX_VERSIONS = 16; + +function size(fn: IrFunction): number { + let nodes = 0; + everyStmtList(fn.body, { + stmt: () => ++nodes <= MAX_FUNCTION_NODES, + expr: () => ++nodes <= MAX_FUNCTION_NODES, + }); + return nodes; +} + +/** The function a direct call or construction runs, if any. */ +function callee(e: IrExpr): string | null { + if (e.kind === "call") return e.callee; + if (e.kind === "new") return `%${e.className}.constructor`; + return null; +} + +/** Direct call and construction sites of each function in the module. */ +function callSites(mod: IrModule): Map { + const sites = new Map(); + for (const fn of mod.functions) + everyStmtList(fn.body, { + stmt: () => true, + expr: (e) => { + const name = callee(e); + if (name !== null) sites.set(name, (sites.get(name) ?? 0) + 1); + return true; + }, + }); + return sites; +} + +/** Expressions and statements of a callee that LLVM inlines at any number + * of call sites: scalar arithmetic and reads, no calls or allocations. */ +const SIMPLE_EXPRESSIONS = new Set([ + "varRef", + "numLit", + "boolLit", + "bin", + "unary", + "logical", + "ternary", + "toBool", + "incDec", + "assignExpr", + "arrayGet", + "fieldGet", + "recordGet", + "unionNarrow", + "unionIsTag", +]); +const SIMPLE_STATEMENTS = new Set([ + "varDecl", + "assign", + "exprStmt", + "if", + "block", + "return", +]); + +function simple(fn: IrFunction): boolean { + return ( + (fn.returnType.kind === "f64" || fn.returnType.kind === "bool") && + everyStmtList(fn.body, { + stmt: (s) => SIMPLE_STATEMENTS.has(s.kind), + expr: (e) => SIMPLE_EXPRESSIONS.has(e.kind), + }) + ); +} + +/** Whether a copy of `fn` would cost a callee its only call site. LLVM + * inlines a function called once wherever its size allows; with a second + * site, both calls can stay out of line, with the records and arguments + * they pass built in memory. A callee that only computes a number or a + * boolean from scalar reads inlines either way. A function calling itself + * would also dispatch on every call. */ +function keepsSingleBody( + fn: IrFunction, + sites: ReadonlyMap, + functions: ReadonlyMap, +): boolean { + return !everyStmtList(fn.body, { + stmt: () => true, + expr: (e) => { + const name = callee(e); + if (name === null) return true; + if (name === fn.name) return false; + if (sites.get(name) !== 1) return true; + const target = functions.get(name); + return target !== undefined && simple(target); + }, + }); +} + +/** Statements and expressions of a loop whose work is numeric: arithmetic, + * element and field reads and writes, `Math` calls and calls of simple + * functions. Loops that allocate, build strings or call other code gain + * little from an integer bound and would only grow the copy. */ +const NUMERIC_STATEMENTS = new Set([ + ...SIMPLE_STATEMENTS, + "for", + "while", + "doWhile", + "break", + "continue", + "arraySet", + "fieldSet", +]); +const NUMERIC_EXPRESSIONS = new Set([...SIMPLE_EXPRESSIONS, "arrayHas", "arrayState"]); + +function numericLoop(loop: IrStmt, functions: ReadonlyMap): boolean { + const expr = (e: IrExpr): boolean => { + let allowed = NUMERIC_EXPRESSIONS.has(e.kind); + if (e.kind === "call") { + const target = functions.get(e.callee); + allowed = target !== undefined && simple(target); + } else if (e.kind === "libCall") allowed = e.fn.startsWith("math."); + else if (e.kind === "arrIntrinsic") allowed = e.method === "length"; + return allowed && everyExprChild(e, expr, stmt); + }; + const stmt = (s: IrStmt): boolean => + NUMERIC_STATEMENTS.has(s.kind) && everyStmtChild(s, expr, stmt); + return stmt(loop); +} + +/** Number parameters of `fn` that bound one of its numeric loops: read in + * the loop's condition or in its `for` initializer. */ +function loopBounds(fn: IrFunction, functions: ReadonlyMap): Set { + const params = new Set( + fn.params.filter((param) => param.type.kind === "f64").map((param) => param.localId), + ); + const result = new Set(); + if (params.size === 0) return result; + const reads = (e: IrExpr): boolean => { + if (e.kind === "varRef" && params.has(e.localId)) result.add(e.localId); + return everyExprChild(e, reads, readsStmt); + }; + const readsStmt = (s: IrStmt): boolean => everyStmtChild(s, reads, readsStmt); + everyStmtList(fn.body, { + stmt: (s) => { + if (s.kind !== "for" && s.kind !== "while" && s.kind !== "doWhile") return true; + if (!numericLoop(s, functions)) return true; + if (s.cond) reads(s.cond); + if (s.kind === "for" && s.init) readsStmt(s.init); + return true; + }, + expr: () => true, + }); + return result; +} + +/** Versions of functions whose number parameters bound their loops. + * + * A parameter that every call site proves an int32 already reaches the + * function's i32 entry (int32-abi.ts). One that some caller passes as an + * unproven number (a value read from a message, a field, a parse) keeps + * its double, so the loops it bounds compare a counter with a double and + * the values derived from it stay doubles. + * + * A parameter qualifies when it bounds a numeric loop (`numericLoop`). + * Such a function gets a copy of its body under a new name, and its own + * body starts by calling the copy when every such parameter holds an + * int32: `(p | 0) === p` (an integer in the int32 range, not NaN) and not + * -0 (`1 / p` is not -Infinity). The copy then receives `p | 0`, which is + * the same double, so it computes what the body would; its only caller + * proves the parameter an int32, so it gets the i32 entry. Other values + * run the body as before. + * + * Only plain synchronous functions qualify: not class members, no + * captures, not the module entry, not dynamic-tier, at most + * MAX_FUNCTION_NODES nodes, not calling themselves, and not holding the + * only call site of another function (`keepsSingleBody`). The caller + * skips modules that observe call stacks, where the copy's frame could be + * seen. */ +export function versionLoopBoundParameters( + mod: IrModule, + symbolName: (name: string) => string = (name) => name, +): IrModule { + const candidates: { fn: IrFunction; params: Set }[] = []; + const sites = callSites(mod); + const functions = new Map(mod.functions.map((fn) => [fn.name, fn])); + const members = new Set((mod.classes ?? []).flatMap((cls) => cls.methods ?? [])); + for (const fn of mod.functions) { + if ( + // Constructors, methods and accessors (`%C.m`) and generated helper + // families (`%arr.map_…`) keep their single body. + fn.name.includes(".") || + members.has(fn.name) || + fn.async || + fn.generator || + fn.captures !== undefined || + fn.classCaptures?.length || + fn.tier === "dynamic" || + fn.name === mod.entry + ) + continue; + const params = loopBounds(fn, functions); + const locals = new Map(fn.locals.map((local) => [local.id, local])); + for (const id of params) { + const local = locals.get(id); + if (!local || local.boxed || local.tdz) params.delete(id); + } + if (params.size === 0 || size(fn) > MAX_FUNCTION_NODES || keepsSingleBody(fn, sites, functions)) + continue; + candidates.push({ fn, params }); + } + if (candidates.length === 0) return mod; + const slots = analyzeInt32Slots(mod); + const names = new Set(mod.functions.map((fn) => symbolName(fn.name))); + const replaced = new Map(); + const added: IrFunction[] = []; + for (const { fn, params } of candidates) { + if (added.length >= MAX_VERSIONS) break; + const unproven = fn.params.filter( + (param) => params.has(param.localId) && !slots.hasParam(fn.name, param.localId), + ); + if (unproven.length === 0) continue; + let name = `${fn.name}.int32Params`; + while (names.has(symbolName(name))) name += "_"; + names.add(symbolName(name)); + const { ownsPrototype: _ownsPrototype, ...implementation } = fn; + added.push({ ...implementation, name, body: fn.body.map(reparseStmt) }); + const loc = fn.body[0]?.loc ?? { file: mod.sourceFile, start: 0, end: 0 }; + const read = (localId: string): IrExpr => ({ kind: "varRef", localId, type: F64, loc }); + const int32 = (localId: string): IrExpr => ({ + kind: "bin", + op: "|", + left: read(localId), + right: { kind: "numLit", value: 0, type: F64, loc }, + type: F64, + loc, + }); + let test: IrExpr | null = null; + for (const param of unproven) { + const exact: IrExpr = { + kind: "bin", + op: "===", + left: int32(param.localId), + right: read(param.localId), + type: BOOL, + loc, + }; + const notNegativeZero: IrExpr = { + kind: "bin", + op: "!==", + left: { + kind: "bin", + op: "/", + left: { kind: "numLit", value: 1, type: F64, loc }, + right: read(param.localId), + type: F64, + loc, + }, + right: { kind: "numLit", value: -Infinity, type: F64, loc }, + type: BOOL, + loc, + }; + const both: IrExpr = { + kind: "logical", + op: "&&", + left: exact, + right: notNegativeZero, + type: BOOL, + loc, + }; + test = + test === null + ? both + : { kind: "logical", op: "&&", left: test, right: both, type: BOOL, loc }; + } + const versioned = new Set(unproven.map((param) => param.localId)); + const call: IrExpr = { + kind: "call", + callee: name, + args: fn.params.map((param) => + versioned.has(param.localId) + ? int32(param.localId) + : { kind: "varRef", localId: param.localId, type: param.type, loc }, + ), + type: fn.returnType, + loc, + }; + const run: IrStmt[] = + fn.returnType.kind === "void" + ? [ + { kind: "exprStmt", expr: call, loc }, + { kind: "return", value: null, loc }, + ] + : [{ kind: "return", value: call, loc }]; + replaced.set(fn, { + ...fn, + body: [{ kind: "if", cond: test!, then: run, else_: null, loc }, ...fn.body], + }); + } + if (added.length === 0) return mod; + return { + ...mod, + functions: [...mod.functions.map((fn) => replaced.get(fn) ?? fn), ...added], + }; +} diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 5b984f318..893c2ad07 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -605,14 +605,23 @@ const STDIO_EXTENDED_NAMES: ReadonlySet = new Set(["setEncoding", "resiz /** The libCall nodes the rules below test. */ type LibCallNode = IrExpr & { kind: "libCall" }; +/** A condition over several nodes of the module (a WebCrypto call and an + * algorithm name): the scan's one walk shows it every expression, and the + * feature is set when `result` answers true after the walk. */ +interface RuleScan { + expr(node: IrExpr): void; + result(): boolean; +} + /** What makes a program need one optional runtime unit. The feature is set * when any condition holds: `initial` from the module's declarations, a * libCall named in `fns` or starting with one of `prefixes`, a libCall - * `libCall` accepts, another expression node `expr` accepts, or a type - * slot of one of the `types` kinds. A libCall or type that needs several - * units appears in each unit's rule. */ + * `libCall` accepts, another expression node `expr` accepts, a type slot of + * one of the `types` kinds, or a `scan` over the whole module. A libCall or + * type that needs several units appears in each unit's rule. */ interface RuntimeFeatureRule { initial?: (mod: IrModule) => boolean; + scan?: () => RuleScan; fns?: readonly IrLibFn[]; prefixes?: readonly string[]; libCall?: (node: LibCallNode) => boolean; @@ -626,6 +635,16 @@ const rule = (r: RuntimeFeatureRule): RuntimeFeatureRule => r; /* The HTTP, HTTPS, HTTP/2 and TLS surfaces, which link the net unit. */ const NET_PREFIXES: readonly string[] = ["net.", "http.", "tls.", "https.", "http2."]; +/** The scr_dh.c entry points (the dh unit). */ +export const DH_LIB_FNS: ReadonlySet = new Set([ + "crypto.dhNew", + "crypto.dhGroupNew", + "crypto.checkPrimeSync", + "crypto.checkPrimeCb", + "crypto.generatePrimeSync", + "crypto.generatePrimeCb", +]); + /** The scr_pki.c entry points (the pki unit). */ export const PKI_LIB_FNS: ReadonlySet = new Set([ "crypto.createPrivateKey", @@ -650,17 +669,32 @@ const WEBCRYPTO_ASYMMETRIC_NAMES: ReadonlySet = new Set([ /** Whether a string literal in the module names Ed25519, without regard * to case ("ed25519" for node:crypto, "Ed25519" for WebCrypto): the * Ed25519 unit links then, alongside the pki unit. */ -export function moduleNamesEd25519(mod: IrModule): boolean { +function ed25519NameScan(): RuleScan { let named = false; - everyModuleNode(mod, { + return { expr: (node) => { - if (node.kind === "strLit" && node.value.toLowerCase() === "ed25519") named = true; - return !named; + if (!named && node.kind === "strLit" && node.value.toLowerCase() === "ed25519") named = true; }, - stmt: () => !named, - type: () => true, - }); - return named; + result: () => named, + }; +} + +/** moduleUsesWebCryptoAsymmetric's condition as a runtime-feature scan. */ +function webCryptoAsymmetricScan(): RuleScan { + let webcrypto = false; + let named = false; + return { + expr: (node) => { + if (node.kind === "libCall" && node.fn.startsWith("webcrypto.")) webcrypto = true; + else if ( + !named && + node.kind === "strLit" && + WEBCRYPTO_ASYMMETRIC_NAMES.has(node.value.toLowerCase()) + ) + named = true; + }, + result: () => webcrypto && named, + }; } /** Whether the module calls WebCrypto and a string literal in it names one @@ -820,9 +854,10 @@ const RUNTIME_FEATURE_RULES = { * process-warning unit, and unknown encodings and formats render through * the inspect unit. */ ecdh: rule({ fns: ["crypto.ecdhNew", "crypto.ecdhConvertKey"] }), - /** scr_dh.c over the mbedTLS archive; unknown encodings render through + /** scr_dh.c over the mbedTLS archive, also for checkPrime and + * generatePrime; unknown encodings and argument values render through * the inspect unit. */ - dh: rule({ fns: ["crypto.dhNew", "crypto.dhGroupNew"] }), + dh: rule({ fns: [...DH_LIB_FNS] }), emitter: rule({ initial: (mod) => mod.workers === true || (mod.classes ?? []).some((c) => c.name === RUNTIME_EMITTER_CLASS), @@ -872,8 +907,7 @@ const RUNTIME_FEATURE_RULES = { "webcrypto.unwrapKey", "crypto.ecdhNew", "crypto.ecdhConvertKey", - "crypto.dhNew", - "crypto.dhGroupNew", + ...DH_LIB_FNS, ...BUILTIN_CLASS_LIB_FNS, ...UTIL_CALLABLE_LIB_FNS, ], @@ -906,10 +940,10 @@ const RUNTIME_FEATURE_RULES = { perfLoop: rule({ fns: ["perf.idleTime"], prefixes: ["perf.loop", "perf.eld"] }), /** scr_pki.c over the mbedTLS archive: asymmetric KeyObjects and * signatures. */ - pki: rule({ initial: moduleUsesWebCryptoAsymmetric, fns: [...PKI_LIB_FNS] }), + pki: rule({ scan: webCryptoAsymmetricScan, fns: [...PKI_LIB_FNS] }), /** scr_ed25519.c (the vendored libsodium Ed25519), linked with pki: a * string literal in the module names Ed25519. */ - ed25519: rule({ initial: moduleNamesEd25519 }), + ed25519: rule({ scan: ed25519NameScan }), processEvents: rule({ fns: [...PROCESS_EVENT_LIB_FNS, "util.styleText"], types: ["procStream"] }), /** scr_url_legacy.c parses and stringifies queries. */ qs: rule({ fns: [...QS_LIB_FNS, ...LEGACY_URL_LIB_FNS] }), @@ -1070,9 +1104,11 @@ for (const name of RUNTIME_FEATURE_NAMES) { function scanRuntimeFeatures(mod: IrModule, stopAt?: RuntimeFeatureName): RuntimeFeatures { const features = noRuntimeFeatures(); + const scans: { name: RuntimeFeatureName; scan: RuleScan }[] = []; for (const name of RUNTIME_FEATURE_NAMES) { - const initial = ruleOf(name).initial; + const { initial, scan } = ruleOf(name); if (initial && initial(mod)) features[name] = true; + if (scan) scans.push({ name, scan: scan() }); } const keepGoing = (): boolean => stopAt === undefined || !features[stopAt]; let tables: TypeTables | undefined; @@ -1083,6 +1119,7 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: RuntimeFeatureName): Runtim classes: new Map((mod.classes ?? []).map((c) => [c.name, c])), }); const expr = (node: IrExpr): boolean => { + for (let i = 0; i < scans.length; i++) scans[i]!.scan.expr(node); if (node.kind === "libCall") { const fn = node.fn; for (const name of FEATURES_BY_FN.get(fn) ?? []) features[name] = true; @@ -1101,6 +1138,7 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: RuntimeFeatureName): Runtim return keepGoing(); }; if (keepGoing()) everyModuleNode(mod, { expr, stmt: keepGoing, type }); + for (const { name, scan } of scans) if (scan.result()) features[name] = true; // Inline-cache sites of dynamic-tier functions use the tier's shapes // (scr_js_object.c, with the SCR_JS_TIER build of scr_json.c). if (!features.dynTier && mod.functions.some((fn) => fn.tier === "dynamic")) { diff --git a/packages/compiler/src/ir/traverse.ts b/packages/compiler/src/ir/traverse.ts index 11b20a210..251904351 100644 --- a/packages/compiler/src/ir/traverse.ts +++ b/packages/compiler/src/ir/traverse.ts @@ -109,8 +109,15 @@ export interface IrModuleVisitor extends IrVisitor { * source text, locations, labels, or other metadata. A false predicate stops * the whole walk. Results are never retained across mutable compiler passes. */ export function everyModuleNode(mod: IrModule, visitor: IrModuleVisitor): boolean { - const type = (node: IrType, loc: SrcLoc): boolean => - visitor.type(node, loc) && everyTypeChild(node, (child) => type(child, loc)); + // Every node of one type tree reports the tree's location: one shared + // child callback serves them all. + let typeLoc: SrcLoc; + const typeNode = (node: IrType): boolean => + visitor.type(node, typeLoc) && everyTypeChild(node, typeNode); + const type = (node: IrType, loc: SrcLoc): boolean => { + typeLoc = loc; + return typeNode(node); + }; const expr = (node: IrExpr): boolean => visitor.expr(node) && everyExprChild(node, expr, stmt) && type(node.type, node.loc); const stmt = (node: IrStmt): boolean => visitor.stmt(node) && everyStmtChild(node, expr, stmt); diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index bb36c7007..163c2a41c 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -61,6 +61,7 @@ import { unionWideningTags } from "./analysis.js"; import { alwaysReturns } from "./control-flow.js"; import { dynamicOperationOf, functionTier } from "./tiers.js"; import { LIB_FN_SIGS, pipelinePumpedRuns } from "./builtin-signatures.js"; +import { ALWAYS_THROW_LIB_FNS } from "./builtin-effects.js"; /** The promisified crypto calls' value kinds besides checked-dynamic. */ const CRYPTO_PROMISE_VALUES: Readonly> = { @@ -5345,32 +5346,8 @@ function validateFunction( const callback = e.args[e.args.length - 1]?.type; if (callback?.kind !== "func") err(`libCall ${e.fn} callback must be a function`, e.loc); } - if (e.fn === "global.undefRead") { - // Always throws — the result type is whatever the declared type - // of the undefined global mapped to (never materialized). - return true; - } - if ( - e.fn === "error.nodeThrow" || - e.fn === "error.argTypeThrow" || - e.fn === "error.propTypeThrow" || - e.fn === "fs.mkdtempChk" || - e.fn === "fs.readFileChk" || - e.fn === "fs.opendirChk" || - e.fn === "fs.watchFileChk" || - e.fn === "fs.lchmodChk" || - e.fn === "fs.readChk" || - e.fn === "fs.streamOptsChk" || - e.fn === "net.connectOptsChk" || - e.fn === "tls.caCertsChk" - ) { - // Always throws — the result type is the replaced expression's - // own (never materialized; the global.undefRead pattern). The - // fs Chk ladders qualify: every validation failure throws - // Node's typed error, and a full pass throws the trailing - // compiler-rendered fence. - return true; - } + // Always throws: the result type is never materialized. + if (ALWAYS_THROW_LIB_FNS.has(e.fn)) return true; if (e.fn === "error.new" || e.fn === "error.newOptions") { // Which builtin the runtime constructs is named by the result type. if (!isBuiltinErrorObject(e.type)) { diff --git a/packages/compiler/src/library/ir-policy.ts b/packages/compiler/src/library/ir-policy.ts index fb0575121..e8fad6dd1 100644 --- a/packages/compiler/src/library/ir-policy.ts +++ b/packages/compiler/src/library/ir-policy.ts @@ -54,6 +54,8 @@ const LIB_MODE_REFUSED_PREFIXES: readonly [string, string][] = [ ["crypto.ecdh", "crypto.createECDH and ECDH.convertKey"], ["crypto.dhNew", "crypto.createDiffieHellman"], ["crypto.dhGroupNew", "crypto.getDiffieHellman and createDiffieHellmanGroup"], + ["crypto.checkPrime", "crypto.checkPrime and checkPrimeSync"], + ["crypto.generatePrime", "crypto.generatePrime and generatePrimeSync"], ["crypto.createPrivateKey", "asymmetric KeyObjects, generateKeyPair, sign and verify"], ["crypto.createPublicKey", "asymmetric KeyObjects, generateKeyPair, sign and verify"], ["crypto.createSign", "asymmetric KeyObjects, generateKeyPair, sign and verify"], diff --git a/packages/compiler/src/native/cli.ts b/packages/compiler/src/native/cli.ts index e2c3a895b..21047b929 100644 --- a/packages/compiler/src/native/cli.ts +++ b/packages/compiler/src/native/cli.ts @@ -20,12 +20,30 @@ import { openNativeCache } from "./cache.js"; import { loadNativeToolchain } from "./toolchain.js"; import { buildSanitizedRuntime } from "./sanitizer.js"; import { runCompilerTask } from "./task.js"; +import { processFork } from "../frontend/process-fork.js"; declare function compilerNativePrivateDirectory(path: string, harden: boolean): boolean; declare function compilerNativeCollectCycles(): void; +declare function compilerNativeFork(): number; +declare function compilerNativeExit(code: number): void; +declare function compilerNativeWait(pid: number): number; +declare function compilerNativeLockOpen(path: string): boolean; +declare function compilerNativeLock(): void; +declare function compilerNativeUnlock(): void; +declare function compilerNativeBoundCycleGrowth(): void; async function main(): Promise { + compilerNativeBoundCycleGrowth(); setFrontendDisposedHook(() => compilerNativeCollectCycles()); + if (process.platform !== "win32") + processFork.primitives = { + fork: () => compilerNativeFork(), + exit: (code) => compilerNativeExit(code), + wait: (pid) => compilerNativeWait(pid), + openLock: (path) => compilerNativeLockOpen(path), + lock: () => compilerNativeLock(), + unlock: () => compilerNativeUnlock(), + }; const manifestPath = process.env["SCRIPTC_TOOLCHAIN"] ?? process.execPath + ".json"; let compiler: NativeCompiler | null = null; let cache: ReturnType = null; diff --git a/packages/compiler/src/native/prepare.ts b/packages/compiler/src/native/prepare.ts index e6e36c08f..6b21b2230 100644 --- a/packages/compiler/src/native/prepare.ts +++ b/packages/compiler/src/native/prepare.ts @@ -7,7 +7,7 @@ import type { CompileLibraryOptions, CompileRequestOptions, } from "../compile-types.js"; -import { emitLlvmModule, lowRegionTarget } from "../backend/llvm/emitter.js"; +import { emitLlvmModule, type ForkPrimitives, lowRegionTarget } from "../backend/llvm/emitter.js"; import { inlineRcOption } from "../backend/llvm/shapes.js"; import { executableLinkFeatures } from "../backend/executable-features.js"; import type { NativeLinkFeatures } from "../backend/native-link-info-core.js"; @@ -23,6 +23,7 @@ import { trackedReadFile, } from "../frontend/input-tracker.js"; import { provenanceSources } from "../frontend/provenance-registry.js"; +import { processFork } from "../frontend/process-fork.js"; import { prepareLibrary } from "../library/prepare.js"; import type { LibraryProfile } from "../library/library-profile.js"; import type { IrModule } from "../ir/ir.js"; @@ -297,6 +298,8 @@ export function prepareNativeExecutable( runtimeAbiMarker: outputKind === "obj" || outputKind === "exe", objectAudit: options.sanitize === true, lowRegion: lowRegionTarget(toolchain.target.platform, options.sanitize === true), + functionWorkers: emitWorkers(), + forkPrimitives: emitForkPrimitives(), ...inlineRcOption(options.optimization, options.sanitize === true), ...(options.detachedExit === true && outputKind === "exe" ? { detachedExit: true } : {}), ...(options.optimization === "dev" && !options.strip @@ -433,3 +436,22 @@ export function prepareNativeLibrary( timing("frontend-cache-publish"); return input; } + +/** Worker processes that emit function bodies at a time + * (SCRIPTC_EMIT_WORKERS; 0 or 1 emits in this process). */ +function emitWorkers(): number { + const value = Number(process.env["SCRIPTC_EMIT_WORKERS"] ?? "4"); + return Number.isFinite(value) && value > 1 ? Math.floor(value) : 1; +} + +/** The process primitives emission workers fork with, unless the host + * cannot fork or this process is a worker itself. */ +function emitForkPrimitives(): ForkPrimitives | undefined { + const primitives = processFork.primitives; + if (primitives === null || processFork.worker) return undefined; + return { + fork: () => primitives.fork(), + exit: (code) => primitives.exit(code), + wait: (pid) => primitives.wait(pid), + }; +} diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index 45b3edbaa..81fb23ae2 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -1,6 +1,6 @@ { "schemaVersion": 2, - "compilerVersion": "0.4.1", + "compilerVersion": "0.4.2", "coverage": [ "Entries are projected mechanically from the compiler's own decision tables: the diagnostics registry, the unsupported-syntax dispatch tables, the stdlib and node-builtin lowering tables, and the supported-builtin-module list. Nothing is hand-maintained; the manifest regenerates byte-identically from the source tree at this version.", "Absence from this manifest means 'not projected', never 'unsupported'. Surfaces lowered through dedicated code paths rather than tables are not yet projected: console, JSON, Promise/async and the timer surface, the net/http/tls/https/dgram/dns/assert/test/stream/readline module member surfaces, template literals, the regex slice, global functions (parseInt, parseFloat, isNaN, isFinite), and the process surface outside its ambient slice.", @@ -480,6 +480,18 @@ "status": "static", "note": "recognized module (bare and node:-prefixed specifiers)" }, + { + "id": "node-builtin.crypto.checkPrime", + "kind": "node-builtin", + "name": "crypto.checkPrime", + "status": "static" + }, + { + "id": "node-builtin.crypto.checkPrimeSync", + "kind": "node-builtin", + "name": "crypto.checkPrimeSync", + "status": "static" + }, { "id": "node-builtin.crypto.createCipheriv", "kind": "node-builtin", @@ -584,13 +596,25 @@ "name": "crypto.generateKeySync", "status": "static" }, + { + "id": "node-builtin.crypto.generatePrime", + "kind": "node-builtin", + "name": "crypto.generatePrime", + "status": "static" + }, + { + "id": "node-builtin.crypto.generatePrimeSync", + "kind": "node-builtin", + "name": "crypto.generatePrimeSync", + "status": "static" + }, { "id": "node-builtin.crypto.getCipherInfo", "kind": "node-builtin", "name": "crypto.getCipherInfo", "status": "unsupported", "code": "SC2020", - "note": "cipher metadata has no lowering — createCipheriv and createDecipheriv support aes-128/192/256 in cbc, ctr, and gcm modes and chacha20-poly1305" + "note": "cipher metadata has no lowering — createCipheriv and createDecipheriv support aes-128/192/256 in cbc, ctr, gcm, ecb, cfb, cfb8, cfb1, and ofb modes, des-ede3 and des-ede, chacha20, and chacha20-poly1305" }, { "id": "node-builtin.crypto.getDiffieHellman", diff --git a/packages/compiler/test/chunked-emission.test.ts b/packages/compiler/test/chunked-emission.test.ts new file mode 100644 index 000000000..b4912c9f6 --- /dev/null +++ b/packages/compiler/test/chunked-emission.test.ts @@ -0,0 +1,122 @@ +import { mkdtemp, readFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule } from "../src/index.js"; +import type { IrModule } from "../src/ir/ir.js"; +import { emitLlvmModuleSource, type LlvmTargetOptions } from "../src/backend/llvm/emitter.js"; +import { inlineRcOption } from "../src/backend/llvm/shapes.js"; + +// Programs whose function bodies request module-level helpers and tables +// that the chunk journal (emit-journal.ts) carries: checked-dynamic +// conversions, inspection, publication walkers, live record views, stream +// adapters, regex instances, colliding literal digests, interned C strings, +// speculative loop sites, typed message listeners and readers. +const PROGRAMS = [ + "1003-json-parse-unions.ts", + "114-live-record-delete.ts", + "1200-regex-test-basics.ts", + "1631-inspect-arrays.ts", + "4598-publish-constructor-stores.ts", + "4900-console-instances.ts", + "5612-readable-from-iterables.cjs", + "5702-publish-candidate-buffers.ts", + "7321-speculative-loop-bail-sites.ts", + "7360-worker-typed-records.ts", + "node24-text-utilities-coercion.js", +]; + +const OPTIONS: Record = { + release: {}, + speed: { + targetTriple: "x86_64-unknown-linux-gnu", + pointerBits: 64, + runtimeAbiMarker: true, + lowRegion: true, + ...inlineRcOption("speed", false), + }, +}; + +const text = (out: string | readonly string[]): string => + typeof out === "string" ? out : out.join(""); + +async function lower(program: string, dir: string): Promise { + const entry = join(import.meta.dirname, "../../../tests/corpus", program); + const outPath = join(dir, `${program}.ir.json`); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + return readFile(outPath, "utf8"); +} + +test("function bodies emitted in chunks merge into the text one emitter gives", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-chunked-emission-")); + try { + for (const program of PROGRAMS) { + const ir = await lower(program, dir); + const fresh = (): IrModule => deserializeModule(ir); + for (const [name, options] of Object.entries(OPTIONS)) { + const serial = text(emitLlvmModuleSource(fresh(), options)); + for (const functionChunks of [3, 7]) { + const chunked = text(emitLlvmModuleSource(fresh(), { ...options, functionChunks })); + expect(chunked === serial, `${program}, ${name}, ${functionChunks} chunks`).toBe(true); + } + } + } + } finally { + await rm(dir, { recursive: true, force: true }); + } +}, 600_000); + +// Debug metadata is numbered per emitter (debug-info.ts) and not journaled: +// a debug build emits its function bodies in one emitter. +test("debug builds emit function bodies serially, with every metadata reference defined", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-chunked-debug-")); + const minFunctions = process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"]; + try { + const corpus = join(import.meta.dirname, "../../../tests/corpus/3066-source-debugging"); + const entry = join(corpus, "main.ts"); + const outPath = join(dir, "main.ir.json"); + const result = await compile(entry, { + outDir: dir, + outPath, + outputKind: "ir", + optimization: "dev", + }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const ir = await readFile(outPath, "utf8"); + const debugSources = new Map(); + for (const name of ["main.ts", "helper.ts"]) + debugSources.set(join(corpus, name), await readFile(join(corpus, name), "utf8")); + const options: LlvmTargetOptions = { debugSources }; + const serial = text(emitLlvmModuleSource(deserializeModule(ir), options)); + expect(serial).toContain("!DILocation"); + + let forks = 0; + process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"] = "1"; + const forkPrimitives = { + fork: (): number => { + forks++; + return -1; + }, + exit: (): void => {}, + wait: (): number => -1, + }; + for (const variant of [ + { functionChunks: 3 }, + { functionWorkers: 4, forkPrimitives }, + ] satisfies LlvmTargetOptions[]) { + const emitted = text(emitLlvmModuleSource(deserializeModule(ir), { ...options, ...variant })); + expect(emitted === serial, JSON.stringify(Object.keys(variant))).toBe(true); + const defined = new Set([...emitted.matchAll(/^(![0-9]+) = /gm)].map((m) => m[1])); + const referenced = new Set([...emitted.matchAll(/(![0-9]+)\b/g)].map((m) => m[1])); + expect([...referenced].filter((id) => !defined.has(id))).toEqual([]); + } + expect(forks).toBe(0); + } finally { + if (minFunctions === undefined) delete process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"]; + else process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"] = minFunctions; + await rm(dir, { recursive: true, force: true }); + } +}, 120_000); diff --git a/packages/compiler/test/native-codegen-integration.test.ts b/packages/compiler/test/native-codegen-integration.test.ts index 35a747d60..71b9570b4 100644 --- a/packages/compiler/test/native-codegen-integration.test.ts +++ b/packages/compiler/test/native-codegen-integration.test.ts @@ -378,7 +378,7 @@ done: expect(await symbols(helperObject, ["-u"])).toEqual(await symbols(clangObject, ["-u"])); expect(await symbols(helperObject, ["-gU"])).toEqual(await symbols(clangObject, ["-gU"])); expect(await symbols(helperObject, ["-gU"])).toEqual(["0000000000000000 T _main"]); - expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v27"); + expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v28"); const clangExe = join(dir, "clang-program"); const linkDriver = join(dir, "clang-driver.c"); @@ -712,7 +712,7 @@ done: .split("\n"); expect(undefinedSymbols).toContain("_sf_scale"); expect(undefinedSymbols).toContain("_sf_callback_mix"); - expect(undefinedSymbols).toContain("_scr_runtime_abi_v27"); + expect(undefinedSymbols).toContain("_scr_runtime_abi_v28"); }); test("helper and clang object paths retain the same outbound FFI C ABI", async () => { diff --git a/packages/compiler/test/package-shim-guard.test.ts b/packages/compiler/test/package-shim-guard.test.ts index ff01acbb8..a8620c1ce 100644 --- a/packages/compiler/test/package-shim-guard.test.ts +++ b/packages/compiler/test/package-shim-guard.test.ts @@ -49,7 +49,10 @@ describe("package shims compile without refusals", () => { const entries = packageShimEntries(); test("the registry lists its shim modules", () => { expect(entries.map((e) => e.slice(shimRoot.length + 1))).toEqual([ + "chalk/v5.ts", "fs-extra/index.ts", + "hot-shots/v8-unix-dgram.ts", + "hot-shots/v8.ts", "undici/v5.ts", "undici/v7.ts", ]); diff --git a/packages/compiler/test/parameter-version-emission.test.ts b/packages/compiler/test/parameter-version-emission.test.ts new file mode 100644 index 000000000..eb6daff56 --- /dev/null +++ b/packages/compiler/test/parameter-version-emission.test.ts @@ -0,0 +1,71 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile } from "../src/index.js"; + +async function emitLlvm(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-parameter-version-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ll"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "llvm" }); + expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true); + return await readFile(outPath, "utf8"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +const versions = (llvm: string): string[] => [ + ...new Set([...llvm.matchAll(/^define [^@]*@sc_\w+?_(\w+?)_int32Params\b/gm)].map((m) => m[1]!)), +]; + +const loops = ` +function sumTo(n: number): number { + let s = 0; + for (let i = 0; i < n; i++) s += i; + return s; +} +function literal(n: number): number { + let s = 0; + for (let i = 0; i < n; i++) s += i; + return s; +} +function recursive(n: number): number { + let s = 0; + for (let i = 0; i < n; i++) s += i; + return n > 0 ? s + recursive(n - 1) : s; +} +function unbounded(n: number): number { + let s = 0; + for (let i = 0; i < 10; i++) s += n; + return s; +} +class Counter { + total(n: number): number { + let s = 0; + for (let i = 0; i < n; i++) s += i; + return s; + } +} +const n = Number(process.argv.length > 5 ? "7.5" : "6"); +console.log(sumTo(n), literal(4), literal(5), recursive(n), unbounded(n), new Counter().total(n)); +`; + +test("a loop-bounding parameter some caller passes unproven gets an int32 version", async () => { + const llvm = await emitLlvm(loops); + expect(versions(llvm)).toEqual(["sumTo"]); + // The version takes the parameter as an i32, and the body dispatches to it + // after testing for an int32 that is not -0. + expect(llvm).toMatch(/^define [^@]*@sc_\w+_sumTo_int32Params\.i32\(i32 /m); + const body = /^define [^@]*@sc_\w+?_sumTo\([^]*?^}/m.exec(llvm)?.[0] ?? ""; + expect(body).toMatch(/call [^@]*@sc_\w+_sumTo_int32Params/); + expect(body).toMatch(/fdiv double 0x3FF0000000000000, %\S+/); +}); + +test("a module that reads stacks keeps a single body", async () => { + const llvm = await emitLlvm(`${loops}\nconsole.log(typeof new Error("x").stack);\n`); + expect(versions(llvm)).toEqual([]); +}); diff --git a/packages/compiler/test/running-extrema-branches.test.ts b/packages/compiler/test/running-extrema-branches.test.ts new file mode 100644 index 000000000..e1f5a7837 --- /dev/null +++ b/packages/compiler/test/running-extrema-branches.test.ts @@ -0,0 +1,75 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile } from "../src/index.js"; + +async function emitLlvm(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-running-extrema-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ll"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "llvm" }); + if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n")); + return await readFile(outPath, "utf8"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +const RARE = + /br i1 %\S+, label %if\.t\d+, label %if\.j\d+, !prof !\{!"branch_weights", i32 1, i32 1000\}/; + +test("a running maximum's update is weighted as rarely taken", async () => { + const llvm = await emitLlvm(` + function argmax(a: Float64Array): number { + let max = -Infinity; + let at = -1; + for (let i = 0; i < a.length; i++) { + const v = a[i]!; + if (v > max) { max = v; at = i; } + } + return at; + } + console.log(argmax(new Float64Array([1, 3, 2]))); + `); + expect(llvm).toMatch(RARE); +}); + +test("a callback's update of a captured minimum is weighted", async () => { + const llvm = await emitLlvm(` + function least(values: number[]): number { + let min = Infinity; + values.forEach((v) => { if (min > v) min = v; }); + return min; + } + console.log(least([3, 1, 2])); + `); + expect(llvm).toMatch(RARE); +}); + +test("clamps and saturating sums keep their selects", async () => { + const llvm = await emitLlvm(` + function clampSum(a: Float64Array, hi: number): number { + let s = 0; + for (let i = 0; i < a.length; i++) { + let v = a[i]!; + if (hi < v) v = hi; + s += v; + } + return s; + } + function saturate(a: Float64Array, hi: number): number { + let v = 0; + for (let i = 0; i < a.length; i++) { + v += a[i]!; + if (hi < v) v = hi; + } + return v; + } + const a = new Float64Array([0.5, 2, 0.25]); + console.log(clampSum(a, 1), saturate(a, 1)); + `); + expect(llvm).not.toContain("branch_weights"); +}); diff --git a/packages/compiler/test/speculative-loop-emission.test.ts b/packages/compiler/test/speculative-loop-emission.test.ts index a691d856d..a81348102 100644 --- a/packages/compiler/test/speculative-loop-emission.test.ts +++ b/packages/compiler/test/speculative-loop-emission.test.ts @@ -78,15 +78,14 @@ test("a failed check finishes its iteration outside the ordinary copy", async () test("a loop site stops entering its fast copy after repeated bails", async () => { const llvm = await emitLlvm(source); - const site = /^(@scr\.spec\.bails\.\d+) = internal global i8 0$/m.exec(llvm)?.[1]; + const site = /^(@sc_spec_bails_\d+) = internal global i8 0$/m.exec(llvm)?.[1]; expect(site).toBeDefined(); const fn = /^define internal double @sc_bf_scaled\([^]*?^}/m.exec(llvm)?.[0] ?? ""; - const escaped = site!.replace(/\./g, "\\."); // The entry test reads the count, a bail adds one, and a completed fast // copy clears it. expect(fn).toMatch( new RegExp( - `(%\\S+) = load atomic i8, ptr ${escaped} unordered, align 1\\n\\s*%\\S+ = icmp ult i8 \\1, 8`, + `(%\\S+) = load atomic i8, ptr ${site} unordered, align 1\\n\\s*%\\S+ = icmp ult i8 \\1, 8`, ), ); expect(fn).toMatch(new RegExp(`call i8 @llvm\\.uadd\\.sat\\.i8\\(i8 %\\S+, i8 1\\)`)); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4638-contract-program-lifetime-arrays.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4638-contract-program-lifetime-arrays.ts.json new file mode 100644 index 000000000..a89ae97b8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4638-contract-program-lifetime-arrays.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/4638-contract-program-lifetime-arrays.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4639-contract-program-lifetime-maps.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4639-contract-program-lifetime-maps.ts.json new file mode 100644 index 000000000..5f7afb8e7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4639-contract-program-lifetime-maps.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/4639-contract-program-lifetime-maps.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4814-integer-or-nan-indexes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4814-integer-or-nan-indexes.ts.json new file mode 100644 index 000000000..0a6abb5a5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4814-integer-or-nan-indexes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/4814-integer-or-nan-indexes.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4815-touint32-wide-magnitudes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4815-touint32-wide-magnitudes.ts.json new file mode 100644 index 000000000..80dcce24d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4815-touint32-wide-magnitudes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/4815-touint32-wide-magnitudes.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6915-key-order-narrowed-throw.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6915-key-order-narrowed-throw.ts.json new file mode 100644 index 000000000..83f8cc5d5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6915-key-order-narrowed-throw.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6915-key-order-narrowed-throw.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6916-index-record-spread-overrides.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6916-index-record-spread-overrides.ts.json new file mode 100644 index 000000000..db4bc557f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6916-index-record-spread-overrides.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6916-index-record-spread-overrides.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6926-dynamic-collection-seeds.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6926-dynamic-collection-seeds.mjs.json new file mode 100644 index 000000000..32edfc402 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6926-dynamic-collection-seeds.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6926-dynamic-collection-seeds.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6927-dynamic-collection-seeds.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6927-dynamic-collection-seeds.ts.json new file mode 100644 index 000000000..89c591dd7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6927-dynamic-collection-seeds.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6927-dynamic-collection-seeds.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6998-typed-array-while-loop-versions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6998-typed-array-while-loop-versions.ts.json new file mode 100644 index 000000000..f5cfba0cc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6998-typed-array-while-loop-versions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6998-typed-array-while-loop-versions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7002-empty-array-contextual-slots/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7002-empty-array-contextual-slots/main.ts.json new file mode 100644 index 000000000..d22715215 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7002-empty-array-contextual-slots/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7002-empty-array-contextual-slots/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7005-dyn-constructor-instanceof/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7005-dyn-constructor-instanceof/main.ts.json new file mode 100644 index 000000000..313888b49 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7005-dyn-constructor-instanceof/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7005-dyn-constructor-instanceof/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7006-evolving-array-aliases.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7006-evolving-array-aliases.ts.json new file mode 100644 index 000000000..cb3d36498 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7006-evolving-array-aliases.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7006-evolving-array-aliases.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7007-filter-written-type-predicates.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7007-filter-written-type-predicates.ts.json new file mode 100644 index 000000000..d77ee1a57 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7007-filter-written-type-predicates.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7007-filter-written-type-predicates.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7008-js-unit-initialized-properties/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7008-js-unit-initialized-properties/main.ts.json new file mode 100644 index 000000000..85f5a6e12 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7008-js-unit-initialized-properties/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7008-js-unit-initialized-properties/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7011-for-in-dictionary-deletes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7011-for-in-dictionary-deletes.ts.json new file mode 100644 index 000000000..3b2b79c3b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7011-for-in-dictionary-deletes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7011-for-in-dictionary-deletes.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7030-spawn-sync-env.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7030-spawn-sync-env.ts.json new file mode 100644 index 000000000..f8afbe00b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7030-spawn-sync-env.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7030-spawn-sync-env.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7036-child-env-path-lookup.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7036-child-env-path-lookup.ts.json new file mode 100644 index 000000000..171ab060a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7036-child-env-path-lookup.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7036-child-env-path-lookup.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7143-optional-read-integer-index.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7143-optional-read-integer-index.ts.json new file mode 100644 index 000000000..ae0d1f610 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7143-optional-read-integer-index.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7143-optional-read-integer-index.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7150-cipher-legacy-modes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7150-cipher-legacy-modes.ts.json new file mode 100644 index 000000000..c0e5f9812 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7150-cipher-legacy-modes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7150-cipher-legacy-modes.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7151-cipher-legacy-errors.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7151-cipher-legacy-errors.cjs.json new file mode 100644 index 000000000..1f37c6b64 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7151-cipher-legacy-errors.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7151-cipher-legacy-errors.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7152-crypto-prime.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7152-crypto-prime.ts.json new file mode 100644 index 000000000..722ebaacb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7152-crypto-prime.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7152-crypto-prime.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7153-crypto-prime-errors.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7153-crypto-prime-errors.cjs.json new file mode 100644 index 000000000..9d121895a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7153-crypto-prime-errors.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7153-crypto-prime-errors.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7184-reduce-runtime-accumulator.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7184-reduce-runtime-accumulator.ts.json new file mode 100644 index 000000000..b93eb61b6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7184-reduce-runtime-accumulator.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7184-reduce-runtime-accumulator.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7185-array-callback-arguments-length.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7185-array-callback-arguments-length.ts.json new file mode 100644 index 000000000..2514bcaa8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7185-array-callback-arguments-length.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7185-array-callback-arguments-length.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7261-acyclic-result-builders.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7261-acyclic-result-builders.ts.json new file mode 100644 index 000000000..4f4afd821 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7261-acyclic-result-builders.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7261-acyclic-result-builders.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7301-running-extrema-branches.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7301-running-extrema-branches.ts.json new file mode 100644 index 000000000..6dcb4b57d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7301-running-extrema-branches.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7301-running-extrema-branches.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7322-union-scalar-equality.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7322-union-scalar-equality.ts.json new file mode 100644 index 000000000..73257a3f4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7322-union-scalar-equality.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7322-union-scalar-equality.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7323-loop-bound-parameter-versions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7323-loop-bound-parameter-versions.ts.json new file mode 100644 index 000000000..0a79c8cdb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7323-loop-bound-parameter-versions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7323-loop-bound-parameter-versions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7324-loop-bound-parameter-window-sums.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7324-loop-bound-parameter-window-sums.ts.json new file mode 100644 index 000000000..aedd853fb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7324-loop-bound-parameter-window-sums.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7324-loop-bound-parameter-window-sums.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/arguments-length-self-call.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/arguments-length-self-call.ts.json new file mode 100644 index 000000000..8ed5027e5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/arguments-length-self-call.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/arguments-length-self-call.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/filter-narrow.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/filter-narrow.ts.json deleted file mode 100644 index 40d5e462a..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/filter-narrow.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/diagnostics/filter-narrow.ts" - ], - "diags": [] -} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/chalk5-builder/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/chalk5-builder/main.ts.json new file mode 100644 index 000000000..8f7268daf --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/chalk5-builder/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/packages/compiler/shims/chalk/v5.ts", + "/tests/fixtures/npm/cases/chalk5-builder/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/chalk5/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/chalk5/main.ts.json index 065c96347..8365d4db9 100644 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/chalk5/main.ts.json +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/chalk5/main.ts.json @@ -1,5 +1,6 @@ { "order": [ + "/packages/compiler/shims/chalk/v5.ts", "/tests/fixtures/npm/cases/chalk5/main.ts" ], "diags": [] diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-buffered/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-buffered/main.ts.json new file mode 100644 index 000000000..a48e1af78 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-buffered/main.ts.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/packages/compiler/shims/hot-shots/client.ts", + "/packages/compiler/shims/hot-shots/v8.ts", + "/tests/fixtures/npm/cases/hot-shots-buffered/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-send-error/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-send-error/main.ts.json new file mode 100644 index 000000000..716cfe02f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-send-error/main.ts.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/packages/compiler/shims/hot-shots/client.ts", + "/packages/compiler/shims/hot-shots/v8.ts", + "/tests/fixtures/npm/cases/hot-shots-send-error/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-tcp/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-tcp/main.ts.json new file mode 100644 index 000000000..f131361e8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-tcp/main.ts.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/packages/compiler/shims/hot-shots/client.ts", + "/packages/compiler/shims/hot-shots/v8.ts", + "/tests/fixtures/npm/cases/hot-shots-tcp/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-udp/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-udp/main.ts.json index 54baff890..a3014fb9f 100644 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-udp/main.ts.json +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-udp/main.ts.json @@ -1,5 +1,7 @@ { "order": [ + "/packages/compiler/shims/hot-shots/client.ts", + "/packages/compiler/shims/hot-shots/v8.ts", "/tests/fixtures/npm/cases/hot-shots-udp/main.ts" ], "diags": [] diff --git a/packages/compiler/test/ts7/facade.test.ts b/packages/compiler/test/ts7/facade.test.ts index 94bab8b31..8e03400a2 100644 --- a/packages/compiler/test/ts7/facade.test.ts +++ b/packages/compiler/test/ts7/facade.test.ts @@ -835,6 +835,37 @@ export function f(input: number): number { expect(panics).toBe(warmPanics); }); +test("a batch method the server answers as unknown is asked once, then node by node", () => { + const w = buildTwoWorlds(RICH_TS, host); + worlds.push(w); + const sf = w.p7.getSourceFile(w.files[0]!)!; + const raw = w.p7.project.checker; + const requests: Record = {}; + // A server without the batch methods, as tsgo is. + const older = new Proxy(raw, { + get(target, prop, receiver) { + const value = Reflect.get(target, prop, receiver); + if (prop === "getContextualTypes" || prop === "getResolvedSignatures") { + return () => { + requests[prop] = (requests[prop] ?? 0) + 1; + throw new Error(`unknown API method "${prop}"`); + }; + } + return typeof value === "function" ? value.bind(target) : value; + }, + }) as Checker; + const facade = new CheckerFacade(older); + + facade.prefetchRoots([sf]); + expect(requests).toEqual({ getContextualTypes: 1, getResolvedSignatures: 1 }); + facade.prefetchRoots(sf.statements.nodes); + facade.prefetchContextualTypes(collectNodes(w)); + expect(requests).toEqual({ getContextualTypes: 1, getResolvedSignatures: 1 }); + // The node-by-node queries still answer. + const call = collectNodes(w).find(ad.isCallExpression)!; + expect(facade.getResolvedSignature(call)).toBe(raw.getResolvedSignature(call)); +}); + test("autoPrefetch: false degrades to per-call queries (the escape hatch works)", () => { const w = buildTwoWorlds(RICH_TS, host); worlds.push(w); diff --git a/packages/compiler/test/union-scalar-equality-emission.test.ts b/packages/compiler/test/union-scalar-equality-emission.test.ts new file mode 100644 index 000000000..62d59e258 --- /dev/null +++ b/packages/compiler/test/union-scalar-equality-emission.test.ts @@ -0,0 +1,45 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile } from "../src/index.js"; + +async function emitLlvm(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-union-scalar-eq-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ll"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "llvm" }); + expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true); + return await readFile(outPath, "utf8"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +const source = ` +function zeros(a: number[], at: number[]): number { + let n = 0; + for (const i of at) if (a[i] === 0) n++; + return n; +} +function flags(a: (boolean | undefined)[]): number { + let n = 0; + for (let i = 0; i < a.length; i++) if (a[i] === true) n++; + return n; +} +console.log(zeros([0, 1, 0], [0, 1, 2, 5]), flags([true, undefined, false])); +`; + +test("comparing a union with a number or boolean reads its payload inline", async () => { + const llvm = await emitLlvm(source); + for (const name of ["zeros", "flags"]) { + const fn = new RegExp(`^define internal double @sc_bf_${name}\\([^]*?^}`, "m").exec(llvm)?.[0]; + expect(fn, name).toBeDefined(); + // The tag test and the payload load stay in the function, so the + // union's stack box has no escaping use. + expect(fn).not.toMatch(/@scr_union_get_(f64|bool)\(/); + expect(fn).toMatch(/getelementptr inbounds %ScrUnion, ptr %\S+, i64 0, i32 5/); + } +}); diff --git a/packages/experimental/package.json b/packages/experimental/package.json index 5b6906de6..cfb4151a4 100644 --- a/packages/experimental/package.json +++ b/packages/experimental/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/experimental", - "version": "0.4.1", + "version": "0.4.2", "description": "Experimental scriptc APIs, with Node.js implementations: lifetime regions for ProgramLifetime objects, intern tables shared between threads, and per-thread slots on shared objects", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/llvm-darwin-arm64/package.json b/packages/llvm-darwin-arm64/package.json index 472056c39..615fe9d81 100644 --- a/packages/llvm-darwin-arm64/package.json +++ b/packages/llvm-darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/llvm-darwin-arm64", - "version": "0.4.1", + "version": "0.4.2", "description": "Pinned LLVM code-generation helper for scriptc on macOS arm64", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", diff --git a/packages/llvm-darwin-x64/package.json b/packages/llvm-darwin-x64/package.json index 7f55cb6b5..fd6caf976 100644 --- a/packages/llvm-darwin-x64/package.json +++ b/packages/llvm-darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/llvm-darwin-x64", - "version": "0.4.1", + "version": "0.4.2", "description": "Pinned LLVM code-generation helper for scriptc on macOS x64", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", diff --git a/packages/llvm-linux-arm64-gnu/package.json b/packages/llvm-linux-arm64-gnu/package.json index 0e1a55b22..7d66257f4 100644 --- a/packages/llvm-linux-arm64-gnu/package.json +++ b/packages/llvm-linux-arm64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/llvm-linux-arm64-gnu", - "version": "0.4.1", + "version": "0.4.2", "description": "Pinned LLVM code-generation helper for scriptc on Linux arm64", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", diff --git a/packages/llvm-linux-arm64-musl/package.json b/packages/llvm-linux-arm64-musl/package.json index efdd97444..b3a01089d 100644 --- a/packages/llvm-linux-arm64-musl/package.json +++ b/packages/llvm-linux-arm64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/llvm-linux-arm64-musl", - "version": "0.4.1", + "version": "0.4.2", "description": "Statically linked LLVM code-generation helper for scriptc on Linux arm64 musl", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", diff --git a/packages/llvm-linux-x64-gnu/package.json b/packages/llvm-linux-x64-gnu/package.json index 39667d535..0b3264ec7 100644 --- a/packages/llvm-linux-x64-gnu/package.json +++ b/packages/llvm-linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/llvm-linux-x64-gnu", - "version": "0.4.1", + "version": "0.4.2", "description": "Pinned LLVM code-generation helper for scriptc on Linux x64", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", diff --git a/packages/llvm-linux-x64-musl/package.json b/packages/llvm-linux-x64-musl/package.json index 9efd41845..6c83d1cb9 100644 --- a/packages/llvm-linux-x64-musl/package.json +++ b/packages/llvm-linux-x64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/llvm-linux-x64-musl", - "version": "0.4.1", + "version": "0.4.2", "description": "Statically linked LLVM code-generation helper for scriptc on Linux x64 musl", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", diff --git a/packages/llvm-win32-x64-msvc/package.json b/packages/llvm-win32-x64-msvc/package.json index 329fa308c..9fb682f0f 100644 --- a/packages/llvm-win32-x64-msvc/package.json +++ b/packages/llvm-win32-x64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/llvm-win32-x64-msvc", - "version": "0.4.1", + "version": "0.4.2", "description": "Pinned LLVM code-generation helper for scriptc on Windows x64", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-android-arm64/package.json b/packages/runtime-android-arm64/package.json index 75c54e9c3..362fbc572 100644 --- a/packages/runtime-android-arm64/package.json +++ b/packages/runtime-android-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-android-arm64", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc library runtime pack for Android arm64", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-darwin-arm64/package.json b/packages/runtime-darwin-arm64/package.json index 7473b05f3..a4176ed60 100644 --- a/packages/runtime-darwin-arm64/package.json +++ b/packages/runtime-darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-darwin-arm64", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc runtime pack for macOS arm64", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index 631750cdf..3d22b2360 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -340,7 +340,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: RUNTIME_PACK_MATRIX.target, - runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" }, + runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime-darwin-x64/package.json b/packages/runtime-darwin-x64/package.json index ab8666c88..91bc4182b 100644 --- a/packages/runtime-darwin-x64/package.json +++ b/packages/runtime-darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-darwin-x64", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc runtime pack for macOS x64", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-ios-arm64/package.json b/packages/runtime-ios-arm64/package.json index a3a254685..2fdd68897 100644 --- a/packages/runtime-ios-arm64/package.json +++ b/packages/runtime-ios-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-ios-arm64", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc library runtime pack for iOS arm64", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-ios-simulator-arm64/package.json b/packages/runtime-ios-simulator-arm64/package.json index 7981422b8..cbdf96d1d 100644 --- a/packages/runtime-ios-simulator-arm64/package.json +++ b/packages/runtime-ios-simulator-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-ios-simulator-arm64", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc library runtime pack for iOS simulator arm64", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-linux-arm64-gnu/package.json b/packages/runtime-linux-arm64-gnu/package.json index d633ac303..e0d228925 100644 --- a/packages/runtime-linux-arm64-gnu/package.json +++ b/packages/runtime-linux-arm64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-linux-arm64-gnu", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc runtime pack for Linux arm64 glibc", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-linux-arm64-musl/package.json b/packages/runtime-linux-arm64-musl/package.json index 460ca25fa..70eee223f 100644 --- a/packages/runtime-linux-arm64-musl/package.json +++ b/packages/runtime-linux-arm64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-linux-arm64-musl", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc runtime pack for Linux arm64 musl", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-linux-x64-gnu/package.json b/packages/runtime-linux-x64-gnu/package.json index 9c6336a04..5822fd21d 100644 --- a/packages/runtime-linux-x64-gnu/package.json +++ b/packages/runtime-linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-linux-x64-gnu", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc runtime pack for Linux x64 glibc", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-linux-x64-musl/package.json b/packages/runtime-linux-x64-musl/package.json index e9f885a5c..1db34d4a7 100644 --- a/packages/runtime-linux-x64-musl/package.json +++ b/packages/runtime-linux-x64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-linux-x64-musl", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc runtime pack for Linux x64 musl", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index 0e81033ad..909412bc7 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -437,7 +437,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: matrix.target, - runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" }, + runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime-wasm32-wasi/package.json b/packages/runtime-wasm32-wasi/package.json index c9a40c2c0..5bbd23755 100644 --- a/packages/runtime-wasm32-wasi/package.json +++ b/packages/runtime-wasm32-wasi/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-wasm32-wasi", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc runtime pack for WASI Preview 1", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-win32-x64-msvc/package.json b/packages/runtime-win32-x64-msvc/package.json index f7e4c9860..df4a9a109 100644 --- a/packages/runtime-win32-x64-msvc/package.json +++ b/packages/runtime-win32-x64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-win32-x64-msvc", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc runtime pack for Windows x64 MSVC", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 1af249b9d..9232266af 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime", - "version": "0.4.1", + "version": "0.4.2", "description": "scriptc native runtime sources and vendored dependencies", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index c00d1b4f6..d4cdf0810 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -60,9 +60,10 @@ static void *scr_arr_alloc_start(const ScrArr *a) { return (char *)scr_arr_block(a) - scr_arr_prefix(a); } -/* Bytes per dense slot with its state: an 8-byte value, or a boolean byte. */ +/* Bytes per dense slot with its state: an 8-byte value, a boolean byte or + * a 4-byte compact reference. */ static size_t scr_arr_slot_unit(const ScrArr *a) { - return a->elem == SCR_ELEM_BOOL ? 2 * sizeof(uint8_t) : sizeof(uint64_t) + sizeof(uint8_t); + return scr_arr_slot_width(a) + sizeof(uint8_t); } /* Bytes of dense storage (head room included) after the prefix. */ @@ -212,6 +213,11 @@ static bool scr_elem_is_ref(ScrElemKind k) { k == SCR_ELEM_REF; } +/* Whether `a`'s element type comes with element ops (scr_arr_ops). */ +static bool scr_arr_has_ops(const ScrArr *a) { + return a->elem == SCR_ELEM_REF || a->elem == SCR_ELEM_CREF; +} + /* Releases an element slot of kind `elem`, with element ops `ops` for * SCR_ELEM_REF. An array's kind and ops never change, so loops look them up * once (element releases may run teardown, which reloads anything read @@ -266,6 +272,7 @@ typedef struct { static uint64_t scr_arr_storage_get(const ScrArrStorage *s, size_t index) { if (s->elem == SCR_ELEM_BOOL) return ((const uint8_t *)s->data)[index]; + if (s->elem == SCR_ELEM_CREF) return scr_cref_decode(((const uint32_t *)s->data)[index]); return s->data[index]; } @@ -388,9 +395,17 @@ static void scr_arr_sparse_drop_prefix(ScrArr *a, size_t count) { } /* Bytes per dense slot: the value and its state byte. Boolean arrays - * store a one-byte value. */ + * store a one-byte value, compact reference arrays a 4-byte one. */ #define SCR_ARR_SLOT_BYTES (sizeof(uint64_t) + sizeof(uint8_t)) #define SCR_ARR_BOOL_SLOT_BYTES (2 * sizeof(uint8_t)) +#define SCR_ARR_CREF_SLOT_BYTES (sizeof(uint32_t) + sizeof(uint8_t)) + +/* SCR_ARR_*_SLOT_BYTES for an element kind. */ +static size_t scr_elem_slot_unit(uint8_t elem) { + return elem == SCR_ELEM_BOOL ? SCR_ARR_BOOL_SLOT_BYTES + : elem == SCR_ELEM_CREF ? SCR_ARR_CREF_SLOT_BYTES + : SCR_ARR_SLOT_BYTES; +} /* Appends to an array smaller than this grow straight to it. */ #define SCR_ARR_MIN_GROWTH ((size_t)4) @@ -469,7 +484,7 @@ static void scr_arr_drop_head(ScrArr *a) { * for storage without head room (booleans, inline slots); the caller then * moves the tail down itself. */ static bool scr_arr_drop_front(ScrArr *a, size_t count) { - if (a->elem == SCR_ELEM_BOOL || scr_arr_inline(a)) return false; + if (scr_arr_slot_width(a) != sizeof(uint64_t) || scr_arr_inline(a)) return false; if (count == 0) return true; size_t head = scr_arr_head(a); memset(scr_arr_state_run(a, 0, count), SCR_ARR_HOLE, count); @@ -504,18 +519,21 @@ static bool scr_arr_drop_front(ScrArr *a, size_t count) { #endif /* Inline capacity of a small construction with `prefix` bytes of block - * before the slots. */ -static size_t scr_arr_small_cap(size_t prefix, bool bools) { + * before the slots of `unit` bytes (SCR_ARR_*_SLOT_BYTES). */ +static size_t scr_arr_small_cap(size_t prefix, size_t unit) { #if SCR_SMALL_ALLOC /* One return per slot unit, so each folds to a constant at its callers. */ - if (bools) + if (unit == SCR_ARR_BOOL_SLOT_BYTES) return (scr_sa_class_size(scr_sa_class(prefix + SCR_ARR_SMALL_SLOTS * SCR_ARR_BOOL_SLOT_BYTES)) - prefix) / SCR_ARR_BOOL_SLOT_BYTES; + if (unit == SCR_ARR_CREF_SLOT_BYTES) + return (scr_sa_class_size(scr_sa_class(prefix + SCR_ARR_SMALL_SLOTS * SCR_ARR_CREF_SLOT_BYTES)) - prefix) / + SCR_ARR_CREF_SLOT_BYTES; return (scr_sa_class_size(scr_sa_class(prefix + SCR_ARR_SMALL_SLOTS * SCR_ARR_SLOT_BYTES)) - prefix) / SCR_ARR_SLOT_BYTES; #else (void)prefix; - (void)bools; + (void)unit; return SCR_ARR_SMALL_SLOTS; #endif } @@ -528,26 +546,27 @@ static size_t scr_arr_small_cap(size_t prefix, bool bools) { #define SCR_ARR_KNOWN_INLINE_MAX ((size_t)16) /* Inline capacity of a construction of `need` elements with `prefix` bytes - * of block before the slots, rounded up to fill the block; 0 when its slots - * stay separate. */ -static size_t scr_arr_known_cap(size_t prefix, size_t need, bool bools) { + * of block before the slots of `unit` bytes (SCR_ARR_*_SLOT_BYTES), rounded + * up to fill the block; 0 when its slots stay separate. */ +static size_t scr_arr_known_cap(size_t prefix, size_t need, size_t unit) { if (need == 0 || need > SCR_ARR_KNOWN_INLINE_MAX) return 0; #if SCR_SMALL_ALLOC - size_t unit = bools ? SCR_ARR_BOOL_SLOT_BYTES : SCR_ARR_SLOT_BYTES; size_t bytes = prefix + need * unit; if (bytes > SCR_SA_MAX) return 0; return (scr_sa_class_size(scr_sa_class(bytes)) - prefix) / unit; #else (void)prefix; - (void)bools; + (void)unit; return need; #endif } -static void scr_arr_use_inline(ScrArr *a, size_t cap) { +/* Points `a` at `cap` inline slots of `unit` bytes (SCR_ARR_*_SLOT_BYTES), + * all holes. */ +static void scr_arr_use_inline(ScrArr *a, size_t cap, size_t unit) { a->data = (uint64_t *)((char *)a + SCR_ARR_INLINE_OFFSET); a->cap = (uint32_t)cap; - memset(scr_arr_state_run(a, 0, cap), SCR_ARR_HOLE, cap); + memset((char *)a->data + cap * (unit - 1), SCR_ARR_HOLE, cap); a->spare |= SCR_ARR_INLINE_SLOTS; } @@ -582,15 +601,15 @@ typedef enum { * reallocation. Growth keeps its own sequence (4, 8, 16, ...): rounding it * as well moves array storage into other size classes and measured * slower. */ -static size_t scr_arr_block_cap(size_t cap, bool bools) { +static size_t scr_arr_block_cap(size_t cap, size_t unit) { #if SCR_SMALL_ALLOC - size_t bytes = cap * (bools ? SCR_ARR_BOOL_SLOT_BYTES : SCR_ARR_SLOT_BYTES); + size_t bytes = cap * unit; if (bytes <= SCR_SA_MAX) { size_t block = scr_sa_class_size(scr_sa_class((bytes + 15) & ~(size_t)15)); - return bools ? block / SCR_ARR_BOOL_SLOT_BYTES : block / SCR_ARR_SLOT_BYTES; + return block / unit; } #else - (void)bools; + (void)unit; #endif return cap; } @@ -609,14 +628,14 @@ static void scr_arr_grow_dense_slow(ScrArr *a, size_t need, ScrArrGrow mode); * paths pick the same capacity. */ static inline void scr_arr_grow_dense_to(ScrArr *a, size_t need, ScrArrGrow mode) { if (need <= a->cap) return; - if (a->elem == SCR_ELEM_BOOL || (a->spare & (SCR_ARR_INLINE_SLOTS | SCR_ARR_HEAD_ROOM | SCR_ARR_HAS_EXT)) || - need > SCR_ARR_DENSE_LIMIT) { + if (a->elem == SCR_ELEM_BOOL || a->elem == SCR_ELEM_CREF || + (a->spare & (SCR_ARR_INLINE_SLOTS | SCR_ARR_HEAD_ROOM | SCR_ARR_HAS_EXT)) || need > SCR_ARR_DENSE_LIMIT) { scr_arr_grow_dense_slow(a, need, mode); return; } size_t cap; if (mode == SCR_ARR_GROW_EXACT) { - cap = scr_arr_block_cap(need, false); + cap = scr_arr_block_cap(need, SCR_ARR_SLOT_BYTES); } else { cap = a->cap < SCR_ARR_MIN_GROWTH ? SCR_ARR_MIN_GROWTH : a->cap; while (cap < need) cap += cap; @@ -660,12 +679,11 @@ static void scr_arr_grow_dense_slow(ScrArr *a, size_t need, ScrArrGrow mode) { mode = SCR_ARR_GROW_APPEND; } } - bool bools = a->elem == SCR_ELEM_BOOL; size_t width = scr_arr_slot_width(a); - size_t unit = bools ? SCR_ARR_BOOL_SLOT_BYTES : SCR_ARR_SLOT_BYTES; + size_t unit = scr_arr_slot_unit(a); size_t cap; if (mode == SCR_ARR_GROW_EXACT) { - cap = scr_arr_block_cap(need, bools); + cap = scr_arr_block_cap(need, unit); } else { /* Inline slots overflow into the ordinary sequence (4, 8, 16, ...). */ size_t from = scr_arr_inline(a) ? 0 : a->cap; @@ -723,7 +741,7 @@ static void scr_arr_grow_dense(ScrArr *a, size_t need) { * array's allocation. Returns false when the caller must move the elements * itself. */ static bool scr_arr_open_head(ScrArr *a, size_t count) { - if (a->elem == SCR_ELEM_BOOL || scr_arr_inline(a)) return false; + if (scr_arr_slot_width(a) != sizeof(uint64_t) || scr_arr_inline(a)) return false; size_t head = scr_arr_head(a); if (head < count) { size_t len = a->len; @@ -851,6 +869,20 @@ static inline uint8_t scr_arr_state_at(const ScrArr *a, size_t index, uint64_t * return sparse.state; } +/* scr_arr_state_at for a number[] (SCR_ELEM_F64): 8-byte slots, with no + * element-kind test. */ +static inline uint8_t scr_arr_f64_state_at(const ScrArr *a, size_t index, uint64_t *out) { + if (index < a->cap) { + uint8_t state = *scr_arr_state_in(scr_arr_states_end_w(a, 8), index); + if (state == SCR_ARR_HOLE) return SCR_ARR_HOLE; + *out = a->data[index]; + return state; + } + ScrArrStateSlot sparse = scr_arr_sparse_state_at(a, index); + if (sparse.state != SCR_ARR_HOLE) *out = sparse.slot; + return sparse.state; +} + static inline bool scr_arr_slot_at(const ScrArr *a, size_t index, uint64_t *out) { return scr_arr_state_at(a, index, out) != SCR_ARR_HOLE; } @@ -1148,9 +1180,10 @@ static void scr_arr_copy_dense(ScrArr *dst, size_t to, const ScrArr *src, if (count == 0) return; scr_arr_cut(dst, to); /* 8-byte slots: a constant slot size, and constant-width state ends. */ - bool wide = dst->elem != SCR_ELEM_BOOL; - uint8_t *dst_states = wide ? scr_arr_states_end_w(dst, 8) : scr_arr_states_end_w(dst, 1); - const uint8_t *src_states = wide ? scr_arr_states_end_w(src, 8) : scr_arr_states_end_w(src, 1); + size_t width = scr_arr_slot_width(dst); + bool wide = width == sizeof(uint64_t); + uint8_t *dst_states = wide ? scr_arr_states_end_w(dst, 8) : scr_arr_states_end_w(dst, width); + const uint8_t *src_states = wide ? scr_arr_states_end_w(src, 8) : scr_arr_states_end_w(src, width); if (!reverse && wide) { scr_arr_copy_slots(dst->data + to, src->data + from, count); /* A range below the source's hole-free prefix holds only values. */ @@ -1185,21 +1218,28 @@ static void scr_arr_copy_dense(ScrArr *dst, size_t to, const ScrArr *src, /* Store `count` 8-byte value slots into dense storage starting at `to`. */ static void scr_arr_put_slots(ScrArr *a, size_t to, size_t count, const uint64_t *slots) { - if (a->elem != SCR_ELEM_BOOL) { + if (scr_arr_slot_width(a) == sizeof(uint64_t)) { scr_arr_copy_slots(a->data + to, slots, count); return; } for (size_t i = 0; i < count; i++) scr_arr_dense_put(a, to + i, slots[i]); } +/* The slot unit of an array that scr_arr_new, scr_arr_new_small or + * scr_arr_new_inline makes. Their kinds have no element ops, so they are + * never SCR_ELEM_CREF (scr_arr_new_ops makes those). */ +static size_t scr_arr_plain_slot_unit(ScrElemKind elem) { + return elem == SCR_ELEM_BOOL ? SCR_ARR_BOOL_SLOT_BYTES : SCR_ARR_SLOT_BYTES; +} + /* Inlined into each constructor, so `small` and the slot unit fold to * constants there. */ static inline __attribute__((always_inline)) ScrArr *scr_arr_make(ScrElemKind elem, size_t initial_cap, bool small) { - size_t unit = elem == SCR_ELEM_BOOL ? SCR_ARR_BOOL_SLOT_BYTES : SCR_ARR_SLOT_BYTES; - size_t inline_cap = small ? scr_arr_small_cap(SCR_ARR_INLINE_OFFSET, elem == SCR_ELEM_BOOL) - : scr_arr_known_cap(SCR_ARR_INLINE_OFFSET, initial_cap, elem == SCR_ELEM_BOOL); + size_t unit = scr_arr_plain_slot_unit(elem); + size_t inline_cap = small ? scr_arr_small_cap(SCR_ARR_INLINE_OFFSET, unit) + : scr_arr_known_cap(SCR_ARR_INLINE_OFFSET, initial_cap, unit); ScrArr *a = scr_mem_alloc(inline_cap ? SCR_ARR_INLINE_OFFSET + inline_cap * unit : sizeof(ScrArr)); if (!a) scr_arr_oom(); a->rc = 1; @@ -1210,7 +1250,7 @@ static inline __attribute__((always_inline)) ScrArr *scr_arr_make(ScrElemKind el a->spare = 0; a->filled = 0; a->data = NULL; - if (inline_cap) scr_arr_use_inline(a, inline_cap); + if (inline_cap) scr_arr_use_inline(a, inline_cap, unit); else if (initial_cap > 0) scr_arr_grow_dense_to(a, initial_cap, SCR_ARR_GROW_EXACT); #ifdef SCR_RC_AUDIT scr_live_arrays++; @@ -1231,7 +1271,7 @@ ScrArr *scr_arr_new_small(ScrElemKind elem) { return scr_arr_make(elem, 0, true) ScrArr *scr_arr_new_inline(ScrElemKind elem, size_t cap) { if (cap == 0) return scr_arr_make(elem, 0, false); if (cap > SCR_ARR_MAX_LENGTH) scr_arr_oom(); - size_t unit = elem == SCR_ELEM_BOOL ? SCR_ARR_BOOL_SLOT_BYTES : SCR_ARR_SLOT_BYTES; + size_t unit = scr_arr_plain_slot_unit(elem); ScrArr *a = scr_mem_alloc(SCR_ARR_INLINE_OFFSET + cap * unit); if (!a) scr_arr_oom(); a->rc = 1; @@ -1242,7 +1282,7 @@ ScrArr *scr_arr_new_inline(ScrElemKind elem, size_t cap) { a->spare = 0; a->filled = 0; a->data = NULL; - scr_arr_use_inline(a, cap); + scr_arr_use_inline(a, cap, unit); #ifdef SCR_RC_AUDIT scr_live_arrays++; #endif @@ -1250,9 +1290,7 @@ ScrArr *scr_arr_new_inline(ScrElemKind elem, size_t cap) { } void scr_arr_outline(ScrArr *a) { - if (scr_arr_inline(a)) - scr_arr_move_inline(a, a->cap, - a->elem == SCR_ELEM_BOOL ? SCR_ARR_BOOL_SLOT_BYTES : SCR_ARR_SLOT_BYTES); + if (scr_arr_inline(a)) scr_arr_move_inline(a, a->cap, scr_arr_slot_unit(a)); /* The swapped fields do not include `spare`: no head room either. */ if (a->spare & SCR_ARR_HEAD_ROOM) { scr_arr_drop_head(a); @@ -1329,18 +1367,20 @@ static void scr_arr_gc_free(void *a0) { scr_cyc_free_inline(a); } -/* Inlined into each constructor like scr_arr_make: with `small` and the - * count constant, scr_arr_new_ops_small's size class folds to a table - * index. */ -static inline __attribute__((always_inline)) ScrArr *scr_arr_make_ops(const ScrArrOps *ops, - size_t initial_cap, - bool small) { +/* Inlined into each constructor like scr_arr_make: with `small`, the count + * and `elem` constant, scr_arr_new_ops_small's size class folds to a table + * index and the slot sizes to constants. */ +static inline __attribute__((always_inline)) ScrArr *scr_arr_make_ops_kind(const ScrArrOps *ops, + size_t initial_cap, + bool small, + uint8_t elem) { ScrArr *a; bool cyc = ops->trace != NULL; + size_t unit = scr_elem_slot_unit(elem); size_t prefix = SCR_ARR_INLINE_OFFSET + (cyc ? sizeof(ScrCycHdr) : 0); size_t inline_cap = - small ? scr_arr_small_cap(prefix, false) : scr_arr_known_cap(prefix, initial_cap, false); - size_t size = inline_cap ? SCR_ARR_INLINE_OFFSET + inline_cap * SCR_ARR_SLOT_BYTES : sizeof(ScrArr); + small ? scr_arr_small_cap(prefix, unit) : scr_arr_known_cap(prefix, initial_cap, unit); + size_t size = inline_cap ? SCR_ARR_INLINE_OFFSET + inline_cap * unit : sizeof(ScrArr); if (cyc) { static ScrCycType cyc_type = { &scr_arr_trace_v, &scr_arr_gc_free, SCR_CYC_KIND_ARRAY }; a = scr_cyc_alloc_inline(size, &cyc_type); @@ -1351,7 +1391,7 @@ static inline __attribute__((always_inline)) ScrArr *scr_arr_make_ops(const ScrA a->rc = 1; a->len = 0; a->cap = 0; - a->elem = SCR_ELEM_REF; + a->elem = elem; a->cyc = cyc; /* The ops id is written with the other header fields: reading `spare` * back after scr_arr_use_inline sets its inline flag stalls the @@ -1360,7 +1400,7 @@ static inline __attribute__((always_inline)) ScrArr *scr_arr_make_ops(const ScrA a->spare = (uint16_t)(id << SCR_ARR_OPS_SHIFT); a->filled = 0; a->data = NULL; - if (inline_cap) scr_arr_use_inline(a, inline_cap); + if (inline_cap) scr_arr_use_inline(a, inline_cap, unit); else if (initial_cap > 0) scr_arr_grow_dense_to(a, initial_cap, SCR_ARR_GROW_EXACT); /* After the storage: ops past the registry live in the ext, whose * pointer precedes the dense allocation, so inline slots move out first @@ -1372,6 +1412,16 @@ static inline __attribute__((always_inline)) ScrArr *scr_arr_make_ops(const ScrA return a; } +/* SCR_ELEM_CREF for compact element types, SCR_ELEM_REF otherwise. Each + * branch inlines scr_arr_make_ops_kind with a constant kind. */ +static inline __attribute__((always_inline)) ScrArr *scr_arr_make_ops(const ScrArrOps *ops, + size_t initial_cap, + bool small) { + if (__builtin_expect(scr_arr_ops_compact(ops), 0)) + return scr_arr_make_ops_kind(ops, initial_cap, small, SCR_ELEM_CREF); + return scr_arr_make_ops_kind(ops, initial_cap, small, SCR_ELEM_REF); +} + ScrArr *scr_arr_new_ops(const ScrArrOps *ops, size_t initial_cap) { return scr_arr_make_ops(ops, initial_cap, false); } @@ -1676,6 +1726,7 @@ static ScrArr *scr_arr_fill_slot(ScrArr *a, uint64_t slot, uint8_t state, if (!refs && until <= a->cap) { if (state != SCR_ARR_VALUE) scr_arr_cut(a, from); if (a->elem == SCR_ELEM_BOOL) memset(scr_arr_dense_at(a, from), (int)slot, until - from); + else if (a->elem == SCR_ELEM_CREF) for (size_t i = from; i < until; i++) scr_arr_dense_put(a, i, slot); else for (size_t i = from; i < until; i++) a->data[i] = slot; memset(scr_arr_state_run(a, from, until - from), state, until - from); } else { @@ -1712,7 +1763,7 @@ double scr_math_max_arr(ScrArr *a) { double best = -INFINITY; for (size_t i = 0; i < a->len; i++) { uint64_t slot; - if (scr_arr_state_at(a, i, &slot) != SCR_ARR_VALUE) return NAN; + if (scr_arr_f64_state_at(a, i, &slot) != SCR_ARR_VALUE) return NAN; double v = scr_slot_to_f64(slot); if (isnan(v)) return v; if (v > best || (v == 0.0 && best == 0.0 && !signbit(v))) best = v; @@ -1724,7 +1775,7 @@ double scr_math_min_arr(ScrArr *a) { double best = INFINITY; for (size_t i = 0; i < a->len; i++) { uint64_t slot; - if (scr_arr_state_at(a, i, &slot) != SCR_ARR_VALUE) return NAN; + if (scr_arr_f64_state_at(a, i, &slot) != SCR_ARR_VALUE) return NAN; double v = scr_slot_to_f64(slot); if (isnan(v)) return v; if (v < best || (v == 0.0 && best == 0.0 && signbit(v))) best = v; @@ -1739,7 +1790,7 @@ double scr_math_hypot_arr(ScrArr *a) { bool has_nan = false; for (size_t i = 0; i < a->len; i++) { uint64_t slot; - if (scr_arr_state_at(a, i, &slot) != SCR_ARR_VALUE) { + if (scr_arr_f64_state_at(a, i, &slot) != SCR_ARR_VALUE) { has_nan = true; continue; } @@ -1754,7 +1805,7 @@ double scr_math_hypot_arr(ScrArr *a) { double correction = 0.0; for (size_t i = 0; i < a->len; i++) { uint64_t slot; - (void)scr_arr_state_at(a, i, &slot); + (void)scr_arr_f64_state_at(a, i, &slot); double v = scr_slot_to_f64(slot) / scale; double term = v * v - correction; double next = sum + term; @@ -1830,9 +1881,9 @@ void *scr_arr_peek_ref(const ScrArr *a, double i) { if (i >= 0 && i < (double)a->cap) { size_t idx = (size_t)i; if ((double)idx == i) { - if (idx < a->filled) return scr_slot_to_ptr(a->data[idx]); + if (idx < a->filled) return scr_slot_to_ptr(scr_arr_dense_get(a, idx)); if (idx >= a->len || *scr_arr_state_ptr(a, idx) != SCR_ARR_VALUE) return NULL; - return scr_slot_to_ptr(a->data[idx]); + return scr_slot_to_ptr(scr_arr_dense_get(a, idx)); } } uint64_t slot; @@ -1886,7 +1937,7 @@ void *scr_arr_get_ref(ScrArr *a, double i) { if (a->elem == SCR_ELEM_STR) scr_str_retain((ScrStr *)p); else if (a->elem == SCR_ELEM_BYTES) scr_bytes_retain((ScrBytes *)p); else if (a->elem == SCR_ELEM_REF) p = scr_arr_ops(a)->retain(p); - else scr_arr_retain((ScrArr *)p); + else if (a->elem == SCR_ELEM_ARR) scr_arr_retain((ScrArr *)p); return p; } @@ -2395,7 +2446,7 @@ ScrArr *scr_arr_splice(ScrArr *a, double start, double deleteCount) { * otherwise the range may be mostly holes, so the capacity is a hint. */ size_t hint = scr_arr_is_dense(a) || n <= SCR_ARR_DENSE_LIMIT ? n : SCR_ARR_DENSE_LIMIT; ScrArr *out = - a->elem == SCR_ELEM_REF + scr_arr_has_ops(a) ? scr_arr_new_ops(scr_arr_ops(a), hint ? hint : 1) : scr_arr_new(a->elem, hint ? hint : 1); out->len = n; @@ -2475,7 +2526,7 @@ ScrArr *scr_arr_splice_insert(ScrArr *a, double start, double deleteCount, * is needed only by direct self-spread runtime callers. */ ScrArr *snapshot = items == a ? scr_arr_slice(a, 0, INFINITY) : NULL; if (snapshot) items = snapshot; - ScrArr *removed = a->elem == SCR_ELEM_REF + ScrArr *removed = scr_arr_has_ops(a) ? scr_arr_new_ops(scr_arr_ops(a), n) : scr_arr_new(a->elem, n); if (n) { @@ -2521,7 +2572,7 @@ void scr_arr_splice_drop(ScrArr *a, double start, double deleteCount, size_t cou size_t next_len = a->len - n + count; bool refs = scr_elem_is_ref(a->elem); if (!scr_arr_is_dense(a) || !scr_arr_dense_ok(a, next_len, count)) { - ScrArr *items = a->elem == SCR_ELEM_REF + ScrArr *items = scr_arr_has_ops(a) ? scr_arr_new_ops(scr_arr_ops(a), count) : scr_arr_new(a->elem, count); scr_arr_push_many(items, count, slots); @@ -2636,7 +2687,8 @@ double scr_arr_index_of_bool(ScrArr *a, bool v) { double scr_arr_index_of_ref(ScrArr *a, void *v) { const uint8_t *states = scr_arr_states_end(a); for (size_t i = 0; i < a->cap && i < a->len; i++) { - if (*scr_arr_state_in(states, i) == SCR_ARR_VALUE && scr_arr_ref_eq(a, a->data[i], v)) return (double)i; + if (*scr_arr_state_in(states, i) == SCR_ARR_VALUE && scr_arr_ref_eq(a, scr_arr_dense_get(a, i), v)) + return (double)i; } for (size_t i = 0; i < SCR_AX(a)->sparse_len; i++) { if (SCR_AX(a)->sparse[i].state == SCR_ARR_VALUE && scr_arr_ref_eq(a, SCR_AX(a)->sparse[i].slot, v)) return (double)SCR_AX(a)->sparse[i].index; @@ -2727,11 +2779,12 @@ ScrArr *scr_arr_slice(ScrArr *a, double start, double end) { * sparse side store may be mostly holes, so its capacity is a hint. */ size_t hint = to <= a->cap || n <= SCR_ARR_DENSE_LIMIT ? n : SCR_ARR_DENSE_LIMIT; ScrArr *out = - a->elem == SCR_ELEM_REF + scr_arr_has_ops(a) ? scr_arr_new_ops(scr_arr_ops(a), hint ? hint : 1) : scr_arr_new(a->elem, hint ? hint : 1); out->len = n; - if (to <= a->cap && to <= a->filled && a->elem != SCR_ELEM_BOOL && !scr_elem_is_ref(a->elem)) { + if (to <= a->cap && to <= a->filled && scr_arr_slot_width(a) == sizeof(uint64_t) && + !scr_elem_is_ref(a->elem)) { /* Numbers below the source's hole-free prefix: values only. */ scr_arr_copy_slots(out->data, a->data + from, n); scr_arr_set_states(scr_arr_states_end_w(out, 8) - n, SCR_ARR_VALUE, n); @@ -3024,14 +3077,14 @@ static ScrSortCompare scr_sort_compare_for(const ScrArr *a, ScrClosure *f, uint3 bool multi = order >= 2; if (order != 0 && (arity != 2 || (order != 1 && order != -1 && !scr_sort_keys_valid(order)) || - (c.numbers ? key != NULL || multi : a->elem != SCR_ELEM_REF || key == NULL))) + (c.numbers ? key != NULL || multi : !scr_arr_has_ops(a) || key == NULL))) scr_trap("scriptc: invalid keyed sort\n"); if (!c.numbers) { c.borrowing = f->fn_borrowed != NULL; if (a->elem == SCR_ELEM_STR) c.retain = scr_sort_retain_str; else if (a->elem == SCR_ELEM_ARR) c.retain = scr_sort_retain_arr; else if (a->elem == SCR_ELEM_BYTES) c.retain = scr_sort_retain_bytes; - else if (a->elem == SCR_ELEM_REF) c.retain = scr_arr_ops(a)->retain; + else if (scr_arr_has_ops(a)) c.retain = scr_arr_ops(a)->retain; else scr_trap("scriptc: invalid comparator sort element kind\n"); } return c; @@ -4619,7 +4672,7 @@ static bool scr_arr_dense_values(const ScrArr *a) { * elements (stored in 1-byte slots). The slots are borrowed and stay valid * while nothing writes the array. */ const uint64_t *scr_arr_dense_slots(const ScrArr *a, size_t *len) { - if (a->len == 0 || a->elem == SCR_ELEM_BOOL || !scr_arr_dense_values(a)) return NULL; + if (a->len == 0 || scr_arr_slot_width(a) != sizeof(uint64_t) || !scr_arr_dense_values(a)) return NULL; *len = a->len; return a->data; } @@ -4646,7 +4699,9 @@ bool scr_arr_sort_dense(ScrArr *a, ScrClosure *f, uint32_t arity, bool owned, in ScrClosure *key) { uint8_t elem = a->elem; const ScrArrOps *ops = scr_arr_ops(a); - if (a->rc == SIZE_MAX || !scr_arr_dense_values(a)) return false; + /* Compact references sort through the snapshot path, which reads and + * writes 8-byte slots. */ + if (a->rc == SIZE_MAX || elem == SCR_ELEM_CREF || !scr_arr_dense_values(a)) return false; size_t n = a->len; if (n < 2) return true; if (n > SIZE_MAX / 2 / sizeof(uint64_t)) scr_arr_oom(); @@ -4687,12 +4742,14 @@ bool scr_arr_sort_dense(ScrArr *a, ScrClosure *f, uint32_t arity, bool owned, in } else { /* Publish each new edge before releasing the value it replaces: a * release can collect cycles. The buffer still holds the slots not - * yet written. */ + * yet written. The kind, and so the slot width, never changes. */ + size_t width = scr_arr_slot_width(a); for (size_t i = 0; i < n; i++) { - bool held = *scr_arr_state_ptr(a, i) == SCR_ARR_VALUE; + uint8_t *state = scr_arr_state_in(scr_arr_states_end_w(a, width), i); + bool held = *state == SCR_ARR_VALUE; uint64_t old = held ? a->data[i] : 0; a->data[i] = src[i]; - *scr_arr_state_ptr(a, i) = SCR_ARR_VALUE; + *state = SCR_ARR_VALUE; if (held) scr_elem_release(a, old); } } @@ -4787,6 +4844,7 @@ ScrStr *scr_arr_join(ScrArr *a, ScrStr *sep) { case SCR_ELEM_ARR: case SCR_ELEM_BYTES: case SCR_ELEM_REF: + case SCR_ELEM_CREF: /* The compiler rejects join on ref-element arrays (SC1090). */ scr_trap("scriptc: internal error: join on a ref-element array\n"); } diff --git a/packages/runtime/src/scr_array.h b/packages/runtime/src/scr_array.h index 6c41b3dbd..67ff7d528 100644 --- a/packages/runtime/src/scr_array.h +++ b/packages/runtime/src/scr_array.h @@ -43,6 +43,11 @@ typedef enum { * constant descriptor per element type, stored once per array; every * array is still monomorphic (one element type, one set of entry points). */ SCR_ELEM_REF, + /* An SCR_ELEM_REF type whose elements are compact class instances in the + * program-lifetime region (SCR_ARR_OPS_COMPACT): 4-byte slots holding the + * address over 8, 0 for NULL. Compact instances are never counted or + * traced, so these arrays retain, release and trace nothing. */ + SCR_ELEM_CREF, } ScrElemKind; typedef enum { @@ -63,8 +68,27 @@ typedef struct ScrArrOps { /* Registry id (scr_arr_ops_table), 0 until scr_arr_ops_register assigns * it on the first array of the type. Descriptors are writable for it. */ uint32_t id; + /* SCR_ARR_OPS_*. On 64-bit targets the field takes the struct's former + * tail padding, so descriptors emitted without it read 0 here; on 32-bit + * targets it lies past their end and is never read (scr_arr_ops_compact). */ + uint32_t flags; } ScrArrOps; +/* The elements are compact class instances in the program-lifetime region: + * arrays of the type are SCR_ELEM_CREF (llvm/shapes.ts arrOps). */ +#define SCR_ARR_OPS_COMPACT 1u + +/* Whether `ops` describes compact class elements. Compact instances exist + * only on 64-bit targets. */ +static inline bool scr_arr_ops_compact(const ScrArrOps *ops) { +#if UINTPTR_MAX > 0xffffffffu + return (ops->flags & SCR_ARR_OPS_COMPACT) != 0; +#else + (void)ops; + return false; +#endif +} + typedef struct ScrArr { size_t rc; /* SIZE_MAX = immortal (unused for arrays; kept per convention) */ /* Length and dense capacity are at most SCR_ARR_MAX_LENGTH (2^32 - 1); @@ -84,7 +108,7 @@ typedef struct ScrArr { /* The low SCR_ARR_OPS_SHIFT bits: storage flags (SCR_ARR_INLINE_SLOTS, * SCR_ARR_HEAD_ROOM, SCR_ARR_UNSHIFTED, SCR_ARR_HAS_EXT). The rest: the * registry id of the element type's ops (scr_arr_ops), 0 for every kind - * but SCR_ELEM_REF. */ + * but SCR_ELEM_REF and SCR_ELEM_CREF. */ uint16_t spare; /* A lower bound on the hole-free prefix: filled <= len, filled <= cap, * and every dense state below it is SCR_ARR_VALUE. Element reads below @@ -93,10 +117,12 @@ typedef struct ScrArr { * undefined) or move states below it lowers it first (scr_arr_cut); * 0 is always valid. */ uint32_t filled; - /* Dense values: `cap` 8-byte slots, or `cap` bytes for SCR_ELEM_BOOL - * (scr_arr_slot_width), followed in the same allocation by one state byte - * per slot in reverse slot order (scr_arr_states_end). Sparse entries and properties keep 8-byte - * slots for every kind; a bool slot is 0 or 1 wherever it lives. Rarely + /* Dense values: `cap` 8-byte slots, `cap` bytes for SCR_ELEM_BOOL or + * `cap` 4-byte slots for SCR_ELEM_CREF (scr_arr_slot_width), followed in + * the same allocation by one state byte per slot in reverse slot order + * (scr_arr_states_end). Sparse entries and properties keep 8-byte slots + * for every kind; a bool slot is 0 or 1 wherever it lives, and a CREF + * slot's 8-byte form is the pointer (scr_arr_dense_get). Rarely * used storage (sparse indices, noncanonical numeric properties, * match-result metadata) lives out of line, and an array that has it * (SCR_ARR_HAS_EXT) keeps a pointer to it in the word before the dense @@ -190,16 +216,24 @@ static inline uint32_t scr_arr_ops_id(const ScrArrOps *ops) { /* The element ops of an array whose ops have no registry id. */ const ScrArrOps *scr_arr_ops_overflow(const ScrArr *a); -/* The element ops of `a`: NULL for every kind but SCR_ELEM_REF. */ +/* The element ops of `a`: NULL for every kind but SCR_ELEM_REF and + * SCR_ELEM_CREF. */ static inline const ScrArrOps *scr_arr_ops(const ScrArr *a) { uint32_t id = (uint32_t)a->spare >> SCR_ARR_OPS_SHIFT; if (__builtin_expect(id == SCR_ARR_OPS_OVERFLOW, 0)) return scr_arr_ops_overflow(a); return scr_arr_ops_table[id]; } +/* Bytes per dense value slot of element kind `elem`. */ +static inline size_t scr_elem_slot_width(uint8_t elem) { + if (elem == SCR_ELEM_BOOL) return 1; + if (__builtin_expect(elem == SCR_ELEM_CREF, 0)) return sizeof(uint32_t); + return sizeof(uint64_t); +} + /* Bytes per dense value slot. */ static inline size_t scr_arr_slot_width(const ScrArr *a) { - return a->elem == SCR_ELEM_BOOL ? 1 : sizeof(uint64_t); + return scr_elem_slot_width(a->elem); } /* The dense states, one byte per slot, share the values' allocation (inline @@ -233,14 +267,23 @@ static inline uint8_t *scr_arr_state_run(const ScrArr *a, size_t from, size_t co return scr_arr_states_end(a) - from - count; } +/* A CREF slot's pointer, and the slot of a pointer (NULL or a compact + * instance below 32 GiB). */ +static inline uint64_t scr_cref_decode(uint32_t ref) { return (uint64_t)ref << 3; } +static inline uint32_t scr_cref_encode(uint64_t slot) { return (uint32_t)(slot >> 3); } + /* The dense value at `index` (< cap) as an 8-byte slot, and its store. */ static inline uint64_t scr_arr_dense_get(const ScrArr *a, size_t index) { if (a->elem == SCR_ELEM_BOOL) return ((const uint8_t *)a->data)[index]; + if (__builtin_expect(a->elem == SCR_ELEM_CREF, 0)) + return scr_cref_decode(((const uint32_t *)a->data)[index]); return a->data[index]; } static inline void scr_arr_dense_put(ScrArr *a, size_t index, uint64_t slot) { if (a->elem == SCR_ELEM_BOOL) ((uint8_t *)a->data)[index] = (uint8_t)slot; + else if (__builtin_expect(a->elem == SCR_ELEM_CREF, 0)) + ((uint32_t *)a->data)[index] = scr_cref_encode(slot); else a->data[index] = slot; } diff --git a/packages/runtime/src/scr_bytes_dyn.c b/packages/runtime/src/scr_bytes_dyn.c index 280aa423c..c492d43b1 100644 --- a/packages/runtime/src/scr_bytes_dyn.c +++ b/packages/runtime/src/scr_bytes_dyn.c @@ -509,8 +509,9 @@ static bool scr_bytes_dyn_buffer(const ScrDyn *recv) { /* One entry per method family, so a program links only the families it * names: each runs its methods on a Buffer receiver and gives any other * receiver the ordinary dispatch. */ -ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, - size_t argc, const char *what) { +ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *method, + int32_t *mid_slot, ScrDyn *const *args, size_t argc, + const char *what) { if (scr_bytes_dyn_buffer(recv)) { bool read = !strncmp(method, "read", 4); const char *rest = method + (read ? 4 : 5); @@ -518,40 +519,45 @@ ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *meth if (!strcmp(rest, scr_bytes_dyn_names[at].name)) return scr_bytes_dyn_num_method(recv->v.bytes, read, at, args, argc); } } - return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); + return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); } -ScrDyn *scr_dyn_invoke_buffer_write(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, - size_t argc, const char *what) { +ScrDyn *scr_dyn_invoke_buffer_write(ScrDyn *recv, ScrDyn *callee, const char *method, + int32_t *mid_slot, ScrDyn *const *args, size_t argc, + const char *what) { if (scr_bytes_dyn_buffer(recv)) return scr_bytes_dyn_write(recv->v.bytes, args, argc); - return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); + return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); } -ScrDyn *scr_dyn_invoke_buffer_fill(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, - size_t argc, const char *what) { +ScrDyn *scr_dyn_invoke_buffer_fill(ScrDyn *recv, ScrDyn *callee, const char *method, + int32_t *mid_slot, ScrDyn *const *args, size_t argc, + const char *what) { if (scr_bytes_dyn_buffer(recv)) return scr_bytes_dyn_fill(recv, args, argc); - return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); + return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); } -ScrDyn *scr_dyn_invoke_buffer_to_string(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, - size_t argc, const char *what) { +ScrDyn *scr_dyn_invoke_buffer_to_string(ScrDyn *recv, ScrDyn *callee, const char *method, + int32_t *mid_slot, ScrDyn *const *args, size_t argc, + const char *what) { if (scr_bytes_dyn_buffer(recv)) return scr_bytes_dyn_to_string(recv->v.bytes, args, argc); - return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); + return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); } /* indexOf, lastIndexOf and includes. */ -ScrDyn *scr_dyn_invoke_buffer_search(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, - size_t argc, const char *what) { +ScrDyn *scr_dyn_invoke_buffer_search(ScrDyn *recv, ScrDyn *callee, const char *method, + int32_t *mid_slot, ScrDyn *const *args, size_t argc, + const char *what) { if (scr_bytes_dyn_buffer(recv)) { bool includes = !strcmp(method, "includes"); return scr_bytes_dyn_index_of(recv->v.bytes, args, argc, strcmp(method, "lastIndexOf") != 0, includes); } - return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); + return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); } /* toJSON, equals, compare, copy, swap16/32/64, and the refused members. */ -ScrDyn *scr_dyn_invoke_buffer(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, - size_t argc, const char *what) { +ScrDyn *scr_dyn_invoke_buffer(ScrDyn *recv, ScrDyn *callee, const char *method, + int32_t *mid_slot, ScrDyn *const *args, size_t argc, + const char *what) { if (scr_bytes_dyn_buffer(recv)) { ScrBytes *b = recv->v.bytes; if (!strcmp(method, "toJSON")) return scr_bytes_dyn_to_json(b); @@ -584,5 +590,5 @@ ScrDyn *scr_dyn_invoke_buffer(ScrDyn *recv, ScrDyn *callee, const char *method, return NULL; } } - return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); + return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); } diff --git a/packages/runtime/src/scr_child.c b/packages/runtime/src/scr_child.c index ec685656d..df7543500 100644 --- a/packages/runtime/src/scr_child.c +++ b/packages/runtime/src/scr_child.c @@ -1105,9 +1105,9 @@ char **scr_child_argv(ScrStr *cmd, ScrArr *args) { static ScrSpawnRes *scr_spawn_sync_core(ScrStr *cmd, ScrArr *args, double timeout_ms, int killsig, int in_mode, int out_mode, - int err_mode) { + int err_mode, ScrArr *env_pairs) { ScrWinSyncRes w; - scr_win_run_sync(cmd, args, NULL, in_mode, out_mode, err_mode, NULL, NULL, + scr_win_run_sync(cmd, args, NULL, in_mode, out_mode, err_mode, NULL, env_pairs, timeout_ms, &w); if (w.spawn_errname != NULL) { free(w.out.data); @@ -1136,19 +1136,26 @@ static ScrSpawnRes *scr_spawn_sync_core(ScrStr *cmd, ScrArr *args, double timeou } ScrSpawnRes *scr_spawn_sync(ScrStr *cmd, ScrArr *args) { - return scr_spawn_sync_core(cmd, args, 0, SIGTERM, 0, 0, 0); + return scr_spawn_sync_core(cmd, args, 0, SIGTERM, 0, 0, 0, NULL); } ScrSpawnRes *scr_spawn_sync_opts(ScrStr *cmd, ScrArr *args, double timeout_ms, ScrStr *killsignal, double in_mode, double out_mode, double err_mode) { + return scr_spawn_sync_env(cmd, args, timeout_ms, killsignal, in_mode, out_mode, err_mode, + NULL); +} + +ScrSpawnRes *scr_spawn_sync_env(ScrStr *cmd, ScrArr *args, double timeout_ms, + ScrStr *killsignal, double in_mode, double out_mode, + double err_mode, ScrArr *env_pairs) { int killsig = SIGTERM; if (killsignal->len > 0) { int resolved = scr_signal_from_name(killsignal); if (resolved > 0) killsig = resolved; } return scr_spawn_sync_core(cmd, args, timeout_ms, killsig, (int)in_mode, - (int)out_mode, (int)err_mode); + (int)out_mode, (int)err_mode, env_pairs); } ScrSpawnRes *scr_spawn_sync_stdio_str(ScrStr *cmd, ScrArr *args, double timeout_ms, @@ -3101,9 +3108,61 @@ static ScrSpawnRes *scr_spawn_res_new(bool has_status, double status, * documented divergence). timeout_ms > 0 sends `killsig` at the deadline * and the result carries error: ETIMEDOUT + the signal — never a throw, * Node's spawnSync shape. */ +static char **scr_exec_envp(ScrArr *pairs); +static void scr_exec_envp_free(char **envp); + +/* posix_spawnp for the child_process spawns. Without `envp` the command + * resolves on this process's PATH over `environ`. With `envp` (a + * replacement environment) it resolves the way Node's libuv runs execvp + * in the child: a name with a '/' runs as given; otherwise each entry of + * the replacement's PATH (an empty entry is the current directory), or of + * the default path when the replacement has no PATH, is tried in order, + * and a candidate whose exec fails with ENOENT, ENOTDIR or EACCES (a + * missing file, or a script whose interpreter is missing) moves on to the + * next entry. The last such failure is EACCES if any candidate was denied, + * else ENOENT. Relative entries are resolved by the child, which starts in + * `cwd`. Returns posix_spawn's error number. */ +static int scr_spawn_command(pid_t *pid, const char *file, const posix_spawn_file_actions_t *fa, + const posix_spawnattr_t *attr, char *const *argv, + char *const *envp, const char *cwd) { + (void)cwd; /* the file actions change into it before the exec */ + if (envp == NULL) return posix_spawnp(pid, file, fa, attr, argv, environ); + if (strchr(file, '/')) return posix_spawn(pid, file, fa, attr, argv, envp); + const char *path = "/usr/bin:/bin"; + for (char *const *e = envp; *e; e++) { + if (strncmp(*e, "PATH=", 5) == 0) { + path = *e + 5; + break; + } + } + size_t file_len = strlen(file); + bool denied = false; + for (const char *dir = path;;) { + const char *end = strchr(dir, ':'); + size_t dir_len = end ? (size_t)(end - dir) : strlen(dir); + const char *prefix = dir_len ? dir : "."; + size_t prefix_len = dir_len ? dir_len : 1; + char *candidate = malloc(prefix_len + 1 + file_len + 1); + if (!candidate) scr_child_oom(); + memcpy(candidate, prefix, prefix_len); + candidate[prefix_len] = '/'; + memcpy(candidate + prefix_len + 1, file, file_len + 1); + int err = posix_spawn(pid, candidate, fa, attr, argv, envp); + free(candidate); + if (err == EACCES) denied = true; + else if (err != ENOENT && err != ENOTDIR) return err; + if (!end) break; + dir = end + 1; + } + return denied ? EACCES : ENOENT; +} + +/* env_pairs: NULL inherits the parent's environment; else [k,v,...] + * replaces it, as Node's `env` option does, and the command is looked up + * on the replacement's PATH (scr_spawn_command). */ static ScrSpawnRes *scr_spawn_sync_core(ScrStr *cmd, ScrArr *args, double timeout_ms, int killsig, int in_mode, int out_mode, - int err_mode) { + int err_mode, ScrArr *env_pairs) { if (out_mode == 2 || err_mode == 2) { /* Inherited output fds: flush the parent's buffers first so earlier * logs precede the child's writes (Node's practical ordering). */ @@ -3138,10 +3197,12 @@ static ScrSpawnRes *scr_spawn_sync_core(ScrStr *cmd, ScrArr *args, double timeou } char **argv = scr_child_argv(cmd, args); + char **envp = env_pairs != NULL ? scr_exec_envp(env_pairs) : NULL; pid_t pid = -1; - int spawn_err = posix_spawnp(&pid, cmd->data, &fa, NULL, argv, environ); + int spawn_err = scr_spawn_command(&pid, cmd->data, &fa, NULL, argv, envp, NULL); posix_spawn_file_actions_destroy(&fa); free(argv); + scr_exec_envp_free(envp); if (cap_out) close(outfd[1]); if (cap_err) close(errfd[1]); @@ -3242,7 +3303,7 @@ static ScrSpawnRes *scr_spawn_sync_core(ScrStr *cmd, ScrArr *args, double timeou } ScrSpawnRes *scr_spawn_sync(ScrStr *cmd, ScrArr *args) { - return scr_spawn_sync_core(cmd, args, 0, SIGTERM, 0, 0, 0); + return scr_spawn_sync_core(cmd, args, 0, SIGTERM, 0, 0, 0, NULL); } /* The options entry (cp.spawnSyncOpts): killSignal as a NAME ("" = the @@ -3252,13 +3313,20 @@ ScrSpawnRes *scr_spawn_sync(ScrStr *cmd, ScrArr *args) { ScrSpawnRes *scr_spawn_sync_opts(ScrStr *cmd, ScrArr *args, double timeout_ms, ScrStr *killsignal, double in_mode, double out_mode, double err_mode) { + return scr_spawn_sync_env(cmd, args, timeout_ms, killsignal, in_mode, out_mode, err_mode, + NULL); +} + +ScrSpawnRes *scr_spawn_sync_env(ScrStr *cmd, ScrArr *args, double timeout_ms, + ScrStr *killsignal, double in_mode, double out_mode, + double err_mode, ScrArr *env_pairs) { int killsig = SIGTERM; if (killsignal->len > 0) { int resolved = scr_signal_from_name(killsignal); if (resolved > 0) killsig = resolved; } return scr_spawn_sync_core(cmd, args, timeout_ms, killsig, (int)in_mode, - (int)out_mode, (int)err_mode); + (int)out_mode, (int)err_mode, env_pairs); } /* The runtime-string stdio entry (cp.spawnSyncStdioStr): the compiler @@ -3478,8 +3546,8 @@ ScrStr *scr_exec_sync_core(ScrStr *cmd, ScrArr *args, const ScrExecOpts *o) { char **argv = scr_child_argv(cmd, args); char **envp = o->env_pairs != NULL ? scr_exec_envp(o->env_pairs) : NULL; pid_t pid = -1; - int spawn_err = posix_spawnp(&pid, cmd->data, &fa, NULL, argv, - envp != NULL ? envp : environ); + int spawn_err = scr_spawn_command(&pid, cmd->data, &fa, NULL, argv, envp, + o->cwd != NULL ? o->cwd->data : NULL); posix_spawn_file_actions_destroy(&fa); free(argv); scr_exec_envp_free(envp); @@ -5037,8 +5105,8 @@ ScrChild *scr_spawn_opts(ScrStr *cmd, ScrArr *args, double in_mode, char **envp = has_env && env_pairs != NULL ? scr_exec_envp(env_pairs) : NULL; pid_t pid = -1; const char *file = shell ? "/bin/sh" : cmd->data; - int spawn_err = posix_spawnp(&pid, file, &fa, &at, argv, - envp != NULL ? envp : environ); + int spawn_err = scr_spawn_command(&pid, file, &fa, &at, argv, envp, + cwd != NULL && cwd->len > 0 ? cwd->data : NULL); posix_spawnattr_destroy(&at); posix_spawn_file_actions_destroy(&fa); if (!shell) free(argv); diff --git a/packages/runtime/src/scr_child.h b/packages/runtime/src/scr_child.h index e899130d8..a5853ad59 100644 --- a/packages/runtime/src/scr_child.h +++ b/packages/runtime/src/scr_child.h @@ -23,6 +23,11 @@ ScrSpawnRes *scr_spawn_sync(ScrStr *cmd, ScrArr *args); /* +1, never throws */ ScrSpawnRes *scr_spawn_sync_opts(ScrStr *cmd, ScrArr *args, double timeout_ms, ScrStr *killsignal, double in_mode, double out_mode, double err_mode); +/* scr_spawn_sync_opts with an `env` option: [k,v,...] pairs replace the + * child's environment (NULL inherits the parent's). */ +ScrSpawnRes *scr_spawn_sync_env(ScrStr *cmd, ScrArr *args, double timeout_ms, + ScrStr *killsignal, double in_mode, double out_mode, + double err_mode, ScrArr *env_pairs); /* The runtime-string stdio entry: the compiler proved the value is * "pipe" | "ignore" | "inherit" by type; the mode mapping happens here. */ ScrSpawnRes *scr_spawn_sync_stdio_str(ScrStr *cmd, ScrArr *args, double timeout_ms, diff --git a/packages/runtime/src/scr_cipher.c b/packages/runtime/src/scr_cipher.c index c8a10a731..00d229bad 100644 --- a/packages/runtime/src/scr_cipher.c +++ b/packages/runtime/src/scr_cipher.c @@ -20,16 +20,30 @@ #include "mbedtls/aes.h" #include "mbedtls/chacha20.h" +#include "mbedtls/des.h" #include "mbedtls/gcm.h" #include "mbedtls/poly1305.h" #include "mbedtls/platform_util.h" -typedef enum { SC_CBC, SC_CTR, SC_GCM, SC_CHACHA } ScCipherMode; +/* SC_CHACHA is chacha20-poly1305; SC_CHACHA20 the bare stream cipher. */ +typedef enum { + SC_CBC, + SC_CTR, + SC_GCM, + SC_CHACHA, + SC_ECB, + SC_CFB, + SC_CFB8, + SC_CFB1, + SC_OFB, + SC_CHACHA20, +} ScCipherMode; typedef struct { const char *name; ScCipherMode mode; unsigned key_len; + bool des; /* triple DES (two- or three-key) rather than AES */ } ScCipherSpec; /* Node's names for the implemented ciphers, including OpenSSL's aliases. @@ -41,6 +55,35 @@ static const ScCipherSpec sc_specs[] = { {"aes-128-gcm", SC_GCM, 16}, {"aes-192-gcm", SC_GCM, 24}, {"aes-256-gcm", SC_GCM, 32}, {"id-aes128-gcm", SC_GCM, 16}, {"id-aes192-gcm", SC_GCM, 24}, {"id-aes256-gcm", SC_GCM, 32}, {"chacha20-poly1305", SC_CHACHA, 32}, + {"aes-128-ecb", SC_ECB, 16}, {"aes-192-ecb", SC_ECB, 24}, {"aes-256-ecb", SC_ECB, 32}, + {"aes-128-cfb", SC_CFB, 16}, {"aes-192-cfb", SC_CFB, 24}, {"aes-256-cfb", SC_CFB, 32}, + {"aes-128-cfb8", SC_CFB8, 16}, {"aes-192-cfb8", SC_CFB8, 24}, {"aes-256-cfb8", SC_CFB8, 32}, + {"aes-128-cfb1", SC_CFB1, 16}, {"aes-192-cfb1", SC_CFB1, 24}, {"aes-256-cfb1", SC_CFB1, 32}, + {"aes-128-ofb", SC_OFB, 16}, {"aes-192-ofb", SC_OFB, 24}, {"aes-256-ofb", SC_OFB, 32}, + {"des-ede3-cbc", SC_CBC, 24, true}, {"des3", SC_CBC, 24, true}, + {"des-ede3", SC_ECB, 24, true}, {"des-ede3-ecb", SC_ECB, 24, true}, + {"des-ede3-cfb", SC_CFB, 24, true}, {"des-ede3-cfb8", SC_CFB8, 24, true}, + {"des-ede3-cfb1", SC_CFB1, 24, true}, {"des-ede3-ofb", SC_OFB, 24, true}, + {"des-ede-cbc", SC_CBC, 16, true}, {"des-ede", SC_ECB, 16, true}, + {"des-ede-ecb", SC_ECB, 16, true}, {"des-ede-cfb", SC_CFB, 16, true}, + {"des-ede-ofb", SC_OFB, 16, true}, + {"chacha20", SC_CHACHA20, 32}, +}; + +/* Ciphers OpenSSL 3 keeps in its legacy provider, which Node 24 does not + * load: crypto.getCipherInfo() knows them, and createCipheriv checks their + * IV length, then fails with OpenSSL's "unsupported" error. */ +static const struct { + const char *name; + unsigned iv_len; +} sc_legacy[] = { + {"des", 8}, {"des-cbc", 8}, {"des-ecb", 0}, {"des-cfb", 8}, {"des-cfb1", 8}, {"des-cfb8", 8}, + {"des-ofb", 8}, {"desx", 8}, {"desx-cbc", 8}, {"bf", 8}, {"bf-cbc", 8}, {"bf-ecb", 0}, + {"bf-cfb", 8}, {"bf-ofb", 8}, {"blowfish", 8}, {"cast", 8}, {"cast-cbc", 8}, {"cast5-cbc", 8}, + {"cast5-ecb", 0}, {"cast5-cfb", 8}, {"cast5-ofb", 8}, {"rc2", 8}, {"rc2-cbc", 8}, {"rc2-ecb", 0}, + {"rc2-cfb", 8}, {"rc2-ofb", 8}, {"rc2-40-cbc", 8}, {"rc2-64-cbc", 8}, {"rc4", 0}, {"rc4-40", 0}, + {"rc4-hmac-md5", 0}, {"seed", 16}, {"seed-cbc", 16}, {"seed-ecb", 0}, {"seed-cfb", 16}, + {"seed-ofb", 16}, {"idea", 8}, {"idea-cbc", 8}, {"idea-ecb", 0}, {"idea-cfb", 8}, {"idea-ofb", 8}, }; /* crypto.getCiphers() on Node 24.22 (OpenSSL 3.5). Names in this list that @@ -88,15 +131,21 @@ typedef struct ScCipher { ScCipherMode mode; bool decrypt; bool active; /* false after final() or a failed backend call */ - bool pad; /* CBC automatic padding */ + bool pad; /* CBC and ECB automatic padding */ + bool des; /* triple DES rather than AES */ + unsigned bs; /* the block size: 16 for AES, 8 for triple DES */ bool data_started; /* a non-empty update() ran, so non-empty AAD is refused */ int tag_len; /* SC_NO_TAG until known */ int tag_state; unsigned char tag[16]; - unsigned char block[16]; /* CBC: chaining value; CTR: counter block */ - unsigned char buf[16]; /* CBC: buffered input; CTR: keystream block */ - size_t bufsz; /* CBC: buffered bytes; CTR: keystream offset */ + unsigned char block[16]; /* CBC: chaining value; CTR: counter block; CFB, OFB: + shift register; chacha20: the IV */ + unsigned char buf[16]; /* CBC, ECB: buffered input; CTR: keystream block */ + size_t bufsz; /* CBC, ECB: buffered bytes; CTR, CFB, OFB: keystream + offset */ + uint64_t chacha_left; /* chacha20: bytes before the low counter word wraps */ mbedtls_aes_context aes; + mbedtls_des3_context des3; mbedtls_gcm_context gcm; mbedtls_chacha20_context chacha; mbedtls_poly1305_context poly; @@ -127,6 +176,7 @@ static void sc_trace(void *h, ScrTraceVisit visit, void *ctx) { static void sc_gcfree(void *h) { ScCipher *c = h; mbedtls_aes_free(&c->aes); + mbedtls_des3_free(&c->des3); mbedtls_gcm_free(&c->gcm); mbedtls_chacha20_free(&c->chacha); mbedtls_poly1305_free(&c->poly); @@ -184,18 +234,21 @@ static void sc_invalid_tag(unsigned long length) { /* An OpenSSL provider error as Node reports it: the formatted message plus * own opensslErrorStack, library, reason and code properties, in Node's * key order. */ -static void sc_throw_openssl(const char *packed, const char *reason, const char *code) { +/* OpenSSL's error with its library, reason and error stack; Node's + * code. */ +static void sc_throw_openssl_in(const char *packed, const char *library, const char *reason, + const char *code) { char message[128]; - snprintf(message, sizeof message, "error:%s:Provider routines::%s", packed, reason); + snprintf(message, sizeof message, "error:%s:%s::%s", packed, library, reason); ScrStr *text = scr_str_new(message, strlen(message)); ScrError *e = scr_error_new(SCR_ERR_ERROR, text); ScrDyn *stack = scr_dyn_new_arr(); scr_dyn_arr_push(stack, scr_dyn_new_str(text)); scr_str_release(text); scr_error_set_prop(e, "opensslErrorStack", 17, stack); - ScrStr *library = scr_str_new("Provider routines", 17); - scr_error_set_prop(e, "library", 7, scr_dyn_new_str(library)); - scr_str_release(library); + ScrStr *library_text = scr_str_new(library, strlen(library)); + scr_error_set_prop(e, "library", 7, scr_dyn_new_str(library_text)); + scr_str_release(library_text); ScrStr *reason_text = scr_str_new(reason, strlen(reason)); scr_error_set_prop(e, "reason", 6, scr_dyn_new_str(reason_text)); scr_str_release(reason_text); @@ -206,6 +259,10 @@ static void sc_throw_openssl(const char *packed, const char *reason, const char scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg()); } +static void sc_throw_openssl(const char *packed, const char *reason, const char *code) { + sc_throw_openssl_in(packed, "Provider routines", reason, code); +} + /* ERR_INVALID_ARG_VALUE's property flavor: "The property 'options.x' is * invalid. Received ". */ static void sc_invalid_property(const char *name, const ScrDyn *value) { @@ -440,6 +497,27 @@ static const ScCipherSpec *sc_lookup(const ScrStr *name) { return NULL; } +/* The legacy-provider IV length of `name`, or -1 for other names. */ +static int sc_legacy_iv(const ScrStr *name) { + for (size_t i = 0; i < sizeof sc_legacy / sizeof sc_legacy[0]; i++) + if (sc_ascii_eq_ci(name->data, name->len, sc_legacy[i].name)) return (int)sc_legacy[i].iv_len; + return -1; +} + +/* The IV length a mode takes; SC_GCM takes any nonzero length. */ +static size_t sc_iv_length(ScCipherMode mode, bool des) { + switch (mode) { + case SC_ECB: + return 0; + case SC_CHACHA: + return 12; + case SC_CHACHA20: + return 16; + default: + return des ? 8 : 16; + } +} + static bool sc_known_name(const ScrStr *name) { for (size_t i = 0; i < sizeof sc_known / sizeof sc_known[0]; i++) if (sc_ascii_eq_ci(name->data, name->len, sc_known[i])) return true; @@ -451,9 +529,10 @@ static void sc_unsupported_cipher(const ScrStr *name) { scr_jb_init(&b); scr_jb_puts(&b, "The cipher '"); scr_jb_put_str(&b, name); - scr_jb_puts(&b, "' has no native implementation; the supported ciphers are aes-128-cbc, aes-192-cbc, " - "aes-256-cbc, aes-128-ctr, aes-192-ctr, aes-256-ctr, aes-128-gcm, aes-192-gcm, " - "aes-256-gcm and chacha20-poly1305"); + scr_jb_puts(&b, "' has no native implementation; the supported ciphers are aes-128, aes-192 and " + "aes-256 in cbc, ctr, gcm, ecb, cfb, cfb8, cfb1 and ofb modes, des-ede3 in cbc, ecb, " + "cfb, cfb8, cfb1 and ofb modes, des-ede in cbc, ecb, cfb and ofb modes, chacha20 and " + "chacha20-poly1305"); ScrStr *message = scr_jb_finish(&b); scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "SC2020"); scr_str_release(message); @@ -522,14 +601,24 @@ static ScrDyn *sc_cipher_new(bool decrypt, ScrDyn *cipher, ScrDyn *key, ScrDyn * goto done; } const ScCipherSpec *spec = sc_lookup(cipher->v.str); + size_t iv_len = iv_bytes ? iv_bytes->len : 0; if (!spec) { - if (sc_known_name(cipher->v.str)) sc_unsupported_cipher(cipher->v.str); - else sc_throw(SCR_ERR_ERROR, "ERR_CRYPTO_UNKNOWN_CIPHER", "Unknown cipher"); + int legacy_iv = sc_legacy_iv(cipher->v.str); + if (legacy_iv >= 0) { + if (iv_len != (size_t)legacy_iv || (legacy_iv > 0 && !iv_bytes)) + sc_throw(SCR_ERR_TYPE, "ERR_CRYPTO_INVALID_IV", "Invalid initialization vector"); + else + sc_throw_openssl_in("0308010C", "digital envelope routines", "unsupported", + "ERR_OSSL_EVP_UNSUPPORTED"); + } else if (sc_known_name(cipher->v.str)) { + sc_unsupported_cipher(cipher->v.str); + } else { + sc_throw(SCR_ERR_ERROR, "ERR_CRYPTO_UNKNOWN_CIPHER", "Unknown cipher"); + } goto done; } - size_t iv_len = iv_bytes ? iv_bytes->len : 0; - bool aead = spec->mode == SC_GCM || spec->mode == SC_CHACHA; - if (iv_len == 0 || (!aead && iv_len != 16) || (spec->mode == SC_CHACHA && iv_len != 12)) { + size_t expected_iv = sc_iv_length(spec->mode, spec->des); + if (spec->mode == SC_GCM ? iv_len == 0 : iv_len != expected_iv || (expected_iv > 0 && !iv_bytes)) { sc_throw(SCR_ERR_TYPE, "ERR_CRYPTO_INVALID_IV", "Invalid initialization vector"); goto done; } @@ -570,8 +659,11 @@ static ScrDyn *sc_cipher_new(bool decrypt, ScrDyn *cipher, ScrDyn *key, ScrDyn * c->decrypt = decrypt; c->active = true; c->pad = true; + c->des = spec->des; + c->bs = spec->des ? 8 : 16; c->tag_len = tag_len; mbedtls_aes_init(&c->aes); + mbedtls_des3_init(&c->des3); mbedtls_gcm_init(&c->gcm); mbedtls_chacha20_init(&c->chacha); mbedtls_poly1305_init(&c->poly); @@ -579,9 +671,39 @@ static ScrDyn *sc_cipher_new(bool decrypt, ScrDyn *cipher, ScrDyn *key, ScrDyn * unsigned bits = spec->key_len * 8; switch (spec->mode) { case SC_CBC: - rc = decrypt ? mbedtls_aes_setkey_dec(&c->aes, key_bytes->data, bits) - : mbedtls_aes_setkey_enc(&c->aes, key_bytes->data, bits); + case SC_ECB: + case SC_CFB: + case SC_CFB8: + case SC_CFB1: + case SC_OFB: { + /* CBC and ECB decrypt with the inverse cipher; the feedback modes run + * the forward cipher both ways. */ + bool inverse = decrypt && (spec->mode == SC_CBC || spec->mode == SC_ECB); + if (spec->des && spec->key_len == 24) + rc = inverse ? mbedtls_des3_set3key_dec(&c->des3, key_bytes->data) + : mbedtls_des3_set3key_enc(&c->des3, key_bytes->data); + else if (spec->des) + rc = inverse ? mbedtls_des3_set2key_dec(&c->des3, key_bytes->data) + : mbedtls_des3_set2key_enc(&c->des3, key_bytes->data); + else + rc = inverse ? mbedtls_aes_setkey_dec(&c->aes, key_bytes->data, bits) + : mbedtls_aes_setkey_enc(&c->aes, key_bytes->data, bits); + if (expected_iv) memcpy(c->block, iv_bytes->data, expected_iv); + break; + } + case SC_CHACHA20: + /* OpenSSL's IV is a 64-bit little-endian block counter and an 8-byte + * nonce; mbedTLS takes a 32-bit counter and a 12-byte nonce, so the + * stream restarts each time the low counter word wraps. */ memcpy(c->block, iv_bytes->data, 16); + rc = mbedtls_chacha20_setkey(&c->chacha, key_bytes->data); + if (rc == 0) { + uint32_t low = (uint32_t)c->block[0] | (uint32_t)c->block[1] << 8 | + (uint32_t)c->block[2] << 16 | (uint32_t)c->block[3] << 24; + c->chacha_left = ((uint64_t)1 << 32) - low; + c->chacha_left *= 64; + rc = mbedtls_chacha20_starts(&c->chacha, c->block + 4, low); + } break; case SC_CTR: rc = mbedtls_aes_setkey_enc(&c->aes, key_bytes->data, bits); @@ -635,39 +757,123 @@ done: /* ── the cipher transforms ────────────────────────────────────────────── */ -/* Raw CBC over whole blocks. */ +/* Raw CBC or ECB over whole blocks. */ static bool sc_cbc_blocks(ScCipher *c, const unsigned char *in, size_t len, unsigned char *out) { + if (c->mode == SC_ECB) { + for (size_t off = 0; off < len; off += c->bs) { + int rc = c->des ? mbedtls_des3_crypt_ecb(&c->des3, in + off, out + off) + : mbedtls_aes_crypt_ecb(&c->aes, c->decrypt ? MBEDTLS_AES_DECRYPT : MBEDTLS_AES_ENCRYPT, + in + off, out + off); + if (rc != 0) return false; + } + return true; + } + if (c->des) + return mbedtls_des3_crypt_cbc(&c->des3, c->decrypt ? MBEDTLS_DES_DECRYPT : MBEDTLS_DES_ENCRYPT, len, c->block, + in, out) == 0; return mbedtls_aes_crypt_cbc(&c->aes, c->decrypt ? MBEDTLS_AES_DECRYPT : MBEDTLS_AES_ENCRYPT, len, c->block, in, out) == 0; } -/* OpenSSL's ossl_cipher_generic_block_update for CBC: complete blocks - * are processed, the remainder is buffered, and a decryption that ends on - * a block boundary holds the last block back for padding removal. Returns - * the output length, or -1 when the backend fails; out has room for - * len + 16 bytes. */ +/* One forward block of the feedback modes' cipher. */ +static bool sc_forward(ScCipher *c, const unsigned char *in, unsigned char *out) { + return (c->des ? mbedtls_des3_crypt_ecb(&c->des3, in, out) + : mbedtls_aes_crypt_ecb(&c->aes, MBEDTLS_AES_ENCRYPT, in, out)) == 0; +} + +/* CFB (full-block feedback), CFB8, CFB1 and OFB, as OpenSSL's + * CRYPTO_cfb128_encrypt, CRYPTO_cfb128_8_encrypt, CRYPTO_cfb128_1_encrypt + * and CRYPTO_ofb128_encrypt: CFB and OFB keep their keystream offset across + * updates; CFB1 feeds back one bit at a time, most significant first. */ +static bool sc_feedback(ScCipher *c, const unsigned char *in, size_t len, unsigned char *out) { + unsigned char stream[16]; + size_t bs = c->bs; + for (size_t i = 0; i < len; i++) { + switch (c->mode) { + case SC_CFB: + case SC_OFB: + if (c->bufsz == 0 && !sc_forward(c, c->block, c->block)) return false; + out[i] = (unsigned char)(in[i] ^ c->block[c->bufsz]); + if (c->mode == SC_CFB) c->block[c->bufsz] = c->decrypt ? in[i] : out[i]; + c->bufsz = (c->bufsz + 1) % bs; + break; + case SC_CFB8: { + if (!sc_forward(c, c->block, stream)) return false; + unsigned char byte = (unsigned char)(in[i] ^ stream[0]); + memmove(c->block, c->block + 1, bs - 1); + c->block[bs - 1] = c->decrypt ? in[i] : byte; + out[i] = byte; + break; + } + default: { + unsigned char result = 0; + for (int bit = 7; bit >= 0; bit--) { + if (!sc_forward(c, c->block, stream)) return false; + unsigned in_bit = (in[i] >> bit) & 1u; + unsigned out_bit = in_bit ^ (stream[0] >> 7); + unsigned feed = c->decrypt ? in_bit : out_bit; + for (size_t k = 0; k + 1 < bs; k++) + c->block[k] = (unsigned char)(c->block[k] << 1 | c->block[k + 1] >> 7); + c->block[bs - 1] = (unsigned char)(c->block[bs - 1] << 1 | feed); + result |= (unsigned char)(out_bit << bit); + } + out[i] = result; + break; + } + } + } + mbedtls_platform_zeroize(stream, sizeof stream); + return true; +} + +/* chacha20 over len bytes: when the 64-bit counter's low word wraps, the + * high word steps (and wraps itself, as OpenSSL's does) and the stream + * restarts from block 0 of the next word. */ +static bool sc_chacha20_update(ScCipher *c, const unsigned char *in, size_t len, unsigned char *out) { + while (len > 0) { + size_t n = len < c->chacha_left ? len : (size_t)c->chacha_left; + if (mbedtls_chacha20_update(&c->chacha, n, in, out) != 0) return false; + in += n; + out += n; + len -= n; + c->chacha_left -= n; + if (c->chacha_left == 0) { + for (int k = 4; k < 8 && ++c->block[k] == 0; k++) { + } + memset(c->block, 0, 4); + c->chacha_left = (uint64_t)1 << 38; + if (mbedtls_chacha20_starts(&c->chacha, c->block + 4, 0) != 0) return false; + } + } + return true; +} + +/* OpenSSL's ossl_cipher_generic_block_update for CBC and ECB: complete + * blocks are processed, the remainder is buffered, and a decryption that + * ends on a block boundary holds the last block back for padding removal. + * Returns the output length, or -1 when the backend fails; out has room + * for len + 16 bytes. */ static long sc_cbc_update(ScCipher *c, const unsigned char *in, size_t inl, unsigned char *out) { + size_t bs = c->bs; size_t outl = 0; size_t nextblocks; if (c->bufsz != 0) { - size_t remain = 16 - c->bufsz; + size_t remain = bs - c->bufsz; if (inl < remain) remain = inl; memcpy(c->buf + c->bufsz, in, remain); in += remain; inl -= remain; c->bufsz += remain; - nextblocks = inl & ~(size_t)15; - } else { - nextblocks = inl & ~(size_t)15; } - if (c->bufsz == 16 && (!c->decrypt || inl > 0 || !c->pad)) { - if (!sc_cbc_blocks(c, c->buf, 16, out)) return -1; + nextblocks = inl - inl % bs; + if (c->bufsz == bs && (!c->decrypt || inl > 0 || !c->pad)) { + if (!sc_cbc_blocks(c, c->buf, bs, out)) return -1; c->bufsz = 0; - outl = 16; - out += 16; + outl = bs; + out += bs; } if (nextblocks > 0) { - if (c->decrypt && c->pad && nextblocks == inl) nextblocks -= 16; + if (c->decrypt && c->pad && nextblocks == inl) nextblocks -= bs; if (nextblocks > 0) { if (!sc_cbc_blocks(c, in, nextblocks, out)) return -1; outl += nextblocks; @@ -682,33 +888,35 @@ static long sc_cbc_update(ScCipher *c, const unsigned char *in, size_t inl, unsi return (long)outl; } -/* ossl_cipher_generic_block_final for CBC. Returns the output length, -1 - * after throwing OpenSSL's error, or -2 when the backend fails. */ +/* ossl_cipher_generic_block_final for CBC and ECB. Returns the output + * length, -1 after throwing OpenSSL's error, or -2 when the backend + * fails. */ static long sc_cbc_final(ScCipher *c, unsigned char out[16]) { + size_t bs = c->bs; if (!c->decrypt) { if (c->pad) { - unsigned char fill = (unsigned char)(16 - c->bufsz); - for (size_t i = c->bufsz; i < 16; i++) c->buf[i] = fill; + unsigned char fill = (unsigned char)(bs - c->bufsz); + for (size_t i = c->bufsz; i < bs; i++) c->buf[i] = fill; } else if (c->bufsz == 0) { return 0; - } else if (c->bufsz != 16) { + } else if (c->bufsz != bs) { sc_throw_openssl("1C80006B", "wrong final block length", "ERR_OSSL_WRONG_FINAL_BLOCK_LENGTH"); return -1; } - if (!sc_cbc_blocks(c, c->buf, 16, out)) return -2; + if (!sc_cbc_blocks(c, c->buf, bs, out)) return -2; c->bufsz = 0; - return 16; + return (long)bs; } - if (c->bufsz != 16) { + if (c->bufsz != bs) { if (c->bufsz == 0 && !c->pad) return 0; sc_throw_openssl("1C80006B", "wrong final block length", "ERR_OSSL_WRONG_FINAL_BLOCK_LENGTH"); return -1; } - if (!sc_cbc_blocks(c, c->buf, 16, c->buf)) return -2; - size_t len = 16; + if (!sc_cbc_blocks(c, c->buf, bs, c->buf)) return -2; + size_t len = bs; if (c->pad) { - unsigned char fill = c->buf[15]; - bool bad = fill == 0 || fill > 16; + unsigned char fill = c->buf[bs - 1]; + bool bad = fill == 0 || fill > bs; for (size_t i = 0; !bad && i < fill; i++) bad = c->buf[--len] != fill; if (bad) { sc_throw_openssl("1C800064", "bad decrypt", "ERR_OSSL_BAD_DECRYPT"); @@ -769,7 +977,8 @@ static ScrBytes *sc_transform(ScCipher *c, const unsigned char *in, size_t len) size_t produced = 0; bool ok = true; switch (c->mode) { - case SC_CBC: { + case SC_CBC: + case SC_ECB: { long n = sc_cbc_update(c, in, len, out->data); ok = n >= 0; produced = ok ? (size_t)n : 0; @@ -786,6 +995,17 @@ static ScrBytes *sc_transform(ScCipher *c, const unsigned char *in, size_t len) ok = len == 0 || sc_chacha_update(c, in, len, out->data); produced = len; break; + case SC_CFB: + case SC_CFB8: + case SC_CFB1: + case SC_OFB: + ok = sc_feedback(c, in, len, out->data); + produced = len; + break; + case SC_CHACHA20: + ok = len == 0 || sc_chacha20_update(c, in, len, out->data); + produced = len; + break; } if (!ok) { mbedtls_platform_zeroize(out->data, room); @@ -926,7 +1146,8 @@ static ScrBytes *sc_final_bytes(ScCipher *c) { out->len = 0; bool ok = true, backend = true; switch (c->mode) { - case SC_CBC: { + case SC_CBC: + case SC_ECB: { long produced = sc_cbc_final(c, out->data); if (produced == -1) { scr_bytes_release(out); @@ -937,6 +1158,11 @@ static ScrBytes *sc_final_bytes(ScCipher *c) { break; } case SC_CTR: + case SC_CFB: + case SC_CFB8: + case SC_CFB1: + case SC_OFB: + case SC_CHACHA20: break; case SC_GCM: { unsigned char full[16]; diff --git a/packages/runtime/src/scr_console.c b/packages/runtime/src/scr_console.c index a3ac43cc0..9bd5abf8c 100644 --- a/packages/runtime/src/scr_console.c +++ b/packages/runtime/src/scr_console.c @@ -148,7 +148,7 @@ static void scr_enable_utf8_console_output(void) { } #endif -void scr_runtime_abi_v27(void) {} +void scr_runtime_abi_v28(void) {} void scr_init(void) { #ifdef SCR_WORKERS diff --git a/packages/runtime/src/scr_copying.c b/packages/runtime/src/scr_copying.c index df4da1738..ee0bb055a 100644 --- a/packages/runtime/src/scr_copying.c +++ b/packages/runtime/src/scr_copying.c @@ -22,7 +22,7 @@ static bool copying_elem_is_ref(ScrElemKind kind) { } static ScrArr *copying_arr_new_like(const ScrArr *a, size_t cap) { - ScrArr *out = a->elem == SCR_ELEM_REF + ScrArr *out = a->elem == SCR_ELEM_REF || a->elem == SCR_ELEM_CREF ? scr_arr_new_ops(scr_arr_ops(a), cap ? cap : 1) : scr_arr_new(a->elem, cap ? cap : 1); out->len = cap; diff --git a/packages/runtime/src/scr_core.h b/packages/runtime/src/scr_core.h index fea020d16..35332d0ca 100644 --- a/packages/runtime/src/scr_core.h +++ b/packages/runtime/src/scr_core.h @@ -81,7 +81,7 @@ void scr_init(void); /* Program objects emitted by the bundled LLVM helper reference this symbol. * Its versioned spelling makes a mismatched manual runtime link fail before * the program can start. */ -void scr_runtime_abi_v27(void); +void scr_runtime_abi_v28(void); void scr_runtime_workers_v8(void); /* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ────── diff --git a/packages/runtime/src/scr_crypto.h b/packages/runtime/src/scr_crypto.h index 546e3d75b..0e6c6e07f 100644 --- a/packages/runtime/src/scr_crypto.h +++ b/packages/runtime/src/scr_crypto.h @@ -108,6 +108,14 @@ ScrDyn *scr_crypto_ecdh_convert_key(ScrDyn *key, ScrDyn *curve, ScrDyn *in_enc, * Node's error pending. */ ScrDyn *scr_crypto_dh_new(ScrDyn *size_or_key, ScrDyn *key_encoding, ScrDyn *generator, ScrDyn *generator_encoding); ScrDyn *scr_crypto_dh_group_new(ScrDyn *name); +/* crypto.checkPrimeSync, checkPrime, generatePrimeSync and generatePrime + * (scr_dh.c): checked-dynamic arguments, borrowed; the sync forms answer + * +1 or NULL with the error pending. The callbacks receive undefined as + * the error, as Node passes it, or null when `null_error`. */ +ScrDyn *scr_crypto_check_prime_sync(ScrDyn *candidate, ScrDyn *options); +void scr_crypto_check_prime_async(ScrDyn *candidate, ScrDyn *options, ScrDyn *callback, bool null_error); +ScrDyn *scr_crypto_generate_prime_sync(ScrDyn *size, ScrDyn *options); +void scr_crypto_generate_prime_async(ScrDyn *size, ScrDyn *options, ScrDyn *callback, bool null_error); ScrBytes *scr_crypto_pbkdf2(ScrBytes *password, ScrBytes *salt, double iterations, double keylen, ScrStr *digest); ScrBytes *scr_crypto_pbkdf2_job(ScrBytes *password, ScrBytes *salt, double iterations, diff --git a/packages/runtime/src/scr_cycle.c b/packages/runtime/src/scr_cycle.c index 5e5029e14..844c47001 100644 --- a/packages/runtime/src/scr_cycle.c +++ b/packages/runtime/src/scr_cycle.c @@ -529,7 +529,8 @@ static SCR_TL size_t scr_cyc_live_after[SCR_CYC_NGENS]; * large program, say) those walks dominate collection time. The cap trades * that walking for memory only when passes find nothing: while they find * garbage, the yield keeps the fraction narrow. SCR_CYCLE_GROWTH_CAP lowers - * the cap for memory-sensitive programs: a power of two from 1 to 16, with + * the cap for memory-sensitive programs, as scr_cyc_set_growth_cap does for + * a program that sets its own: a power of two from 1 to 16, with * other positive values rounded down to a power of two and larger ones read * as 16. At 1 the heap may at most double past its size after the last pass * at a level before the next. */ @@ -539,20 +540,34 @@ static SCR_TL unsigned char scr_cyc_growth_shift[SCR_CYC_NGENS]; static SCR_TL unsigned char scr_cyc_growth_streak[SCR_CYC_NGENS]; #endif +/* The growth cap a program sets for itself (scr_cyc_set_growth_cap), 0 for + * the default; SCR_CYCLE_GROWTH_CAP takes precedence over it. */ +static long scr_cyc_growth_cap_set = 0; +static SCR_TL unsigned char scr_cyc_growth_max_shift_cached = 0; + /* Shift of the configured growth cap: 2 + log2(cap), since the base * fraction is 1/4. */ static unsigned scr_cyc_growth_max_shift(void) { - static SCR_TL unsigned char cached = 0; - if (cached == 0) { + if (scr_cyc_growth_max_shift_cached == 0) { const char *env = scr_getenv("SCR_CYCLE_GROWTH_CAP"); long v = env ? strtol(env, NULL, 10) : 0; + if (v <= 0) v = scr_cyc_growth_cap_set; unsigned shift = SCR_CYC_GROWTH_MAX_SHIFT; if (v > 0) for (shift = 2; shift < SCR_CYC_GROWTH_MAX_SHIFT && (2L << (shift - 2)) <= v;) shift++; - cached = (unsigned char)shift; + scr_cyc_growth_max_shift_cached = (unsigned char)shift; } - return cached; + return scr_cyc_growth_max_shift_cached; +} + +void scr_cyc_set_growth_cap(long cap) { + scr_cyc_growth_cap_set = cap; + scr_cyc_growth_max_shift_cached = 0; + /* Fractions already widened past the new cap start at it. */ + unsigned max = scr_cyc_growth_max_shift(); + for (unsigned gen = 0; gen < SCR_CYC_NGENS; gen++) + if (scr_cyc_growth_shift[gen] > max) scr_cyc_growth_shift[gen] = (unsigned char)max; } /* Doublings of each generation's backlog threshold earned by consecutive diff --git a/packages/runtime/src/scr_dgram.c b/packages/runtime/src/scr_dgram.c index bb14d063e..8d1c7b058 100644 --- a/packages/runtime/src/scr_dgram.c +++ b/packages/runtime/src/scr_dgram.c @@ -2158,3 +2158,73 @@ void scr_dgram_send_chk(ScrDgramSocket *s, const ScrDyn *buffer, const ScrDyn *a * it removes a 'message' listener. Defined last so the functions above * keep their emission order. */ void scr_dgram_rearm(ScrDgramSocket *s) { scr_dgram_update_read(s); } + +/* ── Unix datagram sockets for the hot-shots package shim ────────────── + * The unix-dgram addon's surface, which hot-shots' uds transport uses: a + * non-blocking AF_UNIX datagram socket connected to a path. Each call + * answers its result (fd, 0) or a negative errno, as the addon's binding + * does; send answers 1 when the socket would block (the addon's + * "congestion"). Defined after the functions above so their emission + * order is unchanged. */ +#ifndef _WIN32 +#include +#endif + +double scr_dgram_unix_socket(void) { +#ifdef _WIN32 + return -(double)ENOSYS; +#else + int fd = socket(AF_UNIX, SOCK_DGRAM, 0); + if (fd < 0) return -(double)errno; + int flags = fcntl(fd, F_GETFL, 0); + if (flags >= 0) (void)fcntl(fd, F_SETFL, flags | O_NONBLOCK); + (void)fcntl(fd, F_SETFD, FD_CLOEXEC); + return (double)fd; +#endif +} + +double scr_dgram_unix_connect(double fd, ScrStr *path) { +#ifdef _WIN32 + (void)fd; + (void)path; + return -(double)ENOSYS; +#else + struct sockaddr_un addr; + memset(&addr, 0, sizeof addr); + addr.sun_family = AF_UNIX; + char *owned; + const char *bytes = scr_str_utf8(path, &owned); + size_t n = strlen(bytes); + if (n > sizeof addr.sun_path - 1) n = sizeof addr.sun_path - 1; + memcpy(addr.sun_path, bytes, n); + scr_str_utf8_end(owned); + if (connect((int)fd, (struct sockaddr *)&addr, sizeof addr) != 0) return -(double)errno; + return 0; +#endif +} + +double scr_dgram_unix_send(double fd, ScrBytes *data) { +#ifdef _WIN32 + (void)fd; + (void)data; + return -(double)ENOSYS; +#else + SCR_BYTES_SNAPSHOT(data); + ssize_t sent; + do { + sent = send((int)fd, data->data, data->len, 0); + } while (sent < 0 && errno == EINTR); + if (sent >= 0) return 0; + if (errno == EAGAIN || errno == EWOULDBLOCK) return 1; + return -(double)errno; +#endif +} + +double scr_dgram_unix_close(double fd) { +#ifdef _WIN32 + (void)fd; + return -(double)ENOSYS; +#else + return close((int)fd) != 0 ? -(double)errno : 0; +#endif +} diff --git a/packages/runtime/src/scr_dgram.h b/packages/runtime/src/scr_dgram.h index 8396f0693..08db077df 100644 --- a/packages/runtime/src/scr_dgram.h +++ b/packages/runtime/src/scr_dgram.h @@ -55,6 +55,13 @@ double scr_dgram_addr_port(ScrDgramSocket *s); void scr_dgram_close(ScrDgramSocket *s, ScrClosure *cb /*moves, nullable*/); /* throws on closed */ void scr_dgram_unref(ScrDgramSocket *s); void scr_dgram_ref(ScrDgramSocket *s); +/* Unix datagram sockets for the hot-shots package shim (the unix-dgram + * addon's surface): each answers its result (fd, 0) or a negative errno; + * send answers 1 when the socket would block. */ +double scr_dgram_unix_socket(void); +double scr_dgram_unix_connect(double fd, ScrStr *path /*borrowed*/); +double scr_dgram_unix_send(double fd, ScrBytes *data /*borrowed*/); +double scr_dgram_unix_close(double fd); /* Socket options: Node's checks and errors (ErrnoException " * ", ERR_SOCKET_BAD_BUFFER_SIZE, ERR_SOCKET_BUFFER_SIZE, the * TypeError after close()). The TTL setters and setMulticastLoopback diff --git a/packages/runtime/src/scr_dh.c b/packages/runtime/src/scr_dh.c index cf44338f5..dfa6fd2f3 100644 --- a/packages/runtime/src/scr_dh.c +++ b/packages/runtime/src/scr_dh.c @@ -183,17 +183,17 @@ static void sd_value_too_long(void) { scr_throw_error_msg_code(SCR_ERR_ERROR, message, n < 0 ? 0 : (size_t)n, "SC2020"); } -/* validateInt32(value, name). */ -static bool sd_validate_int32(const char *name, double value) { +/* validateInt32(value, name, min). */ +static bool sd_validate_int32_min(const char *name, double value, int32_t min) { bool integer = isfinite(value) && trunc(value) == value; - if (integer && value >= INT32_MIN && value <= INT32_MAX) return true; + if (integer && value >= min && value <= INT32_MAX) return true; char received[48]; scr_num_received(value, received); char text[192]; int n = integer ? snprintf(text, sizeof text, - "The value of \"%s\" is out of range. It must be >= -2147483648 && <= 2147483647. " + "The value of \"%s\" is out of range. It must be >= %ld && <= 2147483647. " "Received %s", - name, received) + name, (long)min, received) : snprintf(text, sizeof text, "The value of \"%s\" is out of range. It must be an integer. Received %s", name, received); @@ -201,6 +201,11 @@ static bool sd_validate_int32(const char *name, double value) { return false; } +/* validateInt32(value, name). */ +static bool sd_validate_int32(const char *name, double value) { + return sd_validate_int32_min(name, value, INT32_MIN); +} + /* ── arithmetic ───────────────────────────────────────────────────────── */ /* The exponent digit width that keeps every mbedtls_mpi_exp_mod exponent @@ -630,9 +635,9 @@ static int sd_mr_rounds(size_t bits) { return bits > 2048 ? 128 : 64; } -/* BN_check_prime: trial division by the small primes, then Miller-Rabin - * rounds with random bases. */ -static bool sd_is_prime(ScDhSmallPrimes *t, const mbedtls_mpi *x) { +/* BN_is_prime_ex(x, checks): trial division by the small primes, then + * Miller-Rabin rounds with random bases, at least OpenSSL's round count. */ +static bool sd_is_prime_checks(ScDhSmallPrimes *t, const mbedtls_mpi *x, int checks) { if (mbedtls_mpi_cmp_int(x, 2) < 0) return false; if (mbedtls_mpi_get_bit(x, 0) == 0) return mbedtls_mpi_cmp_int(x, 2) == 0; if (!sd_small_residues(t, x)) return false; @@ -643,11 +648,17 @@ static bool sd_is_prime(ScDhSmallPrimes *t, const mbedtls_mpi *x) { ScDhMillerRabin m; bool prime = sd_mr_init(&m, x); int rounds = sd_mr_rounds(mbedtls_mpi_bitlen(x)); + if (checks > rounds) rounds = checks; for (int i = 0; i < rounds && prime; i++) prime = sd_mr_round(&m, x, false) == 1; sd_mr_free(&m); return prime; } +/* BN_check_prime. */ +static bool sd_is_prime(ScDhSmallPrimes *t, const mbedtls_mpi *x) { + return sd_is_prime_checks(t, x, 0); +} + /* DH_check for parameters without q, as ncrypto's CheckDhParams reports * it. */ static int sd_check_params(const mbedtls_mpi *p, const mbedtls_mpi *g) { @@ -793,8 +804,9 @@ static void sd_sieve_window(ScDhSieve *sv, unsigned add) { * when p is a strong probable prime to the base 2 and q = (p - 1) / 2 * passes OpenSSL's Miller-Rabin round count; a prime q then proves p prime * (Pocklington's criterion with 2^(p-1) ≡ 1 and gcd(2^2 - 1, p) = 1). - * Candidates past `bits` bits take a new start. */ -static bool sd_generate_safe_prime(mbedtls_mpi *p, size_t bits, unsigned add, unsigned rem) { + * Candidates past `bits` bits take a new start. With `top_two`, starts + * also set the second-highest bit, as BN_RAND_TOP_TWO does. */ +static bool sd_generate_safe_prime(mbedtls_mpi *p, size_t bits, unsigned add, unsigned rem, bool top_two) { ScDhSieve *sv = sd_sieve_new(bits, add); int rounds = sd_mr_rounds(bits); mbedtls_mpi start, q; @@ -803,7 +815,8 @@ static bool sd_generate_safe_prime(mbedtls_mpi *p, size_t bits, unsigned add, un int ret = 0; while (ret == 0) { mbedtls_mpi_uint r; - if (!sd_random_bits(&start, bits, true) || mbedtls_mpi_mod_int(&r, &start, add) != 0 || + if (!sd_random_bits(&start, bits, true) || (top_two && mbedtls_mpi_set_bit(&start, bits - 2, 1) != 0) || + mbedtls_mpi_mod_int(&r, &start, add) != 0 || mbedtls_mpi_sub_int(&start, &start, (mbedtls_mpi_sint)r) != 0 || mbedtls_mpi_add_int(&start, &start, rem) != 0 || (mbedtls_mpi_bitlen(&start) < bits && mbedtls_mpi_add_int(&start, &start, add) != 0)) { @@ -900,7 +913,7 @@ static ScrDyn *sd_generate(double bits, const ScrBytes *generator_bytes, double if (generator == 2) add = 24; else if (generator == 5) add = 60; ScDh *d = sd_alloc(); - if (!sd_generate_safe_prime(&d->p, (size_t)bits, add, add - 1) || mbedtls_mpi_lset(&d->g, (mbedtls_mpi_sint)generator) != 0) { + if (!sd_generate_safe_prime(&d->p, (size_t)bits, add, add - 1, false) || mbedtls_mpi_lset(&d->g, (mbedtls_mpi_sint)generator) != 0) { sd_release(d); sd_throw(SCR_ERR_TYPE, "ERR_INVALID_ARG_VALUE", "Invalid DH parameters"); return NULL; @@ -1034,6 +1047,505 @@ ScrDyn *scr_crypto_dh_group_new(ScrDyn *name) { return sd_box(d); } +/* ── checkPrime and generatePrime ───────────────────────────────────────── + * crypto.checkPrime(Sync) and generatePrime(Sync) over the primality test + * and prime search above, with random.js's argument checks and ncrypto's + * RandomPrimeConfig and BN_generate_prime_ex2 errors. The callback forms + * compute at once and call back in a later macrotask. */ + +/* The longest candidate the Miller-Rabin rounds take: sd_mont_init's R^2 + * has twice the candidate's limbs, within MBEDTLS_MPI_MAX_LIMBS, and + * mbedtls_mpi_exp_mod stops at MBEDTLS_MPI_MAX_BITS. */ +#if SD_MONT +#define SD_MAX_PRIME_BITS 262144 +#else +#define SD_MAX_PRIME_BITS MBEDTLS_MPI_MAX_BITS +#endif + +static void sd_prime_too_long(const char *what) { + char message[128]; + int n = snprintf(message, sizeof message, "%s longer than %d bits have no native implementation", what, + SD_MAX_PRIME_BITS); + scr_throw_error_msg_code(SCR_ERR_ERROR, message, n < 0 ? 0 : (size_t)n, "SC2020"); +} + +/* An OpenSSL bignum error as a crypto job reports it: an Error with the + * formatted message and no other properties. */ +static void sd_throw_bignum(const char *packed, const char *reason) { + char message[96]; + int n = snprintf(message, sizeof message, "error:%s:bignum routines::%s", packed, reason); + ScrStr *text = scr_str_new(message, n < 0 ? 0 : (size_t)n); + ScrError *e = scr_error_new(SCR_ERR_ERROR, text); + scr_str_release(text); + scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg()); +} + +/* validateObject(value, name): an object that is neither null nor an + * array. */ +static bool sd_validate_object(const char *name, const ScrDyn *value) { + ScrStr *type = scr_dyn_typeof(value); + bool object = type->len == 6 && memcmp(type->data, "object", 6) == 0; + scr_str_release(type); + if (object && value->kind != SCR_DYN_NULL && value->kind != SCR_DYN_ARR) return true; + scr_dyn_arg_type_fail(name, "of type object", value); + return false; +} + +static bool sd_validate_function(const char *name, const ScrDyn *value) { + if (scr_dyn_is_callable(value)) return true; + scr_dyn_arg_type_fail(name, "of type function", value); + return false; +} + +/* options[key] (borrowed), or undefined. */ +static const ScrDyn *sd_option(const ScrDyn *options, const char *key) { + const ScrDyn *value = options->kind == SCR_DYN_OBJ ? scr_dyn_obj_get(options, key, strlen(key)) : NULL; + return value ? value : scr_dyn_undefined(); +} + +static const char sd_large_number_types[] = + "of type bigint or an instance of ArrayBuffer, TypedArray, Buffer, or DataView"; + +/* A LargeNumberLike argument as a bignum: a bigint (unsignedBigIntToBuffer, + * which rejects negative values), or the bytes of an ArrayBuffer or view. + * False with the error pending. */ +static bool sd_large_number(mbedtls_mpi *x, const char *name, bool property, const ScrDyn *value) { + if (value->kind == SCR_DYN_BIGINT) { + ScrStr *text = scr_bigint_to_string(value->v.bigint, 16); + if (!text) return false; + bool negative = text->len > 0 && text->data[0] == '-'; + if (negative) { + scr_str_release(text); + ScrStr *decimal = scr_bigint_to_string(value->v.bigint, 10); + if (!decimal) return false; + /* ERR_OUT_OF_RANGE's received value groups the digits of a bigint + * past 2^32 in threes. */ + ScrBigInt *limit = scr_bigint_from_i64(-((int64_t)1 << 32)); + bool group = scr_bigint_cmp_f64(value->v.bigint, limit) < 0; + scr_bigint_release(limit); + char digits[160]; + size_t len = decimal->len < 120 ? decimal->len : 120, out = 0; + for (size_t i = 0; i < len; i++) { + size_t rest = len - i; + if (group && i > 1 && rest % 3 == 0) digits[out++] = '_'; + digits[out++] = decimal->data[i]; + } + digits[out] = '\0'; + scr_str_release(decimal); + char message[256]; + int n = snprintf(message, sizeof message, "The value of \"%s\" is out of range. It must be >= 0. Received %sn", + name, digits); + scr_throw_error_msg_code(SCR_ERR_RANGE, message, n < 0 ? 0 : (size_t)n, "ERR_OUT_OF_RANGE"); + return false; + } + char *hex = malloc(text->len + 1); + if (!hex) scr_trap("scriptc: out of memory\n"); + memcpy(hex, text->data, text->len); + hex[text->len] = '\0'; + scr_str_release(text); + bool ok = strlen(hex) <= SD_MAX_VALUE_BYTES * 2; + if (ok && mbedtls_mpi_read_string(x, 16, hex) != 0) scr_trap("scriptc: out of memory\n"); + free(hex); + if (!ok) sd_value_too_long(); + return ok; + } + if (!sd_is_view(value)) { + if (property) scr_dyn_prop_type_fail(name, sd_large_number_types, value); + else scr_dyn_arg_type_fail(name, sd_large_number_types, value); + return false; + } + ScrBytes *bytes = scr_bytes_buffer_source(value); + if (!bytes) return false; + bool held = sd_read(x, bytes); + scr_bytes_release(bytes); + if (!held) sd_value_too_long(); + return held; +} + +/* calc_trial_divisions: how many of the first primes (2 included) the + * search divides a candidate of `bits` bits by. */ +static size_t sd_trial_divisions(size_t bits) { + return bits <= 512 ? 64 : bits <= 1024 ? 128 : bits <= 2048 ? 384 : bits <= 4096 ? 1024 : 2048; +} + +static uint64_t sd_word(const mbedtls_mpi *x) { + if (mbedtls_mpi_bitlen(x) > 64) return UINT64_MAX; + unsigned char buf[8]; + if (mbedtls_mpi_write_binary(x, buf, 8) != 0) return UINT64_MAX; + uint64_t w = 0; + for (int i = 0; i < 8; i++) w = w << 8 | buf[i]; + return w; +} + +static bool sd_add_word(mbedtls_mpi *x, uint64_t w) { + unsigned char buf[8]; + for (int i = 7; i >= 0; i--, w >>= 8) buf[i] = (unsigned char)w; + mbedtls_mpi t; + mbedtls_mpi_init(&t); + bool ok = mbedtls_mpi_read_binary(&t, buf, 8) == 0 && mbedtls_mpi_add_mpi(x, x, &t) == 0; + mbedtls_mpi_free(&t); + return ok; +} + +/* probable_prime and bn_probable_prime_dh: a random odd candidate of + * `bits` bits (the top two bits set, or with `add` the top bit, moved to + * rem modulo add), then the first one stepping by 2, by 4 for a safe + * prime, or by add, that no trial-division prime divides, nor for a safe + * prime divides (p - 1) / 2. */ +static bool sd_probable_prime(ScDhSmallPrimes *t, mbedtls_mpi *rnd, size_t bits, bool safe, const mbedtls_mpi *add, + const mbedtls_mpi *rem) { + size_t trials = sd_trial_divisions(bits) - 1; /* odd primes */ + uint64_t step = safe ? 4 : 2; + uint64_t maxdelta = UINT64_MAX - t->primes[trials - 1]; + if (add) { + step = sd_word(add); + if (maxdelta > UINT64_MAX - step) maxdelta = UINT64_MAX - step; + } + mbedtls_mpi r; + mbedtls_mpi_init(&r); + bool ok = true; + for (;;) { + if (!sd_random_bits(rnd, bits, true)) ok = false; + if (ok && !add) { + if ((bits >= 2 && mbedtls_mpi_set_bit(rnd, bits - 2, 1) != 0) || (safe && mbedtls_mpi_set_bit(rnd, 1, 1) != 0)) + ok = false; + } else if (ok) { + ok = mbedtls_mpi_mod_mpi(&r, rnd, add) == 0 && mbedtls_mpi_sub_mpi(rnd, rnd, &r) == 0 && + (rem ? mbedtls_mpi_add_mpi(rnd, rnd, rem) == 0 : mbedtls_mpi_add_int(rnd, rnd, safe ? 3 : 1) == 0); + if (ok && (mbedtls_mpi_bitlen(rnd) < bits || sd_word(rnd) < (safe ? 5u : 3u))) + ok = mbedtls_mpi_add_mpi(rnd, rnd, add) == 0; + } + if (!ok || !sd_small_residues(t, rnd)) { + ok = false; + break; + } + uint64_t delta = 0, word = sd_word(rnd); + bool again = false; + for (size_t i = 0; i < trials && !again;) { + uint64_t prime = t->primes[i]; + /* A one-word candidate needs only the primes up to its square root. */ + if (bits <= 31 && delta <= 0x7fffffff && prime * prime > word + delta) break; + uint64_t m = (t->mods[i] + delta) % prime; + if (safe ? m <= 1 : m == 0) { + delta += step; + if (delta > maxdelta) again = true; + i = 0; + continue; + } + i++; + } + if (again) continue; + if (!sd_add_word(rnd, delta)) { + ok = false; + break; + } + if (add || mbedtls_mpi_bitlen(rnd) == bits) break; + } + mbedtls_mpi_free(&r); + return ok; +} + +/* BN_generate_prime_ex2(p, bits, safe, add, rem): false with OpenSSL's + * error pending. Long safe primes without add take the sieve over + * p ≡ 11 (mod 12), which every safe prime above 7 satisfies. */ +static bool sd_generate_prime(mbedtls_mpi *p, size_t bits, bool safe, const mbedtls_mpi *add, const mbedtls_mpi *rem) { + if (bits < 2 || (!add && safe && bits < 6 && bits != 3)) { + sd_throw_bignum("01800076", "bits too small"); + return false; + } + if (add && mbedtls_mpi_cmp_int(add, 0) == 0) { + sd_throw_bignum("01800067", "div by zero"); + return false; + } + if (!add && safe && bits >= 64) { + if (!sd_generate_safe_prime(p, bits, 12, 11, true)) scr_trap("scriptc: out of memory\n"); + return true; + } + ScDhSmallPrimes *t = sd_small_primes(); + mbedtls_mpi q; + mbedtls_mpi_init(&q); + bool found = false; + while (!found) { + if (!sd_probable_prime(t, p, bits, safe, add, rem)) scr_trap("scriptc: out of memory\n"); + if (!safe) { + found = sd_is_prime(t, p); + } else { + if (mbedtls_mpi_copy(&q, p) != 0 || mbedtls_mpi_shift_r(&q, 1) != 0) scr_trap("scriptc: out of memory\n"); + found = sd_is_prime(t, p) && sd_is_prime(t, &q); + } + } + mbedtls_mpi_free(&q); + free(t); + return true; +} + +/* checkPrime's arguments after the candidate: Node's options and checks + * rules. -1 with the error pending. */ +static int sd_prime_checks(const ScrDyn *options) { + if (!sd_validate_object("options", options)) return -1; + const ScrDyn *checks = sd_option(options, "checks"); + if (checks->kind == SCR_DYN_UNDEF) return 0; + if (checks->kind != SCR_DYN_NUM) { + scr_dyn_prop_type_fail("options.checks", "of type number", checks); + return -1; + } + if (!sd_validate_int32_min("options.checks", checks->v.num, 0)) return -1; + return (int)checks->v.num; +} + +/* The candidate's verdict as a boolean dyn (+1), or NULL with the error + * pending. */ +static ScrDyn *sd_check_prime(const mbedtls_mpi *candidate, int checks) { + if (mbedtls_mpi_bitlen(candidate) > SD_MAX_PRIME_BITS) { + sd_prime_too_long("checkPrime candidates"); + return NULL; + } + ScDhSmallPrimes *t = sd_small_primes(); + bool prime = sd_is_prime_checks(t, candidate, checks); + free(t); + return scr_dyn_new_bool(prime); +} + +/* A borrowed, unboxed view of each argument (+1 each); false after a + * throw. */ +static bool sd_unbox_all(ScrDyn **out, ScrDyn *const *args, size_t count) { + for (size_t i = 0; i < count; i++) out[i] = NULL; + for (size_t i = 0; i < count; i++) { + ScrDyn *value = args[i]; + out[i] = value->kind == SCR_DYN_TYPED_REF ? scr_dyn_typed_ref_materialize(value) : scr_dyn_retain(value); + if (!out[i]) return false; + } + return true; +} + +static void sd_release_all(ScrDyn **values, size_t count) { + for (size_t i = 0; i < count; i++) + if (values[i]) scr_dyn_release(values[i]); +} + +/* checkPrimeSync(candidate[, options]). */ +ScrDyn *scr_crypto_check_prime_sync(ScrDyn *candidate, ScrDyn *options) { + ScrDyn *args[2] = {candidate, options}; + ScrDyn *v[2]; + ScrDyn *result = NULL; + mbedtls_mpi x; + mbedtls_mpi_init(&x); + if (sd_unbox_all(v, args, 2) && sd_large_number(&x, "candidate", false, v[0])) { + ScrDyn *empty = v[1]->kind == SCR_DYN_UNDEF ? scr_dyn_new_obj() : NULL; + int checks = sd_prime_checks(empty ? empty : v[1]); + if (empty) scr_dyn_release(empty); + if (checks >= 0) result = sd_check_prime(&x, checks); + } + mbedtls_mpi_free(&x); + sd_release_all(v, 2); + return result; +} + +/* A deferred callback(...args): the callback and its one or two + * arguments, each in a box. */ +static void sd_deliver(ScrClosure *self) { + ScrDyn *callback = (ScrDyn *)scr_box_get_ref(self->caps[0]); + size_t argc = self->ncaps - 1; + ScrDyn *args[2] = {NULL, NULL}; + for (size_t i = 0; i < argc; i++) args[i] = (ScrDyn *)scr_box_get_ref(self->caps[i + 1]); + ScrDyn *result = scr_dyn_call(callback, args, argc, "callback"); + if (result) scr_dyn_release(result); + for (size_t i = 0; i < argc; i++) scr_dyn_release(args[i]); + scr_dyn_release(callback); +} + +/* Calls callback(first[, second]) in a later macrotask (moves first and + * second). */ +static void sd_defer(const ScrDyn *callback, ScrDyn *first, ScrDyn *second) { + ScrDyn *values[3] = {scr_dyn_retain((ScrDyn *)callback), first, second}; + size_t count = second ? 3 : 2; + ScrClosure *c = scr_closure_new((void *)sd_deliver, count); + for (size_t i = 0; i < count; i++) { + c->caps[i] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, NULL); + scr_box_set_ref(c->caps[i], values[i]); + } + scr_loop_defer(c); +} + +/* The callback's error argument on success (+1): Node's undefined, or + * null for a callback typed to take Error | null. */ +static ScrDyn *sd_no_error(bool null_error) { + return null_error ? scr_dyn_new_null() : scr_dyn_retain((ScrDyn *)scr_dyn_undefined()); +} + +/* The pending error, taken as a dyn value (+1). */ +static ScrDyn *sd_take_error(void) { + ScrCaught *caught = scr_exc_take(); + ScrDyn *error = scr_caught_to_dyn(caught); + scr_caught_release(caught); + return error; +} + +/* checkPrime(candidate[, options], callback): argument errors throw; the + * callback receives (undefined, verdict) in a later macrotask (null for + * undefined when `null_error`). */ +void scr_crypto_check_prime_async(ScrDyn *candidate, ScrDyn *options, ScrDyn *callback, bool null_error) { + ScrDyn *args[3] = {candidate, options, callback}; + ScrDyn *v[3]; + mbedtls_mpi x; + mbedtls_mpi_init(&x); + if (sd_unbox_all(v, args, 3) && sd_large_number(&x, "candidate", false, v[0])) { + ScrDyn *bag = v[1], *fn = v[2]; + ScrDyn *empty = NULL; + if (scr_dyn_is_callable(bag)) { + fn = bag; + bag = empty = scr_dyn_new_obj(); + } else if (bag->kind == SCR_DYN_UNDEF) { + bag = empty = scr_dyn_new_obj(); + } + int checks = sd_validate_function("callback", fn) ? sd_prime_checks(bag) : -1; + ScrDyn *verdict = checks >= 0 ? sd_check_prime(&x, checks) : NULL; + if (verdict) sd_defer(fn, sd_no_error(null_error), verdict); + if (empty) scr_dyn_release(empty); + } + mbedtls_mpi_free(&x); + sd_release_all(v, 3); +} + +/* RandomPrimeConfig: the size and options checks, then the add and rem + * bounds. False with the error pending. */ +typedef struct { + size_t bits; + bool safe, bigint, has_add, has_rem; + mbedtls_mpi add, rem; +} ScDhPrimeConfig; + +static void sd_prime_config_free(ScDhPrimeConfig *c) { + mbedtls_mpi_free(&c->add); + mbedtls_mpi_free(&c->rem); +} + +static bool sd_prime_size(const ScrDyn *size) { + if (size->kind != SCR_DYN_NUM) { + scr_dyn_arg_type_fail("size", "of type number", size); + return false; + } + return sd_validate_int32_min("size", size->v.num, 1); +} + +static bool sd_prime_config(ScDhPrimeConfig *c, double size, const ScrDyn *options) { + if (!sd_validate_object("options", options)) return false; + const ScrDyn *safe = sd_option(options, "safe"); + const ScrDyn *bigint = sd_option(options, "bigint"); + const ScrDyn *add = sd_option(options, "add"); + const ScrDyn *rem = sd_option(options, "rem"); + if (safe->kind != SCR_DYN_UNDEF && safe->kind != SCR_DYN_BOOL) { + scr_dyn_prop_type_fail("options.safe", "of type boolean", safe); + return false; + } + if (bigint->kind != SCR_DYN_UNDEF && bigint->kind != SCR_DYN_BOOL) { + scr_dyn_prop_type_fail("options.bigint", "of type boolean", bigint); + return false; + } + c->safe = safe->kind == SCR_DYN_BOOL && safe->v.b; + c->bigint = bigint->kind == SCR_DYN_BOOL && bigint->v.b; + c->has_add = add->kind != SCR_DYN_UNDEF; + c->has_rem = rem->kind != SCR_DYN_UNDEF; + if (c->has_add && !sd_large_number(&c->add, "options.add", true, add)) return false; + if (c->has_rem && !sd_large_number(&c->rem, "options.rem", true, rem)) return false; + c->bits = (size_t)size; + if (c->has_add) { + if (mbedtls_mpi_bitlen(&c->add) > c->bits) { + sd_throw(SCR_ERR_RANGE, "ERR_OUT_OF_RANGE", "invalid options.add"); + return false; + } + if (c->has_rem && mbedtls_mpi_cmp_mpi(&c->add, &c->rem) <= 0) { + sd_throw(SCR_ERR_RANGE, "ERR_OUT_OF_RANGE", "invalid options.rem"); + return false; + } + } + if (c->bits > SD_MAX_PRIME_BITS) { + sd_prime_too_long("generatePrime sizes"); + return false; + } + return true; +} + +/* The generated prime as an ArrayBuffer of its bytes, or a bigint (+1), + * or NULL with OpenSSL's error pending. */ +static ScrDyn *sd_prime_result(ScDhPrimeConfig *c) { + mbedtls_mpi p; + mbedtls_mpi_init(&p); + ScrDyn *result = NULL; + if (sd_generate_prime(&p, c->bits, c->safe, c->has_add ? &c->add : NULL, c->has_add && c->has_rem ? &c->rem : NULL)) { + if (c->bigint) { + size_t len = 0; + mbedtls_mpi_write_string(&p, 16, NULL, 0, &len); + char *hex = malloc(len + 2); + if (!hex) scr_trap("scriptc: out of memory\n"); + hex[0] = '0'; + hex[1] = 'x'; + if (mbedtls_mpi_write_string(&p, 16, hex + 2, len, &len) != 0) scr_trap("scriptc: out of memory\n"); + ScrStr *text = scr_str_new(hex, strlen(hex)); + free(hex); + ScrBigInt *value = scr_bigint_parse(text); + scr_str_release(text); + if (value) { + result = scr_dyn_new_bigint(value); + scr_bigint_release(value); + } + } else { + size_t len = mbedtls_mpi_size(&p); + ScrBytes *bytes = scr_bytes_new(SCR_BYTES_U8, (double)len); + if (mbedtls_mpi_write_binary(&p, bytes->data, len) != 0) scr_trap("scriptc: out of memory\n"); + result = scr_array_buffer_from_bytes(bytes); + scr_bytes_release(bytes); + } + } + mbedtls_mpi_free(&p); + return result; +} + +/* generatePrimeSync(size[, options]). */ +ScrDyn *scr_crypto_generate_prime_sync(ScrDyn *size, ScrDyn *options) { + ScrDyn *args[2] = {size, options}; + ScrDyn *v[2]; + ScrDyn *result = NULL; + ScDhPrimeConfig c = {0}; + mbedtls_mpi_init(&c.add); + mbedtls_mpi_init(&c.rem); + if (sd_unbox_all(v, args, 2) && sd_prime_size(v[0])) { + ScrDyn *empty = v[1]->kind == SCR_DYN_UNDEF ? scr_dyn_new_obj() : NULL; + if (sd_prime_config(&c, v[0]->v.num, empty ? empty : v[1])) result = sd_prime_result(&c); + if (empty) scr_dyn_release(empty); + } + sd_prime_config_free(&c); + sd_release_all(v, 2); + return result; +} + +/* generatePrime(size[, options], callback): argument errors throw; the + * callback receives (undefined, prime), or OpenSSL's error alone, in a + * later macrotask (null for undefined when `null_error`). */ +void scr_crypto_generate_prime_async(ScrDyn *size, ScrDyn *options, ScrDyn *callback, bool null_error) { + ScrDyn *args[3] = {size, options, callback}; + ScrDyn *v[3]; + ScDhPrimeConfig c = {0}; + mbedtls_mpi_init(&c.add); + mbedtls_mpi_init(&c.rem); + if (sd_unbox_all(v, args, 3) && sd_prime_size(v[0])) { + ScrDyn *bag = v[1], *fn = v[2]; + ScrDyn *empty = NULL; + if (scr_dyn_is_callable(bag)) { + fn = bag; + bag = empty = scr_dyn_new_obj(); + } + if (sd_validate_function("callback", fn) && sd_prime_config(&c, v[0]->v.num, bag)) { + ScrDyn *prime = sd_prime_result(&c); + if (prime) sd_defer(fn, sd_no_error(null_error), prime); + else sd_defer(fn, sd_take_error(), NULL); + } + if (empty) scr_dyn_release(empty); + } + sd_prime_config_free(&c); + sd_release_all(v, 3); +} + /* ── methods ──────────────────────────────────────────────────────────── */ static ScrDyn *sd_arg(ScrDyn *const *args, size_t argc, size_t index) { diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index bb3b06847..00884fc81 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -931,6 +931,7 @@ void scr_dyn_proxy_unsupported(const char *operation); ScrDyn *scr_dyn_new_bytes(const ScrBytes *b); ScrDyn *scr_bytes_constructor(const ScrStr *name); bool scr_bytes_instanceof(const ScrDyn *value, const ScrDyn *callee); +bool scr_bytes_instanceof_at(const ScrDyn *value, const ScrDyn *callee, const ScrStr *refusal); bool scr_dyn_data_view_is(const ScrDyn *value); ScrDyn *scr_dyn_construct(const ScrDyn *callee, const ScrDyn *args, const ScrStr *what); ScrDyn *scr_bytes_construct(const ScrDyn *callee, const ScrDyn *args, const ScrStr *what); @@ -1272,15 +1273,19 @@ bool scr_dyn_strict_eq(const ScrDyn *a, const ScrDyn *b); * result owned (+1). MAY THROW (NULL with the exception pending). */ ScrDyn *scr_dyn_prepare_method(ScrDyn *recv, const char *method); ScrDyn *scr_dyn_invoke_prepared(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); -/* scr_dyn_invoke_prepared for Buffer.prototype method names (scr_bytes_dyn.c), - * one entry per method family: a Buffer receiver runs the method, any other - * takes the ordinary dispatch. */ -ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); -ScrDyn *scr_dyn_invoke_buffer_write(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); -ScrDyn *scr_dyn_invoke_buffer_fill(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); -ScrDyn *scr_dyn_invoke_buffer_to_string(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); -ScrDyn *scr_dyn_invoke_buffer_search(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); -ScrDyn *scr_dyn_invoke_buffer(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); +/* scr_dyn_invoke_prepared for a call site whose method name is a constant: + * *mid_slot, the site's -1-initialized slot, caches the name's method id + * after the first call, so later calls skip the name lookup. */ +ScrDyn *scr_dyn_invoke_prepared_id(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); +/* scr_dyn_invoke_prepared_id for Buffer.prototype method names + * (scr_bytes_dyn.c), one entry per method family: a Buffer receiver runs + * the method, any other takes the ordinary dispatch. */ +ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); +ScrDyn *scr_dyn_invoke_buffer_write(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); +ScrDyn *scr_dyn_invoke_buffer_fill(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); +ScrDyn *scr_dyn_invoke_buffer_to_string(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); +ScrDyn *scr_dyn_invoke_buffer_search(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); +ScrDyn *scr_dyn_invoke_buffer(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); ScrDyn *scr_dyn_invoke(ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc, const char *what); /* Keyed read on a FUNC node: the own-property table first (defineProperties * writes land there), then "name" (the box's best-effort static name; "" diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index 80e01b08c..d4189f070 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 && @@ -861,9 +885,9 @@ static ScrDyn *scr_dyn_invoke_impl( 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 +916,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 +962,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 +973,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 +986,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 +996,16 @@ 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) || (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 +1016,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 +1036,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 +1044,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 +1085,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 +1133,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 +1145,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 +1175,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 +1187,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 +1195,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 +1217,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 +1239,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 +1274,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 +1295,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 +1307,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 +1315,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 +1338,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 +1355,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 +1383,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 +1416,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 +1437,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 +1482,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 +1520,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 +1534,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 +1549,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 +1565,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 +1624,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 +1671,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 +1681,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 +1691,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_http.c b/packages/runtime/src/scr_http.c index a5847e59b..cb88297bf 100644 --- a/packages/runtime/src/scr_http.c +++ b/packages/runtime/src/scr_http.c @@ -4323,14 +4323,32 @@ static void scr_http_client_end_raw(ScrHttpClientReq *c, const char *data, size_ } if (!c->head_sent) { /* whole body known NOW: Content-Length framing (or none — the head - * serializer's empty-body method split) */ + * serializer's empty-body method split). Head and body go out as one + * write, as Node sends them: on a connected (reused) socket two writes + * would be two segments, and the server could answer before the body + * arrives. */ + c->hold_head = true; scr_http_client_send_head(c, (long long)len); + c->hold_head = false; if (scr_exc_pending()) return; if (c->chunked) { scr_http_client_write_raw(c, data, len); if (c->sock) scr_http_send_chunk_end(c->sock, &c->trailers); - } else if (c->sock && len > 0) { - scr_net_sock_write_native(c->sock, data, len); + } else if (c->held_head != NULL) { + char *head = c->held_head; + size_t head_len = c->held_head_len; + c->held_head = NULL; + c->held_head_len = c->held_head_cap = 0; + if (c->sock) { + if (len > 0) { + char *joined = realloc(head, head_len + len); + if (!joined) scr_http_oom(); + head = joined; + memcpy(head + head_len, data, len); + } + scr_net_sock_write_native(c->sock, head, head_len + len); + } + free(head); } } else { scr_http_client_write_raw(c, data, len); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 1c85a928a..ee45f6d93 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -1577,6 +1577,20 @@ ScrDyn *scr_dyn_iter_pack(const ScrDyn *src, const ScrStr *msg) { static ScrDyn *scr_native_iterator_drain(const ScrDyn *src); +/* The values a collection constructor's iterable seed gives, in order: +1 + * array, or NULL with the exception pending. A native iterator (a Map's + * keys(), a Set's values()) drains, which runs no program code; arrays, + * strings, typed arrays and collections pack; other iterables (a + * generator, an object with a Symbol.iterator method) step their + * iterator to the end, so their values are all taken before the + * constructor adds the first one. */ +static ScrDyn *scr_dyn_seed_values(const ScrDyn *src) { + if (src->kind == SCR_DYN_HANDLE && + (src->v.handle.tag == SCR_DYNH_ITERATOR || scr_dyn_handle_ops_of(src)->iter_step)) + return scr_native_iterator_drain(src); + return scr_dyn_destructure_rest(src, NULL); +} + /* The for-of-over-dyn pack accessors: the emitted index loop drives them * over a scr_dyn_iter_pack result (ARR by construction — the defensive * arms cover nothing reachable from that lowering). Never throw. */ @@ -1592,18 +1606,10 @@ ScrDyn *scr_dyn_map_seed_entries(const ScrDyn *src) { if (scr_exc_pending()) return NULL; } /* A native iterator (sp.entries(), a Map's entries()) or a handle that - * steps through live entries (URLSearchParams): producing its entries - * runs no program code, so draining them first is what the consumer - * observes. */ - if (src->kind == SCR_DYN_HANDLE && - (src->v.handle.tag == SCR_DYNH_ITERATOR || scr_dyn_handle_ops_of(src)->iter_step)) - return scr_native_iterator_drain(src); - if (src->kind == SCR_DYN_HANDLE || src->kind == SCR_DYN_TYPED_REF) { - static const char message[] = "new Map(entries) over native non-array iterables has no lowering"; - scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); - return NULL; - } - return scr_dyn_iter_pack(src, NULL); + * steps through live entries (URLSearchParams) drains; a Set, a + * generator or another iterable gives its values in order + * (scr_dyn_seed_values). */ + return scr_dyn_seed_values(src); } ScrDyn *scr_dyn_map_seed_entry(const ScrDyn *entry) { @@ -3973,7 +3979,10 @@ static int scr_dyn_global_constructor_instanceof(const ScrDyn *value, const ScrD return -1; } -bool scr_bytes_instanceof(const ScrDyn *value, const ScrDyn *callee) { +/* `value instanceof callee` for an untyped constructor value. `refusal`, when + * given, is the site's SC1090 message for a runtime constructor with no + * instance test. */ +bool scr_bytes_instanceof_at(const ScrDyn *value, const ScrDyn *callee, const ScrStr *refusal) { if (!callee || callee->kind != SCR_DYN_FUNC) { static const char message[] = "Right-hand side of 'instanceof' is not callable"; scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); @@ -4015,12 +4024,33 @@ bool scr_bytes_instanceof(const ScrDyn *value, const ScrDyn *callee) { scr_dyn_release(prototype); return found; } + bool undefined_prototype = prototype->kind == SCR_DYN_UNDEF; scr_dyn_release(prototype); + /* A function the program compiled with no prototype (an arrow function + * or a method): false for a primitive, and Node's TypeError for every + * object, typed arrays, native handles, promises and typed references + * included. The runtime's own constructors carry "native:" signatures. */ + if (undefined_prototype && (!callee->v.fn.sig || strncmp(callee->v.fn.sig, "native:", 7) != 0)) { + if (value->kind == SCR_DYN_UNDEF || value->kind == SCR_DYN_NULL || value->kind == SCR_DYN_BOOL || + value->kind == SCR_DYN_NUM || value->kind == SCR_DYN_STR || value->kind == SCR_DYN_BIGINT || + value->kind == SCR_DYN_SYMBOL) + return false; + static const char message[] = "Function has non-object prototype 'undefined' in instanceof check"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + return false; + } + } + if (refusal) { + scr_throw_error_msg_code(SCR_ERR_ERROR, refusal->data, refusal->len, "SC1090"); + return false; } static const char message[] = "instanceof against this native constructor is not supported yet"; scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC1090"); return false; } +bool scr_bytes_instanceof(const ScrDyn *value, const ScrDyn *callee) { + return scr_bytes_instanceof_at(value, callee, NULL); +} ScrDyn *scr_bytes_construct(const ScrDyn *callee, const ScrDyn *args, const ScrStr *what) { if (callee->kind == SCR_DYN_FUNC && scr_array_ctor && callee->v.fn.clo == scr_array_ctor->v.fn.clo) return scr_array_ctor_call(callee->v.fn.clo, args->v.arr.items, args->v.arr.len); @@ -14836,7 +14866,7 @@ ScrDyn *scr_dyn_native_set(ScrMap *value) { ScrDyn *scr_dyn_native_set_new(const ScrDyn *seed) { ScrDyn *items = NULL; if (seed->kind != SCR_DYN_NULL && seed->kind != SCR_DYN_UNDEF) { - items = scr_dyn_iter_pack(seed, NULL); + items = scr_dyn_seed_values(seed); if (!items) return NULL; } ScrMap *set = scr_map_new(SCR_MAP_KEY_DYN, SCR_MAP_VAL_F64, &scr_map_dyn_set_ops); diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index 4be596ab5..37ca28f16 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -13002,7 +13002,7 @@ ScrArr *scr_set_to_arr_ref(const ScrMap *s, const ScrArrOps *ops) { for (size_t i = 0; i < s->nentries; i++) { if (!scr_map_entry_live(s, i)) continue; ops->retain((void *)(uintptr_t)scr_map_entry_at(s, i)->key); - out->data[out->len] = scr_map_entry_at(s, i)->key; + scr_arr_dense_put(out, out->len, scr_map_entry_at(s, i)->key); *scr_arr_state_in(states, out->len++) = SCR_ARR_VALUE; } return out; diff --git a/packages/runtime/src/scr_map.c b/packages/runtime/src/scr_map.c index 99caa31a8..648d1bfc8 100644 --- a/packages/runtime/src/scr_map.c +++ b/packages/runtime/src/scr_map.c @@ -220,47 +220,106 @@ static bool scr_map_key_eq(const ScrMap *m, uint64_t stored, uint64_t probe) { * the value: its hash is the key word's scr_key_mix, which is cheap to * recompute, and its stored keys are normalized (one NaN pattern, +0), so * a dead entry marks its key with a NaN pattern that no stored key has. + * A SCR_MAP_VAL_CREF map (string keys, compact class values) keeps 16-byte + * entries: the key, its 32-bit hash and the value as an address over 8. * Every other map keeps 24-byte entries with the cached hash, zero for a - * dead entry. Both layouts put the key at byte 0 and the value at byte 8. */ + * dead entry. Every layout puts the key at byte 0; the 8-byte layouts put + * the value at byte 8. */ static inline bool scr_map_num(const ScrMap *m) { return m->key_kind == SCR_MAP_KEY_F64; } +static inline bool scr_map_cref(const ScrMap *m) { + return __builtin_expect(m->val_kind == SCR_MAP_VAL_CREF, 0); +} + static inline size_t scr_map_entry_size(const ScrMap *m) { - return scr_map_num(m) ? sizeof(ScrMapNumEntry) : sizeof(ScrMapEntry); + if (scr_map_num(m)) return sizeof(ScrMapNumEntry); + if (scr_map_cref(m)) return sizeof(ScrMapCEntry); + return sizeof(ScrMapEntry); } /* The entry at index e. A number-key entry has no hash field: read its key - * and value only (scr_map_hash_at gives the hash). */ + * and value only (scr_map_hash_at gives the hash); a SCR_MAP_VAL_CREF + * entry's value is read with scr_map_val_at. */ static inline ScrMapEntry *scr_map_at(const ScrMap *m, size_t e) { return (ScrMapEntry *)((char *)m->entries + e * scr_map_entry_size(m)); } +static inline ScrMapCEntry *scr_map_c_at(const ScrMap *m, size_t e) { + return (ScrMapCEntry *)m->entries + e; +} + static inline bool scr_map_live_at(const ScrMap *m, size_t e) { - const ScrMapEntry *x = scr_map_at(m, e); - return scr_map_num(m) ? x->key != SCR_MAP_NUM_DEAD : x->hash != 0; + if (scr_map_num(m)) return ((const ScrMapNumEntry *)m->entries)[e].key != SCR_MAP_NUM_DEAD; + if (scr_map_cref(m)) return scr_map_c_at(m, e)->hash != 0; + return m->entries[e].hash != 0; } /* The entry's hash, nonzero when it is live and zero for a dead entry. */ static inline uint64_t scr_map_hash_at(const ScrMap *m, size_t e) { - const ScrMapEntry *x = scr_map_at(m, e); - if (!scr_map_num(m)) return x->hash; - if (x->key == SCR_MAP_NUM_DEAD) return 0; - uint64_t hash = scr_map_hash_word(x->key); - return hash ? hash : 1; + if (scr_map_num(m)) { + uint64_t key = ((const ScrMapNumEntry *)m->entries)[e].key; + if (key == SCR_MAP_NUM_DEAD) return 0; + uint64_t hash = scr_map_hash_word(key); + return hash ? hash : 1; + } + if (scr_map_cref(m)) { + uint32_t h = scr_map_c_at(m, e)->hash; + return h ? scr_map_hash_widen(h) : 0; + } + return m->entries[e].hash; } static inline void scr_map_kill_at(ScrMap *m, size_t e) { - ScrMapEntry *x = scr_map_at(m, e); - if (scr_map_num(m)) x->key = SCR_MAP_NUM_DEAD; - else x->hash = 0; + if (scr_map_num(m)) ((ScrMapNumEntry *)m->entries)[e].key = SCR_MAP_NUM_DEAD; + else if (scr_map_cref(m)) scr_map_c_at(m, e)->hash = 0; + else m->entries[e].hash = 0; } +/* A compact value's 4-byte form and back (scr_cref_encode in scr_array.h). */ +static inline uint32_t scr_map_cref_encode(uint64_t val) { return (uint32_t)(val >> 3); } +static inline uint64_t scr_map_cref_decode(uint32_t val) { return (uint64_t)val << 3; } + +/* The value of entry e as an 8-byte slot, and its store. */ +static inline uint64_t scr_map_val_at(const ScrMap *m, size_t e) { + if (scr_map_num(m)) return ((const ScrMapNumEntry *)m->entries)[e].val; + if (scr_map_cref(m)) return scr_map_cref_decode(scr_map_c_at(m, e)->val); + return m->entries[e].val; +} + +static inline void scr_map_val_put(ScrMap *m, size_t e, uint64_t val) { + if (scr_map_num(m)) ((ScrMapNumEntry *)m->entries)[e].val = val; + else if (scr_map_cref(m)) scr_map_c_at(m, e)->val = scr_map_cref_encode(val); + else m->entries[e].val = val; +} + +/* A reference value as a +1 result: compact values are never counted. */ +static inline void *scr_map_val_retain(const ScrMap *m, uint64_t val) { + if (scr_map_cref(m)) return scr_map_slot_to_ptr(val); + return m->ops->val_retain(scr_map_slot_to_ptr(val)); +} + +/* A SCR_MAP_VAL_CREF map's keys are strings, whose hashes are widened + * 32-bit values: the entry keeps the low half. */ static inline void scr_map_put_at(ScrMap *m, size_t e, uint64_t key, uint64_t val, uint64_t hash) { - ScrMapEntry *x = scr_map_at(m, e); + if (scr_map_num(m)) { + ScrMapNumEntry *x = (ScrMapNumEntry *)m->entries + e; + x->key = key; + x->val = val; + return; + } + if (scr_map_cref(m)) { + ScrMapCEntry *x = scr_map_c_at(m, e); + x->key = key; + x->hash = (uint32_t)hash; + x->val = scr_map_cref_encode(val); + return; + } + ScrMapEntry *x = m->entries + e; x->key = key; x->val = val; - if (!scr_map_num(m)) x->hash = hash; + x->hash = hash; } /* ── absent-key filter ───────────────────────────────────────────────── @@ -282,17 +341,25 @@ static inline uint64_t scr_map_filter_bits(uint64_t hash) { * before equality and survive table growth without hashing the keys again. * Remap zero to one so every live entry has a nonzero hash. */ /* SCR_MAP_LOOKUP_NUM is a word compare on a number-key map, so the probe - * reads 16-byte entries; every other lookup is on a map with 24-byte - * entries. Knowing the layout from the lookup keeps the entry stride a - * constant in the inlined probes. */ + * reads 16-byte entries; SCR_MAP_LOOKUP_STR_C and SPAN_C are the string and + * span probes of a SCR_MAP_VAL_CREF map, whose 16-byte entries keep a + * 32-bit hash; every other lookup is on a map with 24-byte entries. Knowing + * the layout from the lookup keeps the entry stride a constant in the + * inlined probes. */ typedef enum { SCR_MAP_LOOKUP_STR, SCR_MAP_LOOKUP_SPAN, SCR_MAP_LOOKUP_WORD, SCR_MAP_LOOKUP_NUM, - SCR_MAP_LOOKUP_GENERIC + SCR_MAP_LOOKUP_GENERIC, + SCR_MAP_LOOKUP_STR_C, + SCR_MAP_LOOKUP_SPAN_C } ScrMapLookup; +static inline bool scr_map_lookup_c(ScrMapLookup lookup) { + return lookup == SCR_MAP_LOOKUP_STR_C || lookup == SCR_MAP_LOOKUP_SPAN_C; +} + typedef struct { const char *bytes; size_t length; } ScrMapSpan; /* Choose equality once per operation. Inlining the traversal with a constant @@ -301,12 +368,12 @@ typedef struct { const char *bytes; size_t length; } ScrMapSpan; static inline bool scr_map_lookup_eq(const ScrMap *m, uint64_t stored, uint64_t key, ScrMapLookup lookup) { if (lookup == SCR_MAP_LOOKUP_WORD || lookup == SCR_MAP_LOOKUP_NUM) return stored == key; - if (lookup == SCR_MAP_LOOKUP_SPAN) { + if (lookup == SCR_MAP_LOOKUP_SPAN || lookup == SCR_MAP_LOOKUP_SPAN_C) { const ScrStr *a = scr_map_slot_to_ptr(stored); const ScrMapSpan *b = scr_map_slot_to_ptr(key); return a->len == b->length && scr_key_equal(a->data, b->bytes, a->len); } - if (lookup == SCR_MAP_LOOKUP_STR) { + if (lookup == SCR_MAP_LOOKUP_STR || lookup == SCR_MAP_LOOKUP_STR_C) { const ScrStr *a = scr_map_slot_to_ptr(stored); const ScrStr *b = scr_map_slot_to_ptr(key); return a == b || (a->len == b->len && scr_key_equal(a->data, b->data, a->len)); @@ -318,6 +385,7 @@ static inline bool scr_map_lookup_eq(const ScrMap *m, uint64_t stored, uint64_t static inline __attribute__((always_inline)) ScrMapEntry *scr_map_at_with(const ScrMap *m, size_t e, ScrMapLookup lookup) { if (lookup == SCR_MAP_LOOKUP_NUM) return (ScrMapEntry *)((ScrMapNumEntry *)m->entries + e); + if (scr_map_lookup_c(lookup)) return (ScrMapEntry *)((ScrMapCEntry *)m->entries + e); return &m->entries[e]; } @@ -328,6 +396,8 @@ static inline __attribute__((always_inline)) bool scr_map_match_at( const ScrMap *m, size_t e, uint64_t hash, uint64_t key, ScrMapLookup lookup) { const ScrMapEntry *x = scr_map_at_with(m, e, lookup); if (lookup == SCR_MAP_LOOKUP_NUM) return x->key == key; + if (scr_map_lookup_c(lookup)) + return ((const ScrMapCEntry *)x)->hash == (uint32_t)hash && scr_map_lookup_eq(m, x->key, key, lookup); return x->hash == hash && scr_map_lookup_eq(m, x->key, key, lookup); } @@ -437,7 +507,7 @@ static inline __attribute__((always_inline)) size_t scr_map_find_with( static ScrMapLookup scr_map_lookup_kind(const ScrMap *m) { switch (m->key_kind) { - case SCR_MAP_KEY_STR: return SCR_MAP_LOOKUP_STR; + case SCR_MAP_KEY_STR: return scr_map_cref(m) ? SCR_MAP_LOOKUP_STR_C : SCR_MAP_LOOKUP_STR; case SCR_MAP_KEY_F64: return SCR_MAP_LOOKUP_NUM; case SCR_MAP_KEY_BOOL: case SCR_MAP_KEY_REF: @@ -484,6 +554,7 @@ static size_t scr_map_find(const ScrMap *m, uint64_t hash, uint64_t key) { if (m->ndense) return scr_map_find_dense(m, scr_map_slot_to_f64(key)); switch (scr_map_lookup_kind(m)) { case SCR_MAP_LOOKUP_STR: return scr_map_find_with(m, hash, key, SCR_MAP_LOOKUP_STR); + case SCR_MAP_LOOKUP_STR_C: return scr_map_find_with(m, hash, key, SCR_MAP_LOOKUP_STR_C); case SCR_MAP_LOOKUP_WORD: return scr_map_find_with(m, hash, key, SCR_MAP_LOOKUP_WORD); case SCR_MAP_LOOKUP_NUM: return scr_map_find_with(m, hash, key, SCR_MAP_LOOKUP_NUM); default: return scr_map_find_with(m, hash, key, SCR_MAP_LOOKUP_GENERIC); @@ -504,9 +575,24 @@ const ScrMapEntry *scr_map_entry_f64(const ScrMap *m, double key) { /* The key's hash is usually cached, so even the small linear scan rejects * entries by hash before touching their bytes. */ +/* The probes of a SCR_MAP_VAL_CREF map, out of line so that the inlined + * probes of other maps keep their size. */ +static __attribute__((noinline)) size_t scr_map_find_str_c(const ScrMap *m, const ScrStr *key) { + return scr_map_find_with(m, scr_map_hash_str(key), scr_map_slot_from_ptr((void *)key), + SCR_MAP_LOOKUP_STR_C); +} + +static __attribute__((noinline)) size_t scr_map_find_span_c(const ScrMap *m, const char *bytes, + size_t length) { + ScrMapSpan key = { bytes, length }; + return scr_map_find_with(m, scr_map_hash_bytes(bytes, length), scr_map_slot_from_ptr(&key), + SCR_MAP_LOOKUP_SPAN_C); +} + static inline __attribute__((always_inline)) size_t scr_map_find_str(const ScrMap *m, const ScrStr *key) { if (m->nlive == 0) return SCR_MAP_EMPTY; + if (scr_map_cref(m)) return scr_map_find_str_c(m, key); return scr_map_find_with(m, scr_map_hash_str(key), scr_map_slot_from_ptr((void *)key), SCR_MAP_LOOKUP_STR); } @@ -515,6 +601,7 @@ static inline __attribute__((always_inline)) size_t scr_map_find_str(const ScrMa * rules as a string key without allocating or copying the probe. */ static size_t scr_map_find_span(const ScrMap *m, const char *bytes, size_t length) { if (m->nlive == 0) return SCR_MAP_EMPTY; + if (scr_map_cref(m)) return scr_map_find_span_c(m, bytes, length); ScrMapSpan key = { bytes, length }; return scr_map_find_with(m, scr_map_hash_bytes(bytes, length), scr_map_slot_from_ptr(&key), SCR_MAP_LOOKUP_SPAN); @@ -523,20 +610,20 @@ static size_t scr_map_find_span(const ScrMap *m, const char *bytes, size_t lengt bool scr_map_get_span_f64(const ScrMap *m, const char *bytes, size_t length, double *out) { size_t e = scr_map_find_span(m, bytes, length); if (e == SCR_MAP_EMPTY) return false; - *out = scr_map_slot_to_f64(scr_map_at(m, e)->val); + *out = scr_map_slot_to_f64(scr_map_val_at(m, e)); return true; } bool scr_map_get_span_bool(const ScrMap *m, const char *bytes, size_t length, bool *out) { size_t e = scr_map_find_span(m, bytes, length); if (e == SCR_MAP_EMPTY) return false; - *out = scr_map_at(m, e)->val != 0; + *out = scr_map_val_at(m, e) != 0; return true; } void *scr_map_get_span_ref(const ScrMap *m, const char *bytes, size_t length) { size_t e = scr_map_find_span(m, bytes, length); - return e == SCR_MAP_EMPTY ? NULL : m->ops->val_retain(scr_map_slot_to_ptr(scr_map_at(m, e)->val)); + return e == SCR_MAP_EMPTY ? NULL : scr_map_val_retain(m, scr_map_val_at(m, e)); } /* Rebuild the bucket table (size must be a power of two >= 2 * nentries): @@ -666,7 +753,12 @@ static void scr_map_compact(ScrMap *m, bool reindex) { if (k > top) top = k; } filter |= scr_map_filter_bits(scr_map_hash_at(m, r)); - if (w != r) memcpy(scr_map_at(m, w), scr_map_at(m, r), scr_map_entry_size(m)); + if (w != r) { + /* A constant size per layout keeps the copy inline. */ + if (scr_map_num(m)) memcpy(scr_map_at(m, w), scr_map_at(m, r), sizeof(ScrMapNumEntry)); + else if (scr_map_cref(m)) memcpy(scr_map_at(m, w), scr_map_at(m, r), sizeof(ScrMapCEntry)); + else memcpy(scr_map_at(m, w), scr_map_at(m, r), sizeof(ScrMapEntry)); + } w++; } m->nentries = (uint32_t)w; @@ -778,7 +870,7 @@ static void scr_map_trace(void *o, ScrTraceVisit visit, void *ctx) { for (size_t e = 0; e < m->nentries; e++) { if (!scr_map_live_at(m, e)) continue; if (keys) visit(scr_map_slot_to_ptr(scr_map_at(m, e)->key), ctx); - if (vals) visit(scr_map_slot_to_ptr(scr_map_at(m, e)->val), ctx); + if (vals) visit(scr_map_slot_to_ptr(scr_map_val_at(m, e)), ctx); } } @@ -791,7 +883,7 @@ static void scr_map_gcfree(void *o) { for (size_t e = 0; e < m->nentries; e++) { if (!scr_map_live_at(m, e)) continue; if (!keys) scr_map_release_key(m, scr_map_at(m, e)->key); - if (!vals) scr_map_release_val(m, scr_map_at(m, e)->val); + if (!vals) scr_map_release_val(m, scr_map_val_at(m, e)); } scr_mem_free(m->entries); scr_mem_free(m->buckets); @@ -843,10 +935,15 @@ ScrMap *scr_map_retain(ScrMap *m) { static void scr_map_destroy(void *object) { ScrMap *m = object; scr_weak_dispose_inline(m); - for (size_t e = 0; e < m->nentries; e++) { - if (!scr_map_live_at(m, e)) continue; - scr_map_release_key(m, scr_map_at(m, e)->key); - scr_map_release_val(m, scr_map_at(m, e)->val); + /* Number and boolean keys, and values other than references, own + * nothing: a map of only those releases no entries. */ + bool keys = m->key_kind == SCR_MAP_KEY_STR || scr_map_ref_key(m->key_kind); + if (keys || m->val_kind == SCR_MAP_VAL_REF) { + for (size_t e = 0; e < m->nentries; e++) { + if (!scr_map_live_at(m, e)) continue; + scr_map_release_key(m, scr_map_at(m, e)->key); + scr_map_release_val(m, scr_map_val_at(m, e)); + } } scr_mem_free(m->entries); scr_mem_free(m->buckets); @@ -897,7 +994,7 @@ void scr_map_clear(ScrMap *m) { uint32_t *dense = m->ndense ? m->dense : NULL; for (size_t e = 0; e < walked; e++) { if (!scr_map_live_at(m, e)) continue; - uint64_t key = scr_map_at(m, e)->key, val = scr_map_at(m, e)->val; + uint64_t key = scr_map_at(m, e)->key, val = scr_map_val_at(m, e); scr_map_kill_at(m, e); if (dense) dense[(size_t)scr_map_slot_to_f64(key)] = 0; scr_map_release_key(m, key); @@ -955,7 +1052,7 @@ bool scr_map_has_str(const ScrMap *m, const ScrStr *key) { static bool scr_map_delete_found(ScrMap *m, size_t e) { if (e == SCR_MAP_EMPTY) return false; - uint64_t key = scr_map_at(m, e)->key, val = scr_map_at(m, e)->val; + uint64_t key = scr_map_at(m, e)->key, val = scr_map_val_at(m, e); scr_map_kill_at(m, e); /* bucket slot stays: probe chains intact */ m->nlive--; if (m->ndense) m->dense[(size_t)scr_map_slot_to_f64(key)] = 0; @@ -1060,6 +1157,11 @@ static inline __attribute__((always_inline)) void scr_map_set_with( e = scr_map_bucket(m, slot); } if (e != SCR_MAP_EMPTY) { + if (scr_map_lookup_c(lookup)) { + /* Compact values are never counted: nothing to release. */ + ((ScrMapCEntry *)m->entries)[e].val = scr_map_cref_encode(val); + return; + } ScrMapEntry *x = scr_map_at_with(m, e, lookup); uint64_t old = x->val; x->val = val; /* unlink before releasing (cycle collector) */ @@ -1069,7 +1171,18 @@ static inline __attribute__((always_inline)) void scr_map_set_with( scr_map_append(m, hash, key, val, slot); } +/* The insert of a SCR_MAP_VAL_CREF map, out of line so that the string-key + * insert of other maps keeps its size. */ +static __attribute__((noinline)) void scr_map_set_str_c(ScrMap *m, uint64_t hash, uint64_t key, + uint64_t val) { + scr_map_set_with(m, hash, key, val, SCR_MAP_LOOKUP_STR_C); +} + static void scr_map_set_str_slot(ScrMap *m, uint64_t hash, uint64_t key, uint64_t val) { + if (scr_map_cref(m)) { + scr_map_set_str_c(m, hash, key, val); + return; + } scr_map_set_with(m, hash, key, val, SCR_MAP_LOOKUP_STR); } @@ -1157,6 +1270,7 @@ static void scr_map_set(ScrMap *m, uint64_t hash, uint64_t key, uint64_t val) { } switch (scr_map_lookup_kind(m)) { case SCR_MAP_LOOKUP_STR: scr_map_set_str_slot(m, hash, key, val); return; + case SCR_MAP_LOOKUP_STR_C: scr_map_set_str_c(m, hash, key, val); return; case SCR_MAP_LOOKUP_WORD: scr_map_set_word(m, hash, key, val); return; case SCR_MAP_LOOKUP_NUM: scr_map_set_num(m, hash, key, val); return; default: scr_map_set_with(m, hash, key, val, SCR_MAP_LOOKUP_GENERIC); return; @@ -1246,10 +1360,14 @@ bool scr_map_get_str_bool(const ScrMap *m, const ScrStr *key, bool *out) { return true; } +/* scr_map_entry_str serves maps with 24-byte entries (generated code reads + * a SCR_MAP_VAL_CREF map through scr_map_entry_str_c). */ static __attribute__((noinline)) const ScrMapEntry *scr_map_entry_str_slow(const ScrMap *m, const ScrStr *key) { - size_t e = scr_map_find_str(m, key); - return e == SCR_MAP_EMPTY ? NULL : scr_map_at(m, e); + if (m->nlive == 0) return NULL; + size_t e = scr_map_find_with(m, scr_map_hash_str(key), scr_map_slot_from_ptr((void *)key), + SCR_MAP_LOOKUP_STR); + return e == SCR_MAP_EMPTY ? NULL : &m->entries[e]; } /* A stored key with the probe's hash but another identity: compare bytes. @@ -1302,10 +1420,60 @@ const ScrMapEntry *scr_map_entry_str(const ScrMap *m, const ScrStr *key) { return scr_map_entry_str_slow(m, key); } +static __attribute__((noinline)) const ScrMapCEntry *scr_map_entry_str_c_slow(const ScrMap *m, + const ScrStr *key) { + size_t e = scr_map_find_str(m, key); + return e == SCR_MAP_EMPTY ? NULL : scr_map_c_at(m, e); +} + +static __attribute__((noinline)) const ScrMapCEntry *scr_map_entry_str_c_bytes(const ScrMap *m, + const ScrStr *key, + const ScrMapCEntry *e) { + const ScrStr *stored = scr_map_slot_to_ptr(e->key); + if (stored->len == key->len && scr_key_equal(stored->data, key->data, key->len)) return e; + return scr_map_entry_str_c_slow(m, key); +} + +/* scr_map_entry_str over a SCR_MAP_VAL_CREF map's 16-byte entries, whose + * hash is the low half of the widened one. */ +const ScrMapCEntry *scr_map_entry_str_c(const ScrMap *m, const ScrStr *key) { +#if SCR_STR_HASH_CACHE + uint32_t h = key->hash; + size_t nbuckets = m->nbuckets; + if (__builtin_expect(h != 0 && scr_map_compact_buckets(nbuckets), 1)) { + uint64_t hash = scr_map_hash_widen(h); + uint64_t bits = scr_map_filter_bits(hash); + if ((m->filter & bits) != bits) return NULL; + const ScrMapCEntry *entries = (const ScrMapCEntry *)m->entries; + if (nbuckets == 0) { + for (size_t e = 0, n = m->nentries; e < n; e++) { + if (entries[e].hash != h) continue; + if (entries[e].key == scr_map_slot_from_ptr((void *)key)) return &entries[e]; + return scr_map_entry_str_c_bytes(m, key, &entries[e]); + } + return NULL; + } + const uint32_t *buckets = m->buckets; + uint32_t tag = (uint32_t)(hash >> 32 & UINT32_C(0xff000000)); + size_t mask = nbuckets - 1; + for (size_t i = hash & mask;; i = (i + 1) & mask) { + uint32_t bucket = buckets[i]; + if (!bucket) return NULL; + if ((bucket & ~SCR_MAP_INDEX_MASK) != tag) continue; + const ScrMapCEntry *e = &entries[(bucket & SCR_MAP_INDEX_MASK) - 1]; + if (e->hash != h) continue; + if (e->key == scr_map_slot_from_ptr((void *)key)) return e; + return scr_map_entry_str_c_bytes(m, key, e); + } + } +#endif + return scr_map_entry_str_c_slow(m, key); +} + void *scr_map_get_str_ref(const ScrMap *m, const ScrStr *key) { size_t e = scr_map_find_str(m, key); if (e == SCR_MAP_EMPTY) return NULL; - return m->ops->val_retain(scr_map_slot_to_ptr(scr_map_at(m, e)->val)); /* +1 */ + return scr_map_val_retain(m, scr_map_val_at(m, e)); /* +1 */ } bool scr_map_get_ref_f64(const ScrMap *m, const void *key, double *out) { @@ -1408,6 +1576,12 @@ static const ScrMapEntry *scr_map_iter_at(const ScrMap *m, double i) { return scr_map_at(m, (size_t)i); } +/* The value of the live entry at iteration index i as an 8-byte slot. */ +static uint64_t scr_map_iter_val_slot(const ScrMap *m, double i) { + (void)scr_map_iter_at(m, i); + return scr_map_val_at(m, (size_t)i); +} + double scr_map_iter_key_f64(const ScrMap *m, double i) { return scr_map_slot_to_f64(scr_map_iter_at(m, i)->key); } @@ -1442,7 +1616,7 @@ ScrDyn *scr_map_dyn_key(const ScrMap *map, double index) { } ScrDyn *scr_map_dyn_value(const ScrMap *map, double index) { - uint64_t slot = scr_map_iter_at(map, index)->val; + uint64_t slot = scr_map_iter_val_slot(map, index); return map->dyn_ops ? map->dyn_ops->val_box(slot) : scr_dyn_retain(scr_map_slot_to_ptr(slot)); } @@ -1491,6 +1665,9 @@ ScrMap *scr_map_clone(const ScrMap *source, bool keys_only, const ScrMapOps *ops if (!out->entries) scr_map_oom(); out->ecap = (uint32_t)count; size_t top = 0; + /* A key-only copy of a SCR_MAP_VAL_CREF map has the 24-byte layout: its + * entries are copied field by field. */ + bool same_layout = scr_map_cref(source) == scr_map_cref(out); for (size_t i = 0; i < source->nentries; i++) { if (!scr_map_live_at(source, i)) continue; const ScrMapEntry *entry = scr_map_at(source, i); @@ -1498,11 +1675,13 @@ ScrMap *scr_map_clone(const ScrMap *source, bool keys_only, const ScrMapOps *ops size_t k = (size_t)scr_map_slot_to_f64(entry->key); if (k > top) top = k; } - ScrMapEntry *copy = scr_map_at(out, out->nentries++); - memcpy(copy, entry, entry_size); + size_t e = out->nentries++; + ScrMapEntry *copy = scr_map_at(out, e); + if (__builtin_expect(same_layout, 1)) memcpy(copy, entry, entry_size); + else scr_map_put_at(out, e, entry->key, keys_only ? 0 : scr_map_val_at(source, i), scr_map_hash_at(source, i)); if (out->key_kind == SCR_MAP_KEY_STR) scr_str_retain(scr_map_slot_to_ptr(copy->key)); else if (scr_map_ref_key(out->key_kind)) out->ops->key_retain(scr_map_slot_to_ptr(copy->key)); - if (keys_only) copy->val = 0; + if (keys_only) scr_map_val_put(out, e, 0); else if (out->val_kind == SCR_MAP_VAL_REF) out->ops->val_retain(scr_map_slot_to_ptr(copy->val)); } out->nlive = (uint32_t)count; @@ -1550,7 +1729,7 @@ ScrMap *scr_map_clone_dyn(const ScrMap *source, bool keys_only) { void scr_map_values_into(ScrMap *set, const ScrMap *source, bool checked) { for (size_t i = 0; i < source->nentries; i++) { if (!scr_map_live_at(source, i)) continue; - uint64_t value = checked ? scr_map_slot_from_ptr(scr_map_dyn_value(source, (double)i)) : scr_map_at(source, i)->val; + uint64_t value = checked ? scr_map_slot_from_ptr(scr_map_dyn_value(source, (double)i)) : scr_map_val_at(source, i); if (set->key_kind == SCR_MAP_KEY_F64) value = scr_map_f64_bits(scr_map_slot_to_f64(value)); scr_map_set(set, scr_map_hash_key(set, value), value, 0); if (checked) scr_dyn_release(scr_map_slot_to_ptr(value)); @@ -1567,7 +1746,7 @@ ScrArr *scr_set_to_arr_dyn(const ScrMap *map) { } void *scr_map_iter_val_ref(const ScrMap *m, double i) { - return m->ops->val_retain(scr_map_slot_to_ptr(scr_map_iter_at(m, i)->val)); /* +1 */ + return scr_map_val_retain(m, scr_map_iter_val_slot(m, i)); /* +1 */ } /* A published (immortal) map never changes, so iterating it needs no diff --git a/packages/runtime/src/scr_map.h b/packages/runtime/src/scr_map.h index c61df9a90..1f6e3ee12 100644 --- a/packages/runtime/src/scr_map.h +++ b/packages/runtime/src/scr_map.h @@ -50,7 +50,11 @@ typedef enum { SCR_MAP_KEY_BIGINT, SCR_MAP_KEY_BOOL, SCR_MAP_KEY_UNION_VALUE, SCR_MAP_KEY_NULL, SCR_MAP_KEY_UNDEFINED /* tag-only union arms */ } ScrMapKeyKind; -typedef enum { SCR_MAP_VAL_F64, SCR_MAP_VAL_BOOL, SCR_MAP_VAL_REF } ScrMapValKind; +/* SCR_MAP_VAL_CREF: a string-key map whose values are compact class + * instances in the program-lifetime region (llvm/classes.ts + * LlClassMeta.compact). Its entries are ScrMapCEntry; the values are never + * counted or traced. */ +typedef enum { SCR_MAP_VAL_F64, SCR_MAP_VAL_BOOL, SCR_MAP_VAL_REF, SCR_MAP_VAL_CREF } ScrMapValKind; typedef struct { uint64_t key; /* double bits (normalized), ScrStr* (owned), or ref ptr (owned) */ @@ -65,6 +69,15 @@ typedef struct { uint64_t val; } ScrMapNumEntry; #define SCR_MAP_NUM_DEAD UINT64_C(0x7ff0000000000001) +/* A SCR_MAP_VAL_CREF map's entry: the string key, the key's 32-bit hash + * (its cached 64-bit hash is the same value in both halves, + * scr_map.c scr_map_hash_widen; zero = tombstone), and the value as an + * address over 8 (0 for NULL). */ +typedef struct { + uint64_t key; + uint32_t hash; + uint32_t val; +} ScrMapCEntry; /* Checked views use the original storage. Slot converters borrow on reads * and return owned references on writes, just like typed Map operations. */ @@ -140,16 +153,25 @@ typedef struct ScrMap { * the whole deleted prefix again. */ uint32_t head; } ScrMap; -/* The entry at index i of either layout, and whether it is live (code that - * walks a map's entries outside scr_map.c). Key and value are at bytes 0 - * and 8 in both layouts; a number-key entry has no hash field. */ +/* The entry at index i of any layout, whether it is live, and its value as + * an 8-byte slot (code that walks a map's entries outside scr_map.c). The + * key is at byte 0 in every layout; a number-key entry has no hash field, + * and a SCR_MAP_VAL_CREF entry keeps its value in 4 bytes. */ static inline const ScrMapEntry *scr_map_entry_at(const ScrMap *m, size_t i) { - size_t size = m->key_kind == SCR_MAP_KEY_F64 ? sizeof(ScrMapNumEntry) : sizeof(ScrMapEntry); + size_t size = m->key_kind == SCR_MAP_KEY_F64 ? sizeof(ScrMapNumEntry) + : m->val_kind == SCR_MAP_VAL_CREF ? sizeof(ScrMapCEntry) + : sizeof(ScrMapEntry); return (const ScrMapEntry *)((const char *)m->entries + i * size); } static inline bool scr_map_entry_live(const ScrMap *m, size_t i) { const ScrMapEntry *e = scr_map_entry_at(m, i); - return m->key_kind == SCR_MAP_KEY_F64 ? e->key != SCR_MAP_NUM_DEAD : e->hash != 0; + if (m->key_kind == SCR_MAP_KEY_F64) return e->key != SCR_MAP_NUM_DEAD; + if (m->val_kind == SCR_MAP_VAL_CREF) return ((const ScrMapCEntry *)e)->hash != 0; + return e->hash != 0; +} +static inline uint64_t scr_map_entry_val(const ScrMap *m, const ScrMapEntry *e) { + if (m->val_kind == SCR_MAP_VAL_CREF) return (uint64_t)((const ScrMapCEntry *)e)->val << 3; + return e->val; } /* The LLVM backend reads the header through two types (emitter.ts). Its * inline retain/release test the traced byte as %ScrMapRc field 3, as @@ -162,7 +184,8 @@ static inline bool scr_map_entry_live(const ScrMap *m, size_t i) { * third field (sc_map_iter_entry), or for a number-key map its key against * SCR_MAP_NUM_DEAD (sc_map_iter_entry_num). Iteration over a checked * (unknown-key) Map or Set picks one of the two steps from the key kind, - * %ScrMapIx field 1, compared with SCR_MAP_KEY_F64 (0). */ + * %ScrMapIx field 1, compared with SCR_MAP_KEY_F64 (0). Map.size + * (sc_map_size) reads the live count, field 9, as scr_map_size does. */ _Static_assert(offsetof(ScrMap, key_kind) == sizeof(size_t) && SCR_MAP_KEY_F64 == 0, "inline iteration over checked collections reads ScrMap.key_kind as %ScrMapIx field 1"); _Static_assert(offsetof(ScrMap, traced) == sizeof(size_t) + 2, @@ -193,6 +216,9 @@ _Static_assert(sizeof(ScrMapEntry) == 24 && offsetof(ScrMapEntry, val) == 8, "inline lookups read ScrMapEntry.val at byte 8"); _Static_assert(sizeof(ScrMapNumEntry) == 16 && offsetof(ScrMapNumEntry, val) == 8, "inline number-key lookups and iteration read %ScrMapNumEntry { key, val }"); +_Static_assert(sizeof(ScrMapCEntry) == 16 && offsetof(ScrMapCEntry, hash) == 8 && + offsetof(ScrMapCEntry, val) == 12, + "inline compact-value lookups and iteration read %ScrMapCEntry { key, hash, val }"); _Static_assert(sizeof(ScrMap) <= 11 * sizeof(uint64_t), "the ScrMap header stays within 88 bytes"); /* Entry for a number key, or NULL when absent. The pointer is valid until * the next mutation; generated code reads the value slot through it. */ @@ -201,6 +227,9 @@ const ScrMapEntry *scr_map_entry_f64(const ScrMap *m, double key); * reference reads retain the value with its static type's retain instead * of the collection's adapter. */ const ScrMapEntry *scr_map_entry_str(const ScrMap *m, const ScrStr *key); +/* scr_map_entry_str for a SCR_MAP_VAL_CREF map: its entry is a + * ScrMapCEntry. */ +const ScrMapCEntry *scr_map_entry_str_c(const ScrMap *m, const ScrStr *key); /* String-key lookups borrow an exact byte span. Reference results return * +1; scalar outputs are written only on a hit, like the string getters. */ diff --git a/packages/runtime/src/scr_publish.c b/packages/runtime/src/scr_publish.c index 675a13ca9..44f248b5b 100644 --- a/packages/runtime/src/scr_publish.c +++ b/packages/runtime/src/scr_publish.c @@ -199,6 +199,19 @@ static void scr_pub_slot(ScrPub *ctx, uint64_t slot, ScrPubFn fn) { if (fn) scr_pub_push(ctx, p, fn); } +/* The elements of a compact reference array (SCR_ELEM_CREF): 4-byte dense + * slots, 8-byte pointers in the sparse entries. */ +static __attribute__((noinline)) void scr_pub_cref_elems(ScrArr *a, ScrPub *ctx, ScrPubFn fn) { + size_t dense = a->len < a->cap ? a->len : a->cap; + const uint32_t *refs = (const uint32_t *)a->data; + const uint8_t *states = scr_arr_states_end_w(a, sizeof(uint32_t)); + for (size_t i = 0; i < dense; i++) + if (!states || *scr_arr_state_in(states, i)) scr_pub_slot(ctx, scr_cref_decode(refs[i]), fn); + if (scr_arr_ext_of(a)) + for (size_t i = 0; i < scr_arr_ext_of(a)->sparse_len; i++) + if (scr_arr_ext_of(a)->sparse[i].state) scr_pub_slot(ctx, scr_arr_ext_of(a)->sparse[i].slot, fn); +} + void scr_pub_arr(void *v, ScrPub *ctx, ScrPubFn elem) { ScrArr *a = v; if (scr_arr_ext_of(a) && (scr_arr_ext_of(a)->props || scr_arr_ext_of(a)->metadata)) { @@ -206,10 +219,15 @@ void scr_pub_arr(void *v, ScrPub *ctx, ScrPubFn elem) { return; } if (!scr_pub_mark(ctx, a, a->cyc != 0, SCR_PUB_KIND_ARRAY)) return; + if (__builtin_expect(a->elem == SCR_ELEM_CREF, 0)) { + scr_pub_cref_elems(a, ctx, elem); + return; + } ScrPubFn fn = a->elem == SCR_ELEM_STR ? scr_pub_str_fn : a->elem == SCR_ELEM_REF ? elem : NULL; if (!fn) return; + /* String and reference elements have 8-byte slots. */ size_t dense = a->len < a->cap ? a->len : a->cap; - const uint8_t *states = scr_arr_states_end(a); + const uint8_t *states = scr_arr_states_end_w(a, sizeof(uint64_t)); for (size_t i = 0; i < dense; i++) if (!states || *scr_arr_state_in(states, i)) scr_pub_slot(ctx, a->data[i], fn); if (scr_arr_ext_of(a)) @@ -231,12 +249,12 @@ void scr_pub_map(void *v, ScrPub *ctx, ScrPubFn key, ScrPubFn val) { m->key_kind == SCR_MAP_KEY_BIGINT || m->key_kind == SCR_MAP_KEY_UNION_VALUE ? key : NULL; - ScrPubFn vfn = m->val_kind == SCR_MAP_VAL_REF ? val : NULL; + ScrPubFn vfn = m->val_kind == SCR_MAP_VAL_REF || m->val_kind == SCR_MAP_VAL_CREF ? val : NULL; for (size_t i = 0; i < m->nentries; i++) { if (!scr_map_entry_live(m, i)) continue; /* tombstone */ const ScrMapEntry *e = scr_map_entry_at(m, i); if (kfn) scr_pub_slot(ctx, e->key, kfn); - if (vfn) scr_pub_slot(ctx, e->val, vfn); + if (vfn) scr_pub_slot(ctx, scr_map_entry_val(m, e), vfn); } } diff --git a/packages/runtime/src/scr_rc.h b/packages/runtime/src/scr_rc.h index 7a921b451..18df3a237 100644 --- a/packages/runtime/src/scr_rc.h +++ b/packages/runtime/src/scr_rc.h @@ -670,6 +670,10 @@ static inline void scr_cyc_dead(void *obj) { * live heap — do not put it on a per-turn path. */ void scr_collect_cycles(void); void scr_collect_cycles_phase(void); +/* Sets the growth cap SCR_CYCLE_GROWTH_CAP sets (scr_cycle.c), for a program + * that knows its own memory profile; the environment variable takes + * precedence. Call it before the program allocates much. */ +void scr_cyc_set_growth_cap(long cap); void scr_cyc_context_cleanup(void); /* One pass on the normal generational schedule, for callers that reach a diff --git a/packages/runtime/src/scr_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_cycle.c b/packages/runtime/test/test_cycle.c index 4e00b852e..160083d6b 100644 --- a/packages/runtime/test/test_cycle.c +++ b/packages/runtime/test/test_cycle.c @@ -536,6 +536,27 @@ static void check_mixed_dead_structure_narrows(void) { check(live <= narrowed, "a pass that freed a dead mature and old structure did not narrow the growth fraction"); } +/* scr_cyc_set_growth_cap sets the cap SCR_CYCLE_GROWTH_CAP sets, and a + * fraction already widened past it starts at it: after passes that found + * nothing widened the old level's fraction to the configured cap, a cap of + * 1 has the next dead cycle go once the heap has doubled past the floor. + * Restores the configured cap. */ +static void check_program_growth_cap(void) { + enum { FLOOR = 4096 }; + if (getenv("SCR_CYCLE_GROWTH_CAP") != NULL) return; /* it takes precedence */ + size_t slack = 2 * configured_nursery_threshold() + 2; + size_t live = 0; + for (int i = 0; i < 4; i++) live = grow_dead_cycle_until_full_pass(0, false); + check(live > FLOOR + FLOOR * configured_growth_cap(), + "passes that found nothing did not widen the growth fraction to the cap"); + scr_cyc_set_growth_cap(1); + for (int i = 0; i < 4; i++) { + live = grow_dead_cycle_until_full_pass(0, false); + check(live <= FLOOR + FLOOR + slack, "a program's growth cap of 1 did not bound the heap"); + } + scr_cyc_set_growth_cap(0); +} + /* The phase sweep (scr_collect_cycles_phase) is for a caller that knows a * phase's data has just died: it sweeps, and the growth schedule starts * over. After passes that find nothing have widened the old level's @@ -1500,6 +1521,7 @@ int main(void) { check_large_old_buffer_with_nursery_candidates(); check_unjudged_backlog_follows_growth(); check_acyclic_objects(); + check_program_growth_cap(); printf("cycle collection checks passed: threshold=%zu growth_cap=%zu\n", configured_nursery_threshold(), configured_growth_cap()); return 0; diff --git a/packages/runtime/test/test_dh.c b/packages/runtime/test/test_dh.c index aa8f175a3..f459f1047 100644 --- a/packages/runtime/test/test_dh.c +++ b/packages/runtime/test/test_dh.c @@ -199,7 +199,7 @@ static void test_generate(void) { mbedtls_mpi_lset(&g, 2); for (size_t i = 0; i < sizeof adds / sizeof adds[0]; i++) { for (int trial = 0; trial < 2; trial++) { - bool ok = sd_generate_safe_prime(&p, 512, adds[i], adds[i] - 1); + bool ok = sd_generate_safe_prime(&p, 512, adds[i], adds[i] - 1, false); mbedtls_mpi_uint r = 0; ok = ok && mbedtls_mpi_bitlen(&p) == 512 && mbedtls_mpi_mod_int(&r, &p, adds[i]) == 0 && r == adds[i] - 1; mbedtls_mpi_copy(&q, &p); @@ -217,12 +217,64 @@ static void test_generate(void) { mbedtls_mpi_free(&g); } +/* generatePrime's search: the sizes and add forms with one possible + * result, top-two-bit safe-prime starts, and checks above OpenSSL's + * round count. */ +static void test_generate_prime(void) { + static const struct { + size_t bits; + bool safe; + int add, rem; + int want; + } cases[] = { + {2, false, 0, 0, 3}, {3, false, 0, 0, 7}, {4, false, 0, 0, 13}, {3, true, 0, 0, 7}, + {6, true, 0, 0, 59}, {7, true, 0, 0, 107}, {8, false, 200, 1, 401}, {8, false, 128, 0, 257}, + {5, false, 16, 3, 19}, + }; + mbedtls_mpi p, add, rem, q; + mbedtls_mpi_init(&p); + mbedtls_mpi_init(&add); + mbedtls_mpi_init(&rem); + mbedtls_mpi_init(&q); + for (size_t i = 0; i < sizeof cases / sizeof cases[0]; i++) { + mbedtls_mpi_lset(&add, cases[i].add); + mbedtls_mpi_lset(&rem, cases[i].rem); + bool ok = sd_generate_prime(&p, cases[i].bits, cases[i].safe, cases[i].add ? &add : NULL, + cases[i].rem ? &rem : NULL) && + mbedtls_mpi_cmp_int(&p, cases[i].want) == 0; + char what[96]; + snprintf(what, sizeof what, "generatePrime(%zu, safe %d, add %d, rem %d) is %d", cases[i].bits, cases[i].safe, + cases[i].add, cases[i].rem, cases[i].want); + check(ok, what); + } + for (int trial = 0; trial < 2; trial++) { + bool ok = sd_generate_safe_prime(&p, 128, 12, 11, true) && mbedtls_mpi_bitlen(&p) == 128 && + mbedtls_mpi_get_bit(&p, 126) == 1; + mbedtls_mpi_copy(&q, &p); + mbedtls_mpi_shift_r(&q, 1); + ok = ok && mbedtls_mpi_is_prime_ext(&p, 40, sd_random, NULL) == 0 && + mbedtls_mpi_is_prime_ext(&q, 40, sd_random, NULL) == 0; + check(ok, "a 128-bit safe prime with its top two bits set"); + } + ScDhSmallPrimes *small = sd_small_primes(); + mbedtls_mpi_read_string(&q, 10, "3215031751"); + check(!sd_is_prime_checks(small, &q, 200), "200 rounds reject a strong pseudoprime to 2, 3, 5 and 7"); + mbedtls_mpi_read_string(&q, 10, "2305843009213693951"); + check(sd_is_prime_checks(small, &q, 200), "200 rounds accept 2^61 - 1"); + free(small); + mbedtls_mpi_free(&p); + mbedtls_mpi_free(&add); + mbedtls_mpi_free(&rem); + mbedtls_mpi_free(&q); +} + int main(void) { test_mont_pow(); test_is_prime(); test_check_params(); test_long_exponent(); test_generate(); + test_generate_prime(); fprintf(stderr, "%ld/%ld cases passed\n", total - failed, total); return failed ? 1 : 0; } diff --git a/packages/runtime/test/test_map.c b/packages/runtime/test/test_map.c index 32bdb7be1..46805da1e 100644 --- a/packages/runtime/test/test_map.c +++ b/packages/runtime/test/test_map.c @@ -826,11 +826,96 @@ static void test_absent_key_filter(void) { for (int i = 0; i < UNIVERSE; i++) scr_str_release(names[i]); } +/* SCR_MAP_VAL_CREF: string keys with compact class values in 16-byte + * entries (a 32-bit hash, the value as an address over 8). The values here + * are stand-in addresses, never dereferenced. */ +static uint64_t cref_val(size_t i) { return (uint64_t)(i + 1) * 8; } + +static void test_compact_values(void) { + ScrMap *m = scr_map_new(SCR_MAP_KEY_STR, SCR_MAP_VAL_CREF, NULL); + enum { N = 2000 }; + char buf[32]; + ScrStr *keys[N]; + for (size_t i = 0; i < N; i++) { + snprintf(buf, sizeof buf, "k%zu", i); + keys[i] = S(buf); + scr_map_set_str_ref(m, keys[i], (void *)(uintptr_t)cref_val(i)); + } + check(scr_map_size(m) == N, "cref: size after inserts"); + bool all = true; + for (size_t i = 0; i < N; i++) { + all &= scr_map_get_str_ref(m, keys[i]) == (void *)(uintptr_t)cref_val(i); + const ScrMapCEntry *e = scr_map_entry_str_c(m, keys[i]); + all &= e && ((uint64_t)e->val << 3) == cref_val(i) && e->key == (uint64_t)(uintptr_t)keys[i]; + } + check(all, "cref: get and entry_str_c find every value"); + ScrStr *missing = S("absent"); + check(!scr_map_get_str_ref(m, missing) && !scr_map_entry_str_c(m, missing), "cref: absent key"); + check(scr_map_get_span_ref(m, "k7", 2) == (void *)(uintptr_t)cref_val(7), "cref: span lookup"); + /* A key object other than the stored one, equal bytes. */ + ScrStr *again = S("k42"); + check(scr_map_entry_str_c(m, again) && scr_map_get_str_ref(m, again) == (void *)(uintptr_t)cref_val(42), + "cref: equal key bytes, other object"); + scr_str_release(again); + for (size_t i = 0; i < N; i += 2) scr_map_set_str_ref(m, keys[i], (void *)(uintptr_t)cref_val(i + N)); + all = true; + for (size_t i = 0; i < N; i++) + all &= scr_map_get_str_ref(m, keys[i]) == (void *)(uintptr_t)cref_val(i % 2 ? i : i + N); + check(all && scr_map_size(m) == N, "cref: overwrite keeps the entry"); + for (size_t i = 0; i < N; i++) + if (i % 4) scr_map_delete_str(m, keys[i]); + check(scr_map_size(m) == N / 4, "cref: deletes"); + all = true; + for (size_t i = 0; i < N; i++) { + bool want = i % 4 == 0; + all &= scr_map_has_str(m, keys[i]) == want; + if (want) all &= scr_map_get_str_ref(m, keys[i]) == (void *)(uintptr_t)cref_val(i + N); + } + check(all, "cref: lookups after compaction"); + /* Iteration order and values. */ + size_t seen = 0; + all = true; + scr_map_iter_enter(m); + for (double i = scr_map_iter_start(m); i < scr_map_iter_count(m); i = scr_map_iter_skip(m, i + 1)) { + ScrStr *k = scr_map_iter_key_str(m, i); + void *v = scr_map_iter_val_ref(m, i); + size_t want = seen * 4; + all &= k->len == keys[want]->len && memcmp(k->data, keys[want]->data, k->len) == 0; + all &= v == (void *)(uintptr_t)cref_val(want + N); + scr_str_release(k); + seen++; + } + scr_map_iter_exit(m); + check(all && seen == N / 4, "cref: iteration order and values"); + /* Copies keep the layout; a key-only copy takes the 24-byte one. */ + ScrMap *copy = scr_map_clone(m, false, NULL); + ScrMap *set = scr_map_clone(m, true, NULL); + all = scr_map_size(copy) == N / 4 && scr_map_size(set) == N / 4; + for (size_t i = 0; i < N; i += 4) { + all &= scr_map_get_str_ref(copy, keys[i]) == (void *)(uintptr_t)cref_val(i + N); + all &= scr_map_has_str(set, keys[i]); + } + all &= !scr_map_has_str(set, keys[1]); + check(all, "cref: clone and key-only clone"); + scr_map_set_str_ref(copy, keys[1], (void *)(uintptr_t)cref_val(1)); + check(!scr_map_has_str(m, keys[1]) && scr_map_size(copy) == N / 4 + 1, "cref: copies are independent"); + scr_map_clear(m); + check(scr_map_size(m) == 0 && !scr_map_has_str(m, keys[0]), "cref: clear"); + scr_map_set_str_ref(m, keys[3], (void *)(uintptr_t)cref_val(3)); + check(scr_map_get_str_ref(m, keys[3]) == (void *)(uintptr_t)cref_val(3), "cref: reuse after clear"); + scr_map_release(copy); + scr_map_release(set); + scr_map_release(m); + scr_str_release(missing); + for (size_t i = 0; i < N; i++) scr_str_release(keys[i]); +} + int main(void) { test_key_bytes(); test_string_bucket_collisions(); test_string_keys(); test_string_entries(); + test_compact_values(); test_same_value_zero(); test_span_keys(); test_scratch_keys(); diff --git a/packages/threads/package.json b/packages/threads/package.json index 2aeeb7cbb..11b8ef624 100644 --- a/packages/threads/package.json +++ b/packages/threads/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/threads", - "version": "0.4.1", + "version": "0.4.2", "description": "Zero-copy publication of immutable object graphs to worker threads (scriptc), with a deep-freezing Node.js implementation", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/types/package.json b/packages/types/package.json index ffbbdf4a4..689c7b72f 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/types", - "version": "0.4.1", + "version": "0.4.2", "description": "Contract types for scriptc: type-only annotations (Int32, NoThrow, Immutable) that scriptc checks and optimizes, with no runtime semantics", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2404c3885..589de2a51 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -55,7 +55,7 @@ importers: version: 5.9.3 vercel: specifier: 58.1.0 - version: 58.1.0(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(rollup@4.62.2) + version: 58.1.0(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(rollup@4.62.2) vitest: specifier: ^3.0.0 version: 3.2.7(patch_hash=03a418fae669c2cd9846dc3c49e6c1b8ee7047bbdcda9d4f5d5ee09eb525a1be)(@edge-runtime/vm@3.2.0)(@types/node@24.13.3)(tsx@4.23.0) @@ -110,8 +110,8 @@ importers: specifier: workspace:* version: link:../runtime tsc-ts: - specifier: 0.0.5 - version: 0.0.5 + specifier: 0.0.7 + version: 0.0.7 typescript: specifier: 7.0.2 version: 7.0.2 @@ -271,14 +271,14 @@ packages: effect: ^4.0.0 redis: '>=5.0.0 <7.0.0' - '@emnapi/core@2.0.0-alpha.3': - resolution: {integrity: sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==} + '@emnapi/core@1.11.3': + resolution: {integrity: sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==} - '@emnapi/runtime@2.0.0-alpha.3': - resolution: {integrity: sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==} + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} - '@emnapi/wasi-threads@2.0.1': - resolution: {integrity: sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ==} + '@emnapi/wasi-threads@1.2.3': + resolution: {integrity: sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==} '@esbuild/aix-ppc64@0.27.0': resolution: {integrity: sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==} @@ -1360,8 +1360,8 @@ packages: cpu: [x64] os: [win32] - '@scriptc/threads@0.3.6': - resolution: {integrity: sha512-XZb4JhXM/ivhx4MMLxqLLF2mCI28nUK+VVKlCnELO83tacUuGlq7YCe/8LwvzO7fZwy/rCH4+genAYYEc6BF6Q==} + '@scriptc/threads@0.4.1': + resolution: {integrity: sha512-wKgEBiVc07ZIGNFGwJpV5JbTXJN1N42qG6/g7ipnwt3klsU0nuWH2ToKV9vYH+RQstF7+twq6zMCf2qhUloTHQ==} engines: {node: '>=24.0.0'} '@sinclair/typebox@0.25.24': @@ -1377,42 +1377,42 @@ packages: '@ts-morph/common@0.11.1': resolution: {integrity: sha512-7hWZS0NRpEsNV8vWJzg7FEz6V8MaLNeJOmwmghqUXTpzk16V1LLZhdo+4QvE/+zv4cVci0OviuJFnqhEfoV3+g==} - '@tsc-ts/darwin-arm64@0.0.5': - resolution: {integrity: sha512-yP64b2VpAKWG92KepBqxs1P89wpwiheYwNMk4EmmhSl6aw/ImIBrdHYNjvao3sg6FHZCXTc+PB6Vukh6hyQiJQ==} + '@tsc-ts/darwin-arm64@0.0.7': + resolution: {integrity: sha512-nIaTJKBpG6n60xxRYfBlh02G6aSO2punIvj4qPy8/CXZu0Kw4ra/J3PYJtrtYMIh/eIGo4+f1Q01v5dFPPwZbw==} cpu: [arm64] os: [darwin] - '@tsc-ts/darwin-x64@0.0.5': - resolution: {integrity: sha512-CEeHOFfpx5H9V3WSq+V7XGU8tfc06nRN818auiRVwuwPHvpti9gHq0HYkvf/43PH1pIGcgf68sQYdIxJhrQDKQ==} + '@tsc-ts/darwin-x64@0.0.7': + resolution: {integrity: sha512-8ITDyF3FG4SYeS1cV4+oRpbbk0jVsfd23Ww138wbQymbOLYjWPafzj4bymxnIE5kEWjZPVqTFDbaYqgWwNStag==} cpu: [x64] os: [darwin] - '@tsc-ts/linux-arm64-gnu@0.0.5': - resolution: {integrity: sha512-qwybOqGDEa35eV9x/3WO81ExAUbUMY8288ATC83kUfYGmOAVStaO+JM2IdrsiI8hLWVC7/f0RFgZ+7dyWh9/nA==} + '@tsc-ts/linux-arm64-gnu@0.0.7': + resolution: {integrity: sha512-nXGjtbM3YrARWOb+nkDaFdhKGSzPf3ofl/gV4x8JnVQ8SgK4IWJ5J0u93sk3mTYgdLj/Gcobp/D3lZ6WIzjKvA==} cpu: [arm64] os: [linux] libc: [glibc] - '@tsc-ts/linux-arm64-musl@0.0.5': - resolution: {integrity: sha512-6oSw3NbQIrnxxCRuEe190MbMFoOrM+P+u1usascQMgSjsMcVRnJ/ztvTnRrCDtfPEHJ5EC8drgWIpcdrhGsB0w==} + '@tsc-ts/linux-arm64-musl@0.0.7': + resolution: {integrity: sha512-hSjpQsOhrLaDsbvEO5EH9nC3Vy0WAt3aB+GejytEh9fl4F3kT0BasbBP4wJqu6S5SG9upLW7ZClXiNVnGYDGlQ==} cpu: [arm64] os: [linux] libc: [musl] - '@tsc-ts/linux-x64-gnu@0.0.5': - resolution: {integrity: sha512-KrlBLUcG7M6p7lN+ke+kHVLqy7IiY/H0FxFZzq5slXanhrps/BfjdkUahsyREg7KVprr08xFuXqJTBtLEI4eow==} + '@tsc-ts/linux-x64-gnu@0.0.7': + resolution: {integrity: sha512-MCU2MFefQGG/++nepg09BtYnsN2mVzeYfIS6GW30ChxUt2nJUMkEvzbDim9czDevW4LRqHgfnwEt+hs2ud1SDQ==} cpu: [x64] os: [linux] libc: [glibc] - '@tsc-ts/linux-x64-musl@0.0.5': - resolution: {integrity: sha512-fr8yGzkocCcTYoE+NwC+/WGPNteQ2GmOPJTy/z9NcsLchnpSIZ1fzFll15UYPKQlwQdNRJXxKK3npVBtLRdHTg==} + '@tsc-ts/linux-x64-musl@0.0.7': + resolution: {integrity: sha512-abTZZkn74772MPABStuXWJ1GHI07nbEf4a3Nwl7NYI5mLFYxeLSbq6+cGh0BUJeBeMhFNsjtQ5/j5sGmsrJ2tg==} cpu: [x64] os: [linux] libc: [musl] - '@tsc-ts/win32-x64-msvc@0.0.5': - resolution: {integrity: sha512-SQwufJafzsiOBYncHOxYkyELWjLlBijcKB+xOs6hr6Lxxp5MMLoPtvDFNG88c7G5xv2TDb1XbvyxnjQltr6SHw==} + '@tsc-ts/win32-x64-msvc@0.0.7': + resolution: {integrity: sha512-EOhLTkuWYytJYklR5BI1jRtTpEIPh1FQJKD8R37qqlcnPrELlRO6Nc2ncDpdCoYbpN+KrewIVPB3xD4o3VGUaw==} cpu: [x64] os: [win32] @@ -2577,8 +2577,8 @@ packages: ts-toolbelt@6.15.5: resolution: {integrity: sha512-FZIXf1ksVyLcfr7M317jbB67XFJhOO1YqdTcuGaq9q5jLUoTikukZ+98TPjKiP2jC5CgmYdWWYs0s2nLSU0/1A==} - tsc-ts@0.0.5: - resolution: {integrity: sha512-Eea+zNv7y696pqNZtXBd0AX7/6PXIhR7aaEb0EhgpHadbk7VEiURD94/NngeLpYWts2tPjetkpwFLJY0+6yOnQ==} + tsc-ts@0.0.7: + resolution: {integrity: sha512-KRWTXRUvwMXi4At6xOWPDa/0BreY3MR2tPHooEMNWLZUA2eLphNe7PJiHclPLLn9RCnHY4ffDTTeF5hqgyBvtA==} engines: {node: '>=20'} hasBin: true @@ -2840,18 +2840,18 @@ snapshots: - bufferutil - utf-8-validate - '@emnapi/core@2.0.0-alpha.3': + '@emnapi/core@1.11.3': dependencies: - '@emnapi/wasi-threads': 2.0.1 + '@emnapi/wasi-threads': 1.2.3 tslib: 2.8.1 optional: true - '@emnapi/runtime@2.0.0-alpha.3': + '@emnapi/runtime@1.11.3': dependencies: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@2.0.1': + '@emnapi/wasi-threads@1.2.3': dependencies: tslib: 2.8.1 optional: true @@ -3090,10 +3090,10 @@ snapshots: '@napi-rs/keyring-win32-ia32-msvc': 1.2.0 '@napi-rs/keyring-win32-x64-msvc': 1.2.0 - '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)': + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': dependencies: - '@emnapi/core': 2.0.0-alpha.3 - '@emnapi/runtime': 2.0.0-alpha.3 + '@emnapi/core': 1.11.3 + '@emnapi/runtime': 1.11.3 '@tybys/wasm-util': 0.10.3 optional: true @@ -3159,9 +3159,9 @@ snapshots: '@oxc-transform/binding-openharmony-arm64@0.111.0': optional: true - '@oxc-transform/binding-wasm32-wasi@0.111.0(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)': + '@oxc-transform/binding-wasm32-wasi@0.111.0(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': dependencies: - '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3) + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -3340,9 +3340,9 @@ snapshots: '@rolldown/binding-openharmony-arm64@1.0.0-rc.1': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-rc.1(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)': + '@rolldown/binding-wasm32-wasi@1.0.0-rc.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': dependencies: - '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3) + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -3439,7 +3439,7 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.62.2': optional: true - '@scriptc/threads@0.3.6': {} + '@scriptc/threads@0.4.1': {} '@sinclair/typebox@0.25.24': {} @@ -3454,25 +3454,25 @@ snapshots: mkdirp: 1.0.4 path-browserify: 1.0.1 - '@tsc-ts/darwin-arm64@0.0.5': + '@tsc-ts/darwin-arm64@0.0.7': optional: true - '@tsc-ts/darwin-x64@0.0.5': + '@tsc-ts/darwin-x64@0.0.7': optional: true - '@tsc-ts/linux-arm64-gnu@0.0.5': + '@tsc-ts/linux-arm64-gnu@0.0.7': optional: true - '@tsc-ts/linux-arm64-musl@0.0.5': + '@tsc-ts/linux-arm64-musl@0.0.7': optional: true - '@tsc-ts/linux-x64-gnu@0.0.5': + '@tsc-ts/linux-x64-gnu@0.0.7': optional: true - '@tsc-ts/linux-x64-musl@0.0.5': + '@tsc-ts/linux-x64-musl@0.0.7': optional: true - '@tsc-ts/win32-x64-msvc@0.0.5': + '@tsc-ts/win32-x64-msvc@0.0.7': optional: true '@tybys/wasm-util@0.10.3': @@ -3563,7 +3563,7 @@ snapshots: '@typescript/typescript-win32-x64@7.0.2': optional: true - '@vercel/backends@0.8.27(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(rollup@4.62.2)': + '@vercel/backends@0.8.27(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(rollup@4.62.2)': dependencies: '@vercel/build-utils': 13.36.0 '@vercel/nft': 1.10.0(rollup@4.62.2) @@ -3571,10 +3571,10 @@ snapshots: execa: 3.2.0 fs-extra: 11.1.0 get-port: 5.1.1 - oxc-transform: 0.111.0(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3) + oxc-transform: 0.111.0(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) path-to-regexp: 8.3.0 resolve.exports: 2.0.3 - rolldown: 1.0.0-rc.1(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3) + rolldown: 1.0.0-rc.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) srvx: 0.11.16 ts-morph: 12.0.0 tsx: 4.21.0 @@ -3600,9 +3600,9 @@ snapshots: cjs-module-lexer: 1.2.3 es-module-lexer: 1.5.0 - '@vercel/cervel@0.1.35(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(rollup@4.62.2)': + '@vercel/cervel@0.1.35(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(rollup@4.62.2)': dependencies: - '@vercel/backends': 0.8.27(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(rollup@4.62.2) + '@vercel/backends': 0.8.27(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(rollup@4.62.2) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -3638,9 +3638,9 @@ snapshots: '@vercel/error-utils@2.2.0': {} - '@vercel/express@0.1.118(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(rollup@4.62.2)': + '@vercel/express@0.1.118(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(rollup@4.62.2)': dependencies: - '@vercel/cervel': 0.1.35(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(rollup@4.62.2) + '@vercel/cervel': 0.1.35(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(rollup@4.62.2) '@vercel/nft': 1.10.0(rollup@4.62.2) '@vercel/node': 5.9.0(rollup@4.62.2) '@vercel/static-config': 3.4.0 @@ -4442,7 +4442,7 @@ snapshots: os-paths@4.4.0: {} - oxc-transform@0.111.0(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3): + oxc-transform@0.111.0(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3): optionalDependencies: '@oxc-transform/binding-android-arm-eabi': 0.111.0 '@oxc-transform/binding-android-arm64': 0.111.0 @@ -4460,7 +4460,7 @@ snapshots: '@oxc-transform/binding-linux-x64-gnu': 0.111.0 '@oxc-transform/binding-linux-x64-musl': 0.111.0 '@oxc-transform/binding-openharmony-arm64': 0.111.0 - '@oxc-transform/binding-wasm32-wasi': 0.111.0(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3) + '@oxc-transform/binding-wasm32-wasi': 0.111.0(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) '@oxc-transform/binding-win32-arm64-msvc': 0.111.0 '@oxc-transform/binding-win32-ia32-msvc': 0.111.0 '@oxc-transform/binding-win32-x64-msvc': 0.111.0 @@ -4604,7 +4604,7 @@ snapshots: reusify@1.1.0: {} - rolldown@1.0.0-rc.1(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3): + rolldown@1.0.0-rc.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3): dependencies: '@oxc-project/types': 0.110.0 '@rolldown/pluginutils': 1.0.0-rc.1 @@ -4619,7 +4619,7 @@ snapshots: '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.1 '@rolldown/binding-linux-x64-musl': 1.0.0-rc.1 '@rolldown/binding-openharmony-arm64': 1.0.0-rc.1 - '@rolldown/binding-wasm32-wasi': 1.0.0-rc.1(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3) + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.1 '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.1 transitivePeerDependencies: @@ -4813,17 +4813,17 @@ snapshots: ts-toolbelt@6.15.5: {} - tsc-ts@0.0.5: + tsc-ts@0.0.7: dependencies: - '@scriptc/threads': 0.3.6 + '@scriptc/threads': 0.4.1 optionalDependencies: - '@tsc-ts/darwin-arm64': 0.0.5 - '@tsc-ts/darwin-x64': 0.0.5 - '@tsc-ts/linux-arm64-gnu': 0.0.5 - '@tsc-ts/linux-arm64-musl': 0.0.5 - '@tsc-ts/linux-x64-gnu': 0.0.5 - '@tsc-ts/linux-x64-musl': 0.0.5 - '@tsc-ts/win32-x64-msvc': 0.0.5 + '@tsc-ts/darwin-arm64': 0.0.7 + '@tsc-ts/darwin-x64': 0.0.7 + '@tsc-ts/linux-arm64-gnu': 0.0.7 + '@tsc-ts/linux-arm64-musl': 0.0.7 + '@tsc-ts/linux-x64-gnu': 0.0.7 + '@tsc-ts/linux-x64-musl': 0.0.7 + '@tsc-ts/win32-x64-msvc': 0.0.7 tslib@2.8.1: optional: true @@ -4894,9 +4894,9 @@ snapshots: dependencies: punycode: 2.3.1 - vercel@58.1.0(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(rollup@4.62.2): + vercel@58.1.0(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(rollup@4.62.2): dependencies: - '@vercel/backends': 0.8.27(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(rollup@4.62.2) + '@vercel/backends': 0.8.27(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(rollup@4.62.2) '@vercel/blob': 2.4.0 '@vercel/build-utils': 13.36.0 '@vercel/cli-auth': 0.3.1 @@ -4904,7 +4904,7 @@ snapshots: '@vercel/container': 0.1.0 '@vercel/detect-agent': 1.2.3 '@vercel/elysia': 0.1.104(rollup@4.62.2) - '@vercel/express': 0.1.118(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(rollup@4.62.2) + '@vercel/express': 0.1.118(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(rollup@4.62.2) '@vercel/fastify': 0.1.107(rollup@4.62.2) '@vercel/fun': 1.3.0 '@vercel/go': 3.10.2 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a3e992b0a..ceb1506e9 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -7,14 +7,15 @@ minimumReleaseAgeExclude: - "effect@4.0.0" - "@effect/platform-node@4.0.0" - "@effect/platform-node-shared@4.0.0" - - "@tsc-ts/darwin-arm64@0.0.5" - - "@tsc-ts/darwin-x64@0.0.5" - - "@tsc-ts/linux-arm64-gnu@0.0.5" - - "@tsc-ts/linux-arm64-musl@0.0.5" - - "@tsc-ts/linux-x64-gnu@0.0.5" - - "@tsc-ts/linux-x64-musl@0.0.5" - - "@tsc-ts/win32-x64-msvc@0.0.5" - - "tsc-ts@0.0.5" + - "@tsc-ts/darwin-arm64@0.0.7" + - "@tsc-ts/darwin-x64@0.0.7" + - "@tsc-ts/linux-arm64-gnu@0.0.7" + - "@tsc-ts/linux-arm64-musl@0.0.7" + - "@tsc-ts/linux-x64-gnu@0.0.7" + - "@tsc-ts/linux-x64-musl@0.0.7" + - "@tsc-ts/win32-x64-msvc@0.0.7" + - "tsc-ts@0.0.7" + - "@scriptc/threads@0.4.1" allowBuilds: esbuild: true overrides: diff --git a/scripts/gen-dyn-methods.mjs b/scripts/gen-dyn-methods.mjs new file mode 100644 index 000000000..fe4f81a90 --- /dev/null +++ b/scripts/gen-dyn-methods.mjs @@ -0,0 +1,101 @@ +#!/usr/bin/env node +/* Writes packages/runtime/src/scr_dyn_methods.h: the method-name ids the + * checked-dynamic dispatch in scr_dyn_invoke.c compares (DYN_M_), the + * names, and an open-addressed hash table mapping a name to its id. The ids + * are the DYN_M_ names scr_dyn_invoke.c uses, so the header follows the + * dispatch code. + * + * node scripts/gen-dyn-methods.mjs write the header + * node scripts/gen-dyn-methods.mjs --check fail when it is out of date */ +import { readFileSync, writeFileSync } from "node:fs"; + +const source = new URL("../packages/runtime/src/scr_dyn_invoke.c", import.meta.url); +const target = new URL("../packages/runtime/src/scr_dyn_methods.h", import.meta.url); +const SLOTS = 256; + +const used = new Set(); +for (const match of readFileSync(source, "utf8").matchAll(/\bDYN_M_([A-Za-z][A-Za-z0-9]*)\b/g)) + if (match[1] !== "NONE" && match[1] !== "COUNT") used.add(match[1]); +const names = [...used].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)); +if (names.length >= SLOTS / 2) throw new Error("too many method names for the hash table"); + +// The slot dyn_method_of in the header computes. +const slotOf = (name) => + (name.length * 7 + name.charCodeAt(0) * 31 + name.charCodeAt(name.length - 1)) & (SLOTS - 1); +const slots = new Array(SLOTS).fill(0); +names.forEach((name, i) => { + let slot = slotOf(name); + while (slots[slot] !== 0) slot = (slot + 1) & (SLOTS - 1); + slots[slot] = i + 1; +}); + +const rows = (values, width) => { + const out = []; + for (let i = 0; i < values.length; i += width) + out.push(` ${values.slice(i, i + width).join(", ")},`); + return out; +}; +const text = [ + "/* Generated by scripts/gen-dyn-methods.mjs from the DYN_M_ ids scr_dyn_invoke.c", + " * uses. Do not edit by hand. */", + "#ifndef SCR_DYN_METHODS_H", + "#define SCR_DYN_METHODS_H", + "", + "#include ", + "#include ", + "", + "enum {", + " DYN_M_NONE,", + ...names.map((name) => ` DYN_M_${name},`), + " DYN_M_COUNT", + "};", + "", + "static const char *const dyn_method_names[DYN_M_COUNT] = {", + " NULL,", + ...names.map((name) => ` ${JSON.stringify(name)},`), + "};", + "", + "static const unsigned char dyn_method_lengths[DYN_M_COUNT] = {", + ...rows([0, ...names.map((name) => name.length)], 16), + "};", + "", + `/* Each name's id at its slot (or the next free one); 0 ends a probe. */`, + `static const unsigned char dyn_method_slots[${SLOTS}] = {`, + ...rows(slots, 16), + "};", + "", + `#define DYN_METHOD_MAX_LENGTH ${Math.max(...names.map((name) => name.length))}`, + "", + "/* The id of a method name, compared by content; DYN_M_NONE for any other", + " * name. */", + "static inline int dyn_method_of(const char *m) {", + " size_t length = strlen(m);", + " if (length == 0 || length > DYN_METHOD_MAX_LENGTH) return DYN_M_NONE;", + " unsigned slot = (unsigned)(length * 7 + (unsigned char)m[0] * 31 +", + ` (unsigned char)m[length - 1]) & ${SLOTS - 1};`, + " for (;;) {", + " unsigned id = dyn_method_slots[slot];", + " if (id == 0) return DYN_M_NONE;", + " if (dyn_method_lengths[id] == length && memcmp(m, dyn_method_names[id], length) == 0) return (int)id;", + ` slot = (slot + 1) & ${SLOTS - 1};`, + " }", + "}", + "", + "#endif", + "", +].join("\n"); + +if (process.argv.includes("--check")) { + let current = ""; + try { + current = readFileSync(target, "utf8"); + } catch {} + if (current !== text) { + console.error( + "packages/runtime/src/scr_dyn_methods.h is out of date: run node scripts/gen-dyn-methods.mjs", + ); + process.exit(1); + } +} else { + writeFileSync(target, text); +} diff --git a/scripts/generate-ts7-ast-schema.mjs b/scripts/generate-ts7-ast-schema.mjs index 07af474de..3218c8962 100644 --- a/scripts/generate-ts7-ast-schema.mjs +++ b/scripts/generate-ts7-ast-schema.mjs @@ -282,6 +282,26 @@ const adaptedGetters = new Set([ "operand", "questionToken", ]); +// A getter selects its child's wire slot by kind directly: an integer +// switch, without astChildOrder's switch on the child's name. `answer` +// maps a slot to the returned expression; `missing` is returned for kinds +// without the child. +const slotCases = (name, answer, missing) => { + const kindsBySlot = new Map(); + for (const [kind, childNames] of Object.entries(protocol.childProperties)) { + const slot = childNames.indexOf(name); + if (slot < 0) continue; + if (!kindsBySlot.has(slot)) kindsBySlot.set(slot, []); + kindsBySlot.get(slot).push(kind); + } + const lines = [" switch (this.kind) {"]; + for (const [slot, kinds] of [...kindsBySlot].sort((a, b) => a[0] - b[0])) { + for (const kind of kinds) lines.push(` case ${kind}:`); + lines.push(` return ${answer(slot)};`); + } + lines.push(" default:", ` return ${missing};`, " }"); + return lines.join("\n"); +}; const getters = names .filter((name) => !adaptedGetters.has(name)) .map((name) => { @@ -292,12 +312,19 @@ const getters = names .replaceAll("RemoteNode", "AstNode"); const method = type.includes("AstNode |") && type.includes("AstNodeList") - ? "child" + ? "childAt" : type.includes("AstNodeList") - ? "childList" - : "childNode"; - return ` get ${name}(): ${type} {\n return this.${method}(${JSON.stringify(name)});\n }`; + ? "childListAt" + : "childNodeAt"; + return ` get ${name}(): ${type} {\n${slotCases(name, (slot) => `this.${method}(${slot})`, "undefined")}\n }`; }); +// The adapted getters read their raw child through the same slot switch. +const adaptedSlots = [...adaptedGetters] + .sort() + .map( + (name) => + ` private ${name}Slot(): number {\n${slotCases(name, (slot) => String(slot), "-1")}\n }`, + ); const nodeTarget = join(root, "packages/compiler/src/frontend/ts7/ast-node.ts"); const before = readFileSync(nodeTarget, "utf8"); if ( @@ -311,6 +338,7 @@ const after = before.replace( " // BEGIN GENERATED CHILD GETTERS", ` // Generated from typescript@${version}; run scripts/generate-ts7-ast-schema.mjs.`, ...getters, + ...adaptedSlots, " // END GENERATED CHILD GETTERS", ].join("\n"), ); diff --git a/scripts/zero-cost-compare.mjs b/scripts/zero-cost-compare.mjs index 3158de997..160e33438 100644 --- a/scripts/zero-cost-compare.mjs +++ b/scripts/zero-cost-compare.mjs @@ -394,7 +394,8 @@ export function dynamicTierSymbols(names) { * not yet traced to its typed root. bench/thread-slots, * bench/channel-messages and bench/worker-messages, added later, link them * through worker messages: - * node:worker_threads values are dyn objects. Every other bench workload is fully typed and must link none. The gate and + * node:worker_threads values are dyn objects. bench/dynamic-methods is + * untyped JavaScript. Every other bench workload is fully typed and must link none. The gate and * tests/harness/bench-dynamic-tier.test.ts enforce that, and the test also * fails when a listed workload stops linking them, so remove a workload here * when a leak fix makes its binary clean. */ @@ -408,6 +409,7 @@ export const DYNAMIC_WORKLOADS = new Set([ "bench/intern-table", "bench/thread-slots", "bench/channel-messages", + "bench/dynamic-methods", "bench/worker-messages", "bench/typed-array-scan-workers", "bench/log-summary", diff --git a/tests/corpus/4638-contract-program-lifetime-arrays.ts b/tests/corpus/4638-contract-program-lifetime-arrays.ts new file mode 100644 index 000000000..a50094f8b --- /dev/null +++ b/tests/corpus/4638-contract-program-lifetime-arrays.ts @@ -0,0 +1,117 @@ +import type { ProgramLifetime } from "@scriptc/types"; + +// Arrays of ProgramLifetime class instances: literals, growth, removal +// from both ends, splicing, slicing, concatenation, searching, sorting, +// filling, copying methods, holes, length changes, Sets and Maps built +// from them, arrays held in class fields, optional elements, inspection +// and JSON. Each operation sees the same instances, in the same order. + +class Sym implements ProgramLifetime { + readonly name: string; + readonly rank: number; + readonly decls: Sym[] = []; + parent: Sym | undefined = undefined; + constructor(name: string, rank: number) { + this.name = name; + this.rank = rank; + } +} + +class Scope implements ProgramLifetime { + readonly symbols: Sym[] = []; + readonly optional: (Sym | undefined)[] = []; + readonly nullable: (Sym | null)[] = []; +} + +const names = (xs: readonly (Sym | undefined | null)[]): string => + xs.map((s) => (s === undefined ? "u" : s === null ? "n" : s.name)).join(","); + +const a = new Sym("a", 3); +const b = new Sym("b", 1); +const c = new Sym("c", 2); +const d = new Sym("d", 5); +const e = new Sym("e", 4); + +// Literals, push, pop, shift and unshift. +const xs: Sym[] = [a, b, c]; +xs.push(d); +xs.push(e, a); +console.log(names(xs), xs.length); +console.log(xs.pop()?.name, xs.shift()?.name, names(xs)); +xs.unshift(e, d); +console.log(names(xs), xs[0] === e, xs.at(-1) === e); + +// Growth well past the inline slots, then reads by index. +const many: Sym[] = []; +for (let i = 0; i < 1000; i++) many.push(i % 2 === 0 ? a : new Sym(`s${i}`, i)); +let ranks = 0; +for (let i = 0; i < many.length; i++) ranks += many[i]!.rank; +console.log(many.length, ranks, many[999]!.name, many[998] === a); + +// Splice, slice, concat, spread and copying methods. +const ys = xs.slice(1, 4); +const removed = xs.splice(1, 2, a, b, c); +console.log(names(ys), names(removed), names(xs)); +const zs = [...ys, ...removed].concat(xs); +console.log(names(zs), zs.length); +console.log(names(zs.toReversed()), names(zs.with(0, c)), names(zs.toSpliced(1, 3))); + +// Searching by identity. +console.log(zs.indexOf(c), zs.lastIndexOf(c), zs.includes(d), zs.indexOf(new Sym("c", 2))); +console.log(zs.find((s) => s.rank > 3)?.name, zs.findIndex((s) => s === b), zs.some((s) => s === e)); +console.log(zs.every((s) => s.rank > 0), zs.filter((s) => s.rank % 2 === 1).length); + +// Sorting with comparators, reversing and filling. +const sorted = [...zs].sort((p, q) => p.rank - q.rank || p.name.localeCompare(q.name)); +console.log(names(sorted)); +console.log(names(zs.toSorted((p, q) => q.name.localeCompare(p.name)))); +sorted.reverse(); +console.log(names(sorted)); +const filled: Sym[] = new Array(5).fill(b); +filled.fill(c, 2, 4); +console.log(names(filled), filled.copyWithin(0, 3).map((s) => s.name).join("")); + +// Holes, length changes and iteration. +const holey: Sym[] = [a, b]; +holey[5] = c; +console.log(holey.length, 2 in holey, holey[5]?.name, holey.findIndex((s) => s === c)); +holey.length = 3; +console.log(holey.length, holey[5]); +let visited = ""; +for (const s of xs) visited += s.name; +xs.forEach((s, i) => { + visited += i; +}); +console.log(visited, names(xs.map((s) => s.parent ?? s))); + +// Arrays inside class instances, optional and nullable elements. +const scope = new Scope(); +for (const s of [a, b, c, d, e]) { + scope.symbols.push(s); + s.decls.push(s, a); + s.parent = scope.symbols[0]; +} +scope.optional.push(a, undefined, c); +scope.nullable.push(null, b, null); +scope.optional[3] = e; +console.log(names(scope.symbols), names(scope.optional), names(scope.nullable)); +console.log(scope.optional.indexOf(undefined), scope.nullable.indexOf(null), scope.optional.length); +console.log(c.decls.length, c.decls[1] === a, e.parent === a, names(e.decls)); + +// Sets and Maps built from arrays, and arrays built from them. +const set = new Set(zs); +console.log(set.size, set.has(d), names([...set])); +const byName = new Map(zs.map((s) => [s.name, s] as const)); +console.log(names([...byName.values()]), byName.get("b") === b); +const grouped = new Map(); +for (const s of many.slice(0, 10)) { + const list = grouped.get(s.rank % 3) ?? []; + list.push(s); + grouped.set(s.rank % 3, list); +} +console.log([...grouped.entries()].map(([k, v]) => `${k}:${v.length}`).join(" ")); + +// Inspection and JSON. +console.log(xs.slice(0, 2).map((s) => s.name)); +console.log(JSON.stringify(scope.nullable.map((s) => s?.rank ?? null))); +console.log(JSON.stringify([a, b].map((s) => ({ name: s.name, rank: s.rank })))); diff --git a/tests/corpus/4639-contract-program-lifetime-maps.ts b/tests/corpus/4639-contract-program-lifetime-maps.ts new file mode 100644 index 000000000..21f7da33c --- /dev/null +++ b/tests/corpus/4639-contract-program-lifetime-maps.ts @@ -0,0 +1,120 @@ +import type { ProgramLifetime } from "@scriptc/types"; + +// Maps from strings to ProgramLifetime class instances: get, set, has and +// delete, growth past the linear scan into bucket tables, deletion with +// compaction, clear, every iteration form, copies, Sets of keys and +// values, maps held in class fields, optional and nullable values, +// inspection, and a cast through unknown. Each read sees the same +// instance that was stored. + +class Sym implements ProgramLifetime { + readonly name: string; + readonly rank: number; + readonly members = new Map(); + constructor(name: string, rank: number) { + this.name = name; + this.rank = rank; + } +} + +class Scope implements ProgramLifetime { + readonly locals = new Map(); + readonly maybe = new Map(); + readonly nullable = new Map(); +} + +function show(m: ReadonlyMap): string { + return [...m].map(([k, v]) => `${k}=${v === undefined ? "u" : v === null ? "n" : v.name}`).join(","); +} + +const a = new Sym("a", 3); +const b = new Sym("b", 1); +const c = new Sym("c", 2); + +// get, set, has, delete, overwrite and size. +const m = new Map(); +m.set("x", a).set("y", b); +m.set("z", c); +console.log(show(m), m.size, m.get("y") === b, m.has("z"), m.has("w"), m.get("w")); +m.set("x", c); +console.log(show(m), m.get("x") === c, m.delete("y"), m.delete("y"), show(m)); +const seeded = new Map([ + ["p", a], + ["q", b], +]); +console.log(show(seeded), seeded.get("q")?.rank); + +// Growth into bucket tables, then deletes that compact the entries. +const big = new Map(); +const all: Sym[] = []; +for (let i = 0; i < 3000; i++) { + const s = new Sym(`s${i}`, i); + all.push(s); + big.set(s.name, s); +} +let same = 0; +for (let i = 0; i < 3000; i++) if (big.get(`s${i}`) === all[i]) same++; +console.log(big.size, same, big.get("s1234")?.rank, big.has("s3000")); +for (let i = 0; i < 3000; i++) if (i % 3 !== 0) big.delete(`s${i}`); +same = 0; +for (const [k, v] of big) if (v === all[Number(k.slice(1))] && v.rank % 3 === 0) same++; +console.log(big.size, same, big.get("s2997")?.name, big.get("s2998")); +for (let i = 0; i < 3000; i += 3) big.set(`s${i}`, all[(i + 1) % 3000]!); +console.log(big.get("s0")?.name, big.get("s2997")?.name, big.size); + +// Iteration forms. +const order: string[] = []; +m.forEach((v, k) => { + order.push(`${k}:${v.name}`); +}); +console.log(order.join(" ")); +console.log([...m.keys()].join(), [...m.values()].map((s) => s.name).join(), [...m.entries()].length); +console.log(Array.from(m.values(), (s) => s.rank).join(), Array.from(m).length); +for (const v of m.values()) v.members.set("owner", a); +console.log(c.members.get("owner") === a, a.members.size); + +// Mutation during iteration: entries added by the callback are visited. +const grow = new Map([["g0", a]]); +let visits = 0; +for (const [k] of grow) { + visits++; + if (visits < 4) grow.set(`${k}+`, b); +} +console.log(visits, show(grow)); + +// Copies, Sets of keys and values, clear. +const copy = new Map(m); +copy.set("extra", b); +console.log(show(m), show(copy), copy.get("x") === m.get("x")); +const keySet = new Set(m.keys()); +const valueSet = new Set(m.values()); +console.log([...keySet].join(), valueSet.has(c), valueSet.has(b), valueSet.size); +copy.clear(); +console.log(copy.size, copy.get("x"), show(m)); +copy.set("again", a); +console.log(show(copy)); + +// Maps in class fields, optional and nullable values. +const scope = new Scope(); +scope.locals.set("a", a); +scope.maybe.set("a", a).set("gone", undefined); +scope.nullable.set("b", b).set("none", null); +console.log(show(scope.locals), show(scope.maybe), show(scope.nullable)); +console.log(scope.maybe.has("gone"), scope.maybe.get("gone"), scope.nullable.get("none"), scope.nullable.get("b")?.name); +const v = scope.maybe.get("a"); +console.log(v === a, v?.rank); + +// Objects from entries, inspection and JSON. +const obj = Object.fromEntries(m); +console.log(Object.keys(obj).join(), obj["z"] === c); +console.log(new Map([["only", b]])); +console.log(JSON.stringify([...m.keys()]), JSON.stringify(Object.fromEntries([...m].map(([k, s]) => [k, s.rank])))); + +// Through unknown and back. +const u: unknown = m; +if (u instanceof Map) { + const back = u as Map; + const seen: string[] = []; + for (const [k, s] of back) seen.push(`${String(k)}:${(s as Sym).name}`); + console.log(back.size, seen.join(), back.get("x") === c); +} diff --git a/tests/corpus/4814-integer-or-nan-indexes.ts b/tests/corpus/4814-integer-or-nan-indexes.ts new file mode 100644 index 000000000..1bcfa3e02 --- /dev/null +++ b/tests/corpus/4814-integer-or-nan-indexes.ts @@ -0,0 +1,127 @@ +// Typed-array and array indexes that are exact integers or NaN: element +// reads that may miss, bound to locals or used inline, `charCodeAt` past +// either end, and sums of those. A NaN index reads undefined and a write +// to it is ignored; negative and too-large integers miss the same way. +function show(label: string, value: number | undefined): void { + console.log(label, value); +} + +const bytes = new Uint8Array([65, 67, 71, 84, 0, 255, 3]); +const signed = new Int8Array([-1, 2, -128, 127, 5]); +const wide = new Uint32Array([1, 4294967295, 2, 7]); +const halves = new Int16Array([-300, 4, 6]); +const table = new Int32Array(256); +for (let i = 0; i < 256; i++) table[i] = i * 3 - 100; +const list: number[] = []; +for (let i = 0; i < 300; i++) list.push(i * 0.5); + +function lookups(n: number): number { + let acc = 0; + for (let i = 0; i < n; i++) { + const c = bytes[i]!; + const t = table[c]; + acc = (acc * 7 + (t === undefined ? -1 : t)) % 1000003; + } + return acc; +} + +function listLookups(n: number): number { + let acc = 0; + for (let i = 0; i < n; i++) { + const c = bytes[i]!; + const v = list[c]; + acc = acc * 3 + (v === undefined ? -2 : v); + } + return acc; +} + +function signedIndexes(): string { + const out: string[] = []; + for (let i = -1; i <= signed.length; i++) { + const s = signed[i]!; + out.push(String(table[s]), String(list[s + 2]), String(bytes[s - 1])); + } + return out.join(","); +} + +function wideIndexes(): string { + const out: string[] = []; + for (let i = 0; i <= wide.length; i++) { + const w = wide[i]!; + out.push(String(bytes[w]), String(list[w]), String(table[w - 1])); + } + for (let i = 0; i <= halves.length; i++) { + const h = halves[i]!; + out.push(String(table[h + 301]), String(list[h])); + } + return out.join(","); +} + +function codeUnits(s: string): number { + let acc = 0; + for (let i = -2; i < s.length + 2; i++) { + const u = s.charCodeAt(i); + const t = table[u]; + acc = acc * 5 + (t === undefined ? 1 : t); + acc %= 1000003; + } + return acc; +} + +function writes(n: number): string { + const counts = new Uint16Array(8); + const tally: number[] = [0, 0, 0, 0]; + for (let i = 0; i < n; i++) { + const c = bytes[i]!; + counts[c & 7]++; + counts[c - 60] = 9; + tally[c - 65] = (tally[c - 65] ?? 0) + 1; + } + return `${Array.from(counts).join(" ")} | ${tally.join(" ")} | ${tally.length}`; +} + +function reassigned(n: number): string { + const out: number[] = []; + for (let i = 0; i < n; i++) { + let c = bytes[i]!; + if (c > 200) c = signed[i - 5]!; + out.push(table[c] ?? -7); + // A fraction makes `f` an ordinary number index. + let f = bytes[i]!; + if (i === 2) f = f / 2; + out.push(list[f] ?? -8); + } + return out.join(" "); +} + +function growing(n: number): number { + let x = bytes[0]!; + let acc = 0; + for (let i = 0; i < n; i++) { + x = x + bytes[i]!; + acc = acc + (table[x] ?? -3); + } + return acc; +} + +function hashes(n: number): string { + let h = 0; + let k = 0; + for (let i = 0; i < n; i++) { + const c = bytes[i]!; + h = (h * 31 + c) | 0; + k = ((k << 2) | (c >> 1)) & 0xffff; + } + return `${h} ${k}`; +} + +show("lookups", lookups(bytes.length)); +show("lookups past the end", lookups(bytes.length + 3)); +show("list lookups", listLookups(bytes.length + 2)); +console.log("signed", signedIndexes()); +console.log("wide", wideIndexes()); +show("code units", codeUnits("ACGTÿĀ")); +console.log("writes", writes(bytes.length + 2)); +console.log("reassigned", reassigned(bytes.length + 1)); +show("growing", growing(bytes.length + 1)); +console.log("hashes", hashes(bytes.length + 2)); diff --git a/tests/corpus/4815-touint32-wide-magnitudes.ts b/tests/corpus/4815-touint32-wide-magnitudes.ts new file mode 100644 index 000000000..ab60b8c0f --- /dev/null +++ b/tests/corpus/4815-touint32-wide-magnitudes.ts @@ -0,0 +1,79 @@ +// ToInt32 and ToUint32 (`| 0`, `>>> 0`, `^`, `<<`, `>>`) of doubles around +// 2^53 and 2^63, of multiplicative hash products that pass 2^53, and of +// NaN, the infinities and -0. +const values: number[] = [ + 2 ** 53 - 1, + 2 ** 53, + 2 ** 53 + 2, + -(2 ** 53) - 2, + 2 ** 53 * 3 + 4, + 2 ** 62, + 2 ** 62 + 2 ** 40 + 5 * 2 ** 10, + 2 ** 63 - 1024, + -(2 ** 63) + 1024, + 2 ** 63, + -(2 ** 63), + 2 ** 63 + 2048, + 2 ** 64, + 2 ** 64 - 2048, + -(2 ** 64) + 4096, + 2 ** 80 + 2 ** 33, + 1.5e300, + Infinity, + -Infinity, + NaN, + -0, + 0.75, + -2.5, + 4294967295.5, +]; + +function opaque(i: number): number { + return values[i % values.length]!; +} + +for (let i = 0; i < values.length; i++) { + const v = opaque(i); + console.log( + String(v), + v | 0, + v >>> 0, + v ^ 0x5a5a5a5a, + (v << 3) | 0, + v >> 7, + v >>> 9, + ); +} + +function fnv(bytes: Uint8Array): number { + let h = 2166136261; + for (let i = 0; i < bytes.length; i++) h = ((h ^ bytes[i]!) * 16777619) >>> 0; + return h; +} + +function mix(bytes: Uint8Array): number { + let h = 0; + for (let i = 0; i < bytes.length; i++) h = ((h ^ (bytes[i]! - 64)) * 16777619) | 0; + return h; +} + +function poly(bytes: Uint8Array): number { + let h = 7; + for (let i = 0; i < bytes.length; i++) h = (h * 1000003 + bytes[i]!) | 0; + return h; +} + +function wide(bytes: Uint8Array): number { + // Products near 2^63 and past it: the conversion of each must match. + let h = 3; + for (let i = 0; i < bytes.length; i++) h = ((h ^ bytes[i]!) * 2147483647 * (bytes[i]! * 17 + 1)) | 0; + return h; +} + +const bytes = new Uint8Array(4096); +let seed = 12345; +for (let i = 0; i < bytes.length; i++) { + seed = (seed * 1103515245 + 12345) % 2147483648; + bytes[i] = seed >>> 23; +} +console.log("fnv", fnv(bytes), "mix", mix(bytes), "poly", poly(bytes), "wide", wide(bytes)); diff --git a/tests/corpus/6915-key-order-narrowed-throw.ts b/tests/corpus/6915-key-order-narrowed-throw.ts new file mode 100644 index 000000000..d4bc693f2 --- /dev/null +++ b/tests/corpus/6915-key-order-narrowed-throw.ts @@ -0,0 +1,72 @@ +// A record whose reads narrow a possibly-undefined binding (the narrowing +// ternary's throw arm stands in for a value of the record's type) still +// takes its literal's key order: a module-level accumulator typed by an +// interface declared in another order, as an event batcher builds it. +interface Entry { + id: string; + at: number; + text?: string; + stream: "out" | "err"; +} +interface Batch { + team: string; + project: string; + job: string; + origin: "task" | "worker"; + host: string; + entries: Entry[]; + sizes?: number[]; + stage?: "live" | "staging"; + branch?: string; + run?: string; + entry?: string; + path?: string; +} +interface Job { + id: string; + run?: string; + project: string; + team: string; + host: string; + stage: string | null; +} +let acc: Batch | undefined; + +function append(j: Job, text: string): void { + if (!acc) { + acc = { + run: j.run, + job: j.id, + stage: j.stage === null ? "staging" : "live", + project: j.project, + team: j.team, + origin: "task", + host: j.host, + entry: ".", + entries: [], + }; + } + acc.entries.push({ id: String(acc.entries.length), at: 1, text, stream: "out" }); + setTimeout(() => { + const batch = acc; + if (!batch) return; + acc = undefined; + for (const line of split(batch)) console.log(line); + }, 1); +} + +function split(batch: Batch): string[] { + const json = JSON.stringify(batch); + if (json.length < 200 || batch.entries.length < 2) return [json]; + const index = Math.floor(batch.entries.length / 2); + const first = { ...batch, entries: batch.entries.slice(0, index) }; + const second = { ...batch, entries: batch.entries.slice(index) }; + return [...split(first), ...split(second)]; +} + +const j: Job = { id: "job", run: "r1", project: "prj", team: "core", host: "h", stage: "live" }; +append(j, "one"); +append(j, "two"); +setTimeout(() => { + for (let i = 0; i < 6; i++) append(j, "line " + i); +}, 5); diff --git a/tests/corpus/6916-index-record-spread-overrides.ts b/tests/corpus/6916-index-record-spread-overrides.ts new file mode 100644 index 000000000..0a33b0e1b --- /dev/null +++ b/tests/corpus/6916-index-record-spread-overrides.ts @@ -0,0 +1,60 @@ +// Spreading a fixed-shape record into an index-signature record: the +// spread's keys enter in declaration order, and a field whose type does +// not fit the index slot is valid when a later property overrides it +// (its key keeps the spread's position, the override supplies the value). +interface FailureTags { + phase: string; + httpStatus: number; + retried: boolean; + region: string; +} +interface Counts { + zeta: number; + alpha: number; + mid: number; +} + +function emit(name: string, tags: { [key: string]: string }): void { + const parts: string[] = []; + for (const key of Object.keys(tags)) parts.push(key + ":" + tags[key]); + console.log(name, parts.join(","), JSON.stringify(tags)); +} + +const tags: FailureTags = { phase: "fetch", httpStatus: 503, retried: true, region: "iad1" }; +for (let attempt = 1; attempt <= 2; attempt++) { + emit("attempt_failed", { + ...tags, + attempt: String(attempt), + httpStatus: String(tags.httpStatus), + retried: tags.retried ? "yes" : "no", + }); +} + +const base: Record = { team: "core", owner: "infra" }; +emit("queue", { ...base, owner: "builds", shard: "7" }); +emit("size", { ...base }); + +const counts: Counts = { zeta: 3, alpha: 1, mid: 2 }; +const scores: Record = { ...counts, alpha: 10 }; +console.log(Object.keys(scores).join(","), JSON.stringify(scores)); +const labels: Record = { first: "a", ...{ zeta: "z", alpha: "a" }, last: "l" }; +console.log(Object.entries(labels).map(([k, v]) => k + "=" + v).join(" ")); +const flags: { [key: string]: boolean } = { ...counts, mid: true, zeta: false, alpha: true }; +console.log(JSON.stringify(flags)); + +// A source whose values are built in another order than its interface +// declares: the spread follows the order the values are built in. +interface Span { + start: number; + end: number; + label?: string; +} +function span(start: number, size: number, label?: string): Span { + return label === undefined ? { end: start + size, start } : { label, end: start + size, start }; +} +const spans: Record = { ...span(1, 4), step: 2 }; +console.log(Object.keys(spans).join(","), JSON.stringify(spans)); +const named: Record = { first: 0, ...span(5, 2, "w"), last: 9 }; +console.log(Object.keys(named).join(","), JSON.stringify(named)); +const both: Record = { ...counts, ...{ end: 3, start: 1 }, mid: 7 }; +console.log(Object.keys(both).join(","), JSON.stringify(both)); diff --git a/tests/corpus/6926-dynamic-collection-seeds.mjs b/tests/corpus/6926-dynamic-collection-seeds.mjs new file mode 100644 index 000000000..a48fab894 --- /dev/null +++ b/tests/corpus/6926-dynamic-collection-seeds.mjs @@ -0,0 +1,28 @@ +// Set, Map, WeakSet and Object.fromEntries over checked-dynamic iterables: +// native iterators, generators, objects with a Symbol.iterator method and +// collections, as seeds that reach the runtime untyped. +function* pairs() { yield ["g", 1]; yield ["h", 2]; } +function* values() { yield 1; yield 2; yield 2; } +const iterable = { *[Symbol.iterator]() { yield ["i", 3]; } }; +const valueIterable = { *[Symbol.iterator]() { yield "a"; yield "b"; } }; +function attempt(label, make) { + try { console.log(label, make()); } catch (e) { console.log(label, "threw", e.name, e.message); } +} +function viaSet(x) { return new Set(x).size; } +function viaMap(x) { return [...new Map(x).keys()].join(); } +function viaWeakSet(x) { return new WeakSet(x).has(key); } +const key = {}; +const m = new Map([["a", 1], ["b", 2]]); +attempt("set keys()", () => viaSet(m.keys())); +attempt("set values()", () => viaSet(new Set([1, 2]).values())); +attempt("set generator", () => viaSet(values())); +attempt("set iterable", () => viaSet(valueIterable)); +attempt("set string", () => viaSet("abca")); +attempt("set number", () => viaSet(5)); +attempt("map generator", () => viaMap(pairs())); +attempt("map iterable", () => viaMap(iterable)); +attempt("map set", () => viaMap(new Set([["s", 1]]))); +attempt("map entries()", () => viaMap(m.entries())); +attempt("map bad entry", () => viaMap([1])); +attempt("weakset generator", () => viaWeakSet((function* () { yield key; })())); +attempt("fromEntries generator", () => JSON.stringify(Object.fromEntries(pairs()))); diff --git a/tests/corpus/6927-dynamic-collection-seeds.ts b/tests/corpus/6927-dynamic-collection-seeds.ts new file mode 100644 index 000000000..c57433d57 --- /dev/null +++ b/tests/corpus/6927-dynamic-collection-seeds.ts @@ -0,0 +1,9 @@ +// Set and Map seeds held as any: native iterators and generators. +function* values(): Generator { yield 1; yield 2; yield 2; } +function* pairs(): Generator<[string, number]> { yield ["a", 1]; yield ["b", 2]; } +const m = new Map([["x", 1], ["y", 2]]); +const anyKeys: any = m.keys(); +const anyGen: any = values(); +const anyPairs: any = pairs(); +console.log(new Set(anyKeys).size, new Set(anyGen).size, new Map(anyPairs).size); +console.log(new Set(values()).size, new Map(pairs()).get("b"), new Set(m.values()).size); diff --git a/tests/corpus/6998-typed-array-while-loop-versions.ts b/tests/corpus/6998-typed-array-while-loop-versions.ts new file mode 100644 index 000000000..383a2437f --- /dev/null +++ b/tests/corpus/6998-typed-array-while-loop-versions.ts @@ -0,0 +1,149 @@ +// While loops that scan typed arrays with a counter below a bound: scanners +// with nested loops and `&&` conditions, `| 0` increments, look-behind and +// look-ahead reads, writes, DataView reads, string-length bounds, and +// bounds or starts past the array, fractional, negative, -0 or NaN. + +function words(bytes: Uint8Array, end: number): number { + let pos = 0; + let count = 0; + while (pos < end) { + while (pos < end && (bytes[pos] === 32 || bytes[pos] === 10)) pos++; + if (pos < end) count++; + while (pos < end && bytes[pos] !== 32 && bytes[pos] !== 10) pos++; + } + return count; +} + +function identifiers(chars: Uint16Array, start: number, end: number): string { + let pos = start; + let count = 0; + let mix = 0; + while (pos < end) { + let hash = 0x811c9dc5 | 0; + while (pos < end) { + const c = chars[pos]!; + if (!((c >= 97 && c <= 122) || c === 95)) break; + hash = Math.imul(hash ^ c, 0x01000193); + pos = (pos + 1) | 0; + } + mix = (mix + hash) | 0; + count++; + pos = (pos + 1) | 0; + } + return `${count} ${mix >>> 0} ${pos}`; +} + +function around(values: Int16Array, start: number, end: number): number[] { + const out: number[] = []; + let i = start; + while (i <= end) { + out.push(values[i - 1]! + values[i + 2]! * 2); + i += 1; + } + return out; +} + +function fill(out: Float32Array, end: number, value: number): number { + let i = 0; + while (i < end) { + out[i] = value + i; + if (i < end - 1 && out[i] > 2) { + i += 2; + continue; + } + i++; + } + return i; +} + +function afterInner(bytes: Uint8Array): number { + let pos = 0; + let sum = 0; + while (pos < bytes.length) { + while (pos < bytes.length && bytes[pos] !== 0) pos++; + // The inner loop may stop at the length: this read stays checked. + sum += bytes[pos] ?? -1; + pos++; + } + return sum; +} + +function firstAbove(samples: Float64Array, limit: number): number { + let i = 0; + while (i < samples.length) { + if (samples[i]! > limit) return i; + i = i + 1; + } + return -1; +} + +function halves(view: DataView, end: number): number { + let at = 0; + let total = 0; + while (at < end) { + total += view.getUint16(at, true); + at += 2; + } + return total; +} + +function letters(text: string, codes: Uint8Array): number { + let i = 0; + let sum = 0; + while (i < text.length) { + sum += codes[i]! * (text.charCodeAt(i) & 7); + i++; + } + return sum; +} + +function notCounters(bytes: Uint8Array): string { + let i = 1; + let doubled = 0; + while (i < bytes.length) { + doubled += bytes[i]!; + i = i * 2; + } + let j = bytes.length - 1; + let down = 0; + while (j < bytes.length && j >= 0) { + down += bytes[j]!; + j--; + } + return `${doubled} ${down}`; +} + +const text = "let alpha = beta_gamma\nreturn delta "; +const bytes = new Uint8Array(text.length); +const chars = new Uint16Array(text.length); +for (let i = 0; i < text.length; i++) { + bytes[i] = text.charCodeAt(i); + chars[i] = text.charCodeAt(i); +} +console.log("words", words(bytes, bytes.length), words(bytes, 10), words(bytes, bytes.length + 5), words(bytes, 7.5), words(bytes, NaN)); +console.log("identifiers", identifiers(chars, 0, chars.length)); +console.log("identifiers past end", identifiers(chars, 4, chars.length + 3)); +console.log("identifiers starts", identifiers(chars, -2, 9), identifiers(chars, 2.5, 12), identifiers(chars, -0, 6)); + +const values = new Int16Array([5, -6, 7, -8, 9, -10, 11]); +console.log("around", around(values, 1, 4).join(","), around(values, 0, 6).join(","), around(values, -1, 1).join(",")); + +const floats = new Float32Array(6); +console.log("fill", fill(floats, 6, 0.5), floats.join(",")); +const short = new Float32Array(3); +console.log("fill past end", fill(short, 5, 1), short.join(",")); + +console.log("after inner", afterInner(new Uint8Array([1, 2, 0, 3, 0, 4, 5]))); +console.log("first above", firstAbove(new Float64Array([0.5, 1.5, 2.5]), 1), firstAbove(new Float64Array([0.5]), 1)); + +const buffer = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); +const view = new DataView(buffer.buffer); +console.log("halves", halves(view, 8)); +try { + console.log("halves past end", halves(view, 10)); +} catch (error) { + console.log((error as Error).name, (error as Error).message); +} + +console.log("letters", letters("abcdef", new Uint8Array([1, 2, 3, 4, 5, 6])), letters("abcdefgh", new Uint8Array([1, 2, 3]))); +console.log("not counters", notCounters(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]))); diff --git a/tests/corpus/7002-empty-array-contextual-slots/main.ts b/tests/corpus/7002-empty-array-contextual-slots/main.ts new file mode 100644 index 000000000..ec08a297a --- /dev/null +++ b/tests/corpus/7002-empty-array-contextual-slots/main.ts @@ -0,0 +1,58 @@ +// Empty array literals whose own type is `never[]`: `return []` in an async +// function (its contextual type is `string[] | PromiseLike`) and +// an object literal's `files: []` field returned or passed where typed +// arrays are declared. Each value holds the declared element type. +interface Listing { + files: string[]; + sizes: number[]; +} + +async function readNames(text: string): Promise { + try { + return JSON.parse(text) as string[]; + } catch { + return []; + } +} + +const loadIds = async (fail: boolean): Promise => { + if (fail) return []; + return [1, 2]; +}; + +function emptyListing(): Listing { + const listing = { files: [], sizes: [] }; + return listing; +} + +function index(): Record<"files" | "dirs", string[]> { + const out = { files: [], dirs: [] }; + return out; +} + +function total(listing: Listing): number { + return listing.sizes.reduce((sum, size) => sum + size, 0); +} + +async function main(): Promise { + const names = await readNames("not json"); + names.push("fallback"); + console.log(names, (await readNames('["a","b"]')).join("+")); + const ids = await loadIds(true); + ids.push(7); + console.log(ids, await loadIds(false)); + + const listing = emptyListing(); + listing.files.push("a.txt"); + listing.sizes.push(3, 4); + console.log(listing, total(listing)); + + const dirs = index(); + dirs.dirs.push("src"); + console.log(JSON.stringify(dirs), dirs.files.length); + + const blank = { files: [], sizes: [] }; + console.log(total(blank), JSON.stringify(blank)); +} + +main(); diff --git a/tests/corpus/7005-dyn-constructor-instanceof/main.ts b/tests/corpus/7005-dyn-constructor-instanceof/main.ts new file mode 100644 index 000000000..21fcbbd26 --- /dev/null +++ b/tests/corpus/7005-dyn-constructor-instanceof/main.ts @@ -0,0 +1,12 @@ +// A package's own functions, read off an object the checker cannot type, +// on the right of `instanceof`: Node walks the value's prototype chain for +// an ES5 constructor's prototype, at module load and in calls, and throws a +// TypeError for an arrow function, which has no prototype, when the value +// is an object. +import { arrowCheck, arrowObjects, describe, rejects, tags } from "tag-types"; + +console.log(tags()); +console.log(describe("{}"), describe("[1]"), describe("null"), describe('"s"')); +console.log(rejects('{"tag":"!x"}')); +console.log(arrowCheck("{}"), arrowCheck("1")); +console.log(arrowObjects()); diff --git a/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/index.d.ts b/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/index.d.ts new file mode 100644 index 000000000..5b451adfa --- /dev/null +++ b/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/index.d.ts @@ -0,0 +1,5 @@ +export declare function describe(json: string): string; +export declare function tags(): string; +export declare function rejects(json: string): string; +export declare function arrowCheck(json: string): string; +export declare function arrowObjects(): string; diff --git a/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/index.js b/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/index.js new file mode 100644 index 000000000..81165bd58 --- /dev/null +++ b/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/index.js @@ -0,0 +1,71 @@ +"use strict"; +// A bundler's lazy module object holding an ES5 constructor: the checker +// cannot type the object, so the constructor is an untyped value where +// `instanceof` tests it. +var types = {}; +var hasTypes; +function requireTypes() { + if (hasTypes) return types; + hasTypes = 1; + function Type(tag, options) { + this.tag = tag; + this.kind = (options && options.kind) || "scalar"; + } + function Schema(list) { + this.list = list; + } + types.Type = Type; + types.Schema = Schema; + types.make = (tag) => new Type(tag); + return types; +} +var loaded = requireTypes(); +var Type = loaded.Type; +var Schema = loaded.Schema; +var make = loaded.make; + +function compileList(list) { + var tags = []; + list.forEach(function (item) { + if (!(item instanceof Type)) throw new Error("not a Type: " + JSON.stringify(item)); + tags.push(item.tag + ":" + item.kind); + }); + return tags; +} +// Runs when the module loads, as js-yaml's default schema does. +var defaults = new Schema(compileList([new Type("!str"), new Type("!seq", { kind: "sequence" })])); + +module.exports.describe = function (json) { + var value = JSON.parse(json); + return [value instanceof Type, value instanceof Schema, defaults instanceof Schema, defaults instanceof Type].join(","); +}; +module.exports.tags = function () { + return defaults.list.join(" "); +}; +function rejects(json) { + try { + compileList([JSON.parse(json)]); + return "accepted"; + } catch (error) { + return error.message; + } +} +module.exports.rejects = rejects; +// An arrow function has no prototype: Node throws a TypeError for an object. +module.exports.arrowCheck = function (json) { + try { + return String(JSON.parse(json) instanceof make); + } catch (error) { + return error.name + ": " + error.message; + } +}; +// Every object reaches the TypeError, typed arrays, promises and Maps too. +module.exports.arrowObjects = function () { + return [new Uint8Array(2), Promise.resolve(1), new Map(), [1]].map(function (value) { + try { + return String(value instanceof make); + } catch (error) { + return error.name; + } + }).join(","); +}; diff --git a/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/package.json b/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/package.json new file mode 100644 index 000000000..a469a45f3 --- /dev/null +++ b/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/package.json @@ -0,0 +1 @@ +{ "name": "tag-types", "version": "1.0.0", "main": "./index.js", "types": "./index.d.ts" } diff --git a/tests/corpus/7006-evolving-array-aliases.ts b/tests/corpus/7006-evolving-array-aliases.ts new file mode 100644 index 000000000..215b3f3c5 --- /dev/null +++ b/tests/corpus/7006-evolving-array-aliases.ts @@ -0,0 +1,79 @@ +// An unannotated `const xs = []` inside a function whose writes store values +// of more than one element type: an alias taken between the writes +// (`const alias = xs`) and an alias of that alias name the same array, so +// they see every later write, as in Node. A write of another element type +// after reads that agree on one type also keeps the array untyped. +function chain(): string { + const xs = []; + xs.push(1); + const a = xs; + const b = a; + xs.push("two"); + b.push(3); + return `${xs.join(",")} ${a.length} ${b.length} ${a === xs} ${b === xs}`; +} +function viaAlias(): string { + const xs = []; + xs.push("a"); + const alias = xs; + alias.push("b"); + xs.push(2); + return `${xs.join(",")} ${alias.join(",")}`; +} +function letAlias(): string { + const xs = []; + xs.push(true); + let alias = xs; + xs.push("t"); + const seen = alias.length; + alias = []; + return `${seen} ${alias.length} ${xs.length}`; +} +function nested(flag: boolean): string { + const xs = []; + xs.push(1); + if (flag) { + const inner = xs; + xs.push("n"); + return `${inner.length} ${inner.join(":")}`; + } + return `${xs.length}`; +} +function elementWrite(): string { + const xs = []; + xs.push(1); + const alias = xs; + xs[1] = "one"; + return `${alias.length} ${alias.join("+")}`; +} +function agreeing(): string { + const xs = []; + xs.push(1); + const alias = xs; + xs.push(2); + return `${alias.length} ${alias === xs} ${alias.reduce((sum, v) => sum + v, 0)}`; +} +function total(values: number[]): number { + return values.length; +} +function passed(): string { + const xs = []; + xs.push(1); + const alias = xs; + const n = total(alias); + xs.push("z"); + return `${n} ${alias.length}`; +} +console.log(chain()); +console.log(viaAlias(), "|", letAlias()); +console.log(nested(true), "|", nested(false), "|", elementWrite()); +console.log(agreeing(), "|", passed()); + +function spreadSame(): number { + const xs = []; + xs.push(1); + const more = [2, 3]; + xs.push(...more); + return xs.reduce((sum, v) => sum + v, 0); +} +console.log(spreadSame()); diff --git a/tests/corpus/7007-filter-written-type-predicates.ts b/tests/corpus/7007-filter-written-type-predicates.ts new file mode 100644 index 000000000..b75b5727d --- /dev/null +++ b/tests/corpus/7007-filter-written-type-predicates.ts @@ -0,0 +1,108 @@ +// `.filter` with a written type predicate: an annotated callback +// (`(p): p is Cat => ...`) or a guard function passed as a value. The +// result holds the predicate's type: a union arm, a union of arms, a +// number out of `unknown`, a record or tuple with a narrower field, or a +// subclass. An inferred predicate that keeps several arms narrows too. +interface Cat { + kind: "cat"; + meow: string; +} +interface Dog { + kind: "dog"; + bark: number; +} +interface Fish { + kind: "fish"; + fins: number; +} +type Pet = Cat | Dog | Fish; +const pets: Pet[] = [ + { kind: "cat", meow: "m" }, + { kind: "dog", bark: 2 }, + { kind: "fish", fins: 3 }, + { kind: "dog", bark: 5 }, +]; + +const cats = pets.filter((p): p is Cat => p.kind === "cat"); +console.log(cats.length, cats[0]!.meow); +const furry = pets.filter((p): p is Cat | Dog => p.kind !== "fish"); +console.log(furry.map((p) => (p.kind === "cat" ? p.meow : p.bark)).join(",")); +const notFish = pets.filter((p) => p.kind !== "fish"); +console.log(notFish.length); + +function isDog(p: Pet): p is Dog { + return p.kind === "dog"; +} +console.log(pets.filter(isDog).map((d) => d.bark * 10).join(",")); + +const isNumber = (v: unknown): v is number => typeof v === "number"; +const values: unknown[] = JSON.parse('[1, "a", 2, null, 4]'); +console.log(values.filter(isNumber).reduce((sum, n) => sum + n, 0)); + +const rows: unknown[] = JSON.parse('[{"id":1},{"id":"x"},{"id":3}]'); +const withIds = rows.filter( + (r): r is { id: number } => typeof (r as { id: unknown }).id === "number", +); +console.log(withIds.map((r) => r.id + 1).join(",")); + +const entries: [string, unknown][] = [ + ["a", 1], + ["b", "x"], + ["c", 3], +]; +const numeric = entries.filter((e): e is [string, number] => typeof e[1] === "number"); +console.log(numeric.map(([key, value]) => `${key}${value * 2}`).join(" ")); + +class Shape { + name: string; + constructor(name: string) { + this.name = name; + } +} +class Circle extends Shape { + r: number; + constructor(r: number) { + super("circle"); + this.r = r; + } +} +const shapes: Shape[] = [new Shape("square"), new Circle(2), new Circle(3)]; +const circles = shapes.filter((s): s is Circle => s instanceof Circle); +console.log(circles.length, circles.map((c) => c.r).join("+")); + +const mixed: (string | number | boolean)[] = ["a", 1, true, "b"]; +console.log(mixed.filter((v): v is string => typeof v === "string").join("")); +console.log(mixed.filter((v): v is string | number => typeof v !== "boolean").length); + +const pair: [string | number, string | number] = ["x", 4]; +console.log(pair.filter((v): v is number => typeof v === "number").map((n) => n + 1).join()); + +const optional: (string | number | undefined)[] = ["a", 1, undefined, 2]; +const defined = optional.filter((v) => v !== undefined); +console.log(defined.length, defined.map((v) => typeof v).join(",")); +function isText(v: string | undefined): v is string { + return typeof v === "string"; +} +const maybe: (string | undefined)[] = ["a", undefined, "b"]; +console.log(maybe.filter(isText).join("-")); + +// Narrowing a record field keeps each object's own keys, in its own order, +// and an optional field the object lacks stays absent. +interface Row { + id: unknown; + note?: string; + label: string; +} +interface NumericRow { + id: number; + note?: string; + label: string; +} +const tableRows: Row[] = [ + { label: "a", id: 1 }, + { label: "b", id: "s" }, + { label: "c", id: 3, note: "n" }, +]; +for (const row of tableRows.filter((r): r is NumericRow => typeof r.id === "number")) { + console.log(Object.keys(row).join(","), JSON.stringify(row), "note" in row, row.id + 1); +} diff --git a/tests/corpus/7008-js-unit-initialized-properties/main.ts b/tests/corpus/7008-js-unit-initialized-properties/main.ts new file mode 100644 index 000000000..3ac113d6a --- /dev/null +++ b/tests/corpus/7008-js-unit-initialized-properties/main.ts @@ -0,0 +1,8 @@ +// A JavaScript package whose object literals start a property as +// `undefined` or `null` and later store records, numbers and arrays in it, +// as the diff package's path list does (`{ oldPos: -1, lastComponent: +// undefined }`). Node runs the later writes; each property holds them. +import { describe, session } from "edit-paths"; + +console.log(describe()); +console.log(session()); diff --git a/tests/corpus/7008-js-unit-initialized-properties/node_modules/edit-paths/index.d.ts b/tests/corpus/7008-js-unit-initialized-properties/node_modules/edit-paths/index.d.ts new file mode 100644 index 000000000..68e2b409b --- /dev/null +++ b/tests/corpus/7008-js-unit-initialized-properties/node_modules/edit-paths/index.d.ts @@ -0,0 +1,2 @@ +export declare function describe(): string; +export declare function session(): string; diff --git a/tests/corpus/7008-js-unit-initialized-properties/node_modules/edit-paths/index.js b/tests/corpus/7008-js-unit-initialized-properties/node_modules/edit-paths/index.js new file mode 100644 index 000000000..ec3e7c4d0 --- /dev/null +++ b/tests/corpus/7008-js-unit-initialized-properties/node_modules/edit-paths/index.js @@ -0,0 +1,43 @@ +"use strict"; +// A Myers-style path list, as the diff package keeps it: each path starts +// with `lastComponent: undefined` and later holds a linked list of records, +// and a state object starts with `null` fields that later hold other values. +function addToPath(path, added, removed, oldPosInc) { + var last = path.lastComponent; + if (last && last.added === added && last.removed === removed) { + return { oldPos: path.oldPos + oldPosInc, lastComponent: { count: last.count + 1, added: added, removed: removed, previousComponent: last.previousComponent } }; + } + return { oldPos: path.oldPos + oldPosInc, lastComponent: { count: 1, added: added, removed: removed, previousComponent: last } }; +} +function extractCommon(basePath, common) { + if (common) basePath.lastComponent = { count: common, previousComponent: basePath.lastComponent, added: false, removed: false }; + basePath.oldPos += common; +} +function components(lastComponent) { + var list = []; + while (lastComponent) { + list.push(lastComponent); + var next = lastComponent.previousComponent; + delete lastComponent.previousComponent; + lastComponent = next; + } + return list.reverse(); +} +function describe() { + var bestPath = [{ oldPos: -1, lastComponent: undefined }]; + extractCommon(bestPath[0], 1); + var path = addToPath(bestPath[0], false, true, 1); + path = addToPath(path, true, false, 0); + path = addToPath(path, true, false, 0); + return components(path.lastComponent).map(function (c) { + return (c.added ? "+" : c.removed ? "-" : "=") + c.count + ("previousComponent" in c ? "!" : ""); + }).join(" "); +} +function session() { + var state = { user: null, retries: null, tags: undefined }; + state.user = { name: "ada" }; + state.retries = 3; + state.tags = ["a", "b"]; + return state.user.name + " " + state.retries + " " + state.tags.join("+") + " " + JSON.stringify(state); +} +module.exports = { describe: describe, session: session }; diff --git a/tests/corpus/7008-js-unit-initialized-properties/node_modules/edit-paths/package.json b/tests/corpus/7008-js-unit-initialized-properties/node_modules/edit-paths/package.json new file mode 100644 index 000000000..98648c289 --- /dev/null +++ b/tests/corpus/7008-js-unit-initialized-properties/node_modules/edit-paths/package.json @@ -0,0 +1 @@ +{ "name": "edit-paths", "version": "1.0.0", "main": "./index.js", "types": "./index.d.ts" } diff --git a/tests/corpus/7011-for-in-dictionary-deletes.ts b/tests/corpus/7011-for-in-dictionary-deletes.ts new file mode 100644 index 000000000..91dac573b --- /dev/null +++ b/tests/corpus/7011-for-in-dictionary-deletes.ts @@ -0,0 +1,42 @@ +// for-in over a string-keyed dictionary visits the keys present when the +// walk starts, in insertion order, and skips a key deleted before its +// turn; keys added during the walk are not visited. A key deleted and +// added again before its turn is visited. +const d: Record = {}; +for (let i = 0; i < 8; i++) d["k" + i] = i; +const seen: string[] = []; +for (const key in d) { + seen.push(key); + if (key === "k1") { + delete d["k3"]; + delete d["k0"]; + d["k8"] = 8; + } + if (key === "k2") { + delete d["k5"]; + d["k5"] = 50; + } + if (key === "k4") delete d["k4"]; +} +console.log(seen.join(",")); +console.log(Object.keys(d).join(","), JSON.stringify(d)); + +// A numeric-looking key is walked in integer order before string keys. +const n: Record = {}; +n["b"] = "x"; +n["2"] = "y"; +n["10"] = "z"; +n["a"] = "w"; +const order: string[] = []; +for (const key in n) { + order.push(key); + if (key === "2") delete n["a"]; +} +console.log(order.join(",")); + +// A larger dictionary walked many times. +const big: Record = {}; +for (let i = 0; i < 3000; i++) big["e" + i] = i; +let total = 0; +for (let pass = 0; pass < 20; pass++) for (const key in big) total += big[key]!; +console.log(total); diff --git a/tests/corpus/7030-spawn-sync-env.ts b/tests/corpus/7030-spawn-sync-env.ts new file mode 100644 index 000000000..0ab001b8f --- /dev/null +++ b/tests/corpus/7030-spawn-sync-env.ts @@ -0,0 +1,47 @@ +// spawnSync's env option replaces the child's environment, alone or with +// the other options. +import { spawnSync } from "node:child_process"; +import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const merged = spawnSync("sh", ["-c", 'echo "$GREETING $HOME_SET"'], { + env: { ...process.env, GREETING: "hello", HOME_SET: process.env.HOME ? "yes" : "no" }, + encoding: "utf8", +}); +console.log(merged.stdout.trim(), merged.status); + +const only = spawnSync("/bin/sh", ["-c", 'echo "${GREETING:-unset} ${HOME:-nohome}"'], { + env: { GREETING: "only" }, + encoding: "utf8", +}); +console.log(only.stdout.trim()); + +const empty = spawnSync("/bin/sh", ["-c", "env | grep -c ."], { env: {}, encoding: "utf8" }); +console.log(Number(empty.stdout.trim()) <= 3, empty.status); + +const quiet = spawnSync("sh", ["-c", 'echo "$MODE" >&2; exit 4'], { + env: { PATH: process.env.PATH ?? "/usr/bin:/bin", MODE: "loud" }, + stdio: ["ignore", "pipe", "pipe"], + timeout: 5000, +}); +console.log(quiet.status, quiet.stderr.toString().trim(), quiet.stdout.toString() === ""); + +const missing = spawnSync("/nonexistent/tool", [], { env: { A: "1" } }); +console.log(missing.status, missing.error?.message); + +// The command is found on the replacement environment's PATH, or on the +// default path when the replacement has none, not on the parent's PATH. +const dir = mkdtempSync(join(tmpdir(), "scr-spawn-path-")); +writeFileSync(join(dir, "child-only-tool"), "#!/bin/sh\necho child-only\n"); +chmodSync(join(dir, "child-only-tool"), 0o755); +writeFileSync(join(dir, "not-executable"), "#!/bin/sh\necho never\n"); +const viaPath = spawnSync("child-only-tool", [], { env: { PATH: `/nonexistent:${dir}` }, encoding: "utf8" }); +console.log(viaPath.status, viaPath.stdout.trim()); +const defaultPath = spawnSync("sh", ["-c", "echo default"], { env: { A: "1" }, encoding: "utf8" }); +console.log(defaultPath.status, defaultPath.stdout.trim()); +const notOnPath = spawnSync("sh", ["-c", "echo never"], { env: { PATH: dir }, encoding: "utf8" }); +console.log(notOnPath.status, notOnPath.error?.message); +const denied = spawnSync("not-executable", [], { env: { PATH: dir }, encoding: "utf8" }); +console.log(denied.status, denied.error?.message); +rmSync(dir, { recursive: true }); diff --git a/tests/corpus/7036-child-env-path-lookup.ts b/tests/corpus/7036-child-env-path-lookup.ts new file mode 100644 index 000000000..97c93d835 --- /dev/null +++ b/tests/corpus/7036-child-env-path-lookup.ts @@ -0,0 +1,53 @@ +// spawn and execFileSync with an env option find the command on the +// replacement environment's PATH, or on the default path when it has +// none, as Node does; a relative PATH entry is read from the cwd option. +import { execFileSync, spawn } from "node:child_process"; +import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const dir = mkdtempSync(join(tmpdir(), "scr-child-path-")); +mkdirSync(join(dir, "tools")); +writeFileSync(join(dir, "tools", "child-tool"), "#!/bin/sh\necho child-tool $1\n"); +chmodSync(join(dir, "tools", "child-tool"), 0o755); + +// An earlier PATH entry holding a script whose interpreter is missing +// moves the lookup on to the next entry, as execvp does. +mkdirSync(join(dir, "broken")); +writeFileSync(join(dir, "broken", "child-tool"), "#!/nonexistent/interpreter\n"); +chmodSync(join(dir, "broken", "child-tool"), 0o755); + +const show = (e: unknown): string => { + const err = e as NodeJS.ErrnoException; + return `${err.code} ${err.message.replace(dir, "")}`; +}; + +console.log(execFileSync("child-tool", ["sync"], { env: { PATH: join(dir, "tools") }, encoding: "utf8" }).trim()); +console.log(execFileSync("sh", ["-c", "echo default"], { env: { A: "1" }, encoding: "utf8" }).trim()); +try { + execFileSync("sh", ["-c", "echo never"], { env: { PATH: "/nonexistent" }, encoding: "utf8" }); +} catch (e) { + console.log(show(e)); +} +const twoEntries = `${join(dir, "broken")}:${join(dir, "tools")}`; +console.log(execFileSync("child-tool", ["fallthrough"], { env: { PATH: twoEntries }, encoding: "utf8" }).trim()); +try { + execFileSync("child-tool", [], { env: { PATH: join(dir, "broken") }, encoding: "utf8" }); +} catch (e) { + console.log(show(e)); +} +console.log( + execFileSync("child-tool", ["relative"], { env: { PATH: "tools" }, cwd: dir, encoding: "utf8" }).trim(), +); + +const child = spawn("child-tool", ["async"], { env: { PATH: twoEntries }, stdio: ["ignore", "pipe", "ignore"] }); +let out = ""; +child.stdout!.on("data", (chunk: Buffer) => (out += chunk.toString())); +child.on("close", (code) => { + console.log(out.trim(), code); + const missing = spawn("sh", ["-c", "true"], { env: { PATH: "/nonexistent" }, stdio: "ignore" }); + missing.on("error", (e) => { + console.log("error", show(e)); + rmSync(dir, { recursive: true }); + }); +}); diff --git a/tests/corpus/7143-optional-read-integer-index.ts b/tests/corpus/7143-optional-read-integer-index.ts new file mode 100644 index 000000000..43fd59a6e --- /dev/null +++ b/tests/corpus/7143-optional-read-integer-index.ts @@ -0,0 +1,70 @@ +// Element reads that may be undefined, at indices with a proven integer +// range: loop counters with an unknown limit, `i | 0`, `x >>> 0`, negative +// counters, -0, indices past the length and the capacity, and copies +// `a[x] = b[y]!` that meet holes. +function copy(src: number[], n: number): number[] { + const out: number[] = new Array(n); + for (let i = 0; i < n; i++) out[i] = src[i]!; + return out; +} +const src: number[] = [1, 2, 3, 4]; +const copied = copy(src, 6); +console.log(copied.length, copied.join(","), 4 in copied, copied[5] === undefined); + +const holes: number[] = new Array(5); +holes[0] = 10; +holes[3] = 13; +const fromHoles = copy(holes, 5); +console.log(fromHoles.join(","), 1 in fromHoles, 3 in fromHoles); + +function show(a: (string | undefined)[], from: number, to: number): string { + const out: string[] = []; + for (let i = from; i < to; i++) { + const v = a[i]; + out.push(v === undefined ? "_" : v); + } + return out.join(","); +} +const words: (string | undefined)[] = ["a", undefined, "c"]; +words[5] = "f"; +console.log(show(words, -3, 8)); +console.log(show(words, 0, 0), show(words, 2, 1)); + +function masked(a: number[], keys: number[]): string { + const out: string[] = []; + for (const k of keys) { + const v = a[k | 0]; + const w = a[k >>> 0]; + out.push(`${v === undefined ? "-" : v}/${w === undefined ? "-" : w}`); + } + return out.join(" "); +} +const values: number[] = [5, 6, 7]; +console.log(masked(values, [0, 1.9, 2, 3, -1, -0, 4294967297, 2 ** 31])); + +const negativeZero = -0; +console.log(values[negativeZero], values[0 * -1], Object.is(negativeZero, -0)); + +// A counter that starts below zero and one that passes the capacity. +let total = 0; +for (let i = -2; i < values.length + 3; i++) total += values[i] ?? 100; +console.log(total); +const big: number[] = []; +for (let k = 0; k < 20; k++) big.push(k); +big.length = 10; +let seen = 0; +for (let i = 0; i < 40; i++) if (big[i] !== undefined) seen++; +console.log(seen, big.length); + +// Rotations through the read, with a hole that becomes undefined. +function rotate(p: number[], r: number): void { + const first = p[0]!; + for (let i = 0; i < r; i++) p[i] = p[i + 1]!; + p[r] = first; +} +const ring: number[] = new Array(4); +ring[0] = 0; +ring[1] = 1; +ring[3] = 3; +rotate(ring, 3); +console.log(ring.join(","), 1 in ring, ring[1] === undefined); diff --git a/tests/corpus/7150-cipher-legacy-modes.ts b/tests/corpus/7150-cipher-legacy-modes.ts new file mode 100644 index 000000000..80edfd029 --- /dev/null +++ b/tests/corpus/7150-cipher-legacy-modes.ts @@ -0,0 +1,74 @@ +// createCipheriv and createDecipheriv in the legacy modes: AES in ECB, +// CFB, CFB8, CFB1 and OFB, triple DES (three- and two-key) in CBC, ECB, +// CFB, CFB8, CFB1 and OFB, and bare chacha20. Fixed keys, IVs and inputs +// give Node's exact ciphertexts; chunked updates, string encodings, +// disabled padding and chacha20's 64-bit counter carry round-trip. +import { createCipheriv, createDecipheriv } from "node:crypto"; + +function bytes(length: number, seed: number): Buffer { + const out = Buffer.alloc(length); + for (let i = 0; i < length; i++) out[i] = (i * 29 + seed) & 0xff; + return out; +} + +const plaintext = Buffer.from("The five boxing wizards jump quickly; sphinx of black quartz, judge my vow."); + +function encrypt(name: string, key: Buffer, iv: Buffer | null, data: Buffer, chunk: number): Buffer { + const cipher = createCipheriv(name, key, iv); + const parts: Buffer[] = []; + for (let off = 0; off < data.length; off += chunk) parts.push(cipher.update(data.subarray(off, off + chunk))); + parts.push(cipher.final()); + return Buffer.concat(parts); +} + +function decrypt(name: string, key: Buffer, iv: Buffer | null, data: Buffer, chunk: number): Buffer { + const decipher = createDecipheriv(name, key, iv); + const parts: Buffer[] = []; + for (let off = 0; off < data.length; off += chunk) parts.push(decipher.update(data.subarray(off, off + chunk))); + parts.push(decipher.final()); + return Buffer.concat(parts); +} + +const cases: [string, number, number][] = []; +for (const bits of [128, 192, 256]) + for (const mode of ["ecb", "cfb", "cfb8", "cfb1", "ofb"]) cases.push([`aes-${bits}-${mode}`, bits / 8, mode === "ecb" ? 0 : 16]); +for (const name of ["des-ede3-cbc", "des3", "des-ede3", "des-ede3-ecb", "des-ede3-cfb", "des-ede3-cfb8", "des-ede3-cfb1", "des-ede3-ofb"]) + cases.push([name, 24, name === "des-ede3" || name === "des-ede3-ecb" ? 0 : 8]); +for (const name of ["des-ede-cbc", "des-ede", "des-ede-ecb", "des-ede-cfb", "des-ede-ofb"]) + cases.push([name, 16, name === "des-ede" || name === "des-ede-ecb" ? 0 : 8]); +cases.push(["chacha20", 32, 16]); + +for (const [name, keyLength, ivLength] of cases) { + const key = bytes(keyLength, keyLength + 3); + const iv = ivLength ? bytes(ivLength, 11) : null; + const sealed = encrypt(name, key, iv, plaintext, plaintext.length); + const chunked = encrypt(name, key, iv, plaintext, 7); + const opened = decrypt(name, key, iv, sealed, 5); + console.log(name.padEnd(14), sealed.length, sealed.toString("hex").slice(0, 48), chunked.equals(sealed), opened.equals(plaintext)); +} + +// String input and output encodings through a stream mode. +const ofb = createCipheriv("aes-128-ofb", bytes(16, 1), bytes(16, 2)); +const encoded = ofb.update("68656c6c6f20776f726c64", "hex", "base64") + ofb.final("base64"); +const unofb = createDecipheriv("aes-128-ofb", bytes(16, 1), bytes(16, 2)); +console.log(encoded, unofb.update(encoded, "base64", "utf8") + unofb.final("utf8")); + +// ECB and triple-DES CBC without padding over whole blocks. +for (const [name, keyLength, ivLength, block] of [["aes-256-ecb", 32, 0, 16], ["des-ede3-cbc", 24, 8, 8]] as const) { + const cipher = createCipheriv(name, bytes(keyLength, 5), ivLength ? bytes(ivLength, 6) : null); + cipher.setAutoPadding(false); + const sealed = Buffer.concat([cipher.update(bytes(block * 3, 7)), cipher.final()]); + const decipher = createDecipheriv(name, bytes(keyLength, 5), ivLength ? bytes(ivLength, 6) : null); + decipher.setAutoPadding(false); + const opened = Buffer.concat([decipher.update(sealed), decipher.final()]); + console.log(name, "unpadded", sealed.length, sealed.toString("hex").slice(0, 32), opened.equals(bytes(block * 3, 7))); +} + +// chacha20's counter is the IV's first 8 bytes, little-endian: the low +// word carries into the high word, which wraps. +const chachaKey = bytes(32, 9); +for (const counter of ["ffffffff00000000", "ffffffffffffffff", "fffffffe01000000"]) { + const iv = Buffer.from(counter + "0102030405060708", "hex"); + const stream = encrypt("chacha20", chachaKey, iv, Buffer.alloc(200), 50); + console.log("chacha20", counter, stream.toString("hex").slice(120, 152), stream.subarray(64, 96).toString("hex")); +} diff --git a/tests/corpus/7151-cipher-legacy-errors.cjs b/tests/corpus/7151-cipher-legacy-errors.cjs new file mode 100644 index 000000000..fe9c59bcc --- /dev/null +++ b/tests/corpus/7151-cipher-legacy-errors.cjs @@ -0,0 +1,83 @@ +// createCipheriv errors for the legacy modes, in Node's order: the IV +// length each mode takes (none for ECB), the key length, OpenSSL's +// "unsupported" for the ciphers its legacy provider holds, and the block +// modes' final-block and padding failures; the AEAD-only methods throw +// Node's invalid-state errors on the stream modes. +const crypto = require("crypto"); + +function attempt(label, run) { + try { + const value = run(); + console.log(label, "ok", Buffer.isBuffer(value) ? value.toString("hex") : value); + } catch (e) { + console.log(label, e.name, e.code, e.message, e.library, e.reason, JSON.stringify(e.opensslErrorStack)); + } +} + +const k16 = Buffer.alloc(16, 1), k24 = Buffer.alloc(24, 1), k32 = Buffer.alloc(32, 1); +attempt("ecb null iv", () => crypto.createCipheriv("aes-128-ecb", k16, null).final()); +attempt("ecb empty iv", () => crypto.createCipheriv("aes-128-ecb", k16, Buffer.alloc(0)).final()); +attempt("ecb 16-byte iv", () => crypto.createCipheriv("aes-128-ecb", k16, Buffer.alloc(16))); +attempt("cfb 15-byte iv", () => crypto.createCipheriv("aes-128-cfb", k16, Buffer.alloc(15))); +attempt("cfb null iv", () => crypto.createCipheriv("aes-128-cfb", k16, null)); +attempt("ofb key 15", () => crypto.createCipheriv("aes-128-ofb", Buffer.alloc(15), Buffer.alloc(16))); +attempt("des3 16-byte iv", () => crypto.createCipheriv("des-ede3-cbc", k24, Buffer.alloc(16))); +attempt("des3 16-byte key", () => crypto.createCipheriv("des-ede3-cbc", k16, Buffer.alloc(8))); +attempt("des-ede 24-byte key", () => crypto.createCipheriv("des-ede-cbc", k24, Buffer.alloc(8))); +attempt("des-ede3 ecb with iv", () => crypto.createCipheriv("des-ede3", k24, Buffer.alloc(8))); +attempt("chacha20 12-byte iv", () => crypto.createCipheriv("chacha20", k32, Buffer.alloc(12))); +attempt("chacha20 authTagLength ignored", () => crypto.createCipheriv("chacha20", k32, Buffer.alloc(16), { authTagLength: 16 }).update("x")); +attempt("upper-case name", () => crypto.createCipheriv("DES-EDE3-CBC", k24, Buffer.alloc(8)).final()); +attempt("des-cbc", () => crypto.createCipheriv("des-cbc", Buffer.alloc(8), Buffer.alloc(8))); +attempt("des-cbc bad iv", () => crypto.createCipheriv("des-cbc", Buffer.alloc(8), Buffer.alloc(3))); +attempt("des-cbc bad key", () => crypto.createCipheriv("des-cbc", Buffer.alloc(3), Buffer.alloc(8))); +attempt("bf-cbc", () => crypto.createDecipheriv("bf-cbc", k16, Buffer.alloc(8))); +attempt("rc4", () => crypto.createCipheriv("rc4", k16, null)); +attempt("rc4 with iv", () => crypto.createCipheriv("rc4", k16, Buffer.alloc(8))); +attempt("seed-ecb", () => crypto.createCipheriv("seed-ecb", k16, null)); +attempt("unknown cipher", () => crypto.createCipheriv("aes-128-xyz", k16, Buffer.alloc(16))); + +attempt("cfb getAuthTag", () => { + const c = crypto.createCipheriv("aes-128-cfb", k16, Buffer.alloc(16)); + c.update("x"); + c.final(); + return c.getAuthTag(); +}); +attempt("ofb setAAD", () => crypto.createCipheriv("aes-128-ofb", k16, Buffer.alloc(16)).setAAD(Buffer.alloc(1))); +attempt("chacha20 setAuthTag", () => crypto.createDecipheriv("chacha20", k32, Buffer.alloc(16)).setAuthTag(Buffer.alloc(16))); +attempt("cfb update sizes", () => { + const c = crypto.createCipheriv("aes-128-cfb", k16, Buffer.alloc(16)); + return [c.update("abc").length, c.update("defghijklmnopqrstu").length, c.final().length].join(","); +}); +attempt("cfb1 setAutoPadding(false)", () => { + const c = crypto.createCipheriv("aes-128-cfb1", k16, Buffer.alloc(16)); + c.setAutoPadding(false); + return Buffer.concat([c.update("abc"), c.final()]); +}); +attempt("ecb unpadded short", () => { + const c = crypto.createCipheriv("aes-128-ecb", k16, null); + c.setAutoPadding(false); + c.update(Buffer.alloc(31, 7)); + return c.final(); +}); +attempt("des3 unpadded short", () => { + const c = crypto.createCipheriv("des-ede3-cbc", k24, Buffer.alloc(8)); + c.setAutoPadding(false); + c.update(Buffer.alloc(7)); + return c.final(); +}); +attempt("des3 bad decrypt", () => { + const d = crypto.createDecipheriv("des-ede3-cbc", k24, Buffer.alloc(8)); + d.update(Buffer.alloc(16, 9)); + return d.final(); +}); +attempt("ecb decrypt 15 bytes", () => { + const d = crypto.createDecipheriv("aes-128-ecb", k16, null); + d.update(Buffer.alloc(15)); + return d.final(); +}); +attempt("ofb update after final", () => { + const c = crypto.createCipheriv("aes-128-ofb", k16, Buffer.alloc(16)); + c.final(); + return c.update("x"); +}); diff --git a/tests/corpus/7152-crypto-prime.ts b/tests/corpus/7152-crypto-prime.ts new file mode 100644 index 000000000..79edcc502 --- /dev/null +++ b/tests/corpus/7152-crypto-prime.ts @@ -0,0 +1,96 @@ +// checkPrimeSync, checkPrime, generatePrimeSync and generatePrime: known +// primes, composites and strong pseudoprimes as bigints, Buffers, typed +// arrays, ArrayBuffers and DataViews, the checks option, and generated +// primes' length, top bits, primality, residues and safety, as +// ArrayBuffers and bigints, synchronously and through the callbacks (whose +// error parameter, typed Error | null, receives null where Node passes +// undefined). The small sizes and the add and rem forms have one possible +// result. +import { checkPrime, checkPrimeSync, generatePrime, generatePrimeSync } from "node:crypto"; + +const mersenne61 = 2n ** 61n - 1n; +const mersenne127 = 2n ** 127n - 1n; +const mersenne521 = 2n ** 521n - 1n; +const known: [string, bigint][] = [ + ["0", 0n], + ["1", 1n], + ["2", 2n], + ["3", 3n], + ["4", 4n], + ["9", 9n], + ["17863", 17863n], + ["17881", 17881n], + ["561 (Carmichael)", 561n], + ["2047 (strong pseudoprime to 2)", 2047n], + ["3215031751 (strong pseudoprime to 2, 3, 5 and 7)", 3215031751n], + ["2^61 - 1", mersenne61], + ["2^61 + 1", mersenne61 + 2n], + ["2^127 - 1", mersenne127], + ["(2^61 - 1)(2^127 - 1)", mersenne61 * mersenne127], + ["2^521 - 1", mersenne521], + ["2^521 + 1", mersenne521 + 2n], +]; +for (const [label, value] of known) console.log(label, checkPrimeSync(value)); + +console.log("Buffer 13", checkPrimeSync(Buffer.from([0, 0, 13]))); +console.log("Uint8Array 15", checkPrimeSync(new Uint8Array([15]))); +const words = new ArrayBuffer(8); +new DataView(words).setBigUint64(0, mersenne61); +console.log("ArrayBuffer 2^61 - 1", checkPrimeSync(words)); +console.log("DataView 2^61 - 1", checkPrimeSync(new DataView(words))); +console.log("empty", checkPrimeSync(new Uint8Array(0))); +console.log("checks 1", checkPrimeSync(97n, { checks: 1 })); +console.log("checks 200", checkPrimeSync(3215031751n, { checks: 200 })); + +const bitLength = (n: bigint): number => n.toString(2).length; +const fromBuffer = (buffer: ArrayBuffer): bigint => BigInt("0x" + Buffer.from(buffer).toString("hex")); + +for (const bits of [2, 3, 4]) console.log("size", bits, generatePrimeSync(bits, { bigint: true })); +const five = generatePrimeSync(5, { bigint: true }); +console.log("size 5", five === 29n || five === 31n); +for (const bits of [8, 16, 31, 32, 33, 64, 127, 256, 512]) { + const p = generatePrimeSync(bits, { bigint: true }); + console.log("size", bits, bitLength(p) === bits, p >> BigInt(bits - 2), checkPrimeSync(p)); +} +const buffer = generatePrimeSync(96); +console.log("ArrayBuffer", buffer instanceof ArrayBuffer, buffer.byteLength, checkPrimeSync(buffer), bitLength(fromBuffer(buffer))); +console.log("bigint false", generatePrimeSync(40, { bigint: false }).byteLength); + +for (const bits of [3, 6, 7]) console.log("safe", bits, generatePrimeSync(bits, { safe: true, bigint: true })); +for (const bits of [16, 64, 128]) { + const p = generatePrimeSync(bits, { safe: true, bigint: true }); + console.log("safe", bits, bitLength(p) === bits, p >> BigInt(bits - 2), checkPrimeSync(p), checkPrimeSync((p - 1n) / 2n)); +} + +console.log("add 200 rem 1", generatePrimeSync(8, { add: 200n, rem: 1n, bigint: true })); +console.log("add 128", generatePrimeSync(8, { add: 128n, bigint: true })); +console.log("add 16 rem 3", generatePrimeSync(5, { add: 16n, rem: 3n, bigint: true })); +const safe4 = generatePrimeSync(5, { add: 4n, safe: true, bigint: true }); +console.log("add 4 safe", safe4 === 23n || safe4 === 47n); +for (const [label, add, rem] of [ + ["add 12 rem 11", 12n, 11n], + ["add 6", 6n, undefined], + ["add 1000 rem 7", 1000n, 7n], +] as const) { + const p = generatePrimeSync(48, rem === undefined ? { add, bigint: true } : { add, rem, bigint: true }); + console.log(label, p % add, checkPrimeSync(p), bitLength(p) >= 48); +} +const viaBuffer = generatePrimeSync(64, { add: Buffer.from([0x01, 0x00]), rem: new Uint8Array([0x0f]), bigint: true }); +console.log("add and rem as bytes", viaBuffer % 256n, checkPrimeSync(viaBuffer)); +const safeAdd = generatePrimeSync(64, { add: 24n, rem: 23n, safe: true, bigint: true }); +console.log("safe add 24 rem 23", safeAdd % 24n, checkPrimeSync((safeAdd - 1n) / 2n)); +console.log("rem without add", generatePrimeSync(16, { rem: 5n, bigint: true }) < 65536n); + +generatePrime(64, (err, prime) => { + console.log("callback", err ?? "no error", prime instanceof ArrayBuffer, prime.byteLength, checkPrimeSync(prime)); + generatePrime(128, { safe: true, bigint: true }, (err2, safe) => { + console.log("callback safe", err2 ?? "no error", bitLength(safe), checkPrimeSync((safe - 1n) / 2n)); + checkPrime(safe, (err3, result) => { + console.log("checkPrime", err3 ?? "no error", result); + checkPrime(Buffer.from([1, 1]), { checks: 3 }, (err4, result2) => { + console.log("checkPrime 257", err4 ?? "no error", result2); + }); + }); + }); +}); +console.log("sync done"); diff --git a/tests/corpus/7153-crypto-prime-errors.cjs b/tests/corpus/7153-crypto-prime-errors.cjs new file mode 100644 index 000000000..ff32e6e09 --- /dev/null +++ b/tests/corpus/7153-crypto-prime-errors.cjs @@ -0,0 +1,80 @@ +// checkPrime, checkPrimeSync, generatePrime and generatePrimeSync errors from +// JavaScript: argument types and ranges, the options bag, add and rem, and +// OpenSSL's generation failures, synchronously and through the callbacks. +const crypto = require("crypto"); + +function attempt(label, run) { + try { + const value = run(); + console.log(label, "ok", typeof value === "bigint" ? `${value}n` : value instanceof ArrayBuffer ? `ArrayBuffer(${value.byteLength})` : value); + } catch (e) { + console.log(label, e.name, e.code, e.message, JSON.stringify(Object.keys(e))); + } +} + +attempt("check string", () => crypto.checkPrimeSync("7")); +attempt("check number", () => crypto.checkPrimeSync(7)); +attempt("check negative", () => crypto.checkPrimeSync(-7n)); +attempt("check large negative", () => crypto.checkPrimeSync(-(2n ** 40n))); +attempt("check options null", () => crypto.checkPrimeSync(7n, null)); +attempt("check options array", () => crypto.checkPrimeSync(7n, [])); +attempt("check options string", () => crypto.checkPrimeSync(7n, "x")); +attempt("check checks string", () => crypto.checkPrimeSync(7n, { checks: "1" })); +attempt("check checks fraction", () => crypto.checkPrimeSync(7n, { checks: 1.5 })); +attempt("check checks -1", () => crypto.checkPrimeSync(7n, { checks: -1 })); +attempt("check checks 2^31", () => crypto.checkPrimeSync(7n, { checks: 2 ** 31 })); +attempt("check checks -0", () => crypto.checkPrimeSync(7n, { checks: -0 })); +attempt("check checks 10", () => crypto.checkPrimeSync(7n, { checks: 10 })); +attempt("check no callback", () => crypto.checkPrime(7n)); +attempt("check callback string", () => crypto.checkPrime(7n, {}, "cb")); +attempt("check async string", () => crypto.checkPrime("7", () => {})); +attempt("check async options null", () => crypto.checkPrime(7n, null, () => {})); + +attempt("size string", () => crypto.generatePrimeSync("8")); +attempt("size fraction", () => crypto.generatePrimeSync(8.5)); +attempt("size 0", () => crypto.generatePrimeSync(0)); +attempt("size 2^31", () => crypto.generatePrimeSync(2 ** 31)); +attempt("size NaN", () => crypto.generatePrimeSync(NaN)); +attempt("size 1", () => crypto.generatePrimeSync(1)); +attempt("size 2 safe", () => crypto.generatePrimeSync(2, { safe: true })); +attempt("size 4 safe", () => crypto.generatePrimeSync(4, { safe: true })); +attempt("size 5 safe", () => crypto.generatePrimeSync(5, { safe: true })); +attempt("options null", () => crypto.generatePrimeSync(8, null)); +attempt("options number", () => crypto.generatePrimeSync(8, 1)); +attempt("safe string", () => crypto.generatePrimeSync(8, { safe: "yes" })); +attempt("bigint number", () => crypto.generatePrimeSync(8, { bigint: 1 })); +attempt("add number", () => crypto.generatePrimeSync(8, { add: 6 })); +attempt("add string", () => crypto.generatePrimeSync(8, { add: "6" })); +attempt("add negative", () => crypto.generatePrimeSync(8, { add: -6n })); +attempt("rem number", () => crypto.generatePrimeSync(8, { add: 6n, rem: 1 })); +attempt("rem negative", () => crypto.generatePrimeSync(8, { add: 6n, rem: -1n })); +attempt("add too long", () => crypto.generatePrimeSync(8, { add: 1024n })); +attempt("add zero", () => crypto.generatePrimeSync(8, { add: 0n })); +attempt("add zero bytes", () => crypto.generatePrimeSync(8, { add: new Uint8Array(0) })); +attempt("rem equal add", () => crypto.generatePrimeSync(8, { add: 6n, rem: 6n })); +attempt("rem above add", () => crypto.generatePrimeSync(8, { add: 6n, rem: 7n })); +attempt("rem without add", () => crypto.generatePrimeSync(8, { rem: 1000n, bigint: true }) < 256n); +attempt("add 1 bigint", () => crypto.generatePrimeSync(8, { add: 1n, bigint: true }) < 256n); +attempt("gen no callback", () => crypto.generatePrime(8)); +attempt("gen options callback", () => crypto.generatePrime(8, {})); +attempt("gen size string", () => crypto.generatePrime("8", () => {})); +attempt("gen add too long", () => crypto.generatePrime(8, { add: 1024n }, () => {})); +attempt("gen options null", () => crypto.generatePrime(8, null, () => {})); + +// The callback forms' generation errors and results, one after another. +crypto.generatePrime(1, function (err, prime) { + console.log("gen 1 callback", arguments.length, err.name, err.code, err.message, JSON.stringify(Object.keys(err)), prime); + crypto.generatePrime(8, { add: 0n }, function (err2, prime2) { + console.log("gen add 0 callback", arguments.length, err2.message, prime2); + crypto.generatePrime(3, { safe: true, bigint: true }, function (err3, prime3) { + console.log("gen 3 safe callback", arguments.length, err3, prime3); + crypto.checkPrime(7n, function (err4, result) { + console.log("check 7 callback", arguments.length, err4, result); + crypto.checkPrime(new Uint8Array([0, 0, 9]), { checks: 3 }, function (err5, result2) { + console.log("check 9 callback", arguments.length, err5, result2); + }); + }); + }); + }); +}); +console.log("sync end"); diff --git a/tests/corpus/7184-reduce-runtime-accumulator.ts b/tests/corpus/7184-reduce-runtime-accumulator.ts new file mode 100644 index 000000000..bd2c8e4d9 --- /dev/null +++ b/tests/corpus/7184-reduce-runtime-accumulator.ts @@ -0,0 +1,24 @@ +// Array reduce keeps the values its callback and initial value produce at +// run time: an initial value read from a missing element is undefined, a +// callback can produce another primitive through `??`, and a parenthesized +// callback widens like a bare one. +const noStrings: string[] = []; +const noNumbers: number[] = []; + +const fromMissingInit = ([] as number[]).reduce((_acc, _value) => "ok", noStrings[0]); +console.log(typeof fromMissingInit, fromMissingInit); + +const nonEmpty = [1, 2].reduce((acc, value) => acc + value, noNumbers[0]); +console.log(typeof nonEmpty, nonEmpty); + +const fallback = [1, 2].reduce((_acc, _value) => noNumbers[0] ?? "fallback"); +console.log(typeof fallback, fallback); + +const parenthesized = [1, 2].reduce((acc, x) => (x === 1 ? noStrings[0] : acc + "!"), ""); +console.log(parenthesized); + +const wrapped = [1, 2].reduce(((acc, x) => (x === 1 ? noStrings[0] : acc + "!")), ""); +console.log(wrapped); + +const right = ["a", "b"].reduceRight((acc, s) => acc + s, noStrings[0]); +console.log(right); diff --git a/tests/corpus/7185-array-callback-arguments-length.ts b/tests/corpus/7185-array-callback-arguments-length.ts new file mode 100644 index 000000000..ae5a8d847 --- /dev/null +++ b/tests/corpus/7185-array-callback-arguments-length.ts @@ -0,0 +1,42 @@ +// `arguments.length` in a function-expression callback counts every +// argument the array method passes: value, index and array, plus the +// accumulator for reduce. One stored in a fixed-arity function type counts +// that type's parameters, which every call passes. +const seen: number[] = []; +[10, 20].forEach(function (value) { + seen.push(arguments.length, value); +}); +console.log(seen.join(",")); + +console.log( + [1, 2] + .map(function (value) { + return `${value}:${arguments.length}`; + }) + .join(" "), +); + +console.log([5].filter(function () { + return arguments.length === 3; +}).length); + +console.log([1].reduce(function (acc, value) { + return acc + value + arguments.length; +}, 0)); + +console.log(["x", "y"].reduceRight(function (acc: string, value: string) { + return `${acc}${value}${arguments.length}`; +}, "")); + +console.log([3, 4].some(function (value) { + return arguments.length === 3 && value === 4; +})); + +const sum: (a: number, b: number) => number = function (a) { + return a + arguments.length; +}; +console.log(sum(1, 2)); + +console.log([3, 1, 2].sort(function (a, b) { + return arguments.length === 2 ? a - b : 0; +}).join(",")); diff --git a/tests/corpus/7261-acyclic-result-builders.ts b/tests/corpus/7261-acyclic-result-builders.ts new file mode 100644 index 000000000..b6a98fce4 --- /dev/null +++ b/tests/corpus/7261-acyclic-result-builders.ts @@ -0,0 +1,86 @@ +// Records built by functions that return object literals, as tree and list +// builders are written. The backend gives such a function a result-slot +// entry that writes the literal's fields into a caller's frame storage, and +// those stores do not make the record's fields mutable: a type no store +// changes after construction stays acyclic and needs no cycle-collector +// header. This pins what such programs print: rebuilt trees, a shared +// subtree (a DAG), a builder whose result the caller only projects (so the +// entry runs), a list longer than the runtime's teardown depth budget, and +// a record type the program writes after construction, whose rings only +// the collector frees. The sanitized lane asserts every object is freed. + +interface Node { + value: number; + left: Node | null; + right: Node | null; +} + +function leaf(value: number): Node { + return { value, left: null, right: null }; +} + +function branch(value: number, left: Node | null, right: Node | null): Node { + return { value, left, right }; +} + +function tree(depth: number, value: number): Node { + return depth === 0 ? leaf(value) : branch(value, tree(depth - 1, value * 2), tree(depth - 1, value * 2 + 1)); +} + +function sum(node: Node | null): number { + return node === null ? 0 : node.value + sum(node.left) + sum(node.right); +} + +let total = 0; +for (let generation = 0; generation < 8; generation++) total += sum(tree(8, generation + 1)); +console.log("trees", total); + +// A subtree shared by both branches is released once. +const shared = tree(3, 1); +const dag = branch(0, shared, shared); +console.log("dag", sum(dag), JSON.stringify(branch(1, leaf(2), null))); + +// Only projected: the result lives in the caller's frame, which releases +// the subtree it holds. +const top = branch(5, tree(2, 1), null); +console.log("top", top.value, sum(top.left), top.right === null); + +interface Cell { + n: number; + next: Cell | null; +} + +function cons(n: number, next: Cell | null): Cell { + return { n, next }; +} + +let list: Cell | null = null; +for (let i = 0; i < 5000; i++) list = cons(i, list); +let length = 0; +let last = -1; +for (let c = list; c !== null; c = c.next) { + length++; + last = c.n; +} +console.log("list", length, last); +list = null; + +// A record type written after construction keeps its cycle-collector +// header: these rings are garbage only the collector can free. +interface Ring { + id: number; + next: Ring | null; +} + +function ring(id: number, next: Ring | null): Ring { + return { id, next }; +} + +let ids = 0; +for (let i = 0; i < 200; i++) { + const a = ring(i, null); + const b = ring(i + 1, a); + a.next = b; + ids += a.next.id + b.next!.id; +} +console.log("rings", ids); diff --git a/tests/corpus/7301-running-extrema-branches.ts b/tests/corpus/7301-running-extrema-branches.ts new file mode 100644 index 000000000..9941ce538 --- /dev/null +++ b/tests/corpus/7301-running-extrema-branches.ts @@ -0,0 +1,169 @@ +// Running maximum and minimum scans over ascending, descending, alternating, +// pseudo-random and NaN-holding data: plain for, while, do-while and for-of +// loops, `<=` and `>=` ties, an argmax that records its index, typed array +// and array element reads, callbacks that update captured bindings, a scan +// nested in an outer loop, an update nested in another branch, and the clamp +// and saturating updates that are not running extrema. + +function shapes(n: number): [string, Float64Array][] { + const ascending = new Float64Array(n); + const descending = new Float64Array(n); + const alternating = new Float64Array(n); + const random = new Float64Array(n); + const holes = new Float64Array(n); + let x = 12345; + for (let i = 0; i < n; i++) { + ascending[i] = i; + descending[i] = n - i; + alternating[i] = i % 2 === 0 ? 0 : i; + x = (x * 1103515245 + 12345) % 2147483648; + random[i] = (x % 1000) - 500; + holes[i] = i % 7 === 3 ? NaN : (x % 97) - 48; + } + return [ + ["ascending", ascending], + ["descending", descending], + ["alternating", alternating], + ["random", random], + ["nan", holes], + ]; +} + +function argmax(a: Float64Array): string { + let max = -Infinity; + let at = -1; + for (let i = 0; i < a.length; i++) { + const v = a[i]!; + if (v > max) { + max = v; + at = i; + } + } + return `${max}@${at}`; +} + +function lastMin(a: Float64Array): string { + let min = Infinity; + let at = -1; + let i = 0; + while (i < a.length) { + if (a[i]! <= min) { + min = a[i]!; + at = i; + } + i++; + } + return `${min}@${at}`; +} + +function bothEnds(values: number[]): string { + let lo = Infinity; + let hi = -Infinity; + for (const v of values) { + if (lo > v) lo = v; + if (v >= hi) hi = v; + } + return `${lo}..${hi}`; +} + +function firstDrop(a: Float64Array): number { + let floor = a[0]!; + let i = 0; + do { + if (a[i]! < floor) floor = a[i]!; + i++; + } while (i < a.length); + return floor; +} + +function callbackPeak(a: Float64Array): string { + let max = -Infinity; + let at = -1; + a.forEach((v, i) => { + if (v > max) { + max = v; + at = i; + } + }); + return `${max}@${at}`; +} + +function arrayCallbackMin(values: number[]): number { + let min = Infinity; + values.forEach((v) => { + if (v < min) min = v; + }); + return min; +} + +function rowMaxima(a: Float64Array, width: number): string { + const out: number[] = []; + for (let row = 0; row * width < a.length; row++) { + let best = -Infinity; + for (let j = row * width; j < Math.min(a.length, (row + 1) * width); j++) { + if (a[j]! > best) best = a[j]!; + } + out.push(best); + } + return out.join(","); +} + +function longestNonNegativeRun(a: Float64Array): number { + let longest = 0; + let run = 0; + for (let i = 0; i < a.length; i++) { + if (a[i]! >= 0) { + run++; + if (run > longest) longest = run; + } else run = 0; + } + return longest; +} + +function clampSum(a: Float64Array, hi: number): number { + let s = 0; + for (let i = 0; i < a.length; i++) { + let v = a[i]!; + if (hi < v) v = hi; + s += v; + } + return s; +} + +function saturate(a: Float64Array, hi: number): number { + let v = 0; + for (let i = 0; i < a.length; i++) { + v += a[i]!; + if (hi < v) v = hi; + } + return v; +} + +for (const [name, a] of shapes(40)) { + const values = Array.from(a); + console.log( + name, + argmax(a), + lastMin(a), + bothEnds(values), + firstDrop(a), + callbackPeak(a), + arrayCallbackMin(values), + ); + console.log( + " rows", + rowMaxima(a, 7), + "run", + longestNonNegativeRun(a), + "clamp", + clampSum(a, 5), + "saturate", + saturate(a, 30), + ); +} + +const signed = new Float64Array([0, -0, 0, -0]); +let top = -Infinity; +for (let i = 0; i < signed.length; i++) if (signed[i]! >= top) top = signed[i]!; +console.log("signed zero", Object.is(top, -0), argmax(signed), lastMin(signed)); +console.log("empty", argmax(new Float64Array(0)), bothEnds([]), arrayCallbackMin([])); diff --git a/tests/corpus/7322-union-scalar-equality.ts b/tests/corpus/7322-union-scalar-equality.ts new file mode 100644 index 000000000..82e0564fc --- /dev/null +++ b/tests/corpus/7322-union-scalar-equality.ts @@ -0,0 +1,68 @@ +// Strict equality and inequality between a `number | undefined` or +// `boolean | undefined` value and a number or boolean: element reads of +// holes, past the end and of present undefined, NaN, -0 and +0, and +// Object.is, in branches, loops and expressions. + +function zeros(a: number[], at: number[]): string { + const hits: string[] = []; + for (const i of at) { + const v = a[i]; + hits.push(`${i}:${v === 0 ? "eq" : "ne"}${v !== 0 ? "!" : ""}${0 === v ? "r" : ""}`); + } + return hits.join(" "); +} + +const holey: number[] = [0, -0, 1]; +holey[5] = 0; +console.log(zeros(holey, [0, 1, 2, 3, 4, 5, 6, -1])); +console.log(zeros([NaN, 0.5, -1, 0], [0, 1, 2, 3])); + +function nanAndZeros(a: (number | undefined)[]): string { + return a + .map((v) => `${v === -0}${v === 0}${Object.is(v, -0)}${Object.is(v, 0)}${Object.is(v, NaN)}`) + .join(" "); +} +console.log(nanAndZeros([0, -0, NaN, undefined, 1])); + +function flags(a: (boolean | undefined)[]): string { + let t = 0; + let f = 0; + let notTrue = 0; + for (let i = 0; i < a.length + 2; i++) { + if (a[i] === true) t++; + if (a[i] === false) f++; + if (a[i] !== true) notTrue++; + } + return `${t} ${f} ${notTrue}`; +} +const bools: (boolean | undefined)[] = [true, false, undefined, true]; +bools[7] = false; +console.log(flags(bools)); + +// A permutation walk that stops at a zero element, as the flip loops of a +// counting benchmark do. +function walk(p: number[], start: number): number { + let steps = 0; + let at = start; + while (p[at] !== 0 && steps < 20) { + at = p[at]!; + steps++; + } + return steps; +} +console.log(walk([0, 3, 1, 2], 1), walk([1, 2, 3, 0], 0), walk([2, 2, 2], 0)); + +class Slot { + value: number | undefined = undefined; + on: boolean | undefined = undefined; +} +const slots = [new Slot(), new Slot(), new Slot()]; +slots[1]!.value = 0; +slots[1]!.on = true; +slots[2]!.value = -0; +slots[2]!.on = false; +console.log( + slots + .map((s) => `${s.value === 0}${s.value !== 0}${s.on === true}${s.on === false}${s.on !== false}`) + .join(" "), +); diff --git a/tests/corpus/7323-loop-bound-parameter-versions.ts b/tests/corpus/7323-loop-bound-parameter-versions.ts new file mode 100644 index 000000000..cdbb8cfc4 --- /dev/null +++ b/tests/corpus/7323-loop-bound-parameter-versions.ts @@ -0,0 +1,88 @@ +// Functions whose number parameters bound their loops, called with values +// no caller proves to be int32s: integers, fractions, NaN, -0, infinities, +// and integers past the int32 range. Every call prints Node's result, +// including -0 read back through 1 / n, a reassigned parameter, a thrown +// error, a void result and a function passed as a value. + +function show(label: string, value: number): void { + console.log(label, Object.is(value, -0) ? "-0" : String(value)); +} + +function sumTo(n: number): number { + let s = 0; + for (let i = 0; i < n; i++) s += i; + return s; +} + +function fill(n: number, out: number[]): void { + for (let i = 0; i < n; i++) out.push(i * 2); +} + +function sign(n: number): number { + let steps = 0; + for (let i = 0; i < n && steps < 4; i++) steps++; + return 1 / n + steps; +} + +function countdown(n: number, step: number): number { + let k = 0; + while (n > 0) { + n -= step; + k++; + if (k > 100) break; + } + return k * 1000 + n; +} + +function checked(n: number, limit: number): number { + let s = 0; + let over = -1; + for (let i = 0; i <= n; i++) { + if (i > limit && over < 0) over = i; + s += i; + } + if (over >= 0) throw new RangeError(`past ${limit} at ${over}`); + return s; +} + +function fillTo(n: number, out: number[]): number { + for (let i = 0; i < n; i++) out[i] = i * 3; + return out.length; +} + +function window2(a: number[], from: number, to: number): string { + const parts: string[] = []; + for (let i = from; i < to; i++) parts.push(String(a[i])); + return parts.join(","); +} + +// Values arrive as strings and JSON, so no caller proves them int32s. +const inputs: number[] = JSON.parse("[0, 1, 5, 7.5, -3, -0, 2147483647, 2147483648, -2147483649, 1e300]"); +inputs.push(NaN, Infinity, -Infinity, Number("12"), Number.parseFloat("3.25")); + +for (const n of inputs) { + const small = Math.abs(n) > 100 ? (n > 0 ? 3 : -3) : n; + show(`sumTo ${n}`, sumTo(small)); + show(`sign ${n}`, sign(n)); + show(`countdown ${n}`, countdown(small, 2)); + const out: number[] = []; + fill(small, out); + const dense: number[] = []; + const length = fillTo(small, dense); + console.log(`fill ${n}`, out.join(" "), length, dense.join(" ")); +} + +for (const n of [3, 4.5, 6]) { + try { + show(`checked ${n}`, checked(n, 4)); + } catch (e) { + console.log(`checked ${n} threw`, (e as Error).message); + } +} + +const data = [10, 20, 30, 40, 50]; +for (const [from, to] of JSON.parse("[[0, 2], [1.5, 4], [-1, 1], [3, 9], [2, 2]]") as [number, number][]) + console.log(`window ${from}..${to}`, window2(data, from, to)); + +const fns: ((n: number) => number)[] = [sumTo, (n) => sumTo(n) * 2]; +console.log(fns.map((f) => f(Number("4"))).join(" "), [1, 2.5, 3].map(sumTo).join(" ")); diff --git a/tests/corpus/7324-loop-bound-parameter-window-sums.ts b/tests/corpus/7324-loop-bound-parameter-window-sums.ts new file mode 100644 index 000000000..094e9848c --- /dev/null +++ b/tests/corpus/7324-loop-bound-parameter-window-sums.ts @@ -0,0 +1,33 @@ +// Sliding-window sums whose length and width are number parameters read +// from JSON, so no caller proves them int32s: int32 sizes run the +// function's int32 version, and a fractional width, a length past the +// int32 range, -0 and NaN run its ordinary body. + +function windowSums(n: number, width: number, values: number[], out: number[]): number { + let total = 0; + for (let i = 0; i + width <= n; i++) { + let s = 0; + for (let k = 0; k < width; k++) s += values[i + k]!; + out[i] = s; + total += s; + } + return total; +} + +const values: number[] = []; +for (let i = 0; i < 4000; i++) values.push((i * 37) % 101); + +const runs = JSON.parse( + "[[4000, 64, 3], [4000, 1, 2], [100, 2.5, 2], [10, 0, 1], [-0, 3, 1], [4000, 3000000000, 1], [3000000000, 4, 1], [12, 20, 1]]", +) as [number, number, number][]; +runs.push([NaN, 2, 1], [Number("250"), Number("8"), 2]); +for (const [n, width, rounds] of runs) { + const out: number[] = []; + const length = Math.min(Number.isFinite(n) ? n : 0, 4000); + for (let i = 0; i < length; i++) out.push(0); + let total = 0; + // A length past the values reads holes: keep that run within them. + const limit = n > 4000 ? 4000 : n; + for (let r = 0; r < rounds; r++) total += windowSums(limit, width, values, out); + console.log(`${n} ${width} ${rounds}:`, total, out.length, out.slice(0, 3).join(",")); +} diff --git a/tests/coverage-fixtures/blocked-signature-body.ts b/tests/coverage-fixtures/blocked-signature-body.ts new file mode 100644 index 000000000..2c37484e0 --- /dev/null +++ b/tests/coverage-fixtures/blocked-signature-body.ts @@ -0,0 +1,19 @@ +// Functions whose signatures cannot compile: the report still lists what +// their bodies would report. A use of the blocked parameter names the +// declaration; the body's own unsupported statement reports itself, with +// the reached diagnostics for a function the entry calls and as unreached +// for one nothing calls. +function measure(target: WeakRef, label: string): number { + const extra = new WeakRef({ label }); + console.log(target); + return label.length + (extra === undefined ? 0 : 1); +} +function unused(target: WeakRef): number { + const extra = new WeakRef({ target }); + return extra === undefined ? 0 : 1; +} +console.log(measure(new WeakRef({}), "abc")); +// A declared `this` parameter gives the body its receiver. +function described(this: { label: string }, target: WeakRef): string { + return this.label + String(target); +} diff --git a/tests/coverage-fixtures/startup-fences.js b/tests/coverage-fixtures/startup-fences.js new file mode 100644 index 000000000..22aad4e0a --- /dev/null +++ b/tests/coverage-fixtures/startup-fences.js @@ -0,0 +1,9 @@ +// Deferred fences in a module's top-level code run when the module loads; +// a fence inside a function runs when the function is called. +"use strict"; +var value = JSON.parse("{}"); +console.log(value instanceof Object); +function check(other) { + return other instanceof Object; +} +console.log(check([])); diff --git a/tests/diagnostics/arguments-length-self-call.ts b/tests/diagnostics/arguments-length-self-call.ts new file mode 100644 index 000000000..3afc2cb33 --- /dev/null +++ b/tests/diagnostics/arguments-length-self-call.ts @@ -0,0 +1,8 @@ +// `arguments.length` in a function expression stored in a slot with more +// parameters counts that slot's arguments, but a call through the +// expression's own name passes only the declared ones: no constant answers +// both, so the read fences. +const count: (a: number, b: number) => number = function self(a: number): number { + return a > 0 ? self(a - 1) + arguments.length : arguments.length; +}; +console.log(count(2, 0)); diff --git a/tests/diagnostics/filter-narrow.ts b/tests/diagnostics/filter-narrow.ts deleted file mode 100644 index 45452e212..000000000 --- a/tests/diagnostics/filter-narrow.ts +++ /dev/null @@ -1,22 +0,0 @@ -// Narrowing filters re-tag ONLY what the runtime test proved: an inferred -// single-arm predicate or filter(Boolean). Everything else stays fenced — -// multi-arm targets (the re-tag doesn't exist), hand-written predicates -// (unchecked assertions), and predicate VALUES (their annotation is -// written by definition). -const xs: (string | number | undefined)[] = ["a", 1, undefined]; - -// Multi-arm narrowing: string | number is not a single arm. -const defined = xs.filter((x) => x !== undefined); -console.log(defined.length); - -// A hand-written predicate on an inline callback. -const ys: (string | undefined)[] = ["a", undefined]; -const strings = ys.filter((x): x is string => typeof x === "string"); -console.log(strings.length); - -// A predicate VALUE (declared function with a written predicate). -function isString(x: string | undefined): x is string { - return typeof x === "string"; -} -const viaValue = ys.filter(isString); -console.log(viaValue.length); diff --git a/tests/fixtures/node-types/fenced.ts b/tests/fixtures/node-types/fenced.ts index 644e901f4..27e68a618 100644 --- a/tests/fixtures/node-types/fenced.ts +++ b/tests/fixtures/node-types/fenced.ts @@ -70,12 +70,12 @@ http2.createSecureServer({ allowHTTP1: true, cert: "pem", key: "pem", streamRese http2.connect("https://localhost"); /* The crypto surface beyond the lowered slice (randomness, the hash * chain, the introspection statics): RSA encryption names the lowered - * asymmetric surface, symmetric ciphers the missing cipher stack, prime - * checks theirs, and setFips the FIPS truth (getFips() lowers to 0). */ -import { checkPrimeSync, createCipheriv, publicEncrypt, setFips } from "node:crypto"; + * asymmetric surface, symmetric ciphers the missing cipher stack, the + * secure heap the generic hint, and setFips the FIPS truth (getFips() lowers to 0). */ +import { createCipheriv, publicEncrypt, secureHeapUsed, setFips } from "node:crypto"; publicEncrypt("key", Buffer.alloc(1)); createCipheriv("aes-128-ccm", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); -checkPrimeSync(7n); +secureHeapUsed(); setFips(false); fetch("https://example.invalid/", { integrity: "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", diff --git a/tests/fixtures/npm/cases/chalk5-builder/main.ts b/tests/fixtures/npm/cases/chalk5-builder/main.ts new file mode 100644 index 000000000..2a4fff61b --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/main.ts @@ -0,0 +1,48 @@ +// chalk 5 builders used as values (the package compiles from scriptc's +// built-in shim): builders in function slots and arrays, styles picked at +// run time, builders in record fields called with any number of arguments, +// visible at level 0, chalkStderr, and the root's own call. +import chalk, { Chalk, chalkStderr, type ChalkInstance, type ForegroundColorName } from "chalk"; + +const show = (label: string, value: string): void => { + console.log(label + " " + JSON.stringify(value)); +}; + +const c = new Chalk({ level: 1 }); +const paint: (text: string) => string = c.green; +show("slot", paint("go")); +const styles: ChalkInstance[] = [c.red, c.bold, c.underline.blue]; +show("array", styles.map((style) => style("x")).join("|")); +const byName = (name: ForegroundColorName): ChalkInstance => { + switch (name) { + case "red": + return c.red; + case "green": + return c.green; + default: + return c.white; + } +}; +show("picked", byName("red")("r") + byName("green")("g") + byName("cyan")("w")); +let chain: ChalkInstance = c; +for (const step of ["bold", "italic", "yellow"]) { + chain = step === "bold" ? chain.bold : step === "italic" ? chain.italic : chain.yellow; +} +show("built", chain("chain")); +show("cached", c.red === c.red ? "same" : "different"); +show("nested close", c.red("a" + c.blue("b") + "c" + c.red("d") + "e")); +show("bg nested", c.bgRed("a" + c.bgBlue("b") + "c")); + +const off = new Chalk({ level: 0 }); +show("visible off", off.visible("v") + "|" + off.red.visible("v") + "|" + off.red("plain")); +show("root call", c("a", 1, true) + "|" + c() + "|" + c(undefined) + "|" + c(null, "n")); +show("builder args", c.red(undefined) + "|" + c.red(null) + "|" + c.red(1, 2) + "|" + c.red()); +const theme = { error: c.red, note: c.bold.blue }; +show("field args", theme.error("a", "b") + "|" + theme.note("x", 1, true) + "|" + theme.error("one") + "|" + theme.error()); +show("multiline", c.cyan("one\r\ntwo\nthree")); +show("stderr level", String(chalkStderr.level >= 0)); +show("rgb levels", [1, 2, 3].map((level) => new Chalk({ level: level as 1 | 2 | 3 }).rgb(255, 136, 0)("o")).join("|")); +show("hex bg", new Chalk({ level: 2 }).bgHex("#abcdef")("h") + new Chalk({ level: 1 }).bgHex("123")("i")); +c.level = 0; +show("after level 0", paint("go") + "|" + c.red("r")); +show("default red", chalk.level >= 0 ? "ok" : "bad"); diff --git a/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/license b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/license new file mode 100644 index 000000000..fa7ceba3e --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/license @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) Sindre Sorhus (https://sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/package.json b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/package.json new file mode 100644 index 000000000..c9e0dc52b --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/package.json @@ -0,0 +1,83 @@ +{ + "name": "chalk", + "version": "5.6.2", + "description": "Terminal string styling done right", + "license": "MIT", + "repository": "chalk/chalk", + "funding": "https://github.com/chalk/chalk?sponsor=1", + "type": "module", + "main": "./source/index.js", + "exports": "./source/index.js", + "imports": { + "#ansi-styles": "./source/vendor/ansi-styles/index.js", + "#supports-color": { + "node": "./source/vendor/supports-color/index.js", + "default": "./source/vendor/supports-color/browser.js" + } + }, + "types": "./source/index.d.ts", + "sideEffects": false, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "scripts": { + "test": "xo && c8 ava && tsd", + "bench": "matcha benchmark.js" + }, + "files": [ + "source", + "!source/index.test-d.ts" + ], + "keywords": [ + "color", + "colour", + "colors", + "terminal", + "console", + "cli", + "string", + "ansi", + "style", + "styles", + "tty", + "formatting", + "rgb", + "256", + "shell", + "xterm", + "log", + "logging", + "command-line", + "text" + ], + "devDependencies": { + "@types/node": "^16.11.10", + "ava": "^3.15.0", + "c8": "^7.10.0", + "color-convert": "^2.0.1", + "execa": "^6.0.0", + "log-update": "^5.0.0", + "matcha": "^0.7.0", + "tsd": "^0.19.0", + "xo": "^0.57.0", + "yoctodelay": "^2.0.0" + }, + "xo": { + "rules": { + "unicorn/prefer-string-slice": "off", + "@typescript-eslint/consistent-type-imports": "off", + "@typescript-eslint/consistent-type-exports": "off", + "@typescript-eslint/consistent-type-definitions": "off", + "unicorn/expiring-todo-comments": "off" + } + }, + "c8": { + "reporter": [ + "text", + "lcov" + ], + "exclude": [ + "source/vendor" + ] + } +} diff --git a/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/index.d.ts b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/index.d.ts new file mode 100644 index 000000000..8295d9252 --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/index.d.ts @@ -0,0 +1,325 @@ +// TODO: Make it this when TS suports that. +// import {ModifierName, ForegroundColor, BackgroundColor, ColorName} from '#ansi-styles'; +// import {ColorInfo, ColorSupportLevel} from '#supports-color'; +import { + ModifierName, + ForegroundColorName, + BackgroundColorName, + ColorName, +} from './vendor/ansi-styles/index.js'; +import {ColorInfo, ColorSupportLevel} from './vendor/supports-color/index.js'; + +export interface Options { + /** + Specify the color support for Chalk. + + By default, color support is automatically detected based on the environment. + + Levels: + - `0` - All colors disabled. + - `1` - Basic 16 colors support. + - `2` - ANSI 256 colors support. + - `3` - Truecolor 16 million colors support. + */ + readonly level?: ColorSupportLevel; +} + +/** +Return a new Chalk instance. +*/ +export const Chalk: new (options?: Options) => ChalkInstance; // eslint-disable-line @typescript-eslint/naming-convention + +export interface ChalkInstance { + (...text: unknown[]): string; + + /** + The color support for Chalk. + + By default, color support is automatically detected based on the environment. + + Levels: + - `0` - All colors disabled. + - `1` - Basic 16 colors support. + - `2` - ANSI 256 colors support. + - `3` - Truecolor 16 million colors support. + */ + level: ColorSupportLevel; + + /** + Use RGB values to set text color. + + @example + ``` + import chalk from 'chalk'; + + chalk.rgb(222, 173, 237); + ``` + */ + rgb: (red: number, green: number, blue: number) => this; + + /** + Use HEX value to set text color. + + @param color - Hexadecimal value representing the desired color. + + @example + ``` + import chalk from 'chalk'; + + chalk.hex('#DEADED'); + ``` + */ + hex: (color: string) => this; + + /** + Use an [8-bit unsigned number](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit) to set text color. + + @example + ``` + import chalk from 'chalk'; + + chalk.ansi256(201); + ``` + */ + ansi256: (index: number) => this; + + /** + Use RGB values to set background color. + + @example + ``` + import chalk from 'chalk'; + + chalk.bgRgb(222, 173, 237); + ``` + */ + bgRgb: (red: number, green: number, blue: number) => this; + + /** + Use HEX value to set background color. + + @param color - Hexadecimal value representing the desired color. + + @example + ``` + import chalk from 'chalk'; + + chalk.bgHex('#DEADED'); + ``` + */ + bgHex: (color: string) => this; + + /** + Use a [8-bit unsigned number](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit) to set background color. + + @example + ``` + import chalk from 'chalk'; + + chalk.bgAnsi256(201); + ``` + */ + bgAnsi256: (index: number) => this; + + /** + Modifier: Reset the current style. + */ + readonly reset: this; + + /** + Modifier: Make the text bold. + */ + readonly bold: this; + + /** + Modifier: Make the text have lower opacity. + */ + readonly dim: this; + + /** + Modifier: Make the text italic. *(Not widely supported)* + */ + readonly italic: this; + + /** + Modifier: Put a horizontal line below the text. *(Not widely supported)* + */ + readonly underline: this; + + /** + Modifier: Put a horizontal line above the text. *(Not widely supported)* + */ + readonly overline: this; + + /** + Modifier: Invert background and foreground colors. + */ + readonly inverse: this; + + /** + Modifier: Print the text but make it invisible. + */ + readonly hidden: this; + + /** + Modifier: Puts a horizontal line through the center of the text. *(Not widely supported)* + */ + readonly strikethrough: this; + + /** + Modifier: Print the text only when Chalk has a color level above zero. + + Can be useful for things that are purely cosmetic. + */ + readonly visible: this; + + readonly black: this; + readonly red: this; + readonly green: this; + readonly yellow: this; + readonly blue: this; + readonly magenta: this; + readonly cyan: this; + readonly white: this; + + /* + Alias for `blackBright`. + */ + readonly gray: this; + + /* + Alias for `blackBright`. + */ + readonly grey: this; + + readonly blackBright: this; + readonly redBright: this; + readonly greenBright: this; + readonly yellowBright: this; + readonly blueBright: this; + readonly magentaBright: this; + readonly cyanBright: this; + readonly whiteBright: this; + + readonly bgBlack: this; + readonly bgRed: this; + readonly bgGreen: this; + readonly bgYellow: this; + readonly bgBlue: this; + readonly bgMagenta: this; + readonly bgCyan: this; + readonly bgWhite: this; + + /* + Alias for `bgBlackBright`. + */ + readonly bgGray: this; + + /* + Alias for `bgBlackBright`. + */ + readonly bgGrey: this; + + readonly bgBlackBright: this; + readonly bgRedBright: this; + readonly bgGreenBright: this; + readonly bgYellowBright: this; + readonly bgBlueBright: this; + readonly bgMagentaBright: this; + readonly bgCyanBright: this; + readonly bgWhiteBright: this; +} + +/** +Main Chalk object that allows to chain styles together. + +Call the last one as a method with a string argument. + +Order doesn't matter, and later styles take precedent in case of a conflict. + +This simply means that `chalk.red.yellow.green` is equivalent to `chalk.green`. +*/ +declare const chalk: ChalkInstance; + +export const supportsColor: ColorInfo; + +export const chalkStderr: typeof chalk; +export const supportsColorStderr: typeof supportsColor; + +export { + ModifierName, ForegroundColorName, BackgroundColorName, ColorName, + modifierNames, foregroundColorNames, backgroundColorNames, colorNames, +// } from '#ansi-styles'; +} from './vendor/ansi-styles/index.js'; + +export { + ColorInfo, + ColorSupport, + ColorSupportLevel, +// } from '#supports-color'; +} from './vendor/supports-color/index.js'; + +// TODO: Remove these aliases in the next major version +/** +@deprecated Use `ModifierName` instead. + +Basic modifier names. +*/ +export type Modifiers = ModifierName; + +/** +@deprecated Use `ForegroundColorName` instead. + +Basic foreground color names. + +[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support) +*/ +export type ForegroundColor = ForegroundColorName; + +/** +@deprecated Use `BackgroundColorName` instead. + +Basic background color names. + +[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support) +*/ +export type BackgroundColor = BackgroundColorName; + +/** +@deprecated Use `ColorName` instead. + +Basic color names. The combination of foreground and background color names. + +[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support) +*/ +export type Color = ColorName; + +/** +@deprecated Use `modifierNames` instead. + +Basic modifier names. +*/ +export const modifiers: readonly Modifiers[]; + +/** +@deprecated Use `foregroundColorNames` instead. + +Basic foreground color names. +*/ +export const foregroundColors: readonly ForegroundColor[]; + +/** +@deprecated Use `backgroundColorNames` instead. + +Basic background color names. +*/ +export const backgroundColors: readonly BackgroundColor[]; + +/** +@deprecated Use `colorNames` instead. + +Basic color names. The combination of foreground and background color names. +*/ +export const colors: readonly Color[]; + +export default chalk; diff --git a/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/index.js b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/index.js new file mode 100644 index 000000000..8bc993da5 --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/index.js @@ -0,0 +1,225 @@ +import ansiStyles from '#ansi-styles'; +import supportsColor from '#supports-color'; +import { // eslint-disable-line import/order + stringReplaceAll, + stringEncaseCRLFWithFirstIndex, +} from './utilities.js'; + +const {stdout: stdoutColor, stderr: stderrColor} = supportsColor; + +const GENERATOR = Symbol('GENERATOR'); +const STYLER = Symbol('STYLER'); +const IS_EMPTY = Symbol('IS_EMPTY'); + +// `supportsColor.level` → `ansiStyles.color[name]` mapping +const levelMapping = [ + 'ansi', + 'ansi', + 'ansi256', + 'ansi16m', +]; + +const styles = Object.create(null); + +const applyOptions = (object, options = {}) => { + if (options.level && !(Number.isInteger(options.level) && options.level >= 0 && options.level <= 3)) { + throw new Error('The `level` option should be an integer from 0 to 3'); + } + + // Detect level if not set manually + const colorLevel = stdoutColor ? stdoutColor.level : 0; + object.level = options.level === undefined ? colorLevel : options.level; +}; + +export class Chalk { + constructor(options) { + // eslint-disable-next-line no-constructor-return + return chalkFactory(options); + } +} + +const chalkFactory = options => { + const chalk = (...strings) => strings.join(' '); + applyOptions(chalk, options); + + Object.setPrototypeOf(chalk, createChalk.prototype); + + return chalk; +}; + +function createChalk(options) { + return chalkFactory(options); +} + +Object.setPrototypeOf(createChalk.prototype, Function.prototype); + +for (const [styleName, style] of Object.entries(ansiStyles)) { + styles[styleName] = { + get() { + const builder = createBuilder(this, createStyler(style.open, style.close, this[STYLER]), this[IS_EMPTY]); + Object.defineProperty(this, styleName, {value: builder}); + return builder; + }, + }; +} + +styles.visible = { + get() { + const builder = createBuilder(this, this[STYLER], true); + Object.defineProperty(this, 'visible', {value: builder}); + return builder; + }, +}; + +const getModelAnsi = (model, level, type, ...arguments_) => { + if (model === 'rgb') { + if (level === 'ansi16m') { + return ansiStyles[type].ansi16m(...arguments_); + } + + if (level === 'ansi256') { + return ansiStyles[type].ansi256(ansiStyles.rgbToAnsi256(...arguments_)); + } + + return ansiStyles[type].ansi(ansiStyles.rgbToAnsi(...arguments_)); + } + + if (model === 'hex') { + return getModelAnsi('rgb', level, type, ...ansiStyles.hexToRgb(...arguments_)); + } + + return ansiStyles[type][model](...arguments_); +}; + +const usedModels = ['rgb', 'hex', 'ansi256']; + +for (const model of usedModels) { + styles[model] = { + get() { + const {level} = this; + return function (...arguments_) { + const styler = createStyler(getModelAnsi(model, levelMapping[level], 'color', ...arguments_), ansiStyles.color.close, this[STYLER]); + return createBuilder(this, styler, this[IS_EMPTY]); + }; + }, + }; + + const bgModel = 'bg' + model[0].toUpperCase() + model.slice(1); + styles[bgModel] = { + get() { + const {level} = this; + return function (...arguments_) { + const styler = createStyler(getModelAnsi(model, levelMapping[level], 'bgColor', ...arguments_), ansiStyles.bgColor.close, this[STYLER]); + return createBuilder(this, styler, this[IS_EMPTY]); + }; + }, + }; +} + +const proto = Object.defineProperties(() => {}, { + ...styles, + level: { + enumerable: true, + get() { + return this[GENERATOR].level; + }, + set(level) { + this[GENERATOR].level = level; + }, + }, +}); + +const createStyler = (open, close, parent) => { + let openAll; + let closeAll; + if (parent === undefined) { + openAll = open; + closeAll = close; + } else { + openAll = parent.openAll + open; + closeAll = close + parent.closeAll; + } + + return { + open, + close, + openAll, + closeAll, + parent, + }; +}; + +const createBuilder = (self, _styler, _isEmpty) => { + // Single argument is hot path, implicit coercion is faster than anything + // eslint-disable-next-line no-implicit-coercion + const builder = (...arguments_) => applyStyle(builder, (arguments_.length === 1) ? ('' + arguments_[0]) : arguments_.join(' ')); + + // We alter the prototype because we must return a function, but there is + // no way to create a function with a different prototype + Object.setPrototypeOf(builder, proto); + + builder[GENERATOR] = self; + builder[STYLER] = _styler; + builder[IS_EMPTY] = _isEmpty; + + return builder; +}; + +const applyStyle = (self, string) => { + if (self.level <= 0 || !string) { + return self[IS_EMPTY] ? '' : string; + } + + let styler = self[STYLER]; + + if (styler === undefined) { + return string; + } + + const {openAll, closeAll} = styler; + if (string.includes('\u001B')) { + while (styler !== undefined) { + // Replace any instances already present with a re-opening code + // otherwise only the part of the string until said closing code + // will be colored, and the rest will simply be 'plain'. + string = stringReplaceAll(string, styler.close, styler.open); + + styler = styler.parent; + } + } + + // We can move both next actions out of loop, because remaining actions in loop won't have + // any/visible effect on parts we add here. Close the styling before a linebreak and reopen + // after next line to fix a bleed issue on macOS: https://github.com/chalk/chalk/pull/92 + const lfIndex = string.indexOf('\n'); + if (lfIndex !== -1) { + string = stringEncaseCRLFWithFirstIndex(string, closeAll, openAll, lfIndex); + } + + return openAll + string + closeAll; +}; + +Object.defineProperties(createChalk.prototype, styles); + +const chalk = createChalk(); +export const chalkStderr = createChalk({level: stderrColor ? stderrColor.level : 0}); + +export { + modifierNames, + foregroundColorNames, + backgroundColorNames, + colorNames, + + // TODO: Remove these aliases in the next major version + modifierNames as modifiers, + foregroundColorNames as foregroundColors, + backgroundColorNames as backgroundColors, + colorNames as colors, +} from './vendor/ansi-styles/index.js'; + +export { + stdoutColor as supportsColor, + stderrColor as supportsColorStderr, +}; + +export default chalk; diff --git a/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/utilities.js b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/utilities.js new file mode 100644 index 000000000..4366dee0d --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/utilities.js @@ -0,0 +1,33 @@ +// TODO: When targeting Node.js 16, use `String.prototype.replaceAll`. +export function stringReplaceAll(string, substring, replacer) { + let index = string.indexOf(substring); + if (index === -1) { + return string; + } + + const substringLength = substring.length; + let endIndex = 0; + let returnValue = ''; + do { + returnValue += string.slice(endIndex, index) + substring + replacer; + endIndex = index + substringLength; + index = string.indexOf(substring, endIndex); + } while (index !== -1); + + returnValue += string.slice(endIndex); + return returnValue; +} + +export function stringEncaseCRLFWithFirstIndex(string, prefix, postfix, index) { + let endIndex = 0; + let returnValue = ''; + do { + const gotCR = string[index - 1] === '\r'; + returnValue += string.slice(endIndex, (gotCR ? index - 1 : index)) + prefix + (gotCR ? '\r\n' : '\n') + postfix; + endIndex = index + 1; + index = string.indexOf('\n', endIndex); + } while (index !== -1); + + returnValue += string.slice(endIndex); + return returnValue; +} diff --git a/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/ansi-styles/index.d.ts b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/ansi-styles/index.d.ts new file mode 100644 index 000000000..58f133abe --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/ansi-styles/index.d.ts @@ -0,0 +1,236 @@ +export interface CSPair { // eslint-disable-line @typescript-eslint/naming-convention + /** + The ANSI terminal control sequence for starting this style. + */ + readonly open: string; + + /** + The ANSI terminal control sequence for ending this style. + */ + readonly close: string; +} + +export interface ColorBase { + /** + The ANSI terminal control sequence for ending this color. + */ + readonly close: string; + + ansi(code: number): string; + + ansi256(code: number): string; + + ansi16m(red: number, green: number, blue: number): string; +} + +export interface Modifier { + /** + Resets the current color chain. + */ + readonly reset: CSPair; + + /** + Make text bold. + */ + readonly bold: CSPair; + + /** + Emitting only a small amount of light. + */ + readonly dim: CSPair; + + /** + Make text italic. (Not widely supported) + */ + readonly italic: CSPair; + + /** + Make text underline. (Not widely supported) + */ + readonly underline: CSPair; + + /** + Make text overline. + + Supported on VTE-based terminals, the GNOME terminal, mintty, and Git Bash. + */ + readonly overline: CSPair; + + /** + Inverse background and foreground colors. + */ + readonly inverse: CSPair; + + /** + Prints the text, but makes it invisible. + */ + readonly hidden: CSPair; + + /** + Puts a horizontal line through the center of the text. (Not widely supported) + */ + readonly strikethrough: CSPair; +} + +export interface ForegroundColor { + readonly black: CSPair; + readonly red: CSPair; + readonly green: CSPair; + readonly yellow: CSPair; + readonly blue: CSPair; + readonly cyan: CSPair; + readonly magenta: CSPair; + readonly white: CSPair; + + /** + Alias for `blackBright`. + */ + readonly gray: CSPair; + + /** + Alias for `blackBright`. + */ + readonly grey: CSPair; + + readonly blackBright: CSPair; + readonly redBright: CSPair; + readonly greenBright: CSPair; + readonly yellowBright: CSPair; + readonly blueBright: CSPair; + readonly cyanBright: CSPair; + readonly magentaBright: CSPair; + readonly whiteBright: CSPair; +} + +export interface BackgroundColor { + readonly bgBlack: CSPair; + readonly bgRed: CSPair; + readonly bgGreen: CSPair; + readonly bgYellow: CSPair; + readonly bgBlue: CSPair; + readonly bgCyan: CSPair; + readonly bgMagenta: CSPair; + readonly bgWhite: CSPair; + + /** + Alias for `bgBlackBright`. + */ + readonly bgGray: CSPair; + + /** + Alias for `bgBlackBright`. + */ + readonly bgGrey: CSPair; + + readonly bgBlackBright: CSPair; + readonly bgRedBright: CSPair; + readonly bgGreenBright: CSPair; + readonly bgYellowBright: CSPair; + readonly bgBlueBright: CSPair; + readonly bgCyanBright: CSPair; + readonly bgMagentaBright: CSPair; + readonly bgWhiteBright: CSPair; +} + +export interface ConvertColor { + /** + Convert from the RGB color space to the ANSI 256 color space. + + @param red - (`0...255`) + @param green - (`0...255`) + @param blue - (`0...255`) + */ + rgbToAnsi256(red: number, green: number, blue: number): number; + + /** + Convert from the RGB HEX color space to the RGB color space. + + @param hex - A hexadecimal string containing RGB data. + */ + hexToRgb(hex: string): [red: number, green: number, blue: number]; + + /** + Convert from the RGB HEX color space to the ANSI 256 color space. + + @param hex - A hexadecimal string containing RGB data. + */ + hexToAnsi256(hex: string): number; + + /** + Convert from the ANSI 256 color space to the ANSI 16 color space. + + @param code - A number representing the ANSI 256 color. + */ + ansi256ToAnsi(code: number): number; + + /** + Convert from the RGB color space to the ANSI 16 color space. + + @param red - (`0...255`) + @param green - (`0...255`) + @param blue - (`0...255`) + */ + rgbToAnsi(red: number, green: number, blue: number): number; + + /** + Convert from the RGB HEX color space to the ANSI 16 color space. + + @param hex - A hexadecimal string containing RGB data. + */ + hexToAnsi(hex: string): number; +} + +/** +Basic modifier names. +*/ +export type ModifierName = keyof Modifier; + +/** +Basic foreground color names. + +[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support) +*/ +export type ForegroundColorName = keyof ForegroundColor; + +/** +Basic background color names. + +[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support) +*/ +export type BackgroundColorName = keyof BackgroundColor; + +/** +Basic color names. The combination of foreground and background color names. + +[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support) +*/ +export type ColorName = ForegroundColorName | BackgroundColorName; + +/** +Basic modifier names. +*/ +export const modifierNames: readonly ModifierName[]; + +/** +Basic foreground color names. +*/ +export const foregroundColorNames: readonly ForegroundColorName[]; + +/** +Basic background color names. +*/ +export const backgroundColorNames: readonly BackgroundColorName[]; + +/* +Basic color names. The combination of foreground and background color names. +*/ +export const colorNames: readonly ColorName[]; + +declare const ansiStyles: { + readonly modifier: Modifier; + readonly color: ColorBase & ForegroundColor; + readonly bgColor: ColorBase & BackgroundColor; + readonly codes: ReadonlyMap; +} & ForegroundColor & BackgroundColor & Modifier & ConvertColor; + +export default ansiStyles; diff --git a/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/ansi-styles/index.js b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/ansi-styles/index.js new file mode 100644 index 000000000..eaa7bed6c --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/ansi-styles/index.js @@ -0,0 +1,223 @@ +const ANSI_BACKGROUND_OFFSET = 10; + +const wrapAnsi16 = (offset = 0) => code => `\u001B[${code + offset}m`; + +const wrapAnsi256 = (offset = 0) => code => `\u001B[${38 + offset};5;${code}m`; + +const wrapAnsi16m = (offset = 0) => (red, green, blue) => `\u001B[${38 + offset};2;${red};${green};${blue}m`; + +const styles = { + modifier: { + reset: [0, 0], + // 21 isn't widely supported and 22 does the same thing + bold: [1, 22], + dim: [2, 22], + italic: [3, 23], + underline: [4, 24], + overline: [53, 55], + inverse: [7, 27], + hidden: [8, 28], + strikethrough: [9, 29], + }, + color: { + black: [30, 39], + red: [31, 39], + green: [32, 39], + yellow: [33, 39], + blue: [34, 39], + magenta: [35, 39], + cyan: [36, 39], + white: [37, 39], + + // Bright color + blackBright: [90, 39], + gray: [90, 39], // Alias of `blackBright` + grey: [90, 39], // Alias of `blackBright` + redBright: [91, 39], + greenBright: [92, 39], + yellowBright: [93, 39], + blueBright: [94, 39], + magentaBright: [95, 39], + cyanBright: [96, 39], + whiteBright: [97, 39], + }, + bgColor: { + bgBlack: [40, 49], + bgRed: [41, 49], + bgGreen: [42, 49], + bgYellow: [43, 49], + bgBlue: [44, 49], + bgMagenta: [45, 49], + bgCyan: [46, 49], + bgWhite: [47, 49], + + // Bright color + bgBlackBright: [100, 49], + bgGray: [100, 49], // Alias of `bgBlackBright` + bgGrey: [100, 49], // Alias of `bgBlackBright` + bgRedBright: [101, 49], + bgGreenBright: [102, 49], + bgYellowBright: [103, 49], + bgBlueBright: [104, 49], + bgMagentaBright: [105, 49], + bgCyanBright: [106, 49], + bgWhiteBright: [107, 49], + }, +}; + +export const modifierNames = Object.keys(styles.modifier); +export const foregroundColorNames = Object.keys(styles.color); +export const backgroundColorNames = Object.keys(styles.bgColor); +export const colorNames = [...foregroundColorNames, ...backgroundColorNames]; + +function assembleStyles() { + const codes = new Map(); + + for (const [groupName, group] of Object.entries(styles)) { + for (const [styleName, style] of Object.entries(group)) { + styles[styleName] = { + open: `\u001B[${style[0]}m`, + close: `\u001B[${style[1]}m`, + }; + + group[styleName] = styles[styleName]; + + codes.set(style[0], style[1]); + } + + Object.defineProperty(styles, groupName, { + value: group, + enumerable: false, + }); + } + + Object.defineProperty(styles, 'codes', { + value: codes, + enumerable: false, + }); + + styles.color.close = '\u001B[39m'; + styles.bgColor.close = '\u001B[49m'; + + styles.color.ansi = wrapAnsi16(); + styles.color.ansi256 = wrapAnsi256(); + styles.color.ansi16m = wrapAnsi16m(); + styles.bgColor.ansi = wrapAnsi16(ANSI_BACKGROUND_OFFSET); + styles.bgColor.ansi256 = wrapAnsi256(ANSI_BACKGROUND_OFFSET); + styles.bgColor.ansi16m = wrapAnsi16m(ANSI_BACKGROUND_OFFSET); + + // From https://github.com/Qix-/color-convert/blob/3f0e0d4e92e235796ccb17f6e85c72094a651f49/conversions.js + Object.defineProperties(styles, { + rgbToAnsi256: { + value(red, green, blue) { + // We use the extended greyscale palette here, with the exception of + // black and white. normal palette only has 4 greyscale shades. + if (red === green && green === blue) { + if (red < 8) { + return 16; + } + + if (red > 248) { + return 231; + } + + return Math.round(((red - 8) / 247) * 24) + 232; + } + + return 16 + + (36 * Math.round(red / 255 * 5)) + + (6 * Math.round(green / 255 * 5)) + + Math.round(blue / 255 * 5); + }, + enumerable: false, + }, + hexToRgb: { + value(hex) { + const matches = /[a-f\d]{6}|[a-f\d]{3}/i.exec(hex.toString(16)); + if (!matches) { + return [0, 0, 0]; + } + + let [colorString] = matches; + + if (colorString.length === 3) { + colorString = [...colorString].map(character => character + character).join(''); + } + + const integer = Number.parseInt(colorString, 16); + + return [ + /* eslint-disable no-bitwise */ + (integer >> 16) & 0xFF, + (integer >> 8) & 0xFF, + integer & 0xFF, + /* eslint-enable no-bitwise */ + ]; + }, + enumerable: false, + }, + hexToAnsi256: { + value: hex => styles.rgbToAnsi256(...styles.hexToRgb(hex)), + enumerable: false, + }, + ansi256ToAnsi: { + value(code) { + if (code < 8) { + return 30 + code; + } + + if (code < 16) { + return 90 + (code - 8); + } + + let red; + let green; + let blue; + + if (code >= 232) { + red = (((code - 232) * 10) + 8) / 255; + green = red; + blue = red; + } else { + code -= 16; + + const remainder = code % 36; + + red = Math.floor(code / 36) / 5; + green = Math.floor(remainder / 6) / 5; + blue = (remainder % 6) / 5; + } + + const value = Math.max(red, green, blue) * 2; + + if (value === 0) { + return 30; + } + + // eslint-disable-next-line no-bitwise + let result = 30 + ((Math.round(blue) << 2) | (Math.round(green) << 1) | Math.round(red)); + + if (value === 2) { + result += 60; + } + + return result; + }, + enumerable: false, + }, + rgbToAnsi: { + value: (red, green, blue) => styles.ansi256ToAnsi(styles.rgbToAnsi256(red, green, blue)), + enumerable: false, + }, + hexToAnsi: { + value: hex => styles.ansi256ToAnsi(styles.hexToAnsi256(hex)), + enumerable: false, + }, + }); + + return styles; +} + +const ansiStyles = assembleStyles(); + +export default ansiStyles; diff --git a/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/browser.d.ts b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/browser.d.ts new file mode 100644 index 000000000..0e8a9ccb8 --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/browser.d.ts @@ -0,0 +1 @@ +export {default} from './index.js'; diff --git a/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/browser.js b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/browser.js new file mode 100644 index 000000000..fbb6ce0fc --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/browser.js @@ -0,0 +1,34 @@ +/* eslint-env browser */ + +const level = (() => { + if (!('navigator' in globalThis)) { + return 0; + } + + if (globalThis.navigator.userAgentData) { + const brand = navigator.userAgentData.brands.find(({brand}) => brand === 'Chromium'); + if (brand && brand.version > 93) { + return 3; + } + } + + if (/\b(Chrome|Chromium)\//.test(globalThis.navigator.userAgent)) { + return 1; + } + + return 0; +})(); + +const colorSupport = level !== 0 && { + level, + hasBasic: true, + has256: level >= 2, + has16m: level >= 3, +}; + +const supportsColor = { + stdout: colorSupport, + stderr: colorSupport, +}; + +export default supportsColor; diff --git a/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/index.d.ts b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/index.d.ts new file mode 100644 index 000000000..db44a7851 --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/index.d.ts @@ -0,0 +1,55 @@ +import type {WriteStream} from 'node:tty'; + +export type Options = { + /** + Whether `process.argv` should be sniffed for `--color` and `--no-color` flags. + + @default true + */ + readonly sniffFlags?: boolean; +}; + +/** +Levels: +- `0` - All colors disabled. +- `1` - Basic 16 colors support. +- `2` - ANSI 256 colors support. +- `3` - Truecolor 16 million colors support. +*/ +export type ColorSupportLevel = 0 | 1 | 2 | 3; + +/** +Detect whether the terminal supports color. +*/ +export type ColorSupport = { + /** + The color level. + */ + level: ColorSupportLevel; + + /** + Whether basic 16 colors are supported. + */ + hasBasic: boolean; + + /** + Whether ANSI 256 colors are supported. + */ + has256: boolean; + + /** + Whether Truecolor 16 million colors are supported. + */ + has16m: boolean; +}; + +export type ColorInfo = ColorSupport | false; + +export function createSupportsColor(stream?: WriteStream, options?: Options): ColorInfo; + +declare const supportsColor: { + stdout: ColorInfo; + stderr: ColorInfo; +}; + +export default supportsColor; diff --git a/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/index.js b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/index.js new file mode 100644 index 000000000..265d7f858 --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/index.js @@ -0,0 +1,190 @@ +import process from 'node:process'; +import os from 'node:os'; +import tty from 'node:tty'; + +// From: https://github.com/sindresorhus/has-flag/blob/main/index.js +/// function hasFlag(flag, argv = globalThis.Deno?.args ?? process.argv) { +function hasFlag(flag, argv = globalThis.Deno ? globalThis.Deno.args : process.argv) { + const prefix = flag.startsWith('-') ? '' : (flag.length === 1 ? '-' : '--'); + const position = argv.indexOf(prefix + flag); + const terminatorPosition = argv.indexOf('--'); + return position !== -1 && (terminatorPosition === -1 || position < terminatorPosition); +} + +const {env} = process; + +let flagForceColor; +if ( + hasFlag('no-color') + || hasFlag('no-colors') + || hasFlag('color=false') + || hasFlag('color=never') +) { + flagForceColor = 0; +} else if ( + hasFlag('color') + || hasFlag('colors') + || hasFlag('color=true') + || hasFlag('color=always') +) { + flagForceColor = 1; +} + +function envForceColor() { + if ('FORCE_COLOR' in env) { + if (env.FORCE_COLOR === 'true') { + return 1; + } + + if (env.FORCE_COLOR === 'false') { + return 0; + } + + return env.FORCE_COLOR.length === 0 ? 1 : Math.min(Number.parseInt(env.FORCE_COLOR, 10), 3); + } +} + +function translateLevel(level) { + if (level === 0) { + return false; + } + + return { + level, + hasBasic: true, + has256: level >= 2, + has16m: level >= 3, + }; +} + +function _supportsColor(haveStream, {streamIsTTY, sniffFlags = true} = {}) { + const noFlagForceColor = envForceColor(); + if (noFlagForceColor !== undefined) { + flagForceColor = noFlagForceColor; + } + + const forceColor = sniffFlags ? flagForceColor : noFlagForceColor; + + if (forceColor === 0) { + return 0; + } + + if (sniffFlags) { + if (hasFlag('color=16m') + || hasFlag('color=full') + || hasFlag('color=truecolor')) { + return 3; + } + + if (hasFlag('color=256')) { + return 2; + } + } + + // Check for Azure DevOps pipelines. + // Has to be above the `!streamIsTTY` check. + if ('TF_BUILD' in env && 'AGENT_NAME' in env) { + return 1; + } + + if (haveStream && !streamIsTTY && forceColor === undefined) { + return 0; + } + + const min = forceColor || 0; + + if (env.TERM === 'dumb') { + return min; + } + + if (process.platform === 'win32') { + // Windows 10 build 10586 is the first Windows release that supports 256 colors. + // Windows 10 build 14931 is the first release that supports 16m/TrueColor. + const osRelease = os.release().split('.'); + if ( + Number(osRelease[0]) >= 10 + && Number(osRelease[2]) >= 10_586 + ) { + return Number(osRelease[2]) >= 14_931 ? 3 : 2; + } + + return 1; + } + + if ('CI' in env) { + if (['GITHUB_ACTIONS', 'GITEA_ACTIONS', 'CIRCLECI'].some(key => key in env)) { + return 3; + } + + if (['TRAVIS', 'APPVEYOR', 'GITLAB_CI', 'BUILDKITE', 'DRONE'].some(sign => sign in env) || env.CI_NAME === 'codeship') { + return 1; + } + + return min; + } + + if ('TEAMCITY_VERSION' in env) { + return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION) ? 1 : 0; + } + + if (env.COLORTERM === 'truecolor') { + return 3; + } + + if (env.TERM === 'xterm-kitty') { + return 3; + } + + if (env.TERM === 'xterm-ghostty') { + return 3; + } + + if (env.TERM === 'wezterm') { + return 3; + } + + if ('TERM_PROGRAM' in env) { + const version = Number.parseInt((env.TERM_PROGRAM_VERSION || '').split('.')[0], 10); + + switch (env.TERM_PROGRAM) { + case 'iTerm.app': { + return version >= 3 ? 3 : 2; + } + + case 'Apple_Terminal': { + return 2; + } + // No default + } + } + + if (/-256(color)?$/i.test(env.TERM)) { + return 2; + } + + if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(env.TERM)) { + return 1; + } + + if ('COLORTERM' in env) { + return 1; + } + + return min; +} + +export function createSupportsColor(stream, options = {}) { + const level = _supportsColor(stream, { + streamIsTTY: stream && stream.isTTY, + ...options, + }); + + return translateLevel(level); +} + +const supportsColor = { + stdout: createSupportsColor({isTTY: tty.isatty(1)}), + stderr: createSupportsColor({isTTY: tty.isatty(2)}), +}; + +export default supportsColor; diff --git a/tests/fixtures/npm/cases/chalk5-builder/package.json b/tests/fixtures/npm/cases/chalk5-builder/package.json new file mode 100644 index 000000000..3804daa8d --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/package.json @@ -0,0 +1 @@ +{ "name": "chalk5-case", "private": true, "type": "module" } diff --git a/tests/fixtures/npm/cases/hot-shots-buffered/main.ts b/tests/fixtures/npm/cases/hot-shots-buffered/main.ts new file mode 100644 index 000000000..d17ce1167 --- /dev/null +++ b/tests/fixtures/npm/cases/hot-shots-buffered/main.ts @@ -0,0 +1,80 @@ +// The hot-shots StatsD client (compiled from scriptc's built-in shim) in +// buffered mode: metrics join one datagram until maxBufferSize, close +// flushes the rest, child clients share the buffer. Also explicit sample +// rates of 1, the telegraf line format, stat arrays with a callback, and +// suffixes. +import * as dgram from "node:dgram"; +import { StatsD } from "hot-shots"; + +const server = dgram.createSocket("udp4"); +const received: string[] = []; +let waiting: (() => void) | null = null; +let want = 0; +server.on("message", (msg) => { + received.push(msg.toString()); + if (waiting !== null && received.length >= want) { + const done = waiting; + waiting = null; + done(); + } +}); + +function datagrams(count: number): Promise { + want = count; + return new Promise((resolve) => { + if (received.length >= want) resolve(); + else waiting = resolve; + }); +} + +function flush(label: string): void { + console.log("--", label); + for (const line of received.splice(0)) console.log(JSON.stringify(line)); +} + +async function run(port: number): Promise { + const buffered = new StatsD({ + host: "127.0.0.1", + port, + maxBufferSize: 64, + bufferFlushInterval: 60000, + suffix: ".s", + globalTags: ["team:core"], + }); + buffered.increment("one"); + buffered.increment("two", 2); + buffered.gauge("three", 3); + buffered.histogram("four", 4, ["h:1"]); + const child = buffered.childClient({ prefix: "kid.", suffix: ".x" }); + child.increment("five"); + await new Promise((resolve) => buffered.close(() => resolve())); + await datagrams(2); + flush("buffered"); + + const telegraf = new StatsD({ host: "127.0.0.1", port, telegraf: true, globalTags: { region: "eu" } }); + telegraf.increment("hits", 1, 1, { route: "/x" }); + telegraf.timing("lat", 7, ["kind:a"]); + await datagrams(2); + flush("telegraf"); + + const plain = new StatsD({ host: "127.0.0.1", port, prefix: "p." }); + await new Promise((resolve) => { + plain.increment(["a", "b", "c"], 1, 1, ["t:1"], (err: unknown, bytes: unknown) => { + console.log("array sent", err, bytes); + resolve(); + }); + }); + plain.set("users", 42, 1); + plain.unique("visitors", "v1"); + plain.decrement("down", 3, ["d:1"]); + plain.decrement("gone"); + await datagrams(7); + flush("plain"); + await new Promise((resolve) => { + telegraf.close(() => plain.close(() => resolve())); + }); +} + +server.bind(0, "127.0.0.1", () => { + void run(server.address().port).then(() => server.close(() => console.log("server closed"))); +}); diff --git a/tests/fixtures/npm/cases/hot-shots-buffered/package.json b/tests/fixtures/npm/cases/hot-shots-buffered/package.json new file mode 100644 index 000000000..5ffd9800b --- /dev/null +++ b/tests/fixtures/npm/cases/hot-shots-buffered/package.json @@ -0,0 +1 @@ +{ "type": "module" } diff --git a/tests/fixtures/npm/cases/hot-shots-send-error/main.ts b/tests/fixtures/npm/cases/hot-shots-send-error/main.ts new file mode 100644 index 000000000..e829c5e33 --- /dev/null +++ b/tests/fixtures/npm/cases/hot-shots-send-error/main.ts @@ -0,0 +1,27 @@ +// The hot-shots StatsD client (compiled from scriptc's built-in shim) when a +// send fails: with a callback or an errorHandler the error goes there; with +// neither, the client logs it and emits it on the socket, where an 'error' +// event nobody listens for ends the process with status 1. +import { StatsD } from "hot-shots"; + +// A datagram larger than udp allows fails with EMSGSIZE before it leaves +// the host; port 9 keeps the message the same on every run. +const big = "x".repeat(70000); + +const withCallback = new StatsD({ host: "127.0.0.1", port: 9 }); +withCallback.increment(big, 1, 1, [], (err: Error | null | undefined) => { + console.log("callback:", err ? err.message : "no error"); + withCallback.close(); + + const withHandler = new StatsD({ + host: "127.0.0.1", + port: 9, + errorHandler: (error: Error) => { + console.log("errorHandler:", error.message); + withHandler.close(); + console.log("without a callback or errorHandler"); + new StatsD({ host: "127.0.0.1", port: 9 }).increment(big); + }, + }); + withHandler.increment(big); +}); diff --git a/tests/fixtures/npm/cases/hot-shots-send-error/package.json b/tests/fixtures/npm/cases/hot-shots-send-error/package.json new file mode 100644 index 000000000..5ffd9800b --- /dev/null +++ b/tests/fixtures/npm/cases/hot-shots-send-error/package.json @@ -0,0 +1 @@ +{ "type": "module" } diff --git a/tests/fixtures/npm/cases/hot-shots-tcp/main.ts b/tests/fixtures/npm/cases/hot-shots-tcp/main.ts new file mode 100644 index 000000000..0812c90f1 --- /dev/null +++ b/tests/fixtures/npm/cases/hot-shots-tcp/main.ts @@ -0,0 +1,33 @@ +// The hot-shots StatsD client over tcp (compiled from scriptc's built-in +// shim): a send completes once the socket has written its line, so close() +// waits for lines that a full socket buffer holds back, and the receiver +// gets every line before the connection ends. +import * as net from "node:net"; +import { StatsD } from "hot-shots"; + +const LINES = 20000; +const name = "m".repeat(1000); +let bytes = 0; +let ended: () => void = () => {}; +const serverDone = new Promise((resolve) => { + ended = resolve; +}); +const server = net.createServer((socket) => { + socket.pause(); + setTimeout(() => socket.resume(), 150); + socket.on("data", (chunk: Buffer) => { + bytes += chunk.length; + }); + socket.on("end", () => ended()); +}); + +server.listen(0, "127.0.0.1", () => { + const port = (server.address() as net.AddressInfo).port; + const client = new StatsD({ host: "127.0.0.1", port, protocol: "tcp" }); + for (let i = 0; i < LINES; i++) client.increment(name); + const closed = new Promise((resolve) => client.close(() => resolve())); + void Promise.all([closed, serverDone]).then(() => { + console.log("closed; received", bytes, "of", LINES * (name.length + 5), "bytes"); + server.close(() => console.log("server closed")); + }); +}); diff --git a/tests/fixtures/npm/cases/hot-shots-tcp/package.json b/tests/fixtures/npm/cases/hot-shots-tcp/package.json new file mode 100644 index 000000000..5ffd9800b --- /dev/null +++ b/tests/fixtures/npm/cases/hot-shots-tcp/package.json @@ -0,0 +1 @@ +{ "type": "module" } diff --git a/tests/harness/__snapshots__/arguments-length-self-call.ts.txt b/tests/harness/__snapshots__/arguments-length-self-call.ts.txt new file mode 100644 index 000000000..7e94b56c6 --- /dev/null +++ b/tests/harness/__snapshots__/arguments-length-self-call.ts.txt @@ -0,0 +1,6 @@ +arguments-length-self-call.ts:6:32 - error SC1090: 'arguments.length' in a function expression that is called both through its own name and through a function type with more parameters (the count varies per call) are not supported yet + + 5 | const count: (a: number, b: number) => number = function self(a: number): number { + 6 | return a > 0 ? self(a - 1) + arguments.length : arguments.length; + | ^~~~~~~~~~~~~~~~ + 7 | }; \ No newline at end of file diff --git a/tests/harness/__snapshots__/coverage-js-builtin-callable-alias.txt b/tests/harness/__snapshots__/coverage-js-builtin-callable-alias.txt index 53eac6c3c..6291c291f 100644 --- a/tests/harness/__snapshots__/coverage-js-builtin-callable-alias.txt +++ b/tests/harness/__snapshots__/coverage-js-builtin-callable-alias.txt @@ -3,7 +3,7 @@ scriptc coverage tests/coverage-fixtures/js-builtin-callable-alias.js statements analyzed 3 compile statically 2 (66%) - deferred to runtime 1 site (JS statements that throw their fence if executed) + runs at startup 1 site (JS statements in module top-level code, which runs when the module loads) ×1 SC1090 a builtin function alias as an escaping value (call 'ext' directly) at tests/coverage-fixtures/js-builtin-callable-alias.js:8:13 hint: this form is valid TypeScript but does not compile yet — rewrite it with a supported equivalent of the construct the message names (https://scriptc.dev/limitations lists the boundary) diff --git a/tests/harness/__snapshots__/filter-narrow.ts.txt b/tests/harness/__snapshots__/filter-narrow.ts.txt deleted file mode 100644 index bc5180219..000000000 --- a/tests/harness/__snapshots__/filter-narrow.ts.txt +++ /dev/null @@ -1,20 +0,0 @@ -filter-narrow.ts:9:17 - error SC1090: '.filter' narrowing 'number | string | undefined' elements to the multi-arm 'number | string' (only a SINGLE arm re-tags — keep the receiver's element type instead — annotate the callback's return ': boolean' (the checker then skips the predicate) or annotate the result with the receiver's own element type — and narrow the elements after) is not supported yet - - 8 | // Multi-arm narrowing: string | number is not a single arm. - 9 | const defined = xs.filter((x) => x !== undefined); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 10 | console.log(defined.length); - -filter-narrow.ts:14:27 - error SC1090: narrowing '.filter' with a hand-written type predicate (a written 'x is T' is an unchecked assertion nothing validates at runtime — keep the receiver's element type instead — annotate the callback's return ': boolean' (the checker then skips the predicate) or annotate the result with the receiver's own element type — and narrow the elements after) is not supported yet - - 13 | const ys: (string | undefined)[] = ["a", undefined]; - 14 | const strings = ys.filter((x): x is string => typeof x === "string"); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 15 | console.log(strings.length); - -filter-narrow.ts:21:28 - error SC1090: narrowing '.filter' through a callback VALUE (only an inline callback whose predicate the checker inferred can re-tag — keep the receiver's element type instead — annotate the callback's return ': boolean' (the checker then skips the predicate) or annotate the result with the receiver's own element type — and narrow the elements after) is not supported yet - - 20 | } - 21 | const viaValue = ys.filter(isString); - | ^~~~~~~~ - 22 | console.log(viaValue.length); \ No newline at end of file diff --git a/tests/harness/__snapshots__/node-types-fenced.txt b/tests/harness/__snapshots__/node-types-fenced.txt index 133978b5e..cabce49c4 100644 --- a/tests/harness/__snapshots__/node-types-fenced.txt +++ b/tests/harness/__snapshots__/node-types-fenced.txt @@ -117,7 +117,7 @@ fenced.ts:69:71 - error SC2020: 'createSecureServer option 'streamResetBurst' wi fenced.ts:76:1 - error SC2020: 'crypto.publicEncrypt' is typed by @types/node but has no scriptc lowering yet - 75 | import { checkPrimeSync, createCipheriv, publicEncrypt, setFips } from "node:crypto"; + 75 | import { createCipheriv, publicEncrypt, secureHeapUsed, setFips } from "node:crypto"; 76 | publicEncrypt("key", Buffer.alloc(1)); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 77 | createCipheriv("aes-128-ccm", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); @@ -129,22 +129,22 @@ fenced.ts:77:16 - error SC2020: 'crypto.createCipheriv with cipher 'aes-128-ccm' 76 | publicEncrypt("key", Buffer.alloc(1)); 77 | createCipheriv("aes-128-ccm", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); | ^~~~~~~~~~~~~ - 78 | checkPrimeSync(7n); + 78 | secureHeapUsed(); - hint: the supported ciphers are aes-128-cbc, aes-192-cbc, aes-256-cbc, aes-128-ctr, aes-192-ctr, aes-256-ctr, aes-128-gcm, aes-192-gcm, aes-256-gcm, and chacha20-poly1305 + hint: the supported ciphers are aes-128, aes-192 and aes-256 in cbc, ctr, gcm, ecb, cfb, cfb8, cfb1 and ofb modes, des-ede3 in cbc, ecb, cfb, cfb8, cfb1 and ofb modes, des-ede in cbc, ecb, cfb and ofb modes, chacha20, and chacha20-poly1305 -fenced.ts:78:1 - error SC2020: 'crypto.checkPrimeSync' is typed by @types/node but has no scriptc lowering yet +fenced.ts:78:1 - error SC2020: 'crypto.secureHeapUsed' is typed by @types/node but has no scriptc lowering yet 77 | createCipheriv("aes-128-ccm", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); - 78 | checkPrimeSync(7n); - | ^~~~~~~~~~~~~~~~~~ + 78 | secureHeapUsed(); + | ^~~~~~~~~~~~~~~~ 79 | setFips(false); hint: the type checker sees everything @types/node declares, but only the supported surface compiles (https://scriptc.dev/limitations) fenced.ts:79:1 - error SC2020: 'crypto.setFips' is typed by @types/node but has no scriptc lowering yet - 78 | checkPrimeSync(7n); + 78 | secureHeapUsed(); 79 | setFips(false); | ^~~~~~~~~~~~~~ 80 | fetch("https://example.invalid/", { diff --git a/tests/harness/bootstrap-seed.test.ts b/tests/harness/bootstrap-seed.test.ts index 2e8b3bad9..76b1f6de5 100644 --- a/tests/harness/bootstrap-seed.test.ts +++ b/tests/harness/bootstrap-seed.test.ts @@ -78,6 +78,10 @@ test("CI restores each seed only into its matching instrumented bootstrap lane", const execute = checks.steps.find((step) => step.run?.split("\n").includes("node scripts/ci-native-bootstrap.mjs"), )!; + // The sanitized seed's build needs more memory than the runner has: swap backs it. + const swap = seed.steps.findIndex((step) => step.run?.includes("swapon /mnt/swapfile")); + expect(seed.steps[swap]?.if).toBe("matrix.flavor == 'san'"); + expect(swap).toBeLessThan(seed.steps.indexOf(prepare)); expect(prepare.env?.SCRIPTC_BOOTSTRAP_SEED_DIRECTORY).toBe("${{ runner.temp }}/native-bootstrap"); expect(execute.env?.SCRIPTC_BOOTSTRAP_SEED_DIRECTORY).toBe( prepare.env?.SCRIPTC_BOOTSTRAP_SEED_DIRECTORY, diff --git a/tests/harness/corpus-costs/4638-contract-program-lifetime-arrays.ts.json b/tests/harness/corpus-costs/4638-contract-program-lifetime-arrays.ts.json new file mode 100644 index 000000000..8512b117c --- /dev/null +++ b/tests/harness/corpus-costs/4638-contract-program-lifetime-arrays.ts.json @@ -0,0 +1 @@ +{ "plain": 2800, "san": 4600 } diff --git a/tests/harness/corpus-costs/4639-contract-program-lifetime-maps.ts.json b/tests/harness/corpus-costs/4639-contract-program-lifetime-maps.ts.json new file mode 100644 index 000000000..d12d0c3a4 --- /dev/null +++ b/tests/harness/corpus-costs/4639-contract-program-lifetime-maps.ts.json @@ -0,0 +1 @@ +{ "plain": 2500, "san": 3600 } diff --git a/tests/harness/corpus-costs/4814-integer-or-nan-indexes.ts.json b/tests/harness/corpus-costs/4814-integer-or-nan-indexes.ts.json new file mode 100644 index 000000000..23f4b5b65 --- /dev/null +++ b/tests/harness/corpus-costs/4814-integer-or-nan-indexes.ts.json @@ -0,0 +1 @@ +{"plain":3473,"san":4706} diff --git a/tests/harness/corpus-costs/4815-touint32-wide-magnitudes.ts.json b/tests/harness/corpus-costs/4815-touint32-wide-magnitudes.ts.json new file mode 100644 index 000000000..ee076af6b --- /dev/null +++ b/tests/harness/corpus-costs/4815-touint32-wide-magnitudes.ts.json @@ -0,0 +1 @@ +{"plain":2871,"san":4039} diff --git a/tests/harness/corpus-costs/6915-key-order-narrowed-throw.ts.json b/tests/harness/corpus-costs/6915-key-order-narrowed-throw.ts.json new file mode 100644 index 000000000..fae2adeb4 --- /dev/null +++ b/tests/harness/corpus-costs/6915-key-order-narrowed-throw.ts.json @@ -0,0 +1 @@ +{ "plain": 1500, "san": 9000 } diff --git a/tests/harness/corpus-costs/6916-index-record-spread-overrides.ts.json b/tests/harness/corpus-costs/6916-index-record-spread-overrides.ts.json new file mode 100644 index 000000000..fae2adeb4 --- /dev/null +++ b/tests/harness/corpus-costs/6916-index-record-spread-overrides.ts.json @@ -0,0 +1 @@ +{ "plain": 1500, "san": 9000 } diff --git a/tests/harness/corpus-costs/6926-dynamic-collection-seeds.mjs.json b/tests/harness/corpus-costs/6926-dynamic-collection-seeds.mjs.json new file mode 100644 index 000000000..b4a56f5c5 --- /dev/null +++ b/tests/harness/corpus-costs/6926-dynamic-collection-seeds.mjs.json @@ -0,0 +1 @@ +{ "plain": 3813, "san": 3374 } diff --git a/tests/harness/corpus-costs/6927-dynamic-collection-seeds.ts.json b/tests/harness/corpus-costs/6927-dynamic-collection-seeds.ts.json new file mode 100644 index 000000000..93da087d2 --- /dev/null +++ b/tests/harness/corpus-costs/6927-dynamic-collection-seeds.ts.json @@ -0,0 +1 @@ +{ "plain": 2436, "san": 2273 } diff --git a/tests/harness/corpus-costs/6998-typed-array-while-loop-versions.ts.json b/tests/harness/corpus-costs/6998-typed-array-while-loop-versions.ts.json new file mode 100644 index 000000000..204a01a83 --- /dev/null +++ b/tests/harness/corpus-costs/6998-typed-array-while-loop-versions.ts.json @@ -0,0 +1 @@ +{ "plain": 3700, "san": 5000 } diff --git a/tests/harness/corpus-costs/7002-empty-array-contextual-slots/main.ts.json b/tests/harness/corpus-costs/7002-empty-array-contextual-slots/main.ts.json new file mode 100644 index 000000000..9495bb65c --- /dev/null +++ b/tests/harness/corpus-costs/7002-empty-array-contextual-slots/main.ts.json @@ -0,0 +1 @@ +{ "plain": 2282, "san": 1173 } diff --git a/tests/harness/corpus-costs/7005-dyn-constructor-instanceof/main.ts.json b/tests/harness/corpus-costs/7005-dyn-constructor-instanceof/main.ts.json new file mode 100644 index 000000000..f84e21437 --- /dev/null +++ b/tests/harness/corpus-costs/7005-dyn-constructor-instanceof/main.ts.json @@ -0,0 +1 @@ +{ "plain": 2308, "san": 955 } diff --git a/tests/harness/corpus-costs/7006-evolving-array-aliases.ts.json b/tests/harness/corpus-costs/7006-evolving-array-aliases.ts.json new file mode 100644 index 000000000..f34efd332 --- /dev/null +++ b/tests/harness/corpus-costs/7006-evolving-array-aliases.ts.json @@ -0,0 +1 @@ +{ "plain": 2292, "san": 3445 } diff --git a/tests/harness/corpus-costs/7007-filter-written-type-predicates.ts.json b/tests/harness/corpus-costs/7007-filter-written-type-predicates.ts.json new file mode 100644 index 000000000..188fa7fab --- /dev/null +++ b/tests/harness/corpus-costs/7007-filter-written-type-predicates.ts.json @@ -0,0 +1 @@ +{ "plain": 3510, "san": 2873 } diff --git a/tests/harness/corpus-costs/7008-js-unit-initialized-properties/main.ts.json b/tests/harness/corpus-costs/7008-js-unit-initialized-properties/main.ts.json new file mode 100644 index 000000000..3a6df58d2 --- /dev/null +++ b/tests/harness/corpus-costs/7008-js-unit-initialized-properties/main.ts.json @@ -0,0 +1 @@ +{ "plain": 3065, "san": 2623 } diff --git a/tests/harness/corpus-costs/7011-for-in-dictionary-deletes.ts.json b/tests/harness/corpus-costs/7011-for-in-dictionary-deletes.ts.json new file mode 100644 index 000000000..e172eed8a --- /dev/null +++ b/tests/harness/corpus-costs/7011-for-in-dictionary-deletes.ts.json @@ -0,0 +1 @@ +{ "plain": 1527, "san": 2135 } diff --git a/tests/harness/corpus-costs/7030-spawn-sync-env.ts.json b/tests/harness/corpus-costs/7030-spawn-sync-env.ts.json new file mode 100644 index 000000000..cf8ef4d26 --- /dev/null +++ b/tests/harness/corpus-costs/7030-spawn-sync-env.ts.json @@ -0,0 +1 @@ +{ "plain": 2460, "san": 4060 } diff --git a/tests/harness/corpus-costs/7036-child-env-path-lookup.ts.json b/tests/harness/corpus-costs/7036-child-env-path-lookup.ts.json new file mode 100644 index 000000000..f9bfce7d9 --- /dev/null +++ b/tests/harness/corpus-costs/7036-child-env-path-lookup.ts.json @@ -0,0 +1 @@ +{ "plain": 2570, "san": 4110 } diff --git a/tests/harness/corpus-costs/7143-optional-read-integer-index.ts.json b/tests/harness/corpus-costs/7143-optional-read-integer-index.ts.json new file mode 100644 index 000000000..16148d8ce --- /dev/null +++ b/tests/harness/corpus-costs/7143-optional-read-integer-index.ts.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 5400 } diff --git a/tests/harness/corpus-costs/7150-cipher-legacy-modes.ts.json b/tests/harness/corpus-costs/7150-cipher-legacy-modes.ts.json new file mode 100644 index 000000000..40196decf --- /dev/null +++ b/tests/harness/corpus-costs/7150-cipher-legacy-modes.ts.json @@ -0,0 +1 @@ +{ "plain": 6099, "san": 8251 } diff --git a/tests/harness/corpus-costs/7151-cipher-legacy-errors.cjs.json b/tests/harness/corpus-costs/7151-cipher-legacy-errors.cjs.json new file mode 100644 index 000000000..6a82dd081 --- /dev/null +++ b/tests/harness/corpus-costs/7151-cipher-legacy-errors.cjs.json @@ -0,0 +1 @@ +{ "plain": 7845, "san": 9581 } diff --git a/tests/harness/corpus-costs/7152-crypto-prime.ts.json b/tests/harness/corpus-costs/7152-crypto-prime.ts.json new file mode 100644 index 000000000..4df2be436 --- /dev/null +++ b/tests/harness/corpus-costs/7152-crypto-prime.ts.json @@ -0,0 +1 @@ +{ "plain": 7273, "san": 10284 } diff --git a/tests/harness/corpus-costs/7153-crypto-prime-errors.cjs.json b/tests/harness/corpus-costs/7153-crypto-prime-errors.cjs.json new file mode 100644 index 000000000..d20dc198a --- /dev/null +++ b/tests/harness/corpus-costs/7153-crypto-prime-errors.cjs.json @@ -0,0 +1 @@ +{ "plain": 5893, "san": 5691 } diff --git a/tests/harness/corpus-costs/7184-reduce-runtime-accumulator.ts.json b/tests/harness/corpus-costs/7184-reduce-runtime-accumulator.ts.json new file mode 100644 index 000000000..c480bea3f --- /dev/null +++ b/tests/harness/corpus-costs/7184-reduce-runtime-accumulator.ts.json @@ -0,0 +1 @@ +{ "plain": 2541, "san": 3260 } diff --git a/tests/harness/corpus-costs/7185-array-callback-arguments-length.ts.json b/tests/harness/corpus-costs/7185-array-callback-arguments-length.ts.json new file mode 100644 index 000000000..b86a9e70a --- /dev/null +++ b/tests/harness/corpus-costs/7185-array-callback-arguments-length.ts.json @@ -0,0 +1 @@ +{ "plain": 2528, "san": 3201 } diff --git a/tests/harness/corpus-costs/7261-acyclic-result-builders.ts.json b/tests/harness/corpus-costs/7261-acyclic-result-builders.ts.json new file mode 100644 index 000000000..30b7c116f --- /dev/null +++ b/tests/harness/corpus-costs/7261-acyclic-result-builders.ts.json @@ -0,0 +1 @@ +{ "plain": 3336, "san": 4712 } diff --git a/tests/harness/corpus-costs/7301-running-extrema-branches.ts.json b/tests/harness/corpus-costs/7301-running-extrema-branches.ts.json new file mode 100644 index 000000000..74df88f0c --- /dev/null +++ b/tests/harness/corpus-costs/7301-running-extrema-branches.ts.json @@ -0,0 +1 @@ +{ "plain": 5300, "san": 5800 } diff --git a/tests/harness/corpus-costs/7322-union-scalar-equality.ts.json b/tests/harness/corpus-costs/7322-union-scalar-equality.ts.json new file mode 100644 index 000000000..2df20a0ed --- /dev/null +++ b/tests/harness/corpus-costs/7322-union-scalar-equality.ts.json @@ -0,0 +1 @@ +{ "plain": 2100, "san": 2800 } diff --git a/tests/harness/corpus-costs/7323-loop-bound-parameter-versions.ts.json b/tests/harness/corpus-costs/7323-loop-bound-parameter-versions.ts.json new file mode 100644 index 000000000..ee18f5f11 --- /dev/null +++ b/tests/harness/corpus-costs/7323-loop-bound-parameter-versions.ts.json @@ -0,0 +1 @@ +{ "plain": 2000, "san": 2800 } diff --git a/tests/harness/corpus-costs/7324-loop-bound-parameter-window-sums.ts.json b/tests/harness/corpus-costs/7324-loop-bound-parameter-window-sums.ts.json new file mode 100644 index 000000000..9622c5beb --- /dev/null +++ b/tests/harness/corpus-costs/7324-loop-bound-parameter-window-sums.ts.json @@ -0,0 +1 @@ +{ "plain": 2000, "san": 3000 } diff --git a/tests/harness/coverage.test.ts b/tests/harness/coverage.test.ts index 86d202b46..3ed562aea 100644 --- a/tests/harness/coverage.test.ts +++ b/tests/harness/coverage.test.ts @@ -182,6 +182,57 @@ test("statements of a signature-blocked function count as not static", () => { expect(coverage.stats.statementsFailed + (un?.statementsFailed ?? 0)).toBe(5); }); +test("the body of a signature-blocked function reports its diagnostics", () => { + const { coverage } = analyze(fixture("blocked-signature-body.ts")); + const sites = (diags: readonly { code: string; message: string }[]) => + diags.map((d) => `${d.code} ${d.message}`).sort(); + // measure is called: its body's diagnostics join the reached ones. + expect(sites(coverage.diagnostics)).toEqual([ + "SC2004 uses of 'extra' inherit the blocker on its declaration", + "SC2004 uses of 'measure' inherit the blocker on its declaration", + "SC2004 uses of 'target' inherit the blocker on its declaration", + "SC2020 'WeakRef' is part of the standard library types but has no scriptc lowering yet", + "SC2020 'new WeakRef' is part of the standard library types but has no scriptc lowering yet", + ]); + // Nothing calls unused or described: their signatures and bodies report + // as unreached. described's declared `this` is its receiver, so + // `this.label` reports nothing. + expect(sites(coverage.unreached?.diagnostics ?? [])).toEqual([ + "SC2004 uses of 'extra' inherit the blocker on its declaration", + "SC2004 uses of 'target' inherit the blocker on its declaration", + "SC2020 'WeakRef' is part of the standard library types but has no scriptc lowering yet", + "SC2020 'WeakRef' is part of the standard library types but has no scriptc lowering yet", + "SC2020 'new WeakRef' is part of the standard library types but has no scriptc lowering yet", + ]); + // The bodies' statements still count once each, as not static. + const un = coverage.unreached?.stats; + expect(coverage.stats.functionsSkipped + (un?.functionsSkipped ?? 0)).toBe(3); + expect(coverage.stats.statementsTotal + (un?.statementsTotal ?? 0)).toBe(7); + expect(coverage.stats.statementsFailed + (un?.statementsFailed ?? 0)).toBe(7); +}); + +test("deferred fences in a module's top-level code report as running at startup", () => { + const { coverage, sourceTexts } = analyze(fixture("startup-fences.js")); + expect( + (coverage.runtimeFences ?? []).map((d) => [d.code, d.loc.start, d.startup === true]), + ).toEqual([ + ["SC1090", expect.any(Number), true], + ["SC1090", expect.any(Number), false], + ]); + const out = report(fixture("startup-fences.js")); + expect(out).toMatch( + /runs at startup {3}1 site .*\n.*instanceof against this native constructor\n\s+at tests\/coverage-fixtures\/startup-fences\.js:5:13/, + ); + expect(out).toMatch( + /deferred to runtime {3}1 site .*\n.*instanceof against this native constructor\n\s+at tests\/coverage-fixtures\/startup-fences\.js:7:10/, + ); + const envelope = coverageEnvelope(coverage, { compilerVersion: "test", sourceTexts }); + expect(envelope.diagnostics.map((d) => [d.scope, d.line, d.startup])).toEqual([ + ["deferred", 5, true], + ["deferred", 7, undefined], + ]); +}); + test("a failed unreached JavaScript statement is always explained", () => { const { coverage } = analyze(fixture("unreached-js-fence.js")); const failed = diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index b5fb028f9..c8b0b51b8 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -847,8 +847,9 @@ try { expect(r.exitCode).toBe(0); expect(r.stdout).toBe( "Error SC2020 The cipher 'aes-128-ccm' has no native implementation; the supported ciphers are " + - "aes-128-cbc, aes-192-cbc, aes-256-cbc, aes-128-ctr, aes-192-ctr, aes-256-ctr, aes-128-gcm, " + - "aes-192-gcm, aes-256-gcm and chacha20-poly1305\n" + + "aes-128, aes-192 and aes-256 in cbc, ctr, gcm, ecb, cfb, cfb8, cfb1 and ofb modes, des-ede3 in " + + "cbc, ecb, cfb, cfb8, cfb1 and ofb modes, des-ede in cbc, ecb, cfb and ofb modes, chacha20 and " + + "chacha20-poly1305\n" + "Error ERR_CRYPTO_UNKNOWN_CIPHER Unknown cipher\n", ); expect(r.stderr).toBe(""); diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index c0176eb93..c4013d288 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -108,7 +108,7 @@ for (const backend of ["llvm"] as const) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" }, + runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: [], sanitizer: "external-toolchain-required" }, flavors: {