Merge main

This commit is contained in:
Chris Tate
2026-10-24 02:22:49 -05:00
376 changed files with 47912 additions and 723 deletions
+10
View File
@@ -216,9 +216,19 @@ jobs:
# Start the corpus immediately beside a serial side queue. One shared
# worker budget covers both processes; file and case shard axes stay
# separate so every fixture still runs exactly once in each lane.
# The tests run in a transient scope capped below the runner's 16 GB, so
# the runner itself keeps memory: past 14 GB of RAM the scope swaps, as
# the whole runner did before (a sanitized shard peaks near 16 GB), and
# when RAM and swap run out the kernel kills the scope's largest process.
# OOMPolicy=continue leaves the rest of the scope running, so vitest
# reports the killed worker and the job log names the test.
- name: Tests (corpus and integration suites)
run: |
ulimit -c 0
if [ -d /run/systemd/system ]; then
exec sudo --preserve-env systemd-run --scope --quiet -p MemoryMax=14G -p OOMPolicy=continue \
--uid="$(id -u)" --gid="$(id -g)" -- env PATH="$PATH" HOME="$HOME" node scripts/ci-test.mjs
fi
node scripts/ci-test.mjs
tests_macos:
+25 -2
View File
@@ -6,6 +6,31 @@ All notable changes to scriptc will be documented in this file.
<!-- release:start -->
## 0.4.4
### Highlights
- **Fixes for 0.4.3:**
- Unchanged rebuilds hit the frontend cache again. An unchanged rebuild of a 23-module program takes 93 ms instead of 1.26 s, and of tsc-ts (dev) 1.6 s instead of 58.2 s.
- The native compiler builds programs with several typed-array accesses at one masked index in a loop again, including tsc-ts's release build, which failed with SC3004.
- `x!` returned from a function, used as an arrow function's result, or passed as an argument keeps null or undefined when the destination type holds it, as JavaScript does. `x!` operands of `+`, `+=` and other conversions that hold undefined or null convert as Node does.
- Effect 4 programs whose generators yield clients built by other generators run again.
- The cache for untyped property access in programs that declare classes is reverted, fixing a crash in Effect 4 observability programs.
- **Faster:**
- Number compares and typed-array indexes use the exact integer of sums like `i + 1`: a heap sift-down over an `Int32Array` runs 17.5% faster.
- glibc's per-thread heaps use huge pages: the VS Code type-check runs 3.4% faster.
- Mid-size executables (over 1 MiB of LLVM text) build in at least eight partitions: the LLVM backend phase takes 34 to 74% less time for three.js programs of 1.4 to 7.8 MB.
- JavaScript class instances are boxed at their exact class, and stores into nested string-keyed dictionaries keep class method calls direct.
- `--target-cpu` selects an x86-64 microarchitecture level.
- **Node.js parity:**
- X25519 keys and `crypto.diffieHellman`; `existsSync` and `fs.promises` accept `file:` URLs; `Buffer.from` ranges over ArrayBuffers, the deprecated `Buffer()` constructor and `Buffer` as a value.
- Dates are objects with identity; `Promise.all` over different entry types settles on Node's turns; completion callbacks run in their call's AsyncLocalStorage context.
- TypedArray methods run on typed arrays held as untyped values, and unions of typed-array classes compile. Borrowed `TypedArray.prototype.set` calls compile and throw Node's TypeError for other targets.
- Split CommonJS `require` bindings, `__`-prefixed exports, string-key destructuring of `any` values, and record key order for values JavaScript built.
- **npm packages:** zod 4 and `@hono/zod-validator` values are projected at the declaration boundary.
<!-- release:end -->
## 0.4.3
### Highlights
@@ -26,8 +51,6 @@ All notable changes to scriptc will be documented in this file.
- **npm packages:** zod 3 compiles natively and zod 4's core runs, minified packages such as marked compile, chalk 5 and hot-shots 8 build from built-in shims, and `@hono/node-server` 2.x is built in. User code types npm package declarations at a checked, identity-preserving boundary.
- **Builds:** the cached build is reused after whitespace and comment edits, function bodies are emitted on forked workers, and the LLVM emitter reads its analyses from one recorded walk of each module.
<!-- release:end -->
## 0.4.2
### Highlights
@@ -4,4 +4,5 @@
The `Deflate`, `Inflate`, `Gzip`, `Gunzip`, `DeflateRaw`, `InflateRaw`, and `Unzip` streams, from `createGzip()` and the other factories or `new zlib.Gzip()`, accept Node's options and support writes, piping, `pipeline`, async iteration, `flush`, `params`, `close`, `reset`, and `bytesWritten`.
Each output chunk is its own Buffer rather than a slice of a shared chunk-size buffer.
`zlib.constants` members and their module-level copies read as Node's values.
`util.inspect`, `instanceof zlib.Gzip`, and Brotli and Zstd compression are unsupported.
The Brotli one-shots `brotliCompress` and `brotliDecompress` and their `Sync` forms, and the `BrotliCompress` and `BrotliDecompress` streams, accept Node's options, including `params` and `dictionary`, and produce the same bytes as Node.
`util.inspect` and Zstd compression are unsupported.
@@ -1,4 +1,4 @@
- **URL:** Non-ASCII or percent-escaped hosts are unsupported.
- **URL:** Hosts of special URLs that hold non-ASCII text, or percent-escapes that decode to it, throw `ERR_INVALID_URL`, and `xn--` labels are kept without Punycode validation.
Absolute inputs and relative inputs with runtime string or URL bases, including optional bases, compile.
`URL.canParse`, `URL.parse`, `toString()`, and `toJSON()` compile; the factories use the same parser as construction, returning `false` or `null` for rejected inputs while preserving conversion errors.
`href`, `protocol`, `username`, `password`, `host`, `hostname`, `port`, `pathname`, `search`, and `hash` support assignment in native code.
@@ -6,4 +6,3 @@
Native URL assignments require statically known property keys; `+=` accepts primitive operands, while other assignment operators and object operands for `+=` are refused.
`origin` is read-only.
`searchParams` is a live view whose identity survives query and URL replacement.
Opaque paths retain their input bytes without WHATWG C0 encoding.
@@ -4,6 +4,8 @@ Empty record layouts are also snapshots because they have no property storage.
`scriptc coverage` and `scriptc build` report `SC6001` where a copied value is then written or compared, whether the record was passed as an argument, stored in a typed binding, asserted with `as`, assigned to a typed binding later, or stored as an element of a typed array literal.
Typed records with declared fields, indexed records, and arrays retain identity and mutations through native checked storage.
A record or class instance converted to a string-keyed dictionary type such as `Record<string, string>`, directly or through a `{}`, `object` or `unknown` binding or parameter, keeps its identity when the program writes through, compares or inspects values of that dictionary type.
Every dictionary type with the same value type then uses checked dynamic storage, which is slower than a native dictionary.
Class instances retain identity at their exact type or a base class, and at any interface or object type that a class instance is passed, stored, returned, or asserted as when the program could tell a copy from the instance: values of such a type are checked views of the original object, so writes, identity comparisons, Set and Map keys, `util.inspect`, and `JSON.stringify` through them reach the instance, as in Node.js.
These types use checked dynamic storage, which is slower than native record fields, for all of their values, including plain objects.
A type whose values are only read field by field, from fields the class does not write after construction and does not implement as accessors, stays a native record and holds a copy.
@@ -1,4 +1,5 @@
- **Symbol fields:** Instance fields require a stable module-level `Symbol()` or `Symbol.for()` binding with a literal description or constant string binding.
A `[Symbol.toStringTag]` field or getter with a string value sets the tag that `String()`, `Object.prototype.toString` and `util.inspect` print; fields keyed by other well-known symbols are unsupported.
Reassigned keys, runtime-computed descriptions, distinct symbols with the same description in one class, and copying symbol fields with rest bindings or `Object.assign` are unsupported.
JavaScript object spreads copy represented own symbol fields.
Equal registry keys share a field across modules.
@@ -15,3 +15,6 @@ JavaScript argument and return conversions preserve native array references; oth
Supported operations on `unknown` include truthiness, `typeof` narrowing, property access, `+`, `switch`, and `throw`.
Cast before using other operations.
In JavaScript, a call whose untyped argument could select an overload with another result type returns a checked value, whichever overload the type checker picks, and so do the unannotated bindings and functions that hold or return it.
In TypeScript, an `any` argument keeps the overload the type checker picks, and a value that selects another overload at runtime throws a `TypeError` where the result is used.
+59 -59
View File
@@ -18268,9 +18268,9 @@
"anchor": "err_brotli_invalid_param",
"nodeStability": null,
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -71509,9 +71509,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"
}
},
{
@@ -84302,9 +84302,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "BrotliCompress and BrotliDecompress are Transform streams over the brotli version Node bundles, with Node's parameters, dictionaries, flush, reset, error codes, and option validation; like Node's, they have no params().",
"verification": "test-backed"
}
},
{
@@ -84325,9 +84325,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "BrotliCompress and BrotliDecompress are Transform streams over the brotli version Node bundles, with Node's parameters, dictionaries, flush, reset, error codes, and option validation; like Node's, they have no params().",
"verification": "test-backed"
}
},
{
@@ -84349,7 +84349,7 @@
},
"static": {
"status": "partial",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
"verification": "test-backed"
}
},
@@ -84372,7 +84372,7 @@
},
"static": {
"status": "partial",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
"verification": "test-backed"
}
},
@@ -84395,7 +84395,7 @@
},
"static": {
"status": "partial",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
"verification": "test-backed"
}
},
@@ -84418,7 +84418,7 @@
},
"static": {
"status": "partial",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
"verification": "test-backed"
}
},
@@ -84441,7 +84441,7 @@
},
"static": {
"status": "partial",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
"verification": "test-backed"
}
},
@@ -84464,7 +84464,7 @@
},
"static": {
"status": "partial",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
"verification": "test-backed"
}
},
@@ -84487,7 +84487,7 @@
},
"static": {
"status": "partial",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
"verification": "test-backed"
}
},
@@ -84510,7 +84510,7 @@
},
"static": {
"status": "partial",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
"verification": "test-backed"
}
},
@@ -84533,7 +84533,7 @@
},
"static": {
"status": "partial",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
"verification": "test-backed"
}
},
@@ -84556,7 +84556,7 @@
},
"static": {
"status": "partial",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
"verification": "test-backed"
}
},
@@ -84579,7 +84579,7 @@
},
"static": {
"status": "partial",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
"verification": "test-backed"
}
},
@@ -84602,7 +84602,7 @@
},
"static": {
"status": "partial",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
"verification": "test-backed"
}
},
@@ -84625,7 +84625,7 @@
},
"static": {
"status": "partial",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
"verification": "test-backed"
}
},
@@ -84716,9 +84716,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -84739,9 +84739,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -84969,9 +84969,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -84992,9 +84992,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -85015,9 +85015,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -85038,9 +85038,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -85499,7 +85499,7 @@
},
"static": {
"status": "partial",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
"verification": "test-backed"
}
},
@@ -85521,9 +85521,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "BrotliCompress and BrotliDecompress are Transform streams over the brotli version Node bundles, with Node's parameters, dictionaries, flush, reset, error codes, and option validation; like Node's, they have no params().",
"verification": "test-backed"
}
},
{
@@ -85567,9 +85567,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -85590,9 +85590,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -85613,9 +85613,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -85636,9 +85636,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -32679,6 +32679,7 @@
"tests": [
"tests/corpus/6702-ecdh-errors.js",
"tests/corpus/6708-dh-errors.js",
"tests/corpus/7714-zlib-brotli-compress.cjs",
"tests/corpus/caught-system-error-fields.ts",
"tests/corpus/error-stack-frames.ts",
"tests/corpus/error-stack-limit.ts",
@@ -33471,8 +33472,11 @@
"anchor": "err_brotli_invalid_param",
"nodeStability": null,
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:errors"
"status": "partial",
"evidence": "compiler-feature:errors.ERR_BROTLI_INVALID_PARAM",
"tests": [
"tests/corpus/7714-zlib-brotli-compress.cjs"
]
},
"anchorSource": "exact"
},
@@ -99483,6 +99487,7 @@
"evidence": "derived:descendants",
"tests": [
"packages/runtime/test/url-legacy.test.ts",
"tests/corpus/1794-searchparams-url-live.ts",
"tests/corpus/5220-url-parse-warnings.ts",
"tests/corpus/5221-url-parse-shapes.ts",
"tests/corpus/5222-url-parse-hosts.ts",
@@ -100741,8 +100746,11 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:url"
"status": "partial",
"evidence": "surface-manifest:node-builtin.url.urlToHttpOptions",
"tests": [
"tests/corpus/1794-searchparams-url-live.ts"
]
},
"anchorSource": "exact"
},
@@ -115756,7 +115764,10 @@
"tests/corpus/5377-zlib-stream-backpressure.cjs",
"tests/corpus/5378-zlib-sync-options.cjs",
"tests/corpus/5379-zlib-callback-options.ts",
"tests/corpus/5415-zlib-dynamic-inputs.cjs"
"tests/corpus/5415-zlib-dynamic-inputs.cjs",
"tests/corpus/7714-zlib-brotli-compress.cjs",
"tests/corpus/7715-zlib-brotli-decompress.cjs",
"tests/corpus/7716-zlib-brotli.ts"
]
},
"anchorSource": "chapter"
@@ -116239,8 +116250,11 @@
"inherited": true
},
"static": {
"status": "refused",
"evidence": "surface-manifest:unsupported:node-builtin.zlib.BrotliCompress"
"status": "partial",
"evidence": "compiler-feature:zlib.zlib.BrotliCompress",
"tests": [
"tests/corpus/7714-zlib-brotli-compress.cjs"
]
},
"anchorSource": "exact"
},
@@ -116262,8 +116276,12 @@
"inherited": true
},
"static": {
"status": "refused",
"evidence": "surface-manifest:unsupported:node-builtin.zlib.BrotliDecompress"
"status": "partial",
"evidence": "compiler-feature:zlib.zlib.BrotliDecompress",
"tests": [
"tests/corpus/7715-zlib-brotli-decompress.cjs",
"tests/corpus/7716-zlib-brotli.ts"
]
},
"anchorSource": "exact"
},
@@ -116781,8 +116799,11 @@
"inherited": true
},
"static": {
"status": "refused",
"evidence": "surface-manifest:unsupported:node-builtin.zlib.createBrotliCompress"
"status": "partial",
"evidence": "surface-manifest:node-builtin.zlib.createBrotliCompress",
"tests": [
"tests/corpus/7714-zlib-brotli-compress.cjs"
]
},
"anchorSource": "exact"
},
@@ -116804,8 +116825,12 @@
"inherited": true
},
"static": {
"status": "refused",
"evidence": "surface-manifest:unsupported:node-builtin.zlib.createBrotliDecompress"
"status": "partial",
"evidence": "surface-manifest:node-builtin.zlib.createBrotliDecompress",
"tests": [
"tests/corpus/7715-zlib-brotli-decompress.cjs",
"tests/corpus/7716-zlib-brotli.ts"
]
},
"anchorSource": "exact"
},
@@ -117111,8 +117136,11 @@
"inherited": true
},
"static": {
"status": "refused",
"evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliCompress"
"status": "partial",
"evidence": "surface-manifest:node-builtin.zlib.brotliCompress",
"tests": [
"tests/corpus/7714-zlib-brotli-compress.cjs"
]
},
"anchorSource": "exact"
},
@@ -117134,8 +117162,11 @@
"inherited": true
},
"static": {
"status": "refused",
"evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliCompressSync"
"status": "partial",
"evidence": "surface-manifest:node-builtin.zlib.brotliCompressSync",
"tests": [
"tests/corpus/7714-zlib-brotli-compress.cjs"
]
},
"anchorSource": "exact"
},
@@ -117157,8 +117188,12 @@
"inherited": true
},
"static": {
"status": "refused",
"evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliDecompress"
"status": "partial",
"evidence": "surface-manifest:node-builtin.zlib.brotliDecompress",
"tests": [
"tests/corpus/7715-zlib-brotli-decompress.cjs",
"tests/corpus/7716-zlib-brotli.ts"
]
},
"anchorSource": "exact"
},
@@ -117180,8 +117215,12 @@
"inherited": true
},
"static": {
"status": "refused",
"evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliDecompressSync"
"status": "partial",
"evidence": "surface-manifest:node-builtin.zlib.brotliDecompressSync",
"tests": [
"tests/corpus/7715-zlib-brotli-decompress.cjs",
"tests/corpus/7716-zlib-brotli.ts"
]
},
"anchorSource": "exact"
},
@@ -118198,8 +118237,11 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:zlib"
"status": "partial",
"evidence": "compiler-feature:zlib.BrotliOptions",
"tests": [
"tests/corpus/7714-zlib-brotli-compress.cjs"
]
},
"anchorSource": "exact"
},
@@ -118267,8 +118309,11 @@
"inherited": true
},
"static": {
"status": "refused",
"evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliCompress"
"status": "partial",
"evidence": "surface-manifest:node-builtin.zlib.brotliCompress",
"tests": [
"tests/corpus/7714-zlib-brotli-compress.cjs"
]
},
"anchorSource": "ancestor"
},
@@ -118290,8 +118335,11 @@
"inherited": true
},
"static": {
"status": "refused",
"evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliCompressSync"
"status": "partial",
"evidence": "surface-manifest:node-builtin.zlib.brotliCompressSync",
"tests": [
"tests/corpus/7714-zlib-brotli-compress.cjs"
]
},
"anchorSource": "ancestor"
},
@@ -118313,8 +118361,12 @@
"inherited": true
},
"static": {
"status": "refused",
"evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliDecompress"
"status": "partial",
"evidence": "surface-manifest:node-builtin.zlib.brotliDecompress",
"tests": [
"tests/corpus/7715-zlib-brotli-decompress.cjs",
"tests/corpus/7716-zlib-brotli.ts"
]
},
"anchorSource": "ancestor"
},
@@ -118336,8 +118388,12 @@
"inherited": true
},
"static": {
"status": "refused",
"evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliDecompressSync"
"status": "partial",
"evidence": "surface-manifest:node-builtin.zlib.brotliDecompressSync",
"tests": [
"tests/corpus/7715-zlib-brotli-decompress.cjs",
"tests/corpus/7716-zlib-brotli.ts"
]
},
"anchorSource": "ancestor"
},
+8 -1
View File
@@ -652,7 +652,14 @@ function publicDetail(tier) {
source,
)
) {
return "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.";
return "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.";
}
if (
/^compiler-feature:zlib\.(?:zlib\.(?:BrotliCompress|BrotliDecompress)|BrotliOptions)$/.test(
source,
)
) {
return "BrotliCompress and BrotliDecompress are Transform streams over the brotli version Node bundles, with Node's parameters, dictionaries, flush, reset, error codes, and option validation; like Node's, they have no params().";
}
if (source === "compiler-feature:zlib.zlib.constants") {
return "Each zlib.constants member and its module-level copy reads as Node's value; the constants object itself is not a runtime value.";
@@ -190,6 +190,12 @@
"node-builtin.util": ["tests/corpus/2440-console-inspect-args.ts", "tests/corpus/2912-promisify-fs-readfile.ts"],
"node-builtin.worker_threads": ["tests/corpus/957-builtins-namespace.ts", "tests/corpus/native-worker-messages.ts", "tests/corpus/native-worker-references.ts"],
"node-builtin.zlib": ["tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", "tests/corpus/3115-zlib-static-compression-levels.ts", "tests/corpus/5370-zlib-stream-roundtrip.ts", "tests/corpus/5371-zlib-stream-chunking.cjs", "tests/corpus/5373-zlib-stream-options.cjs", "tests/corpus/5378-zlib-sync-options.cjs", "tests/corpus/5379-zlib-callback-options.ts", "tests/corpus/5415-zlib-dynamic-inputs.cjs"],
"node-builtin.zlib.brotliCompressSync": ["tests/corpus/7714-zlib-brotli-compress.cjs"],
"node-builtin.zlib.brotliCompress": ["tests/corpus/7714-zlib-brotli-compress.cjs"],
"node-builtin.zlib.createBrotliCompress": ["tests/corpus/7714-zlib-brotli-compress.cjs"],
"node-builtin.zlib.brotliDecompressSync": ["tests/corpus/7715-zlib-brotli-decompress.cjs", "tests/corpus/7716-zlib-brotli.ts"],
"node-builtin.zlib.brotliDecompress": ["tests/corpus/7715-zlib-brotli-decompress.cjs", "tests/corpus/7716-zlib-brotli.ts"],
"node-builtin.zlib.createBrotliDecompress": ["tests/corpus/7715-zlib-brotli-decompress.cjs", "tests/corpus/7716-zlib-brotli.ts"],
"stdlib.fetch": ["tests/harness/fetch-conformance.test.ts"],
"stdlib.map": ["tests/corpus/typed-collection-iterators.ts", "tests/corpus/typed-collection-iterator-references.ts", "tests/corpus/537-map-iter-drains.ts"],
"stdlib.set": ["tests/corpus/typed-collection-iterators.ts", "tests/corpus/typed-collection-iterator-references.ts", "tests/corpus/checked-collection-iterators.ts"],
@@ -404,6 +410,9 @@
{ "chapter": "util", "signatures": ["`util.promisify(original)`"], "status": "partial", "evidence": ["tests/corpus/1473-promisify-execfile.ts", "tests/corpus/1573-promisify-execfile-env-spread.ts", "tests/corpus/2912-promisify-fs-readfile.ts", "tests/corpus/5625-promisify-exec.ts", "tests/corpus/5626-promisify-forms.ts", "tests/fixtures/node-types/promisify-fs.ts", "tests/corpus/5628-promisify-timers.ts", "tests/fixtures/node-types/promisify-timers.ts", "tests/corpus/5629-promisify-stream.ts", "tests/corpus/5630-promisify-zlib.ts", "tests/fixtures/node-types/promisify-zlib.ts", "tests/corpus/5631-promisify-crypto.ts", "tests/fixtures/node-types/promisify-crypto.ts", "tests/corpus/5632-promisify-dns.ts", "tests/fixtures/node-types/promisify-dns.ts", "tests/corpus/6428-promisify-callback-functions.ts", "tests/fixtures/node-types/promisify-callback-functions.ts", "tests/harness/project-config.test.ts"] },
{ "chapter": "typescript", "symbols": ["type"], "status": "supported", "evidence": ["tests/corpus/2702-type-import-link-static/main.ts", "tests/corpus/2703-type-import-link-static-reexport/main.ts", "tests/corpus/2704-type-import-link-static-success/main.ts", "tests/corpus/2707-type-import-link-static-default-reexport/main.ts", "tests/harness/errors.test.ts"] },
{ "chapter": "zlib", "signatures": ["Class: `zlib.Deflate`", "Class: `zlib.Inflate`", "Class: `zlib.Gzip`", "Class: `zlib.Gunzip`", "Class: `zlib.DeflateRaw`", "Class: `zlib.InflateRaw`", "Class: `zlib.Unzip`", "Class: `zlib.ZlibBase`", "`zlib.close([callback])`", "`zlib.flush([kind, ]callback)`", "`zlib.params(level, strategy, callback)`", "`zlib.reset()`", "`bytesWritten` Type: {number}", "Class: `Options`"], "status": "partial", "evidence": ["tests/corpus/5370-zlib-stream-roundtrip.ts", "tests/corpus/5371-zlib-stream-chunking.cjs", "tests/corpus/5372-zlib-stream-errors.cjs", "tests/corpus/5373-zlib-stream-options.cjs", "tests/corpus/5374-zlib-stream-flush-params.ts", "tests/corpus/5375-zlib-stream-promises.ts", "tests/corpus/5377-zlib-stream-backpressure.cjs"] },
{ "chapter": "zlib", "signatures": ["Class: `zlib.BrotliCompress`", "Class: `BrotliOptions`"], "status": "partial", "evidence": ["tests/corpus/7714-zlib-brotli-compress.cjs"] },
{ "chapter": "errors", "symbols": ["ERR_BROTLI_INVALID_PARAM"], "status": "partial", "evidence": ["tests/corpus/7714-zlib-brotli-compress.cjs"] },
{ "chapter": "zlib", "signatures": ["Class: `zlib.BrotliDecompress`"], "status": "partial", "evidence": ["tests/corpus/7715-zlib-brotli-decompress.cjs", "tests/corpus/7716-zlib-brotli.ts"] },
{ "chapter": "zlib", "signatures": ["`zlib.constants`"], "status": "partial", "evidence": ["tests/corpus/5376-zlib-constants.cjs", "tests/corpus/5374-zlib-stream-flush-params.ts"] },
{ "chapter": "dns", "symbols": ["dns.lookup", "dns.lookupService", "dns.getServers", "dns.getDefaultResultOrder", "dns.setDefaultResultOrder"], "status": "partial", "evidence": ["tests/corpus/5648-dns-lookup-forms.ts", "tests/corpus/5649-dns-service-servers.ts", "tests/corpus/5650-dns-lookup-errors-untyped.cjs"] },
{ "chapter": "dns", "symbols": ["dnsPromises.lookup", "dnsPromises.lookupService", "dnsPromises.getDefaultResultOrder"], "status": "partial", "evidence": ["tests/corpus/dns-promises-lookup.ts", "tests/corpus/dns-promises-untyped.cjs"] },
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-darwin-arm64",
"version": "0.4.3",
"version": "0.4.4",
"description": "Native scriptc compiler for darwin-arm64",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-darwin-x64",
"version": "0.4.3",
"version": "0.4.4",
"description": "Native scriptc compiler for darwin-x64",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-linux-arm64-gnu",
"version": "0.4.3",
"version": "0.4.4",
"description": "Native scriptc compiler for linux-arm64-gnu",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-linux-arm64-musl",
"version": "0.4.3",
"version": "0.4.4",
"description": "Native scriptc compiler for linux-arm64-musl",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-linux-x64-gnu",
"version": "0.4.3",
"version": "0.4.4",
"description": "Native scriptc compiler for linux-x64-gnu",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-linux-x64-musl",
"version": "0.4.3",
"version": "0.4.4",
"description": "Native scriptc compiler for linux-x64-musl",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-win32-x64-msvc",
"version": "0.4.3",
"version": "0.4.4",
"description": "Native scriptc compiler for win32-x64-msvc",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "scriptc",
"version": "0.4.3",
"version": "0.4.4",
"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",
+57 -27
View File
@@ -2796,8 +2796,9 @@ declare var URLSearchParams: {
* slashes, non-empty host); pathToFileURL resolves the path and percent-
* encodes it into a file: URL. The legacy API: parse() answers a Url
* object, format() serializes a Url-shaped object, a string, or a URL,
* resolve() resolves one URL string against another, and the domain pair
* converts a hostname to and from its Punycode form. */
* resolve() resolves one URL string against another, the domain pair
* converts a hostname to and from its Punycode form, and urlToHttpOptions
* builds http.request options from a URL. */
declare module "url" {
export type URL = globalThis.URL;
export type URLSearchParams = globalThis.URLSearchParams;
@@ -2865,6 +2866,19 @@ declare module "url" {
export function resolve(from: string, to: string): string;
export function domainToASCII(domain: string): string;
export function domainToUnicode(domain: string): string;
/** urlToHttpOptions's null-prototype request options. */
export interface UrlHttpOptions {
protocol: string;
hostname: string;
hash: string;
search: string;
pathname: string;
path: string;
href: string;
port?: number;
auth?: string;
}
export function urlToHttpOptions(url: URL): UrlHttpOptions;
}
declare module "node:url" {
export * from "url";
@@ -4858,9 +4872,9 @@ declare module "node:readline/promises" {
* inputs with Node's default options; libz links only into zlib-using
* binaries. Callback forms use the executable worker pool. Options stay
* declared so reached uses receive a named SC2020 refusal. The Deflate,
* Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw and Unzip streams are
* runtime Transform streams; Brotli and Zstd remain outside the lowered
* surface. */
* Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, Unzip, BrotliCompress
* and BrotliDecompress streams are runtime Transform streams; Zstd remains
* outside the lowered surface. */
declare module "zlib" {
import { Transform } from "stream";
export interface ZlibOptions {
@@ -4917,50 +4931,66 @@ declare module "zlib" {
): void;
export function unzipSync(data: string | Uint8Array, options?: ZlibOptions): Buffer;
export function crc32(data: string | Uint8Array, value?: number): number;
export function brotliCompressSync(data: string | Uint8Array): Buffer;
export function brotliDecompressSync(data: Uint8Array): Buffer;
/* Brotli needs a codec the static runtime does not vendor: these are
* declared so their uses receive the named refusal. */
/* Brotli over the vendored brotli 1.2.0 (scr_brotli.c): `params` keys
* are the numeric BROTLI_PARAM_* and BROTLI_DECODER_PARAM_* constants. */
export interface BrotliOptions {
[option: string]: unknown;
}
export function brotliCompress(data: string | Uint8Array, callback: ZlibCallback): void;
export function brotliDecompress(data: Uint8Array, callback: ZlibCallback): void;
export function createBrotliCompress(options?: ZlibOptions): Transform;
export function createBrotliDecompress(options?: ZlibOptions): Transform;
export class BrotliCompress extends Transform {
constructor(options?: ZlibOptions);
}
export class BrotliDecompress extends Transform {
constructor(options?: ZlibOptions);
}
export function brotliCompress(
data: string | Uint8Array,
options: BrotliOptions,
callback: ZlibCallback,
): void;
export function brotliCompressSync(data: string | Uint8Array, options?: BrotliOptions): Buffer;
export function brotliDecompress(data: string | Uint8Array, callback: ZlibCallback): void;
export function brotliDecompress(
data: string | Uint8Array,
options: BrotliOptions,
callback: ZlibCallback,
): void;
export function brotliDecompressSync(data: string | Uint8Array, options?: BrotliOptions): Buffer;
/* The stream classes: runtime Transform streams over the zlib engine
* (scr_zlib_stream.c). createGzip(options) and new Gzip(options) build
* the same stream; options are validated at runtime with Node's rules. */
* the same stream; options are validated at runtime with Node's rules.
* Brotli streams have no params(), as in Node. */
class ZlibBase extends Transform {
readonly bytesWritten: number;
flush(kind?: number, callback?: () => void): void;
flush(callback: () => void): void;
params(level: number, strategy: number, callback?: () => void): void;
close(callback?: () => void): void;
reset(): void;
}
export class Deflate extends ZlibBase {
class Zlib extends ZlibBase {
params(level: number, strategy: number, callback?: () => void): void;
}
export class BrotliCompress extends ZlibBase {
constructor(options?: BrotliOptions);
}
export class BrotliDecompress extends ZlibBase {
constructor(options?: BrotliOptions);
}
export function createBrotliCompress(options?: BrotliOptions): BrotliCompress;
export function createBrotliDecompress(options?: BrotliOptions): BrotliDecompress;
export class Deflate extends Zlib {
constructor(options?: ZlibOptions);
}
export class Inflate extends ZlibBase {
export class Inflate extends Zlib {
constructor(options?: ZlibOptions);
}
export class Gzip extends ZlibBase {
export class Gzip extends Zlib {
constructor(options?: ZlibOptions);
}
export class Gunzip extends ZlibBase {
export class Gunzip extends Zlib {
constructor(options?: ZlibOptions);
}
export class DeflateRaw extends ZlibBase {
export class DeflateRaw extends Zlib {
constructor(options?: ZlibOptions);
}
export class InflateRaw extends ZlibBase {
export class InflateRaw extends Zlib {
constructor(options?: ZlibOptions);
}
export class Unzip extends ZlibBase {
export class Unzip extends Zlib {
constructor(options?: ZlibOptions);
}
export function createDeflate(options?: ZlibOptions): Deflate;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/compiler",
"version": "0.4.3",
"version": "0.4.4",
"description": "The scriptc compiler — TypeScript/JavaScript frontend, typed IR, LLVM backend",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
@@ -20,6 +20,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
...(features.warningReplace ? { warningReplace: true } : {}),
...(features.stdioExtended ? { stdioExtended: true } : {}),
zlib: features.zlib,
...(features.brotli ? { brotli: true } : {}),
assert: features.assert,
inspect: features.inspect,
dynInvoke: features.dynInvoke,
@@ -385,7 +385,10 @@ test("while loop counters below a bound version their accesses", () => {
]);
const declared = (loop: IrStmt): IrFunction =>
fn([{ kind: "varDecl", localId: "i", init: ref("k"), loc }, loop]);
const versionOf = (loop: IrStmt & { kind: "while" }): BytesLoopVersion | null => {
const versionOf = (
loop: IrStmt & { kind: "while" },
workers = false,
): BytesLoopVersion | null => {
const f = declared(loop);
const ranges = analyzeIntegerRanges(f);
return matchBytesLoopVersion(loop, null, {
@@ -396,7 +399,7 @@ test("while loop counters below a bound version their accesses", () => {
initialized: findInitializedByteLoopBindings(f).get(loop) ?? new Set(),
proven: new Set(),
scalarStrings: new Set(),
workers: false,
workers,
owned: new Set(),
});
};
@@ -413,6 +416,11 @@ test("while loop counters below a bound version their accesses", () => {
// 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();
// In a worker program, a while loop versions for its counter's accesses,
// and not for thread-owned receivers alone.
expect(versionOf(copy, true)!.ownedReceivers).toHaveLength(2);
const job = whileLoop([set(ref("out", f64s), ref("k"), get(ref("src", f64s), ref("k"))), step]);
expect(versionOf(job, true)).toBeNull();
const mod: IrModule = {
irVersion: 17,
sourceFile: loc.file,
@@ -581,6 +581,11 @@ export function matchBytesLoopVersion(
pairedKept.length === 0
)
return null;
// A while loop versions only for accesses at its counters. Its other
// checks (thread-owned receivers, static indices) save a test or two per
// pass, while a second copy of its body doubles the calls it makes, which
// can keep a worker's job loop from inlining the function that runs a job.
if (loop.kind === "while" && boundedKept.length === 0) return null;
const bounds = new Set<Node>();
const ownedNodes = new Set<Node>();
const used = new Set<number>();
@@ -760,3 +760,154 @@ test("a parameter stored as its last use takes the owned convention", () => {
const passed = ctor([{ kind: "exprStmt", expr: call("use", [ref("item", cell)]), loc }]);
expect(analyze(passed).borrowed.get("%Box.constructor")).toEqual(new Set([0, 1]));
});
test("a wrap into a pointer union forwards the wrapped binding to the callee's parameter", () => {
const wrap = (id: string): IrExpr => ({
kind: "unionWrap",
unionId: "optional",
tag: 0,
value: ref(id, record),
type: optional,
loc,
});
const leaf = helper("leaf");
const outer = helper("outer");
outer.params[0]!.type = outer.locals[0]!.type = record;
outer.locals.push(local("cell", record));
outer.body = [
{
kind: "varDecl",
localId: "cell",
init: { kind: "recordLit", fields: [], type: record, loc },
loc,
},
{ kind: "exprStmt", expr: call("leaf", [wrap("value")]), loc },
ret(call("leaf", [wrap("cell")])),
];
const functions = new Map([leaf, outer].map((fn) => [fn.name, fn]));
const analyzeWith = (pointer: boolean) =>
analyzeCallLifetimes(
functions,
undefined,
undefined,
undefined,
undefined,
undefined,
() => pointer,
);
// A boxed union builds a new box around the payload: a whole-value use.
expect(analyzeWith(false).parameters.get("outer")).toBeUndefined();
expect(analyzeWith(false).locals.get("outer")).toEqual(new Set());
// A pointer union passes the payload pointer itself.
expect(analyzeWith(true).parameters.get("outer")).toEqual(new Set([0]));
expect(analyzeWith(true).locals.get("outer")).toEqual(new Set(["cell"]));
// A callee that keeps its parameter keeps the wrapped binding too.
leaf.body.push(ret(ref("value")));
expect(analyzeWith(true).parameters.get("outer")).toBeUndefined();
expect(analyzeWith(true).locals.get("outer")).toEqual(new Set());
});
test("a pointer-wrapped binding needs a callee that keeps the narrowed payload in place", () => {
const wrapCell: IrExpr = {
kind: "unionWrap",
unionId: "optional",
tag: 0,
value: ref("cell", record),
type: optional,
loc,
};
const outer = helper("outer", []);
outer.locals.push(local("cell", record));
outer.body = [
{
kind: "varDecl",
localId: "cell",
init: { kind: "recordLit", fields: [], type: record, loc },
loc,
},
ret(call("leaf", [wrapCell])),
];
const read = (value: IrExpr): IrExpr => ({
kind: "recordGet",
obj: value,
shapeId: "cell",
field: "x",
type: F64,
loc,
});
const keeps = (leaf: IrFunction, ...more: IrFunction[]): boolean => {
const result = analyzeCallLifetimes(
new Map([outer, leaf, ...more].map((fn) => [fn.name, fn])),
(className) => className === "Node",
undefined,
undefined,
undefined,
undefined,
() => true,
);
return result.locals.get("outer")?.has("cell") !== true;
};
const leafWith = (body: IrStmt[]): IrFunction => ({ ...helper("leaf"), body });
// Tag tests and field reads of the narrowed payload leave it in place.
expect(keeps(leafWith([{ kind: "exprStmt", expr: tag("value"), loc }]))).toBe(false);
expect(keeps(leafWith([ret(read(narrow("value")))]))).toBe(false);
// So do field reads through a checked narrowing, which throws on the
// other arm (`value.x` of a `T | undefined` parameter).
const checked: IrExpr = {
kind: "ternary",
cond: tag("value"),
then: { kind: "libCall", fn: "error.nodeThrow", args: [], type: record, loc },
else_: narrow("value"),
type: record,
loc,
};
expect(keeps(leafWith([ret(read(checked))]))).toBe(false);
// Returning it keeps the payload.
const returnsChecked = leafWith([ret(checked)]);
returnsChecked.returnType = record;
expect(keeps(returnsChecked)).toBe(true);
// Returning the narrowed payload, or a narrowed nullish read, keeps it,
// although the union itself is only projected.
const returned = leafWith([ret(narrow("value"))]);
returned.returnType = record;
expect(analyze(returned).parameters.get("leaf")).toEqual(new Set([0]));
expect(keeps(returned)).toBe(true);
const defaulted = leafWith([
ret({ kind: "nullish", left: ref("value"), right: ref("other", record), type: record, loc }),
]);
defaulted.locals.push(local("other", record));
expect(keeps(defaulted)).toBe(true);
// So does a nullable-pointer field store of the union.
const self: IrType = { kind: "object", className: "Node" };
const stored = leafWith([
{
kind: "fieldSet",
obj: ref("this", self),
className: "Node",
field: "next",
value: ref("value"),
loc,
},
]);
stored.params = [{ localId: "value", name: "value", type: optional }];
stored.locals.push(local("this", self));
expect(keeps(stored)).toBe(true);
// A narrowed payload passed on, or the union passed on, is in place when
// the next callee keeps it in place.
const inner = (body: IrStmt[], type: IrType = record): IrFunction => {
const fn = helper("inner");
fn.params[0]!.type = fn.locals[0]!.type = type;
fn.body = body;
return fn;
};
const passNarrowed = leafWith([ret(call("inner", [narrow("value")]))]);
expect(keeps(passNarrowed, inner([ret(read(ref("value", record)))]))).toBe(false);
const returnsRecord = inner([ret(ref("value", record))]);
returnsRecord.returnType = record;
expect(keeps(passNarrowed, returnsRecord)).toBe(true);
const passUnion = leafWith([ret(call("inner", [ref("value")]))]);
expect(keeps(passUnion, inner([ret(read(narrow("value")))], optional))).toBe(false);
const innerReturned = inner([ret(narrow("value"))], optional);
innerReturned.returnType = record;
expect(keeps(passUnion, innerReturned)).toBe(true);
});
@@ -14,6 +14,11 @@ import { storedLastUseRead } from "./local-extents.js";
interface ForwardedUse {
callee: string;
index: number;
/** The binding reached the call inside a nullable-pointer union
* (PointerWrapTest): the callee's parameter holds the binding itself as
* its payload, so the callee must keep the payload, not only the union,
* in place. */
wrapped: boolean;
}
interface Uses {
@@ -21,6 +26,13 @@ interface Uses {
written: Set<string>;
declarations: Map<string, number>;
forwards: Map<string, ForwardedUse[]>;
/** Union bindings whose payload a use may keep: a narrowed read that is
* not projected or passed on, a narrowed nullish read, a store into a
* nullable-pointer field. The union itself can still be only projected. */
payloadKept: Set<string>;
/** Narrowed payloads of union bindings passed directly as call arguments:
* the payload stays in place if the callee's parameter is proved. */
payloadForwards: Map<string, ForwardedUse[]>;
}
interface Parameter {
@@ -99,19 +111,55 @@ const NO_INLINE_FIELDS: InlineFieldTest = { record: () => false, classField: ()
* reads the record and keeps no reference to it. */
export type RecordDynCopyTest = (shapeId: string) => boolean;
/** Wraps into a nullable-pointer union whose value is the payload pointer
* itself (LlEmitter.borrowableNullableWrap): the wrapped binding reaches the
* callee's parameter unchanged. */
export type PointerWrapTest = (wrap: IrExpr & { kind: "unionWrap" }) => boolean;
function collectUses(
fn: IrFunction,
nullableField: NullableFieldTest,
registerReturn: boolean,
inlineField: InlineFieldTest,
recordDynCopy: RecordDynCopyTest,
pointerWrap: PointerWrapTest,
): Uses {
const uses: Uses = {
invalid: new Set(),
written: new Set(),
declarations: new Map(),
forwards: new Map(),
payloadKept: new Set(),
payloadForwards: new Map(),
};
const forward = (
table: Map<string, ForwardedUse[]>,
localId: string,
use: ForwardedUse,
): void => {
let forwards = table.get(localId);
if (!forwards) table.set(localId, (forwards = []));
forwards.push(use);
};
/** The union binding `x` whose payload `value` is: `unionNarrow(x)`, or a
* checked narrowing `unionIsTag(x) ? <libCall> : unionNarrow(x)` (either
* order), whose other branch is visited as any use. */
function narrowedBinding(value: IrExpr): string | null {
if (value.kind === "unionNarrow")
return value.value.kind === "varRef" ? value.value.localId : null;
if (
value.kind !== "ternary" ||
value.cond.kind !== "unionIsTag" ||
value.cond.value.kind !== "varRef"
)
return null;
const id = value.cond.value.localId;
if (value.else_.kind === "libCall" && narrowedBinding(value.then) === id)
return expr(value.else_) ? id : null;
if (value.then.kind === "libCall" && narrowedBinding(value.else_) === id)
return expr(value.then) ? id : null;
return null;
}
const aliases: { alias: string; source: string }[] = [];
const stringInput = (value: IrExpr): boolean => {
if (value.kind === "varRef" && value.type.kind === "string") return true;
@@ -145,6 +193,12 @@ function collectUses(
if (borrowsMapReadInputs(node)) return expr(node.receiver) && node.args.every(stringInput);
break;
case "unionNarrow":
// The payload may escape; the union itself does not.
if (node.value.kind === "varRef") {
uses.payloadKept.add(node.value.localId);
return true;
}
break;
case "unionIsTag":
if (node.value.kind === "varRef") return true;
break;
@@ -161,12 +215,16 @@ function collectUses(
node.left.kind === "varRef" &&
node.left.type.kind === "union" &&
node.type.kind !== "union"
)
) {
uses.payloadKept.add(node.left.localId);
return expr(node.right);
}
break;
case "fieldGet":
case "recordGet":
if (node.obj.kind === "varRef") return true;
// A field read of a binding, or of a union binding's narrowed
// payload, keeps neither.
if (node.obj.kind === "varRef" || narrowedBinding(node.obj) !== null) return true;
break;
case "dynFrom":
// A copied record converts field by field; a live capsule keeps it.
@@ -200,13 +258,19 @@ function collectUses(
return node.args.every(expr);
case "call":
node.args.forEach((arg, index) => {
if (arg.kind !== "varRef") {
const narrowed = narrowedBinding(arg);
if (narrowed !== null) {
forward(uses.payloadForwards, narrowed, { callee: node.callee, index, wrapped: false });
return;
}
const wrapped =
arg.kind === "unionWrap" && arg.value.kind === "varRef" && pointerWrap(arg);
const value = wrapped ? arg.value : arg;
if (value.kind !== "varRef") {
expr(arg);
return;
}
let forwards = uses.forwards.get(arg.localId);
if (!forwards) uses.forwards.set(arg.localId, (forwards = []));
forwards.push({ callee: node.callee, index });
forward(uses.forwards, value.localId, { callee: node.callee, index, wrapped });
});
return true;
case "assignExpr":
@@ -250,8 +314,11 @@ function collectUses(
node.value.kind === "varRef" &&
node.value.type.kind === "union" &&
nullableField(node.className, node.field)
)
) {
// The field retains the payload.
uses.payloadKept.add(node.value.localId);
return expr(node.obj);
}
const stored = fieldStore(
node.obj,
node.value,
@@ -345,10 +412,14 @@ export function analyzeCallLifetimes(
registerReturns: ReadonlySet<string> = new Set(),
inlineField: InlineFieldTest = NO_INLINE_FIELDS,
recordDynCopy: RecordDynCopyTest = () => false,
pointerWrap: PointerWrapTest = () => false,
): CallLifetimes {
const usesByFunction = new Map<string, Uses>();
const lazyCaptures = new LazyCaptures(functions);
const nodes = new Map<string, Parameter[]>();
/** Union parameters that keep their payload in place too, which a
* pointer-wrapped argument needs (ForwardedUse.wrapped). */
const payloadNodes = new Map<string, (Parameter | undefined)[]>();
const unsafe: Parameter[] = [];
const consumed: { fn: string; index: number }[] = [];
const result: CallLifetimes = {
@@ -361,7 +432,7 @@ export function analyzeCallLifetimes(
};
for (const fn of functions.values()) {
if (fn.generator !== undefined && fn.classCaptures === undefined) {
const uses = collectUses(fn, nullableField, false, inlineField, recordDynCopy);
const uses = collectUses(fn, nullableField, false, inlineField, recordDynCopy, pointerWrap);
const locals = new Map(fn.locals.map((local) => [local.id, local]));
const stable = new Set<string>();
for (const param of fn.params) {
@@ -386,6 +457,7 @@ export function analyzeCallLifetimes(
registerReturns.has(fn.name),
inlineField,
recordDynCopy,
pointerWrap,
);
usesByFunction.set(fn.name, uses);
// Class-capturing bodies get only local facts (computed below from
@@ -460,23 +532,50 @@ export function analyzeCallLifetimes(
return node;
}),
);
payloadNodes.set(
fn.name,
fn.params.map((param) => {
if (param.type.kind !== "union") return undefined;
const node: Parameter = { safe: !uses.payloadKept.has(param.localId), callers: new Set() };
if (!node.safe) unsafe.push(node);
return node;
}),
);
}
/** The fact a forwarded use needs from the callee's parameter. */
const target = (use: ForwardedUse): Parameter | undefined =>
use.wrapped ? payloadNodes.get(use.callee)?.[use.index] : nodes.get(use.callee)?.[use.index];
for (const fn of functions.values()) {
const params = nodes.get(fn.name);
const uses = usesByFunction.get(fn.name);
if (!params || !uses) continue;
fn.params.forEach((param, index) => {
const node = params[index]!;
const depend = (node: Parameter, callees: (Parameter | undefined)[]): void => {
if (!node.safe) return;
for (const use of uses.forwards.get(param.localId) ?? []) {
const callee = nodes.get(use.callee)?.[use.index];
for (const callee of callees) {
if (!callee) {
node.safe = false;
unsafe.push(node);
break;
return;
}
callee.callers.add(node);
}
};
fn.params.forEach((param, index) => {
const node = params[index]!;
const forwards = uses.forwards.get(param.localId) ?? [];
depend(node, forwards.map(target));
const payload = payloadNodes.get(fn.name)?.[index];
if (!payload) return;
// The payload stays in place when the union does, when every callee
// the union reaches keeps its payload too, and when every callee its
// narrowed payload reaches proves that parameter.
depend(payload, [
node,
...forwards.map((use) => payloadNodes.get(use.callee)?.[use.index]),
...(uses.payloadForwards.get(param.localId) ?? []).map(
(use) => nodes.get(use.callee)?.[use.index],
),
]);
});
}
for (let i = 0; i < unsafe.length; i++) {
@@ -508,7 +607,7 @@ export function analyzeCallLifetimes(
)
continue;
const forwards = uses.forwards.get(local.id) ?? [];
if (forwards.every((use) => result.parameters.get(use.callee)?.has(use.index) === true)) {
if (forwards.every((use) => target(use)?.safe === true)) {
projected.add(local.id);
if (eligible(local) && !uses.written.has(local.id)) safe.add(local.id);
}
@@ -1521,18 +1521,18 @@ export function emitClassShapes(
* statics plus their construct thunks — allocate, run the constructor over
* a +1 `this`, and hand the remaining +1 out. No pending check is needed:
* in-tier constructors are throw-free by construction.
* `classObjs` maps className → { nameSym } registered during body emission
* (the literal interned while the table was open). */
* `classObjs` maps className → { nameSym, baseNameSym } registered during
* body emission (the literals interned while the table was open). */
export function emitClassObjDefs(
host: ClassHost,
metaMap: Map<string, LlClassMeta>,
classObjs: Map<string, { nameSym: string }>,
classObjs: Map<string, { nameSym: string; baseNameSym: string | null }>,
fnByName: Map<string, IrFunction>,
llType: (t: IrType) => string,
threadLocal = false,
): string[] {
const out: string[] = [];
for (const [className, { nameSym }] of classObjs) {
for (const [className, { nameSym, baseNameSym }] of classObjs) {
const meta = metaMap.get(className);
if (!meta)
throw new InternalCompilerError(
@@ -1558,7 +1558,7 @@ export function emitClassObjDefs(
const ctorArgs = params.map((p, i) => `${llType(p.type)} %a${i}`);
const classObj =
`@${mangleClassObj(className)} = internal ${threadLocal ? "thread_local " : ""}global %ScrClassObj ` +
`{ ${host.sizeType} -1, ${host.sizeType} ${intervalMeta.pre}, ${host.sizeType} ${intervalMeta.post}, ptr @${mangleCtorThunk(className)}, ptr ${nameSym}, ${host.sizeType} 0, ${host.sizeType} ${meta.def.jsLength ?? (meta.def.runtime ? 1 : 0)}, ptr null, ptr null } ; class ${className}`;
`{ ${host.sizeType} -1, ${host.sizeType} ${intervalMeta.pre}, ${host.sizeType} ${intervalMeta.post}, ptr @${mangleCtorThunk(className)}, ptr ${nameSym}, ${host.sizeType} 0, ${host.sizeType} ${meta.def.jsLength ?? (meta.def.runtime ? 1 : 0)}, ptr null, ptr null, ptr ${baseNameSym ?? "null"}, ${host.sizeType} ${meta.def.ownStaticFields ? 1 : 0} } ; class ${className}`;
if (meta.def.runtime) {
// A runtime-provided class: %C.construct allocates through the
// runtime and returns the instance.
+4 -1
View File
@@ -979,7 +979,10 @@ export class LlDyn {
const lPlain = B.newLabel("dm.tr.plain");
B.condBr(capsule, lCapsule, lPlain);
B.startBlock(lCapsule);
if (preserveRefs) {
// A class target matches only capsules of that class (tested above):
// a materialized view is a plain object, which never carries a class
// brand, so materializing one cannot change the answer.
if (preserveRefs || (t.kind === "object" && t.className !== "%Error")) {
B.terminate(`ret i1 false`);
} else {
const materialized = B.tmp();
@@ -646,7 +646,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
// class-object shape { rc, pre, post, ctor, name } — field reads on
// builtin errors and classval loads GEP through these.
`%ScrError = type { ${emitter.sizeType}, ptr, ptr, ptr, ptr, ptr, i8, i8, i8, i8, i8, ptr, ptr }`,
`%ScrClassObj = type { ${emitter.sizeType}, ${emitter.sizeType}, ${emitter.sizeType}, ptr, ptr, ${emitter.sizeType}, ${emitter.sizeType}, ptr, ptr }`,
`%ScrClassObj = type { ${emitter.sizeType}, ${emitter.sizeType}, ${emitter.sizeType}, ptr, ptr, ${emitter.sizeType}, ${emitter.sizeType}, ptr, ptr, ptr, ${emitter.sizeType} }`,
// The runtime emitter prefix { rc, vt, reg, cls } — user subclasses
// embed it (classes.ts), and bare-emitter GEPs address through it.
`%ScrEmitter = type { ${emitter.sizeType}, ptr, ptr, ptr }`,
@@ -1331,7 +1331,7 @@ export class LlEmitter {
/** Class objects (classes as first-class values): className → the
* interned .name literal ref — registered during body emission, the
* statics and construct thunks assemble around the bodies. */
readonly classObjs = new Map<string, { nameSym: string }>();
readonly classObjs = new Map<string, { nameSym: string; baseNameSym: string | null }>();
private readonly unionFieldReadGroups = new Map<string, number[][]>();
/** Preorder intervals of the runtime error classes under THIS module's
* class-forest numbering (main() stamps scr_error_vts with them, exactly
@@ -1781,6 +1781,7 @@ export class LlEmitter {
const shape = this.recordsById.get(shapeId);
return shape !== undefined && !recordDynKeepsIdentity(shape);
},
(wrap) => this.borrowableNullableWrap(wrap) === "ref",
);
this.lentStringParameters =
this.debug === null ? findLentStringParameters(this.fnByName, this.callLifetimes) : new Map();
@@ -4068,7 +4069,12 @@ export class LlEmitter {
classObjSym(className: string): string {
if (!this.classObjs.has(className)) {
const meta = this.classMetaOf(className);
this.classObjs.set(className, { nameSym: this.internLiteral(classJsName(meta.def)) });
// util.inspect prints `[class C extends B]` with the base's name.
const baseName = meta.base ? classJsName(meta.base.def) : "";
this.classObjs.set(className, {
nameSym: this.internLiteral(classJsName(meta.def)),
baseNameSym: baseName ? this.internLiteral(baseName) : null,
});
}
this.journal?.entry("classObj", className, this.classObjs.get(className));
return mangleClassObj(className);
@@ -4934,7 +4940,7 @@ export class LlEmitter {
return;
}
case "classObj":
addEntry(this.classObjs, key, value as { nameSym: string });
addEntry(this.classObjs, key, value as { nameSym: string; baseNameSym: string | null });
return;
case "retainWrapper":
addEntry(this.nullableUnions.retainWrappers, key, value as string);
@@ -46,6 +46,7 @@ import {
type ArrayAccess,
} from "./dense-array-access.js";
import { emitWrappingSum, markWrappingOperands } from "./wrapping-sums.js";
import { emitPowerOfTwoQuotient, isPowerOfTwoCall } from "./lib-filesystem.js";
/** The signed integer predicates of the number compares. */
const INTEGER_COMPARES: Record<string, string> = {
@@ -348,6 +349,8 @@ export function emitOperatorExpr(
: emitCheckedRemainder(host, l, r, li, ri);
if (checked) return checked;
}
if (e.op === "/" && isPowerOfTwoCall(e.right))
return { name: emitPowerOfTwoQuotient(host, l.name, r.name), type: e.type };
if (arith[e.op] !== undefined) {
B.line(`${t} = ${arith[e.op]} double ${l.name}, ${r.name}`);
const uint32 = emitWrappingSum(host, e, l, r, li, ri);
@@ -20,6 +20,7 @@ import {
isRefCounted,
shapeHasAccessorSlots,
typeKey,
type IrClassDef,
} from "../../ir/ir.js";
import { mangleFunction, mangleGlobal, mangleRecordStruct } from "../mangle.js";
import { BlockBuilder } from "./blocks.js";
@@ -272,9 +273,7 @@ export function streamTypedRefCommitAdapter(
);
});
}
const symbols = new Map(
(meta.def.symbolFields ?? []).map((symbol) => [symbol.field, symbol.globalId]),
);
const symbols = new Map((meta.def.symbolFields ?? []).map((symbol) => [symbol.field, symbol]));
const fields = meta.def.fields.filter(
(field) =>
(isClassOwnEnumerableFieldName(field.name) || symbols.has(field.name)) &&
@@ -294,13 +293,15 @@ export function streamTypedRefCommitAdapter(
const inline = host.inlineUnions.classField(t.className, field.name);
const fieldTy = int32 ? "i32" : llFieldType(field.type, inline);
const checkTy = host.llType(field.type);
const symbolGlobal = symbols.get(field.name);
if (symbolGlobal) {
const symbolKey = symbols.get(field.name);
if (symbolKey) {
host.declare(`declare ptr @scr_dyn_symbol_key_get(ptr, ptr, i1 zeroext)`);
host.declare(`declare void @scr_dyn_release_v(ptr)`);
const keyLines = symbolKeyLines(host, symbolKey, `f${index}_keyraw`);
lines.push(
` %f${index}_keyraw = load ptr, ptr @${mangleGlobal(symbolGlobal)}`,
...keyLines.load,
` %f${index}_key = call ptr @${host.dyn.toDynHelper(SYMBOL_T)}(ptr %f${index}_keyraw)`,
...keyLines.release,
);
if (field.type.kind === "symbol") {
// A base constructor can mutate the instance before this
@@ -328,7 +329,7 @@ export function streamTypedRefCommitAdapter(
lines.push(
` %${raw} = call ptr @scr_dyn_obj_get(ptr %d, ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")})`,
);
if (meta.def.tracksOwnFields && !symbolGlobal)
if (meta.def.tracksOwnFields && !symbolKey)
lines.push(
` %f${index}_present = icmp ne ptr %${raw}, null`,
` br i1 %f${index}_present, label %f${index}_read, label %${after}`,
@@ -339,7 +340,7 @@ export function streamTypedRefCommitAdapter(
` %${undef} = call ptr @scr_dyn_undefined()`,
` %${input} = select i1 %${missing}, ptr %${undef}, ptr %${raw}`,
` %${next} = call ${field.type.kind === "bool" ? "zeroext " : ""}${checkTy} @${host.dyn.dynCheckHelper(field.type)}(ptr %${input}, ptr null)`,
...(symbolGlobal ? [` call void @scr_dyn_release_v(ptr %${raw})`] : []),
...(symbolKey ? [` call void @scr_dyn_release_v(ptr %${raw})`] : []),
...host.pendingTestLines(`%${pending}`),
` br i1 %${pending}, label %done, label %${store}`,
`${store}:`,
@@ -494,16 +495,18 @@ export function streamTypedRefCommitAdapter(
? `declare void @scr_dyn_symbol_key_placeholder(ptr, ptr)`
: `declare void @scr_dyn_key_delete_computed(ptr, ptr, i1 zeroext)`,
);
[...symbols.values()].forEach((globalId, index) =>
[...symbols.values()].forEach((symbol, index) => {
const keyLines = symbolKeyLines(host, symbol, `bag_symraw${index}`);
lines.push(
` %bag_symraw${index} = load ptr, ptr @${mangleGlobal(globalId)}`,
...keyLines.load,
` %bag_sym${index} = call ptr @${host.dyn.toDynHelper(SYMBOL_T)}(ptr %bag_symraw${index})`,
...keyLines.release,
meta.def.tracksOwnFields
? ` call void @scr_dyn_symbol_key_placeholder(ptr %bag, ptr %bag_sym${index})`
: ` call void @scr_dyn_key_delete_computed(ptr %bag, ptr %bag_sym${index}, i1 zeroext false)`,
` call void @scr_dyn_release_v(ptr %bag_sym${index})`,
),
);
);
});
const { index } = classFieldIndex(meta, DYN_CLASS_PROPERTIES);
lines.push(
` %bag_ptr = getelementptr inbounds %${classStructSym(t.className)}, ptr %target, i64 0, i32 ${index}`,
@@ -700,6 +703,27 @@ export function liveDynUnionRefAdapter(
return sym;
}
/** The raw ScrSym of a class's symbol field key into `%name`: a module
* key is borrowed from its global, a well-known one comes from the
* runtime's registry (+1, released by the `release` lines once the key
* value holds it). */
function symbolKeyLines(
host: LlvmEmitterContext,
symbol: NonNullable<IrClassDef["symbolFields"]>[number],
name: string,
): { load: string[]; release: string[] } {
if ("globalId" in symbol)
return { load: [` %${name} = load ptr, ptr @${mangleGlobal(symbol.globalId)}`], release: [] };
host.declare("declare ptr @scr_sym_well_known(ptr)");
host.declare("declare void @scr_sym_release(ptr)");
return {
load: [
` %${name} = call ptr @scr_sym_well_known(ptr ${host.internLiteral(symbol.wellKnown)})`,
],
release: [` call void @scr_sym_release(ptr %${name})`],
};
}
export function streamTypedRefBoxValue(
host: LlvmEmitterContext,
B: BlockBuilder,
@@ -924,8 +948,10 @@ export function streamTypedRefMaterializeAdapter(
B.line(`${fieldValue} = sitofp i32 ${raw} to double`);
} else if (inline) fieldValue = loadStructField(B, host.sizeType, fieldPtr, field.type, true);
else B.line(`${fieldValue} = load ${llFieldType(field.type, false)}, ptr ${fieldPtr}`);
B.line(`${rawKey} = load ptr, ptr @${mangleGlobal(symbol.globalId)}`);
const keyLines = symbolKeyLines(host, symbol, rawKey.slice(1));
for (const line of keyLines.load) B.line(line.trim());
const key = host.streamTypedRefBoxValue(B, SYMBOL_T, rawKey);
for (const line of keyLines.release) B.line(line.trim());
const afterSymbol =
meta.def.tracksOwnFields || field.type.kind === "symbol"
? B.newLabel("symbol.after")
@@ -993,6 +1019,28 @@ export function streamTypedRefMaterializeAdapter(
: `call void @scr_error_dyn_place_name(ptr ${out}, ptr ${host.cstr(nameAfter)}, ${host.sizeType} ${Buffer.byteLength(nameAfter, "utf8")})`,
);
}
// A JavaScript Error subclass's property bag records, once a dynamic
// write has committed the view, where `name` and `message` were created
// (zod's error factory assigns `name` before it defines `message`); the
// slots hold their values. A `name` the class's construction sets is
// already where errorNameAfter places it among the fields.
if (
meta.def.errorBagOrder &&
meta.root.def.name === "%Error" &&
meta.def.fields.some((field) => field.name === DYN_CLASS_PROPERTIES)
) {
host.declare(`declare void @scr_error_dyn_follow_bag(ptr, ptr, ptr, i1)`);
const { index } = classFieldIndex(meta, DYN_CLASS_PROPERTIES);
const slot = B.tmp(),
bag = B.tmp();
B.line(
`${slot} = getelementptr inbounds %${classStructSym(t.className)}, ptr %p, i64 0, i32 ${index}`,
);
B.line(`${bag} = load ptr, ptr ${slot}`);
B.line(
`call void @scr_error_dyn_follow_bag(ptr ${out}, ptr ${bag}, ptr %p, i1 ${nameAfter === undefined ? "false" : "true"})`,
);
}
// A frozen, sealed or non-extensible instance's view says so.
if (withAttributes) {
host.declare(`declare void @scr_dyn_class_view_attributes(ptr, double)`);
@@ -1468,9 +1516,26 @@ function liveClassOps(host: LlvmEmitterContext, snapshot: string, meta: LlClassM
// The constructor's name, as util.inspect prints before the fields.
const jsName = meta.def.jsName ?? meta.def.name.replace(/^%/, "");
const className = jsName === "" ? "null" : host.internLiteral(jsName);
// Its Symbol.toStringTag getter's string, for Object.prototype.toString
// and util.inspect.
let stringTag = "null";
if (meta.def.stringTagHelper) {
// The runtime lends the instance; the helper takes its own reference.
stringTag = `@${snapshot}_ctag`;
const retain = vAdapters(host.shapeHost, { kind: "object", className: meta.def.name }).retain;
host.resolveThunkDefs.push(
`define internal ptr ${stringTag}(ptr %p) ${FN_ATTRS} { ; Symbol.toStringTag of ${meta.def.name}`,
`entry:`,
` %owned = call ptr ${retain}(ptr %p)`,
` %tag = call ptr @${mangleFunction(meta.def.stringTagHelper)}(ptr %owned)`,
` ret ptr %tag`,
`}`,
``,
);
}
host.resolveThunkDefs.push(
`@${names} = internal constant [${members.length} x ptr] [${literals.join(", ")}]`,
`@${ops} = internal constant { ptr, ${host.sizeType}, ptr, ptr } { ptr ${exact}, ${host.sizeType} ${members.length}, ptr @${names}, ptr ${className} }`,
`@${ops} = internal constant { ptr, ${host.sizeType}, ptr, ptr, ptr } { ptr ${exact}, ${host.sizeType} ${members.length}, ptr @${names}, ptr ${className}, ptr ${stringTag} }`,
``,
);
}
@@ -17,7 +17,7 @@ export function emitLlvmLayouts(
host: ClassHost,
mod: IrModule,
classes: Map<string, LlClassMeta>,
classObjects: Map<string, { nameSym: string }>,
classObjects: Map<string, { nameSym: string; baseNameSym: string | null }>,
functions: Map<string, IrFunction>,
llType: (type: IrType) => string,
nullable?: NullableRefFields,
@@ -1,7 +1,7 @@
/* Focused LLVM library-call emission extracted from emitter.ts. */
import { InternalCompilerError } from "../../errors.js";
import { undefinedArmTag } from "../../ir/analysis.js";
import { DYN, F64, STATS_T, STRING } from "../../ir/ir.js";
import { DYN, F64, STATS_T, STRING, type IrExpr } from "../../ir/ir.js";
import { mangleRecordNew } from "../mangle.js";
import { arrNewCall, traceArg, vAdapters } from "./shapes.js";
import type { LlvmEmitterContext, LibCallExpr, LlValue } from "./expr-context.js";
@@ -1027,6 +1027,57 @@ function emitPowerOfTwo(host: LlvmEmitterContext, n: string): string {
return out;
}
/** `Math.pow(2, n)`, the divisor emitPowerOfTwoQuotient takes. */
export function isPowerOfTwoCall(e: IrExpr): boolean {
return (
e.kind === "libCall" &&
e.fn === "math.pow" &&
e.args.length === 2 &&
e.args[0]!.kind === "numLit" &&
e.args[0]!.value === 2
);
}
/** `left / divisor` for a divisor that is usually a power of two
* (`x / Math.pow(2, n)`). A positive power of two from 2^-1022 to 2^1022
* has a reciprocal that is a power of two too, whose bits are
* 0x7FE0000000000000 minus the divisor's, and the product with it is the
* quotient for every `left`: both are `left` scaled by the same power of
* two, rounded once. Any other divisor divides. */
export function emitPowerOfTwoQuotient(
host: LlvmEmitterContext,
left: string,
divisor: string,
): string {
const B = host.B;
const bits = B.tmp(),
mantissa = B.tmp(),
plain = B.tmp(),
biased = B.tmp(),
offset = B.tmp(),
normal = B.tmp(),
power = B.tmp(),
inverseBits = B.tmp(),
inverse = B.tmp(),
scaled = B.tmp(),
divided = B.tmp(),
out = B.tmp();
B.line(`${bits} = bitcast double ${divisor} to i64`);
B.line(`${mantissa} = and i64 ${bits}, ${2n ** 52n - 1n}`);
B.line(`${plain} = icmp eq i64 ${mantissa}, 0`);
// Biased exponents 1 to 2045 with a clear sign bit.
B.line(`${biased} = lshr i64 ${bits}, 52`);
B.line(`${offset} = sub i64 ${biased}, 1`);
B.line(`${normal} = icmp ult i64 ${offset}, 2045`);
B.line(`${power} = and i1 ${plain}, ${normal}`);
B.line(`${inverseBits} = sub i64 ${0x7fe0000000000000n}, ${bits}`);
B.line(`${inverse} = bitcast i64 ${inverseBits} to double`);
B.line(`${scaled} = fmul double ${left}, ${inverse}`);
B.line(`${divided} = fdiv double ${left}, ${divisor}`);
B.line(`${out} = select i1 ${power}, double ${scaled}, double ${divided}`);
return out;
}
export function emitPrimitiveLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
const B = host.B;
if (e.fn === "math.min" || e.fn === "math.max") {
@@ -575,6 +575,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"zlib.deflateSync": "scr_zlib_deflate",
"zlib.inflateSync": "scr_zlib_inflate",
"zlib.crc32": "scr_zlib_crc32",
"zlib.brotliInstall": "scr_brotli_install",
// The CA-store unit. get/set throw (an unknown type name, a
// certificate-free set) and take the generic path's pending check.
"tlsca.root": "scr_tls_ca_root",
@@ -677,6 +678,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
"insp.moreItems": "scr_insp_more_items",
"insp.end": "scr_insp_end",
"insp.nestedBegin": "scr_insp_nested_begin",
"insp.mark": "scr_insp_mark",
"insp.unwind": "scr_insp_unwind",
"insp.nestedEnd": "scr_insp_nested_end",
// Circular references over cycle-capable composites (Node's
// seen/circular machinery — none of these throw; refWrap borrows its
@@ -732,6 +735,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"punycode.ucs2Encode": "scr_str_from_code_point",
"punycode.warn": "scr_punycode_warn",
"url.domainToUnicode": "scr_url_domain_to_unicode",
"url.toHttpOptions": "scr_url_to_http_options_dyn",
"url.pathToFileURL": "scr_url_from_path",
"url.pathToFileURLPlatform": "scr_url_from_path_platform",
"url.pathToFileURLWin32": "scr_url_from_path",
@@ -1063,6 +1067,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"dyn.generatorThis": "scr_gen_receiver",
"insp.dyn": "scr_insp_dyn",
"insp.dynS": "scr_insp_dyn_s",
"insp.dynHidden": "scr_insp_dyn_hidden",
"fs.readFileSyncDyn": "scr_fs_read_file_sync_dyn",
"fs.readFileEncoded": "scr_fs_read_file_encoded",
"fs.readFdEncoded": "scr_fs_read_fd_encoded",
@@ -35,6 +35,8 @@ export interface NativeLinkFeatures {
* or stdio 'resize' events: links the SCR_STDIO_EXTENDED events unit. */
stdioExtended?: boolean;
zlib: boolean;
/** node:zlib's Brotli forms: scr_brotli.c and the vendored brotli archive. */
brotli?: boolean;
assert: boolean;
inspect: boolean;
dynInvoke: boolean;
@@ -85,6 +85,9 @@ export interface CcOptions {
* compares round-trips and fixed-blob inflation, never raw deflate
* output. */
zlib?: boolean;
/** The program uses node:zlib's Brotli forms (with zlib): compiles
* scr_brotli.c and links the vendored brotli archive. */
brotli?: boolean;
/** The program uses node:assert (index.ts detects assert.* libCalls on
* the IR): compiles scr_assert.c — the zlib gating precedent, so
* assert-free binaries keep their exact size class. scr_regex.c calls
@@ -82,15 +82,18 @@ import {
currentVendorCacheBuildIdentity,
vendorBuildCacheRoot,
tlsArchivePath,
brotliArchivePath,
lreObjectPaths,
zlibObjectPaths,
stageVendorInputs,
ensureTlsArchive,
ensureBrotliArchive,
ensureLreObjects,
ensureZlibObjects,
vendorEngineDir,
vendorZlibDir,
vendorTlsDir,
vendorBrotliDir,
} from "./vendor.js";
import { execFileAsync, subprocessFailureDetail, CcCompileError } from "./process.js";
import { implicitLinkerFingerprint } from "./linker-fingerprint.js";
@@ -119,6 +122,7 @@ interface RuntimeUnitContext {
lreObjects: readonly string[];
zlibObjects: readonly string[];
tlsArchive: string | null;
brotliArchive: string | null;
}
/** An optional runtime input: runtime sources compiled when `when` holds,
@@ -222,6 +226,18 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
: []
: [],
),
// Brotli (scr_brotli.c) plugs into the zlib engine and links the
// vendored brotli archive.
unitArgs((u) =>
u.brotliArchive
? [
"-I",
join(vendorBrotliDir(), "include"),
u.rt(join(u.rtDir, "scr_brotli.c")),
u.brotliArchive,
]
: [],
),
unit((u) => u.opts.events, "scr_events.c", "scr_readline.c"),
// readline's terminal mode: rl.terminal sets the regex unit it
// measures with.
@@ -398,6 +414,7 @@ export async function compileCInternal(
const dh = opts.dh ?? false;
const pki = opts.pki ?? false;
const mbedtls = tls || cipher || ecdh || dh || pki;
const brotli = (opts.zlib ?? false) && opts.brotli === true;
const driver = resolveCc();
const darwinDebugSymbols = needsDarwinDebugSymbols(
targetPlatform(driver),
@@ -637,6 +654,9 @@ export async function compileCInternal(
let tlsArchive = mbedtls
? tlsArchivePath(sanitize, driver, vendorBuildIdentity, vendorCacheRoot)
: null;
let brotliArchive = brotli
? brotliArchivePath(sanitize, driver, vendorBuildIdentity, vendorCacheRoot)
: null;
// ours would collide; see scr_regex.c) — the standalone objects are for
// static builds only.
@@ -674,6 +694,23 @@ export async function compileCInternal(
: await stageVendorInputs(materialize, join(stageRoot, "tls"));
tlsArchive = paths[0]!;
}
if (brotli) {
const cachedArchive = brotliArchivePath(
sanitize,
driver,
vendorBuildIdentity,
materializeCacheRoot,
);
protectCachedArtifact(cacheWarmPaths, cachedArchive);
const materialize = async (): Promise<string[]> => [
await ensureBrotliArchive(sanitize, driver, vendorBuildIdentity, materializeCacheRoot),
];
const paths =
stageRoot === undefined
? await materialize()
: await stageVendorInputs(materialize, join(stageRoot, "brotli"));
brotliArchive = paths[0]!;
}
if (regex) {
for (const object of lreObjectPaths(
sanitize,
@@ -760,6 +797,7 @@ export async function compileCInternal(
lreObjects,
zlibObjects,
tlsArchive,
brotliArchive,
}),
// A .ll program TU (the LLVM backend) deliberately carries no target
// triple (byte-stable output; clang supplies the host/SCRIPTC_TARGET
@@ -3,7 +3,7 @@ import { createRequire } from "node:module";
import { readdir, readFile, realpath, stat } from "node:fs/promises";
import { basename, dirname, extname, join, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { MBEDTLS_VERSION, QJS_COMMIT, ZLIB_VERSION } from "../vendor-inputs.js";
import { BROTLI_VERSION, MBEDTLS_VERSION, QJS_COMMIT, ZLIB_VERSION } from "../vendor-inputs.js";
import { stableTestMemo } from "./session.js";
const implementationPath = fileURLToPath(import.meta.url);
@@ -134,6 +134,7 @@ async function runtimeFingerprintFresh(rtDir: string): Promise<string> {
.update(QJS_COMMIT)
.update(MBEDTLS_VERSION)
.update(ZLIB_VERSION)
.update(BROTLI_VERSION)
.update("\0backend-recipe\0");
for (const path of NATIVE_RECIPE_IMPLEMENTATION_PATHS) {
h.update(basename(path))
@@ -164,6 +165,7 @@ async function runtimeFingerprintInputGroups(
{ label: "quickjs-ng", dir: join(rtDir, "..", "vendor", "quickjs-ng") },
{ label: "mbedtls", dir: join(rtDir, "..", "vendor", "mbedtls") },
{ label: "zlib", dir: join(rtDir, "..", "vendor", "zlib") },
{ label: "brotli", dir: join(rtDir, "..", "vendor", "brotli", "c") },
{ label: "curl", dir: join(rtDir, "..", "vendor", "curl") },
].map(async (group) => {
const names = (await nativeSourceFiles(group.dir, true))
@@ -6,6 +6,9 @@ import { resolvedToolIdentity } from "./tool-identity.js";
export const {
vendorEngineDir,
vendorTlsDir,
vendorBrotliDir,
brotliArchivePath,
ensureBrotliArchive,
vendorBuildCacheRoot,
vendorCacheTargetFlavor,
vendorCacheBuildIdentity,
@@ -34,7 +34,7 @@ interface RuntimePackUnit {
}
interface RuntimePackArchive extends RuntimePackArtifact {
id: "quickjs" | "libregexp" | "zlib" | "mbedtls";
id: "quickjs" | "libregexp" | "zlib" | "brotli" | "mbedtls";
predicate: RuntimePredicate;
}
@@ -93,6 +93,7 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures {
builtinValues: boolean;
warningReplace: boolean;
stdioExtended: boolean;
brotli: boolean;
nativeFetch: boolean;
netEffective: boolean;
httpEffective: boolean;
@@ -291,6 +292,7 @@ export function effectiveRuntimeFeatures(
builtinValues: features.builtinValues === true,
warningReplace: features.warningReplace === true,
stdioExtended: features.stdioExtended === true,
brotli: features.brotli === true,
nativeFetch,
netEffective,
httpEffective,
@@ -18,6 +18,7 @@ import {
QJS_COMMIT,
MBEDTLS_VERSION,
ZLIB_VERSION,
BROTLI_VERSION,
LRE_SOURCES,
ZLIB_SOURCES,
} from "./vendor-inputs.js";
@@ -25,6 +26,7 @@ export {
QJS_COMMIT,
MBEDTLS_VERSION,
ZLIB_VERSION,
BROTLI_VERSION,
LRE_SOURCES,
ZLIB_SOURCES,
} from "./vendor-inputs.js";
@@ -418,9 +420,98 @@ export function createVendorArchives(context: VendorArchiveContext) {
return archive;
}
function vendorBrotliDir(): string {
return join(runtimeSrcDir(), "..", "vendor", "brotli", "c");
}
function brotliArchivePath(
sanitize: boolean,
driver: CcDriver,
buildIdentity: string,
cacheRoot: string = vendorBuildCacheRoot(),
): string {
const flavor = `${sanitize ? "asan" : "plain"}-${vendorCacheTargetFlavor(driver)}-${buildIdentity}`;
return join(cacheRoot, `brotli-${BROTLI_VERSION}-${flavor}`, "libbrotli.a");
}
/** The brotli archive for one flavor, compiled lazily on the first
* Brotli-using build and cached under
* <build-cache>/vendor/brotli-<version>-<flavor>-<target>-<toolchain>/libbrotli.a,
* with the mbedTLS archive's recipe: per-TU compiles of c/common, c/dec
* and c/enc against c/include, then one `ar rcs`. dec/ and enc/ share
* file names, so each object is named after its path (dec-static_init.o). */
async function ensureBrotliArchive(
sanitize: boolean,
driver: CcDriver,
buildIdentity: string,
cacheRoot: string = vendorBuildCacheRoot(),
): Promise<string> {
const flavor = `${sanitize ? "asan" : "plain"}-${vendorCacheTargetFlavor(driver)}-${buildIdentity}`;
const compileArgv = isZigDriver(driver) ? driver.argv : ["clang"];
const arArgv = isZigDriver(driver) ? [...driver.argv.slice(0, 1), "ar"] : ["ar"];
const vendor = vendorBrotliDir();
const archive = brotliArchivePath(sanitize, driver, buildIdentity, cacheRoot);
if (await validVendorArtifact(archive)) return archive;
await mkdir(cacheRoot, { recursive: true });
const buildDir = await mkdtemp(join(tmpdir(), `scriptc-vendor-brotli-${flavor}-`));
try {
const sources = (
await Promise.all(
["common", "dec", "enc"].map(async (dir) =>
(await readdir(join(vendor, dir)))
.filter((name) => !name.startsWith(".") && name.endsWith(".c"))
.map((name) => `${dir}/${name}`),
),
)
)
.flat()
.sort();
const object = (src: string): string =>
join(buildDir, `${src.replaceAll("/", "-").replace(/\.c$/, "")}.o`);
const cflags = [
"-std=c11",
...driver.targetArgs,
...(sanitize ? ["-O1", "-fsanitize=address"] : ["-Os"]),
"-I",
join(vendor, "include"),
];
const width = availableParallelism();
for (let i = 0; i < sources.length; i += width) {
await Promise.all(
sources
.slice(i, i + width)
.map((src) =>
execFileAsync(compileArgv[0] ?? "clang", [
...compileArgv.slice(1),
...cflags,
"-c",
join(vendor, src),
"-o",
object(src),
]),
),
);
}
await execFileAsync(arArgv[0] ?? "ar", [
...arArgv.slice(1),
"rcs",
join(buildDir, "libbrotli.a"),
...sources.map(object),
]);
await publishVendorArtifact(join(buildDir, "libbrotli.a"), archive);
} finally {
await rm(buildDir, { recursive: true, force: true });
}
return archive;
}
return {
vendorEngineDir,
vendorTlsDir,
vendorBrotliDir,
brotliArchivePath,
ensureBrotliArchive,
vendorBuildCacheRoot,
vendorCacheTargetFlavor,
vendorCacheBuildIdentity,
@@ -1,7 +1,9 @@
/** The pinned QuickJS, mbedTLS, and zlib inputs used by native recipes. */
/** The pinned QuickJS, mbedTLS, zlib, and brotli inputs used by native recipes. */
export const QJS_COMMIT = "3c8f3d68953955950074c41c6e4d999562ae82a7";
export const MBEDTLS_VERSION = "3.6.7";
export const ZLIB_VERSION = "1.3.1";
/** The brotli release Node v24.22.0 bundles (packages/runtime/vendor/brotli). */
export const BROTLI_VERSION = "1.2.0";
/** Exact translation-unit membership shared by cache builds and external
* source-pack recipes. */
export const LRE_SOURCES = ["libregexp.c", "libunicode.c"] as const;
@@ -63,6 +63,7 @@ export interface EarlyExecutableNativeFeatures {
warningReplace?: boolean;
stdioExtended?: boolean;
zlib: boolean;
brotli?: boolean;
assert: boolean;
inspect: boolean;
dynInvoke: boolean;
@@ -13,6 +13,7 @@ import {
funcOf,
type IrExpr,
type IrLibFn,
type IrStmt,
type IrType,
type SrcLoc,
STRING,
@@ -40,6 +41,36 @@ const ZLIB_CALLBACK_FNS: Readonly<Record<string, IrLibFn | undefined>> = {
unzip: "zlib.unzipCb",
};
/* Node's BROTLI_DECODE and BROTLI_ENCODE modes. */
const BROTLI_DECODE = 8;
const BROTLI_ENCODE = 9;
/** The Brotli one-shots: every form takes the options entries. */
const BROTLI_SYNC_MEMBERS: ReadonlySet<string> = new Set([
"brotliCompressSync",
"brotliDecompressSync",
]);
const BROTLI_CALLBACK_MEMBERS: ReadonlySet<string> = new Set([
"brotliCompress",
"brotliDecompress",
]);
/** A Brotli mode connects the codec (scr_brotli.c) to the zlib engine
* before any module initializer runs. */
function noteBrotliMode(lowerer: Lowerer, mode: number, loc: SrcLoc): void {
if (mode !== BROTLI_DECODE && mode !== BROTLI_ENCODE) return;
const installs = (stmt: IrStmt): boolean =>
stmt.kind === "exprStmt" &&
stmt.expr.kind === "libCall" &&
stmt.expr.fn === "zlib.brotliInstall";
if (lowerer.mainPrelude.some(installs)) return;
lowerer.mainPrelude.push({
kind: "exprStmt",
expr: { kind: "libCall", fn: "zlib.brotliInstall", args: [], type: VOID, loc },
loc,
});
}
const ZLIB_STREAM_CLASS = "%Transform";
const ZLIB_STREAM_T: IrType = { kind: "object", className: ZLIB_STREAM_CLASS };
@@ -115,6 +146,7 @@ export function lowerZlibStreamNew(
}
const options =
args[0] !== undefined ? zlibOptionsValue(lowerer, args[0], what, loc) : dynUndefinedExpr(loc);
noteBrotliMode(lowerer, mode, loc);
return {
kind: "libCall",
fn: "zlib.streamNew",
@@ -154,8 +186,8 @@ function isZlibFallbackDecl(lowerer: Lowerer, d: ts.Node): boolean {
return false;
}
/** True when `access` names a member the zlib stream base class declares
* (bytesWritten, flush, params, close, reset). */
/** True when `access` names a member the zlib stream base classes declare
* (bytesWritten, flush, close and reset on ZlibBase; params on Zlib). */
export function isZlibStreamMember(lowerer: Lowerer, access: ts.PropertyAccessExpression): boolean {
const symbol = lowerer.checker.getSymbolAtLocation(access.name);
if (!symbol) return false;
@@ -163,7 +195,7 @@ export function isZlibStreamMember(lowerer: Lowerer, access: ts.PropertyAccessEx
(d) =>
d.parent !== undefined &&
ts.isClassDeclaration(d.parent) &&
d.parent.name?.text === "ZlibBase" &&
(d.parent.name?.text === "ZlibBase" || d.parent.name?.text === "Zlib") &&
isZlibFallbackDecl(lowerer, d.parent),
);
}
@@ -281,12 +313,17 @@ const ZLIB_ONE_SHOT_MODES: Readonly<Record<string, number | undefined>> = {
deflateRaw: 5,
inflateRaw: 6,
unzip: 7,
brotliDecompressSync: BROTLI_DECODE,
brotliCompressSync: BROTLI_ENCODE,
brotliDecompress: BROTLI_DECODE,
brotliCompress: BROTLI_ENCODE,
};
/** True for a zlib callback one-shot util.promisify converts (deflate,
* inflate, deflateRaw, inflateRaw, gzip, gunzip and unzip). */
* inflate, deflateRaw, inflateRaw, gzip, gunzip, unzip, brotliCompress and
* brotliDecompress). */
export function isPromisifiedZlibMember(member: string): boolean {
return ZLIB_CALLBACK_FNS[member] !== undefined;
return ZLIB_CALLBACK_FNS[member] !== undefined || BROTLI_CALLBACK_MEMBERS.has(member);
}
/** A call through `promisify(zlib.gzip)` and the other callback one-shots:
@@ -311,6 +348,7 @@ export function lowerPromisifiedZlibCall(
);
}
const input = zlibInput(lowerer, expr.arguments.nodes[0]!, loc);
noteBrotliMode(lowerer, ZLIB_ONE_SHOT_MODES[member]!, loc);
return {
kind: "libCall",
fn: input.dyn ? "zlib.promiseDyn" : "zlib.promise",
@@ -334,6 +372,8 @@ const ZLIB_STREAM_FACTORIES: Readonly<Record<string, number | undefined>> = {
createDeflateRaw: 5,
createInflateRaw: 6,
createUnzip: 7,
createBrotliDecompress: BROTLI_DECODE,
createBrotliCompress: BROTLI_ENCODE,
};
export function lowerZlibModuleCall(
@@ -356,6 +396,9 @@ export function lowerZlibModuleCall(
loc,
);
}
if (BROTLI_SYNC_MEMBERS.has(bi.member) || BROTLI_CALLBACK_MEMBERS.has(bi.member)) {
return lowerBrotliOneShot(lowerer, expr, bi.member, loc);
}
const syncFn = ZLIB_SYNC_FNS[bi.member];
if (syncFn !== undefined) {
if (expr.arguments.nodes.length === 0 || expr.arguments.nodes.length > 2) {
@@ -502,6 +545,57 @@ export function lowerZlibModuleCall(
throw new InternalCompilerError(`unhandled lowered zlib member ${bi.member}`);
}
/** brotliCompressSync(data[, options]), brotliCompress(data[, options],
* callback) and the decompress pair: the options entries with Node's
* BROTLI_ENCODE or BROTLI_DECODE mode, an absent options value passing
* undefined. */
function lowerBrotliOneShot(
lowerer: Lowerer,
expr: ts.CallExpression,
member: string,
loc: SrcLoc,
): IrExpr {
const args = expr.arguments.nodes;
const mode = ZLIB_ONE_SHOT_MODES[member]!;
const sync = BROTLI_SYNC_MEMBERS.has(member);
if (sync ? args.length === 0 || args.length > 2 : args.length !== 2 && args.length !== 3) {
lowerer.noLowering(
`zlib.${member} with ${args.length} arguments`,
expr,
sync
? `${member}(data) and ${member}(data, options) are supported`
: `${member}(data, callback) and ${member}(data, options, callback) are supported`,
);
}
const input = zlibInput(lowerer, args[0]!, loc);
const withOptions = sync ? args.length === 2 : args.length === 3;
const options = withOptions
? zlibOptionsValue(lowerer, args[1]!, `zlib.${member}`, loc)
: dynUndefinedExpr(loc);
noteBrotliMode(lowerer, mode, loc);
if (sync) {
return {
kind: "libCall",
fn: input.dyn ? "zlib.syncOptsDyn" : "zlib.syncOpts",
args: [numLit(mode, loc), input.value, options],
type: BYTES_U8,
loc,
};
}
return {
kind: "libCall",
fn: input.dyn ? "zlib.cbOptsDyn" : "zlib.cbOpts",
args: [
numLit(mode, loc),
input.value,
options,
errorFirstBytesCallback(lowerer, args[withOptions ? 2 : 1]!, "zlib"),
],
type: VOID,
loc,
};
}
/** The class name of a zlib stream mode (Node's constructor name). */
export function zlibClassName(mode: number): string {
for (const [name, m] of ZLIB_STREAM_MODES) if (m === mode) return name;
@@ -22,7 +22,10 @@ import { builtinImportOf } from "./module-bindings.js";
* lib/internal/fs/glob.js). The pattern and options cross as dyn values so
* the runtime validates them in Node's order. The result form (strings or
* Dirent rows) comes from the call's mapped result type, so withFileTypes
* must be statically known; the runtime checks that the option agrees. */
* must be statically known; the runtime checks that the option agrees.
* Because of that check, the form follows the checker's resolved overload
* even where an untyped options argument leaves the overload undecided
* (dynamicOverloadChoice). */
/** The interned Dirent record array (type-mapper.ts), or null. */
function direntArray(lowerer: Lowerer, type: IrType | null | undefined): boolean {
@@ -67,7 +70,7 @@ export function lowerFsGlobSyncCall(
loc: SrcLoc,
): IrExpr {
const args = globArgs(lowerer, call, "fs.globSync");
const callType = lowerer.mapTypeOf(lowerer.typeOf(call));
const callType = lowerer.mapTypeOf(lowerer.checker.getTypeAtLocation(call));
const types = direntArray(lowerer, callType);
if (types) fenceDirentExclude(lowerer, call, "fs.globSync");
if (!types && !(callType?.kind === "array" && callType.elem.kind === "string")) {
@@ -132,7 +135,9 @@ export function lowerFspGlobCall(lowerer: Lowerer, call: ts.CallExpression, loc:
}
const args = globArgs(lowerer, call, "fs/promises glob");
// The async iterator's yielded type (its first type argument).
const yielded = lowerer.checker.getTypeArguments(lowerer.typeOf(call) as ts.TypeReference)[0];
const yielded = lowerer.checker.getTypeArguments(
lowerer.checker.getTypeAtLocation(call) as ts.TypeReference,
)[0];
const element = yielded ? lowerer.mapTypeOf(yielded) : null;
const rows: IrType | null = element ? { kind: "array", elem: element } : null;
const types = direntArray(lowerer, rows);
@@ -1,4 +1,5 @@
import { primitiveConstructorOr } from "./lower-exprs.js";
import { TO_STRING_TAG_FIELD, wellKnownSymbolOfField } from "./symbol-fields.js";
import { InternalCompilerError } from "../../errors.js";
import { dynUndefinedExpr, helperLoc, nodeThrowExpr, varRef } from "../../ir/build.js";
import { constructedValue } from "./constructor-returns.js";
@@ -26,6 +27,7 @@ import {
isUnitType,
typeEquals,
typeKey,
type IrClassDef,
type IrExpr,
type IrFunction,
type IrParam,
@@ -153,6 +155,41 @@ const STREAM_NUM_PROPERTIES = new Set([
const streamProperty = (name: string): boolean =>
STREAM_BOOL_PROPERTIES.has(name) || STREAM_NUM_PROPERTIES.has(name);
/** `(instance) => string`, the string a class's inherited
* Symbol.toStringTag getter answers, which the runtime reads for
* Object.prototype.toString and util.inspect of an instance seen as
* `unknown`. None for a class without a string getter, or whose own tag
* field shadows it. */
function stringTagHelper(lowerer: Lowerer, info: ClassInfo, loc: SrcLoc): IrFunction | null {
if (info.fields.has(TO_STRING_TAG_FIELD)) return null;
const getter = findMethodOn(lowerer, info, "get:sym:toStringTag");
if (getter?.sig.ret.kind !== "string") return null;
const type: IrType = { kind: "object", className: info.def.name };
const owner = getter.declarer.def.name;
return {
name: `%class.stringTag:${info.def.name}`,
params: [{ localId: "this", name: "this", type }],
locals: [{ id: "this", name: "this", type, mutable: false }],
returnType: STRING,
body: [
{
kind: "return",
value: accessorCall(
lowerer,
owner,
"get:sym:toStringTag",
lowerer.upcastTo(varRef("this", type, loc), owner),
[],
STRING,
loc,
),
loc,
},
],
loc,
};
}
function symbolMemberKey(
lowerer: Lowerer,
info: ClassInfo,
@@ -717,6 +754,105 @@ export function classForInHasKey(
return { kind: "call", callee: name, args: [value, key], type: BOOL, loc };
}
/** Box class instances at their exact class. `%dyn.class.normalize` turns a
* capsule of a class with subclasses into a capsule of the instance's own
* class before any member table reads it, so every such box was followed
* by a second capsule. Each conversion of a value whose static class has a
* normalization branch now selects the exact class at the conversion, with
* the same tests in the same order (%dyn.class.boxExact:C), and the
* normalization of the resulting capsule falls through. Only JavaScript
* class hierarchies, whose instances live in untyped code, take the exact
* box. */
export function boxClassInstancesExactly(lowerer: Lowerer, functions: IrFunction[]): void {
const normalize = lowerer.liftedFnNamed("%dyn.class.normalize");
if (!normalize) return;
// Static class → its constructible descendants in normalization order.
const branches = new Map<string, string[]>();
for (const stmt of normalize.body) {
if (stmt.kind !== "if" || stmt.cond.kind !== "libCall" || stmt.cond.fn !== "dyn.typedRefIs")
continue;
const key = stmt.cond.args[1];
if (key?.kind !== "strLit" || !key.value.startsWith("object:")) continue;
const children: string[] = [];
for (const inner of stmt.then) {
if (inner.kind !== "if" || inner.cond.kind !== "instanceOf") continue;
children.push(inner.cond.className);
}
// TypeScript hierarchies keep their static-class capsules: their boxes
// are rarer, and an exact box would emit each subclass's checked view.
const className = key.value.slice("object:".length);
const decl = lowerer.classes.get(className)?.decl;
if (!decl || !isJsSourceFile(ts.getSourceFileOfNode(decl))) continue;
if (children.length) branches.set(className, children);
}
if (branches.size === 0) return;
const loc = normalize.loc;
const helpers = new Map<string, string>();
const helperFor = (className: string): string => {
const known = helpers.get(className);
if (known !== undefined) return known;
const name = `%dyn.class.boxExact:${className}`;
helpers.set(className, name);
const type: IrType = { kind: "object", className };
const value = varRef("value", type, loc);
functions.push({
name,
params: [{ localId: "value", name: "value", type }],
returnType: DYN,
locals: [{ id: "value", name: "value", type, mutable: false }],
body: [
...branches.get(className)!.map((child): IrStmt => ({
kind: "if",
cond: { kind: "instanceOf", value, className: child, type: BOOL, loc },
then: [
{
kind: "return",
value: {
kind: "dynFrom",
value: {
kind: "downcast",
value,
type: { kind: "object", className: child },
loc,
},
type: DYN,
loc,
},
loc,
},
],
else_: null,
loc,
})),
{ kind: "return", value: { kind: "dynFrom", value, type: DYN, loc }, loc },
],
loc,
});
return name;
};
const rewrite = (fn: IrFunction): void => {
fn.body = transformStmtList(fn.body, {
stmt: (statement) => statement,
expr: (expression) =>
expression.kind === "dynFrom" &&
expression.value.type.kind === "object" &&
branches.has(expression.value.type.className)
? {
kind: "call",
callee: helperFor(expression.value.type.className),
args: [expression.value],
type: DYN,
loc: expression.loc,
}
: expression,
});
};
for (const fn of [...functions]) {
if (fn === normalize || fn.name.startsWith("%dyn.class.boxExact:")) continue;
rewrite(fn);
}
}
/** Record field creation after the dispatch worklist closes, including writes
* in generated member tables. Native values stay in their slots; the bag
* owns only their property presence, attributes, and insertion position. */
@@ -1276,10 +1412,27 @@ export class ClassDynamicDispatch {
const info = lowerer.classes.get(className);
if (!info || info.builtinEmitter || info.builtinStream || info.builtinError) continue;
if (info.symbolFields && info.def.symbolFields === undefined) {
info.def.symbolFields = [...info.symbolFields].flatMap(([symbol, field]) => {
const global = lowerer.globalsBySymbol.get(symbol);
return global?.type.kind === "symbol" ? [{ field, globalId: global.id }] : [];
});
info.def.symbolFields = [...info.symbolFields].flatMap(
([symbol, field]): NonNullable<IrClassDef["symbolFields"]> => {
const global = lowerer.globalsBySymbol.get(symbol);
if (global?.type.kind === "symbol") return [{ field, globalId: global.id }];
const wellKnown = wellKnownSymbolOfField(field);
return wellKnown === null ? [] : [{ field, wellKnown }];
},
);
}
const tagged = (current: ClassInfo): ClassInfo[] => [
current,
...current.subclasses.flatMap(tagged),
];
for (const current of tagged(info)) {
if (current.def.stringTagHelper !== undefined) continue;
const fn = stringTagHelper(lowerer, current, functions[0]!.loc);
if (!fn) continue;
current.def.stringTagHelper = fn.name;
this.generated.add(fn);
lowerer.liftedFns.push(fn);
changed = true;
}
if (this.prototypeHelper && !this.prototypeClasses.has(className)) {
this.prototypeClasses.add(className);
@@ -1634,6 +1787,15 @@ export class ClassDynamicDispatch {
if (this.deletedNames === undefined)
this.deletedNames = deletedPropertyNames(lowerer.moduleOrder);
if (!root.base?.def.runtime && !fixedFieldFamily(root, this.deletedNames)) track(root);
// An Error family's bag records where `name` and `message` were
// created, which the error slots do not.
if (root.base?.def.name === "%Error") {
const order = (current: ClassInfo): void => {
current.def.errorBagOrder = true;
current.subclasses.forEach(order);
};
order(info);
}
}
const loc = this.propertyBag.loc;
const { receiver, condition } = this.propertyReceiver(plan, loc);
@@ -3554,10 +3716,29 @@ export class ClassDynamicDispatch {
name,
lowerer.coerceToExpected(varRef(global.id, global.type, loc), DYN),
);
} else if (wellKnownSymbolOfField(name) !== null) {
// A field keyed by a well-known symbol (Symbol.toStringTag).
symbols.set(name, {
kind: "dynFrom",
type: DYN,
loc,
value: {
kind: "libCall",
fn: "sym.wellKnown",
args: [
{ kind: "strLit", value: wellKnownSymbolOfField(name)!, type: STRING, loc },
],
type: SYMBOL_T,
loc,
},
});
}
}
for (let owner: ClassInfo | null = info; owner; owner = owner.base) {
for (const name of [...owner.methods.keys()])
for (const method of [...owner.methods.keys()]) {
// Accessors key by the property they define (`get [Symbol.toStringTag]`).
const name =
method.startsWith("get:") || method.startsWith("set:") ? method.slice(4) : method;
if (name.startsWith("sym:")) {
symbols.set(name, {
kind: "dynFrom",
@@ -3572,6 +3753,7 @@ export class ClassDynamicDispatch {
},
});
}
}
}
const branches: IrStmt[] = [];
if (!dispatch.write && !dispatch.reflect && !info.def.tracksOwnFields) {
@@ -46,6 +46,13 @@ export function collectClassMethodMutations(
const typedClasses: (ts.ClassDeclaration | ts.ClassExpression)[] = [];
const symbolMethods = new Set<string>();
let unknownKey: ts.Node | undefined;
/** Every write with an unknown key: each may install a replacement of any
* method name, and the key-order witness asks what each one installs. */
const unknownKeySites: ts.Node[] = [];
const markUnknownKey = (site: ts.Node): void => {
unknownKey = site;
unknownKeySites.push(site);
};
let numericKey: ts.Node | undefined;
let unknownOwnField = false;
// `sym:iterator` for `Symbol.iterator`: the name a class method declared
@@ -83,7 +90,7 @@ export function collectClassMethodMutations(
for (const key of computedMethods)
for (const name of literalValues(lowerer.typeOf(key)) ?? []) methods.add(String(name));
const markName = (name: string, site: ts.Node, arrow = false): void => {
if (name === "prototype" || name === "__proto__") unknownKey = site;
if (name === "prototype" || name === "__proto__") markUnknownKey(site);
if (methods.has(name)) lowerer.noteMethodAccess(name, site);
if (!arrow) receiverFields.add(name);
};
@@ -170,14 +177,32 @@ export function collectClassMethodMutations(
// A string index whose values cannot be functions excludes callable
// method slots. Class instances have no implicit index signature, so such
// a dictionary is only a class view when the class declares that index
// signature, and then every method must be assignable to its value type.
const functionFreeDictionary = (value: ts.Expression): boolean =>
lowerer.checker
.getIndexInfosOfType(lowerer.typeOf(value))
// signature, and then every method must be assignable to its value type,
// or when the dictionary keeps checked identity: it then holds the
// existing objects it receives, class instances included.
// A string-keyed dictionary that keeps checked identity holds the
// existing objects it receives, class instances included, so any write
// through it can replace a method.
const dictionaryView = (value: ts.Expression): boolean => {
const type = lowerer.typeOf(value);
return (
lowerer.checker.getPropertiesOfType(type).length === 0 &&
lowerer.checker
.getIndexInfosOfType(type)
.some((index) => (index.keyType.flags & ts.TypeFlags.StringLike) !== 0) &&
lowerer.classImplementedProtocol(type)
);
};
const functionFreeDictionary = (value: ts.Expression): boolean => {
const type = lowerer.typeOf(value);
if (lowerer.classImplementedProtocol(type)) return false;
return lowerer.checker
.getIndexInfosOfType(type)
.some(
(index) =>
(index.keyType.flags & ts.TypeFlags.StringLike) !== 0 && functionFree(index.type),
);
};
const literalLoopKeys = (key: ts.Expression): (string | number)[] | null => {
if (!ts.isIdentifier(key)) return null;
const symbol = lowerer.checker.getSymbolAtLocation(key);
@@ -401,7 +426,19 @@ export function collectClassMethodMutations(
(ts.isElementAccessExpression(base) &&
ts.isStringLiteralLike(base.argumentExpression) &&
base.argumentExpression.text === "prototype");
if (prototype) {
// `Object.prototype.toString.call(value)` calls the builtin and
// keeps no reference to it, so it replaces nothing.
const builtinCall =
ts.isPropertyAccessExpression(node) &&
ts.isPropertyAccessExpression(base) &&
ts.isIdentifier(unwrap(base.expression)) &&
lowerer.isStdlibGlobal(unwrap(base.expression) as ts.Identifier, "Object") &&
ts.isPropertyAccessExpression(node.parent) &&
node.parent.expression === node &&
(node.parent.name.text === "call" || node.parent.name.text === "apply") &&
ts.isCallExpression(node.parent.parent) &&
node.parent.parent.expression === node.parent;
if (prototype && !builtinCall) {
if (ts.isPropertyAccessExpression(node)) lowerer.noteMethodAccess(node.name.text, node);
else if (ts.isStringLiteralLike(node.argumentExpression))
lowerer.noteMethodAccess(node.argumentExpression.text, node);
@@ -449,11 +486,12 @@ export function collectClassMethodMutations(
(lowerer.isStdlibGlobal(node, "Object") || lowerer.isStdlibGlobal(node, "Reflect")) &&
(!ts.isPropertyAccessExpression(node.parent) || node.parent.expression !== node)
)
unknownKey = node;
markUnknownKey(node);
for (const node of reflection) {
const objectMethod = lowerer.stdlibGlobalMember(node, "Object");
const reflectMethod = lowerer.stdlibGlobalMember(node, "Reflect");
if (objectMethod === "setPrototypeOf" || reflectMethod === "setPrototypeOf") unknownKey = node;
if (objectMethod === "setPrototypeOf" || reflectMethod === "setPrototypeOf")
markUnknownKey(node);
const single =
objectMethod === "defineProperty" ||
reflectMethod === "defineProperty" ||
@@ -463,7 +501,7 @@ export function collectClassMethodMutations(
if (!single && !multiple) continue;
const call = node.parent;
if (!ts.isCallExpression(call) || call.expression !== node) {
unknownKey = node;
markUnknownKey(node);
continue;
}
const receiver = call.arguments.nodes[0];
@@ -577,7 +615,10 @@ export function collectClassMethodMutations(
);
for (const { target, site } of propertyWrites) {
const name = target.name.text;
if (functionFree(lowerer.typeOf(target)) || !methodOwnerCanInhabit(target.expression, name))
if (
!dictionaryView(target.expression) &&
(functionFree(lowerer.typeOf(target)) || !methodOwnerCanInhabit(target.expression, name))
)
continue;
if (lowerer.prototypeMethodAccesses.has(name)) lowerer.noteMethodSite(name, site);
else lowerer.noteMethodAccess(name, site);
@@ -618,7 +659,7 @@ export function collectClassMethodMutations(
// Numeric writes can replace numeric names but not ordinary names.
numericKey = site;
} else if (!(flags & ts.TypeFlags.ESSymbolLike)) {
unknownKey = site;
markUnknownKey(site);
}
}
if (numericKey) {
@@ -627,7 +668,10 @@ export function collectClassMethodMutations(
}
}
if (unknownKey) {
for (const method of methods) lowerer.noteMethodAccess(method, unknownKey);
for (const method of methods) {
lowerer.prototypeMethodAccesses.set(method, unknownKey);
for (const site of unknownKeySites) lowerer.noteMethodSite(method, site);
}
for (const method of symbolMethods) lowerer.replacedSymbolMethods.add(method);
}
if (!unknownKey && !unknownOwnField)
@@ -10,6 +10,9 @@ export class ClassProtocols {
private readonly accepted = new Map<ts.Type, ts.Symbol[]>();
/** What markIdentityFlows learned about the program, while it runs. */
private facts: FlowFacts | null = null;
/** The value types of the string-keyed dictionaries that keep checked
* identity (see markDictionaryViews). */
private readonly dictionaryViews = new Set<ts.Type>();
constructor(
private readonly checker: ts.TypeChecker,
@@ -323,6 +326,7 @@ export class ClassProtocols {
else if (names !== "all") names.add(name);
}
}
this.markDictionaryViews(sites, sourceNodes, observedNodes, writeNodes);
for (let i = 0; i < looseArguments.length; i++) {
const type = typeOf(looseNodes[i]!);
if (type === undefined) continue;
@@ -697,6 +701,163 @@ export class ClassProtocols {
return source !== undefined && this.isProgramClassInstance(source);
}
/** String-keyed dictionaries (`Record<string, V>`, `{ [key: string]: V }`)
* that receive an existing record or class instance. Natively a
* dictionary is a key/value record, and converting another object into
* one copies its fields, so writes through the dictionary and identity
* checks would miss the original. When the program writes through, or
* observes, a value of such a dictionary type, every dictionary with that
* value type keeps checked identity instead (records with the same value
* type share one native layout, so values pass between them). A source
* counts when it is an existing object (a binding, member, element or
* `this`, not a fresh literal or call result) whose type is a program
* class instance or a record with declared properties, or a constant
* `{}`, `object` or `unknown` binding initialized with one. */
private markDictionaryViews(
sites: readonly {
source: ts.Expression;
target: () => ts.Type | undefined;
contextual?: ts.Expression;
}[],
sourceNodes: readonly ts.Expression[],
observedNodes: readonly ts.Expression[],
writeNodes: readonly ts.Expression[],
): void {
// The calls the sites' arguments belong to, by callee name, built on
// first use.
let calls: Map<string, (ts.CallExpression | ts.NewExpression)[]> | null = null;
const callsNamed = (name: string): readonly (ts.CallExpression | ts.NewExpression)[] => {
if (calls === null) {
calls = new Map();
for (const site of sites) {
const call = site.contextual?.parent;
if (call === undefined || (!ts.isCallExpression(call) && !ts.isNewExpression(call)))
continue;
const callee = call.expression;
const named = ts.isIdentifier(callee)
? callee.text
: ts.isPropertyAccessExpression(callee)
? callee.name.text
: undefined;
if (named === undefined) continue;
const list = calls.get(named) ?? [];
if (!list.includes(call)) list.push(call);
calls.set(named, list);
}
}
return calls.get(name) ?? [];
};
const candidates: { index: number; sourceType: ts.Type }[] = [];
for (let i = 0; i < sites.length; i++) {
const source = sourceNodes[i]!;
if (!isExistingObject(source)) continue;
const sourceType = attempt(() => this.checker.getTypeAtLocation(source));
if (sourceType !== undefined && this.dictionarySource(source, sourceType, callsNamed))
candidates.push({ index: i, sourceType });
}
if (candidates.length === 0) return;
// One batch for the contextual targets of the candidate sites.
attempt(() =>
this.checker.prefetchContextualTypes(
candidates.flatMap(({ index }) => {
const contextual = sites[index]!.contextual;
return contextual === undefined ? [] : [contextual];
}),
),
);
const received = new Set<ts.Type>();
for (const { index, sourceType } of candidates) {
const target = attempt(sites[index]!.target);
const parts =
target === undefined ? [] : target.isUnionType() ? ts.constituentTypes(target) : [target];
for (const part of parts) {
const value = this.dictionaryValueType(part);
// A dictionary of `unknown` or `any` values is checked-dynamic already.
if (
value !== undefined &&
(value.flags & (ts.TypeFlags.Unknown | ts.TypeFlags.Any)) === 0 &&
this.dictionaryValueType(sourceType) !== value
)
received.add(value);
}
}
if (received.size === 0) return;
// The dictionary values the program writes through or observes.
const seen = new Set<ts.Type>();
for (const node of [...writeNodes, ...observedNodes]) {
const type = attempt(() => this.checker.getTypeAtLocation(node));
const parts =
type === undefined ? [] : type.isUnionType() ? ts.constituentTypes(type) : [type];
for (const part of parts) {
const value = this.dictionaryValueType(part);
if (value !== undefined) seen.add(value);
}
}
for (const value of received) if (seen.has(value)) this.dictionaryViews.add(value);
}
/** Whether `source` holds an existing object that a dictionary
* conversion would copy. */
private dictionarySource(
source: ts.Expression,
type: ts.Type,
callsNamed: (name: string) => readonly (ts.CallExpression | ts.NewExpression)[],
): boolean {
if (this.isProgramClassInstance(type) || this.isRecordWithFields(type)) return true;
if (!isDynamicView(this.checker, type) || !ts.isIdentifier(source)) return false;
const decl = attempt(() => this.checker.getSymbolAtLocation(source))?.valueDeclaration;
if (decl === undefined) return false;
const holdsObject = (expr: ts.Expression): boolean => {
const held = attempt(() => this.checker.getTypeAtLocation(this.flowSourceNode(expr)));
return (
held !== undefined && (this.isProgramClassInstance(held) || this.isRecordWithFields(held))
);
};
if (ts.isParameter(decl)) {
// A parameter holds what its callers pass: the calls of the function
// by name whose signature resolves to it.
const fn = decl.parent;
if (fn === undefined || !ts.isFunctionLike(fn)) return false;
const index = fn.parameters.nodes.indexOf(decl);
const name = functionName(fn);
if (name === undefined || index < 0) return false;
return callsNamed(name).some((call) => {
const argument = call.arguments?.nodes[index];
return (
argument !== undefined &&
!ts.isSpreadElement(argument) &&
attempt(() => this.checker.getResolvedSignature(call)?.declaration) === fn &&
holdsObject(argument)
);
});
}
return (
ts.isVariableDeclaration(decl) &&
decl.initializer !== undefined &&
ts.isVariableDeclarationList(decl.parent) &&
(decl.parent.flags & ts.NodeFlags.Const) !== 0 &&
holdsObject(decl.initializer)
);
}
/** A program object type with declared properties that is not a class
* instance, array, tuple, function or library type. */
private isRecordWithFields(type: ts.Type): boolean {
return this.isStructuralTarget(type) && this.checker.getPropertiesOfType(type).length > 0;
}
/** The value type of a string-keyed dictionary with no declared
* properties, or undefined for any other type. */
private dictionaryValueType(type: ts.Type): ts.Type | undefined {
if ((type.flags & ts.TypeFlags.Object) === 0) return undefined;
if (this.checker.isArrayType(type) || this.checker.isTupleType(type)) return undefined;
if (this.checker.getCallSignatures(type).length > 0) return undefined;
if (this.checker.getPropertiesOfType(type).length > 0) return undefined;
return this.checker
.getIndexInfosOfType(type)
.find((index) => (index.keyType.flags & ts.TypeFlags.String) !== 0)?.type;
}
private isProgramClassInstance(type: ts.Type): boolean {
const symbol = type.getSymbol();
return (
@@ -822,7 +983,13 @@ export class ClassProtocols {
if ((type.flags & ts.TypeFlags.Object) === 0) return false;
const previous = this.answers.get(type);
if (previous !== undefined) return previous;
const answer = this.protocol(type);
// A dictionary keeps checked identity when the protocols already mark
// it (a literal whose methods read `this`) or when it receives existing
// objects (markDictionaryViews).
const dictionaryValue = this.dictionaryValueType(type);
const answer =
this.protocol(type) ||
(dictionaryValue !== undefined && this.dictionaryViews.has(dictionaryValue));
this.answers.set(type, answer);
return answer;
}
@@ -922,6 +1089,44 @@ const IDENTITY_METHODS = new Set([
"includes",
]);
/** An expression that denotes an object that already exists, not a fresh
* literal or call result. */
function isExistingObject(node: ts.Expression): boolean {
return (
ts.isIdentifier(node) ||
ts.isPropertyAccessExpression(node) ||
ts.isElementAccessExpression(node) ||
node.kind === ts.SyntaxKind.ThisKeyword
);
}
/** `{}`, `object` and `unknown`: types whose values are checked-dynamic
* and keep the original object. */
function isDynamicView(checker: ts.TypeChecker, type: ts.Type): boolean {
if (type.flags & (ts.TypeFlags.Unknown | ts.TypeFlags.NonPrimitive)) return true;
return (
(type.flags & ts.TypeFlags.Object) !== 0 &&
checker.getPropertiesOfType(type).length === 0 &&
checker.getIndexInfosOfType(type).length === 0 &&
checker.getCallSignatures(type).length === 0 &&
checker.getConstructSignatures(type).length === 0
);
}
/** The name calls use for a function, method or function-valued binding. */
function functionName(fn: ts.Node): string | undefined {
if ((ts.isFunctionDeclaration(fn) || ts.isMethodDeclaration(fn)) && fn.name !== undefined)
return ts.isIdentifier(fn.name) ? fn.name.text : undefined;
if (
(ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) &&
fn.parent !== undefined &&
ts.isVariableDeclaration(fn.parent) &&
ts.isIdentifier(fn.parent.name)
)
return fn.parent.name.text;
return undefined;
}
function isNullishLiteral(node: ts.Expression): boolean {
while (ts.isParenthesizedExpression(node)) node = node.expression;
return (
@@ -13,6 +13,7 @@ import * as ts from "../ts7/adapter.js";
import type { Lowerer } from "./lowerer.js";
import { accessorCall, findMethodOn, type ClassInfo } from "./lower-classes.js";
import { isClassCallback } from "./class-callbacks.js";
import { TO_STRING_TAG_FIELD } from "./symbol-fields.js";
import { isSafeToRepeat } from "./expressions/evaluation-safety.js";
import { DATE_TIME_FIELD, dateRootOf } from "./builtins/date-classes.js";
import { dateValueMethod } from "./builtins/dates.js";
@@ -29,7 +30,15 @@ type ConversionMethod = "sym:toPrimitive" | "valueOf" | "toString";
* Date`, or through Object.prototype.toString ("[object Object]"). */
type Plan =
| { method: ConversionMethod; ret: IrType }
| { method: null; date?: "toString" | "valueOf" };
| { method: null; date?: "toString" | "valueOf"; tag?: ToStringTag };
/** Where Object.prototype.toString finds a class's Symbol.toStringTag: a
* string field (`[Symbol.toStringTag] = "LRUCache"`) or a getter that
* returns a string, declared on `owner`. */
interface ToStringTag {
owner: ClassInfo;
getter: boolean;
}
const PRIMITIVE_KINDS = new Set(["f64", "string", "bool", "bigint", "undefinedT", "nullT"]);
@@ -52,6 +61,17 @@ export function classToStringConversion(
);
}
/** Object.prototype.toString of a program class instance: "[object " +
* its Symbol.toStringTag + "]" (an own tag field, else an inherited
* getter), or "[object Object]". The class's own conversion methods do not
* take part. Null as for classToStringConversion, and for Date subclasses. */
export function classObjectTag(lowerer: Lowerer, value: IrExpr, node: ts.Node): IrExpr | null {
if (value.type.kind !== "object") return null;
const info = lowerer.classes.get(value.type.className);
if (!info || dateRootOf(info) !== null) return null;
return classConversion(lowerer, value, "tag", node, STRING, (tag) => tag);
}
/** ToNumber(ToPrimitive(value, "number")) of a program class instance
* (`Number(x)`): `[Symbol.toPrimitive]("number")`, then `valueOf` and then
* `toString`, and NaN for "[object Object]". Null as for
@@ -122,7 +142,7 @@ function mayBeBigint(lowerer: Lowerer, type: IrType): boolean {
function classConversion(
lowerer: Lowerer,
value: IrExpr,
hint: ConversionHint,
hint: ConversionHint | "tag",
node: ts.Node,
resultType: IrType,
finish: (primitive: IrExpr) => IrExpr,
@@ -131,7 +151,7 @@ function classConversion(
const info = lowerer.classes.get(value.type.className);
if (!info || !plainProgramClass(info)) return null;
const classes = subtree(lowerer, info);
for (const cls of classes) refuseReplacedConversions(lowerer, cls, node);
if (hint !== "tag") for (const cls of classes) refuseReplacedConversions(lowerer, cls, node);
const plans = new Map(classes.map((cls) => [cls, planOf(lowerer, cls, hint, node)]));
// A class whose plan differs from its base's starts a group; the deepest
// groups are tested first, so each instance takes its own class's plan.
@@ -164,12 +184,52 @@ function classConversion(
};
return finish(dateValueMethod(plan.date, time, loc)!);
}
if (plan.method === null && plan.tag) {
// Object.prototype.toString: "[object " + the tag + "]".
const owner = plan.tag.owner.def.name;
const tag: IrExpr = plan.tag.getter
? accessorCall(
lowerer,
owner,
"get:sym:toStringTag",
lowerer.upcastTo(object, owner),
[],
STRING,
loc,
)
: {
kind: "fieldGet",
obj: lowerer.upcastTo(object, owner),
className: owner,
field: TO_STRING_TAG_FIELD,
type: STRING,
loc,
};
return finish({
kind: "strConcat",
left: {
kind: "strConcat",
left: { kind: "strLit", value: "[object ", type: STRING, loc },
right: tag,
type: STRING,
loc,
},
right: { kind: "strLit", value: "]", type: STRING, loc },
type: STRING,
loc,
});
}
if (plan.method === null)
return finish({ kind: "strLit", value: "[object Object]", type: STRING, loc });
const args: IrExpr[] =
plan.method === "sym:toPrimitive" ? [{ kind: "strLit", value: hint, type: STRING, loc }] : [];
return finish(accessorCall(lowerer, cls.def.name, plan.method, object, args, plan.ret, loc));
};
// "[object Object]" reads nothing of the instance, but the receiver
// expression still runs, once.
const own = plans.get(info)!;
if (tops.length === 0 && own.method === null && !own.date && !own.tag && !isSafeToRepeat(value))
prefix.push({ kind: "exprStmt", expr: value, loc });
let result = convert(info, receiver);
for (let i = tops.length - 1; i >= 0; i--) {
const top = tops[i]!;
@@ -228,10 +288,28 @@ function depth(info: ClassInfo): number {
}
function samePlan(a: Plan, b: Plan): boolean {
return (
a.method === b.method &&
(a.method !== null || b.method !== null || a.date === (b as { date?: string }).date)
);
if (a.method !== null || b.method !== null) return a.method === b.method;
return a.date === b.date && a.tag?.owner === b.tag?.owner && a.tag?.getter === b.tag?.getter;
}
/** The class's Symbol.toStringTag, when it has one: the class at the top
* of the chain whose layout holds the tag field (an own property, which
* shadows any getter), or its getter. Other tag types would make
* Object.prototype.toString answer "Object" or the value depending on the
* run, and are refused. */
function toStringTagOf(lowerer: Lowerer, info: ClassInfo, node: ts.Node): ToStringTag | null {
const type = info.fields.get(TO_STRING_TAG_FIELD);
if (type !== undefined) {
if (type.kind !== "string") refuse(lowerer, node, info, "a non-string Symbol.toStringTag");
let owner = info;
while (owner.base?.fields.has(TO_STRING_TAG_FIELD)) owner = owner.base;
return { owner, getter: false };
}
const getter = findMethodOn(lowerer, info, "get:sym:toStringTag");
if (!getter) return null;
if (getter.sig.ret.kind !== "string")
refuse(lowerer, node, info, "a non-string Symbol.toStringTag");
return { owner: getter.declarer, getter: true };
}
/** A conversion method the class declares or inherits (a field of that
@@ -246,7 +324,16 @@ function declared(
return found ? { ret: found.sig.ret } : null;
}
function planOf(lowerer: Lowerer, info: ClassInfo, hint: ConversionHint, node: ts.Node): Plan {
function planOf(
lowerer: Lowerer,
info: ClassInfo,
hint: ConversionHint | "tag",
node: ts.Node,
): Plan {
if (hint === "tag") {
const tag = toStringTagOf(lowerer, info, node);
return tag ? { method: null, tag } : { method: null };
}
const exotic = declared(lowerer, info, "sym:toPrimitive");
if (exotic) {
if (!primitive(lowerer, exotic.ret))
@@ -271,7 +358,8 @@ function planOf(lowerer: Lowerer, info: ClassInfo, hint: ConversionHint, node: t
refuse(lowerer, node, info, `a '${method}' method that can return an object`);
return { method, ret: found.ret };
}
return { method: null };
const tag = toStringTagOf(lowerer, info, node);
return tag ? { method: null, tag } : { method: null };
}
function primitive(lowerer: Lowerer, type: IrType): boolean {
@@ -292,8 +380,6 @@ function refuseReplacedConversions(lowerer: Lowerer, info: ClassInfo, node: ts.N
if (lowerer.prototypeMethodAccesses.has(method))
refuse(lowerer, node, info, `a '${method}' the program reaches through a prototype`);
}
if (findMethodOn(lowerer, info, "get:sym:toStringTag") || info.fields.has("sym:toStringTag"))
refuse(lowerer, node, info, "a Symbol.toStringTag");
}
function refuse(lowerer: Lowerer, node: ts.Node, info: ClassInfo, what: string): never {
@@ -707,11 +707,17 @@ function rewriteFunction(
* unchecked. */
export function refuseUncheckedCycleReads(
lowerer: Lowerer,
classes: readonly { baseValueGlobal?: string; symbolFields?: { globalId: string }[] }[],
classes: readonly {
baseValueGlobal?: string;
symbolFields?: readonly ({ globalId: string } | { wellKnown: string })[];
}[],
): void {
if (lowerer.cycleInitFlags.size === 0) return;
for (const cls of classes) {
const ids = [cls.baseValueGlobal, ...(cls.symbolFields ?? []).map((f) => f.globalId)];
const ids = [
cls.baseValueGlobal,
...(cls.symbolFields ?? []).map((f) => ("globalId" in f ? f.globalId : undefined)),
];
for (const id of ids) {
const flag = id === undefined ? undefined : lowerer.cycleInitFlags.get(id);
if (flag === undefined) continue;
@@ -45,6 +45,7 @@ import {
lowerDynMemberAssignment,
lowerOptionalArrayIndexRead,
lowerOptionalNumber,
assertedReadObservesUndefined,
recordKeyResultOk,
representedClassFieldTarget,
runtimeOptionalElementKey,
@@ -381,13 +382,18 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre
if (index.type.kind !== "f64") {
lowerer.unsupported("SC1090", expr.argumentExpression, "indexing with non-number keys");
}
// `t[i]!` asserts presence: the numeric element read (NaN when
// invalid). Every other read answers undefined for an invalid index,
// like Node.
// `t[i]!` performs no runtime check, but its static type is a number:
// it takes the numeric element read (NaN when invalid), except where
// the value reaches a use that tells undefined from NaN (an equality, a
// string conversion, `typeof`, an array element), which reads undefined
// for an invalid index like `t[i]`. Every other read answers undefined
// for an invalid index, like Node.
// A read whose every consumer treats undefined like NaN (see
// nan-coded-reads.ts) is the same plain numeric read.
if (
(ts.isNonNullExpression(expr.parent) && expr.parent.expression === expr) ||
(ts.isNonNullExpression(expr.parent) &&
expr.parent.expression === expr &&
!assertedReadObservesUndefined(lowerer, expr.parent)) ||
lowerer.nanCodedReads?.isNumericRead(expr) === true
) {
return {
@@ -17,6 +17,7 @@ import { PoisonError, dynFallbackType } from "./lowerer.js";
import { functionCanReturnUndefined } from "../function-completion.js";
import { jsBindingAppendsOnly, jsBindingHasOpenWrites } from "./lower-stmts.js";
import type { FnCtx } from "./function-context.js";
import { returnsGuessedOverloadValue } from "./overload-choice.js";
/** Keep the conservative completion rule shared by return construction
* and inference. Nested control flow may still make completion unreachable. */
@@ -256,6 +257,22 @@ export function declaredReturnType(
return decl.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword)
? { kind: "promise", inner: DYN }
: DYN;
// A JS function returning a value whose type rests on an undecided
// overload (guessedOverloadValue): the inferred result is the first
// overload's, which the returned value need not have.
if (
isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
!decl.type &&
!decl.asteriskToken &&
!hasExplicitJsDocReturn(decl) &&
mappedReturn !== null &&
mappedReturn.kind !== "dyn" &&
!(mappedReturn.kind === "promise" && mappedReturn.inner.kind === "dyn") &&
returnsGuessedOverloadValue(lowerer, decl)
)
return decl.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword)
? { kind: "promise", inner: DYN }
: DYN;
// JS inference assigns Number to arithmetic over any, although the
// runtime operands can both be BigInts. Preserve that native result.
if (
@@ -29,7 +29,7 @@ interface Fixture {
notes: KeyOrderNotes;
record(fields: [string, IrType][], order: string[]): IrType & { kind: "record" };
literal(type: IrType, keys: string[], segments?: KeyOrderSegment[]): IrExpr;
run(body: IrStmt[]): IrFunction;
run(body: IrStmt[], name?: string): IrFunction;
order(type: IrType & { kind: "record" }): string[] | undefined;
}
@@ -73,8 +73,8 @@ function fixture(): Fixture {
segments ?? keys.map((name) => ({ kind: "key", name, definite: true })),
);
};
const run = (body: IrStmt[]): IrFunction => {
const fn: IrFunction = { name: "main", params: [], returnType: F64, locals: [], body, loc };
const run = (body: IrStmt[], name = "main"): IrFunction => {
const fn: IrFunction = { name, params: [], returnType: F64, locals: [], body, loc };
applyKeyOrderWitnesses(lowerer, [fn]);
return fn;
};
@@ -706,7 +706,10 @@ test("a spread reads its source's order right after the literal evaluates the so
/** `{ a, b }` with a declared table `["a", "b"]` and a literal `{ b, a }`
* plus `{ ...value, a }` of the same shape, beside `extra`. */
function selfCopy(extra: (f: Fixture, ab: IrType & { kind: "record" }) => IrStmt[]): string[] {
function selfCopy(
extra: (f: Fixture, ab: IrType & { kind: "record" }) => IrStmt[],
fnName = "main",
): string[] {
const f = fixture();
const ab = f.record(
[
@@ -716,11 +719,14 @@ function selfCopy(extra: (f: Fixture, ab: IrType & { kind: "record" }) => IrStmt
["a", "b"],
);
const self: KeyOrderSegment = { kind: "spread", shapeId: ab.shapeId, maybeAbsent: false };
f.run([
decl("v", f.literal(ab, ["b", "a"])),
observe(f.literal(ab, ["a", "b"], [self, { kind: "key", name: "a", definite: true }])),
...extra(f, ab),
]);
f.run(
[
decl("v", f.literal(ab, ["b", "a"])),
observe(f.literal(ab, ["a", "b"], [self, { kind: "key", name: "a", definite: true }])),
...extra(f, ab),
],
fnName,
);
return f.order(ab)!;
}
@@ -762,6 +768,31 @@ test("a replaceable method's checked result builds records when a replacement ma
).toEqual(["a", "b"]);
});
/** A native callback taking the shape, boxed for JavaScript. */
const boxedCallback = (ab: IrType): IrExpr => ({
kind: "dynFrom",
value: {
kind: "closure",
fnName: "callback",
captures: [],
type: { kind: "func", params: [ab], ret: { kind: "void" } },
loc,
},
type: { kind: "dyn" },
loc,
});
test("a native callback boxed for JavaScript receives records JavaScript builds", () => {
expect(selfCopy((_, ab) => [observe(boxedCallback(ab))])).toEqual(["a", "b"]);
});
test("a class's prototype slots box its methods without building their parameters", () => {
expect(selfCopy((_, ab) => [observe(boxedCallback(ab))], "%prototype.data.Service")).toEqual([
"b",
"a",
]);
});
test("a replaceable method's result keeps its witness through an IR rewrite", () => {
expect(
selfCopy((f, ab) => {
@@ -292,6 +292,17 @@ function replacesWithoutRecords(lowerer: Lowerer, site: ts.Node): boolean {
);
}
/** A compiler helper that boxes a class's own state for JavaScript's reads:
* its prototype data and the property and method dispatch over boxed
* instances. */
function boxesOwnState(fnName: string): boolean {
return (
fnName.startsWith("%prototype.data.") ||
fnName.startsWith("%dyn.class.") ||
fnName.startsWith("%class.")
);
}
/** A type whose values are primitives (or void): it holds no record. */
function primitiveOnly(type: ts.Type): boolean {
if (type.isUnionType()) return ts.constituentTypes(type).every(primitiveOnly);
@@ -357,8 +368,18 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
* its checked field setters, each a witness of its own. A checked
* function's parameters receive native values: a record parameter is
* passed out, never built, though a callback among them receives values
* built from JavaScript again. */
const shapesIn = (type: IrType, out: Set<string>, seen: Set<string>, produced = false): void => {
* built from JavaScript again. `built` false starts the walk at a value
* passed out (a native value boxed for JavaScript); `writes` false leaves
* out what JavaScript could write into the containers passed out, so the
* walk counts only what the functions among them receive. */
const shapesIn = (
type: IrType,
out: Set<string>,
seen: Set<string>,
produced = false,
built = true,
writes = true,
): void => {
const visit = (t: IrType, built: boolean): boolean => {
const mark = built ? "" : "-";
// A container JavaScript builds converts into fresh native storage, so
@@ -367,7 +388,7 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
// such as callbacks whose parameters it then builds (children passed
// out). Those children count in both directions.
const child = (type: IrType): boolean => {
visit(type, true);
if (built || writes) visit(type, true);
if (!built) visit(type, false);
return true;
};
@@ -401,7 +422,7 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
}
return everyTypeChild(t, child);
};
visit(type, true);
visit(type, built);
};
const opaqueShapes = new Set<string>();
const opaqueSeen = new Set<string>();
@@ -412,6 +433,7 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
* declaration's order, the order their table holds. */
const declaredShapes = new Set<string>();
const declaredSeen = new Set<string>();
const boxedSeen = new Set<string>();
/** The checked result of a replaceable method call while every site that
* can replace the method installs functions that return no record. */
const unchangedMethodResult = (node: IrExpr): boolean => {
@@ -518,6 +540,16 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
case "jsonStringify":
case "dynFrom":
observedRoots.push(e.value.type);
// A native value the program hands to JavaScript: the callbacks it
// holds (itself, a field, an element) receive records JavaScript
// builds. The helpers that box the program's own state for
// JavaScript's reads (a class's prototype slots and property
// dispatch) hand out the class's methods, whose arguments the
// program's own calls pass as native values, as a replaceable
// method's result above. Writes through the boxed value are left
// out: reading a boxed value creates no record.
if (!boxesOwnState(fnName))
shapesIn(e.value.type, runtimeShapes, boxedSeen, true, false, false);
return;
case "recordKeyGet":
observedRoots.push(e.obj.type);
@@ -1355,7 +1355,7 @@ function lowerAssertMatch(
lowerer.noLowering(`${surface} with ${expr.arguments.nodes.length} arguments`, expr);
}
const s = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING);
const re = lowerer.lowerExpr(expr.arguments.nodes[1]!);
const re = lowerExpectedValue(lowerer, expr.arguments.nodes[1]!);
if (re.type.kind !== "regex") {
lowerer.noLowering(
`${surface} with a non-regex pattern`,
@@ -1420,6 +1420,18 @@ function inErrorHierarchy(lowerer: Lowerer, className: string): boolean {
return false;
}
/** An expected string or regex: a value JavaScript stores checked-dynamic
* (an object literal's field, an untyped binding) converts to the string
* or regex its checker type names, so the comparison takes that arm. */
function lowerExpectedValue(lowerer: Lowerer, node: ts.Expression): IrExpr {
const value = lowerer.lowerExpr(node);
if (value.type.kind !== "dyn") return value;
const expected = lowerer.mapTypeOf(lowerer.typeOf(node));
return expected?.kind === "string" || expected?.kind === "regex"
? lowerer.coerceToExpected(value, expected)
: value;
}
/** Classify the expected argument: string → message, regex value → the
* regex test over String(error), an error-class identifier → instanceof,
* an object LITERAL over name/message/code (string or regex values) →
@@ -1436,7 +1448,7 @@ function classifyThrowsExpected(
return { form: "message", msg: m.msg, hasMsg: m.hasMsg };
}
if (expectedT?.kind === "regex") {
return { form: "regex", value: lowerer.lowerExpr(node) };
return { form: "regex", value: lowerExpectedValue(lowerer, node) };
}
const sym = ts.isIdentifier(node) ? lowerer.resolveValueSymbol(node) : null;
const info =
@@ -1509,7 +1521,7 @@ function classifyThrowsExpected(
);
}
const valueNode = ts.isPropertyAssignment(named) ? named.initializer : named.name;
const value = lowerer.lowerExpr(valueNode);
const value = lowerExpectedValue(lowerer, valueNode);
if (value.type.kind === "string") {
byId.set(id, { id, kind: "str", value, enameBake: null });
} else if (value.type.kind === "regex") {
@@ -192,7 +192,7 @@ import {
import { isSafeToDiscard } from "./expressions/evaluation-safety.js";
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
import { lowerChildMethodValue, lowerChildPropertyCall } from "./handle-properties.js";
import { classToNumberConversion } from "./class-to-primitive.js";
import { classObjectTag, classToNumberConversion } from "./class-to-primitive.js";
import { hasRuntimeStatics } from "./class-runtime-statics.js";
import { structuralReadType } from "./class-unions.js";
import {
@@ -265,6 +265,7 @@ import {
completeArgs,
} from "./call-arguments.js";
import { isThisParameter, type ParamShape } from "./call-signatures.js";
import { dynamicOverloadChoice } from "./overload-choice.js";
import { lowerStringIteratorValue } from "./iterator-adapters.js";
import { checkedArmExtraction } from "./coercions/unions.js";
import { maybePromiseReturnStmt, promiseArmedUnionOf } from "./maybe-promise.js";
@@ -3121,6 +3122,10 @@ function lowerObjectPrototypeCall(
const immediate = immediateObjectTagReceiver(lowerer, receiverNode);
const receiver = immediate?.effect ?? lowerer.lowerExpr(receiverNode);
const tag = immediate?.tag ?? objectToStringTag(lowerer, receiver.type);
if (tag === null && immediate === null && call.arguments.nodes.length === 1) {
const classTag = classObjectTag(lowerer, receiver, receiverNode);
if (classTag) return classTag;
}
// A generator object answers its tag through its checked capsule.
if (tag === null && receiver.type.kind !== "dyn" && receiver.type.kind !== "generator") {
lowerer.noLowering(
@@ -5512,6 +5517,9 @@ function reconcileOverloadReturn(
expr: ts.CallExpression | ts.TaggedTemplateExpression,
call: IrExpr,
): IrExpr {
// The checker's overload is a guess for a checked-dynamic argument
// another overload accepts: the implementation's value stands.
if (ts.isCallExpression(expr) && dynamicOverloadChoice(lowerer, expr)) return call;
const rsig = lowerer.checker.getResolvedSignature(expr);
const rdecl = rsig ? rsig.declaration : undefined;
// Nominal covariant overrides share the base's return ABI, but retain
@@ -4432,6 +4432,14 @@ export function collectClassShapeInner(
// INSTANTIATION prints its family's name — JS has one `Box`.
jsName: jsNameOverride ?? decl.name?.text ?? "",
...(base ? { base: base.def.name } : {}),
...(decl.members.nodes.some(
(member) =>
ts.isPropertyDeclaration(member) &&
!ts.isPrivateIdentifier(member.name) &&
member.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.StaticKeyword),
)
? { ownStaticFields: true as const }
: {}),
jsLength:
ctor?.parameters?.nodes.findIndex(
(parameter) => parameter.initializer || parameter.dotDotDotToken,
@@ -8197,7 +8205,10 @@ export function classMemberNameOf(lowerer: Lowerer, name: ts.PropertyName): stri
member === "asyncIterator" ||
member === "dispose" ||
member === "asyncDispose" ||
member === "toPrimitive"
member === "toPrimitive" ||
// A toStringTag field keeps its symbol field name; its accessor and
// method forms take the method slot name.
(member === "toStringTag" && !ts.isPropertyDeclaration(name.parent))
) {
return `sym:${member}`;
}
@@ -249,7 +249,8 @@ export function lowerConsoleArg(
// inspect at the rest-args depth 2 (formatWithOptions) — scalar
// kinds byte-exactly, boxed functions as [Function: name] /
// [Function (anonymous)], composites through the dyn walk
// (insp.dyn). Never throws — Node's console.log never does.
// (insp.dyn). A value the inspector does not render throws the
// 'not supported yet' Error before anything is written.
if (lowered.type.kind === "dyn") {
return {
kind: "libCall",
@@ -431,6 +431,10 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
return inner;
const use = runtimeOptionalUseOf(expr);
if (runtimeOptionalAssertionErases(lowerer, expr, inner, use)) return inner;
// A destination that holds null or undefined keeps the value, as `!`
// does in JavaScript (`return cache.get(key)!` from a function that
// returns `T | null`); the destination's own coercion re-tags it.
if (assertionDestinationHoldsNullish(lowerer, expr, inner.type)) return inner;
const operand = assertionOperand(lowerer, expr, inner, target ?? null, loc);
if (operand) return operand;
if (target && target.kind !== "union" && !typeEquals(target, inner.type)) {
@@ -973,6 +977,65 @@ export function assertedElementKeepsUndefined(
);
}
/** Whether `node!` is a returned value, an arrow function's body or a call
* argument whose destination type (the return or parameter type) is `any`
* or `unknown` (as for `String(x!)` and `Number(x!)`) or holds null or
* undefined that the union `inner` holds and the assertion would remove.
* Bindings and assignments are left out: the checker narrows them to the
* asserted type for later reads. */
function assertionDestinationHoldsNullish(
lowerer: Lowerer,
node: ts.NonNullExpression,
inner: IrType & { kind: "union" },
): boolean {
const holdsNull = lowerer.armTag(inner.unionId, NULL_T) >= 0;
const holdsUndefined = lowerer.armTag(inner.unionId, UNDEFINED_T) >= 0;
if (!holdsNull && !holdsUndefined) return false;
let outer: ts.Expression = node;
while (ts.isParenthesizedExpression(outer.parent)) outer = outer.parent;
const parent = outer.parent;
const destination =
(ts.isReturnStatement(parent) && parent.expression === outer) ||
(ts.isArrowFunction(parent) && parent.body === outer) ||
((ts.isCallExpression(parent) || ts.isNewExpression(parent)) &&
parent.arguments?.nodes.includes(outer) === true);
if (!destination) return false;
const contextual = lowerer.checker.getContextualType(outer);
if (!contextual) return false;
const nullish =
ts.TypeFlags.Any |
ts.TypeFlags.Unknown |
(holdsNull ? ts.TypeFlags.Null : 0) |
(holdsUndefined && !optionalStdlibParameter(lowerer, parent, outer)
? ts.TypeFlags.Undefined
: 0);
if ((contextual.flags & ts.TypeFlags.Union) === 0) return (contextual.flags & nullish) !== 0;
return constituentTypes(contextual).some((type) => (type.flags & nullish) !== 0);
}
/** Whether `argument` of `call` binds an optional parameter of a standard
* library declaration (`decoder.decode(input?)`). The undefined such a
* parameter's type holds comes from its optionality, and the builtin
* lowerings read the argument as the declared type, so `x!` keeps the
* checked narrowing there. */
function optionalStdlibParameter(
lowerer: Lowerer,
call: ts.Node,
argument: ts.Expression,
): boolean {
if (!ts.isCallExpression(call) && !ts.isNewExpression(call)) return false;
const declaration = lowerer.checker.getResolvedSignature(call)?.declaration;
if (declaration === undefined || !lowerer.isStdlibFile(ts.getSourceFileOfNode(declaration)))
return false;
const index = call.arguments?.nodes.indexOf(argument) ?? -1;
const parameter = declaration.parameters?.nodes[index];
return (
parameter !== undefined &&
parameter.dotDotDotToken === undefined &&
(parameter.questionToken !== undefined || parameter.initializer !== undefined)
);
}
/** `x!` as the operand of an operator that converts its operands: an
* arithmetic, bitwise, relational or equality operator, `+`, a compound
* assignment, a unary `-`, `+`, `~` or `!`, a template span, or an argument
@@ -999,13 +1062,14 @@ function assertionOperand(
)
)
return null;
// Array and typed-array element reads keep their own handling of a
// missing element.
// Array element reads keep their own handling of a missing element. A
// typed-array read is optional here only where its use tells undefined
// from NaN (assertedReadObservesUndefined).
let operand: ts.Expression = expr.expression;
while (ts.isParenthesizedExpression(operand)) operand = operand.expression;
if (ts.isElementAccessExpression(operand)) {
const kind = lowerer.mapTypeOf(lowerer.typeOf(operand.expression))?.kind;
if (kind === "array" || kind === "bytes") return null;
if (kind === "array") return null;
}
const use = convertingUseOf(lowerer, expr);
if (use === null) return null;
@@ -1022,6 +1086,7 @@ function convertingUseOf(lowerer: Lowerer, expr: ts.NonNullExpression): "number"
while (ts.isParenthesizedExpression(outer.parent)) outer = outer.parent;
const parent = outer.parent;
if (ts.isTemplateSpan(parent)) return parent.expression === outer ? "value" : null;
if (ts.isTypeOfExpression(parent)) return "value";
if (ts.isPrefixUnaryExpression(parent)) {
if (parent.operator === ts.SyntaxKind.ExclamationToken) return "value";
return parent.operator === ts.SyntaxKind.MinusToken ||
@@ -1118,6 +1183,23 @@ const NUMERIC_COMPOUNDS = new Set<ts.SyntaxKind>([
ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken,
]);
/** Whether the use of `t[i]!` tells undefined from NaN: a converting use
* that takes the value itself (see convertingUseOf), or a destination that
* holds a missing element (see assertedElementKeepsUndefined) other than a
* return, whose slot has the function's number type. */
export function assertedReadObservesUndefined(
lowerer: Lowerer,
node: ts.NonNullExpression,
): boolean {
if (convertingUseOf(lowerer, node) === "value") return true;
let outer: ts.Expression = node;
while (ts.isParenthesizedExpression(outer.parent)) outer = outer.parent;
const parent = outer.parent;
if (ts.isReturnStatement(parent) || (ts.isArrowFunction(parent) && parent.body === outer))
return false;
return assertedElementKeepsUndefined(lowerer, node);
}
export function runtimeOptionalAssertionErases(
lowerer: Lowerer,
expr: ts.Expression,
@@ -18,6 +18,7 @@ import { jsFuncNameOf, type Lowerer } from "./lowerer.js";
import { isJsSourceFile } from "../program.js";
import {
BOOL,
CAUGHT,
DYN,
F64,
type IrExpr,
@@ -44,9 +45,9 @@ import {
shapeKeyOrders,
typeKey,
} from "../../ir/ir.js";
import type { ClassInfo } from "./lower-classes.js";
import { accessorCall, findMethodOn, type ClassInfo } from "./lower-classes.js";
import { isSafeToDiscard, isSafeToRepeat } from "./expressions/evaluation-safety.js";
import { symbolFieldDisplayName } from "./symbol-fields.js";
import { symbolFieldDisplayName, TO_STRING_TAG_FIELD } from "./symbol-fields.js";
import { eventTargetRootOf } from "./lower-event-target.js";
import { dateRootOf } from "./builtins/date-classes.js";
import { lowerSystemErrorMap } from "./lower-system-errors.js";
@@ -857,7 +858,10 @@ function inspectSupport(
}
// Generator objects print through their checked capsule: Node's
// `Object [Generator] {}`, or the tag of the iterator they implement.
// URLs and URLSearchParams print through their handles.
case "generator":
case "url":
case "searchParams":
return null;
default:
return `'${lowerer.fmt(t)}' values have no inspect lowering`;
@@ -898,6 +902,117 @@ export function inspectExpr(
recurse: IrExpr,
depth: IrExpr,
loc: SrcLoc,
): IrExpr {
const render = inspectExprBody(lowerer, t, value, recurse, depth, loc);
// A top-level render through emitted helpers that can reach a refusal
// (the 'not supported yet' Error of insp.dyn) unwinds the inspector to
// its state before the render on every exit: the Error propagates out of
// the helpers before they close their frames.
if (
render.kind !== "call" ||
recurse.kind !== "numLit" ||
recurse.value !== 0 ||
!inspectMayRefuse(lowerer, t)
)
return render;
return withInspectUnwind(lowerer, render, t, loc);
}
/** A top-level helper render (`call helper(v, r, d)`) through a lifted
* wrapper that takes insp.mark first and runs insp.unwind however the
* render ends: `try { return helper(v, r, d); } finally { insp.unwind(); }`.
* A seqExpr cannot hold the try. */
function withInspectUnwind(
lowerer: Lowerer,
render: IrExpr & { kind: "call" },
t: IrType,
loc: SrcLoc,
): IrExpr {
const key = `unwind:${render.callee}`;
let name = lowerer.inspectHelpers.get(key);
if (name === undefined) {
name = keyedHelperName(lowerer, "%util.inspUnwound", key);
lowerer.inspectHelpers.set(key, name);
const params = [
{ localId: "v.0", name: "v", type: t },
{ localId: "r.0", name: "r", type: F64 },
{ localId: "d.0", name: "d", type: F64 },
];
const call = (fn: "insp.mark" | "insp.unwind"): IrStmt => ({
kind: "exprStmt",
expr: { kind: "libCall", fn, args: [], type: VOID, loc },
loc,
});
lowerer.liftedFns.push({
name,
params,
returnType: STRING,
locals: params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })),
body: [
call("insp.mark"),
{
kind: "tryCatch",
tryBody: [
{
kind: "return",
value: {
kind: "call",
callee: render.callee,
args: params.map((p) => varRef(p.localId, p.type, loc)),
type: STRING,
loc,
},
loc,
},
],
catchBody: null,
catchLocalId: null,
finallyBody: [call("insp.unwind")],
loc,
},
],
loc,
});
}
return { ...render, callee: name };
}
/** Whether inspecting a `t` value can end in an inspection refusal: `t`
* or a type reachable from it is a checked-dynamic value, a generator, a
* Dirent row or a class instance, or a record or array that can keep
* undeclared properties, which can render through a checked-dynamic view. */
function inspectMayRefuse(lowerer: Lowerer, t: IrType): boolean {
const seen = new Set<string>();
const walk = (u: IrType): boolean => {
if (u.kind === "dyn" || u.kind === "generator") return true;
if (u.kind === "record") {
const shape = lowerer.shapes.get(u.shapeId);
if (shape && isDirentShape(shape)) return true;
// The kept source properties render through the checked view (the
// helper's source gate).
if (recordMayKeepSource(lowerer, u)) return true;
}
// So do an array's properties besides its elements.
if (u.kind === "array" && lowerer.dynConvertible(u)) return true;
// A class instance can render through its checked-dynamic view, which
// a helper built earlier may only learn at the end of lowering
// (classInspectsAddedProperties).
if (u.kind === "object") return true;
const k = typeKey(u);
if (seen.has(k)) return false;
seen.add(k);
return typeConstituents(lowerer, u).some(walk);
};
return walk(t);
}
function inspectExprBody(
lowerer: Lowerer,
t: IrType,
value: IrExpr,
recurse: IrExpr,
depth: IrExpr,
loc: SrcLoc,
): IrExpr {
// A Dirent row renders through its dyn view: Node's `Dirent { ... }`
// with the Symbol(type) key.
@@ -957,6 +1072,18 @@ export function inspectExpr(
};
case "dyn":
return { kind: "libCall", fn: "insp.dyn", args: [value, recurse, depth], type: STRING, loc };
// URLs and URLSearchParams render through their checked-dynamic
// handles: Node's `URL { href: ..., hash: ... }` and
// `URLSearchParams { 'a' => '1' }`.
case "url":
case "searchParams":
return {
kind: "libCall",
fn: "insp.dyn",
args: [lowerer.coerceToExpected(value, DYN), recurse, depth],
type: STRING,
loc,
};
case "generator":
return {
kind: "libCall",
@@ -1874,8 +2001,86 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string {
// observable way — Node's inspect omits them entirely (verified),
// so a class whose only fields are private prints as `C {}`.
const visible = info.def.fields.filter((f) => isClassOwnEnumerableFieldName(f.name));
// A Symbol.toStringTag getter (an own tag field is listed instead)
// brackets the tag after the name, `Name [tag] {`, when it is a
// nonempty string other than the name. A getter that throws shows
// no tag, as in Node.
const tagGetter = info.fields.has(TO_STRING_TAG_FIELD)
? null
: findMethodOn(lowerer, info, "get:sym:toStringTag");
let named: (plain: string, make: (head: IrExpr) => IrExpr) => IrExpr = (plain) =>
strLit(plain, loc);
if (tagGetter?.sig.ret.kind === "string") {
const owner = tagGetter.declarer.def.name;
locals.push(
{ id: "tag.0", name: "tag", type: STRING, mutable: true },
{ id: "tagError.0", name: "tagError", type: CAUGHT, mutable: false },
);
body.push(
{ kind: "varDecl", localId: "tag.0", init: strLit("", loc), loc },
{
kind: "tryCatch",
tryBody: [
{
kind: "assign",
localId: "tag.0",
value: accessorCall(
lowerer,
owner,
"get:sym:toStringTag",
lowerer.upcastTo(v(), owner),
[],
STRING,
loc,
),
loc,
},
],
catchBody: [],
catchLocalId: "tagError.0",
finallyBody: null,
loc,
},
);
const tag = (): IrExpr => varRef("tag.0", STRING, loc);
const shown: IrExpr = {
kind: "logical",
op: "&&",
left: {
kind: "strEq",
negated: true,
left: tag(),
right: strLit("", loc),
type: BOOL,
loc,
},
right: {
kind: "strEq",
negated: true,
left: tag(),
right: strLit(display, loc),
type: BOOL,
loc,
},
type: BOOL,
loc,
};
const head = concatAll(
[strLit(`${display === "" ? "Object" : display} [`, loc), tag(), strLit("]", loc)],
loc,
);
named = (plain, make) => ({
kind: "ternary",
cond: shown,
then: make(head),
else_: strLit(plain, loc),
type: STRING,
loc,
});
}
const after = (text: string) => (head: IrExpr) => concatAll([head, strLit(text, loc)], loc);
if (visible.length === 0) {
body = [ret(strLit(display === "" ? "{}" : `${display} {}`, loc))];
body.push(ret(named(display === "" ? "{}" : `${display} {}`, after(" {}"))));
break;
}
const get = (field: string, type: IrType): IrExpr => ({
@@ -1886,7 +2091,22 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string {
type,
loc,
});
body = [depthGate(display === "" ? "[Object]" : `[${display}]`), ...begin()];
body.push(
{
kind: "if",
cond: { kind: "bin", op: ">", left: r(), right: d(), type: BOOL, loc },
then: [
ret(
named(display === "" ? "[Object]" : `[${display}]`, (head) =>
concatAll([strLit("[", loc), head, strLit("]", loc)], loc),
),
),
],
else_: null,
loc,
},
...begin(),
);
// def.fields carries layout order: the base chain first, then own —
// exactly the own-property insertion order of a constructor that
// assigns in declaration order (SEMANTICS.md 36's stance). SYMBOL-
@@ -1915,7 +2135,7 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string {
ret(
end(
strLit("", loc),
strLit(display === "" ? "{" : `${display} {`, loc),
named(display === "" ? "{" : `${display} {`, after(" {")),
strLit("}", loc),
false,
boolLit(false, loc),
@@ -2364,40 +2584,55 @@ function parseInspectOptions(
/** A render under non-default breakLength/compact/sorted: the value is evaluated
* first, the layout applies to this render only, and the defaults return
* afterwards. */
* however the render ends, an inspection refusal included. The render runs
* in a lifted function, `(v) => { insp.layout(...); try { return render(v); }
* finally { insp.layout(80, 3, false); } }`, since a seqExpr cannot hold the
* try; `tag` names the render for the function's key. */
function withInspectLayout(
lowerer: Lowerer,
options: InspectOptions,
value: IrExpr,
tag: string,
render: (value: IrExpr) => IrExpr,
loc: SrcLoc,
): IrExpr {
if (options.breakLength === 80 && options.compact === 3 && !options.sorted) return render(value);
const input = lowerer.declareHiddenLocal("%inspectInput", value.type);
const text = lowerer.declareHiddenLocal("%inspectText", STRING);
const layout = (breakLength: number, compact: number, sorted: boolean): IrStmt => ({
kind: "exprStmt",
expr: {
kind: "libCall",
fn: "insp.layout",
args: [numLit(breakLength, loc), numLit(compact, loc), boolLit(sorted, loc)],
type: VOID,
const key = `layout:${tag}:${typeKey(value.type)}:${options.breakLength}:${options.compact}:${options.sorted}:${options.depth}`;
let name = lowerer.inspectHelpers.get(key);
if (name === undefined) {
name = keyedHelperName(lowerer, "%util.inspLayout", key);
lowerer.inspectHelpers.set(key, name);
const layout = (breakLength: number, compact: number, sorted: boolean): IrStmt => ({
kind: "exprStmt",
expr: {
kind: "libCall",
fn: "insp.layout",
args: [numLit(breakLength, loc), numLit(compact, loc), boolLit(sorted, loc)],
type: VOID,
loc,
},
loc,
},
loc,
});
return {
kind: "seqExpr",
stmts: [
{ kind: "varDecl", localId: input.id, init: value, loc },
layout(options.breakLength, options.compact, options.sorted),
{ kind: "varDecl", localId: text.id, init: render(varRef(input.id, value.type, loc)), loc },
layout(80, 3, false),
],
result: varRef(text.id, STRING, loc),
type: STRING,
loc,
};
});
lowerer.liftedFns.push({
name,
params: [{ localId: "v.0", name: "v", type: value.type }],
returnType: STRING,
locals: [{ id: "v.0", name: "v", type: value.type, mutable: false }],
body: [
layout(options.breakLength, options.compact, options.sorted),
{
kind: "tryCatch",
tryBody: [{ kind: "return", value: render(varRef("v.0", value.type, loc)), loc }],
catchBody: null,
catchLocalId: null,
finallyBody: [layout(80, 3, false)],
loc,
},
],
loc,
});
}
return { kind: "call", callee: name, args: [value], type: STRING, loc };
}
/** `util.inspect(fn)` / `util.inspect(Cls)` over a DIRECT identifier
@@ -2530,6 +2765,7 @@ export function lowerInspectValue(
lowerer,
options,
value,
"typedArray",
(input) => ({
kind: "libCall",
fn:
@@ -2558,6 +2794,7 @@ export function lowerInspectValue(
lowerer,
options,
value,
"dirDyn",
(input) => ({
kind: "libCall",
fn: "console.dirDyn",
@@ -2572,6 +2809,7 @@ export function lowerInspectValue(
lowerer,
options,
value,
"inspect",
(input) => inspectExpr(lowerer, value.type, input, numLit(0, loc), numLit(depth, loc), loc),
loc,
);
@@ -2648,10 +2886,21 @@ function formatSArg(lowerer: Lowerer, node: ts.Expression, depth: number, loc: S
}
/** %O (depth 2) / %o (showHidden, depth 4) — inspect with the spec's
* option deltas; %o fences over arrays (showHidden adds their hidden
* [length] entry, which has no lowering). */
* option deltas. A checked-dynamic argument renders through the runtime's
* showHidden walk; static %o fences over arrays and nested dynamic values
* (showHidden adds an array's hidden [length] entry, which the static
* rendering has no lowering for). */
function formatOArg(lowerer: Lowerer, node: ts.Expression, depth: number, loc: SrcLoc): IrExpr {
const value = lowerer.lowerExpr(node);
if (depth === 4 && value.type.kind === "dyn") {
return {
kind: "libCall",
fn: "insp.dynHidden",
args: [value, numLit(4, loc)],
type: STRING,
loc,
};
}
if (value.type.kind === "bytes") {
lowerer.noLowering(
`util.format %${depth === 4 ? "o" : "O"} of typed-array values`,
@@ -2667,11 +2916,11 @@ function formatOArg(lowerer: Lowerer, node: ts.Expression, depth: number, loc: S
node,
reason,
);
if (depth === 4 && typeTreeHasArray(lowerer, value.type, new Set())) {
if (depth === 4 && typeTreeHasHidden(lowerer, value.type, new Set())) {
lowerer.noLowering(
"util.format %o over arrays",
node,
"%o renders with showHidden (arrays gain a hidden [length] entry) — use %O for the default rendering",
"%o renders with showHidden (arrays gain a hidden [length] entry) — use %O for the default rendering, or pass the value as unknown",
);
}
return inspectExpr(lowerer, value.type, value, numLit(0, loc), numLit(depth, loc), loc);
@@ -3072,35 +3321,36 @@ export function lowerFormatCall(
}
/** Arrays anywhere in the tree — the %o (showHidden) gate. */
function typeTreeHasArray(lowerer: Lowerer, t: IrType, visiting: Set<string>): boolean {
function typeTreeHasHidden(lowerer: Lowerer, t: IrType, visiting: Set<string>): boolean {
switch (t.kind) {
case "array":
case "dyn":
return true;
case "record": {
if (visiting.has(t.shapeId)) return false;
visiting.add(t.shapeId);
const shape = lowerer.shapes.get(t.shapeId);
if (shape?.tuple) return true;
return (shape?.fields ?? []).some((f) => typeTreeHasArray(lowerer, f.type, visiting));
return (shape?.fields ?? []).some((f) => typeTreeHasHidden(lowerer, f.type, visiting));
}
case "map":
return (
typeTreeHasArray(lowerer, t.key, visiting) || typeTreeHasArray(lowerer, t.value, visiting)
typeTreeHasHidden(lowerer, t.key, visiting) || typeTreeHasHidden(lowerer, t.value, visiting)
);
case "set":
return typeTreeHasArray(lowerer, t.elem, visiting);
return typeTreeHasHidden(lowerer, t.elem, visiting);
case "union": {
if (visiting.has(t.unionId)) return false;
visiting.add(t.unionId);
return (lowerer.unions.get(t.unionId)?.arms ?? []).some((a) =>
typeTreeHasArray(lowerer, a, visiting),
typeTreeHasHidden(lowerer, a, visiting),
);
}
case "object": {
if (visiting.has(t.className)) return false;
visiting.add(t.className);
return (lowerer.classes.get(t.className)?.def.fields ?? []).some((f) =>
typeTreeHasArray(lowerer, f.type, visiting),
typeTreeHasHidden(lowerer, f.type, visiting),
);
}
case "bytes":
@@ -2386,7 +2386,6 @@ export function collectGlobals(
// References from separately-declared functions cascade to
// their own per-site runtime fences.
const factoryType =
!isLet &&
isJsSourceFile(sf) &&
!decl.type &&
!hasJsTypeAnnotation(decl) &&
@@ -157,7 +157,11 @@ import { InternalCompilerError } from "../../errors.js";
import type { FsPathPlan } from "./builtins/fs-paths.js";
import { defaultAfterUndefined, lowerStaticallyUndefinedArgument } from "./optional-arguments.js";
import { type ProtocolDispatch } from "./class-protocol-dispatch.js";
import { ClassDynamicDispatch, trackClassFieldCreation } from "./class-dynamic-dispatch.js";
import {
ClassDynamicDispatch,
boxClassInstancesExactly,
trackClassFieldCreation,
} from "./class-dynamic-dispatch.js";
import { guardClassAttributeWrites } from "./class-attributes.js";
import { guardFrozenWrites, markSloppyRecordWrites } from "./frozen-records.js";
import { applyHandleProperties } from "./handle-properties.js";
@@ -437,6 +441,7 @@ import {
collectSignatureInner,
lambdaSignature,
} from "./call-signatures.js";
import { dynamicOverloadChoice, guessedOverloadIdentifier } from "./overload-choice.js";
import {
implicitAnyParamSymbolsOf,
implicitLocalFnNodeOf,
@@ -2848,6 +2853,8 @@ export class Lowerer {
/** Parameters read as `any` in the error-path copy of an error-first
* callback. */
readonly anySymbols = new Set<ts.Symbol>();
/** overload-choice's answers per call, binding and function. */
readonly dynamicOverloadAnswers = new Map<ts.Node, boolean>();
/** Error parameters of an error-first callback with a known value in
* the copy being lowered: true while it holds an Error, false while it
* holds null (staticCondition folds conditions on them). */
@@ -6488,6 +6495,22 @@ export class Lowerer {
});
return keys;
};
// `t[i]!` on a typed array, stored into an array, keeps an invalid
// index's undefined (assertedReadObservesUndefined): `!` performs no
// runtime check. A read whose consumers treat undefined like NaN stores
// NaN instead.
const assertedReadMayBeUndefined = (node: ts.Expression): boolean => {
let e = node;
while (ts.isParenthesizedExpression(e)) e = e.expression;
if (!ts.isNonNullExpression(e)) return false;
let read = e.expression;
while (ts.isParenthesizedExpression(read)) read = read.expression;
return (
ts.isElementAccessExpression(read) &&
this.mapTypeOf(this.typeOf(read.expression))?.kind === "bytes" &&
!nanCoded.isNumericRead(read)
);
};
const elementStates = new ArrayElementStates({
elementAdmitsUndefined: (key) => {
siteDependencies?.read(key);
@@ -6497,7 +6520,7 @@ export class Lowerer {
allocationOwner: (node) => ownership.allocationOwner(node),
arrayKeys: arrayKeysOf,
mayBeUndefined: (node) => {
if (explicitlyNonNull(node)) return false;
if (explicitlyNonNull(node)) return assertedReadMayBeUndefined(node);
if (mayBeOptional(node)) return true;
// `undefined as unknown as T` keeps the undefined value.
for (
@@ -7774,6 +7797,7 @@ export class Lowerer {
finalizeClassMethodValues(this, functions);
finalizePrivateBrands(this, functions);
trackClassFieldCreation(this, functions);
boxClassInstancesExactly(this, functions);
guardClassAttributeWrites(this, functions);
guardFrozenWrites(this, functions);
applyHandleProperties(this, functions);
@@ -9057,6 +9081,20 @@ export class Lowerer {
typeOf(node: ts.Node): ts.Type {
if (this.anySymbols.size > 0 && this.readsAnySymbol(node)) return this.checker.getAnyType();
// A checked-dynamic argument that another overload could take at run
// time leaves the resolved overload's return a guess, and JS bindings
// initialized with that result hold whatever it was.
if (
(ts.isCallExpression(node) || ts.isNewExpression(node)) &&
dynamicOverloadChoice(this, node)
)
return this.checker.getAnyType();
if (
ts.isIdentifier(node) &&
isJsSourceFile(ts.getSourceFileOfNode(node)) &&
guessedOverloadIdentifier(this, node)
)
return this.checker.getAnyType();
if (ts.isPropertyAccessExpression(node) && isJsSourceFile(ts.getSourceFileOfNode(node))) {
const receiver =
node.expression.kind === ts.SyntaxKind.ThisKeyword && this.fnStack.length > 0
@@ -6,10 +6,14 @@ import { bindingNeverReassigned } from "./binding-analysis.js";
import { implicitDefaultInstance } from "./generic-functions.js";
import { type FnSig } from "./call-signatures.js";
/** An ordinary constructor's explicit object return replaces its receiver.
* Without observing this or new.target, it can use the function's call ABI. */
function isObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): boolean {
if (!ts.isIdentifier(expr.expression) || lowerer.peekLocal(expr.expression)) return false;
/** The JavaScript function declaration (or never-reassigned function
* expression binding) `new` calls, when it is an ordinary constructor: not
* a generator or async function. */
function jsConstructorFunctionOf(
lowerer: Lowerer,
expr: ts.NewExpression,
): ts.FunctionDeclaration | ts.FunctionExpression | null {
if (!ts.isIdentifier(expr.expression) || lowerer.peekLocal(expr.expression)) return null;
// This probe also sees ordinary classes during global collection. Leave
// their deferred diagnostics for the class declaration's lowering.
const wasCollecting = lowerer.collecting;
@@ -20,7 +24,7 @@ function isObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): boolean {
} finally {
lowerer.collecting = wasCollecting;
}
if (!symbol) return false;
if (!symbol) return null;
const owner = symbol.declarations.find(
(node) =>
(ts.isFunctionDeclaration(node) && node.body !== undefined) ||
@@ -45,7 +49,15 @@ function isObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): boolean {
declaration.asteriskToken ||
declaration.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)
)
return false;
return null;
return declaration;
}
/** An ordinary constructor's explicit object return replaces its receiver.
* Without observing this or new.target, it can use the function's call ABI. */
function isObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): boolean {
const declaration = jsConstructorFunctionOf(lowerer, expr);
if (!declaration?.body) return false;
const objectReturn = (expression: ts.Expression): boolean =>
ts.isObjectLiteralExpression(expression) ||
ts.isArrayLiteralExpression(expression) ||
@@ -81,8 +93,13 @@ function isObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): boolean {
/** Collection must register the shared result before reaching factory
* bodies: those bodies can reference later globals in the same module. */
/** The storage of a JavaScript module binding initialized with `new` of an
* ordinary function: a factory's result and a constructor function's
* receiver (`const head = new Segment()`) are both checked-dynamic
* values, which functions declared elsewhere in the module read through
* the global. */
export function objectFactoryGlobalType(lowerer: Lowerer, expr: ts.NewExpression): IrType | null {
return isObjectFactoryNew(lowerer, expr) ? DYN : null;
return jsConstructorFunctionOf(lowerer, expr) !== null ? DYN : null;
}
export function objectFactorySignature(lowerer: Lowerer, expr: ts.NewExpression): FnSig | null {
@@ -0,0 +1,197 @@
import * as ts from "../ts7/adapter.js";
import type { Lowerer } from "./lowerer.js";
import { typeEquals, type IrType } from "../../ir/ir.js";
import { isJsSourceFile } from "../program.js";
import { isThisParameter } from "./call-signatures.js";
import { hasExplicitJsDocReturn } from "./function-returns.js";
import { hasJsTypeAnnotation } from "./lower-stmts.js";
/** Whether a checked-dynamic argument leaves a JS call's overload
* undecided.
*
* The checker resolves an overloaded call whose argument is `any` (an
* untyped parameter, a value read from one) to the FIRST overload that
* argument satisfies, but the value can match any overload at run time:
* `generateKeyPairSync("ec", options)` answers KeyObjects or encoded
* strings by the options' contents, `digest(encoding)` a Buffer when the
* encoding is undefined. When another overload the call could take at
* run time returns a different type, the resolved overload's return is a
* guess, and the result is checked-dynamic instead.
*
* An overload is reachable when the arguments fit its arity (trailing
* checked-dynamic or `undefined` arguments may be absent) and every
* statically typed argument is assignable to its parameter. Generic
* overloads whose return cannot be mapped without instantiation do not
* count, so the resolved instantiation stands for them. */
export function dynamicOverloadChoice(
lowerer: Lowerer,
expr: ts.CallExpression | ts.NewExpression,
): boolean {
return cachedAnswer(lowerer, expr, () => computeDynamicOverloadChoice(lowerer, expr));
}
/** Whether a JavaScript value's checker type rests on an undecided
* overload (dynamicOverloadChoice): the call itself, an awaited one, a
* call to an unannotated JS function returning such a value, or a read of
* an unannotated JS binding initialized with one (destructured or not).
* Such a value is checked-dynamic wherever the checker's inference would
* otherwise carry the guess into a binding or a return. */
export function guessedOverloadValue(lowerer: Lowerer, expr: ts.Expression): boolean {
while (ts.isParenthesizedExpression(expr) || ts.isAwaitExpression(expr)) expr = expr.expression;
if (ts.isCallExpression(expr) || ts.isNewExpression(expr)) {
if (!isJsSourceFile(ts.getSourceFileOfNode(expr))) return false;
if (dynamicOverloadChoice(lowerer, expr)) return true;
const decl = lowerer.checker.getResolvedSignature(expr)?.declaration;
return (
decl !== undefined &&
(ts.isFunctionDeclaration(decl) ||
ts.isFunctionExpression(decl) ||
ts.isArrowFunction(decl)) &&
isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
returnsGuessedOverloadValue(lowerer, decl)
);
}
return ts.isIdentifier(expr) && guessedOverloadIdentifier(lowerer, expr);
}
/** Whether a JS identifier, as a binding's declared name or as a read,
* names a guessedOverloadBinding. */
export function guessedOverloadIdentifier(lowerer: Lowerer, node: ts.Identifier): boolean {
return cachedAnswer(lowerer, node, () => {
const parent = node.parent;
if (
parent !== undefined &&
(ts.isVariableDeclaration(parent) || ts.isBindingElement(parent)) &&
parent.name === node
)
return guessedOverloadBinding(lowerer, parent);
const decl = lowerer.checker.getSymbolAtLocation(node)?.valueDeclaration;
return decl !== undefined && guessedOverloadBinding(lowerer, decl);
});
}
/** Whether `decl` is an unannotated JS variable or destructured binding
* whose declaration's initializer is a guessedOverloadValue. */
function guessedOverloadBinding(lowerer: Lowerer, decl: ts.Node): boolean {
let root: ts.Node | undefined = decl;
while (root && ts.isBindingElement(root) && root.parent && ts.isBindingPattern(root.parent)) {
root = root.parent.parent;
}
if (!root) return false;
if (!ts.isVariableDeclaration(root) || !root.initializer || root.type) return false;
if (!isJsSourceFile(ts.getSourceFileOfNode(root)) || hasJsTypeAnnotation(root)) return false;
const init = root.initializer;
return cachedAnswer(lowerer, root, () => guessedOverloadValue(lowerer, init));
}
/** Whether a return of the unannotated JS function `decl` itself is a
* guessedOverloadValue. */
export function returnsGuessedOverloadValue(
lowerer: Lowerer,
decl: ts.SignatureDeclaration,
): boolean {
if (decl.type || decl.asteriskToken || hasExplicitJsDocReturn(decl)) return false;
return cachedAnswer(lowerer, decl, () => {
if (ts.isArrowFunction(decl) && decl.body && !ts.isBlock(decl.body)) {
return guessedOverloadValue(lowerer, decl.body);
}
let found = false;
const scan = (node: ts.Node): void => {
if (found || (node !== decl && ts.isFunctionLike(node))) return;
if (ts.isReturnStatement(node) && node.expression) {
found = guessedOverloadValue(lowerer, node.expression);
}
ts.forEachChild(node, scan);
};
scan(decl);
return found;
});
}
/** One answer per node; a node asked again while its answer is being
* computed (recursive functions, self-referencing bindings) answers
* false. */
function cachedAnswer(lowerer: Lowerer, node: ts.Node, compute: () => boolean): boolean {
const cached = lowerer.dynamicOverloadAnswers.get(node);
if (cached !== undefined) return cached;
lowerer.dynamicOverloadAnswers.set(node, false);
const answer = compute();
lowerer.dynamicOverloadAnswers.set(node, answer);
return answer;
}
function computeDynamicOverloadChoice(
lowerer: Lowerer,
expr: ts.CallExpression | ts.NewExpression,
): boolean {
// TypeScript sources keep the resolved overload: their reads validate
// the value against it where it is used.
if (!isJsSourceFile(ts.getSourceFileOfNode(expr))) return false;
const args = expr.arguments?.nodes ?? [];
if (args.length === 0 || args.some(ts.isSpreadElement)) return false;
const dynamic = args.map((arg) => checkedDynamicArgument(lowerer, arg));
if (!dynamic.includes(true)) return false;
const checker = lowerer.checker;
const calleeType = checker.getTypeAtLocation(expr.expression);
const signatures = ts.isNewExpression(expr)
? checker.getConstructSignatures(calleeType)
: checker.getCallSignatures(calleeType);
if (signatures.length < 2) return false;
const resolved = checker.getResolvedSignature(expr);
if (!resolved) return false;
const resolvedReturn = lowerer.mapTypeOf(checker.getReturnTypeOfSignature(resolved));
if (resolvedReturn === null) return false;
// Arguments that may be absent at run time: the trailing run of
// checked-dynamic and `undefined` arguments.
let present = args.length;
while (present > 0 && (dynamic[present - 1] || isUndefinedArgument(args[present - 1]!))) {
present--;
}
for (const signature of signatures) {
const decl = signature.declaration;
if (!decl || decl === resolved.declaration || !ts.isFunctionLike(decl)) continue;
const generic = signature.getTypeParameters().length > 0;
const params = decl.parameters.nodes.filter((param) => !isThisParameter(param));
const required = params.filter(
(param) => !param.questionToken && !param.initializer && !param.dotDotDotToken,
).length;
const maximum = params.some((param) => param.dotDotDotToken) ? Infinity : params.length;
if (required > args.length || present > maximum) continue;
const symbols = signature.getParameters();
let fits = true;
for (let i = 0; i < args.length && i < params.length && fits; i++) {
if (dynamic[i] || generic || params[i]!.dotDotDotToken) continue;
const symbol = symbols[i];
if (!symbol) continue;
fits = checker.isTypeAssignableTo(
checker.getTypeAtLocation(args[i]!),
checker.getTypeOfSymbol(symbol),
);
}
if (!fits) continue;
const candidate = lowerer.mapTypeOf(checker.getReturnTypeOfSignature(signature));
if (candidate !== null && !holdsResult(resolvedReturn, candidate)) return true;
}
return false;
}
/** Whether a value of the resolved overload's mapped return also holds
* every value of `candidate`: the same type, or a checked-dynamic one (a
* promise of one fulfills with anything). */
function holdsResult(resolved: IrType, candidate: IrType): boolean {
if (resolved.kind === "dyn" || typeEquals(resolved, candidate)) return true;
return (
resolved.kind === "promise" &&
candidate.kind === "promise" &&
holdsResult(resolved.inner, candidate.inner)
);
}
function checkedDynamicArgument(lowerer: Lowerer, arg: ts.Expression): boolean {
if (lowerer.checker.getTypeAtLocation(arg).flags & ts.TypeFlags.Any) return true;
return (lowerer.typeOf(arg).flags & ts.TypeFlags.Any) !== 0;
}
function isUndefinedArgument(arg: ts.Expression): boolean {
return (ts.isIdentifier(arg) && arg.text === "undefined") || ts.isVoidExpression(arg);
}
@@ -1635,6 +1635,12 @@ export const BUILTIN_MODULE_FNS: Record<
unzip: { fn: "zlib.unzipCb", params: [], result: VOID },
unzipSync: { fn: "zlib.unzipSync", params: [BYTES_U8], result: BYTES_U8 },
crc32: { fn: "zlib.crc32", params: [], result: F64 },
// Brotli (special-cased in lowerZlibModuleCall): every form takes the
// options entries with Node's BROTLI_ENCODE or BROTLI_DECODE mode.
brotliCompress: { fn: "zlib.cbOpts", params: [], result: VOID },
brotliCompressSync: { fn: "zlib.syncOpts", params: [], result: BYTES_U8 },
brotliDecompress: { fn: "zlib.cbOpts", params: [], result: VOID },
brotliDecompressSync: { fn: "zlib.syncOpts", params: [], result: BYTES_U8 },
// The stream factories (special-cased in lowerZlibModuleCall): each
// answers a runtime Transform stream over the zlib engine.
createDeflate: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM },
@@ -1644,6 +1650,8 @@ export const BUILTIN_MODULE_FNS: Record<
createDeflateRaw: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM },
createInflateRaw: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM },
createUnzip: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM },
createBrotliCompress: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM },
createBrotliDecompress: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM },
},
url: {
fileURLToPath: {
@@ -1681,6 +1689,7 @@ export const BUILTIN_MODULE_FNS: Record<
resolve: { fn: "url.legacyResolve", params: [DYN, DYN], result: STRING },
domainToASCII: { fn: "url.domainToASCII", params: [DYN], result: STRING },
domainToUnicode: { fn: "url.domainToUnicode", params: [DYN], result: STRING },
urlToHttpOptions: { fn: "url.toHttpOptions", params: [DYN], result: DYN },
},
child_process: {
// spawnSync's and spawn's call completions are entirely special-cased
@@ -3057,17 +3066,6 @@ export function builtinModulesArrayLit(loc: { file: string; start: number; end:
};
}
/** Member-specific hints for RECOGNIZED builtin modules whose member has
* no lowering. The default-options one-shot zlib/deflate-raw/gzip codecs
* lower for strings and Buffers; the remaining surface points at them. */
const ZLIB_HINT =
"the deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms with Node's options, crc32, " +
"and the Deflate/Inflate/Gzip/Gunzip/DeflateRaw/InflateRaw/Unzip streams are the lowered zlib surface";
/** Brotli has no codec in the static runtime (zlib is the only vendored
* compression library). */
const BROTLI_HINT =
"Brotli needs a Brotli codec, which the static runtime does not vendor — " + ZLIB_HINT;
const ASSERT_MODULE_HINTS: Record<string, string | undefined> = {
ifError:
"test explicitly instead: assert.strictEqual(err, null) / " +
@@ -3129,16 +3127,6 @@ export const BUILTIN_MODULE_FENCE_HINTS: Record<
"a compiled binary has no FIPS provider to enable, and Node itself throws on setFips(true) " +
"in a non-FIPS build — getFips() answers 0 here",
},
zlib: {
brotliCompressSync: BROTLI_HINT,
brotliDecompressSync: BROTLI_HINT,
brotliCompress: BROTLI_HINT,
brotliDecompress: BROTLI_HINT,
createBrotliCompress: BROTLI_HINT,
createBrotliDecompress: BROTLI_HINT,
BrotliCompress: BROTLI_HINT,
BrotliDecompress: BROTLI_HINT,
},
http2: {
connect:
"HTTP/2 client sessions have no lowering — the lowered http2 surface is the SERVER side: " +
@@ -77,6 +77,8 @@ export function fenceSymbolFieldCopy(
}
export function classSymbolKeyOf(lowerer: Lowerer, key: ts.Expression): ClassSymbolKey | null {
const wellKnown = wellKnownSymbolKeyOf(lowerer, key);
if (wellKnown !== undefined) return wellKnown;
if (
!ts.isIdentifier(key) &&
(!ts.isPropertyAccessExpression(key) || key.questionDotToken || !ts.isIdentifier(key.name))
@@ -90,6 +92,53 @@ export function classSymbolKeyOf(lowerer: Lowerer, key: ts.Expression): ClassSym
return sym ? classSymbolKeyOfSymbol(lowerer, sym) : null;
}
/** Well-known symbols a class field may be keyed by: those no protocol
* consults on an instance, or whose protocol the lowering reads from the
* field (Symbol.toStringTag, by the class string conversions). A field
* keyed by any other well-known symbol (Symbol.iterator, toPrimitive,
* hasInstance, ...) would be skipped by its protocol, so it keeps the
* computed-key refusal. */
const FIELD_WELL_KNOWN_SYMBOLS: ReadonlySet<string> = new Set(["toStringTag"]);
/** The key of `Symbol.<name>` on the standard global: undefined when the
* expression is not one, null when it is a well-known symbol a field may
* not be keyed by. The identity is the lib's declaration of the property,
* one symbol for every program reference. */
function wellKnownSymbolKeyOf(
lowerer: Lowerer,
key: ts.Expression,
): ClassSymbolKey | null | undefined {
while (ts.isParenthesizedExpression(key)) key = key.expression;
if (
!ts.isPropertyAccessExpression(key) ||
key.questionDotToken ||
!ts.isIdentifier(key.expression) ||
!ts.isIdentifier(key.name) ||
!lowerer.isStdlibGlobal(key.expression, "Symbol")
)
return undefined;
const name = key.name.text;
const sym = lowerer.checker.getSymbolAtLocation(key.name);
if (!sym || !(lowerer.typeOf(key).flags & ts.TypeFlags.UniqueESSymbol)) return undefined;
if (!FIELD_WELL_KNOWN_SYMBOLS.has(name)) return null;
return { sym, identity: sym, fieldName: wellKnownSymbolFieldName(name) };
}
/** The layout name of a field keyed by the well-known `Symbol.<name>`: a
* reserved symbol-field name, printed as Node spells the key. */
export function wellKnownSymbolFieldName(name: string): string {
return `${FIELD_PREFIX}Symbol(Symbol.${name})`;
}
/** The well-known symbol a field name is keyed by (`toStringTag`), or null. */
export function wellKnownSymbolOfField(field: string): string | null {
const match = /^%symbol:Symbol\(Symbol\.(\w+)\)$/.exec(field);
return match === null ? null : match[1]!;
}
/** The layout name of a `Symbol.toStringTag` field. */
export const TO_STRING_TAG_FIELD = wellKnownSymbolFieldName("toStringTag");
/** A literal Symbol()/Symbol.for() initialized once at module scope. The
* whole-file proof also covers var declarations emitted by bundlers. Keys
* cannot be used before initialization: a computed class field evaluates
+5 -1
View File
@@ -3927,10 +3927,14 @@ function exactCycleRefusal7(
if (ts.isExportAssignment(stmt) && !stmt.isExportEquals) {
return `the default export expression at ${lineOf(stmt)} has no initialization state inside the cycle`;
}
// `export * from` binds each name to the declaring member's own
// binding, like a named re-export, so only `export * as ns` (a
// namespace object of a member) has state of its own.
if (
ts.isExportDeclaration(stmt) &&
!stmt.isTypeOnly &&
(stmt.exportClause === undefined || ts.isNamespaceExport(stmt.exportClause))
stmt.exportClause !== undefined &&
ts.isNamespaceExport(stmt.exportClause)
) {
const dep = targetOf(stmt);
if (dep !== null && members.has(dep)) {
@@ -1431,16 +1431,18 @@ let mapTypeDepth = 0;
let contextResolutions = 0;
/** node:url's legacy record interfaces: url.parse()'s Url instance (and
* its two query-typed refinements), url.format()'s UrlObject input, and the
* URL form's URLFormatOptions. The values are checked-dynamic objects —
* Node's Url instance carries its class name and null fields, and format()
* reads any object's fields with JS truthiness. */
* its two query-typed refinements), url.format()'s UrlObject input, the
* URL form's URLFormatOptions, and urlToHttpOptions's result. The values
* are checked-dynamic objects — Node's Url instance carries its class name
* and null fields, format() reads any object's fields with JS truthiness,
* and urlToHttpOptions answers a null-prototype object. */
const LEGACY_URL_TYPES = new Set([
"Url",
"UrlObject",
"UrlWithParsedQuery",
"UrlWithStringQuery",
"URLFormatOptions",
"UrlHttpOptions",
]);
export function isLegacyUrlType(
+1
View File
@@ -596,6 +596,7 @@ async function compileExecutableNative(
...(features.warningReplace ? { warningReplace: true } : {}),
...(features.stdioExtended ? { stdioExtended: true } : {}),
zlib: features.zlib,
...(features.brotli ? { brotli: true } : {}),
assert: features.assert,
inspect: features.inspect,
dynInvoke: features.dynInvoke,
@@ -184,6 +184,13 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"num.toStringRadix",
"num.toStringRadixF64",
"insp.jsonDyn",
// A checked-dynamic value the inspector does not render (a promise, a
// URL, a RegExp) throws the 'not supported yet' Error.
"insp.dyn",
"insp.dynS",
// %o's showHidden walk refuses the kinds whose hidden entries it cannot
// render.
"insp.dynHidden",
// diagnostics_channel: publish runs subscribers synchronously (a throw
// propagates — the documented divergence from triggerUncaughtException);
// the subscribe/unsubscribe forms throw Node's ERR_INVALID_ARG_TYPE for
@@ -753,6 +760,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"url.legacyResolve",
"url.domainToASCII",
"url.domainToUnicode",
// ERR_INVALID_ARG_TYPE and the credentials' URIError.
"url.toHttpOptions",
// The win32-target flavor of pathToFileURL (same runtime entry point —
// the bridge dispatches by the binary's platform): Node's win32 arm
// raises ERR_INVALID_ARG_VALUE TypeErrors for malformed UNC inputs, so
@@ -1028,6 +1028,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"url.legacyResolve": { argTypes: [DYN, DYN, BOOL], result: STRING },
"url.domainToASCII": { argTypes: [DYN, BOOL], result: STRING },
"url.domainToUnicode": { argTypes: [DYN, BOOL], result: STRING },
/** url.urlToHttpOptions over a checked-dynamic argument: a URL answers
* Node's null-prototype options object; a primitive or null throws
* ERR_INVALID_ARG_TYPE. */
"url.toHttpOptions": { argTypes: [DYN], result: DYN },
/** node:punycode (scr_url.c): punycode.js 2.1.0's string conversions;
* decode/toUnicode (and, past 32-bit deltas, encode/toASCII) throw the
* module's RangeErrors, ucs2Encode String.fromCodePoint's. warn marks the
@@ -3189,6 +3193,8 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
* the +1 %Transform stream. flush takes the kind value, params the level
* and strategy values; the Cb forms move their () => void callback. The
* receivers are borrowed. */
/** Connects scr_brotli.c to the zlib engine (%main, before any module). */
"zlib.brotliInstall": { argTypes: [], result: VOID },
"zlib.streamNew": { argTypes: [F64, DYN], result: VOID },
"zlib.streamBytesWritten": { argTypes: [null], result: F64 },
/** `stream instanceof zlib.Gzip`: the borrowed stream and the class's
@@ -4495,6 +4501,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"insp.errorCause": { argTypes: [null, F64, F64], result: STRING },
"insp.dyn": { argTypes: [DYN, F64, F64], result: STRING },
"insp.dynS": { argTypes: [DYN, F64], result: STRING },
/** util.format's %o over a checked-dynamic value: inspect with
* showHidden. Kinds whose hidden entries have no rendering throw the
* 'not supported yet' Error. */
"insp.dynHidden": { argTypes: [DYN, F64], result: STRING },
"insp.begin": { argTypes: [F64], result: VOID },
/** util.inspect's breakLength, compact (0 for false) and sorted for the
* renders that follow; the call site restores 80, 3 and false afterwards. */
@@ -4530,6 +4540,11 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
* `prefix inner` re-indented at the caller's level (+1). */
"insp.nestedBegin": { argTypes: [], result: VOID },
"insp.nestedEnd": { argTypes: [STRING, STRING], result: STRING },
/** Around a top-level render that an inspection refusal can end: mark
* records the inspector's frame and nesting depth, unwind (in the
* render's finally) closes what the render left open above it. */
"insp.mark": { argTypes: [], result: VOID },
"insp.unwind": { argTypes: [], result: VOID },
/** node:string_decoder's utf8 StringDecoder (scr_string.c): the decoder
* value is a one-field record whose f64 PACKS the pending partial
* sequence (count + up to 3 raw bytes — Node buffers at most 3 for
@@ -159,10 +159,12 @@ const MODULE_FIELDS: Record<StringFields<Omit<IrModule, "functions">>, Role> = {
"classes[].instancePrototypeHelper": "reference",
"classes[].arrayFields": "reference",
"classes[].arraySpecies": "reference",
"classes[].stringTagHelper": "reference",
"classes[].localCaptures[].name": "other",
"classes[].localCaptures[].localId": "other",
"classes[].symbolFields[].field": "other",
"classes[].symbolFields[].globalId": "other",
"classes[].symbolFields[].wellKnown": "other",
"classes[].fields[].name": "other",
"classes[].methods[]": "method",
"classes[].prototypeNames[]": "other",
@@ -291,6 +293,10 @@ const REFERENCE_FIXTURES: Record<string, () => IrModule> = {
...module(),
classes: [{ name: "%m0.Row", fields: [], arrayElem: F64, arraySpecies: TARGET, loc }],
}),
"classes[].stringTagHelper": () => ({
...module(),
classes: [{ name: "%m0.Cell", fields: [], stringTagHelper: TARGET, loc }],
}),
"lib.exports[].fnName": () => ({
...module(),
lib: {
@@ -26,6 +26,7 @@ export function moduleFunctionReferences(mod: IrModule): string[] {
if (cls.instancePrototypeHelper !== undefined) names.push(cls.instancePrototypeHelper);
if (cls.arrayFields !== undefined) names.push(cls.arrayFields);
if (cls.arraySpecies !== undefined) names.push(cls.arraySpecies);
if (cls.stringTagHelper !== undefined) names.push(cls.stringTagHelper);
}
return names;
}
@@ -281,6 +281,7 @@ test("dynamic stores disqualify the field names they can reach", () => {
"dyn.typeof",
"insp.dyn",
"insp.dynS",
"insp.dynHidden",
"dyn.objCreate",
"dyn.getPrototype",
"dyn.nameClassPrototype",
@@ -357,6 +358,52 @@ test("an Error subclass constructor forwarding dynamic arguments stores no class
expect(slots.isField("C", "a")).toBe(true);
});
test("Buffer.from over an ArrayBuffer range and Buffer as a value keep class fields int32", () => {
// `Buffer.from(view.buffer, offset, length)` passes its operands as
// checked values, and a program that reads the Buffer function's value
// installs it in %main; neither stores into a class instance.
const classes = [cls("C", ["a"])];
const view: IrExpr = { loc, kind: "dynFrom", value: ref("o", obj("C")), type: DYN };
const libCall = (fnName: string, args: IrExpr[], type: IrType = VOID): IrStmt =>
stmt({ loc, kind: "libCall", fn: fnName, args, type } as IrExpr);
const slots = analyzeInt32Slots(
mod(classes, [
fn(
"load",
[
{ id: "o", type: obj("C") },
{ id: "storage", type: DYN },
],
[
set(ref("o", obj("C")), "C", "a", num(1)),
libCall("dyn.typedRefIs", [
view,
{ loc, kind: "strLit", value: "object:C", type: STRING },
]),
libCall("buffer.installValue", []),
libCall("buffer.constructor", [], DYN),
libCall(
"buffer.poolFromDynRange",
[ref("storage", DYN), num(2), num(4)].map((arg): IrExpr =>
arg.type.kind === "dyn" ? arg : { loc, kind: "dynFrom", value: arg, type: DYN },
),
),
libCall("buffer.poolFromDynOffset", [
ref("storage", DYN),
{
loc,
kind: "dynFrom",
value: num(2),
type: DYN,
},
]),
],
),
]),
);
expect(slots.isField("C", "a")).toBe(true);
});
test("captured bindings share one proof across the declaring function and its closures", () => {
const closure: IrExpr = { loc, kind: "closure", fnName: "inner", captures: ["s"], type: DYN };
const outer = (): IrFunction => ({
+6
View File
@@ -451,8 +451,14 @@ const DYN_READ_ONLY = new Set<string>([
"fs.utimesSync",
// Buffer.prototype members read as values: a lookup by key.
"buffer.prototypeMember",
// Buffer.from(value, offset[, length]) over checked values: an
// ArrayBuffer's view after Node's offset and length conversions, or a copy
// of a string, array-like or typed array's values.
"buffer.poolFromDynOffset",
"buffer.poolFromDynRange",
"insp.dyn",
"insp.dynS",
"insp.dynHidden",
// The dynamic-tier operators run their dyn twins (scr_js_value.c); the
// conversions move a value between dyn and jsval, and js.toF64 is
// dynCheck's number check.
+17 -2
View File
@@ -1485,11 +1485,18 @@ export interface IrClassDef {
jsName?: string;
/** JavaScript constructor arity, ending before the first default or rest parameter. */
jsLength?: number;
/** The class declares public static fields. util.inspect prints them
* after the class tag; a class object reached through `unknown` does
* not carry them unless the runtime holds its static table. */
ownStaticFields?: true;
/** Zero-argument native helper returning the shared prototype data object.
* Used when materializing an instance's own-property view. */
prototypeDataHelper?: string;
/** Native instance to its actual prototype, including factory captures and subclasses. */
instancePrototypeHelper?: string;
/** Native instance to the string its inherited Symbol.toStringTag getter
* answers, read by the runtime for an instance seen as `unknown`. */
stringTagHelper?: string;
/** Public JavaScript fields record their creation in the shared property
* bag. Its descriptors preserve presence and insertion order independently
* of native layout order; field values remain in their native slots. */
@@ -1499,8 +1506,14 @@ export interface IrClassDef {
* class of the chain sets `name` during construction, and then an own
* `name` follows every other own property. */
errorNameAfter?: string | null;
/** Stable module symbol identities for public instance layout fields. */
symbolFields?: { field: string; globalId: string }[];
/** JavaScript Error subclasses with a property bag: a view committed by a
* dynamic write keeps `name` and `message` where the bag recorded them
* (scr_error_dyn_follow_bag). */
errorBagOrder?: boolean;
/** Stable symbol identities for public instance layout fields: the
* module global holding the key, or the name of the well-known symbol
* (`toStringTag` for `Symbol.toStringTag`). */
symbolFields?: ({ field: string } & ({ globalId: string } | { wellKnown: string }))[];
/** RUNTIME-PROVIDED class (the builtin Error hierarchy): the struct, RC
* helpers, and vtable live in the runtime (ScrError / scr_error_*), so
* backends emit no definitions for it — only the preorder-interval
@@ -1684,6 +1697,8 @@ export const ZLIB_STREAM_MODES: ReadonlyMap<string, number> = new Map([
["DeflateRaw", 5],
["InflateRaw", 6],
["Unzip", 7],
["BrotliDecompress", 8],
["BrotliCompress", 9],
]);
export interface IrRecordShape {
@@ -1121,6 +1121,9 @@ const RUNTIME_FEATURE_RULES = {
* scr_webcrypto.c. */
webcrypto: rule({ prefixes: ["webcrypto."] }),
zlib: rule({ prefixes: ["zlib."] }),
/** node:zlib's Brotli forms: zlib.brotliInstall, which %main calls when
* the program uses Brotli (scr_brotli.c). */
brotli: rule({ fns: ["zlib.brotliInstall"] }),
};
type RuntimeFeatureName = keyof typeof RUNTIME_FEATURE_RULES;
@@ -1190,6 +1193,7 @@ function noRuntimeFeatures(): RuntimeFeatures {
wasm: false,
webcrypto: false,
zlib: false,
brotli: false,
};
}
+22 -3
View File
@@ -502,10 +502,29 @@ export function validateModule(mod: IrModule): IrValidationError[] {
});
}
}
for (const symbol of cls.symbolFields ?? []) {
const global = mod.globals?.find((global) => global.id === symbol.globalId);
if (cls.stringTagHelper !== undefined) {
const helper = functionsByName.get(cls.stringTagHelper);
const receiver = helper?.params[0]?.type;
if (
global?.type.kind !== "symbol" ||
!helper ||
helper.params.length !== 1 ||
receiver?.kind !== "object" ||
receiver.className !== cls.name ||
helper.returnType.kind !== "string" ||
helper.captures !== undefined ||
helper.classCaptures !== undefined
) {
errors.push({
message: `class ${cls.name}: string tag helper must be a noncapturing (instance) => string function`,
loc: cls.loc,
});
}
}
for (const symbol of cls.symbolFields ?? []) {
const global =
"globalId" in symbol ? mod.globals?.find((global) => global.id === symbol.globalId) : null;
if (
("globalId" in symbol ? global?.type.kind !== "symbol" : symbol.wellKnown === "") ||
!cls.fields.some((field) => field.name === symbol.field)
) {
errors.push({ message: `class ${cls.name}: invalid symbol field metadata`, loc: cls.loc });
@@ -31,6 +31,7 @@ const LIB_MODE_REFUSED_PREFIXES: readonly [string, string][] = [
["zlib.cbOpts", "the async node:zlib callback surface"],
["zlib.promise", "the async node:zlib callback surface (util.promisify)"],
["zlib.stream", "the node:zlib stream classes"],
["zlib.brotliInstall", "node:zlib's Brotli forms"],
["process.stdoutWriteBytesCb", "process.stdout.write completion callbacks"],
["process.stderrWriteBytesCb", "process.stderr.write completion callbacks"],
["timers.", "the timers surface (setTimeout family)"],
+32 -2
View File
@@ -1,6 +1,7 @@
import { createHash } from "node:crypto";
import {
closeSync,
existsSync,
mkdirSync,
mkdtempSync,
openSync,
@@ -88,14 +89,19 @@ export class NativeCache {
}
}
write(family: string, key: string, bytes: string | Uint8Array): void {
write(
family: string,
key: string,
bytes: string | Uint8Array,
digest = contentDigest(bytes),
): void {
let stage: string | null = null;
try {
const directory = join(this.root, family);
mkdirSync(directory, { recursive: true, mode: 0o700 });
stage = mkdtempSync(join(directory, ".write-"));
writeFileSync(join(stage, "payload"), bytes);
writeFileSync(join(stage, "digest"), contentDigest(bytes) + "\n");
writeFileSync(join(stage, "digest"), digest + "\n");
const path = join(directory, key);
// Concurrent writers publish identical content under the same key;
// a reader between the two renames simply treats it as a miss.
@@ -112,6 +118,29 @@ export class NativeCache {
}
}
/** Whether a marker or entry exists, without reading or checking it. */
has(family: string, key: string): boolean {
return existsSync(join(this.root, family, key));
}
/** Records an empty marker that has() finds. */
mark(family: string, key: string): void {
try {
const directory = join(this.root, family);
mkdirSync(directory, { recursive: true, mode: 0o700 });
writeFileSync(join(directory, key), "");
} catch {
/* Cache failures never change a build's result. */
}
}
/** Writes `bytes` under their content digest, and returns the digest. */
writeContent(family: string, bytes: Uint8Array): string {
const digest = contentDigest(bytes);
this.write(family, digest, bytes, digest);
return digest;
}
/** Publish text parts without joining them into one string. */
writeParts(family: string, key: string, parts: Generator<string>): void {
let stage: string | null = null;
@@ -149,6 +178,7 @@ export class NativeCache {
"object",
"sanitizer",
"executable",
"linked",
"binary",
"dsym",
"coverage",
+26 -4
View File
@@ -56,7 +56,7 @@ import { clearFenceEvalCaches } from "../library/fence-eval.js";
import { archiveNativeLibrary, localizeNativeLibrary, linkNativeWasmLibrary } from "./library.js";
import type { NativeToolchain } from "./toolchain.js";
import { evaluateNativeComptime } from "./comptime.js";
import { contentDigest, fileDigest, type NativeCache } from "./cache.js";
import { contentDigest, fileDigest, partsDigest, type NativeCache } from "./cache.js";
import {
nativeProfileKey,
nativeProfileRefusal,
@@ -168,6 +168,7 @@ export class NativeCompiler {
outputKind: "obj" | "asm",
partitions = 1,
profile?: NativeProfileOptions,
llvmDigest: () => string = () => fileDigest(input),
): string[] {
const toolchain = this.toolchain;
const outputs = nativePartitionPaths(output, partitions);
@@ -186,7 +187,7 @@ export class NativeCompiler {
key = contentDigest(
JSON.stringify({
schema: 1,
llvm: fileDigest(input),
llvm: llvmDigest(),
outputKind,
optimization,
source,
@@ -253,6 +254,7 @@ export class NativeCompiler {
stage: string,
library: boolean,
profile?: NativeProfileOptions,
llvmDigest?: () => string,
): string[] {
// Library archives merge dev shards relocatably; executables link the
// helper's program partitions directly.
@@ -263,7 +265,16 @@ export class NativeCompiler {
const split = llvm === null ? null : splitLlvmLibraryProgram(llvm);
if (split === null) {
const partitions = library ? 1 : nativeProgramPartitions(statSync(input).size);
return this.emitObject(input, output, source, optimization, "obj", partitions, profile);
return this.emitObject(
input,
output,
source,
optimization,
"obj",
partitions,
profile,
llvmDigest,
);
}
const objects: string[] = [];
for (const [index, shard] of split.shards.entries()) {
@@ -358,6 +369,9 @@ export class NativeCompiler {
return { ok: true, artifact: { kind: "ir", path: output } };
}
const { llvm, features } = prepared;
// The executable and object caches key the same LLVM text.
let llvmDigest: string | undefined;
const digestLlvm = (): string => (llvmDigest ??= partsDigest(llvm));
const inputDirectory = join(stage, "input");
mkdirSync(inputDirectory);
const llvmInput = outputKind === "llvm" ? stagedOutput : join(inputDirectory, "program.ll");
@@ -385,7 +399,14 @@ export class NativeCompiler {
const executableCache =
executablePack === null
? null
: openNativeExecutableCache(this.cache, toolchain, llvm, options, ffi, executablePack);
: openNativeExecutableCache(
this.cache,
toolchain,
digestLlvm,
options,
ffi,
executablePack,
);
const restored = executableCache?.restore(stagedOutput) ?? false;
timing(restored ? "executable-cache-hit" : "executable-cache-miss");
if (!restored) {
@@ -408,6 +429,7 @@ export class NativeCompiler {
stage,
false,
profile,
digestLlvm,
);
else
this.emitObject(
@@ -0,0 +1,86 @@
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, expect, test } from "vitest";
import type { NativeRuntimeSelection } from "../backend/runtime-pack-native.js";
import { NativeCache } from "./cache.js";
import { openNativeExecutableCache } from "./executable-cache.js";
import type { NativeToolchain } from "./toolchain.js";
const directories: string[] = [];
const saved = new Map<string, string | undefined>();
beforeEach(() => {
for (const name of ["SCRIPTC_LINKER", "SCRIPTC_LLVM_HELPER", "CC", "LDFLAGS", "LIBRARY_PATH"]) {
saved.set(name, process.env[name]);
delete process.env[name];
}
});
afterEach(() => {
for (const [name, value] of saved) {
if (value === undefined) delete process.env[name];
else process.env[name] = value;
}
saved.clear();
for (const path of directories.splice(0)) rmSync(path, { recursive: true, force: true });
});
/** A toolchain whose linker is a driver script named `driver`; it answers
* `-print-prog-name=clang` with its own path. */
function fixture(platform: string, driver: string) {
const root = mkdtempSync(join(tmpdir(), "scriptc-executable-cache-open-"));
directories.push(root);
const bin = join(root, "bin");
const helper = join(root, "helper");
const runtime = join(root, "runtime");
for (const directory of [bin, helper, runtime]) mkdirSync(directory);
const linker = join(bin, driver);
writeFileSync(linker, '#!/bin/sh\necho "$0"\n');
chmodSync(linker, 0o755);
writeFileSync(join(helper, "codegen"), "helper");
writeFileSync(join(helper, "package.json"), "{}");
writeFileSync(join(runtime, "package.json"), "{}");
writeFileSync(join(runtime, "runtime-pack.json"), "{}");
const toolchain = {
compilerVersion: "test",
target: { platform },
helperExecutable: join(helper, "codegen"),
helperPackageRoot: helper,
runtimePackRoot: runtime,
linker,
linkerArgs: [],
dsymutil: "dsymutil",
} as unknown as NativeToolchain;
const pack = {
root: runtime,
manifest: { licenses: [] },
packageText: "{}",
manifestText: "{}",
selected: { runtime: [], archives: [] },
flavor: "release",
} as unknown as NativeRuntimeSelection;
const open = () =>
openNativeExecutableCache(
new NativeCache(join(root, "cache")),
toolchain,
() => "llvm digest",
{ outPath: join(root, "program"), outDir: root, optimization: "release" },
null,
pack,
);
return { open };
}
test.skipIf(process.platform === "win32")(
"Linux builds that link with a clang driver use the executable cache",
() => {
expect(fixture("linux", "clang").open()).not.toBeNull();
expect(fixture("linux", "clang-22").open()).not.toBeNull();
expect(fixture("linux", "gcc").open()).toBeNull();
expect(fixture("windows", "clang").open()).toBeNull();
expect(fixture("wasi", "clang").open()).toBeNull();
// On darwin only Apple's driver at /usr/bin/clang qualifies.
expect(fixture("darwin", "clang").open()).toBeNull();
process.env["SCRIPTC_LLVM_HELPER"] = "/elsewhere/codegen";
expect(fixture("linux", "clang").open()).toBeNull();
},
);
@@ -40,6 +40,11 @@ function fixture(debug = false) {
const library = join(sdk, "libSystem.tbd");
writeFileSync(runtime, "runtime");
writeFileSync(library, "system library");
// A directory's identity is its stat. Where the file system stamps times
// at the kernel tick, a file added within the tick of the last write
// leaves the directory's times unchanged; an old time makes every later
// change to the SDK directory visible.
utimesSync(sdk, 0, 0);
const cache = new NativeCache(join(root, "cache"));
const key = contentDigest("llvm and options");
const open = () => new NativeExecutableCache(cache, key, "clang", debug, [runtime]);
@@ -217,3 +222,61 @@ test("a failed or empty link trace cannot publish a completed executable", () =>
});
expect(entry.trace([], f.stage)).toBe(false);
});
test.each(["ld", "crt", "libc"] as const)(
"a Linux key's second link records the linker, startup objects and libc; replacing the %s misses",
(input) => {
const f = fixture();
const lib = join(f.root, "lib");
mkdirSync(lib);
const paths = {
ld: join(f.root, "ld"),
crt: join(lib, "Scrt1.o"),
libc: join(lib, "libc.so.6"),
};
writeFileSync(paths.ld, "linker");
writeFileSync(paths.crt, "startup object");
writeFileSync(paths.libc, "c library");
const nonshared = join(lib, "libc_nonshared.a");
writeFileSync(nonshared, "archive");
const program = join(f.stage, "program.o");
writeFileSync(program, "program");
// With -v, clang prints the linker job it runs, as -### would; GNU ld's
// -t names each file it loads, and an archive member as archive(member).
const job = ` "${paths.ld}" "-pie" "-o" "${f.output}" "${paths.crt}" "-L${lib}" "${program}" "-lc"`;
const loaded = [paths.crt, program, paths.libc, `${nonshared}(atexit.oS)`].join("\n");
vi.mocked(spawnSync).mockImplementation((_command, args) => {
const verbose = (args as string[]).includes("-v");
return {
pid: 1,
status: 0,
signal: null,
output: [],
stdout: verbose ? loaded + "\n" : "",
stderr: verbose ? job + "\n" : "",
};
});
const open = () => new NativeExecutableCache(f.cache, f.key, "clang", false, [f.runtime], true);
// A key's first link is not traced; the caller links the output itself.
expect(open().trace(["-o", f.output], f.stage)).toBe(false);
expect(spawnSync).not.toHaveBeenCalled();
expect(open().restore(f.destination)).toBe(false);
// Its next link traces both the driver and the linker in one run.
const entry = open();
expect(entry.trace(["-o", f.output], f.stage)).toBe(true);
expect(spawnSync).toHaveBeenCalledTimes(1);
expect(vi.mocked(spawnSync).mock.calls[0]![1]).toEqual(["-o", f.output, "-v", "-Wl,-t"]);
entry.publish(f.output);
const recorded = (
JSON.parse(f.cache.read("executable", f.key)!.toString()) as { inputs: { path: string }[] }
).inputs.map((saved) => saved.path);
expect(recorded).toEqual(expect.arrayContaining([...Object.values(paths), nonshared, lib]));
expect(recorded).not.toContain(program);
expect(open().restore(f.destination)).toBe(true);
const before = statSync(paths[input]);
writeFileSync(paths[input] + ".new", "x".repeat(before.size));
utimesSync(paths[input] + ".new", before.atime, before.mtime);
renameSync(paths[input] + ".new", paths[input]);
expect(open().restore(f.destination)).toBe(false);
},
);
@@ -11,7 +11,7 @@ import {
import { runNativeTool } from "../backend/native-tools.js";
import type { NativeRuntimeSelection } from "../backend/runtime-pack-native.js";
import type { NativeToolchain } from "./toolchain.js";
import { contentDigest, NativeCache, partsDigest } from "./cache.js";
import { contentDigest, NativeCache } from "./cache.js";
interface InputIdentity {
path: string;
@@ -48,9 +48,22 @@ function identity(path: string): InputIdentity {
};
}
function sameIdentity(a: InputIdentity, b: InputIdentity): boolean {
return (
a.path === b.path &&
a.canonical === b.canonical &&
a.kind === b.kind &&
a.dev === b.dev &&
a.ino === b.ino &&
a.size === b.size &&
a.mtime === b.mtime &&
a.ctime === b.ctime
);
}
function stillMatches(inputs: InputIdentity[]): boolean {
try {
return inputs.every((input) => JSON.stringify(identity(input.path)) === JSON.stringify(input));
return inputs.every((input) => sameIdentity(identity(input.path), input));
} catch {
return false;
}
@@ -165,6 +178,8 @@ function stageSymbols(bytes: Buffer, binary: string): void {
export class NativeExecutableCache {
private inputs: InputIdentity[];
private traced = false;
/** Linux: no entry exists and the key was never linked before. */
private readonly firstLink: boolean;
constructor(
private readonly cache: NativeCache,
@@ -172,23 +187,28 @@ export class NativeExecutableCache {
private readonly linker: string,
private readonly debug: boolean,
paths: string[],
/** Linux: trace with one verbose link, from a key's second link on. */
private readonly linux = false,
) {
this.inputs = [...new Set(paths)].sort().map(identity);
this.firstLink = linux && !cache.has("executable", key) && !cache.has("linked", key);
// A first link neither restores nor traces, so it needs no identities.
this.inputs = this.firstLink ? [] : [...new Set(paths)].sort().map(identity);
}
restore(output: string): boolean {
if (this.firstLink) return false;
try {
const metadata = this.cache.read("executable", this.key);
if (metadata === null) return false;
const entry: unknown = JSON.parse(metadata.toString("utf8"));
if (!validEntry(entry) || !stillMatches(entry.inputs) || !stillMatches(this.inputs))
return false;
const saved = new Map(entry.inputs.map((input) => [input.path, input]));
if (
!this.inputs.every((input) =>
entry.inputs.some(
(saved) => saved.path === input.path && JSON.stringify(saved) === JSON.stringify(input),
),
)
!this.inputs.every((input) => {
const recorded = saved.get(input.path);
return recorded !== undefined && sameIdentity(recorded, input);
})
)
return false;
if (this.debug !== (entry.symbols !== null)) return false;
@@ -209,14 +229,24 @@ export class NativeExecutableCache {
* Directories from the driver's search line detect a newly added candidate.
* Private build inputs already belong to the LLVM/runtime cache identity.
* A true result means the requested output was linked successfully and its
* inputs remained stable. The caller can publish that output directly. */
* inputs remained stable. The caller can publish that output directly. On
* false, the caller links the output itself. */
trace(args: string[], stage: string): boolean {
this.traced = false;
try {
// On Linux a key's first link only records that it was linked; a later
// link of the same key traces, so a build that runs once traces nothing.
if (this.firstLink) {
this.cache.mark("linked", this.key);
return false;
}
const roots = [resolve(stage), realpathSync(stage)];
const dry = toolOutput(this.linker, [...args, "-###"]);
const driverInputs = tracePaths(dry, true, roots).map(identity);
const linked = toolOutput(this.linker, [...args, "-Wl,-t"]);
let driverInputs = this.linux
? []
: tracePaths(toolOutput(this.linker, [...args, "-###"]), true, roots).map(identity);
// On Linux, -v prints the jobs that -### would, while the link runs.
const linked = toolOutput(this.linker, [...args, ...(this.linux ? ["-v"] : []), "-Wl,-t"]);
if (this.linux) driverInputs = tracePaths(linked, true, roots).map(identity);
const paths = tracePaths(linked, false, roots);
if (paths.length === 0 || !stillMatches(this.inputs) || !stillMatches(driverInputs))
return false;
@@ -233,10 +263,8 @@ export class NativeExecutableCache {
if (!this.traced || !stillMatches(this.inputs)) return;
const binary = readFileSync(output);
const symbols = this.debug ? readSymbols(output) : null;
const binaryKey = contentDigest(binary);
const symbolsKey = symbols === null ? null : contentDigest(symbols);
this.cache.write("binary", binaryKey, binary);
if (symbols !== null && symbolsKey !== null) this.cache.write("dsym", symbolsKey, symbols);
const binaryKey = this.cache.writeContent("binary", binary);
const symbolsKey = symbols === null ? null : this.cache.writeContent("dsym", symbols);
if (!stillMatches(this.inputs)) return;
const entry: ExecutableEntry = {
schema: "scriptc.native-executable.v1",
@@ -254,16 +282,18 @@ export class NativeExecutableCache {
export function openNativeExecutableCache(
cache: NativeCache | null,
toolchain: NativeToolchain,
llvm: readonly string[],
llvmDigest: () => string,
options: CompileRequestOptions,
ffi: FfiProfile | null,
pack: NativeRuntimeSelection,
): NativeExecutableCache | null {
// Match the established complete-cache contract: default Apple driver,
// known runtime inputs, and no wrapper or caller-supplied library searches.
// Match the established complete-cache contract: the default Apple driver
// or a Linux clang driver, known runtime inputs, and no wrapper or
// caller-supplied library searches.
const platform = toolchain.target.platform;
if (
cache === null ||
toolchain.target.platform !== "darwin" ||
(platform !== "darwin" && platform !== "linux") ||
options.sanitize ||
// The key names a profile by path, not by its bytes.
options.profileGenerate !== undefined ||
@@ -277,9 +307,23 @@ export function openNativeExecutableCache(
return null;
try {
const linker = commandPath(toolchain.linker);
if (realpathSync(linker) !== "/usr/bin/clang") return null;
const effective = commandPath(runNativeTool(linker, ["-print-prog-name=clang"]).trim());
const debug = options.optimization === "dev" && !options.strip;
// Apple's driver at /usr/bin/clang, or a clang driver on Linux. Both
// name every input of a link in their -### and -Wl,-t traces.
if (
platform === "darwin"
? realpathSync(linker) !== "/usr/bin/clang"
: !/^clang(-\d+)?$/.test(basename(realpathSync(linker)))
)
return null;
// Apple's /usr/bin/clang is a shim for the active developer directory's
// driver; a Linux clang driver is itself the driver.
const effective =
platform === "darwin"
? commandPath(runNativeTool(linker, ["-print-prog-name=clang"]).trim())
: linker;
// Linux keeps debug information in the executable; darwin keeps it in a
// separate dSYM, which the cache stores beside the executable.
const debug = platform === "darwin" && options.optimization === "dev" && !options.strip;
const paths = [
linker,
effective,
@@ -291,6 +335,7 @@ export function openNativeExecutableCache(
(artifact) => join(pack.root, artifact.path),
),
];
if (platform === "linux") paths.push(join(pack.root, "runtime-pack.json"));
if (debug) {
const dsymutil = commandPath(toolchain.dsymutil);
if (realpathSync(dsymutil) !== "/usr/bin/dsymutil") return null;
@@ -299,11 +344,16 @@ export function openNativeExecutableCache(
const key = contentDigest(
JSON.stringify({
schema: 1,
llvm: partsDigest(llvm),
llvm: llvmDigest(),
options,
toolchain,
runtimeIdentity: pack.packageText,
runtimeManifest: pack.manifestText,
// On Linux the manifest's identity names it: restore checks that
// identity again, as it does every other input.
runtimeManifest:
platform === "darwin"
? pack.manifestText
: identity(join(pack.root, "runtime-pack.json")),
linker: identity(linker),
effective: identity(effective),
environment: toolchainEnvironmentFingerprint(),
@@ -311,7 +361,7 @@ export function openNativeExecutableCache(
path: process.env["PATH"] ?? null,
}),
);
return new NativeExecutableCache(cache, key, linker, debug, paths);
return new NativeExecutableCache(cache, key, linker, debug, paths, platform === "linux");
} catch {
return null;
}
@@ -86,6 +86,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")(
builtinValues: false,
warningReplace: false,
stdioExtended: false,
brotli: false,
regex: true,
copying: false,
textDecoderLegacy: false,
+18 -1
View File
@@ -94,6 +94,7 @@ export function buildSanitizedRuntime(
const quickjs = join(vendor, "quickjs-ng");
const zlib = join(vendor, "zlib");
const mbedtls = join(vendor, "mbedtls");
const brotli = join(vendor, "brotli", "c");
const cacheIdentity = contentDigest(
JSON.stringify({
schema: 1,
@@ -137,6 +138,8 @@ export function buildSanitizedRuntime(
zlib,
"-I",
join(mbedtls, "include"),
"-I",
join(brotli, "include"),
...(toolchain.target.platform === "linux" ? ["-ffunction-sections", "-fdata-sections"] : []),
];
const runtimeObjects: string[] = [];
@@ -173,9 +176,23 @@ export function buildSanitizedRuntime(
.filter((file) => file.endsWith(".c") && !file.startsWith("."))
.sort();
args = ["-std=c11", "-I", join(mbedtls, "include"), "-I", directory];
} else if (name === "brotli") {
directory = brotli;
sources = ["common", "dec", "enc"]
.flatMap((dir) =>
readdirSync(join(brotli, dir))
.filter((file) => file.endsWith(".c") && !file.startsWith("."))
.map((file) => `${dir}/${file}`),
)
.sort();
args = ["-std=c11", "-I", join(brotli, "include")];
} else throw new NativeCodegenError("SC3003", `unsupported runtime vendor archive: ${name}`);
const objects = sources.map((source) =>
compile(join(directory, source), name + "-" + source.replace(/\.c$/, ".o"), args),
compile(
join(directory, source),
name + "-" + source.replaceAll("/", "-").replace(/\.c$/, ".o"),
args,
),
);
const output = join(stage, "libscriptc-" + name + ".a");
runNativeTool(
+13 -35
View File
@@ -1,6 +1,6 @@
{
"schemaVersion": 2,
"compilerVersion": "0.4.3",
"compilerVersion": "0.4.4",
"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.",
@@ -2469,6 +2469,12 @@
"name": "url.resolve",
"status": "static"
},
{
"id": "node-builtin.url.urlToHttpOptions",
"kind": "node-builtin",
"name": "url.urlToHttpOptions",
"status": "static"
},
{
"id": "node-builtin.util",
"kind": "node-builtin",
@@ -2876,22 +2882,6 @@
"status": "static",
"note": "recognized module (bare and node:-prefixed specifiers)"
},
{
"id": "node-builtin.zlib.BrotliCompress",
"kind": "node-builtin",
"name": "zlib.BrotliCompress",
"status": "unsupported",
"code": "SC2020",
"note": "Brotli needs a Brotli codec, which the static runtime does not vendor — the deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms with Node's options, crc32, and the Deflate/Inflate/Gzip/Gunzip/DeflateRaw/InflateRaw/Unzip streams are the lowered zlib surface"
},
{
"id": "node-builtin.zlib.BrotliDecompress",
"kind": "node-builtin",
"name": "zlib.BrotliDecompress",
"status": "unsupported",
"code": "SC2020",
"note": "Brotli needs a Brotli codec, which the static runtime does not vendor — the deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms with Node's options, crc32, and the Deflate/Inflate/Gzip/Gunzip/DeflateRaw/InflateRaw/Unzip streams are the lowered zlib surface"
},
{
"id": "node-builtin.zlib.DEFLATE",
"kind": "node-builtin",
@@ -3645,33 +3635,25 @@
"id": "node-builtin.zlib.brotliCompress",
"kind": "node-builtin",
"name": "zlib.brotliCompress",
"status": "unsupported",
"code": "SC2020",
"note": "Brotli needs a Brotli codec, which the static runtime does not vendor — the deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms with Node's options, crc32, and the Deflate/Inflate/Gzip/Gunzip/DeflateRaw/InflateRaw/Unzip streams are the lowered zlib surface"
"status": "static"
},
{
"id": "node-builtin.zlib.brotliCompressSync",
"kind": "node-builtin",
"name": "zlib.brotliCompressSync",
"status": "unsupported",
"code": "SC2020",
"note": "Brotli needs a Brotli codec, which the static runtime does not vendor — the deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms with Node's options, crc32, and the Deflate/Inflate/Gzip/Gunzip/DeflateRaw/InflateRaw/Unzip streams are the lowered zlib surface"
"status": "static"
},
{
"id": "node-builtin.zlib.brotliDecompress",
"kind": "node-builtin",
"name": "zlib.brotliDecompress",
"status": "unsupported",
"code": "SC2020",
"note": "Brotli needs a Brotli codec, which the static runtime does not vendor — the deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms with Node's options, crc32, and the Deflate/Inflate/Gzip/Gunzip/DeflateRaw/InflateRaw/Unzip streams are the lowered zlib surface"
"status": "static"
},
{
"id": "node-builtin.zlib.brotliDecompressSync",
"kind": "node-builtin",
"name": "zlib.brotliDecompressSync",
"status": "unsupported",
"code": "SC2020",
"note": "Brotli needs a Brotli codec, which the static runtime does not vendor — the deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms with Node's options, crc32, and the Deflate/Inflate/Gzip/Gunzip/DeflateRaw/InflateRaw/Unzip streams are the lowered zlib surface"
"status": "static"
},
{
"id": "node-builtin.zlib.crc32",
@@ -3683,17 +3665,13 @@
"id": "node-builtin.zlib.createBrotliCompress",
"kind": "node-builtin",
"name": "zlib.createBrotliCompress",
"status": "unsupported",
"code": "SC2020",
"note": "Brotli needs a Brotli codec, which the static runtime does not vendor — the deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms with Node's options, crc32, and the Deflate/Inflate/Gzip/Gunzip/DeflateRaw/InflateRaw/Unzip streams are the lowered zlib surface"
"status": "static"
},
{
"id": "node-builtin.zlib.createBrotliDecompress",
"kind": "node-builtin",
"name": "zlib.createBrotliDecompress",
"status": "unsupported",
"code": "SC2020",
"note": "Brotli needs a Brotli codec, which the static runtime does not vendor — the deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms with Node's options, crc32, and the Deflate/Inflate/Gzip/Gunzip/DeflateRaw/InflateRaw/Unzip streams are the lowered zlib surface"
"status": "static"
},
{
"id": "node-builtin.zlib.createDeflate",
@@ -167,3 +167,47 @@ test("a store into a dictionary whose values can be functions keeps checked disp
);
expect(await callersOf(source, "%dyn.class.properties")).not.toEqual([]);
});
test("a store into a generic dictionary of dictionaries keeps checked dispatch", async () => {
const source = dictionaries
.replace(
"interface Settings { groups: Record<string, Record<string, string>> }",
"interface Settings<T> { groups: Record<string, Record<string, T>> }",
)
.replace("function addGroup(settings: Settings,", "function addGroup<T>(settings: Settings<T>,")
.replace("const settings: Settings =", "const settings: Settings<string> =");
expect(await callersOf(source, "%dyn.class.properties")).not.toEqual([]);
});
test("a runtime-key store into a dictionary view of a class instance keeps checked dispatch", async () => {
const source = `
class Counter {
n = 0;
tick(): number { return ++this.n; }
}
const counter = new Counter();
const view: {} = counter;
const slots: Record<string, string> = view;
const key: string = String(process.argv.length);
slots[key] = "replaced";
console.log(counter.tick());
`;
// The method has a slot the write can replace, which calls consult.
expect(await callersOf(source, "%Counter.tick")).toContain("%method.value:%Counter.tick");
});
test("a dot store into a dictionary view of a class instance keeps checked dispatch", async () => {
const source = `
class Lamp {
on = false;
toggle(): boolean { this.on = !this.on; return this.on; }
}
const lamp = new Lamp();
const view: {} = lamp;
const slots: Record<string, string> = view;
slots.toggle = "replaced";
console.log(lamp.toggle());
`;
// The method has a slot the write can replace, which calls consult.
expect(await callersOf(source, "%Lamp.toggle")).toContain("%method.value:%Lamp.toggle");
});
@@ -0,0 +1,89 @@
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, test } from "vitest";
import { compile, deserializeModule } from "../src/index.js";
/** The IR type kind of each named module-level binding. */
async function globalKinds(source: string): Promise<Record<string, string>> {
const dir = await mkdtemp(join(tmpdir(), "scriptc-dictionary-views-"));
try {
const entry = join(dir, "main.ts");
const outPath = join(dir, "main.ir.json");
await writeFile(entry, source);
const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" });
if (!result.ok)
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
const mod = deserializeModule(await readFile(outPath, "utf8"));
return Object.fromEntries((mod.globals ?? []).map((g) => [g.name, g.type.kind]));
} finally {
await rm(dir, { recursive: true, force: true });
}
}
const program = (body: string): string => `
const key = String(process.argv.length);
const counts: Record<string, number> = { a: 1 };
counts[key] = 2;
${body}
console.log(JSON.stringify(counts));
`;
test("a record written through a dictionary keeps that dictionary type checked", async () => {
const kinds = await globalKinds(
program(`const point: { a: string } = { a: "x" };
const table: Record<string, string> = point;
table[key] = "y";
console.log(JSON.stringify(point));`),
);
expect(kinds["table"]).toBe("dyn");
// Dictionaries of other value types keep their native layout.
expect(kinds["counts"]).toBe("record");
});
test("a record reaching a dictionary through a {} parameter keeps it checked", async () => {
const kinds = await globalKinds(
program(`function stamp(value: {}): void {
const inner: Record<string, string> = value;
inner[key] = "y";
}
const point: { a: string } = { a: "x" };
stamp(point);
const other: Record<string, string> = { b: "z" };
console.log(JSON.stringify(point), other["b"]);`),
);
expect(kinds["other"]).toBe("dyn");
});
test("dictionaries keep their native layout without a record flowing in", async () => {
const kinds = await globalKinds(
program(`const table: Record<string, string> = { a: "x" };
table[key] = "y";
const copy: Record<string, string> = table;
copy[key] = "z";
console.log(JSON.stringify(table));`),
);
expect(kinds["table"]).toBe("record");
expect(kinds["copy"]).toBe("record");
});
test("a record converted to a dictionary that is only read keeps the native layout", async () => {
const kinds = await globalKinds(
program(`const point: { a: string } = { a: "x" };
const table: Record<string, string> = point;
console.log(table["a"]);`),
);
expect(kinds["table"]).toBe("record");
});
test("a dictionary literal whose methods read this keeps checked identity", async () => {
const kinds = await globalKinds(
program(`interface Table { [key: string]: () => number }
const methods: Table = {
base() { return 2; },
twice() { return this.base() * 2; },
};
console.log(methods.twice());`),
);
expect(kinds["methods"]).toBe("dyn");
});
@@ -221,3 +221,21 @@ test("a JavaScript file TypeScript checks uses its JSDoc types for class layouts
expect(layout(unchecked, "Account").fields).toBe("owner:dyn balance:dyn");
expect(params(unchecked, "%Account.constructor")).toBe("(object, dyn, dyn): void");
});
test("instances of a class with subclasses box at their exact class", async () => {
const module = await lower(`${SHAPES}
/** @param {any} value */
function keep(value) {
return value;
}
/** @param {Shape} shape */
function cross(shape) {
return keep(shape);
}
console.log(cross(new Square()).size, cross(new Shape()).label);
`);
const helper = module.functions.find((fn) => fn.name === "%dyn.class.boxExact:Shape");
expect(helper).toBeDefined();
expect(calls(module, "cross")).toContain("%dyn.class.boxExact:Shape");
expect(JSON.stringify(helper!.body)).toContain('"className":"Square"');
});
@@ -51,14 +51,12 @@ test("decompression options lower for run-time validation", () => {
expect(diags).toEqual([]);
});
test("Brotli keeps its SC2020 refusal", () => {
test("Brotli options lower for run-time validation", () => {
const diags = diagnostics(`
import { brotliCompressSync } from "node:zlib";
console.log(brotliCompressSync("payload").length);
import { brotliCompressSync, constants } from "node:zlib";
console.log(brotliCompressSync("payload", { params: { [constants.BROTLI_PARAM_QUALITY]: 4 } }).length);
`);
expect(diags).toHaveLength(1);
expect(diags[0]!.code).toBe("SC2020");
expect(diags[0]!.message).toContain("brotliCompressSync");
expect(diags).toEqual([]);
});
test("union writes keep the refusal for incompatible field storage", () => {
@@ -0,0 +1,46 @@
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, test } from "vitest";
import { compile, deserializeModule } from "../src/index.js";
import { emitLlvmModule } from "../src/backend/llvm/emitter.js";
test("a quotient by Math.pow(2, n) multiplies by the divisor's reciprocal power of two", async () => {
const dir = await mkdtemp(join(tmpdir(), "scriptc-pow2-quotient-"));
try {
const entry = join(dir, "main.ts");
const outPath = join(dir, "main.ir.json");
await writeFile(
entry,
`
function scaled(x: number, n: number): number { return x / Math.pow(2, n); }
function plain(x: number, d: number): number { return x / d; }
function other(x: number, n: number): number { return x / Math.pow(3, n); }
console.log(scaled(3, 4), plain(3, 16), other(3, 4));
`,
);
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"));
const mod = deserializeModule(await readFile(outPath, "utf8"));
const ll = emitLlvmModule(mod);
const fn = (name: string): string =>
new RegExp(`^define internal [^\\n]*@sc_[bf]+_${name}(?:\\.i32)?\\([^]*?^}`, "m").exec(
ll,
)![0];
// The reciprocal's bits are 0x7FE0000000000000 minus the divisor's; a
// divisor that is not a power of two from 2^-1022 to 2^1022 divides.
const quotient = fn("scaled");
expect(quotient).toContain(`sub i64 ${0x7fe0000000000000n}, `);
expect(quotient).toMatch(/fmul double %\w+, %\w+/);
expect(quotient).toMatch(/icmp ult i64 %\w+, 2045/);
expect(quotient).toMatch(/select i1 %\w+, double %\w+, double %\w+/);
expect(quotient.match(/ = fdiv double /g)).toHaveLength(1);
for (const name of ["plain", "other"]) {
expect(fn(name)).not.toContain(`${0x7fe0000000000000n}`);
expect(fn(name)).toMatch(/fdiv double/);
}
} finally {
await rm(dir, { recursive: true, force: true });
}
});
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6343-inspect-class-values.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6344-inspect-function-properties.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6357-format-o-hidden.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6358-format-o-hidden-js.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7012-dictionary-views-keep-objects.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7013-dictionary-view-parameter.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7189-non-null-assertion-nullish-destination.ts"
],
"diags": []
}

Some files were not shown because too many files have changed in this diff Show More