mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-25 06:24:27 +08:00
Merge main
This commit is contained in:
@@ -8,7 +8,9 @@ node_modules/
|
||||
!tests/fixtures/npm/node_modules/
|
||||
!tests/corpus/*/node_modules/
|
||||
!tests/fixtures/npm-static/node_modules/
|
||||
!tests/coverage-fixtures/wasm/node_modules/
|
||||
!tests/fixtures/npm-static/nested/inner/node_modules/
|
||||
!tests/fixtures/npm-static/node_modules/*/node_modules/
|
||||
!tests/fixtures/npm/store/*/node_modules/
|
||||
!tests/fixtures/npm/cases/*/node_modules/
|
||||
!tests/library-mode/*/node_modules/
|
||||
@@ -32,6 +34,7 @@ output/
|
||||
!tests/fixtures/npm/cases/workspace-copied/members/*/dist/
|
||||
!tests/fixtures/npm/cases/workspace-copied/node_modules/*/dist/
|
||||
!tests/fixtures/provenance/node_modules/*/dist/
|
||||
!tests/fixtures/npm-static/node_modules/*/dist/
|
||||
.scriptc/
|
||||
*.log
|
||||
.DS_Store
|
||||
|
||||
@@ -270,6 +270,7 @@
|
||||
"packages/compiler/src/backend/llvm/**",
|
||||
"packages/compiler/src/backend/loop-polls.test.ts",
|
||||
"packages/compiler/src/backend/may-throw.test.ts",
|
||||
"packages/compiler/src/backend/stack-observations.test.ts",
|
||||
"packages/compiler/src/ir/**",
|
||||
"packages/compiler/src/library/*.test.ts",
|
||||
"packages/compiler/test/**",
|
||||
|
||||
@@ -48,6 +48,12 @@ Corpus programs are differential tests against Node: every program runs under No
|
||||
|
||||
Before opening a PR that adds or changes corpus, diagnostic, or frontend fixtures, run `pnpm test:ts7 --baselines-only`. This fast completeness check uses the same fixture discovery as the parity suite; it does not type-check or replace behavioral parity. Generate missing preflight/order baselines, review their diagnostics and module order, and run the affected `order-parity.test.ts` batches before pushing. Keep existing baseline entries unchanged unless the corresponding behavior intentionally changes. The full `pnpm test:ts7` command also checks completeness before starting its sweep.
|
||||
|
||||
A change that must not cost good code anything, such as a compatibility fix that replaces a refusal, proves it with the zero-cost gate. The gate compiles the benchmark workloads, the self-hosted compiler, and the TypeScript corpus with two built checkouts. It requires byte-identical IR, LLVM, and release executables, and compile time within tolerance. `--allow=<regex>` names the change's pattern signature: programs whose sources match may differ, and the PR lists them. Any other difference fails the gate, which names the first differing function. The gate also enforces the two-tier runtime separation: no executable may gain symbols from `DYNAMIC_TIER_PREFIXES` in `scripts/zero-cost-compare.mjs`, and fully typed benchmarks may link none; extend that list as dynamic-tier units land. `--quick` checks only the benchmarks; `--cachegrind` adds per-function instruction counts for executables that differ. Run it in a Sandbox with both checkouts prepared:
|
||||
|
||||
```bash
|
||||
node scripts/zero-cost-gate.mjs --baseline=/vercel/sandbox/prev --candidate=. --allow='\barguments\b'
|
||||
```
|
||||
|
||||
Test location follows scope:
|
||||
|
||||
- Co-locate white-box unit tests with implementation files under
|
||||
|
||||
@@ -119,7 +119,7 @@ Verifies and loads the installed precompiled runtime objects and vendored librar
|
||||
<dd>On Linux, when the program calls <code>process.exit()</code>, hand the release of its memory to a helper process so the parent sees the exit sooner. The kernel otherwise frees the whole address space before the exit is reported, which takes tens of milliseconds per gigabyte. Opt in only where the trade-offs are acceptable: for up to about 100 ms after the exit, the helper keeps the executable mapped (overwriting the binary in place fails with "text file busy"), its memory stays charged to the cgroup, and the teardown CPU time no longer appears in the parent's resource usage. Valid only for executable builds; it has no effect on other platforms.</dd>
|
||||
|
||||
<dt><code>--npm-static <pkg[,pkg…]|auto></code></dt>
|
||||
<dd>Name npm packages to compile even if they don't ship their own type declarations (repeatable). <code>auto</code> uses the default automatic selection. Packages that can't compile produce diagnostics. See <a href="/docs/dependencies#explicit-package-selection">npm Dependencies</a>.</dd>
|
||||
<dd>Name npm packages to compile even if they don't ship their own type declarations (repeatable). A trailing <code>...</code>, as in <code>fs-extra...</code>, also names every package that package depends on. <code>auto</code> uses the default automatic selection. Packages that can't compile produce diagnostics. See <a href="/docs/dependencies#explicit-package-selection">npm Dependencies</a>.</dd>
|
||||
|
||||
<dt><code>--provenance-sources</code></dt>
|
||||
<dd>Experimental. Compile npm dependencies from the source at the commit their provenance attestation names. Packages without a usable attestation compile from their installed files.</dd>
|
||||
@@ -127,6 +127,15 @@ Verifies and loads the installed precompiled runtime objects and vendored librar
|
||||
<dt><code>--external-types <specifier=file.d.ts></code></dt>
|
||||
<dd>Coverage only. Map an exact bare module specifier to a local declaration file supplied by an embedder. Repeat the option for multiple modules. The declaration supplies checker types so application coverage can continue; runtime imports and values remain explicit <code>SC1010</code> blockers. Relative paths resolve from the current working directory. Accepted files end in <code>.d.ts</code>, <code>.d.mts</code>, or <code>.d.cts</code>.</dd>
|
||||
|
||||
<dt><code>--tier <auto|static-only></code></dt>
|
||||
<dd>The tier policy for code that has no static lowering. <code>auto</code> is the default. <code>static-only</code> guarantees that the program contains no dynamic-tier demotions: every such construct stays a compile error in TypeScript and a deferred runtime error in JavaScript, which is how both modes behave today. Use it in CI to keep a program fully static. Library mode is always static-only. See <a href="/docs/coverage#dynamic-tier">Dynamic tier</a>.</dd>
|
||||
|
||||
<dt><code>--explain-dynamic[=<name>]</code></dt>
|
||||
<dd>Coverage only. After the report, list the functions that already operate on values typed only at runtime and the bindings, fields, functions, classes and modules that unsupported constructs would move to the dynamic tier, each with the diagnostic behind it. With a name such as <code>x</code>, <code>Point</code>, or <code>Point.x</code>, show only the matching units. See <a href="/docs/coverage#dynamic-tier">Dynamic tier</a>.</dd>
|
||||
|
||||
<dt><code>--wasm <file.wasm></code></dt>
|
||||
<dd>Coverage only. Report a WebAssembly module the program loads from a path its sources do not spell out. The report already lists modules whose paths the compiler resolves, modules inlined as base64 literals, and <code>.wasm</code> files shipped by packages that use WebAssembly; a <code>scriptc.wasm</code> array in <code>package.json</code> adds files the same way as this flag. Each module is listed with its size, whether it validates, and the WebAssembly proposals it uses. scriptc does not compile WebAssembly modules to native code yet. Repeatable.</dd>
|
||||
|
||||
<dt><code>--sanitize</code></dt>
|
||||
<dd>Build an executable with AddressSanitizer and a reference-count audit at exit, the same checks scriptc's own test suite uses. Requires a C compiler and is available only for host builds. Not supported with <code>--emit=asm</code> or <code>--emit=obj</code>.</dd>
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ When a program has unsupported operations, the report adds a group for each root
|
||||
<tbody>
|
||||
<tr><td><code>statements analyzed</code></td><td>Executable statements in application code, excluding dependency implementation code.</td></tr>
|
||||
<tr><td><code>compile statically</code></td><td>Statements that compile to native instructions.</td></tr>
|
||||
<tr><td><code>in dynamic tier</code></td><td>Of those, the statements in functions that operate on values typed only at runtime, such as <code>any</code> values and untyped JavaScript. Shown when there are any. See <a href="#dynamic-tier">Dynamic tier</a>.</td></tr>
|
||||
<tr><td><code>blockers</code></td><td>Unsupported operations with counts, diagnostic codes, reasons, locations, and hints.</td></tr>
|
||||
<tr><td><code>in unreached code</code></td><td>Blockers in code the entry never reaches. A build never compiles that code, so these cannot fail it.</td></tr>
|
||||
<tr><td><code>differs from Node</code></td><td>Sites that compile but can behave differently from Node.js. See <a href="#node-divergences">Node divergences</a>.</td></tr>
|
||||
@@ -51,6 +52,16 @@ If a function's signature can't compile, the statements in its body count as not
|
||||
<span id="analyze-with-dynamic" />
|
||||
<span id="embedded-nodejs-builtins" />
|
||||
|
||||
<span id="dynamic-tier" />
|
||||
|
||||
## Dynamic tier
|
||||
|
||||
Code that operates on values whose types are known only at runtime, such as `any` values, the result of an untyped `JSON.parse`, or untyped JavaScript, compiles into dynamic-tier functions. They are native code too, but they check types and look up properties at runtime, so they are slower than code with static types. The report's `in dynamic tier` line counts their statements.
|
||||
|
||||
`--explain-dynamic` lists those functions and the operation that makes each one dynamic. It also lists the units an unsupported construct would move to the dynamic tier: the binding, field, class, function, or module that has no static representation for the construct, with the diagnostic behind each one. These are the constructs the dynamic tier is planned to compile; today they remain blockers in TypeScript and deferred runtime errors in JavaScript. Add a name, such as `--explain-dynamic=Point`, to show only the matching units. With `--print=diagnostics`, the document gains a `tiers` object with the same information.
|
||||
|
||||
`--tier=static-only` keeps every unsupported construct a blocker instead of moving it to the dynamic tier; code that already operates on runtime-typed values compiles as before. Today `auto`, the default, behaves the same way.
|
||||
|
||||
## Diagnostic codes
|
||||
|
||||
Coverage and `scriptc build` report the same diagnostic codes. Common ones include:
|
||||
@@ -160,7 +171,7 @@ For editors, CI annotations, and other tools, `scriptc coverage --print=diagnost
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>stats</code></td>
|
||||
<td>Coverage only, absent after a preflight failure: the report's statement counts. <code>statementsDynamic</code> is always 0 and remains for schema compatibility.</td>
|
||||
<td>Coverage only, absent after a preflight failure: the report's statement counts. <code>statementsStatic</code> counts every statement that compiles to native code; <code>statementsDynamic</code> is the part of those in dynamic-tier functions.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>category</code></td>
|
||||
|
||||
@@ -46,6 +46,8 @@ Your callback and the commander implementation both compile to native code. No e
|
||||
|
||||
scriptc finds packages in `node_modules` with Node.js's resolution rules, including the export conditions in `package.json`, and compiles their dependencies too. TypeScript workspace packages compile as part of your program, including `.js` import specifiers that resolve to `.ts` sources.
|
||||
|
||||
Type-only imports follow Node.js's type stripping. `import type` and `export type … from` statements are erased, so a package or module named only by them supplies types and isn't compiled or loaded; it can even be a package with no JavaScript, such as `type-fest`. A clause whose specifiers are all marked `type`, such as `import { type Options } from "./config.ts"`, still loads its module, as it does under Node.js.
|
||||
|
||||
## Which packages compile
|
||||
|
||||
scriptc compiles a package automatically when it:
|
||||
@@ -53,7 +55,7 @@ scriptc compiles a package automatically when it:
|
||||
- ships its own type declarations, and
|
||||
- ships readable, unminified JavaScript.
|
||||
|
||||
A package that doesn't meet these requirements, or that uses something scriptc can't compile, gets a package-specific diagnostic that explains why. The usual fixes are to switch to a version or alternative package that compiles, or to replace the call with code you write yourself.
|
||||
A package that doesn't meet these requirements, or that uses something scriptc can't compile, gets a package-specific diagnostic that explains why. The usual fixes are to switch to a version or alternative package that compiles, or to replace the call with code you write yourself. A package whose declarations come from a separate `@types/` package doesn't ship its own; its diagnostic names the package and the `--npm-static` option that compiles it anyway.
|
||||
|
||||
<span id="value-conversion" />
|
||||
<span id="static-package-compilation-experimental" />
|
||||
@@ -67,7 +69,7 @@ Some packages rely on separately installed declarations, such as an `@types/` pa
|
||||
$ scriptc build app.ts --npm-static=three -o app >/dev/null
|
||||
```
|
||||
|
||||
Naming a package skips the declaration requirement above, but the package still has to compile. `--npm-static=auto` uses the default automatic selection. The option accepts a comma-separated list and can be repeated.
|
||||
Naming a package skips the declaration requirement above, but the package still has to compile. The option selects only the packages it names: a dependency of a named package that doesn't ship its own declarations is still refused. Add a trailing `...` to select a package together with every package it depends on, for example `--npm-static=fs-extra...`. `--npm-static=auto` uses the default automatic selection. The option accepts a comma-separated list and can be repeated.
|
||||
|
||||
## CommonJS packages
|
||||
|
||||
|
||||
@@ -76,6 +76,8 @@ title: "Limitations"
|
||||
|
||||
<include>../limitations/builtin-apis/legacy-url-api.mdx</include>
|
||||
|
||||
<include>../limitations/builtin-apis/deprecated-modules.mdx</include>
|
||||
|
||||
<include>../limitations/builtin-apis/filesystem-flags.mdx</include>
|
||||
|
||||
<include>../limitations/builtin-apis/copy.mdx</include>
|
||||
@@ -118,6 +120,8 @@ title: "Limitations"
|
||||
|
||||
<include>../limitations/builtin-apis/synchronous-iteration.mdx</include>
|
||||
|
||||
<include>../limitations/builtin-apis/iterator-helpers.mdx</include>
|
||||
|
||||
<include>../limitations/builtin-apis/regular-expressions.mdx</include>
|
||||
|
||||
<include>../limitations/builtin-apis/object-freezing.mdx</include>
|
||||
@@ -236,6 +240,10 @@ title: "Limitations"
|
||||
|
||||
<include>../limitations/native-addons.mdx</include>
|
||||
|
||||
## WebAssembly modules
|
||||
|
||||
<include>../limitations/webassembly-modules.mdx</include>
|
||||
|
||||
## WASI target
|
||||
|
||||
<include>../limitations/wasi-target.mdx</include>
|
||||
|
||||
@@ -1 +1 @@
|
||||
- **Crypto:** Hashing, HMAC, PBKDF2, and HKDF support every digest `crypto.getHashes()` lists in Node 24, with OpenSSL's aliases and object identifiers. Hash and Hmac accept every Buffer encoding and any TypedArray or DataView, `createHash` and `hash.copy` take `outputLength`, and `crypto.hash` takes its options object; HMAC, PBKDF2, and HKDF reject the SHAKE and KMAC digests with Node's errors. Hash and Hmac values are also Transforms: they can be written, piped into, used as `pipeline()` stages, read, and listened to, and they report Node's errors after the stream flushes. `copy()` of a Hash whose stream flushed, and stream use of a value after its stream closed, throw a coded SC2020 error. `timingSafeEqual` compares any view or ArrayBuffer. Secret KeyObjects (`createSecretKey`, `generateKey` and `generateKeySync` for `hmac` and `aes`) support `type`, `symmetricKeySize`, `export()`, `equals()`, and `instanceof KeyObject`, and createHmac, hkdf, createCipheriv, and createDecipheriv accept them; asymmetric keys, `KeyObject.from()`, and `toCryptoKey()` are not available. `randomFill`, `randomFillSync` (every view kind and ArrayBuffer), `randomInt`, `randomUUID`, and `randomUUIDv7` follow Node 24.22, including callback timing; a `randomInt` callback whose error parameter is typed `Error | null` receives `null` where Node passes `undefined`. Scrypt supports its default parameters, numeric options, aliases, and memory limit. Both KDFs accept strings, Buffers, numeric typed arrays, DataViews, and fixed-length ArrayBuffers. `createCipheriv` and `createDecipheriv` support aes-128, aes-192, and aes-256 in CBC, CTR, and GCM modes and chacha20-poly1305, on native targets, through `update()`, `final()`, `setAAD()`, `setAutoPadding()`, `getAuthTag()`, and `setAuthTag()`; other ciphers, cipher streams, and wasm32-wasi builds are refused. Decrypting chacha20-poly1305 without an authentication tag fails in `final()`; Node 24.22 returns unverified plaintext. After `update()` fails, for example past the AES-GCM limit of 2^36 - 32 bytes per message, every later call on that cipher throws; Node still lets `final()` run. An output encoding that is a function or an instance of a class defining `toString` throws a coded SC2020 error. Public-key operations remain unsupported. Native derivations finish before their deferred callbacks, so computation blocks the caller.
|
||||
- **Crypto:** Hashing, HMAC, PBKDF2, and HKDF support every digest `crypto.getHashes()` lists in Node 24, with OpenSSL's aliases and object identifiers. Hash and Hmac accept every Buffer encoding and any TypedArray or DataView, `createHash` and `hash.copy` take `outputLength`, and `crypto.hash` takes its options object; HMAC, PBKDF2, and HKDF reject the SHAKE and KMAC digests with Node's errors. Hash and Hmac values are also Transforms: they can be written, piped into, used as `pipeline()` stages, read, and listened to, and they report Node's errors after the stream flushes. `copy()` of a Hash whose stream flushed, and stream use of a value after its stream closed, throw a coded SC2020 error. Through `unknown` and `any` slots and untyped JavaScript, Hash and Hmac values keep their identity and support `update()`, `digest()`, and Hash's `copy()`, called or read as values; their stream and event members there throw a coded SC2020 error. `timingSafeEqual` compares any view or ArrayBuffer. Secret KeyObjects (`createSecretKey`, `generateKey` and `generateKeySync` for `hmac` and `aes`) support `type`, `symmetricKeySize`, `export()`, `equals()`, and `instanceof KeyObject`, and createHmac, hkdf, createCipheriv, and createDecipheriv accept them; asymmetric keys, `KeyObject.from()`, and `toCryptoKey()` are not available. `randomFill`, `randomFillSync` (every view kind and ArrayBuffer), `randomInt`, `randomUUID`, and `randomUUIDv7` follow Node 24.22, including callback timing; a `randomInt` callback whose error parameter is typed `Error | null` receives `null` where Node passes `undefined`. Scrypt supports its default parameters, numeric options, aliases, and memory limit. Both KDFs accept strings, Buffers, numeric typed arrays, DataViews, and fixed-length ArrayBuffers. `createCipheriv` and `createDecipheriv` support aes-128, aes-192, and aes-256 in CBC, CTR, and GCM modes and chacha20-poly1305, on native targets, through `update()`, `final()`, `setAAD()`, `setAutoPadding()`, `getAuthTag()`, and `setAuthTag()`; other ciphers, cipher streams, and wasm32-wasi builds are refused. Decrypting chacha20-poly1305 without an authentication tag fails in `final()`; Node 24.22 returns unverified plaintext. After `update()` fails, for example past the AES-GCM limit of 2^36 - 32 bytes per message, every later call on that cipher throws; Node still lets `final()` run. An output encoding that is a function or an instance of a class defining `toString` throws a coded SC2020 error. Public-key operations remain unsupported. Native derivations finish before their deferred callbacks, so computation blocks the caller.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
- **Deprecated `punycode` and `constants` modules:** `punycode.encode()`, `decode()`, `toASCII()`, `toUnicode()`, `ucs2.decode()`, `ucs2.encode()`, and `version` compile, with the `DEP0040` warning Node.js reports when the module loads. A `require("punycode")` made from inside `node_modules` doesn't warn; a `require()` used inside a larger expression, such as `require("punycode").toASCII(name)`, doesn't warn either. The `punycode.ucs2` object itself is unsupported as a value. The `constants` module is the frozen object Node.js builds from `os.constants` and `fs.constants`, plus the `crypto.constants` of Node.js 24.22.0.
|
||||
@@ -0,0 +1 @@
|
||||
- **Iterator helpers:** `map`, `filter`, `take`, `drop`, `flatMap`, `toArray`, `forEach`, `reduce`, `some`, `every` and `find` compile on generators, on array, Map and Set iterators, and on the objects the helpers return. Helper objects support `.next()`, `for...of`, spreads, `Array.from` and `yield*`, pull their source lazily, and close it (run its `return()`) at the same points as Node: when `take` reaches its count, when a short-circuiting method finds its answer, when a callback throws, and when the loop consuming the helper exits early. `flatMap` callbacks return arrays or generators. Iterator helpers on string iterators, `Iterator.from`, the `Iterator` global, calling `return()` on a helper object in TypeScript, printing helper objects, and helper calls on untyped iterators in JavaScript are unsupported.
|
||||
@@ -1 +1 @@
|
||||
- **Map and Set:** Keys and elements support numbers and strings by value, and records, classes, arrays, symbols, server handles, and unions of these reference types by identity. `unknown` slots use native checked values, comparing supported primitives by value and retaining supported object references. Typed nested collections other than `Map<unknown, unknown>` and `Set<unknown>`, and promises requiring payload adapters, cannot pass into these slots. `Promise<unknown>` retains its native representation. Stored `keys()`, `values()`, and `entries()` iterators compile, retain element identity, and observe live collection mutations until exhausted. They support `.next()`, `for...of`, array spreads, and mapper-less `Array.from`. Detached collection methods and iterator helper methods such as `.map()` and `.take()` are unsupported. The Set composition methods (`union`, `intersection`, `difference`, `symmetricDifference`, `isSubsetOf`, `isSupersetOf`, `isDisjointFrom`) accept a Set of the same element type or a Map keyed by it; other set-like arguments are unsupported.
|
||||
- **Map and Set:** Keys and elements support numbers and strings by value, and records, classes, arrays, symbols, server handles, and unions of these reference types by identity. `unknown` slots use native checked values, comparing supported primitives by value and retaining supported object references. Typed nested collections other than `Map<unknown, unknown>` and `Set<unknown>`, and promises requiring payload adapters, cannot pass into these slots. `Promise<unknown>` retains its native representation. Stored `keys()`, `values()`, and `entries()` iterators compile, retain element identity, and observe live collection mutations until exhausted. They support `.next()`, `for...of`, array spreads, and mapper-less `Array.from`. They also support the iterator helper methods, such as `.map()` and `.take()`. Detached collection methods are unsupported. The Set composition methods (`union`, `intersection`, `difference`, `symmetricDifference`, `isSubsetOf`, `isSupersetOf`, `isDisjointFrom`) accept a Set of the same element type or a Map keyed by it; other set-like arguments are unsupported.
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
- **Snapshots:** `t.assert.snapshot` and `t.assert.fileSnapshot` compile, with Node's default serializer, `JSON.stringify(value, null, 2)`, or an `options.serializers` array literal. A compiled test reads the snapshot file Node writes beside the entry file, `<entry file>.snapshot`, at the entry file's path when the program was compiled. Running the binary with Node's flag `--test-update-snapshots` as its first argument writes the snapshot files instead; as in Node, the flag is kept out of `process.argv` and appears in `process.execArgv`. Snapshot files are read in the form Node writes them, `` exports[`key`] = `value`; `` statements. The `snapshot.setDefaultSnapshotSerializers` and `snapshot.setResolveSnapshotPath` module functions throw `SC2020`.
|
||||
- **TestContext values:** `t.filePath` is the entry file's path when the program was compiled. `t.attempt` is `0`, since a test runs once. `t.passed` is `false` while the body runs and the test's outcome in its `afterEach` and `t.after` hooks. `t.workerId` is `Number(process.env.NODE_TEST_WORKER_ID) || undefined`, as in Node. `t.tags` is `[]`, since the `tags` test option throws `SC2020`; the array is not frozen as Node's is.
|
||||
- **Suite and hook contexts:** a `describe` body may take its `SuiteContext`, which has `name`, `fullName`, `filePath`, `attempt`, `passed` and `diagnostic`. Hooks may take their context, as in Node: the suite's for `before` and `after`, the test's for `beforeEach`, `afterEach`, `t.before` and `t.after`. The `SuiteContext` `signal` and `log` members, and hook `done` callbacks, throw `SC2020`.
|
||||
- **Module-level hooks:** `before` hooks outside any suite run at their `before()` call, and the first top-level test waits for the async ones; `after` hooks outside any suite run after the last top-level test. Both get the root's context, named `<root>`. As in Node, a throwing root `before` fails each top-level test with its error, without running its body, and cancels the tests and suites inside each top-level suite. A throwing root `after` reports the entry file as a failed result, and the exit code is unchanged.
|
||||
|
||||
@@ -1 +1 @@
|
||||
- **Class reflection:** `JSON.stringify` and `Object.keys` require every declared field to have a representation that can pass through runtime-checked storage. They raise an error for classes with opaque fields such as native Maps. Class fields, inherited accessors, and added properties retain identity through untyped values.
|
||||
- **Class reflection:** `JSON.stringify` and `Object.keys` require every declared field to have a representation that can pass through runtime-checked storage. `JSON.stringify` writes Date fields as their ISO text and Map and Set fields as `{}`, as Node.js does, and refuses classes with RegExp or Promise fields with `SC1090`. `Object.keys` raises an error for classes with opaque fields such as native Maps. Class fields, inherited accessors, and added properties retain identity through untyped values.
|
||||
|
||||
@@ -1 +1 @@
|
||||
- **Local class expressions:** Static members, decorators, private brands, generic members, explicitly generic factories returning local constructors, and references through the class's internal name are unsupported. Supported expressions preserve fresh constructor identity and shared captures across constructors, methods, accessors, and arrow fields. Local inheritance requires a compiled base class. Constructors retain identity, `typeof`, `name`, and `length` through `unknown`; cast back to their exact type before construction. Construction and static-member access directly through `unknown` are unsupported.
|
||||
- **Local class expressions:** Static members, decorators, private brands, generic members, and references through the class's internal name are unsupported. Supported expressions preserve fresh constructor identity and shared captures across constructors, methods, accessors, and arrow fields. Local inheritance requires a compiled base class. Constructors retain identity, `typeof`, `name`, and `length` through `unknown`; cast back to their exact type before construction. Construction and static-member access directly through `unknown` are unsupported.
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
Compiled programs can't load WebAssembly modules yet. Code that only mentions the `WebAssembly` types type-checks, including packages whose declarations name them, and `typeof WebAssembly` answers as it does in Node.js. Every use of the API itself is rejected: `WebAssembly.compile`, `instantiate` and `validate`, `new WebAssembly.Module` and the other constructors, `instanceof` checks, and reads of `globalThis.WebAssembly`. TypeScript sources fail to build with SC2020 at each use, and JavaScript sources throw that diagnostic when the statement runs.
|
||||
|
||||
`scriptc coverage` lists the modules a program would load, with their size, whether they validate, and the WebAssembly proposals they use. It finds modules at paths the compiler resolves, modules inlined as base64 literals, `.wasm` files shipped by packages that use WebAssembly, and files named with [`--wasm`](/docs/cli) or a `scriptc.wasm` array in `package.json`.
|
||||
@@ -2,6 +2,6 @@
|
||||
"schemaVersion": 3,
|
||||
"nodeVersion": "24.22.0",
|
||||
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
|
||||
"artifactVersion": "a610bdd1ae56e1c4cd38",
|
||||
"artifactVersion": "efc18bcd8a7a03fd8622",
|
||||
"rowCount": 3880
|
||||
}
|
||||
|
||||
@@ -330,8 +330,8 @@
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 12,
|
||||
"refused": 0,
|
||||
"not-implemented": 80,
|
||||
"refused": 1,
|
||||
"not-implemented": 79,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -584,9 +584,9 @@
|
||||
"apiEntries": 9,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 0,
|
||||
"partial": 8,
|
||||
"refused": 0,
|
||||
"not-implemented": 9,
|
||||
"not-implemented": 1,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -36590,9 +36590,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "refused",
|
||||
"detail": "Explicitly rejected by a named compiler diagnostic.",
|
||||
"verification": "explicit-refusal"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -58489,7 +58489,7 @@
|
||||
"inherited": false
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"status": "partial",
|
||||
"detail": "Derived from the exact API rows in this chapter.",
|
||||
"verification": "derived"
|
||||
}
|
||||
@@ -58512,9 +58512,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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -58535,9 +58535,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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -58558,9 +58558,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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -58581,9 +58581,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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -58627,9 +58627,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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -58650,9 +58650,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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -58673,9 +58673,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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
"tierItemCount": 3456,
|
||||
"tiers": {
|
||||
"static": {
|
||||
"audit-partial": 1155,
|
||||
"verify-gap": 2174,
|
||||
"replace-refusal": 40,
|
||||
"audit-partial": 1162,
|
||||
"verify-gap": 2166,
|
||||
"replace-refusal": 41,
|
||||
"classify": 2,
|
||||
"implement": 85
|
||||
}
|
||||
@@ -6736,6 +6736,8 @@
|
||||
"source": "compiler-dedicated:crypto.createHash",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -6774,6 +6776,8 @@
|
||||
"source": "compiler-dedicated:crypto.createHmac",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -7396,6 +7400,8 @@
|
||||
"source": "compiler-dedicated:crypto.hash",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -7519,6 +7525,8 @@
|
||||
"source": "compiler-dedicated:crypto.pbkdf2",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -7557,6 +7565,8 @@
|
||||
"source": "compiler-dedicated:crypto.pbkdf2Sync",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -9037,6 +9047,8 @@
|
||||
"source": "compiler-dedicated:Hash",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -9075,6 +9087,8 @@
|
||||
"source": "compiler-dedicated:hash.copy",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -9113,6 +9127,8 @@
|
||||
"source": "compiler-dedicated:hash.digest",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -9151,6 +9167,8 @@
|
||||
"source": "compiler-dedicated:hash.update",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -9189,6 +9207,8 @@
|
||||
"source": "compiler-dedicated:Hmac",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -9227,6 +9247,8 @@
|
||||
"source": "compiler-dedicated:hmac.digest",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -9265,6 +9287,8 @@
|
||||
"source": "compiler-dedicated:hmac.update",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -34178,11 +34202,11 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:globals",
|
||||
"status": "refused",
|
||||
"action": "replace-refusal",
|
||||
"verification": "explicit-refusal",
|
||||
"confidence": "high",
|
||||
"source": "surface-manifest:unsupported:stdlib.webassembly",
|
||||
"tests": []
|
||||
}
|
||||
}
|
||||
@@ -58559,12 +58583,16 @@
|
||||
"priority": "low",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:punycode",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "surface-manifest:node-builtin.punycode.decode",
|
||||
"tests": [
|
||||
"tests/corpus/6122-punycode-conversions.ts",
|
||||
"tests/corpus/6124-punycode-require-warnings.cjs",
|
||||
"tests/corpus/6126-punycode-dependency/main.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -58584,12 +58612,16 @@
|
||||
"priority": "low",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:punycode",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "surface-manifest:node-builtin.punycode.encode",
|
||||
"tests": [
|
||||
"tests/corpus/6122-punycode-conversions.ts",
|
||||
"tests/corpus/6124-punycode-require-warnings.cjs",
|
||||
"tests/corpus/6126-punycode-dependency/main.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -58609,12 +58641,16 @@
|
||||
"priority": "low",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:punycode",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "surface-manifest:node-builtin.punycode.toASCII",
|
||||
"tests": [
|
||||
"tests/corpus/6122-punycode-conversions.ts",
|
||||
"tests/corpus/6124-punycode-require-warnings.cjs",
|
||||
"tests/corpus/6126-punycode-dependency/main.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -58634,12 +58670,16 @@
|
||||
"priority": "low",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:punycode",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "surface-manifest:node-builtin.punycode.toUnicode",
|
||||
"tests": [
|
||||
"tests/corpus/6122-punycode-conversions.ts",
|
||||
"tests/corpus/6124-punycode-require-warnings.cjs",
|
||||
"tests/corpus/6126-punycode-dependency/main.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -58684,12 +58724,16 @@
|
||||
"priority": "low",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:punycode",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "surface-manifest:node-builtin.punycode.ucs2.decode",
|
||||
"tests": [
|
||||
"tests/corpus/6122-punycode-conversions.ts",
|
||||
"tests/corpus/6124-punycode-require-warnings.cjs",
|
||||
"tests/corpus/6126-punycode-dependency/main.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -58709,12 +58753,16 @@
|
||||
"priority": "low",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:punycode",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "surface-manifest:node-builtin.punycode.ucs2.encode",
|
||||
"tests": [
|
||||
"tests/corpus/6122-punycode-conversions.ts",
|
||||
"tests/corpus/6124-punycode-require-warnings.cjs",
|
||||
"tests/corpus/6126-punycode-dependency/main.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -58734,12 +58782,16 @@
|
||||
"priority": "low",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:punycode",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "surface-manifest:node-builtin.punycode.version",
|
||||
"tests": [
|
||||
"tests/corpus/6122-punycode-conversions.ts",
|
||||
"tests/corpus/6124-punycode-require-warnings.cjs",
|
||||
"tests/corpus/6126-punycode-dependency/main.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -342,8 +342,8 @@
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 12,
|
||||
"refused": 0,
|
||||
"not-implemented": 80,
|
||||
"refused": 1,
|
||||
"not-implemented": 79,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -611,9 +611,9 @@
|
||||
"apiEntries": 9,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 0,
|
||||
"partial": 8,
|
||||
"refused": 0,
|
||||
"not-implemented": 9,
|
||||
"not-implemented": 1,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -19861,6 +19861,8 @@
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/node24-crypto-kdf-errors.ts",
|
||||
"tests/corpus/node24-crypto-kdf-options.ts",
|
||||
"tests/corpus/node24-crypto-kdf.ts",
|
||||
@@ -20402,6 +20404,8 @@
|
||||
"evidence": "compiler-dedicated:crypto.createHash",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -20439,6 +20443,8 @@
|
||||
"evidence": "compiler-dedicated:crypto.createHmac",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -21040,6 +21046,8 @@
|
||||
"evidence": "compiler-dedicated:crypto.hash",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -21158,6 +21166,8 @@
|
||||
"evidence": "compiler-dedicated:crypto.pbkdf2",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -21195,6 +21205,8 @@
|
||||
"evidence": "compiler-dedicated:crypto.pbkdf2Sync",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -23095,6 +23107,8 @@
|
||||
"evidence": "compiler-dedicated:Hash",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -23132,6 +23146,8 @@
|
||||
"evidence": "compiler-dedicated:hash.copy",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -23169,6 +23185,8 @@
|
||||
"evidence": "compiler-dedicated:hash.digest",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -23206,6 +23224,8 @@
|
||||
"evidence": "compiler-dedicated:hash.update",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -23243,6 +23263,8 @@
|
||||
"evidence": "compiler-dedicated:Hmac",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -23280,6 +23302,8 @@
|
||||
"evidence": "compiler-dedicated:hmac.digest",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -23317,6 +23341,8 @@
|
||||
"evidence": "compiler-dedicated:hmac.update",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -25031,6 +25057,8 @@
|
||||
"evidence": "compiler-dedicated:crypto.pbkdf2",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -28114,6 +28142,8 @@
|
||||
"evidence": "compiler-dedicated:Hash",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -28164,6 +28194,8 @@
|
||||
"evidence": "compiler-dedicated:Hmac",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -53521,8 +53553,8 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:globals"
|
||||
"status": "refused",
|
||||
"evidence": "surface-manifest:unsupported:stdlib.webassembly"
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -81945,8 +81977,13 @@
|
||||
"inherited": false
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "derived:descendants"
|
||||
"status": "partial",
|
||||
"evidence": "derived:descendants",
|
||||
"tests": [
|
||||
"tests/corpus/6122-punycode-conversions.ts",
|
||||
"tests/corpus/6124-punycode-require-warnings.cjs",
|
||||
"tests/corpus/6126-punycode-dependency/main.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "chapter"
|
||||
},
|
||||
@@ -81968,8 +82005,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:punycode"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.punycode.decode",
|
||||
"tests": [
|
||||
"tests/corpus/6122-punycode-conversions.ts",
|
||||
"tests/corpus/6124-punycode-require-warnings.cjs",
|
||||
"tests/corpus/6126-punycode-dependency/main.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -81991,8 +82033,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:punycode"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.punycode.encode",
|
||||
"tests": [
|
||||
"tests/corpus/6122-punycode-conversions.ts",
|
||||
"tests/corpus/6124-punycode-require-warnings.cjs",
|
||||
"tests/corpus/6126-punycode-dependency/main.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -82014,8 +82061,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:punycode"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.punycode.toASCII",
|
||||
"tests": [
|
||||
"tests/corpus/6122-punycode-conversions.ts",
|
||||
"tests/corpus/6124-punycode-require-warnings.cjs",
|
||||
"tests/corpus/6126-punycode-dependency/main.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -82037,8 +82089,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:punycode"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.punycode.toUnicode",
|
||||
"tests": [
|
||||
"tests/corpus/6122-punycode-conversions.ts",
|
||||
"tests/corpus/6124-punycode-require-warnings.cjs",
|
||||
"tests/corpus/6126-punycode-dependency/main.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -82083,8 +82140,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:punycode"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.punycode.ucs2.decode",
|
||||
"tests": [
|
||||
"tests/corpus/6122-punycode-conversions.ts",
|
||||
"tests/corpus/6124-punycode-require-warnings.cjs",
|
||||
"tests/corpus/6126-punycode-dependency/main.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -82106,8 +82168,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:punycode"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.punycode.ucs2.encode",
|
||||
"tests": [
|
||||
"tests/corpus/6122-punycode-conversions.ts",
|
||||
"tests/corpus/6124-punycode-require-warnings.cjs",
|
||||
"tests/corpus/6126-punycode-dependency/main.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -82129,8 +82196,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:punycode"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.punycode.version",
|
||||
"tests": [
|
||||
"tests/corpus/6122-punycode-conversions.ts",
|
||||
"tests/corpus/6124-punycode-require-warnings.cjs",
|
||||
"tests/corpus/6126-punycode-dependency/main.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "chapter"
|
||||
},
|
||||
|
||||
@@ -203,6 +203,10 @@ function featureOf(row, entries, parentSignature) {
|
||||
|
||||
function symbolCandidates(row) {
|
||||
const result = new Set([row.apiSymbol]);
|
||||
// A global's own row (`WebAssembly`) matches the compiler's
|
||||
// `globalThis.<name>` entries.
|
||||
if (row.chapter === "globals" && row.apiSymbol && !row.apiSymbol.includes("."))
|
||||
result.add(`globalThis.${row.apiSymbol}`);
|
||||
const aliases = [
|
||||
["fsPromises.", "fs/promises."],
|
||||
["dnsPromises.", "dns/promises."],
|
||||
|
||||
@@ -147,6 +147,7 @@
|
||||
"node-builtin.process.rows": ["tests/corpus/2992-stream-rows.ts", "tests/harness/terminal-geometry.test.ts"],
|
||||
"node-builtin.process.columns": ["tests/corpus/1448-stream-columns.ts", "tests/harness/terminal-geometry.test.ts"],
|
||||
"node-builtin.process.exitCode": ["tests/corpus/5809-process-exit-and-rejections.cjs", "tests/corpus/2941-process-exit-code.ts", "tests/corpus/2942-process-exit-code-override.ts", "tests/corpus/2943-process-exit-code-invalid.ts", "tests/corpus/2944-process-exit-code-wrap.ts"],
|
||||
"node-builtin.punycode": ["tests/corpus/6122-punycode-conversions.ts", "tests/corpus/6124-punycode-require-warnings.cjs", "tests/corpus/6126-punycode-dependency/main.cjs"],
|
||||
"node-builtin.querystring": ["tests/corpus/957-builtins-namespace.ts", "tests/corpus/2917-builtin-callable-widths.ts"],
|
||||
"node-builtin.readline": ["tests/corpus/1475-readline-closed-stdin.ts"],
|
||||
"node-builtin.stream": ["tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1746-stream-for-await.ts", "tests/corpus/1814-stream-pipeline.cjs", "tests/corpus/2629-stream-consumers.ts", "tests/corpus/2861-readable-iterator-close.ts"],
|
||||
@@ -386,7 +387,7 @@
|
||||
{ "symbols": ["assert.deepEqual", "assert.notDeepEqual"], "status": "partial", "evidence": ["tests/corpus/5513-assert-deep-equal-loose.js", "tests/corpus/5514-assert-deep-equal-typed.ts", "tests/corpus/5516-assert-deep-equal-edges.js"] },
|
||||
{ "symbols": ["assert", "assert.ok", "assert.equal", "assert.notEqual", "assert.strictEqual", "assert.notStrictEqual", "assert.deepStrictEqual", "assert.notDeepStrictEqual", "assert.fail", "assert.match", "assert.doesNotMatch", "assert.throws", "assert.rejects", "assert.ifError"], "status": "partial", "evidence": ["tests/corpus/1600-assert-passing.ts", "tests/corpus/1604-assert-deep-structures.ts", "tests/corpus/1609-assert-async.ts", "tests/corpus/1721-assert-throws-regex-class.ts", "tests/corpus/2936-static-loose-equality.ts", "tests/corpus/5515-assert-deep-strict-typed-messages.ts"] },
|
||||
{ "chapter": "crypto", "symbols": ["Cipheriv", "cipher.update", "cipher.final", "cipher.setAAD", "cipher.getAuthTag", "cipher.setAutoPadding", "Decipheriv", "decipher.update", "decipher.final", "decipher.setAAD", "decipher.setAuthTag", "decipher.setAutoPadding"], "status": "partial", "evidence": ["tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/fixtures/node-types/cipher.ts"] },
|
||||
{ "symbols": ["crypto.createHash", "Hash", "hash.update", "hash.digest", "hash.copy", "crypto.createHmac", "Hmac", "hmac.update", "hmac.digest", "crypto.hash", "crypto.pbkdf2", "crypto.pbkdf2Sync"], "status": "partial", "evidence": ["tests/corpus/5760-crypto-hash-stream.ts", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", "tests/corpus/node24-crypto-sha2.ts", "tests/corpus/5435-crypto-hash-algorithms.ts", "tests/corpus/5436-crypto-hash-names.js", "tests/corpus/5437-crypto-hmac-algorithms.ts", "tests/corpus/5438-crypto-kdf-digests.ts", "tests/corpus/5440-crypto-hash-encodings.js", "tests/corpus/5441-crypto-hash-output-length.ts", "tests/harness/npm-static.test.ts"] },
|
||||
{ "symbols": ["crypto.createHash", "Hash", "hash.update", "hash.digest", "hash.copy", "crypto.createHmac", "Hmac", "hmac.update", "hmac.digest", "crypto.hash", "crypto.pbkdf2", "crypto.pbkdf2Sync"], "status": "partial", "evidence": ["tests/corpus/5760-crypto-hash-stream.ts", "tests/corpus/5768-crypto-hash-unknown.ts", "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", "tests/corpus/node24-crypto-sha2.ts", "tests/corpus/5435-crypto-hash-algorithms.ts", "tests/corpus/5436-crypto-hash-names.js", "tests/corpus/5437-crypto-hmac-algorithms.ts", "tests/corpus/5438-crypto-kdf-digests.ts", "tests/corpus/5440-crypto-hash-encodings.js", "tests/corpus/5441-crypto-hash-output-length.ts", "tests/harness/npm-static.test.ts"] },
|
||||
{ "symbols": ["crypto.randomFill", "crypto.randomFillSync", "crypto.randomUUID", "crypto.randomUUIDv7"], "status": "supported", "evidence": ["tests/corpus/5442-crypto-random-values.ts", "tests/corpus/5443-crypto-random-arguments.js", "tests/corpus/1358-crypto-random.ts"] },
|
||||
{ "symbols": ["crypto.createSecretKey", "crypto.generateKey", "crypto.generateKeySync", "KeyObject", "keyObject.export", "keyObject.equals", "keyObject.symmetricKeySize", "keyObject.type"], "status": "partial", "evidence": ["tests/corpus/5445-crypto-secret-key-objects.ts", "tests/corpus/5446-crypto-secret-key-errors.js"] },
|
||||
{ "symbols": ["crypto.timingSafeEqual"], "status": "supported", "evidence": ["tests/corpus/5444-crypto-timing-safe-equal.js", "tests/corpus/2890-crypto-static-utility.ts"] },
|
||||
|
||||
@@ -418,7 +418,12 @@ std::optional<ProgramPartitions> partitionProgramText(StringRef Text,
|
||||
|
||||
// An internal function referenced by exactly one other function, and by
|
||||
// no global, is a whole-program inlining candidate; keep it with that
|
||||
// function so its partition can inline it without importing.
|
||||
// function so its partition can inline it without importing. A function
|
||||
// body larger than this is past LLVM's inline cost limit even for the only
|
||||
// call to a local function, so it is placed by its own name. Otherwise a
|
||||
// program's module initializers, which only the entry point calls, would
|
||||
// all share the entry point's partition.
|
||||
constexpr size_t MaxInlinedBodyBytes = 256 * 1024;
|
||||
constexpr unsigned Shared = ~1u;
|
||||
std::vector<unsigned> Referrer(Symbols.size(), NoPartition);
|
||||
for (unsigned I = 0; I < Symbols.size(); ++I) {
|
||||
@@ -441,7 +446,8 @@ std::optional<ProgramPartitions> partitionProgramText(StringRef Text,
|
||||
for (unsigned Steps = 0; Steps < 1024; ++Steps) {
|
||||
const Symbol &S = Symbols[Root];
|
||||
unsigned Parent = Referrer[Root];
|
||||
if (!S.Local || Parent == NoPartition || Parent == Shared ||
|
||||
if (!S.Local || S.Body.size() > MaxInlinedBodyBytes ||
|
||||
Parent == NoPartition || Parent == Shared ||
|
||||
Symbols[Parent].Kind != SymbolKind::Function || Parent == I)
|
||||
break;
|
||||
Root = Parent;
|
||||
|
||||
@@ -22,8 +22,8 @@ struct ProgramPartitions {
|
||||
// Divides the LLVM assembly scriptc emits into Count modules without parsing
|
||||
// it, so the partitions can be parsed concurrently. Functions are placed by a
|
||||
// stable hash of their name; an internal function referenced by exactly one
|
||||
// other function stays with that function, and a global stays with the first
|
||||
// function that references it. Returns nullopt for any construct outside the
|
||||
// other function stays with that function unless its body is too large to
|
||||
// inline, and a global stays with the first function that references it. Returns nullopt for any construct outside the
|
||||
// emitter's narrow top-level grammar; callers then split a parsed module.
|
||||
std::optional<ProgramPartitions> partitionProgramText(llvm::StringRef Text,
|
||||
unsigned Count);
|
||||
|
||||
@@ -3762,6 +3762,39 @@ declare module "node:querystring" {
|
||||
export * from "querystring";
|
||||
}
|
||||
|
||||
/* node:punycode (deprecated, DEP0040) — punycode.js 2.1.0: decode and
|
||||
* toUnicode throw its RangeErrors on malformed input; ucs2 converts
|
||||
* between strings and UTF-16 code point arrays. */
|
||||
declare module "punycode" {
|
||||
export function decode(string: string): string;
|
||||
export function encode(string: string): string;
|
||||
export function toUnicode(domain: string): string;
|
||||
export function toASCII(domain: string): string;
|
||||
export namespace ucs2 {
|
||||
function decode(string: string): number[];
|
||||
function encode(codePoints: readonly number[]): string;
|
||||
}
|
||||
export const version: string;
|
||||
}
|
||||
declare module "node:punycode" {
|
||||
export * from "punycode";
|
||||
}
|
||||
|
||||
/* node:constants (deprecated, DEP0008) — one frozen object: os.constants'
|
||||
* dlopen, errno, priority and signals groups, fs.constants, and
|
||||
* crypto.constants. A platform's missing constant reads as undefined. */
|
||||
declare module "constants" {
|
||||
const constants: { readonly [name: string]: number } & {
|
||||
readonly defaultCoreCipherList: string;
|
||||
readonly defaultCipherList: string;
|
||||
};
|
||||
export = constants;
|
||||
}
|
||||
declare module "node:constants" {
|
||||
import constants = require("constants");
|
||||
export = constants;
|
||||
}
|
||||
|
||||
/* node:readline — the question/close slice: createInterface over exactly
|
||||
* { input: process.stdin, output: process.stdout }, question(query, cb)
|
||||
* (the query writes to stdout, the callback gets the next line's text),
|
||||
|
||||
+134
@@ -13,6 +13,9 @@
|
||||
* they MERGE as overloads instead of colliding — the Timeout handle maps
|
||||
* to the numeric timer id either way, and .unref()/.ref()/.hasRef() are
|
||||
* loop-liveness bookkeeping).
|
||||
* 4. The WebAssembly namespace and BufferSource, which the DOM library
|
||||
* supplies in ordinary TypeScript projects (types only; every reached
|
||||
* use of the API is refused at its site).
|
||||
*
|
||||
* This file ships to EVERY program scriptc builds — the lowering program
|
||||
* and preflight's project-world second-chance program alike (a project
|
||||
@@ -108,3 +111,134 @@ declare function clearTimeout(handle?: Timeout | number | null | undefined): voi
|
||||
* baked result is exactly what Node would compute at runtime (the
|
||||
* differential harness verifies this with a `comptime = (f) => f()` shim). */
|
||||
declare function comptime<T>(compute: () => T): T;
|
||||
|
||||
/* The JavaScript WebAssembly API, as the DOM library declares it (scriptc's
|
||||
* ES-only type world never loads lib.dom, and @types/node leaves it to
|
||||
* lib.dom). Packages whose declarations name these types (quickjs-wasi,
|
||||
* photon-node, ...) typecheck against them. These are types only: the
|
||||
* type mapper gives every type declared here the checked-dynamic
|
||||
* representation, and the lowerer refuses each reached use of the API with
|
||||
* a located diagnostic (frontend/lowering/builtins/webassembly.ts).
|
||||
* BufferSource admits every ArrayBufferView, as lib.dom did before
|
||||
* TypeScript 5.7: Node's Buffer types are views over ArrayBufferLike. */
|
||||
type BufferSource = ArrayBufferView | ArrayBuffer;
|
||||
declare namespace WebAssembly {
|
||||
interface CompileError extends Error {}
|
||||
var CompileError: {
|
||||
prototype: CompileError;
|
||||
new (message?: string): CompileError;
|
||||
(message?: string): CompileError;
|
||||
};
|
||||
interface Global<T extends ValueType = ValueType> {
|
||||
value: ValueTypeMap[T];
|
||||
valueOf(): ValueTypeMap[T];
|
||||
}
|
||||
var Global: {
|
||||
prototype: Global;
|
||||
new <T extends ValueType = ValueType>(
|
||||
descriptor: GlobalDescriptor<T>,
|
||||
v?: ValueTypeMap[T],
|
||||
): Global<T>;
|
||||
};
|
||||
interface Instance {
|
||||
readonly exports: Exports;
|
||||
}
|
||||
var Instance: {
|
||||
prototype: Instance;
|
||||
new (module: Module, importObject?: Imports): Instance;
|
||||
};
|
||||
interface LinkError extends Error {}
|
||||
var LinkError: {
|
||||
prototype: LinkError;
|
||||
new (message?: string): LinkError;
|
||||
(message?: string): LinkError;
|
||||
};
|
||||
interface Memory {
|
||||
readonly buffer: ArrayBuffer;
|
||||
grow(delta: number): number;
|
||||
}
|
||||
var Memory: {
|
||||
prototype: Memory;
|
||||
new (descriptor: MemoryDescriptor): Memory;
|
||||
};
|
||||
interface Module {}
|
||||
var Module: {
|
||||
prototype: Module;
|
||||
new (bytes: BufferSource): Module;
|
||||
customSections(moduleObject: Module, sectionName: string): ArrayBuffer[];
|
||||
exports(moduleObject: Module): ModuleExportDescriptor[];
|
||||
imports(moduleObject: Module): ModuleImportDescriptor[];
|
||||
};
|
||||
interface RuntimeError extends Error {}
|
||||
var RuntimeError: {
|
||||
prototype: RuntimeError;
|
||||
new (message?: string): RuntimeError;
|
||||
(message?: string): RuntimeError;
|
||||
};
|
||||
interface Table {
|
||||
readonly length: number;
|
||||
get(index: number): any;
|
||||
grow(delta: number, value?: any): number;
|
||||
set(index: number, value?: any): void;
|
||||
}
|
||||
var Table: {
|
||||
prototype: Table;
|
||||
new (descriptor: TableDescriptor, value?: any): Table;
|
||||
};
|
||||
interface GlobalDescriptor<T extends ValueType = ValueType> {
|
||||
mutable?: boolean;
|
||||
value: T;
|
||||
}
|
||||
interface MemoryDescriptor {
|
||||
initial: number;
|
||||
maximum?: number;
|
||||
shared?: boolean;
|
||||
}
|
||||
interface ModuleExportDescriptor {
|
||||
kind: ImportExportKind;
|
||||
name: string;
|
||||
}
|
||||
interface ModuleImportDescriptor {
|
||||
kind: ImportExportKind;
|
||||
module: string;
|
||||
name: string;
|
||||
}
|
||||
interface TableDescriptor {
|
||||
element: TableKind;
|
||||
initial: number;
|
||||
maximum?: number;
|
||||
}
|
||||
interface ValueTypeMap {
|
||||
anyfunc: Function;
|
||||
externref: any;
|
||||
f32: number;
|
||||
f64: number;
|
||||
i32: number;
|
||||
i64: bigint;
|
||||
v128: never;
|
||||
}
|
||||
interface WebAssemblyInstantiatedSource {
|
||||
instance: Instance;
|
||||
module: Module;
|
||||
}
|
||||
type ImportExportKind = "function" | "global" | "memory" | "table";
|
||||
type TableKind = "anyfunc" | "externref";
|
||||
type ExportValue = Function | Global | Memory | Table;
|
||||
type Exports = Record<string, ExportValue>;
|
||||
type ImportValue = ExportValue | number;
|
||||
type Imports = Record<string, ModuleImports>;
|
||||
type ModuleImports = Record<string, ImportValue>;
|
||||
type ValueType = keyof ValueTypeMap;
|
||||
function compile(bytes: BufferSource): Promise<Module>;
|
||||
function compileStreaming(source: Response | PromiseLike<Response>): Promise<Module>;
|
||||
function instantiate(
|
||||
bytes: BufferSource,
|
||||
importObject?: Imports,
|
||||
): Promise<WebAssemblyInstantiatedSource>;
|
||||
function instantiate(moduleObject: Module, importObject?: Imports): Promise<Instance>;
|
||||
function instantiateStreaming(
|
||||
source: Response | PromiseLike<Response>,
|
||||
importObject?: Imports,
|
||||
): Promise<WebAssemblyInstantiatedSource>;
|
||||
function validate(bytes: BufferSource): boolean;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { type AllocHost, emitObjectAlloc, emitObjectFree, inlineAllocSupported } from "./alloc.js";
|
||||
import {
|
||||
type AllocHost,
|
||||
disposeHookLoadLines,
|
||||
emitObjectAlloc,
|
||||
emitObjectFree,
|
||||
inlineAllocSupported,
|
||||
} from "./alloc.js";
|
||||
|
||||
function host(inlineAlloc: boolean): AllocHost & { decls: Set<string>; oom: boolean } {
|
||||
const h = {
|
||||
@@ -63,7 +69,12 @@ describe("emitted object allocation", () => {
|
||||
]);
|
||||
expect(emitObjectFree(h, false)).toEqual([
|
||||
" call void @scr_obj_free_note()",
|
||||
" %sf.obs = load i1, ptr @sc_weak_keys",
|
||||
" br i1 %sf.obs, label %sf.observe, label %sf.block",
|
||||
"sf.observe:",
|
||||
" call void @scr_weak_dispose(ptr %o)",
|
||||
" br label %sf.block",
|
||||
"sf.block:",
|
||||
" call void @scr_rt_free(ptr %o)",
|
||||
]);
|
||||
expect([...h.decls].some((d) => d.includes("@scr_sa"))).toBe(false);
|
||||
@@ -167,4 +178,30 @@ describe("emitted object allocation", () => {
|
||||
expect(lines.at(-1)).toBe("sf.done:");
|
||||
}
|
||||
});
|
||||
|
||||
test("frees test the dispose hook only where the program can observe them", () => {
|
||||
const h = host(true);
|
||||
const weak = emitObjectFree(h, false);
|
||||
expectSingleDefinitions(weak);
|
||||
expect(weak.slice(0, 2)).toEqual([
|
||||
" %sf.obs = load i1, ptr @sc_weak_keys",
|
||||
" br i1 %sf.obs, label %sf.hooked, label %sf.block",
|
||||
]);
|
||||
const records = emitObjectFree(h, true, "weak-or-source");
|
||||
expectSingleDefinitions(records);
|
||||
expect(records.slice(0, 4)).toEqual([
|
||||
" %sf.obsw = load i1, ptr @sc_weak_keys",
|
||||
" %sf.obss = load i1, ptr @sc_record_sources",
|
||||
" %sf.obs = or i1 %sf.obsw, %sf.obss",
|
||||
" br i1 %sf.obs, label %sf.hooked, label %sf.block",
|
||||
]);
|
||||
expect(records).toContain(" call void %sf.hook(ptr %o)");
|
||||
});
|
||||
|
||||
test("loads the dispose hook from emitted code only with the inline allocator", () => {
|
||||
expect(disposeHookLoadLines(host(false), "%t1")).toBeNull();
|
||||
expect(disposeHookLoadLines(host(true), "%t1")).toEqual([
|
||||
" %t1 = load ptr, ptr @scr_weak_dispose_hook",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -79,6 +79,17 @@ function threadLocalState(host: AllocHost, prefix: string, lines: string[]): str
|
||||
return [...preamble, ...text.split("\n")];
|
||||
}
|
||||
|
||||
/** Lines that load the runtime's dispose hook into `dest`, or null on
|
||||
* targets without the inline allocator, where the hook's storage is not
|
||||
* addressed from emitted code. The hook is NULL until a weak collection
|
||||
* or a kept record source installs it, and then never NULL again. */
|
||||
export function disposeHookLoadLines(host: AllocHost, dest: string): string[] | null {
|
||||
if (host.inlineAlloc !== true) return null;
|
||||
declareGlobal(host, `@scr_weak_dispose_hook = external global ptr`);
|
||||
const prefix = dest.replace(/^%/, "").replace(/[^-$._A-Za-z0-9]/g, "_");
|
||||
return threadLocalState(host, prefix, [` ${dest} = load ptr, ptr @scr_weak_dispose_hook`]);
|
||||
}
|
||||
|
||||
/** Worker executables: the calling script thread's ScrThreadHot block into
|
||||
* `dest`. Its address also identifies the thread (the owner word of a
|
||||
* lifetime-region chunk; llvm/regions.ts). */
|
||||
@@ -325,20 +336,56 @@ function allocLines(
|
||||
return lines;
|
||||
}
|
||||
|
||||
/* ── dispose observers ────────────────────────────────────────────────
|
||||
* The runtime's dispose hook (scr_weak_dispose_hook) has two clients: weak
|
||||
* collections, whose keys can be any object, and the kept sources of
|
||||
* checked records (scr_record_source_note and the JSON schema parser),
|
||||
* keyed by closed records. A program without either can never install the
|
||||
* hook, so its frees need not test it. Whether the program has them is
|
||||
* known only once every helper is emitted (dynCheck builders and JSON
|
||||
* schemas are requested lazily), so the emitted frees and record writers
|
||||
* test these two module constants, which the emitter defines last
|
||||
* (LlvmEmitter.emitParts) and LLVM folds away. */
|
||||
|
||||
/** True when the program constructs a WeakMap or WeakSet. */
|
||||
export const WEAK_KEYS_GATE = "@sc_weak_keys";
|
||||
/** True when a checked conversion can keep a record's undeclared source. */
|
||||
export const RECORD_SOURCES_GATE = "@sc_record_sources";
|
||||
|
||||
/** Which dispose observers an object type can have: weak-collection keys
|
||||
* only, or (closed record layouts) kept record sources too. */
|
||||
export type FreeObservers = "weak" | "weak-or-source";
|
||||
|
||||
/**
|
||||
* Lines that free the object `%o` after its members were released: the
|
||||
* RC-audit free note, the weak-collection dispose hook, and the block —
|
||||
* `scr_cyc_free` for collector-headered objects, `scr_rt_free` otherwise.
|
||||
* Labels use the `sf.` prefix; the lines end in an open block.
|
||||
* RC-audit free note, the dispose hook (when the program can observe this
|
||||
* type's disposals), and the block — `scr_cyc_free` for collector-headered
|
||||
* objects, `scr_rt_free` otherwise. Labels use the `sf.` prefix; the lines
|
||||
* end in an open block.
|
||||
*/
|
||||
export function emitObjectFree(host: AllocHost, cyc: boolean): string[] {
|
||||
const lines = threadLocalState(host, "sf", freeLines(host, cyc));
|
||||
export function emitObjectFree(
|
||||
host: AllocHost,
|
||||
cyc: boolean,
|
||||
observers: FreeObservers = "weak",
|
||||
): string[] {
|
||||
const lines = threadLocalState(host, "sf", freeLines(host, cyc, observers));
|
||||
if (host.objectAudit === false) return lines.filter((line) => line !== FREE_NOTE);
|
||||
host.declare(`declare void @scr_obj_free_note()`);
|
||||
return lines;
|
||||
}
|
||||
|
||||
function freeLines(host: AllocHost, cyc: boolean): string[] {
|
||||
/** Lines that leave in `%<prefix>.obs` whether the program can observe a
|
||||
* disposal of this type (folds to a constant). */
|
||||
function observedLines(prefix: string, observers: FreeObservers): string[] {
|
||||
if (observers === "weak") return [` %${prefix}.obs = load i1, ptr ${WEAK_KEYS_GATE}`];
|
||||
return [
|
||||
` %${prefix}.obsw = load i1, ptr ${WEAK_KEYS_GATE}`,
|
||||
` %${prefix}.obss = load i1, ptr ${RECORD_SOURCES_GATE}`,
|
||||
` %${prefix}.obs = or i1 %${prefix}.obsw, %${prefix}.obss`,
|
||||
];
|
||||
}
|
||||
|
||||
function freeLines(host: AllocHost, cyc: boolean, observers: FreeObservers): string[] {
|
||||
if (host.inlineAlloc !== true) {
|
||||
if (cyc) {
|
||||
host.declare(`declare void @scr_cyc_free(ptr)`);
|
||||
@@ -348,7 +395,12 @@ function freeLines(host: AllocHost, cyc: boolean): string[] {
|
||||
host.declare(`declare void @scr_weak_dispose(ptr)`);
|
||||
return [
|
||||
` call void @scr_obj_free_note()`,
|
||||
...observedLines("sf", observers),
|
||||
` br i1 %sf.obs, label %sf.observe, label %sf.block`,
|
||||
`sf.observe:`,
|
||||
` call void @scr_weak_dispose(ptr %o)`,
|
||||
` br label %sf.block`,
|
||||
`sf.block:`,
|
||||
` call void @scr_rt_free(ptr %o)`,
|
||||
];
|
||||
}
|
||||
@@ -356,11 +408,14 @@ function freeLines(host: AllocHost, cyc: boolean): string[] {
|
||||
host.declare(`declare void @scr_rt_free(ptr)`);
|
||||
declareGlobal(host, `@scr_weak_dispose_hook = external global ptr`);
|
||||
const lines = [
|
||||
...observedLines("sf", observers),
|
||||
` br i1 %sf.obs, label %sf.hooked, label %sf.block`,
|
||||
`sf.hooked:`,
|
||||
` %sf.hook = load ptr, ptr @scr_weak_dispose_hook`,
|
||||
` %sf.has = icmp ne ptr %sf.hook, null`,
|
||||
` %sf.hasx = call i1 @llvm.expect.i1(i1 %sf.has, i1 false)`,
|
||||
` br i1 %sf.hasx, label %sf.observe, label %sf.block`,
|
||||
`sf.observe: ; weak collections observe the disposal`,
|
||||
`sf.observe: ; a weak collection or a kept record source observes the disposal`,
|
||||
` call void %sf.hook(ptr %o)`,
|
||||
` br label %sf.block`,
|
||||
`sf.block:`,
|
||||
|
||||
@@ -47,7 +47,8 @@ interface DenseGuard {
|
||||
* then extends the length over hole states exactly the same way). An index
|
||||
* below the hole-free prefix bound branches to `filledLabel` first; both
|
||||
* that block and the returned (state-checked) dense block may use the
|
||||
* guard's values. */
|
||||
* guard's values. A `proven` index is known to lie below the bound (a dense
|
||||
* array loop's fast copy, dense-array-loops.ts) and always takes it. */
|
||||
function emitDenseGuard(
|
||||
host: LlvmEmitterContext,
|
||||
array: string,
|
||||
@@ -56,6 +57,7 @@ function emitDenseGuard(
|
||||
slow: string,
|
||||
requireBelowLen: boolean,
|
||||
filledLabel: string,
|
||||
proven = false,
|
||||
): DenseGuard {
|
||||
const B = host.B;
|
||||
const size = host.sizeType;
|
||||
@@ -137,8 +139,10 @@ function emitDenseGuard(
|
||||
B.line(`${offset} = trunc i64 ${wide} to i32`);
|
||||
}
|
||||
// The bound never exceeds the length or the dense capacity.
|
||||
let inFilled = B.tmp();
|
||||
B.line(`${inFilled} = icmp ult i64 ${wide}, ${filledWide}`);
|
||||
// The proof covers the counter's integer value only.
|
||||
const below = proven && integral === null;
|
||||
let inFilled = below ? "true" : B.tmp();
|
||||
if (!below) B.line(`${inFilled} = icmp ult i64 ${wide}, ${filledWide}`);
|
||||
if (integral) {
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = and i1 ${integral}, ${inFilled}`);
|
||||
@@ -272,6 +276,7 @@ export function emitDenseArrayGet(
|
||||
element: IrType,
|
||||
getter: string,
|
||||
retain: boolean,
|
||||
proven = false,
|
||||
): string {
|
||||
const B = host.B;
|
||||
const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
|
||||
@@ -285,7 +290,7 @@ export function emitDenseArrayGet(
|
||||
const slow = B.newLabel("arr.get.slow"),
|
||||
join = B.newLabel("arr.get.join"),
|
||||
valueLabel = B.newLabel("arr.get.value");
|
||||
const guard = emitDenseGuard(host, array, index, indexExpr, slow, true, valueLabel);
|
||||
const guard = emitDenseGuard(host, array, index, indexExpr, slow, true, valueLabel, proven);
|
||||
const state = loadState(host, guard);
|
||||
const present = B.tmp();
|
||||
B.line(`${present} = icmp eq i8 ${state}, ${SCR_ARR_VALUE}`);
|
||||
@@ -331,6 +336,7 @@ export function emitDenseArrayNumber(
|
||||
array: string,
|
||||
index: LlValue,
|
||||
indexExpr: IrExpr | undefined,
|
||||
proven = false,
|
||||
): string {
|
||||
return emitDenseArrayGet(
|
||||
host,
|
||||
@@ -341,6 +347,7 @@ export function emitDenseArrayNumber(
|
||||
{ kind: "f64" },
|
||||
"scr_arr_get_number",
|
||||
false,
|
||||
proven,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -399,6 +406,7 @@ export function emitDenseArraySet(
|
||||
acc: ArrayAccess,
|
||||
element: IrType,
|
||||
value: string,
|
||||
proven = false,
|
||||
): void {
|
||||
const B = host.B;
|
||||
const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
|
||||
@@ -413,7 +421,7 @@ export function emitDenseArraySet(
|
||||
const slow = B.newLabel("arr.set.slow"),
|
||||
join = B.newLabel("arr.set.join"),
|
||||
filledLabel = B.newLabel("arr.set.filled");
|
||||
const guard = emitDenseGuard(host, array, index, indexExpr, slow, false, filledLabel);
|
||||
const guard = emitDenseGuard(host, array, index, indexExpr, slow, false, filledLabel, proven);
|
||||
const { offset, len } = guard;
|
||||
const storeValue = (valuePtr: string): void => {
|
||||
if (acc === "f64") {
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { BOOL, F64, arrayOf, type IrExpr, type IrLocal, type IrStmt } from "../../ir/ir.js";
|
||||
import {
|
||||
countedDenseShape,
|
||||
matchDenseArrayLoop,
|
||||
type DenseLoopShape,
|
||||
} from "./dense-array-loops.js";
|
||||
|
||||
const loc = { file: "dense-array-loops.ts", start: 0, end: 0 };
|
||||
const ARR = arrayOf(F64);
|
||||
const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc });
|
||||
const ref = (localId: string): IrExpr => ({ kind: "varRef", localId, type: F64, loc });
|
||||
const arr = (localId: string): IrExpr & { kind: "varRef" } => ({
|
||||
kind: "varRef",
|
||||
localId,
|
||||
type: ARR,
|
||||
loc,
|
||||
});
|
||||
const method = (
|
||||
receiver: string,
|
||||
name: Extract<IrExpr, { kind: "arrIntrinsic" }>["method"],
|
||||
args: IrExpr[] = [],
|
||||
): IrExpr => ({
|
||||
kind: "arrIntrinsic",
|
||||
method: name,
|
||||
receiver: arr(receiver),
|
||||
args,
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
const read = (receiver: string, index = ref("i")): IrExpr => method(receiver, "getNumber", [index]);
|
||||
const write = (receiver: string, value: IrExpr, index = ref("i")): IrStmt => ({
|
||||
kind: "arraySet",
|
||||
arr: arr(receiver),
|
||||
index,
|
||||
value,
|
||||
loc,
|
||||
});
|
||||
const locals = new Map<string, IrLocal>([
|
||||
["i", { id: "i", name: "i", type: F64, mutable: true }],
|
||||
["a", { id: "a", name: "a", type: ARR, mutable: false }],
|
||||
["b", { id: "b", name: "b", type: ARR, mutable: true }],
|
||||
["c", { id: "c", name: "c", type: ARR, mutable: false }],
|
||||
]);
|
||||
|
||||
/** for (let i = 0; i < a.length; i++) body */
|
||||
function loop(body: IrStmt[]): IrStmt & { kind: "for" } {
|
||||
return {
|
||||
kind: "for",
|
||||
loc,
|
||||
init: { kind: "varDecl", localId: "i", init: num(0), loc },
|
||||
cond: { kind: "bin", op: "<", left: ref("i"), right: method("a", "length"), type: BOOL, loc },
|
||||
update: {
|
||||
kind: "assign",
|
||||
localId: "i",
|
||||
value: { kind: "bin", op: "+", left: ref("i"), right: num(1), type: F64, loc },
|
||||
loc,
|
||||
},
|
||||
body,
|
||||
};
|
||||
}
|
||||
const shape: DenseLoopShape = {
|
||||
counter: "i",
|
||||
length: arr("a"),
|
||||
range: { min: 0, max: 4294967294 },
|
||||
};
|
||||
const match = (body: IrStmt[], initialized = new Set(["a", "b"])) =>
|
||||
matchDenseArrayLoop(loop(body), shape, locals, new Set(), initialized);
|
||||
|
||||
test("arrays indexed at the counter of a length-preserving loop get an int32 fast copy", () => {
|
||||
const scale = write("b", {
|
||||
kind: "bin",
|
||||
op: "*",
|
||||
left: read("a"),
|
||||
right: num(2),
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
const dense = match([scale]);
|
||||
expect([...dense!.arrays.keys()].sort()).toEqual(["a", "b"]);
|
||||
expect(dense!.range).toEqual({ min: 0, max: 2147483646 });
|
||||
// A binding not definitely assigned before the loop is not read by the guard.
|
||||
expect([...match([scale], new Set())!.arrays.keys()]).toEqual(["a"]);
|
||||
});
|
||||
|
||||
test("loops that may resize, rebind or move storage keep the ordinary copy only", () => {
|
||||
expect(match([{ kind: "exprStmt", expr: method("a", "push", [num(1)]), loc }])).toBeNull();
|
||||
expect(match([write("a", num(0), ref("unknown"))])).toBeNull();
|
||||
expect(
|
||||
match([
|
||||
{ kind: "exprStmt", expr: { kind: "call", callee: "f", args: [], type: F64, loc }, loc },
|
||||
]),
|
||||
).toBeNull();
|
||||
// A nested loop would be duplicated too.
|
||||
expect(match([{ kind: "exprStmt", expr: read("a"), loc }, loop([])])).toBeNull();
|
||||
// No access at the counter: nothing to prove.
|
||||
expect(match([{ kind: "exprStmt", expr: read("a", num(0)), loc }])).toBeNull();
|
||||
// Copying the bounding array into another binding lets it escape.
|
||||
expect(
|
||||
match([
|
||||
{ kind: "exprStmt", expr: read("a"), loc },
|
||||
{ kind: "assign", localId: "b", value: arr("a"), loc },
|
||||
]),
|
||||
).toBeNull();
|
||||
// A rebound array is not covered, but the bounding array still is.
|
||||
const rebound = match([
|
||||
{ kind: "exprStmt", expr: read("b"), loc },
|
||||
{ kind: "exprStmt", expr: read("a"), loc },
|
||||
{ kind: "assign", localId: "b", value: arr("c"), loc },
|
||||
]);
|
||||
expect([...rebound!.arrays.keys()]).toEqual(["a"]);
|
||||
});
|
||||
|
||||
test("counted shapes need a non-negative counter strictly below the length", () => {
|
||||
const ascending = (limit: IrExpr, inclusive: boolean, min = 0) =>
|
||||
countedDenseShape(loop([]), {
|
||||
localId: "i",
|
||||
start: num(min),
|
||||
limit,
|
||||
step: 1,
|
||||
inclusive,
|
||||
range: { min, max: 100 },
|
||||
});
|
||||
const minus = (offset: number): IrExpr => ({
|
||||
kind: "bin",
|
||||
op: "-",
|
||||
left: method("a", "length"),
|
||||
right: num(offset),
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
expect(ascending(method("a", "length"), false)?.length.localId).toBe("a");
|
||||
expect(ascending(minus(1), true)?.length.localId).toBe("a");
|
||||
expect(ascending(method("a", "length"), true)).toBeNull();
|
||||
expect(ascending(minus(1), false, -1)).toBeNull();
|
||||
const descending = (offset: number) =>
|
||||
countedDenseShape(loop([]), {
|
||||
localId: "i",
|
||||
start: minus(offset),
|
||||
limit: num(0),
|
||||
step: -1,
|
||||
inclusive: true,
|
||||
range: { min: 0, max: 100 },
|
||||
});
|
||||
expect(descending(1)?.length.localId).toBe("a");
|
||||
expect(descending(0)).toBeNull();
|
||||
});
|
||||
@@ -0,0 +1,215 @@
|
||||
/* Dense array loops: a versioned copy of an index loop over an array whose
|
||||
* length the loop never changes.
|
||||
*
|
||||
* `for (let i = 0; i < a.length; i++) b[i] = a[i] * 2` reads its bound from
|
||||
* `a`, and every element access at the counter normally tests the index
|
||||
* against the array's hole-free prefix bound (`filled`, see
|
||||
* dense-array-access.ts) before it touches the slot. When the loop provably
|
||||
* never changes `a.length` (ir/array-length-stability.ts) and never moves or
|
||||
* cuts the storage of the arrays it indexes, one test before the loop covers
|
||||
* every pass: if `a.length` is at most each indexed array's `filled` bound,
|
||||
* every counter value the loop can reach (at least 0, below `a.length`)
|
||||
* names a present value in dense storage. The fast copy then reads and
|
||||
* replaces those slots directly, which leaves LLVM a loop without
|
||||
* per-element branches (vectorizable where the body allows it). The same
|
||||
* test bounds `a.length` by 2^31 - 1, so the fast copy's counter is an int32.
|
||||
* Arrays longer than that are sparse in practice (a dense one would need
|
||||
* 16 GB of slots) and take the ordinary copy, as does every array with a
|
||||
* hole, present undefined or sparse storage below `a.length`. */
|
||||
|
||||
import type { IrExpr, IrLocal, IrStmt } from "../../ir/ir.js";
|
||||
import type { IntegerRange } from "../../ir/integer-ranges.js";
|
||||
import { preservesArrayLength } from "../../ir/array-length-stability.js";
|
||||
import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
|
||||
import type { LlvmEmitterContext } from "./expr-context.js";
|
||||
import { SCR_ARR_FIELD } from "./common.js";
|
||||
|
||||
export const INT32_MAX = 2147483647;
|
||||
|
||||
export interface DenseArrayLoop {
|
||||
/** The loop's counter binding. */
|
||||
counter: string;
|
||||
/** The array whose length bounds the counter. */
|
||||
length: IrExpr & { kind: "varRef" };
|
||||
/** Arrays whose accesses at the counter are proven to be inside the
|
||||
* hole-free prefix in the fast copy, with a read of each binding. */
|
||||
arrays: ReadonlyMap<string, IrExpr & { kind: "varRef" }>;
|
||||
/** The counter's range on every pass of the fast copy. */
|
||||
range: IntegerRange;
|
||||
}
|
||||
|
||||
/** The fast copy duplicates the loop; keep the duplicated code small. */
|
||||
const MAX_LOOP_NODES = 120;
|
||||
|
||||
/** How a matched integer loop bounds its counter below `length.length`. */
|
||||
export interface DenseLoopShape {
|
||||
counter: string;
|
||||
length: IrExpr & { kind: "varRef" };
|
||||
/** The counter's proven range on every pass of the ordinary loop; the
|
||||
* fast copy keeps its minimum, which must be at least 0. */
|
||||
range: IntegerRange;
|
||||
}
|
||||
|
||||
/** `length.length - c` (c an integer literal, 0 when absent). */
|
||||
function lengthOffset(e: IrExpr): { receiver: IrExpr & { kind: "varRef" }; offset: number } | null {
|
||||
let offset = 0;
|
||||
if (
|
||||
e.kind === "bin" &&
|
||||
e.op === "-" &&
|
||||
e.right.kind === "numLit" &&
|
||||
Number.isSafeInteger(e.right.value)
|
||||
) {
|
||||
offset = e.right.value;
|
||||
e = e.left;
|
||||
}
|
||||
return e.kind === "arrIntrinsic" &&
|
||||
e.method === "length" &&
|
||||
e.args.length === 0 &&
|
||||
e.receiver.kind === "varRef" &&
|
||||
e.receiver.type.kind === "array"
|
||||
? { receiver: e.receiver, offset }
|
||||
: null;
|
||||
}
|
||||
|
||||
/** The array bounding a counted loop's counter below its length: an
|
||||
* ascending loop compared below `a.length - c`, or a descending one starting
|
||||
* at `a.length - c` (c >= 1). */
|
||||
export function countedDenseShape(
|
||||
loop: IrStmt & { kind: "for" },
|
||||
counted: {
|
||||
localId: string;
|
||||
start: IrExpr;
|
||||
limit: IrExpr;
|
||||
step: number;
|
||||
inclusive: boolean;
|
||||
range: IntegerRange;
|
||||
},
|
||||
): DenseLoopShape | null {
|
||||
if (counted.range.min < 0) return null;
|
||||
if (counted.step > 0) {
|
||||
const bound = lengthOffset(counted.limit);
|
||||
if (!bound || bound.offset < (counted.inclusive ? 1 : 0)) return null;
|
||||
return { counter: counted.localId, length: bound.receiver, range: counted.range };
|
||||
}
|
||||
// The start is evaluated once, before the loop; the counter only falls.
|
||||
const start = lengthOffset(counted.start);
|
||||
if (!start || start.offset < 1 || loop.init?.kind !== "varDecl") return null;
|
||||
return { counter: counted.localId, length: start.receiver, range: counted.range };
|
||||
}
|
||||
|
||||
/** Match the fast copy of a loop of the given shape. `initialized` holds the
|
||||
* bindings definitely assigned when the loop starts; `unaliased` the local
|
||||
* arrays no other binding holds. */
|
||||
export function matchDenseArrayLoop(
|
||||
loop: IrStmt & { kind: "for" },
|
||||
shape: DenseLoopShape,
|
||||
locals: ReadonlyMap<string, IrLocal>,
|
||||
unaliased: ReadonlySet<string>,
|
||||
initialized: ReadonlySet<string>,
|
||||
): DenseArrayLoop | null {
|
||||
const { counter, length } = shape;
|
||||
if (shape.range.min < 0 || !loop.cond) return null;
|
||||
let nodes = 0,
|
||||
nested = false;
|
||||
const candidates = new Map<string, IrExpr & { kind: "varRef" }>();
|
||||
// Arrays written, resized, or indexed other than at the counter by an
|
||||
// element write.
|
||||
const disturbed = new Set<string>();
|
||||
const access = (arr: IrExpr, index: IrExpr): void => {
|
||||
if (arr.kind === "varRef" && index.kind === "varRef" && index.localId === counter)
|
||||
candidates.set(arr.localId, arr);
|
||||
};
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
if (++nodes > MAX_LOOP_NODES) return false;
|
||||
if (e.kind === "assignExpr" || e.kind === "incDec") disturbed.add(e.localId);
|
||||
if (e.kind === "arrayGet") access(e.arr, e.index);
|
||||
if (e.kind === "arrIntrinsic") {
|
||||
if (e.method === "getNumber" && e.args[0]) access(e.receiver, e.args[0]);
|
||||
else if (e.method !== "length" && e.receiver.kind === "varRef")
|
||||
disturbed.add(e.receiver.localId);
|
||||
}
|
||||
return everyExprChild(e, expr, stmt);
|
||||
};
|
||||
const stmt = (s: IrStmt): boolean => {
|
||||
if (++nodes > MAX_LOOP_NODES) return false;
|
||||
if (s.kind === "for" || s.kind === "while" || s.kind === "doWhile" || s.kind === "forOf")
|
||||
nested = true;
|
||||
if (s.kind === "assign" || s.kind === "varDecl") disturbed.add(s.localId);
|
||||
if (s.kind === "arraySet") {
|
||||
access(s.arr, s.index);
|
||||
if (s.arr.kind === "varRef" && (s.index.kind !== "varRef" || s.index.localId !== counter))
|
||||
disturbed.add(s.arr.localId);
|
||||
}
|
||||
return everyStmtChild(s, expr, stmt);
|
||||
};
|
||||
if (!loop.body.every(stmt) || !expr(loop.cond) || nested) return null;
|
||||
if (
|
||||
!preservesArrayLength(
|
||||
[loop.cond, ...loop.body],
|
||||
{ receiver: length.localId, inRange: counter },
|
||||
locals,
|
||||
unaliased,
|
||||
)
|
||||
)
|
||||
return null;
|
||||
const arrays = new Map<string, IrExpr & { kind: "varRef" }>();
|
||||
for (const [id, ref] of candidates) {
|
||||
const local = locals.get(id);
|
||||
if (
|
||||
!local ||
|
||||
local.type.kind !== "array" ||
|
||||
local.boxed ||
|
||||
local.tdz ||
|
||||
disturbed.has(id) ||
|
||||
(id !== length.localId && !initialized.has(id))
|
||||
)
|
||||
continue;
|
||||
arrays.set(id, ref);
|
||||
}
|
||||
if (arrays.size === 0) return null;
|
||||
return {
|
||||
counter,
|
||||
length,
|
||||
arrays,
|
||||
range: { min: shape.range.min, max: Math.min(shape.range.max, INT32_MAX - 1) },
|
||||
};
|
||||
}
|
||||
|
||||
/** The fast copy's entry test: the bounding length is at most 2^31 - 1 and
|
||||
* at most every indexed array's hole-free prefix bound. */
|
||||
export function emitDenseLoopGuard(host: LlvmEmitterContext, loop: DenseArrayLoop): string {
|
||||
const B = host.B;
|
||||
const size = host.sizeType;
|
||||
const header = (array: string, field: number): string => {
|
||||
const ptr = B.tmp();
|
||||
B.line(`${ptr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 ${field}`);
|
||||
host.markMemoryPointer(ptr, "array:header");
|
||||
return ptr;
|
||||
};
|
||||
const receiver = host.emitStableReceiver(loop.length, []);
|
||||
const lenPtr = header(receiver.name, 1),
|
||||
len = B.tmp(),
|
||||
small = B.tmp();
|
||||
B.line(
|
||||
`${len} = load ${size}, ptr ${lenPtr}${host.fieldAliasAttachment(lenPtr)}${host.arrayLengthRange}`,
|
||||
);
|
||||
B.line(`${small} = icmp ule ${size} ${len}, ${INT32_MAX}`);
|
||||
let valid = small;
|
||||
for (const [id, ref] of loop.arrays) {
|
||||
const array = id === loop.length.localId ? receiver : host.emitStableReceiver(ref, []);
|
||||
const filledPtr = header(array.name, SCR_ARR_FIELD.filled),
|
||||
filled = B.tmp(),
|
||||
covered = B.tmp(),
|
||||
both = B.tmp();
|
||||
B.line(`${filled} = load i32, ptr ${filledPtr}${host.fieldAliasAttachment(filledPtr)}`);
|
||||
let bound = filled;
|
||||
if (size === "i64") {
|
||||
bound = B.tmp();
|
||||
B.line(`${bound} = zext i32 ${filled} to i64`);
|
||||
}
|
||||
B.line(`${covered} = icmp ule ${size} ${len}, ${bound}`);
|
||||
B.line(`${both} = and i1 ${valid}, ${covered}`);
|
||||
valid = both;
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
@@ -84,6 +84,16 @@ const DYN_HANDLE_TAG_NUM: Record<string, number> = {
|
||||
child: 16,
|
||||
fileHandle: 17,
|
||||
dir: 44,
|
||||
cryptoHash: 52,
|
||||
cryptoHmac: 53,
|
||||
};
|
||||
|
||||
/** Handle kinds whose runtime unit boxes them through its own function,
|
||||
* which installs the tag's ops on first use (scr_crypto.h). Programs that
|
||||
* never box one of these values do not link its dynamic members. */
|
||||
const DYN_HANDLE_BOX_FN: Record<string, string> = {
|
||||
cryptoHash: "scr_crypto_hash_to_dyn",
|
||||
cryptoHmac: "scr_crypto_hash_to_dyn",
|
||||
};
|
||||
|
||||
export const DYN_KIND = {
|
||||
@@ -1106,6 +1116,10 @@ export class LlDyn {
|
||||
private readonly jsonSchemas = new Map<string, string>();
|
||||
private readonly jsonParsers = new Map<string, string>();
|
||||
private readonly recordNameTables = new Map<string, { names: string; lens: string }>();
|
||||
/** Some emitted helper can keep a record's undeclared source: a checked
|
||||
* record builder that notes it, or a JSON schema of a closed record
|
||||
* (alloc.ts RECORD_SOURCES_GATE). */
|
||||
keepsRecordSources = false;
|
||||
|
||||
/** Targets the one-pass parser (scr_json_parse_schema) can build
|
||||
* directly: number, boolean and string leaves inside arrays and records
|
||||
@@ -1217,12 +1231,14 @@ export class LlDyn {
|
||||
`llvm emitter bug: JSON schema of unknown shape ${t.shapeId}`,
|
||||
);
|
||||
const struct = mangleRecordStruct(t.shapeId);
|
||||
const index = new Map(shape.fields.map((f, i) => [f.name, i + 1]));
|
||||
const base = host.nullableUnions.fieldBase(t.shapeId);
|
||||
const index = new Map(shape.fields.map((f, i) => [f.name, i + base]));
|
||||
// The runtime names kept-source keys by this index (record-sources.ts).
|
||||
const order = recordSourceFieldOrder(shape);
|
||||
if (recordMayKeepSource(shape)) this.keepsRecordSources = true;
|
||||
const fieldTy = `{ ptr, ${S}, ${S}, ptr }`;
|
||||
const entries = order.map((fieldName) => {
|
||||
const field = shape.fields[index.get(fieldName)! - 1]!;
|
||||
const field = shape.fields[index.get(fieldName)! - base]!;
|
||||
const sub = this.jsonSchema(field.type);
|
||||
const offset = `ptrtoint (ptr getelementptr (%${struct}, ptr null, i64 0, i32 ${index.get(fieldName)}) to ${S})`;
|
||||
return `${fieldTy} { ptr ${host.cstr(fieldName)}, ${S} ${Buffer.byteLength(fieldName, "utf8")}, ${S} ${offset}, ptr @${sub} }`;
|
||||
@@ -1240,7 +1256,7 @@ export class LlDyn {
|
||||
const overflow = shape.indexValue
|
||||
? {
|
||||
elem: `@${this.jsonSchema(shape.indexValue)}`,
|
||||
offset: `ptrtoint (ptr getelementptr (%${struct}, ptr null, i64 0, i32 ${shape.fields.length + 1}) to ${S})`,
|
||||
offset: `ptrtoint (ptr getelementptr (%${struct}, ptr null, i64 0, i32 ${shape.fields.length + base}) to ${S})`,
|
||||
}
|
||||
: { elem: "null", offset: "0" };
|
||||
body = `${S} 3, ${S} ${entries.length}, ptr ${fields}, ptr ${overflow.elem}, ptr @${mangleRecordNew(t.shapeId)}, ptr null, ptr ${vAdapters(host, t).release}, ptr null, ${S} ${overflow.offset}`;
|
||||
@@ -1547,18 +1563,19 @@ export class LlDyn {
|
||||
B.line(`call void @scr_arr_outline(ptr ${cached})`);
|
||||
B.line(`call void @scr_arr_outline(ptr ${checked})`);
|
||||
}
|
||||
const base = t.kind === "record" ? host.nullableUnions.fieldBase(t.shapeId) : 1;
|
||||
const members =
|
||||
t.kind === "record"
|
||||
? [
|
||||
...shape!.fields.map((field, index) => ({
|
||||
index: index + 1,
|
||||
index: index + base,
|
||||
type: llFieldType(field.type, host.inlineUnions.record(field.type)),
|
||||
name: field.name,
|
||||
})),
|
||||
...(shape!.indexValue
|
||||
? [
|
||||
{
|
||||
index: shape!.fields.length + 1,
|
||||
index: shape!.fields.length + base,
|
||||
type: "ptr" as const,
|
||||
name: "[key: string] overflow",
|
||||
},
|
||||
@@ -1819,7 +1836,8 @@ export class LlDyn {
|
||||
`llvm emitter bug: dynCheck of unknown shape ${t.shapeId}`,
|
||||
);
|
||||
const struct = mangleRecordStruct(t.shapeId);
|
||||
const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + 1]));
|
||||
const base = host.nullableUnions.fieldBase(t.shapeId);
|
||||
const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + base]));
|
||||
const releaseR = (): void => {
|
||||
B.line(`call void ${releaseSym(host, t)}(ptr %r0)`);
|
||||
};
|
||||
@@ -1978,6 +1996,7 @@ export class LlDyn {
|
||||
// that has them stays beside the record so its reflective
|
||||
// surfaces still observe them (scr_record_source_note).
|
||||
if (recordMayKeepSource(shape)) {
|
||||
this.keepsRecordSources = true;
|
||||
const names = this.recordNameTable(t.shapeId, shape);
|
||||
host.declare(
|
||||
`declare void @scr_record_source_note(ptr, ptr, ptr, ptr, ${host.sizeType})`,
|
||||
@@ -1996,7 +2015,7 @@ export class LlDyn {
|
||||
const ovfp = B.tmp();
|
||||
const ovf = B.tmp();
|
||||
B.line(
|
||||
`${ovfp} = getelementptr inbounds %${struct}, ptr %r0, i64 0, i32 ${shape.fields.length + 1}`,
|
||||
`${ovfp} = getelementptr inbounds %${struct}, ptr %r0, i64 0, i32 ${shape.fields.length + base}`,
|
||||
);
|
||||
B.line(`${ovf} = load ptr, ptr ${ovfp} ; overflow map`);
|
||||
if (shape.fields.length === 0) {
|
||||
@@ -2578,7 +2597,8 @@ export class LlDyn {
|
||||
if (!shape)
|
||||
throw new InternalCompilerError(`llvm emitter bug: to-dyn of unknown shape ${t.shapeId}`);
|
||||
const struct = mangleRecordStruct(t.shapeId);
|
||||
const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + 1]));
|
||||
const base = host.nullableUnions.fieldBase(t.shapeId);
|
||||
const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + base]));
|
||||
const loadFieldOf = (fname: string, ft: IrType): string => {
|
||||
const p = B.tmp();
|
||||
B.line(
|
||||
@@ -2702,7 +2722,7 @@ export class LlDyn {
|
||||
const ovfp = B.tmp();
|
||||
const ovf = B.tmp();
|
||||
B.line(
|
||||
`${ovfp} = getelementptr inbounds %${struct}, ptr %v, i64 0, i32 ${shape.fields.length + 1}`,
|
||||
`${ovfp} = getelementptr inbounds %${struct}, ptr %v, i64 0, i32 ${shape.fields.length + base}`,
|
||||
);
|
||||
B.line(`${ovf} = load ptr, ptr ${ovfp} ; overflow map`);
|
||||
const ks = B.tmp();
|
||||
@@ -2941,9 +2961,17 @@ export class LlDyn {
|
||||
// tag's installed ops.
|
||||
const h = DYN_HANDLE_KINDS.get(t.kind);
|
||||
if (h) {
|
||||
host.declare(`declare ptr @scr_dyn_new_handle(ptr, i32)`);
|
||||
const box = DYN_HANDLE_BOX_FN[t.kind];
|
||||
const r = B.tmp();
|
||||
B.line(`${r} = call ptr @scr_dyn_new_handle(ptr %v, i32 ${DYN_HANDLE_TAG_NUM[t.kind]})`);
|
||||
if (box) {
|
||||
host.declare(`declare ptr @${box}(ptr)`);
|
||||
B.line(`${r} = call ptr @${box}(ptr %v)`);
|
||||
} else {
|
||||
host.declare(`declare ptr @scr_dyn_new_handle(ptr, i32)`);
|
||||
B.line(
|
||||
`${r} = call ptr @scr_dyn_new_handle(ptr %v, i32 ${DYN_HANDLE_TAG_NUM[t.kind]})`,
|
||||
);
|
||||
}
|
||||
B.terminate(`ret ptr ${r}`);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,14 @@ import { type ArraySearchKind, emitArraySearchHelpers } from "./array-search.js"
|
||||
import { emitStringInputs } from "./string-lifetimes.js";
|
||||
import { emitLlvmLayouts } from "./layouts.js";
|
||||
import { compileRegexMatcher, type RegexMatcherCode } from "./regex-matchers.js";
|
||||
import { CYC_BUF, CYC_HDR, FIRST_EMITTED_CYC_KIND, inlineAllocSupported } from "./alloc.js";
|
||||
import {
|
||||
CYC_BUF,
|
||||
CYC_HDR,
|
||||
FIRST_EMITTED_CYC_KIND,
|
||||
inlineAllocSupported,
|
||||
RECORD_SOURCES_GATE,
|
||||
WEAK_KEYS_GATE,
|
||||
} from "./alloc.js";
|
||||
import { hasLoneSurrogate, llvmBytes as llBytes, wtf8Bytes } from "../literals.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { createHash } from "node:crypto";
|
||||
@@ -50,11 +57,19 @@ import {
|
||||
VOID,
|
||||
} from "../../ir/ir.js";
|
||||
import { moduleRuntimeFeatures, moduleUsesMockTimers } from "../../ir/runtime-features.js";
|
||||
import { captureFreeFunctions, observesStacks } from "../stack-observations.js";
|
||||
import {
|
||||
matchIntegerArrayForLoop,
|
||||
matchIntegerBytesForLoop,
|
||||
matchIntegerCountedForLoop,
|
||||
} from "../../ir/integer-loops.js";
|
||||
import { findUnaliasedLocalArrays } from "../../ir/array-length-stability.js";
|
||||
import {
|
||||
countedDenseShape,
|
||||
emitDenseLoopGuard,
|
||||
matchDenseArrayLoop,
|
||||
type DenseArrayLoop,
|
||||
} from "./dense-array-loops.js";
|
||||
import { emitCountedLoopLimit } from "./counted-loops.js";
|
||||
import {
|
||||
emitSafeIntegerCheck,
|
||||
@@ -151,6 +166,7 @@ import {
|
||||
mangleFnClosure,
|
||||
mangleFunction,
|
||||
mangleGenDrop,
|
||||
mangleGenSourceClose,
|
||||
mangleGenSpawn,
|
||||
mangleGlobal,
|
||||
mangleLocal,
|
||||
@@ -229,7 +245,10 @@ import { findAppendedArrayLiterals } from "./appended-array-literals.js";
|
||||
import { AmbientReceiverReaders, findConstantCallbacks } from "./constant-callbacks.js";
|
||||
import {
|
||||
findScalarStringSlices,
|
||||
findScratchStringSlices,
|
||||
findLentStringParameters,
|
||||
emitStringSliceSnapshot,
|
||||
type LentStringParameters,
|
||||
type StringSliceSnapshot,
|
||||
} from "./string-slices.js";
|
||||
import { LazyCaptures } from "./lazy-captures.js";
|
||||
@@ -383,6 +402,7 @@ import {
|
||||
elemAccess,
|
||||
emitInlineRcHelpers,
|
||||
emitNullableRetainWrappers,
|
||||
emitShapeUnionHelpers,
|
||||
FN_ATTRS,
|
||||
inlineRcDecls,
|
||||
type InlineRcMode,
|
||||
@@ -623,6 +643,10 @@ export class LlEmitter {
|
||||
private loopArrayBorrows: ReadonlySet<IrStmt> = new Set();
|
||||
private scalarStringSlices = new Map<string, IrExpr & { kind: "strIntrinsic" }>();
|
||||
readonly stringSlices = new Map<string, StringSliceSnapshot>();
|
||||
/** Slices written into stack buffers (findScratchStringSlices). */
|
||||
scratchStringSlices: ReadonlySet<IrExpr> = new Set();
|
||||
/** String parameters a caller may lend scratch slices to. */
|
||||
private lentStringParameters: LentStringParameters = new Map();
|
||||
private localStackUnions = new Map<string, IrExpr & { kind: "unionWrap" }>();
|
||||
private localStackObjects = new Map<string, StackObjectSource>();
|
||||
private reassignedStackObjects = new Set<string>();
|
||||
@@ -674,6 +698,21 @@ export class LlEmitter {
|
||||
return this.sizeType === "i64" ? ", !range !{i64 0, i64 4294967296}" : "";
|
||||
}
|
||||
|
||||
/** Whether an element access at `index` of `arr` lies in the hole-free
|
||||
* prefix: the counter of a dense array loop's fast copy indexing one of
|
||||
* the arrays its entry test covered. */
|
||||
provenDenseIndex(arr: IrExpr, index: IrExpr): boolean {
|
||||
const loop = this.denseLoop;
|
||||
return (
|
||||
loop !== null &&
|
||||
arr.kind === "varRef" &&
|
||||
loop.arrays.has(arr.localId) &&
|
||||
index.kind === "varRef" &&
|
||||
index.localId === loop.counter &&
|
||||
this.integerLoopBindings.has(loop.counter)
|
||||
);
|
||||
}
|
||||
|
||||
fieldAliasAttachment(ptr: string): string {
|
||||
const tag = this.fieldPointerTags.get(ptr);
|
||||
return tag === undefined ? "" : `, !tbaa !${tag}`;
|
||||
@@ -1030,6 +1069,14 @@ export class LlEmitter {
|
||||
* immortal from construction (classes.ts programLifetimeClass). */
|
||||
readonly programLifetime: ReadonlySet<string>;
|
||||
private numericLocals = new Map<string, IrLocal>();
|
||||
/** Local arrays of the current function that no other binding can hold
|
||||
* (ir/array-length-stability.ts). */
|
||||
private unaliasedArrays: ReadonlySet<string> = new Set();
|
||||
/** Dense array loops (dense-array-loops.ts) run in this function. */
|
||||
private denseLoopsEnabled = false;
|
||||
private denseLoopEntries: ReturnType<typeof findInitializedByteLoopBindings> = new Map();
|
||||
/** The fast copy of a dense array loop being emitted. */
|
||||
private denseLoop: DenseArrayLoop | null = null;
|
||||
private captureIds = new Set<string>();
|
||||
/** Active induction bindings retain their width independently of size_t. */
|
||||
integerLoopBindings = new Map<
|
||||
@@ -1134,19 +1181,15 @@ export class LlEmitter {
|
||||
readonly chainSlots = new Map<string, LlValue>();
|
||||
logArgSlots = 0;
|
||||
private readonly stackTraces: boolean;
|
||||
/** Stack-trace mode: functions no capture can include (no frame push). */
|
||||
private readonly unframedFunctions: ReadonlySet<string>;
|
||||
|
||||
constructor(
|
||||
readonly mod: IrModule,
|
||||
options: LlvmTargetOptions,
|
||||
) {
|
||||
this.stackTraces = mod.functions.some(
|
||||
(fn) =>
|
||||
!everyStmtList(fn.body, {
|
||||
stmt: () => true,
|
||||
expr: (expr) => !(expr.kind === "libCall" && expr.fn === "error.stack"),
|
||||
}),
|
||||
);
|
||||
this.nullableUnions = new NullableUnions(mod);
|
||||
this.stackTraces = observesStacks(mod);
|
||||
this.nullableUnions = new NullableUnions(mod, options.debugSources === undefined);
|
||||
this.mockTimers = moduleUsesMockTimers(mod);
|
||||
this.debug =
|
||||
options.debugSources === undefined
|
||||
@@ -1227,6 +1270,9 @@ export class LlEmitter {
|
||||
// which Windows lacks.
|
||||
this.stacklessAsyncAllowed = this.stacklessGenerators && mod.lib === undefined && !windows;
|
||||
this.publishedTypes = computePublishedTypes(mod);
|
||||
this.unframedFunctions = this.stackTraces
|
||||
? captureFreeFunctions(mod, this.stackChecks, this.publishedTypes)
|
||||
: new Set();
|
||||
this.immutableFields = computeImmutableFields(mod);
|
||||
this.constructorFunctions = new Set((mod.classes ?? []).map((c) => `%${c.name}.constructor`));
|
||||
this.indirectMayThrow = mt.indirect || mod.workers === true;
|
||||
@@ -1245,8 +1291,10 @@ export class LlEmitter {
|
||||
// Optional array reads also cover nullable unions: emitLocalArrayRead
|
||||
// then yields the (borrowed or owned) element pointer, not a stack box.
|
||||
// Arrays of such a union are read the same way.
|
||||
this.optionalArrayReads = new OptionalArrayReads(this.fnByName, this.unionsById, (id) =>
|
||||
this.nullableUnions.has(id),
|
||||
this.optionalArrayReads = new OptionalArrayReads(
|
||||
this.fnByName,
|
||||
this.unionsById,
|
||||
(id) => this.nullableUnions.single(id) !== null,
|
||||
);
|
||||
this.unionSources = {
|
||||
unions: this.boxedUnionsById,
|
||||
@@ -1299,7 +1347,8 @@ export class LlEmitter {
|
||||
(type) =>
|
||||
this.plainReference(type) &&
|
||||
type.kind !== "string" &&
|
||||
(type.kind !== "union" || this.nullableUnions.get(type.unionId)?.arm.kind !== "string"),
|
||||
(type.kind !== "union" ||
|
||||
this.nullableUnions.single(type.unionId)?.arm.kind !== "string"),
|
||||
this.walkBorrowsByFunction.get(fn.name) ?? new Set(),
|
||||
);
|
||||
if (rebound.size === 0) continue;
|
||||
@@ -1325,6 +1374,8 @@ export class LlEmitter {
|
||||
return shape !== undefined && !recordDynKeepsIdentity(shape);
|
||||
},
|
||||
);
|
||||
this.lentStringParameters =
|
||||
this.debug === null ? findLentStringParameters(this.fnByName, this.callLifetimes) : new Map();
|
||||
this.constantCallbacks = findConstantCallbacks(mod, this.callLifetimes);
|
||||
// Integer clones signal a stack overflow through their result, which
|
||||
// needs the single-threaded guard; worker programs also poll for
|
||||
@@ -1350,6 +1401,27 @@ export class LlEmitter {
|
||||
const traced = computeTraced(mod);
|
||||
this.tracedShapes = traced.shapes;
|
||||
this.tracedUnions = traced.unions;
|
||||
// A shape union's value is whichever record it holds, and the collector
|
||||
// visits it as that record: when one arm carries a collector header,
|
||||
// every arm must (nullable-unions.ts). Unions that share an arm pass a
|
||||
// header on to each other's arms, so repeat until no union adds one.
|
||||
const shapeUnions = this.nullableUnions.allShapeUnions();
|
||||
for (let changed = true; changed;) {
|
||||
changed = false;
|
||||
for (const union of shapeUnions) {
|
||||
if (
|
||||
!this.tracedUnions.has(union.unionId) &&
|
||||
!union.arms.some((arm) => this.tracedShapes.has(`record:${arm.shapeId}`))
|
||||
)
|
||||
continue;
|
||||
for (const arm of union.arms) {
|
||||
const key = `record:${arm.shapeId}`;
|
||||
if (this.tracedShapes.has(key)) continue;
|
||||
this.tracedShapes.add(key);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.shapeHost = {
|
||||
sizeType: this.sizeType,
|
||||
cycleBufOffset: this.cycleBufOffset,
|
||||
@@ -1900,7 +1972,10 @@ export class LlEmitter {
|
||||
}
|
||||
|
||||
emitParts(): string[] {
|
||||
const parts = limitInlineDynReleases(this.emitModuleParts());
|
||||
const parts = [
|
||||
...limitInlineDynReleases(this.emitModuleParts()),
|
||||
...this.disposeObserverGates(),
|
||||
];
|
||||
if (!this.executableTls) return parts;
|
||||
// A worker executable's thread-locals all live in the executable's own
|
||||
// TLS block: the program's internal ones, and the runtime's external
|
||||
@@ -1923,6 +1998,21 @@ export class LlEmitter {
|
||||
);
|
||||
}
|
||||
|
||||
/** The dispose-observer gates the emitted frees and record writers test
|
||||
* (alloc.ts): defined after every helper is emitted, because dynCheck
|
||||
* builders, JSON schemas and weak collection constructors are requested
|
||||
* lazily. */
|
||||
private disposeObserverGates(): string[] {
|
||||
const weak = [...this.decls].some(
|
||||
(d) => d.includes("@scr_weak_map_new(") || d.includes("@scr_weak_set_new("),
|
||||
);
|
||||
return [
|
||||
`${WEAK_KEYS_GATE} = internal constant i1 ${weak}`,
|
||||
`${RECORD_SOURCES_GATE} = internal constant i1 ${this.dyn.keepsRecordSources}`,
|
||||
``,
|
||||
];
|
||||
}
|
||||
|
||||
private emitModuleParts(): string[] {
|
||||
// Function bodies first (the literal/unit/fn-value tables fill as they
|
||||
// emit), then the file assembles around them — the runtime ABI’s order.
|
||||
@@ -3690,6 +3780,7 @@ export class LlEmitter {
|
||||
return [
|
||||
...late,
|
||||
...emitNullableRetainWrappers(this.shapeHost),
|
||||
...emitShapeUnionHelpers(this.shapeHost),
|
||||
...emitInlineRcHelpers(this.shapeHost),
|
||||
];
|
||||
}
|
||||
@@ -4533,6 +4624,25 @@ export class LlEmitter {
|
||||
dr.push(` call void @free(ptr %ap)`, ` ret void`, `}`, ``);
|
||||
for (const line of dr) out.push(line);
|
||||
|
||||
// An iterator helper's return() before its first next() closes the
|
||||
// source generator in its first argument slot.
|
||||
const closesSource = fn.generator.closesSource === true;
|
||||
if (closesSource) {
|
||||
const field = lifted ? 1 : 0;
|
||||
this.declare(`declare void @scr_gen_close(ptr)`);
|
||||
this.declare(`declare void @scr_gen_set_unstarted_return(ptr, ptr)`);
|
||||
out.push(
|
||||
`define internal void @${mangleGenSourceClose(fn.name)}(ptr %ap) ${FN_ATTRS} {`,
|
||||
`entry:`,
|
||||
` %sp = getelementptr inbounds %${pack}, ptr %ap, i64 0, i32 ${field}`,
|
||||
` %sv = load ptr, ptr %sp`,
|
||||
` call void @scr_gen_close(ptr %sv)`,
|
||||
` ret void`,
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
}
|
||||
|
||||
// Spawn wrapper: pack the args (+1 moves in), allocate the
|
||||
// SUSPENDED fiber — nothing runs until the first .next().
|
||||
if (lifted) {
|
||||
@@ -4549,6 +4659,11 @@ export class LlEmitter {
|
||||
settleAsync === null
|
||||
? ` %gg = call ptr @${genNew}(ptr @${mangleTrampoline(fn.name)}, ptr %ap, ptr @${mangleGenDrop(fn.name)})`
|
||||
: ` %gg = call ptr @scr_async_gen_new(ptr @${mangleTrampoline(fn.name)}, ptr %ap, ptr @${mangleGenDrop(fn.name)}, ptr @${settleAsync})`,
|
||||
...(closesSource
|
||||
? [
|
||||
` call void @scr_gen_set_unstarted_return(ptr %gg, ptr @${mangleGenSourceClose(fn.name)})`,
|
||||
]
|
||||
: []),
|
||||
` ret ptr %gg`,
|
||||
`}`,
|
||||
``,
|
||||
@@ -5857,7 +5972,11 @@ export class LlEmitter {
|
||||
const nullable = this.nullableUnions.get(unionId);
|
||||
if (nullable) {
|
||||
// The owned payload IS the nullable union's value.
|
||||
if (tag !== nullable.refTag)
|
||||
if (
|
||||
nullable.repr === "single"
|
||||
? tag !== nullable.refTag
|
||||
: !nullable.arms.some((arm) => arm.tag === tag)
|
||||
)
|
||||
throw new InternalCompilerError(
|
||||
`llvm emitter bug: payload wrap into unit arm ${tag} of ${unionId}`,
|
||||
);
|
||||
@@ -5962,7 +6081,7 @@ export class LlEmitter {
|
||||
const index = shape.fields.findIndex((entry) => entry.name === field);
|
||||
if (index < 0)
|
||||
throw new InternalCompilerError(`llvm emitter bug: missing union field ${field}`);
|
||||
layout = "record:";
|
||||
layout = this.nullableUnions.fieldBase(arm.shapeId) === 2 ? "record:tagged;" : "record:";
|
||||
for (let i = 0; i <= index; i++) {
|
||||
const type = shape.fields[i]!.type;
|
||||
layout += `${llFieldType(type, this.inlineUnions.record(type))};`;
|
||||
@@ -5998,7 +6117,7 @@ export class LlEmitter {
|
||||
);
|
||||
const p = this.B.tmp();
|
||||
this.B.line(
|
||||
`${p} = getelementptr inbounds %${mangleRecordStruct(shapeId)}, ptr ${objName}, i64 0, i32 ${idx + 1}`,
|
||||
`${p} = getelementptr inbounds %${mangleRecordStruct(shapeId)}, ptr ${objName}, i64 0, i32 ${idx + this.nullableUnions.fieldBase(shapeId)}`,
|
||||
);
|
||||
const type = shape.fields[idx]!.type;
|
||||
return { ptr: p, type, inline: this.inlineUnions.record(type) };
|
||||
@@ -6012,7 +6131,7 @@ export class LlEmitter {
|
||||
const p = this.B.tmp();
|
||||
const v = this.B.tmp();
|
||||
this.B.line(
|
||||
`${p} = getelementptr inbounds %${mangleRecordStruct(shapeId)}, ptr ${objName}, i64 0, i32 ${shape.fields.length + 1}`,
|
||||
`${p} = getelementptr inbounds %${mangleRecordStruct(shapeId)}, ptr ${objName}, i64 0, i32 ${shape.fields.length + this.nullableUnions.fieldBase(shapeId)}`,
|
||||
);
|
||||
this.B.line(`${v} = load ptr, ptr ${p}`);
|
||||
return v;
|
||||
@@ -6675,6 +6794,7 @@ export class LlEmitter {
|
||||
const initializerBindings = this.initializerBindings.get(fn.name) ?? [];
|
||||
const numericFn = withInitializerBindings(fn, initializerBindings);
|
||||
this.numericLocals = new Map(numericFn.locals.map((l) => [l.id, l]));
|
||||
this.unaliasedArrays = findUnaliasedLocalArrays(fn);
|
||||
this.borrowedParameters.clear();
|
||||
this.referenceWrites?.enter(fn);
|
||||
this.reboundOwnerSlots.clear();
|
||||
@@ -6713,6 +6833,12 @@ export class LlEmitter {
|
||||
this.finalFieldBorrows =
|
||||
!fn.async && !fn.generator && constructorClass(fn.name) === null && this.finalFields.size > 0;
|
||||
this.byteWindowsEnabled = this.countedLoopsEnabled;
|
||||
this.denseLoopsEnabled = this.countedLoopsEnabled;
|
||||
this.denseLoop = null;
|
||||
this.denseLoopEntries =
|
||||
this.denseLoopsEnabled && numericFn.locals.some((l) => l.type.kind === "array")
|
||||
? findInitializedByteLoopBindings(fn)
|
||||
: new Map();
|
||||
this.byteWindowEntries =
|
||||
this.byteWindowsEnabled && numericFn.locals.some((l) => l.type.kind === "bytes")
|
||||
? findInitializedByteLoopBindings(fn)
|
||||
@@ -6750,6 +6876,15 @@ export class LlEmitter {
|
||||
);
|
||||
this.scalarStringSlices =
|
||||
this.debug === null ? findScalarStringSlices(fn, this.callLifetimes) : new Map();
|
||||
this.scratchStringSlices =
|
||||
this.debug === null
|
||||
? findScratchStringSlices(
|
||||
fn,
|
||||
this.callLifetimes,
|
||||
this.scalarStringSlices,
|
||||
this.lentStringParameters,
|
||||
)
|
||||
: new Set();
|
||||
this.mapReadLifetimes = findMapReadLifetimes(fn, this.boxedUnionsById, this.callLifetimes);
|
||||
this.readNumberMapWrites = findReadNumberMapWrites(fn);
|
||||
this.mapEntryFusion = this.debug === null ? findMapEntryFusions(fn) : null;
|
||||
@@ -7040,13 +7175,39 @@ export class LlEmitter {
|
||||
// Stackful native fibers and native coroutine frames retain these frames
|
||||
// while suspended. The active exception context owns the chain, so
|
||||
// concurrent fibers isolate it.
|
||||
if (this.stackTraces && !this.wasi && fn.sourceName !== undefined) {
|
||||
this.declare(`declare void @scr_stack_enter(ptr, ptr)`);
|
||||
this.declare(`declare void @scr_stack_leave(ptr)`);
|
||||
if (
|
||||
this.stackTraces &&
|
||||
!this.wasi &&
|
||||
fn.sourceName !== undefined &&
|
||||
!this.unframedFunctions.has(fn.name)
|
||||
) {
|
||||
B.entryAllocas.push(`%source_frame = alloca { ptr, ptr }`);
|
||||
const frame = ` at ${fn.sourceName} (${fn.loc.file})`;
|
||||
B.line(`call void @scr_stack_enter(ptr %source_frame, ptr ${this.cstr(frame)})`);
|
||||
B.returnEpilogue = `call void @scr_stack_leave(ptr %source_frame)`;
|
||||
const frame = this.cstr(` at ${fn.sourceName} (${fn.loc.file})`);
|
||||
if (
|
||||
B.excCellInvariant &&
|
||||
this.mod.lib === undefined &&
|
||||
this.mod.workers !== true &&
|
||||
this.sizeType === "i64"
|
||||
) {
|
||||
// Executables link the active-cell pointer as a plain global, and a
|
||||
// synchronous body sees one cell from entry to every return: push
|
||||
// and pop the frame inline (ScrExcCell.stack is at byte 56 on
|
||||
// 64-bit targets) instead of calling the runtime twice per call.
|
||||
this.declare(`@scr_exc_active = external global ptr`);
|
||||
B.line(`%source_frame.cell = load ptr, ptr @scr_exc_active`);
|
||||
B.line(`%source_frame.chain = getelementptr inbounds i8, ptr %source_frame.cell, i64 56`);
|
||||
B.line(`%source_frame.previous = load ptr, ptr %source_frame.chain`);
|
||||
B.line(`store ptr %source_frame.previous, ptr %source_frame`);
|
||||
B.line(`%source_frame.text = getelementptr inbounds i8, ptr %source_frame, i64 8`);
|
||||
B.line(`store ptr ${frame}, ptr %source_frame.text`);
|
||||
B.line(`store ptr %source_frame, ptr %source_frame.chain`);
|
||||
B.returnEpilogue = `store ptr %source_frame.previous, ptr %source_frame.chain`;
|
||||
} else {
|
||||
this.declare(`declare void @scr_stack_enter(ptr, ptr)`);
|
||||
this.declare(`declare void @scr_stack_leave(ptr)`);
|
||||
B.line(`call void @scr_stack_enter(ptr %source_frame, ptr ${frame})`);
|
||||
B.returnEpilogue = `call void @scr_stack_leave(ptr %source_frame)`;
|
||||
}
|
||||
}
|
||||
if (this.stackChecks?.has(fn.name)) this.emitStackCheck();
|
||||
if (this.workerEntryPolls?.has(fn.name)) this.emitPendingCheck(undefined, true);
|
||||
@@ -8185,7 +8346,16 @@ export class LlEmitter {
|
||||
const acc = elemAccess(s.arr.type.elem);
|
||||
this.emitPublishedArrayGuard(arr.name, s.arr.type, 0, idx.name);
|
||||
if (acc === "ref") this.moveTemp(v);
|
||||
emitDenseArraySet(this, arr.name, idx, s.index, acc, s.arr.type.elem, v.name);
|
||||
emitDenseArraySet(
|
||||
this,
|
||||
arr.name,
|
||||
idx,
|
||||
s.index,
|
||||
acc,
|
||||
s.arr.type.elem,
|
||||
v.name,
|
||||
this.provenDenseIndex(s.arr, s.index),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "arraySetLength": {
|
||||
@@ -8632,7 +8802,12 @@ export class LlEmitter {
|
||||
(this.debug === null ? matchIntegerBytesForLoop(s, this.numericLocals) : null);
|
||||
const countedLoop =
|
||||
!integerLoop && this.countedLoopsEnabled
|
||||
? matchIntegerCountedForLoop(s, this.numericLocals, this.integerRanges)
|
||||
? matchIntegerCountedForLoop(
|
||||
s,
|
||||
this.numericLocals,
|
||||
this.integerRanges,
|
||||
this.unaliasedArrays,
|
||||
)
|
||||
: null;
|
||||
const window =
|
||||
countedLoop && this.byteWindowsEnabled
|
||||
@@ -8645,6 +8820,49 @@ export class LlEmitter {
|
||||
this.byteWindowEntries.get(s) ?? new Set(),
|
||||
)
|
||||
: null;
|
||||
const denseShape = !this.denseLoopsEnabled
|
||||
? null
|
||||
: integerArrayLoop
|
||||
? {
|
||||
counter: integerArrayLoop.localId,
|
||||
length: integerArrayLoop.limitReceiver as IrExpr & { kind: "varRef" },
|
||||
range: { min: 0, max: 4294967294 },
|
||||
}
|
||||
: countedLoop && !countedLoop.guarded
|
||||
? countedDenseShape(s, countedLoop)
|
||||
: null;
|
||||
const dense =
|
||||
denseShape && !window
|
||||
? matchDenseArrayLoop(
|
||||
s,
|
||||
denseShape,
|
||||
this.numericLocals,
|
||||
this.unaliasedArrays,
|
||||
this.denseLoopEntries.get(s) ?? new Set(),
|
||||
)
|
||||
: null;
|
||||
if (dense) {
|
||||
const valid = emitDenseLoopGuard(this, dense);
|
||||
const fast = B.newLabel("dense.loop.fast"),
|
||||
slow = B.newLabel("dense.loop.slow"),
|
||||
done = B.newLabel("dense.loop.done");
|
||||
B.condBr(valid, fast, slow);
|
||||
this.denseLoopsEnabled = false;
|
||||
try {
|
||||
B.startBlock(slow);
|
||||
this.emitStmt(s);
|
||||
B.br(done);
|
||||
B.startBlock(fast);
|
||||
this.denseLoop = dense;
|
||||
this.emitStmt(s);
|
||||
B.br(done);
|
||||
} finally {
|
||||
this.denseLoop = null;
|
||||
this.denseLoopsEnabled = true;
|
||||
}
|
||||
B.startBlock(done);
|
||||
break;
|
||||
}
|
||||
if (window) {
|
||||
const valid = emitByteWindowGuard(this, window);
|
||||
const fast = B.newLabel("bytes.window.fast"),
|
||||
@@ -8747,7 +8965,10 @@ export class LlEmitter {
|
||||
this.integerLoopBindings.set(integerLoop.localId, {
|
||||
slot: integerSlot,
|
||||
type: this.sizeType,
|
||||
range: { min: 0, max: integerArrayLoop ? 4294967294 : Number.MAX_SAFE_INTEGER - 1 },
|
||||
range:
|
||||
this.denseLoop?.counter === integerLoop.localId
|
||||
? this.denseLoop.range
|
||||
: { min: 0, max: integerArrayLoop ? 4294967294 : Number.MAX_SAFE_INTEGER - 1 },
|
||||
});
|
||||
} else if (countedLoop) {
|
||||
integerSlot = B.slot();
|
||||
@@ -8762,7 +8983,10 @@ export class LlEmitter {
|
||||
slot: integerSlot,
|
||||
type: "i64",
|
||||
signed: true,
|
||||
range: countedLoop.range,
|
||||
range:
|
||||
this.denseLoop?.counter === countedLoop.localId
|
||||
? this.denseLoop.range
|
||||
: countedLoop.range,
|
||||
});
|
||||
countedLimit = emitCountedLoopLimit(this, countedLoop);
|
||||
} else if (s.init) {
|
||||
@@ -9344,14 +9568,15 @@ export class LlEmitter {
|
||||
B.line(`${stash} = call ptr @scr_exc_take() ; stash across finally`);
|
||||
B.line(`store ptr ${stash}, ptr ${stashSlot}`);
|
||||
this.scopes.push([{ slot: stashSlot, type: CAUGHT }]);
|
||||
const suppressHandler = s.suppressFinallyErrors
|
||||
? {
|
||||
label: B.newLabel("try.fs"),
|
||||
used: false,
|
||||
frameDepth: this.frames.length,
|
||||
scopeDepth: this.scopes.length,
|
||||
}
|
||||
: null;
|
||||
const suppressHandler =
|
||||
s.suppressFinallyErrors || s.dropFinallyErrors
|
||||
? {
|
||||
label: B.newLabel("try.fs"),
|
||||
used: false,
|
||||
frameDepth: this.frames.length,
|
||||
scopeDepth: this.scopes.length,
|
||||
}
|
||||
: null;
|
||||
if (suppressHandler) this.tryStack.push(suppressHandler);
|
||||
this.emitBlock(s.finallyBody!);
|
||||
if (suppressHandler) this.tryStack.pop();
|
||||
@@ -9361,8 +9586,9 @@ export class LlEmitter {
|
||||
this.emitUnwind();
|
||||
if (suppressHandler?.used) {
|
||||
B.startBlock(suppressHandler.label);
|
||||
this.declare(`declare void @scr_exc_suppress(ptr)`);
|
||||
B.line(`call void @scr_exc_suppress(ptr ${stash}) ; consumes stash`);
|
||||
const cleanup = s.dropFinallyErrors ? "scr_exc_keep_first" : "scr_exc_suppress";
|
||||
this.declare(`declare void @${cleanup}(ptr)`);
|
||||
B.line(`call void @${cleanup}(ptr ${stash}) ; consumes stash`);
|
||||
this.emitUnwind();
|
||||
}
|
||||
}
|
||||
@@ -10105,7 +10331,12 @@ export class LlEmitter {
|
||||
unionsById: this.unionsById,
|
||||
recordsById: this.recordsById,
|
||||
inlineUnions: this.inlineUnions,
|
||||
nullableArm: (t) => this.nullableUnions.of(t)?.arm ?? null,
|
||||
nullableArm: (t) => {
|
||||
const nullable = this.nullableUnions.of(t);
|
||||
return nullable?.repr === "single" ? nullable.arm : null;
|
||||
},
|
||||
shapeUnion: (t) => this.nullableUnions.of(t)?.repr === "shapes",
|
||||
fieldBase: (shapeId) => this.nullableUnions.fieldBase(shapeId),
|
||||
classInfo: (className) => {
|
||||
const meta = this.classMetaOf(className);
|
||||
return {
|
||||
@@ -10826,7 +11057,11 @@ export class LlEmitter {
|
||||
const nullable = this.nullableUnions.get(e.unionId);
|
||||
if (!nullable) return null;
|
||||
if (isUnitType(e.value.type)) return e.value.kind === "unitLit" ? "unit" : null;
|
||||
return e.tag === nullable.refTag && isRefCounted(e.value.type) ? "ref" : null;
|
||||
const refArm =
|
||||
nullable.repr === "single"
|
||||
? e.tag === nullable.refTag
|
||||
: nullable.arms.some((arm) => arm.tag === e.tag);
|
||||
return refArm && isRefCounted(e.value.type) ? "ref" : null;
|
||||
}
|
||||
|
||||
materializeSplitLocal(localId: string): void {
|
||||
|
||||
@@ -182,6 +182,7 @@ export function genResultThunkFor(
|
||||
sym = mangleGenResThunk(host.resolveThunks.size);
|
||||
host.resolveThunks.set(key, sym);
|
||||
const shape = host.recordsById.get(recT.shapeId);
|
||||
const base = host.nullableUnions.fieldBase(recT.shapeId);
|
||||
const valueT = shape?.fields.find((f) => f.name === "value")?.type;
|
||||
if (!shape || !valueT)
|
||||
throw new InternalCompilerError("llvm emitter bug: genResume record lacks its value field");
|
||||
@@ -242,7 +243,7 @@ export function genResultThunkFor(
|
||||
` %r = call ptr @${mangleRecordNew(recT.shapeId)}()`,
|
||||
` %d = call zeroext i1 @scr_gen_done(ptr %g)`,
|
||||
` %dz = zext i1 %d to i8`,
|
||||
` %dp = getelementptr inbounds %${mangleRecordStruct(recT.shapeId)}, ptr %r, i64 0, i32 ${doneIdxD + 1}`,
|
||||
` %dp = getelementptr inbounds %${mangleRecordStruct(recT.shapeId)}, ptr %r, i64 0, i32 ${doneIdxD + base}`,
|
||||
` store i8 %dz, ptr %dp`,
|
||||
` %has = call zeroext i1 @scr_gen_out_has(ptr %g)`,
|
||||
` br i1 %has, label %take, label %undefv`,
|
||||
@@ -259,7 +260,7 @@ export function genResultThunkFor(
|
||||
` br label %join`,
|
||||
`join:`,
|
||||
` %v = load ptr, ptr %vslot`,
|
||||
` %vp = getelementptr inbounds %${mangleRecordStruct(recT.shapeId)}, ptr %r, i64 0, i32 ${valueIdxD + 1}`,
|
||||
` %vp = getelementptr inbounds %${mangleRecordStruct(recT.shapeId)}, ptr %r, i64 0, i32 ${valueIdxD + base}`,
|
||||
` store ptr %v, ptr %vp`,
|
||||
` ret ptr %r`,
|
||||
`}`,
|
||||
@@ -293,7 +294,7 @@ export function genResultThunkFor(
|
||||
` %r = call ptr @${mangleRecordNew(recT.shapeId)}()`,
|
||||
` %d = call zeroext i1 @scr_gen_done(ptr %g)`,
|
||||
` %dz = zext i1 %d to i8`,
|
||||
` %dp = getelementptr inbounds %${mangleRecordStruct(recT.shapeId)}, ptr %r, i64 0, i32 ${doneIdx + 1}`,
|
||||
` %dp = getelementptr inbounds %${mangleRecordStruct(recT.shapeId)}, ptr %r, i64 0, i32 ${doneIdx + base}`,
|
||||
` store i8 %dz, ptr %dp`,
|
||||
` br i1 %d, label %doneb, label %susp`,
|
||||
];
|
||||
@@ -410,7 +411,7 @@ export function genResultThunkFor(
|
||||
` br label %join`,
|
||||
`join:`,
|
||||
` %v = load ptr, ptr %vslot`,
|
||||
` %vp = getelementptr inbounds %${mangleRecordStruct(recT.shapeId)}, ptr %r, i64 0, i32 ${valueIdx + 1}`,
|
||||
` %vp = getelementptr inbounds %${mangleRecordStruct(recT.shapeId)}, ptr %r, i64 0, i32 ${valueIdx + base}`,
|
||||
// An inline value field copies the box's bits and drops the box.
|
||||
...(host.inlineUnions.record(valueT)
|
||||
? [
|
||||
@@ -1092,7 +1093,8 @@ export function settledStoreFor(
|
||||
return { tag, arm };
|
||||
};
|
||||
const fieldIndex = (arm: IrType & { kind: "record" }, field: string): number =>
|
||||
host.recordsById.get(arm.shapeId)!.fields.findIndex((f) => f.name === field) + 1;
|
||||
host.recordsById.get(arm.shapeId)!.fields.findIndex((f) => f.name === field) +
|
||||
host.nullableUnions.fieldBase(arm.shapeId);
|
||||
const fulfilled = armOf("value");
|
||||
const rejected = armOf("reason");
|
||||
const valueT = host.recordsById
|
||||
|
||||
@@ -42,7 +42,7 @@ import {
|
||||
} from "./map-number-lookup.js";
|
||||
import { emitBorrowedInput } from "./borrowed-inputs.js";
|
||||
import { emitFusedMapSet, rememberMapEntry } from "./map-entry-fusion.js";
|
||||
import { emitStringSliceRead } from "./string-slices.js";
|
||||
import { emitScratchStringSlice, emitStringSliceRead } from "./string-slices.js";
|
||||
import {
|
||||
STRING_CHAR_CODE_AT_DECL,
|
||||
STRING_CHAR_CODE_AT_INT_MARKER,
|
||||
@@ -395,6 +395,7 @@ export function emitStrIntrinsic(
|
||||
false,
|
||||
);
|
||||
case "slice":
|
||||
if (host.scratchStringSlices.has(e)) return emitScratchStringSlice(host, e, r, args);
|
||||
return call(
|
||||
"scr_str_slice",
|
||||
"ptr (ptr, double, double)",
|
||||
@@ -403,6 +404,7 @@ export function emitStrIntrinsic(
|
||||
true,
|
||||
);
|
||||
case "substring":
|
||||
if (host.scratchStringSlices.has(e)) return emitScratchStringSlice(host, e, r, args);
|
||||
return call(
|
||||
"scr_str_substring",
|
||||
"ptr (ptr, double, double)",
|
||||
@@ -575,7 +577,16 @@ export function emitArrIntrinsic(
|
||||
? host.constantNumericTables.get(e.receiver.localId)
|
||||
: undefined;
|
||||
if (!table && elem.kind === "f64")
|
||||
return { name: emitDenseArrayNumber(host, r.name, index, e.args[0]), type: e.type };
|
||||
return {
|
||||
name: emitDenseArrayNumber(
|
||||
host,
|
||||
r.name,
|
||||
index,
|
||||
e.args[0],
|
||||
host.provenDenseIndex(e.receiver, e.args[0]!),
|
||||
),
|
||||
type: e.type,
|
||||
};
|
||||
host.declare(`declare double @scr_arr_get_number(ptr, double)`);
|
||||
const t = B.tmp();
|
||||
B.line(
|
||||
|
||||
@@ -330,7 +330,7 @@ export function emitControlExpr(
|
||||
);
|
||||
B.line(`store ${ty} ${t}, ptr ${slot}`);
|
||||
if (host.mayThrow.has(e.retag)) host.emitPendingCheck();
|
||||
} else if (host.nullableUnions.has(e.left.type.unionId)) {
|
||||
} else if (host.nullableUnions.single(e.left.type.unionId)) {
|
||||
// The owned nullable pointer IS the owned arm value.
|
||||
B.line(`store ${ty} ${l.name}, ptr ${slot}`);
|
||||
} else {
|
||||
@@ -440,7 +440,7 @@ export function emitControlExpr(
|
||||
host.emitBranchInto(slot, e.right);
|
||||
B.br(lj);
|
||||
B.startBlock(lv);
|
||||
if (typeEquals(e.type, e.left.type) || host.nullableUnions.has(def.id)) {
|
||||
if (typeEquals(e.type, e.left.type) || host.nullableUnions.single(def.id)) {
|
||||
// Pass-through, or a nullable pointer that IS its owned arm value.
|
||||
B.line(`store ${ty} ${l.name}, ptr ${slot}`);
|
||||
} else {
|
||||
|
||||
@@ -64,16 +64,25 @@ export function emitLiteralExpr(
|
||||
const integer = B.tmp();
|
||||
const number = B.tmp();
|
||||
B.line(`${integer} = load ${integerSlot.type}, ptr ${integerSlot.slot}`);
|
||||
// A 64-bit induction value is a safe integer, so its sign bit is
|
||||
// clear and the signed conversion is exact: one cvtsi2sd instead of
|
||||
// x86-64's multi-instruction unsigned 64-bit conversion.
|
||||
const signedConversion = integerSlot.signed === true || integerSlot.type === "i64";
|
||||
B.line(
|
||||
`${number} = ${signedConversion ? "sitofp" : "uitofp"} ${integerSlot.type} ${integer} to double`,
|
||||
);
|
||||
const uint32 = integerSlot.type === "i32" ? integer : B.tmp();
|
||||
if (integerSlot.type !== "i32")
|
||||
B.line(`${uint32} = trunc ${integerSlot.type} ${integer} to i32`);
|
||||
// An int32 counter (a dense array loop's) converts from its low half,
|
||||
// so LLVM folds a later `fptosi` of the double (an inlined callee's
|
||||
// index) back to the integer. Otherwise a 64-bit induction value is
|
||||
// a safe integer, so its sign bit is clear and the signed conversion
|
||||
// is exact: one cvtsi2sd instead of x86-64's multi-instruction
|
||||
// unsigned 64-bit conversion. (A uint32 counter would fold through
|
||||
// `uitofp i32` too, but the zero extension it adds on x86-64 cost
|
||||
// more in loops that never convert the double than it saved.)
|
||||
const range = integerSlot.range;
|
||||
const signedConversion = integerSlot.signed === true || integerSlot.type === "i64";
|
||||
if (range && range.min >= -2147483648 && range.max <= 2147483647)
|
||||
B.line(`${number} = sitofp i32 ${uint32} to double`);
|
||||
else
|
||||
B.line(
|
||||
`${number} = ${signedConversion ? "sitofp" : "uitofp"} ${integerSlot.type} ${integer} to double`,
|
||||
);
|
||||
return {
|
||||
name: number,
|
||||
type: e.type,
|
||||
@@ -851,6 +860,7 @@ export function emitContainerExpr(
|
||||
elem,
|
||||
`scr_arr_get_${acc}`,
|
||||
true,
|
||||
host.provenDenseIndex(e.arr, e.index),
|
||||
);
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
|
||||
@@ -62,9 +62,17 @@ export function emitUnionRetype(
|
||||
const from = host.nullableUnions.get(e.fromId);
|
||||
const to = host.nullableUnions.get(e.unionId);
|
||||
if (from && to) {
|
||||
// Equal encodings make the pointer itself the target value.
|
||||
if (from.refTag === to.refTag && from.unitTag === to.unitTag && from.nullTag === to.nullTag)
|
||||
return host.own({ name: value.name, type: e.type });
|
||||
// Equal encodings make the pointer itself the target value. A shape
|
||||
// union derives its record arms from the record, so only the unit
|
||||
// encodings must agree.
|
||||
const same =
|
||||
from.repr === "single" && to.repr === "single"
|
||||
? from.refTag === to.refTag && from.unitTag === to.unitTag && from.nullTag === to.nullTag
|
||||
: from.repr === "shapes" &&
|
||||
to.repr === "shapes" &&
|
||||
from.unitTag === to.unitTag &&
|
||||
from.nullTag === to.nullTag;
|
||||
if (same) return host.own({ name: value.name, type: e.type });
|
||||
return host.own({ name: rebuildRetype(host, value.name, e), type: e.type });
|
||||
}
|
||||
if (from || to) return host.own({ name: rebuildRetype(host, value.name, e), type: e.type });
|
||||
|
||||
@@ -500,16 +500,17 @@ export function streamTypedRefCommitAdapter(
|
||||
` br i1 %missing, label %done, label %swap`,
|
||||
`swap:`,
|
||||
];
|
||||
const base = host.nullableUnions.fieldBase(t.shapeId);
|
||||
const members = [
|
||||
...shape.fields.map((field, index) => ({
|
||||
index: index + 1,
|
||||
index: index + base,
|
||||
type: llFieldType(field.type, host.inlineUnions.record(field.type)),
|
||||
name: field.name,
|
||||
})),
|
||||
...(shape.indexValue
|
||||
? [
|
||||
{
|
||||
index: shape.fields.length + 1,
|
||||
index: shape.fields.length + base,
|
||||
type: "ptr" as const,
|
||||
name: "[key: string] overflow",
|
||||
},
|
||||
@@ -943,7 +944,7 @@ export function streamTypedRefMaterializeAdapter(
|
||||
B.line(`${out} = call ptr @scr_dyn_new_arr()`);
|
||||
const fields = [...shape.fields].sort((a, b) => Number(a.name) - Number(b.name));
|
||||
for (const field of fields) {
|
||||
const index = shape.fields.indexOf(field) + 1;
|
||||
const index = shape.fields.indexOf(field) + host.nullableUnions.fieldBase(t.shapeId);
|
||||
const fieldPtr = B.tmp();
|
||||
B.line(
|
||||
`${fieldPtr} = getelementptr inbounds %${mangleRecordStruct(t.shapeId)}, ptr %p, i64 0, i32 ${index}`,
|
||||
@@ -980,7 +981,7 @@ export function streamTypedRefMaterializeAdapter(
|
||||
liveRecordOps(host, t, snapshot, fields);
|
||||
}
|
||||
for (const field of fields) {
|
||||
const index = shape.fields.indexOf(field) + 1;
|
||||
const index = shape.fields.indexOf(field) + host.nullableUnions.fieldBase(t.shapeId);
|
||||
const fieldPtr = B.tmp();
|
||||
B.line(
|
||||
`${fieldPtr} = getelementptr inbounds %${mangleRecordStruct(t.shapeId)}, ptr %p, i64 0, i32 ${index}`,
|
||||
@@ -1202,7 +1203,7 @@ function liveRecordOps(
|
||||
const names = `${snapshot}_names`;
|
||||
const ops = `${snapshot}_rops`;
|
||||
const loadField = (B: BlockBuilder, field: IrRecordShape["fields"][number]): string => {
|
||||
const index = shape.fields.indexOf(field) + 1;
|
||||
const index = shape.fields.indexOf(field) + host.nullableUnions.fieldBase(t.shapeId);
|
||||
const fieldPtr = B.tmp();
|
||||
B.line(`${fieldPtr} = getelementptr inbounds %${struct}, ptr %p, i64 0, i32 ${index}`);
|
||||
return loadStructField(
|
||||
|
||||
@@ -29,7 +29,7 @@ function messageAdapter(host: LlvmEmitterContext, type: IrType): string {
|
||||
const isString = field === "address" || field === "family";
|
||||
if (isString) lines.push(` %${field}.owned = call ptr @scr_str_retain_v(ptr %${field})`);
|
||||
lines.push(
|
||||
` %${field}.slot = getelementptr inbounds %${mangleRecordStruct(type.shapeId)}, ptr %record, i64 0, i32 ${index + 1}`,
|
||||
` %${field}.slot = getelementptr inbounds %${mangleRecordStruct(type.shapeId)}, ptr %record, i64 0, i32 ${index + host.nullableUnions.fieldBase(type.shapeId)}`,
|
||||
` store ${isString ? "ptr" : "double"} %${field}${isString ? ".owned" : ""}, ptr %${field}.slot`,
|
||||
);
|
||||
}
|
||||
@@ -169,7 +169,7 @@ export function emitDatagramLibCall(host: LlvmEmitterContext, e: LibCallExpr): L
|
||||
);
|
||||
const slot = host.B.tmp();
|
||||
host.B.line(
|
||||
`${slot} = getelementptr inbounds %${mangleRecordStruct(type.shapeId)}, ptr ${record}, i64 0, i32 ${index + 1}`,
|
||||
`${slot} = getelementptr inbounds %${mangleRecordStruct(type.shapeId)}, ptr ${record}, i64 0, i32 ${index + host.nullableUnions.fieldBase(type.shapeId)}`,
|
||||
);
|
||||
host.B.line(`store ${ty} ${value}, ptr ${slot}`);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { emitTestLibCall } from "./lib-test.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
|
||||
import { MAY_THROW_LIB_FNS } from "../../ir/builtin-effects.js";
|
||||
import type { IrExpr, IrType } from "../../ir/ir.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
import type { LlvmEmitterContext, LibCallExpr, LibCallPrefix, LlValue } from "./expr-context.js";
|
||||
import {
|
||||
@@ -21,6 +22,7 @@ import { isDynUndefined } from "../../ir/runtime-features.js";
|
||||
import { DYN_KIND } from "./dyn.js";
|
||||
import { rawBytes } from "./lib-abi.js";
|
||||
import { emitInlineAtomicsOp } from "./expr-bytes.js";
|
||||
import { regexLiteralCaptures } from "./regex-matchers.js";
|
||||
|
||||
export function emitAssertInspectLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
const B = host.B;
|
||||
@@ -642,8 +644,66 @@ function emitAssertFailing(
|
||||
* `a.name.localeCompare(b.name)` reads both fields without retaining). */
|
||||
const READ_ONLY_STRING_LIB_FNS: ReadonlySet<string> = new Set(["str.localeCompare"]);
|
||||
|
||||
/** The parameter count of a replace callback the runtime can call through
|
||||
* the closure ABI (scr_regex_replace_fn), or null when it needs the dyn
|
||||
* box. Each declared parameter must receive a value of its declared kind
|
||||
* in every match: the match string at 0; with more than one parameter the
|
||||
* pattern must be a literal whose captures are known, so position i is a
|
||||
* capture that every match sets (a string), the offset (a number) right
|
||||
* after the captures, or the subject (a string) after that. */
|
||||
function typedReplacerArity(callback: IrType, pattern: IrExpr): number | null {
|
||||
if (callback.kind !== "func" || callback.rest || callback.argumentsAll) return null;
|
||||
if (callback.ret.kind !== "string") return null;
|
||||
const params = callback.params;
|
||||
if (params.length > 4) return null;
|
||||
if (params.length === 0) return 0;
|
||||
if (params[0]!.kind !== "string") return null;
|
||||
if (params.length === 1) return 1;
|
||||
if (pattern.kind !== "regexLit") return null;
|
||||
const captures = regexLiteralCaptures(pattern.pattern, pattern.flags);
|
||||
if (!captures) return null;
|
||||
for (let i = 1; i < params.length; i++) {
|
||||
const kind = params[i]!.kind;
|
||||
const ok =
|
||||
i < captures.count
|
||||
? kind === "string" && captures.always[i] === true
|
||||
: i === captures.count
|
||||
? kind === "f64"
|
||||
: i === captures.count + 1 && kind === "string";
|
||||
if (!ok) return null;
|
||||
}
|
||||
return params.length;
|
||||
}
|
||||
|
||||
/** `s.replace(re, fn)` / `replaceAll` with a typed callback the runtime can
|
||||
* call directly: the dyn box the IR wraps it in is never observable (the
|
||||
* runtime only calls it, with `this` undefined), so the closure goes to
|
||||
* scr_regex_replace_fn as it is. Null keeps the generic path. */
|
||||
function emitTypedRegexReplace(host: LlvmEmitterContext, e: LibCallExpr): LlValue | null {
|
||||
const [subject, pattern, callback, all] = e.args;
|
||||
if (!subject || !pattern || callback?.kind !== "dynFrom" || all?.kind !== "boolLit") return null;
|
||||
const arity = typedReplacerArity(callback.value.type, pattern);
|
||||
if (arity === null) return null;
|
||||
const B = host.B;
|
||||
const s = host.emitExpr(subject);
|
||||
const re = host.emitExpr(pattern);
|
||||
const fn = host.emitExpr(callback.value);
|
||||
host.declare(`declare ptr @scr_regex_replace_fn(ptr, ptr, ptr, i32, i1 zeroext)`);
|
||||
const result = B.tmp();
|
||||
B.line(
|
||||
`${result} = call ptr @scr_regex_replace_fn(ptr ${s.name}, ptr ${re.name}, ptr ${fn.name}, i32 ${arity}, i1 zeroext ${all.value})`,
|
||||
);
|
||||
const output = host.own({ name: result, type: e.type });
|
||||
host.emitPendingCheck();
|
||||
return output;
|
||||
}
|
||||
|
||||
export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
if (USES_TIMERS_LIB_FNS.has(e.fn)) host.usesTimers = true;
|
||||
if (e.fn === "regex.replaceCallback") {
|
||||
const typed = emitTypedRegexReplace(host, e);
|
||||
if (typed) return typed;
|
||||
}
|
||||
if (e.fn === "process.nextTick") return host.emitAsyncContextLibCall(e);
|
||||
if (
|
||||
e.fn === "sp.pipeline" ||
|
||||
@@ -774,6 +834,8 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
case "blob":
|
||||
case "record":
|
||||
case "disposable":
|
||||
case "punycode":
|
||||
case "constants":
|
||||
return host.emitGenericLibCall(e);
|
||||
case "test":
|
||||
return emitTestLibCall(host, e);
|
||||
|
||||
@@ -912,11 +912,8 @@ export function emitPathUrlLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
scopeid.inline,
|
||||
);
|
||||
}
|
||||
const rc = vAdapters(host.shapeHost, t);
|
||||
const rowU = B.tmp();
|
||||
B.line(
|
||||
`${rowU} = call ptr @scr_union_new_ref(i32 ${tag}, ptr ${r}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host.shapeHost, t)})`,
|
||||
);
|
||||
// The row (+1) moves into its union arm (a shape union is the row).
|
||||
const rowU = host.unionNewOwned(infoT.unionId, tag, { name: r, type: t });
|
||||
B.line(`store ptr ${rowU}, ptr ${rowSlot}`);
|
||||
B.br(lRow);
|
||||
};
|
||||
|
||||
@@ -65,7 +65,7 @@ export function emitLookupConnect(host: LlvmEmitterContext, e: LibCallExpr): LlV
|
||||
" br i1 %more, label %body, label %finish",
|
||||
"body:",
|
||||
" %record = call ptr @scr_arr_get_ref(ptr %addresses, double %index)",
|
||||
` %addressp = getelementptr inbounds %${struct}, ptr %record, i64 0, i32 ${field + 1}`,
|
||||
` %addressp = getelementptr inbounds %${struct}, ptr %record, i64 0, i32 ${field + host.nullableUnions.fieldBase(addresses.elem.shapeId)}`,
|
||||
" %address = load ptr, ptr %addressp",
|
||||
" %owned.address = call ptr @scr_str_retain_v(ptr %address)",
|
||||
" call double @scr_arr_push_ref(ptr %ips, ptr %owned.address)",
|
||||
|
||||
@@ -168,7 +168,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr)
|
||||
const i = shape.fields.findIndex((f) => f.name === name);
|
||||
if (i < 0)
|
||||
throw new InternalCompilerError(`llvm emitter bug: net.serverAddress record lacks ${name}`);
|
||||
return i + 1;
|
||||
return i + host.nullableUnions.fieldBase(recT.shapeId);
|
||||
};
|
||||
const store = (name: string, ty: string, v: string): void => {
|
||||
const p = B.tmp();
|
||||
|
||||
@@ -144,6 +144,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"num.toFixed0": "scr_num_to_fixed0",
|
||||
"num.toFixed": "scr_num_to_fixed",
|
||||
"num.toStringRadix": "scr_num_to_string_radix",
|
||||
"num.toStringRadixF64": "scr_num_to_string_radix_f64",
|
||||
"num.sameValue": "scr_num_same_value",
|
||||
"intl.numFormatEnUs": "scr_intl_num_format_en_us",
|
||||
"number.isFinite": "scr_num_is_finite",
|
||||
@@ -194,6 +195,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"os.cpusModel": "scr_os_cpus_model",
|
||||
"os.cpusField": "scr_os_cpus_field",
|
||||
"os.constants": "scr_os_constants",
|
||||
"constants.object": "scr_node_constants",
|
||||
"os.release": "scr_os_release",
|
||||
"os.userName": "scr_os_user_name",
|
||||
"os.userShell": "scr_os_user_shell",
|
||||
@@ -405,6 +407,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"buffer.fromDyn": "scr_buffer_from_dyn",
|
||||
"buffer.concat": "scr_buffer_concat",
|
||||
"buffer.concatLen": "scr_buffer_concat_len",
|
||||
"buffer.concatDyn": "scr_buffer_concat_dyn",
|
||||
"buffer.concatLenDyn": "scr_buffer_concat_len_dyn",
|
||||
"buffer.alloc": "scr_buffer_alloc",
|
||||
"buffer.allocUnsafe": "scr_buffer_alloc_unsafe",
|
||||
"buffer.allocUnsafeSlow": "scr_buffer_alloc_unsafe_slow",
|
||||
@@ -638,6 +642,13 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"url.legacyFormat": "scr_url_legacy_format",
|
||||
"url.legacyResolve": "scr_url_legacy_resolve",
|
||||
"url.domainToASCII": "scr_url_domain_to_ascii",
|
||||
"punycode.decode": "scr_punycode_decode",
|
||||
"punycode.encode": "scr_punycode_encode",
|
||||
"punycode.toASCII": "scr_punycode_to_ascii",
|
||||
"punycode.toUnicode": "scr_punycode_to_unicode",
|
||||
"punycode.ucs2Decode": "scr_punycode_ucs2_decode",
|
||||
"punycode.ucs2Encode": "scr_str_from_code_point",
|
||||
"punycode.warn": "scr_punycode_warn",
|
||||
"url.domainToUnicode": "scr_url_domain_to_unicode",
|
||||
"url.pathToFileURL": "scr_url_from_path",
|
||||
"url.pathToFileURLPlatform": "scr_url_from_path_platform",
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { expect, test } from "vitest";
|
||||
import {
|
||||
F64,
|
||||
NULL_T,
|
||||
UNDEFINED_T,
|
||||
type IrModule,
|
||||
type IrType,
|
||||
type IrUnionDef,
|
||||
} from "../../ir/ir.js";
|
||||
import { NullableUnions } from "./nullable-unions.js";
|
||||
|
||||
const loc = { file: "nullable-unions.ts", start: 0, end: 0 };
|
||||
const a: IrType = { kind: "record", shapeId: "a" };
|
||||
const b: IrType = { kind: "record", shapeId: "b" };
|
||||
const c: IrType = { kind: "record", shapeId: "c" };
|
||||
|
||||
function module(unions: IrUnionDef[], extra: Partial<IrModule> = {}): IrModule {
|
||||
return {
|
||||
irVersion: 16,
|
||||
sourceFile: loc.file,
|
||||
entry: "main",
|
||||
records: ["a", "b", "c"].map((id) => ({ id, fields: [{ name: id, type: F64 }] })),
|
||||
unions,
|
||||
functions: [],
|
||||
...extra,
|
||||
};
|
||||
}
|
||||
|
||||
test("unions of two or more records become shape unions", () => {
|
||||
const unions = new NullableUnions(
|
||||
module([
|
||||
{ id: "ab", arms: [a, b] },
|
||||
{ id: "abu", arms: [a, UNDEFINED_T, b] },
|
||||
{ id: "abnu", arms: [a, b, NULL_T, UNDEFINED_T] },
|
||||
{ id: "af", arms: [a, F64] },
|
||||
{ id: "au", arms: [a, UNDEFINED_T] },
|
||||
]),
|
||||
);
|
||||
expect(unions.shapes("ab")).toMatchObject({ unitTag: -1, nullTag: -1 });
|
||||
expect(unions.shapes("abu")).toMatchObject({ unitTag: 1, nullTag: -1 });
|
||||
expect(unions.shapes("abnu")).toMatchObject({ unitTag: 3, nullTag: 2 });
|
||||
expect(unions.shapes("abnu")?.arms).toEqual([
|
||||
{ tag: 0, shapeId: "a" },
|
||||
{ tag: 1, shapeId: "b" },
|
||||
]);
|
||||
// A scalar arm keeps the box; one record arm is a single-arm nullable.
|
||||
expect(unions.get("af")).toBeNull();
|
||||
expect(unions.single("au")).not.toBeNull();
|
||||
expect(unions.shapes("au")).toBeNull();
|
||||
// Tagged shapes number from 2 and move their fields after the word.
|
||||
expect([unions.shapeNumber("a"), unions.shapeNumber("b"), unions.shapeNumber("c")]).toEqual([
|
||||
2, 3, 0,
|
||||
]);
|
||||
expect([unions.fieldBase("a"), unions.fieldBase("c")]).toEqual([2, 1]);
|
||||
expect(unions.boxedUnions(new Map([["af", { id: "af", arms: [a, F64] }]])).size).toBe(1);
|
||||
});
|
||||
|
||||
test("records of runtime-inspected unions keep every union over them boxed", () => {
|
||||
const set: IrType = { kind: "set", elem: { kind: "union", unionId: "ab" } };
|
||||
const unions = new NullableUnions(
|
||||
module(
|
||||
[
|
||||
{ id: "ab", arms: [a, b] },
|
||||
{ id: "abu", arms: [a, b, UNDEFINED_T] },
|
||||
{ id: "bc", arms: [b, c] },
|
||||
{ id: "ac", arms: [a, c] },
|
||||
],
|
||||
{ globals: [{ id: "%g.s", name: "s", type: set, mutable: false }] },
|
||||
),
|
||||
);
|
||||
expect(unions.get("ab")).toBeNull();
|
||||
expect(unions.get("abu")).toBeNull();
|
||||
expect(unions.get("bc")).toBeNull();
|
||||
expect(unions.get("ac")).toBeNull();
|
||||
expect(unions.shapeNumber("c")).toBe(0);
|
||||
});
|
||||
|
||||
test("library, lifetime-region and debug builds keep record unions boxed", () => {
|
||||
const unions = [{ id: "ab", arms: [a, b] }];
|
||||
expect(new NullableUnions(module(unions), false).get("ab")).toBeNull();
|
||||
expect(new NullableUnions(module(unions, { lifetimeRegions: true })).get("ab")).toBeNull();
|
||||
expect(new NullableUnions(module(unions)).shapes("ab")).not.toBeNull();
|
||||
});
|
||||
@@ -17,6 +17,7 @@ import { everyModuleNode } from "../../ir/traverse.js";
|
||||
* every container, field, capture, parameter, and return slot holds the
|
||||
* plain pointer. Every other union keeps the tagged `%ScrUnion` box. */
|
||||
export interface NullableUnion {
|
||||
repr: "single";
|
||||
unionId: string;
|
||||
refTag: number;
|
||||
/** The unit arm NULL encodes. */
|
||||
@@ -24,7 +25,57 @@ export interface NullableUnion {
|
||||
/** The `null` arm of a two-unit-arm union, encoded by NULLABLE_NULL;
|
||||
* -1 when the union has one unit arm. */
|
||||
nullTag: number;
|
||||
arm: NullableRefArm;
|
||||
/** One of NullableRefArm. Typed as the plain IrType so the self-hosted
|
||||
* frontend keeps IrType's structurally identical arms one record shape
|
||||
* (a PointerUnion arm holding the narrowed intersection would split them). */
|
||||
arm: IrType;
|
||||
}
|
||||
|
||||
/** A union of two or more record arms and at most two unit arms (`A | B`,
|
||||
* `A | B | undefined`, discriminated unions of object literals) whose
|
||||
* VALUES are the record pointer itself, like a NullableUnion: NULL is the
|
||||
* unit arm (`undefined` when both exist, NULLABLE_NULL is then `null`).
|
||||
* Every record shape that is an arm of such a union is a TAGGED shape: its
|
||||
* struct carries the shape's module-wide number in a hidden `i32` word
|
||||
* right after the reference count (struct index 1), stored by every
|
||||
* allocation, so the arm of a pointer is that word mapped through the
|
||||
* union's arm table (the per-union `@sc_stag.<id>` helper). Records share
|
||||
* one retain; release and trace dispatch on the arm. */
|
||||
export interface ShapeUnion {
|
||||
repr: "shapes";
|
||||
unionId: string;
|
||||
/** The unit arm NULL encodes; -1 when the union has no unit arm. */
|
||||
unitTag: number;
|
||||
/** The `null` arm of a two-unit-arm union, encoded by NULLABLE_NULL;
|
||||
* -1 otherwise. */
|
||||
nullTag: number;
|
||||
/** The record arms in tag order. */
|
||||
arms: readonly { tag: number; shapeId: string }[];
|
||||
}
|
||||
|
||||
/** A union whose values are plain pointers (no `%ScrUnion` box). */
|
||||
export type PointerUnion = NullableUnion | ShapeUnion;
|
||||
|
||||
/** The struct index of a tagged record shape's hidden shape-number word. */
|
||||
export const SHAPE_WORD_INDEX = 1;
|
||||
|
||||
/** The LLVM type through which any tagged record's shape word is read
|
||||
* (the reference count, then the word). */
|
||||
export function shapeWordHeader(sizeType: "i32" | "i64"): string {
|
||||
return `{ ${sizeType}, i32 }`;
|
||||
}
|
||||
|
||||
/** The per-union helper mapping a shape-union value to its tag. */
|
||||
export function shapeTagFn(unionId: string): string {
|
||||
return `@sc_stag.${unionId.replace(/[^A-Za-z0-9_.]/g, "_")}`;
|
||||
}
|
||||
|
||||
/** The per-union release and trace dispatchers of a shape union. */
|
||||
export function shapeReleaseFn(unionId: string): string {
|
||||
return `@sc_srelease.${unionId.replace(/[^A-Za-z0-9_.]/g, "_")}`;
|
||||
}
|
||||
export function shapeTraceFn(unionId: string): string {
|
||||
return `@sc_strace.${unionId.replace(/[^A-Za-z0-9_.]/g, "_")}`;
|
||||
}
|
||||
|
||||
/** The module-scope immortal sentinel standing for `null` in a
|
||||
@@ -48,44 +99,98 @@ export type NullableRefArm =
|
||||
| (IrType & { kind: "array" | "map" | "set" });
|
||||
|
||||
/** The arm is a JS object: always truthy, compared by identity. */
|
||||
export function isObjectArm(nullable: NullableUnion): boolean {
|
||||
return nullable.arm.kind !== "string";
|
||||
export function isObjectArm(nullable: PointerUnion): boolean {
|
||||
return nullable.repr === "shapes" || nullable.arm.kind !== "string";
|
||||
}
|
||||
|
||||
/** The representation decision for every union of one module. It is a
|
||||
* pure function of the module, so serialized-IR and in-memory emission
|
||||
* agree. */
|
||||
export class NullableUnions {
|
||||
private readonly byId = new Map<string, NullableUnion>();
|
||||
private readonly byId = new Map<string, PointerUnion>();
|
||||
/** Tagged record shapes (arms of a shape union) by their module-wide
|
||||
* shape number (2 and up: 0 and 1 are the unit encodings' pseudo
|
||||
* numbers, never stored). */
|
||||
private readonly shapeNumbers = new Map<string, number>();
|
||||
/** NULL-skipping retain wrappers requested so far: symbol → inner retain
|
||||
* call target. The emitter defines each once. */
|
||||
readonly retainWrappers = new Map<string, string>();
|
||||
/** Some nullable union encodes `null` with NULLABLE_NULL. */
|
||||
readonly usesNullSentinel: boolean = false;
|
||||
|
||||
constructor(mod: IrModule) {
|
||||
/** `shapes`: allow shape unions (debug builds keep boxes, whose
|
||||
* debug-info layouts describe every arm). */
|
||||
constructor(mod: IrModule, shapes = true) {
|
||||
const emitted = new Set<string>();
|
||||
for (const cls of mod.classes ?? []) if (!cls.runtime) emitted.add(cls.name);
|
||||
const records = new Set<string>();
|
||||
for (const shape of mod.records ?? []) records.add(shape.id);
|
||||
const boxed = boxedUnionIds(mod);
|
||||
// Shape unions stay out of library builds (their host ABI marshals
|
||||
// boxes) and of programs with lifetime regions (whose walks report a
|
||||
// reference slot by its type's release entry point).
|
||||
const allowShapes = shapes && mod.lib === undefined && mod.lifetimeRegions !== true;
|
||||
// A record that is an arm of a boxed union never becomes a shape
|
||||
// union's arm: conversions between unions over the same records then
|
||||
// keep one representation.
|
||||
const boxedRecords = new Set<string>();
|
||||
for (const def of mod.unions ?? [])
|
||||
if (boxed.has(def.id))
|
||||
for (const arm of def.arms) if (arm.kind === "record") boxedRecords.add(arm.shapeId);
|
||||
for (const def of mod.unions ?? []) {
|
||||
if (boxed.has(def.id)) continue;
|
||||
const nullable = nullableShape(def, emitted, records);
|
||||
const nullable =
|
||||
nullableShape(def, emitted, records) ??
|
||||
(allowShapes ? shapeUnion(def, records, boxedRecords) : null);
|
||||
if (!nullable) continue;
|
||||
this.byId.set(def.id, nullable);
|
||||
if (nullable.nullTag >= 0) this.usesNullSentinel = true;
|
||||
if (nullable.repr === "shapes")
|
||||
for (const arm of nullable.arms)
|
||||
if (!this.shapeNumbers.has(arm.shapeId))
|
||||
this.shapeNumbers.set(arm.shapeId, this.shapeNumbers.size + 2);
|
||||
}
|
||||
}
|
||||
|
||||
get(unionId: string): NullableUnion | null {
|
||||
get(unionId: string): PointerUnion | null {
|
||||
return this.byId.get(unionId) ?? null;
|
||||
}
|
||||
|
||||
of(type: IrType): NullableUnion | null {
|
||||
of(type: IrType): PointerUnion | null {
|
||||
return type.kind === "union" ? this.get(type.unionId) : null;
|
||||
}
|
||||
|
||||
/** The single-reference-arm form only (shape unions answer null). */
|
||||
single(unionId: string): NullableUnion | null {
|
||||
const nullable = this.byId.get(unionId);
|
||||
return nullable?.repr === "single" ? nullable : null;
|
||||
}
|
||||
|
||||
/** The shape-union form only. */
|
||||
shapes(unionId: string): ShapeUnion | null {
|
||||
const nullable = this.byId.get(unionId);
|
||||
return nullable?.repr === "shapes" ? nullable : null;
|
||||
}
|
||||
|
||||
/** Every shape union, in module order. */
|
||||
allShapeUnions(): ShapeUnion[] {
|
||||
const out: ShapeUnion[] = [];
|
||||
for (const nullable of this.byId.values()) if (nullable.repr === "shapes") out.push(nullable);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The module-wide number a tagged record shape stores in its shape
|
||||
* word, or 0 for shapes that carry none. */
|
||||
shapeNumber(shapeId: string): number {
|
||||
return this.shapeNumbers.get(shapeId) ?? 0;
|
||||
}
|
||||
|
||||
/** The struct index of a record shape's first declared field: 2 for
|
||||
* tagged shapes (after the shape word), 1 otherwise. */
|
||||
fieldBase(shapeId: string): number {
|
||||
return this.shapeNumbers.has(shapeId) ? 2 : 1;
|
||||
}
|
||||
|
||||
has(unionId: string): boolean {
|
||||
return this.byId.has(unionId);
|
||||
}
|
||||
@@ -94,10 +199,11 @@ export class NullableUnions {
|
||||
return this.byId.size;
|
||||
}
|
||||
|
||||
/** The reference arm of every nullable union, by union id. */
|
||||
/** The reference arm of every single-arm nullable union, by union id. */
|
||||
armTypes(): Map<string, IrType> {
|
||||
const result = new Map<string, IrType>();
|
||||
for (const [id, nullable] of this.byId) result.set(id, nullable.arm);
|
||||
for (const [id, nullable] of this.byId)
|
||||
if (nullable.repr === "single") result.set(id, nullable.arm);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -129,7 +235,7 @@ function nullableShape(
|
||||
if (unitTag < 0 || nullTag < 0) return null;
|
||||
}
|
||||
const arm = def.arms[refTag]!;
|
||||
const base = { unionId: def.id, refTag, unitTag, nullTag };
|
||||
const base = { repr: "single" as const, unionId: def.id, refTag, unitTag, nullTag };
|
||||
if (arm.kind === "object" && emittedClasses.has(arm.className))
|
||||
return { ...base, arm: { kind: "object", className: arm.className } };
|
||||
if (arm.kind === "record" && records.has(arm.shapeId))
|
||||
@@ -140,6 +246,34 @@ function nullableShape(
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Two or more emitted record arms and at most two unit arms (both:
|
||||
* undefined and null). */
|
||||
function shapeUnion(
|
||||
def: IrUnionDef,
|
||||
records: ReadonlySet<string>,
|
||||
boxedRecords: ReadonlySet<string>,
|
||||
): ShapeUnion | null {
|
||||
const units = def.arms.filter(isUnitType).length;
|
||||
if (units > 2 || def.arms.length - units < 2) return null;
|
||||
const arms: { tag: number; shapeId: string }[] = [];
|
||||
for (let tag = 0; tag < def.arms.length; tag++) {
|
||||
const arm = def.arms[tag]!;
|
||||
if (isUnitType(arm)) continue;
|
||||
if (arm.kind !== "record" || !records.has(arm.shapeId) || boxedRecords.has(arm.shapeId))
|
||||
return null;
|
||||
arms.push({ tag, shapeId: arm.shapeId });
|
||||
}
|
||||
let unitTag = -1,
|
||||
nullTag = -1;
|
||||
if (units === 1) unitTag = def.arms.findIndex(isUnitType);
|
||||
else if (units === 2) {
|
||||
unitTag = def.arms.findIndex((arm) => arm.kind === "undefinedT");
|
||||
nullTag = def.arms.findIndex((arm) => arm.kind === "nullT");
|
||||
if (unitTag < 0 || nullTag < 0) return null;
|
||||
}
|
||||
return { repr: "shapes", unionId: def.id, unitTag, nullTag, arms };
|
||||
}
|
||||
|
||||
/** Unions the runtime itself inspects as tagged boxes keep that
|
||||
* representation: Map keys and Set elements hash and compare union keys by
|
||||
* tag and payload inside the C map. */
|
||||
|
||||
@@ -25,6 +25,7 @@ import type { IrClassDef, IrRecordShape, IrType, IrUnionDef, SrcLoc } from "../.
|
||||
import { mapOf, STRING, typeKey } from "../../ir/ir.js";
|
||||
import { mangleClassStruct, mangleRecordStruct } from "../mangle.js";
|
||||
import { FN_ATTRS } from "./shapes.js";
|
||||
import { shapeTagFn } from "./nullable-unions.js";
|
||||
import type { InlineUnionFields } from "./inline-unions.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
|
||||
@@ -44,6 +45,10 @@ export interface PublishHost {
|
||||
readonly inlineUnions: InlineUnionFields;
|
||||
/** The nullable-pointer representation of a union, when it has one. */
|
||||
nullableArm(t: IrType): IrType | null;
|
||||
/** The union's values are tagged record pointers (nullable-unions.ts). */
|
||||
shapeUnion(t: IrType): boolean;
|
||||
/** The struct index of a record shape's first field. */
|
||||
fieldBase(shapeId: string): number;
|
||||
/** Class layout: hierarchy membership, preorder interval, field slots
|
||||
* (with their storage types), runtime roots. */
|
||||
classInfo(className: string): PublishClassInfo;
|
||||
@@ -210,6 +215,19 @@ export class PublishWalkers {
|
||||
if (def.arms.some((arm) => scalarRefusal(arm)))
|
||||
throw new LlvmUnsupportedError(`publish of ${typeKey(t)}`, loc);
|
||||
const live = arms.flatMap((a) => (a.fn === null ? [] : [{ tag: a.tag, fn: a.fn }]));
|
||||
if (this.host.shapeUnion(t)) {
|
||||
// The value is the record itself (or a unit encoding): no box to
|
||||
// mark; the arm's walker marks and walks the record.
|
||||
const lines = [head, `entry:`];
|
||||
lines.push(
|
||||
` %tag = call i32 ${shapeTagFn(t.unionId)}(ptr %o)`,
|
||||
` switch i32 %tag, label %done [ ${live.map((a) => `i32 ${a.tag}, label %arm${a.tag}`).join(" ")} ]`,
|
||||
);
|
||||
for (const a of live)
|
||||
lines.push(`arm${a.tag}:`, ` call void ${a.fn}(ptr %o, ptr %ctx)`, ` br label %done`);
|
||||
lines.push(`done:`, ` ret void`, `}`);
|
||||
return lines;
|
||||
}
|
||||
const lines = [
|
||||
head,
|
||||
`entry:`,
|
||||
@@ -240,17 +258,18 @@ export class PublishWalkers {
|
||||
const shape = this.host.recordsById.get(t.shapeId);
|
||||
if (!shape)
|
||||
throw new LlvmUnsupportedError(`publish of an unknown record ${t.shapeId}`, loc);
|
||||
const base = this.host.fieldBase(shape.id);
|
||||
const members = [
|
||||
...shape.fields.flatMap((f, i) =>
|
||||
this.host.inlineUnions.record(f.type)
|
||||
? []
|
||||
: [{ name: f.name, index: i + 1, type: f.type }],
|
||||
: [{ name: f.name, index: i + base, type: f.type }],
|
||||
),
|
||||
...(shape.indexValue
|
||||
? [
|
||||
{
|
||||
name: "[key: string]",
|
||||
index: shape.fields.length + 1,
|
||||
index: shape.fields.length + base,
|
||||
type: mapOf(STRING, shape.indexValue),
|
||||
},
|
||||
]
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, test } from "vitest";
|
||||
import {
|
||||
compileRegexMatcher,
|
||||
regexLiteralBailReason,
|
||||
regexLiteralCaptures,
|
||||
regexLiteralCompiles,
|
||||
} from "./regex-matchers.js";
|
||||
|
||||
@@ -11,6 +12,30 @@ function body(pattern: string, flags = "", vector = true): string {
|
||||
return code.lines.join("\n");
|
||||
}
|
||||
|
||||
describe("regex literal captures", () => {
|
||||
test("counts captures with the whole match and marks the ones every match sets", () => {
|
||||
expect(regexLiteralCaptures("[&<>]", "g")).toEqual({ count: 1, always: [true] });
|
||||
expect(regexLiteralCaptures("(\\w+)=(\\d)+", "")).toEqual({
|
||||
count: 3,
|
||||
always: [true, true, true],
|
||||
});
|
||||
expect(regexLiteralCaptures("(?<k>a)(b)?|(c)", "")).toEqual({
|
||||
count: 4,
|
||||
always: [true, false, false, false],
|
||||
});
|
||||
expect(regexLiteralCaptures("x(?:(a)|(b))y(c)*(d){0,2}(e){1,3}", "")).toEqual({
|
||||
count: 6,
|
||||
always: [true, false, false, false, false, true],
|
||||
});
|
||||
});
|
||||
|
||||
test("answers null outside the parsed subset", () => {
|
||||
expect(regexLiteralCaptures("(?=a)(b)", "")).toBeNull();
|
||||
expect(regexLiteralCaptures("(a)\\1", "")).toBeNull();
|
||||
expect(regexLiteralCaptures("a", "v")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("regex literal matcher subset", () => {
|
||||
test.each([
|
||||
['^(\\d+\\.\\d+\\.\\d+\\.\\d+) - - \\[([^\\]]+)\\] "(\\w+) ([^ ?"]+)(?:\\?([^ "]*))?$', ""],
|
||||
|
||||
@@ -1639,6 +1639,40 @@ function parseSubset(pattern: string, flags: string): { root: RxNode; ncap: numb
|
||||
}
|
||||
}
|
||||
|
||||
/** The capture count of a regex literal (the whole match included) and,
|
||||
* per capture, whether every match sets it: no alternation of several
|
||||
* branches and no repetition that can run zero times encloses it. Null
|
||||
* when the pattern is outside the parsed subset. */
|
||||
export function regexLiteralCaptures(
|
||||
pattern: string,
|
||||
flags: string,
|
||||
): { count: number; always: boolean[] } | null {
|
||||
const parsed = parseSubset(pattern, flags);
|
||||
if (!parsed) return null;
|
||||
const always = Array.from({ length: parsed.ncap }, (_, i) => i === 0);
|
||||
const walk = (node: RxNode, sure: boolean): void => {
|
||||
switch (node.kind) {
|
||||
case "cap":
|
||||
always[node.index] = sure;
|
||||
walk(node.body, sure);
|
||||
return;
|
||||
case "seq":
|
||||
for (const item of node.items) walk(item, sure);
|
||||
return;
|
||||
case "alt":
|
||||
for (const alt of node.alts) walk(alt, sure && node.alts.length === 1);
|
||||
return;
|
||||
case "rep":
|
||||
walk(node.body, sure && node.min > 0);
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
walk(parsed.root, true);
|
||||
return { count: parsed.ncap, always };
|
||||
}
|
||||
|
||||
/** Whether a literal compiles to a native matcher (tests and size caps). */
|
||||
export function regexLiteralCompiles(pattern: string, flags: string): boolean {
|
||||
return parseSubset(pattern, flags) !== null;
|
||||
|
||||
@@ -101,8 +101,8 @@ export class LifetimeRegions {
|
||||
/** Can a value of type `t` hold (or own something that holds) a
|
||||
* reference to a ProgramLifetime instance the walk can explain? */
|
||||
private reaches(t: IrType): boolean {
|
||||
const arm = this.host.nullableUnions.of(t)?.arm;
|
||||
if (arm !== undefined) return this.reaches(arm);
|
||||
const nullable = this.host.nullableUnions.of(t);
|
||||
if (nullable?.repr === "single") return this.reaches(nullable.arm);
|
||||
switch (t.kind) {
|
||||
case "object":
|
||||
return this.isLifetime(t.className) || this.reach.get(`object:${t.className}`) === true;
|
||||
@@ -287,7 +287,8 @@ export class LifetimeRegions {
|
||||
* ProgramLifetime instance). */
|
||||
private reportLines(struct: string, f: Field, i: number): string[] {
|
||||
if (!isRefCounted(f.type) || !this.reaches(f.type)) return [];
|
||||
const arm = this.host.nullableUnions.of(f.type)?.arm ?? f.type;
|
||||
const nullable = this.host.nullableUnions.of(f.type);
|
||||
const arm = nullable?.repr === "single" ? nullable.arm : f.type;
|
||||
const load = [
|
||||
` %f${i} = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${f.index}`,
|
||||
` %v${i} = load ptr, ptr %f${i}`,
|
||||
@@ -411,7 +412,7 @@ export class LifetimeRegions {
|
||||
...this.reportBody(
|
||||
`define internal void ${walk}(ptr %o, ptr %w) ${FN_ATTRS} { ; region walk record ${shape.id}`,
|
||||
mangleRecordStruct(shape.id),
|
||||
rcMembers(shape, this.host.inlineUnions),
|
||||
rcMembers(shape, this.host.inlineUnions, this.host.nullableUnions.fieldBase(shape.id)),
|
||||
),
|
||||
);
|
||||
rows.push(`{ ptr, ptr } { ptr @${mangleRecordRelease(shape.id)}, ptr ${walk} }`);
|
||||
|
||||
@@ -31,13 +31,23 @@ import {
|
||||
import { cycTypeDescriptor, emitObjectAlloc, emitObjectFree } from "./alloc.js";
|
||||
import type { BlockBuilder } from "./blocks.js";
|
||||
import { SCR_ARR_FIELD, llvmCommentText } from "./common.js";
|
||||
import type { NullableUnions } from "./nullable-unions.js";
|
||||
import {
|
||||
NULLABLE_NULL,
|
||||
SHAPE_WORD_INDEX,
|
||||
shapeReleaseFn,
|
||||
shapeTagFn,
|
||||
shapeTraceFn,
|
||||
shapeWordHeader,
|
||||
type NullableUnions,
|
||||
type ShapeUnion,
|
||||
} from "./nullable-unions.js";
|
||||
import {
|
||||
INLINE_UNION_FIELD,
|
||||
loadInlineUnion,
|
||||
stackUnionBox,
|
||||
type InlineUnionFields,
|
||||
} from "./inline-unions.js";
|
||||
import { recordMayKeepSource } from "./record-sources.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
|
||||
/** What the tables need from the emitter: the extern-declaration ledger
|
||||
@@ -129,6 +139,7 @@ export { computeTraced } from "../cycle-analysis.js";
|
||||
* `_v`-shaped. */
|
||||
export function vAdapters(host: ShapeHost, t: IrType): { retain: string; release: string } {
|
||||
const nullable = host.nullableUnions.of(t);
|
||||
if (nullable?.repr === "shapes") return shapeUnionRc(nullable);
|
||||
if (nullable) {
|
||||
const arm = vAdapters(host, nullable.arm);
|
||||
return { retain: nullableRetainSym(host, nullable.arm), release: arm.release };
|
||||
@@ -184,6 +195,7 @@ export function vAdapters(host: ShapeHost, t: IrType): { retain: string; release
|
||||
* Families with a mirrored fast path call the inline helper instead. */
|
||||
export function retainSym(host: ShapeHost, t: IrType): string {
|
||||
const nullable = host.nullableUnions.of(t);
|
||||
if (nullable?.repr === "shapes") return shapeUnionRc(nullable).retain;
|
||||
if (nullable) return nullableRetainSym(host, nullable.arm);
|
||||
const stem = runtimeRcStem(t);
|
||||
if (host.rcHelpers !== null && stem !== null && inlineRcFamily(host, stem, "retain") !== null) {
|
||||
@@ -195,6 +207,99 @@ export function retainSym(host: ShapeHost, t: IrType): string {
|
||||
return vAdapters(host, t).retain;
|
||||
}
|
||||
|
||||
/** A shape union's RC entry points: every record retain is the same
|
||||
* NULL- and immortal-skipping increment (the null sentinel is immortal),
|
||||
* so the first arm's serves; release dispatches on the arm. */
|
||||
function shapeUnionRc(union: ShapeUnion): { retain: string; release: string } {
|
||||
return {
|
||||
retain: `@${mangleRecordRetain(union.arms[0]!.shapeId)}`,
|
||||
release: shapeReleaseFn(union.unionId),
|
||||
};
|
||||
}
|
||||
|
||||
/** The tag, release and trace helpers of every shape union
|
||||
* (nullable-unions.ts). The tag helper maps a value to its arm: NULL and
|
||||
* the null sentinel are the unit arms, anything else a tagged record whose
|
||||
* shape word names its arm (the switch folds to a lookup table). */
|
||||
export function emitShapeUnionHelpers(host: ShapeHost): string[] {
|
||||
const out: string[] = [];
|
||||
const S = host.sizeType;
|
||||
for (const union of host.nullableUnions.allShapeUnions()) {
|
||||
const header = shapeWordHeader(S);
|
||||
const tagFn = shapeTagFn(union.unionId);
|
||||
const cases = union.arms
|
||||
.map((arm) => `i32 ${host.nullableUnions.shapeNumber(arm.shapeId)}, label %a${arm.tag}`)
|
||||
.join(" ");
|
||||
out.push(
|
||||
`define internal i32 ${tagFn}(ptr %u) alwaysinline ${FN_ATTRS} { ; shape union tag`,
|
||||
`entry:`,
|
||||
` %isnull = icmp eq ptr %u, null`,
|
||||
` br i1 %isnull, label %unit, label %ref`,
|
||||
`unit:`,
|
||||
` ret i32 ${union.unitTag}`,
|
||||
`ref:`,
|
||||
);
|
||||
if (union.nullTag >= 0)
|
||||
out.push(
|
||||
` %isnullarm = icmp eq ptr %u, ${NULLABLE_NULL}`,
|
||||
` br i1 %isnullarm, label %nullarm, label %rec`,
|
||||
`nullarm:`,
|
||||
` ret i32 ${union.nullTag}`,
|
||||
`rec:`,
|
||||
);
|
||||
out.push(
|
||||
` %wp = getelementptr inbounds ${header}, ptr %u, i32 0, i32 ${SHAPE_WORD_INDEX}`,
|
||||
` %w = load i32, ptr %wp`,
|
||||
` switch i32 %w, label %bad [ ${cases} ]`,
|
||||
...union.arms.flatMap((arm) => [`a${arm.tag}:`, ` ret i32 ${arm.tag}`]),
|
||||
`bad:`,
|
||||
` ret i32 -1`,
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
// release: unit encodings are NULL or immortal; each arm releases
|
||||
// through its own (NULL- and immortal-tolerant) helper.
|
||||
out.push(
|
||||
`define internal void ${shapeReleaseFn(union.unionId)}(ptr %u) ${FN_ATTRS} { ; shape union release`,
|
||||
`entry:`,
|
||||
` %t = call i32 ${tagFn}(ptr %u)`,
|
||||
` switch i32 %t, label %done [ ${union.arms.map((arm) => `i32 ${arm.tag}, label %a${arm.tag}`).join(" ")} ]`,
|
||||
...union.arms.flatMap((arm) => [
|
||||
`a${arm.tag}:`,
|
||||
` call void @${mangleRecordRelease(arm.shapeId)}(ptr %u)`,
|
||||
` br label %done`,
|
||||
]),
|
||||
`done:`,
|
||||
` ret void`,
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
if (shapeUnionTraced(host, union))
|
||||
out.push(
|
||||
`define internal void ${shapeTraceFn(union.unionId)}(ptr %u, ptr %visit, ptr %ctx) ${FN_ATTRS} { ; shape union trace`,
|
||||
`entry:`,
|
||||
` %t = call i32 ${tagFn}(ptr %u)`,
|
||||
` switch i32 %t, label %done [ ${union.arms.map((arm) => `i32 ${arm.tag}, label %a${arm.tag}`).join(" ")} ]`,
|
||||
...union.arms.flatMap((arm) => [
|
||||
`a${arm.tag}:`,
|
||||
` call void @${mangleRecordTrace(arm.shapeId)}(ptr %u, ptr %visit, ptr %ctx)`,
|
||||
` br label %done`,
|
||||
]),
|
||||
`done:`,
|
||||
` ret void`,
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** A shape union's arms carry collector headers (all or none: the emitter
|
||||
* traces every arm of a union with one traced arm). */
|
||||
function shapeUnionTraced(host: ShapeHost, union: ShapeUnion): boolean {
|
||||
return union.arms.some((arm) => host.tracedShapes.has(`record:${arm.shapeId}`));
|
||||
}
|
||||
|
||||
/** The retain entry point of a nullable union's value: the arm's own when
|
||||
* it tolerates NULL (emitted class and record helpers), otherwise a
|
||||
* NULL-skipping wrapper around it (the null sentinel and literals are
|
||||
@@ -233,6 +338,7 @@ export function emitNullableRetainWrappers(host: ShapeHost): string[] {
|
||||
* Families with a mirrored fast path call the inline helper instead. */
|
||||
export function releaseSym(host: ShapeHost, t: IrType): string {
|
||||
const nullable = host.nullableUnions.of(t);
|
||||
if (nullable?.repr === "shapes") return shapeUnionRc(nullable).release;
|
||||
if (nullable) return releaseSym(host, nullable.arm);
|
||||
const stem = runtimeRcStem(t);
|
||||
if (host.rcHelpers !== null && stem !== null && inlineRcFamily(host, stem, "release") !== null) {
|
||||
@@ -643,6 +749,8 @@ export function traceAdapter(host: ShapeHost, t: IrType): string | null {
|
||||
return "@scr_promise_trace_v";
|
||||
case "union": {
|
||||
const nullable = host.nullableUnions.get(t.unionId);
|
||||
if (nullable?.repr === "shapes")
|
||||
return shapeUnionTraced(host, nullable) ? shapeTraceFn(nullable.unionId) : null;
|
||||
if (nullable) return traceAdapter(host, nullable.arm);
|
||||
if (!host.tracedUnions.has(t.unionId)) return null;
|
||||
host.declare(`declare void @scr_union_trace_v(ptr, ptr, ptr)`);
|
||||
@@ -945,15 +1053,17 @@ export function mapValKindNum(value: IrType): number {
|
||||
export function rcMembers(
|
||||
shape: IrRecordShape,
|
||||
inline: InlineUnionFields,
|
||||
/** The struct index of the first field (2 on tagged shapes). */
|
||||
base = 1,
|
||||
): { index: number; type: IrType; name: string }[] {
|
||||
return [
|
||||
...shape.fields.flatMap((f, i) =>
|
||||
inline.record(f.type) ? [] : [{ index: i + 1, type: f.type, name: f.name }],
|
||||
inline.record(f.type) ? [] : [{ index: i + base, type: f.type, name: f.name }],
|
||||
),
|
||||
...(shape.indexValue
|
||||
? [
|
||||
{
|
||||
index: shape.fields.length + 1,
|
||||
index: shape.fields.length + base,
|
||||
type: mapOf(STRING, shape.indexValue),
|
||||
name: "[key: string] overflow",
|
||||
},
|
||||
@@ -1107,7 +1217,12 @@ export function directDestroyTarget(
|
||||
}
|
||||
|
||||
function directDestroyKey(host: ShapeHost, type: IrType): string | null {
|
||||
const t = host.nullableUnions.of(type)?.arm ?? type;
|
||||
const nullable = host.nullableUnions.of(type);
|
||||
let t = type;
|
||||
if (nullable !== null) {
|
||||
if (nullable.repr === "shapes") return null;
|
||||
t = nullable.arm;
|
||||
}
|
||||
if (t.kind === "record") return `record:${t.shapeId}`;
|
||||
return t.kind === "object" ? `object:${t.className}` : null;
|
||||
}
|
||||
@@ -1338,8 +1453,12 @@ export function emitRecordShapes(
|
||||
|
||||
for (const shape of records) {
|
||||
const struct = mangleRecordStruct(shape.id);
|
||||
const fieldTys = shape.fields.map((f) => llFieldType(f.type, host.inlineUnions.record(f.type)));
|
||||
const fieldTys: string[] = shape.fields.map((f) =>
|
||||
llFieldType(f.type, host.inlineUnions.record(f.type)),
|
||||
);
|
||||
if (shape.indexValue) fieldTys.push("ptr"); // the overflow ScrMap *
|
||||
// A tagged shape (an arm of a shape union) carries its shape number.
|
||||
if (host.nullableUnions.shapeNumber(shape.id) !== 0) fieldTys.unshift("i32");
|
||||
typeDefs.push(
|
||||
`%${struct} = type { ${host.sizeType}${fieldTys.length ? ", " + fieldTys.join(", ") : ""} } ` +
|
||||
`; record ${shape.id} { ${shape.fields.map((f) => llvmCommentText(f.name)).join("; ")}${shape.indexValue ? "; [key: string]" : ""} }`,
|
||||
@@ -1352,7 +1471,7 @@ export function emitRecordShapes(
|
||||
const candidates: [string, DirectDestroy][] = [];
|
||||
const memberTypes: IrType[] = [];
|
||||
for (const shape of records) {
|
||||
const members = rcMembers(shape, host.inlineUnions);
|
||||
const members = rcMembers(shape, host.inlineUnions, host.nullableUnions.fieldBase(shape.id));
|
||||
for (const m of members) memberTypes.push(m.type);
|
||||
if (!members.some((m) => isRefCounted(m.type) && needsBoundedRelease(m.type))) continue;
|
||||
const release = mangleRecordRelease(shape.id);
|
||||
@@ -1370,7 +1489,9 @@ export function emitRecordShapes(
|
||||
for (const shape of records) {
|
||||
const struct = mangleRecordStruct(shape.id);
|
||||
const traced = host.tracedShapes.has(`record:${shape.id}`);
|
||||
const members = rcMembers(shape, host.inlineUnions);
|
||||
const base = host.nullableUnions.fieldBase(shape.id);
|
||||
const shapeNumber = host.nullableUnions.shapeNumber(shape.id);
|
||||
const members = rcMembers(shape, host.inlineUnions, base);
|
||||
const refMembers = members.filter((m) => isRefCounted(m.type));
|
||||
const sizeOf = `ptrtoint (ptr getelementptr (%${struct}, ptr null, i32 1) to ${host.sizeType})`;
|
||||
|
||||
@@ -1404,7 +1525,9 @@ export function emitRecordShapes(
|
||||
}
|
||||
t++;
|
||||
}
|
||||
freeBody.push(...emitObjectFree(host, traced));
|
||||
// Closed layouts can carry a kept record source (record-sources.ts).
|
||||
const observers = recordMayKeepSource(shape) ? "weak-or-source" : "weak";
|
||||
freeBody.push(...emitObjectFree(host, traced, observers));
|
||||
defs.push(
|
||||
...releaseBody(
|
||||
host,
|
||||
@@ -1439,6 +1562,11 @@ export function emitRecordShapes(
|
||||
);
|
||||
nw.push(...emitObjectAlloc(host, sizeOf, traced ? { type: cycType } : null));
|
||||
nw.push(` store ${host.sizeType} 1, ptr %o`);
|
||||
if (shapeNumber !== 0)
|
||||
nw.push(
|
||||
` %shp = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${SHAPE_WORD_INDEX}`,
|
||||
` store i32 ${shapeNumber}, ptr %shp ; shape number`,
|
||||
);
|
||||
if (shape.indexValue) {
|
||||
// The overflow map (string-keyed): value handling is type-directed
|
||||
// exactly like shapes.ts's overflowNewC.
|
||||
@@ -1449,7 +1577,7 @@ export function emitRecordShapes(
|
||||
const trace = valKind === 2 ? traceArg(host, v) : "null";
|
||||
nw.push(
|
||||
` %ovf = call ptr @scr_map_new(i32 1, i32 ${valKind}, ptr ${host.mapOps(["null", "null", "null", rc.retain, rc.release, trace, "null"])})`,
|
||||
` %ovfp = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${shape.fields.length + 1}`,
|
||||
` %ovfp = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${shape.fields.length + base}`,
|
||||
` store ptr %ovf, ptr %ovfp`,
|
||||
);
|
||||
}
|
||||
@@ -1465,7 +1593,7 @@ export function emitRecordShapes(
|
||||
];
|
||||
let i = 0;
|
||||
for (const field of shape.fields) {
|
||||
const index = i + 1;
|
||||
const index = i + base;
|
||||
const inline = host.inlineUnions.record(field.type);
|
||||
const fieldTy = llFieldType(field.type, inline);
|
||||
clone.push(
|
||||
@@ -1514,7 +1642,7 @@ export function emitRecordShapes(
|
||||
` call void ${releaseSym(host, m.type)}(ptr %v${i}) ; ${llvmCommentText(m.name)} (acyclic)`,
|
||||
);
|
||||
});
|
||||
gf.push(...emitObjectFree(host, true), ` ret void`, `}`, ``);
|
||||
gf.push(...emitObjectFree(host, true, observers), ` ret void`, `}`, ``);
|
||||
defs.push(...gf);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import type { CallLifetimes } from "./call-lifetimes.js";
|
||||
import { emitCallExpr, emitConstruction } from "./expr-calls.js";
|
||||
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import { emitRecordLitFields } from "./expr-primitives.js";
|
||||
import { SHAPE_WORD_INDEX } from "./nullable-unions.js";
|
||||
import { llFieldType } from "./shapes.js";
|
||||
|
||||
export type StackObjectSource = IrExpr & { kind: "recordLit" | "recordClone" | "new" | "call" };
|
||||
@@ -196,12 +197,13 @@ export function stackObjectFields(
|
||||
const cached = host.stackObjectFieldPtrs.get(slot);
|
||||
if (cached) return cached;
|
||||
const struct = stackObjectStruct(type);
|
||||
const base = host.nullableUnions.fieldBase(type.shapeId);
|
||||
const fields: StackObjectField[] = [];
|
||||
host.recordShape(type.shapeId).fields.forEach((field, index) => {
|
||||
if (!isRefCounted(field.type) || host.inlineUnions.record(field.type)) return;
|
||||
const ptr = host.B.slot();
|
||||
host.B.entryAllocas.push(
|
||||
`${ptr} = getelementptr inbounds ${struct}, ptr ${slot}, i64 0, i32 ${index + 1}`,
|
||||
`${ptr} = getelementptr inbounds ${struct}, ptr ${slot}, i64 0, i32 ${index + base}`,
|
||||
);
|
||||
fields.push({ name: field.name, ptr, type: field.type });
|
||||
});
|
||||
@@ -296,6 +298,17 @@ export function emitStackObject(
|
||||
}
|
||||
}
|
||||
B.line(`store ${host.sizeType} -1, ptr ${slot}`);
|
||||
if (type.kind === "record") {
|
||||
// A tagged shape's word names it to every shape union's tag read.
|
||||
const shapeNumber = host.nullableUnions.shapeNumber(type.shapeId);
|
||||
if (shapeNumber !== 0) {
|
||||
const p = B.tmp();
|
||||
B.line(
|
||||
`${p} = getelementptr inbounds ${struct}, ptr ${slot}, i64 0, i32 ${SHAPE_WORD_INDEX}`,
|
||||
);
|
||||
B.line(`store i32 ${shapeNumber}, ptr ${p}`);
|
||||
}
|
||||
}
|
||||
if (e.kind === "call") {
|
||||
// The entry writes the result into the slot it receives last.
|
||||
emitCallExpr(
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { F64, STRING, VOID, type IrExpr, type IrFunction, type IrStmt } from "../../ir/ir.js";
|
||||
import { BOOL, F64, STRING, VOID, type IrExpr, type IrFunction, type IrStmt } from "../../ir/ir.js";
|
||||
import { analyzeCallLifetimes } from "./call-lifetimes.js";
|
||||
import { findScalarStringSlices } from "./string-slices.js";
|
||||
import {
|
||||
findLentStringParameters,
|
||||
findScalarStringSlices,
|
||||
findScratchStringSlices,
|
||||
} from "./string-slices.js";
|
||||
const loc = { file: "slices.ts", start: 0, end: 0 };
|
||||
const ref = (id: string): IrExpr => ({ kind: "varRef", localId: id, type: STRING, loc });
|
||||
function fixture(): IrFunction {
|
||||
@@ -95,3 +99,146 @@ test.each(["escape", "capture", "write", "other-consumer", "suspend"])(
|
||||
expect(facts(fn).size).toBe(0);
|
||||
},
|
||||
);
|
||||
|
||||
const slice = (receiver: string, start: number, end: number): IrExpr => ({
|
||||
kind: "strIntrinsic",
|
||||
method: "slice",
|
||||
receiver: ref(receiver),
|
||||
args: [
|
||||
{ kind: "numLit", value: start, type: F64, loc },
|
||||
{ kind: "numLit", value: end, type: F64, loc },
|
||||
],
|
||||
type: STRING,
|
||||
loc,
|
||||
});
|
||||
const equals = (left: IrExpr): IrExpr => ({
|
||||
kind: "strEq",
|
||||
negated: false,
|
||||
left,
|
||||
right: { kind: "strLit", value: "GET", type: STRING, loc },
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
const parse = (value: IrExpr): IrExpr => ({
|
||||
kind: "libCall",
|
||||
fn: "num.fromString",
|
||||
args: [value],
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
function lending(body: IrStmt[]): IrFunction {
|
||||
return {
|
||||
name: "scan",
|
||||
params: [{ localId: "source", name: "source", type: STRING }],
|
||||
locals: [
|
||||
{ id: "source", name: "source", type: STRING, mutable: false },
|
||||
{ id: "word", name: "word", type: STRING, mutable: false },
|
||||
],
|
||||
returnType: VOID,
|
||||
loc,
|
||||
body,
|
||||
};
|
||||
}
|
||||
function scratch(fn: IrFunction, extra: IrFunction[] = []): Set<IrExpr> {
|
||||
const functions = new Map([fn, ...extra].map((f) => [f.name, f]));
|
||||
const lifetimes = analyzeCallLifetimes(functions);
|
||||
return findScratchStringSlices(
|
||||
fn,
|
||||
lifetimes,
|
||||
findScalarStringSlices(fn, lifetimes),
|
||||
findLentStringParameters(functions, lifetimes),
|
||||
);
|
||||
}
|
||||
test("slices read only by lending consumers use a stack buffer", () => {
|
||||
const inline = slice("source", 0, 3);
|
||||
const parsed = slice("source", 4, 6);
|
||||
const init = slice("source", 1, 5);
|
||||
const fn = lending([
|
||||
{ kind: "exprStmt", expr: equals(inline), loc },
|
||||
{ kind: "exprStmt", expr: parse(parsed), loc },
|
||||
{ kind: "varDecl", localId: "word", init, loc },
|
||||
{ kind: "exprStmt", expr: equals(ref("word")), loc },
|
||||
{ kind: "exprStmt", expr: parse(ref("word")), loc },
|
||||
]);
|
||||
expect(scratch(fn)).toEqual(new Set([inline, parsed, init]));
|
||||
});
|
||||
test.each(["return", "concat", "slice-of-slice", "capture", "async"])(
|
||||
"a slice local that may be kept (%s) stays owned",
|
||||
(reason) => {
|
||||
const init = slice("source", 1, 5);
|
||||
const fn = lending([
|
||||
{ kind: "varDecl", localId: "word", init, loc },
|
||||
{ kind: "exprStmt", expr: equals(ref("word")), loc },
|
||||
]);
|
||||
if (reason === "return") fn.body.push({ kind: "return", value: ref("word"), loc });
|
||||
if (reason === "concat")
|
||||
fn.body.push({
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "strConcat",
|
||||
left: { kind: "strLit", value: "", type: STRING, loc },
|
||||
right: ref("word"),
|
||||
type: STRING,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
});
|
||||
if (reason === "slice-of-slice")
|
||||
fn.body.push({ kind: "exprStmt", expr: equals(slice("word", 0, 2)), loc });
|
||||
if (reason === "capture") fn.locals[1]!.boxed = true;
|
||||
if (reason === "async") fn.async = true;
|
||||
expect(scratch(fn).has(init)).toBe(false);
|
||||
},
|
||||
);
|
||||
function helper(name: string, body: IrStmt[]): IrFunction {
|
||||
return {
|
||||
name,
|
||||
params: [{ localId: "text", name: "text", type: STRING }],
|
||||
locals: [{ id: "text", name: "text", type: STRING, mutable: false }],
|
||||
returnType: BOOL,
|
||||
loc,
|
||||
body,
|
||||
};
|
||||
}
|
||||
const callOf = (callee: string, arg: IrExpr): IrExpr => ({
|
||||
kind: "call",
|
||||
callee,
|
||||
args: [arg],
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
test("helper parameters read only by lending consumers accept stack slices", () => {
|
||||
const recursive = helper("isGet", [
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "logical",
|
||||
op: "||",
|
||||
left: equals(ref("text")),
|
||||
right: callOf("isGet", ref("text")),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
]);
|
||||
const keeps = helper("keeps", [{ kind: "return", value: callOf("stores", ref("text")), loc }]);
|
||||
const stores = helper("stores", [
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "arrayLit", elems: [ref("text")], type: { kind: "array", elem: STRING }, loc },
|
||||
loc,
|
||||
},
|
||||
{ kind: "return", value: { kind: "boolLit", value: true, type: BOOL, loc }, loc },
|
||||
]);
|
||||
const lent = slice("source", 0, 3);
|
||||
const kept = slice("source", 0, 4);
|
||||
const fn = lending([
|
||||
{ kind: "exprStmt", expr: callOf("isGet", lent), loc },
|
||||
{ kind: "exprStmt", expr: callOf("keeps", kept), loc },
|
||||
]);
|
||||
const functions = [recursive, keeps, stores];
|
||||
expect(scratch(fn, functions)).toEqual(new Set([lent]));
|
||||
const all = new Map([fn, ...functions].map((f) => [f.name, f]));
|
||||
expect([...findLentStringParameters(all, analyzeCallLifetimes(all)).keys()]).toEqual(["isGet"]);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { type IrExpr, type IrFunction, type IrStmt } from "../../ir/ir.js";
|
||||
import {
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrStmt,
|
||||
type IrStrIntrinsicMethod,
|
||||
} from "../../ir/ir.js";
|
||||
import { everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse.js";
|
||||
import { F64_INF, f64Lit, sizeToDouble } from "./common.js";
|
||||
import type { CallLifetimes } from "./call-lifetimes.js";
|
||||
@@ -6,6 +11,12 @@ import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
|
||||
type Slice = IrExpr & { kind: "strIntrinsic" };
|
||||
|
||||
function isSlice(value: IrExpr): value is Slice {
|
||||
return (
|
||||
value.kind === "strIntrinsic" && (value.method === "slice" || value.method === "substring")
|
||||
);
|
||||
}
|
||||
|
||||
/** A local used exclusively for scalar indexing needs no copied bytes.
|
||||
* Snapshot the receiver and normalized bounds at its initializer, including
|
||||
* side effects in the bounds. Escapes, captures, writes, suspension, and all
|
||||
@@ -111,3 +122,234 @@ export function emitStringSliceRead(host: LlvmEmitterContext, value: Slice): LlV
|
||||
);
|
||||
return { name: result, type: value.type };
|
||||
}
|
||||
|
||||
/** Bytes of the stack buffer behind one scratch slice, header included. */
|
||||
export const SCRATCH_SLICE_BYTES = 96;
|
||||
|
||||
/** String methods that read a lent receiver or argument only during their
|
||||
* runtime call and never return it. Methods returning a string can return
|
||||
* their unchanged receiver, so they stay on the owned path. */
|
||||
const LENDING_METHODS: ReadonlySet<IrStrIntrinsicMethod> = new Set([
|
||||
"length",
|
||||
"charCodeAt",
|
||||
"indexOf",
|
||||
"includes",
|
||||
"startsWith",
|
||||
"endsWith",
|
||||
]);
|
||||
|
||||
/** Numeric parsers read their text during the call. */
|
||||
const LENDING_LIB_FNS: ReadonlySet<string> = new Set([
|
||||
"num.fromString",
|
||||
"num.parseInt",
|
||||
"num.parseFloat",
|
||||
]);
|
||||
|
||||
/** Typed string-keyed collection operations: reads probe with the key, and
|
||||
* an insertion stores scr_str_scratch_keep's copy (scr_map_append). */
|
||||
const LENDING_KEY_METHODS: ReadonlySet<string> = new Set(["get", "has", "set", "add", "delete"]);
|
||||
|
||||
function stringKeyed(value: IrExpr & { kind: "mapIntrinsic" | "setIntrinsic" }): boolean {
|
||||
const receiver = value.receiver.type;
|
||||
if (receiver.kind === "map")
|
||||
return receiver.key.kind === "string" && receiver.value.kind !== "dyn";
|
||||
if (receiver.kind === "set") return receiver.elem.kind === "string";
|
||||
return false;
|
||||
}
|
||||
|
||||
/** String parameters, by function and index, that the body reads only in
|
||||
* lending positions (findLentStringParameters). */
|
||||
export type LentStringParameters = ReadonlyMap<string, ReadonlySet<number>>;
|
||||
|
||||
/** The operands `value` reads only while it runs, keeping no reference. */
|
||||
function lentOperands(value: IrExpr, parameters: LentStringParameters): readonly IrExpr[] {
|
||||
switch (value.kind) {
|
||||
case "strEq":
|
||||
case "strCmp":
|
||||
return [value.left, value.right];
|
||||
case "strIntrinsic":
|
||||
return LENDING_METHODS.has(value.method) ? [value.receiver, ...value.args] : [];
|
||||
case "libCall":
|
||||
return LENDING_LIB_FNS.has(value.fn) ? value.args.slice(0, 1) : [];
|
||||
case "mapIntrinsic":
|
||||
case "setIntrinsic":
|
||||
return LENDING_KEY_METHODS.has(value.method) && stringKeyed(value)
|
||||
? value.args.slice(0, 1)
|
||||
: [];
|
||||
case "call": {
|
||||
const lent = parameters.get(value.callee);
|
||||
return lent ? value.args.filter((_, index) => lent.has(index)) : [];
|
||||
}
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
interface LendingScan {
|
||||
/** Slice operands of lending consumers, excluding any node also reached
|
||||
* in an owning position. */
|
||||
scratch: Set<IrExpr>;
|
||||
/** Candidates read anywhere other than a lending position. */
|
||||
invalid: Set<string>;
|
||||
/** Direct callees, for the parameter fixpoint. */
|
||||
callees: Set<string>;
|
||||
}
|
||||
|
||||
/** One pass over `fn`: which candidate bindings only lending consumers read
|
||||
* (a slice initializer of a candidate local is not itself a read), and
|
||||
* which slice operands those consumers lend. A switch discriminant is only
|
||||
* compared with the case tests. */
|
||||
function scanLending(
|
||||
fn: IrFunction,
|
||||
candidates: ReadonlySet<string>,
|
||||
initializers: ReadonlyMap<string, Slice>,
|
||||
parameters: LentStringParameters,
|
||||
): LendingScan {
|
||||
const scan: LendingScan = { scratch: new Set(), invalid: new Set(), callees: new Set() };
|
||||
const owned = new Set<IrExpr>();
|
||||
function lent(value: IrExpr): boolean {
|
||||
if (value.kind === "varRef" && candidates.has(value.localId)) return true;
|
||||
if (!isSlice(value)) return expr(value);
|
||||
scan.scratch.add(value);
|
||||
return everyExprChild(value, expr, stmt);
|
||||
}
|
||||
function expr(value: IrExpr): boolean {
|
||||
if (isSlice(value)) owned.add(value);
|
||||
if (value.kind === "varRef" && candidates.has(value.localId)) scan.invalid.add(value.localId);
|
||||
if (value.kind === "closure" || value.kind === "classRef")
|
||||
for (const id of value.captures ?? []) scan.invalid.add(id);
|
||||
if (value.kind === "call") scan.callees.add(value.callee);
|
||||
const operands = lentOperands(value, parameters);
|
||||
if (operands.length === 0) return everyExprChild(value, expr, stmt);
|
||||
return everyExprChild(
|
||||
value,
|
||||
(child) => (operands.includes(child) ? lent(child) : expr(child)),
|
||||
stmt,
|
||||
);
|
||||
}
|
||||
function stmt(value: IrStmt): boolean {
|
||||
// A candidate's single declaration initializes it with its slice.
|
||||
if (value.kind === "varDecl" && value.init !== null && initializers.has(value.localId))
|
||||
return everyExprChild(value.init, expr, stmt);
|
||||
if (value.kind === "switch" && value.disc.type.kind === "string") {
|
||||
lent(value.disc);
|
||||
for (const entry of value.cases) {
|
||||
if (entry.test) expr(entry.test);
|
||||
entry.body.forEach(stmt);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return everyStmtChild(value, expr, stmt);
|
||||
}
|
||||
fn.body.forEach(stmt);
|
||||
for (const node of owned) scan.scratch.delete(node);
|
||||
return scan;
|
||||
}
|
||||
|
||||
/** String parameters that every body reads only in lending positions,
|
||||
* including lent arguments of other such parameters: a caller may pass a
|
||||
* scratch slice to them. Start from every borrowed (unchanged) string
|
||||
* parameter of a synchronous function and drop violations until nothing changes, so
|
||||
* recursion through a lent parameter stays lent. A coroutine can keep its
|
||||
* parameters past the caller's frame, so it never lends. */
|
||||
export function findLentStringParameters(
|
||||
functions: ReadonlyMap<string, IrFunction>,
|
||||
lifetimes: CallLifetimes,
|
||||
): Map<string, Set<number>> {
|
||||
const lent = new Map<string, Set<number>>();
|
||||
for (const fn of functions.values()) {
|
||||
if (fn.async || fn.generator) continue;
|
||||
// Borrowed parameters are the unchanged ones nothing redeclares.
|
||||
const stable = lifetimes.borrowed.get(fn.name);
|
||||
const indexes = new Set<number>();
|
||||
fn.params.forEach((param, index) => {
|
||||
if (param.type.kind === "string" && stable?.has(index)) indexes.add(index);
|
||||
});
|
||||
if (indexes.size > 0) lent.set(fn.name, indexes);
|
||||
}
|
||||
const callers = new Map<string, Set<string>>();
|
||||
const pending = new Set(lent.keys());
|
||||
while (pending.size > 0) {
|
||||
const name = pending.values().next().value as string;
|
||||
pending.delete(name);
|
||||
const indexes = lent.get(name);
|
||||
const fn = functions.get(name);
|
||||
if (!indexes || !fn) continue;
|
||||
const ids = new Set([...indexes].map((index) => fn.params[index]!.localId));
|
||||
const scan = scanLending(fn, ids, new Map(), lent);
|
||||
for (const callee of scan.callees) {
|
||||
let set = callers.get(callee);
|
||||
if (!set) callers.set(callee, (set = new Set()));
|
||||
set.add(name);
|
||||
}
|
||||
let changed = false;
|
||||
for (const index of [...indexes]) {
|
||||
if (!scan.invalid.has(fn.params[index]!.localId)) continue;
|
||||
indexes.delete(index);
|
||||
changed = true;
|
||||
}
|
||||
if (!changed) continue;
|
||||
if (indexes.size === 0) lent.delete(name);
|
||||
const affected = callers.get(name);
|
||||
if (affected) for (const caller of affected) if (lent.has(caller)) pending.add(caller);
|
||||
}
|
||||
return lent;
|
||||
}
|
||||
|
||||
/** Slices whose every reader borrows them are written into a stack buffer
|
||||
* instead of a heap allocation (scr_str_slice_scratch): a slice operand of
|
||||
* a lending consumer, and the initializer of a stable local read only by
|
||||
* lending consumers. The copy is ASCII, bounded, and immortal for retain and
|
||||
* release, so it neither pins its source nor needs to be freed; longer or
|
||||
* non-ASCII results keep the ordinary owned string. Each site owns its
|
||||
* buffer: a site runs again only after its previous value is dead, since
|
||||
* nothing can keep it. Coroutine frames do not keep stack buffers across a
|
||||
* suspension, so they are left alone. */
|
||||
export function findScratchStringSlices(
|
||||
fn: IrFunction,
|
||||
lifetimes: CallLifetimes,
|
||||
scalar: ReadonlyMap<string, Slice>,
|
||||
parameters: LentStringParameters = new Map(),
|
||||
): Set<IrExpr> {
|
||||
if (fn.async || fn.generator) return new Set();
|
||||
const bindings = lifetimes.bindings.get(fn.name);
|
||||
const locals = new Map<string, Slice>();
|
||||
everyStmtList(fn.body, {
|
||||
expr: () => true,
|
||||
stmt: (stmt) => {
|
||||
if (
|
||||
stmt.kind === "varDecl" &&
|
||||
bindings?.has(stmt.localId) &&
|
||||
!scalar.has(stmt.localId) &&
|
||||
stmt.init !== null &&
|
||||
isSlice(stmt.init)
|
||||
)
|
||||
locals.set(stmt.localId, stmt.init);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
const scan = scanLending(fn, new Set(locals.keys()), locals, parameters);
|
||||
for (const [id, init] of locals) if (!scan.invalid.has(id)) scan.scratch.add(init);
|
||||
return scan.scratch;
|
||||
}
|
||||
|
||||
/** Write a scratch slice into its own stack buffer. The result is owned:
|
||||
* releasing the scratch copy does nothing, and a heap fallback is freed. */
|
||||
export function emitScratchStringSlice(
|
||||
host: LlvmEmitterContext,
|
||||
slice: Slice,
|
||||
receiver: LlValue,
|
||||
args: readonly LlValue[],
|
||||
): LlValue {
|
||||
const B = host.B;
|
||||
const buffer = B.slot();
|
||||
B.entryAllocas.push(`${buffer} = alloca [${SCRATCH_SLICE_BYTES} x i8], align 8`);
|
||||
host.declare(
|
||||
`declare ptr @scr_str_slice_scratch(ptr, double, double, i1 zeroext, ptr, ${host.sizeType})`,
|
||||
);
|
||||
const result = B.tmp();
|
||||
B.line(
|
||||
`${result} = call ptr @scr_str_slice_scratch(ptr ${receiver.name}, double ${args[0]?.name ?? f64Lit(0)}, double ${args[1]?.name ?? F64_INF}, i1 ${slice.method === "substring" ? "true" : "false"}, ptr ${buffer}, ${host.sizeType} ${SCRATCH_SLICE_BYTES})`,
|
||||
);
|
||||
return host.own({ name: result, type: slice.type });
|
||||
}
|
||||
|
||||
@@ -2,14 +2,19 @@
|
||||
* helper generators (walkers, dyn). A tagged union is a `%ScrUnion` box; a
|
||||
* nullable union (nullable-unions.ts) is the arm pointer itself: NULL for
|
||||
* its NULL-encoded unit arm and, in `C | null | undefined`, the immortal
|
||||
* NULLABLE_NULL sentinel for `null`. Every structural read of a union value
|
||||
* NULLABLE_NULL sentinel for `null`. A shape union's value is likewise the
|
||||
* record pointer; its tag comes from the record's shape word. Every structural read of a union value
|
||||
* goes through here. */
|
||||
import type { BlockBuilder } from "./blocks.js";
|
||||
import { NULLABLE_NULL, type NullableUnion } from "./nullable-unions.js";
|
||||
import { NULLABLE_NULL, shapeTagFn, type PointerUnion } from "./nullable-unions.js";
|
||||
|
||||
/** The i32 tag of a union value. */
|
||||
export function emitUnionTag(B: BlockBuilder, nullable: NullableUnion | null, u: string): string {
|
||||
export function emitUnionTag(B: BlockBuilder, nullable: PointerUnion | null, u: string): string {
|
||||
const t = B.tmp();
|
||||
if (nullable?.repr === "shapes") {
|
||||
B.line(`${t} = call i32 ${shapeTagFn(nullable.unionId)}(ptr ${u})`);
|
||||
return t;
|
||||
}
|
||||
if (nullable) {
|
||||
const isNull = B.tmp();
|
||||
B.line(`${isNull} = icmp eq ptr ${u}, null`);
|
||||
@@ -32,8 +37,13 @@ export function emitUnionTag(B: BlockBuilder, nullable: NullableUnion | null, u:
|
||||
|
||||
/** i1: a nullable union value holds its reference arm (neither NULL nor
|
||||
* the `null` sentinel). */
|
||||
export function emitNullablePresent(B: BlockBuilder, nullable: NullableUnion, u: string): string {
|
||||
export function emitNullablePresent(B: BlockBuilder, nullable: PointerUnion, u: string): string {
|
||||
const present = B.tmp();
|
||||
if (nullable.repr === "shapes" && nullable.unitTag < 0) {
|
||||
// No unit arm: every value is a record (NULL only as a pending dummy).
|
||||
B.line(`${present} = icmp ne ptr ${u}, null`);
|
||||
return present;
|
||||
}
|
||||
B.line(`${present} = icmp ne ptr ${u}, null`);
|
||||
if (nullable.nullTag < 0) return present;
|
||||
const notNullArm = B.tmp(),
|
||||
@@ -46,12 +56,18 @@ export function emitNullablePresent(B: BlockBuilder, nullable: NullableUnion, u:
|
||||
/** i1: a nullable union value holds the arm `tag` (`negated`: does not). */
|
||||
export function emitNullableIsTag(
|
||||
B: BlockBuilder,
|
||||
nullable: NullableUnion,
|
||||
nullable: PointerUnion,
|
||||
u: string,
|
||||
tag: number,
|
||||
negated: boolean,
|
||||
): string {
|
||||
if (tag === nullable.refTag) {
|
||||
if (nullable.repr === "shapes" && tag !== nullable.unitTag && tag !== nullable.nullTag) {
|
||||
const actual = emitUnionTag(B, nullable, u),
|
||||
t = B.tmp();
|
||||
B.line(`${t} = icmp ${negated ? "ne" : "eq"} i32 ${actual}, ${tag}`);
|
||||
return t;
|
||||
}
|
||||
if (nullable.repr === "single" && tag === nullable.refTag) {
|
||||
const present = emitNullablePresent(B, nullable, u);
|
||||
if (!negated) return present;
|
||||
const t = B.tmp();
|
||||
@@ -65,7 +81,7 @@ export function emitNullableIsTag(
|
||||
}
|
||||
|
||||
/** The BORROWED payload pointer of a reference arm. */
|
||||
export function emitUnionPeek(B: BlockBuilder, nullable: NullableUnion | null, u: string): string {
|
||||
export function emitUnionPeek(B: BlockBuilder, nullable: PointerUnion | null, u: string): string {
|
||||
if (nullable) return u;
|
||||
const p = B.tmp();
|
||||
const t = B.tmp();
|
||||
@@ -76,7 +92,9 @@ export function emitUnionPeek(B: BlockBuilder, nullable: NullableUnion | null, u
|
||||
|
||||
/** Text-template forms for helpers written as raw LLVM lines: `%out`
|
||||
* receives the tag / payload of `u`. */
|
||||
export function unionTagLines(nullable: NullableUnion | null, u: string, out: string): string[] {
|
||||
export function unionTagLines(nullable: PointerUnion | null, u: string, out: string): string[] {
|
||||
if (nullable?.repr === "shapes")
|
||||
return [` ${out} = call i32 ${shapeTagFn(nullable.unionId)}(ptr ${u})`];
|
||||
if (nullable && nullable.nullTag < 0)
|
||||
return [
|
||||
` ${out}.isnull = icmp eq ptr ${u}, null`,
|
||||
@@ -95,7 +113,7 @@ export function unionTagLines(nullable: NullableUnion | null, u: string, out: st
|
||||
];
|
||||
}
|
||||
|
||||
export function unionPeekLines(nullable: NullableUnion | null, u: string, out: string): string[] {
|
||||
export function unionPeekLines(nullable: PointerUnion | null, u: string, out: string): string[] {
|
||||
// A plain copy of the pointer (NULL included) under the template's name.
|
||||
if (nullable) return [` ${out} = getelementptr i8, ptr ${u}, i64 0`];
|
||||
return [
|
||||
|
||||
@@ -183,7 +183,9 @@ test("a nullable-pointer source rebuilds the target box", () => {
|
||||
expect(retag).not.toContain("retype.share");
|
||||
});
|
||||
|
||||
async function llvmFor(source: string): Promise<{ module: IrModule; ir: string }> {
|
||||
/** `boxed`: keep every union a tagged box (record unions otherwise become
|
||||
* shape unions, which retype without any box). */
|
||||
async function llvmFor(source: string, boxed = true): Promise<{ module: IrModule; ir: string }> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-union-retype-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts"),
|
||||
@@ -194,7 +196,7 @@ async function llvmFor(source: string): Promise<{ module: IrModule; ir: string }
|
||||
throw new Error(result.diagnostics.map((item) => `${item.code}: ${item.message}`).join("\n"));
|
||||
const module = deserializeModule(await readFile(output, "utf8"));
|
||||
expect(validateModule(module)).toEqual([]);
|
||||
return { module, ir: emitLlvmModule(module) };
|
||||
return { module, ir: emitLlvmModule(boxed ? keepUnionsBoxed(module) : module) };
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
@@ -253,3 +255,16 @@ console.log(leafValue(parsed));
|
||||
expect(stored).not.toBeNull();
|
||||
expect(ir).toContain(`load i8, ptr ${stored![1]}`);
|
||||
});
|
||||
|
||||
test("record unions retype as the record pointer itself", async () => {
|
||||
const { module, ir } = await llvmFor(VISITOR, false);
|
||||
const helpers = module.functions.filter((fn) => fn.name.startsWith("%union.retag."));
|
||||
expect(helpers.length).toBeGreaterThan(0);
|
||||
for (const helper of helpers) {
|
||||
const symbol = `sc_bf__x25_union_retag_${helper.name.slice("%union.retag.".length)}`;
|
||||
const retag = body(ir, symbol);
|
||||
expect(retag).not.toMatch(/@scr_union_new|retype\.share|sc_retype_guard/);
|
||||
}
|
||||
// No record of the visitor is ever boxed.
|
||||
expect(ir).not.toMatch(/call ptr @scr_union_new_ref/);
|
||||
});
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
traceAdapter,
|
||||
type ShapeHost,
|
||||
} from "./shapes.js";
|
||||
import { disposeHookLoadLines, RECORD_SOURCES_GATE } from "./alloc.js";
|
||||
import { recordMayKeepSource, recordSourceFieldOrder } from "./record-sources.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
import { emitUnionPeek, emitUnionTag } from "./union-repr.js";
|
||||
@@ -284,7 +285,8 @@ export class LlWalkers {
|
||||
throw new InternalCompilerError(
|
||||
`llvm emitter bug: jsonStringify of unknown shape ${shapeId}`,
|
||||
);
|
||||
const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + 1]));
|
||||
const fieldBase = this.host.nullableUnions.fieldBase(shapeId);
|
||||
const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + fieldBase]));
|
||||
if (recordMayKeepSource(shape)) this.emitRecordSourceWriter(B, shapeId);
|
||||
// CYCLE-CAPABLE shapes bracket the walk with the circular-detection
|
||||
// stack; edge labels stamp before members whose walk can re-enter —
|
||||
@@ -396,6 +398,24 @@ export class LlWalkers {
|
||||
const writer = this.recordFieldWriter(shapeId);
|
||||
const lSource = B.newLabel("jw.source");
|
||||
const lNative = B.newLabel("jw.native");
|
||||
// Only a program that can keep record sources looks one up (the gate
|
||||
// folds to a constant), and only once one was kept: keeping a source
|
||||
// installs the dispose hook, so a NULL hook means the table is empty.
|
||||
const lHooked = B.newLabel("jw.hooked");
|
||||
const kept = B.tmp();
|
||||
B.line(`${kept} = load i1, ptr ${RECORD_SOURCES_GATE}`);
|
||||
B.condBr(kept, lHooked, lNative);
|
||||
B.startBlock(lHooked);
|
||||
const hook = B.tmp();
|
||||
const hookLines = disposeHookLoadLines(host, hook);
|
||||
if (hookLines !== null) {
|
||||
const lProbe = B.newLabel("jw.probe");
|
||||
for (const line of hookLines) B.line(line.trimStart());
|
||||
const installed = B.tmp();
|
||||
B.line(`${installed} = icmp ne ptr ${hook}, null`);
|
||||
B.condBr(installed, lProbe, lNative);
|
||||
B.startBlock(lProbe);
|
||||
}
|
||||
if (traceAdapter(host, { kind: "record", shapeId }) === null) {
|
||||
// One lookup: the runtime writes the record when it kept a source.
|
||||
host.declare(`declare zeroext i1 @scr_jb_try_record_source(ptr, ptr, ptr)`);
|
||||
@@ -437,7 +457,8 @@ export class LlWalkers {
|
||||
throw new InternalCompilerError(`llvm emitter bug: field writer of unknown shape ${shapeId}`);
|
||||
const name = `sc_jwf_${this.recordFieldWriters.size}`;
|
||||
this.recordFieldWriters.set(shapeId, name);
|
||||
const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + 1]));
|
||||
const fieldBase = this.host.nullableUnions.fieldBase(shapeId);
|
||||
const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + fieldBase]));
|
||||
const byName = new Map(shape.fields.map((f) => [f.name, f]));
|
||||
const cyclic = traceAdapter(this.host, { kind: "record", shapeId }) !== null;
|
||||
const order = recordSourceFieldOrder(shape);
|
||||
@@ -534,7 +555,7 @@ export class LlWalkers {
|
||||
const ovfp = B.tmp();
|
||||
const ovf = B.tmp();
|
||||
B.line(
|
||||
`${ovfp} = getelementptr inbounds %${mangleRecordStruct(shape.id)}, ptr %v, i64 0, i32 ${shape.fields.length + 1}`,
|
||||
`${ovfp} = getelementptr inbounds %${mangleRecordStruct(shape.id)}, ptr %v, i64 0, i32 ${shape.fields.length + this.host.nullableUnions.fieldBase(shape.id)}`,
|
||||
);
|
||||
B.line(`${ovf} = load ptr, ptr ${ovfp} ; overflow map`);
|
||||
if (iv.kind === "dyn") {
|
||||
|
||||
@@ -204,6 +204,11 @@ export function mangleGenSpawn(fnName: string): string {
|
||||
export function mangleGenDrop(fnName: string): string {
|
||||
return `sc_gd_${sanitize(fnName)}`;
|
||||
}
|
||||
/** The never-started return() hook of a generator that closes its source
|
||||
* argument (an iterator helper). */
|
||||
export function mangleGenSourceClose(fnName: string): string {
|
||||
return `sc_gc_${sanitize(fnName)}`;
|
||||
}
|
||||
/** Emitted ref-kind resolve thunk, interned per inner-type key. */
|
||||
export function mangleResolveThunk(n: number): string {
|
||||
return `sc_rslv_${n}`;
|
||||
|
||||
@@ -580,7 +580,7 @@ function breaksTo(
|
||||
|
||||
/** Statements no execution reaches: those after a statement that cannot
|
||||
* complete normally in the same statement list. */
|
||||
function unreachableStatements(body: readonly IrStmt[]): Set<IrStmt> {
|
||||
export function unreachableStatements(body: readonly IrStmt[]): Set<IrStmt> {
|
||||
const dead = new Set<IrStmt>();
|
||||
const { statement } = completion();
|
||||
const list = (stmts: readonly IrStmt[]): void => {
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import { expect, test } from "vitest";
|
||||
import {
|
||||
F64,
|
||||
STRING,
|
||||
VOID,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrModule,
|
||||
type IrStmt,
|
||||
} from "../ir/ir.js";
|
||||
import { captureFreeFunctions, observesStacks } from "./stack-observations.js";
|
||||
|
||||
const loc = { file: "stack-observations.ts", start: 0, end: 0 };
|
||||
const ERROR = { kind: "object", className: "%Error" } as const;
|
||||
const err: IrExpr = { loc, kind: "varRef", localId: "e", type: ERROR };
|
||||
const stackRead: IrStmt = {
|
||||
loc,
|
||||
kind: "exprStmt",
|
||||
expr: { loc, kind: "libCall", fn: "error.stack", args: [err], type: STRING },
|
||||
};
|
||||
const call = (callee: string): IrStmt => ({
|
||||
loc,
|
||||
kind: "exprStmt",
|
||||
expr: { loc, kind: "call", callee, args: [], type: VOID },
|
||||
});
|
||||
const throwStmt: IrStmt = { loc, kind: "throw", value: err };
|
||||
const ret: IrStmt = { loc, kind: "return", value: null };
|
||||
const fn = (name: string, body: IrStmt[]): IrFunction => ({
|
||||
loc,
|
||||
name,
|
||||
params: [],
|
||||
locals: [],
|
||||
returnType: VOID,
|
||||
body,
|
||||
});
|
||||
const mod = (functions: IrFunction[], extra: Partial<IrModule> = {}): IrModule => ({
|
||||
irVersion: 16,
|
||||
sourceFile: "stack-observations.ts",
|
||||
entry: "%main",
|
||||
functions,
|
||||
classes: [],
|
||||
records: [],
|
||||
unions: [],
|
||||
globals: [],
|
||||
...extra,
|
||||
});
|
||||
|
||||
test("a reachable stack read is observed", () => {
|
||||
expect(observesStacks(mod([fn("%main", [call("parse")]), fn("parse", [stackRead])]))).toBe(true);
|
||||
});
|
||||
|
||||
test("programs without stack reads are not", () => {
|
||||
expect(observesStacks(mod([fn("%main", [call("helper")]), fn("helper", [ret])]))).toBe(false);
|
||||
});
|
||||
|
||||
test("reads after an unconditional throw or return never run", () => {
|
||||
expect(observesStacks(mod([fn("%main", [throwStmt, stackRead])]))).toBe(false);
|
||||
expect(observesStacks(mod([fn("%main", [ret, call("parse")]), fn("parse", [stackRead])]))).toBe(
|
||||
false,
|
||||
);
|
||||
// A terminator ends only its own list: the outer list continues.
|
||||
const cond: IrExpr = { loc, kind: "numLit", value: 1, type: F64 };
|
||||
expect(
|
||||
observesStacks(
|
||||
mod([fn("%main", [{ kind: "if", cond, then: [ret], else_: null, loc }, stackRead])]),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("functions referenced only from dead code are unreached", () => {
|
||||
// The production React shape: a DEV-only body behind a throw stub calls
|
||||
// the stack parser, and a closure created after the stub reaches it too.
|
||||
const closure: IrStmt = {
|
||||
loc,
|
||||
kind: "assign",
|
||||
localId: "replacer",
|
||||
value: {
|
||||
loc,
|
||||
kind: "closure",
|
||||
fnName: "replacer",
|
||||
captures: [],
|
||||
type: { kind: "func", params: [], ret: VOID },
|
||||
},
|
||||
};
|
||||
const program = mod([
|
||||
fn("%main", [call("outline")]),
|
||||
fn("outline", [throwStmt, closure, call("parse")]),
|
||||
fn("replacer", [call("parse")]),
|
||||
fn("parse", [stackRead]),
|
||||
]);
|
||||
expect(observesStacks(program)).toBe(false);
|
||||
// The same closure created before the stub keeps the read reachable.
|
||||
const live = mod([
|
||||
fn("%main", [call("outline")]),
|
||||
fn("outline", [closure, throwStmt]),
|
||||
fn("replacer", [call("parse")]),
|
||||
fn("parse", [stackRead]),
|
||||
]);
|
||||
expect(observesStacks(live)).toBe(true);
|
||||
});
|
||||
|
||||
test("class methods are roots", () => {
|
||||
const program = mod([fn("%main", [ret]), fn("%Parser.read", [stackRead])], {
|
||||
classes: [
|
||||
{ name: "Parser", fields: [], methods: ["read"] } as unknown as NonNullable<
|
||||
IrModule["classes"]
|
||||
>[number],
|
||||
],
|
||||
});
|
||||
expect(observesStacks(program)).toBe(true);
|
||||
});
|
||||
|
||||
test("leaf functions that cannot create an Error need no frame", () => {
|
||||
const num = (value: number): IrExpr => ({ loc, kind: "numLit", value, type: F64 });
|
||||
const add: IrStmt = {
|
||||
loc,
|
||||
kind: "return",
|
||||
value: { loc, kind: "bin", op: "+", left: num(1), right: num(2), type: F64 },
|
||||
};
|
||||
const construct: IrStmt = {
|
||||
loc,
|
||||
kind: "exprStmt",
|
||||
expr: { loc, kind: "libCall", fn: "error.new", args: [], type: ERROR },
|
||||
};
|
||||
const program = mod([
|
||||
fn("%main", [call("leaf"), call("caller"), call("creates"), call("viaCreates")]),
|
||||
fn("leaf", [add]),
|
||||
fn("caller", [call("leaf"), add]),
|
||||
fn("creates", [construct]),
|
||||
fn("viaCreates", [call("creates")]),
|
||||
fn("recursive", [call("recursive")]),
|
||||
]);
|
||||
const free = captureFreeFunctions(program, new Set(["recursive"]), null);
|
||||
expect([...free].sort()).toEqual(["caller", "leaf"]);
|
||||
// A worker program keeps every frame.
|
||||
expect(captureFreeFunctions({ ...program, workers: true }, null, null).size).toBe(0);
|
||||
});
|
||||
@@ -0,0 +1,237 @@
|
||||
/* Whole-program stack-trace gate (see observesStacks). Pure function of the
|
||||
* IR module; the emitter keeps a source frame in every function only when
|
||||
* it answers true. */
|
||||
import { exprFunctionReference, moduleFunctionReferences } from "../ir/function-references.js";
|
||||
import { MAY_THROW_LIB_FNS } from "../ir/builtin-effects.js";
|
||||
import type { IrExpr, IrFunction, IrModule, IrStmt } from "../ir/ir.js";
|
||||
import { MAY_THROW_ARR_METHODS } from "../ir/ir.js";
|
||||
import { everyExprChild, everyStmtChild, everyStmtList } from "../ir/traverse.js";
|
||||
import { unreachableStatements } from "./may-throw.js";
|
||||
import type { PublishedTypes } from "./publication.js";
|
||||
import { writesPublished } from "./publication.js";
|
||||
|
||||
/** Lib calls that read a captured stack trace. */
|
||||
const STACK_READS: ReadonlySet<string> = new Set(["error.stack"]);
|
||||
|
||||
function readsStack(expr: IrExpr): boolean {
|
||||
return expr.kind === "libCall" && STACK_READS.has(expr.fn);
|
||||
}
|
||||
|
||||
function containsStackRead(fn: IrFunction): boolean {
|
||||
return !everyStmtList(fn.body, { stmt: () => true, expr: (expr) => !readsStack(expr) });
|
||||
}
|
||||
|
||||
/** Visit every expression of `body` that execution can reach: statements
|
||||
* after one that cannot complete normally in the same list are skipped. */
|
||||
function visitLiveExprs(body: IrStmt[], visit: (expr: IrExpr) => void): void {
|
||||
const dead = unreachableStatements(body);
|
||||
const expr = (node: IrExpr): boolean => {
|
||||
visit(node);
|
||||
return everyExprChild(node, expr, stmt);
|
||||
};
|
||||
const stmt = (node: IrStmt): boolean => dead.has(node) || everyStmtChild(node, expr, stmt);
|
||||
body.every(stmt);
|
||||
}
|
||||
|
||||
/** Whether the program can read a captured stack trace, which is what makes
|
||||
* every function keep a source frame. A read counts when it can execute:
|
||||
* it sits in live code of a function reachable from the module's roots (its
|
||||
* entry, library exports, class prototype helpers and every class's
|
||||
* methods) through live code. Code after a statement that cannot complete
|
||||
* normally is not live: production builds of libraries such as React's
|
||||
* Flight server keep development-only stack parsing behind `throw` stubs,
|
||||
* and those reads must not cost every call of the program a frame.
|
||||
* Programs without any stack read answer false without the walk. */
|
||||
export function observesStacks(mod: IrModule): boolean {
|
||||
if (!mod.functions.some(containsStackRead)) return false;
|
||||
const byName = new Map(mod.functions.map((fn) => [fn.name, fn]));
|
||||
const classNames = new Set((mod.classes ?? []).map((cls) => cls.name));
|
||||
const pending: string[] = [...moduleFunctionReferences(mod)];
|
||||
// Class methods, accessors and constructors are reachable through
|
||||
// dispatch the IR does not name per site: all of them are roots.
|
||||
for (const fn of mod.functions) {
|
||||
if (!fn.name.startsWith("%")) continue;
|
||||
for (let dot = fn.name.indexOf("."); dot !== -1; dot = fn.name.indexOf(".", dot + 1)) {
|
||||
if (classNames.has(fn.name.slice(1, dot))) {
|
||||
pending.push(fn.name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const reached = new Set<string>();
|
||||
let observed = false;
|
||||
while (pending.length > 0 && !observed) {
|
||||
const name = pending.pop()!;
|
||||
if (reached.has(name)) continue;
|
||||
reached.add(name);
|
||||
const fn = byName.get(name);
|
||||
if (fn === undefined) continue;
|
||||
visitLiveExprs(fn.body, (expr) => {
|
||||
if (readsStack(expr)) observed = true;
|
||||
if (expr.kind === "call" && !reached.has(expr.callee)) pending.push(expr.callee);
|
||||
const referenced = exprFunctionReference(expr);
|
||||
if (referenced !== undefined && !reached.has(referenced)) pending.push(referenced);
|
||||
});
|
||||
}
|
||||
return observed;
|
||||
}
|
||||
|
||||
/** Expressions that neither create an Error nor throw one by themselves.
|
||||
* Calls, `new` and virtual calls are capture-free when their callees are. */
|
||||
const CAPTURE_FREE_EXPRS: ReadonlySet<IrExpr["kind"]> = new Set([
|
||||
"numLit",
|
||||
"boolLit",
|
||||
"strLit",
|
||||
"unitLit",
|
||||
"varRef",
|
||||
"incDec",
|
||||
"assignExpr",
|
||||
"bin",
|
||||
"unary",
|
||||
"logical",
|
||||
"ternary",
|
||||
"nullish",
|
||||
"toBool",
|
||||
"fieldGet",
|
||||
"recordGet",
|
||||
"arrayGet",
|
||||
"arrayHas",
|
||||
"arrayState",
|
||||
"strCmp",
|
||||
"strEq",
|
||||
"strConcat",
|
||||
"upcast",
|
||||
"unionDisc",
|
||||
"unionIsTag",
|
||||
"unionEq",
|
||||
"instanceOf",
|
||||
"recordLit",
|
||||
"arrayLit",
|
||||
"unionWrap",
|
||||
"unionNarrow",
|
||||
"libCall",
|
||||
"arrIntrinsic",
|
||||
"call",
|
||||
"new",
|
||||
"virtualCall",
|
||||
]);
|
||||
|
||||
/** Statements that neither create an Error nor throw one by themselves. */
|
||||
const CAPTURE_FREE_STMTS: ReadonlySet<IrStmt["kind"]> = new Set([
|
||||
"varDecl",
|
||||
"assign",
|
||||
"exprStmt",
|
||||
"if",
|
||||
"while",
|
||||
"doWhile",
|
||||
"for",
|
||||
"switch",
|
||||
"block",
|
||||
"return",
|
||||
"break",
|
||||
"continue",
|
||||
"fieldSet",
|
||||
"recordSet",
|
||||
"arraySet",
|
||||
]);
|
||||
|
||||
/** Functions whose frame no stack capture can ever include: a capture runs
|
||||
* where an Error is created (by the program or by a throwing runtime
|
||||
* operation), and these functions only perform operations from a fixed
|
||||
* list that never create one and only call functions of the same kind.
|
||||
* Their frames are therefore never observable and the emitter omits them,
|
||||
* which keeps small leaf methods and helpers free of the per-call frame
|
||||
* push in programs that read stacks. Recursive functions (the stack guard
|
||||
* throws RangeError), async and generator bodies, and every function of a
|
||||
* worker program keep their frames. */
|
||||
export function captureFreeFunctions(
|
||||
mod: IrModule,
|
||||
stackChecks: ReadonlySet<string> | null,
|
||||
published: PublishedTypes | null,
|
||||
): Set<string> {
|
||||
const free = new Set<string>();
|
||||
if (mod.workers === true) return free;
|
||||
const methodImpls = new Map<string, string[]>();
|
||||
for (const cls of mod.classes ?? []) {
|
||||
for (const m of cls.methods ?? []) {
|
||||
let list = methodImpls.get(m);
|
||||
if (!list) methodImpls.set(m, (list = []));
|
||||
list.push(`%${cls.name}.${m}`);
|
||||
}
|
||||
}
|
||||
const tdzGlobals = new Set((mod.globals ?? []).filter((g) => g.tdz).map((g) => g.id));
|
||||
const callees = new Map<string, string[]>();
|
||||
for (const fn of mod.functions) {
|
||||
if (fn.async === true || fn.generator !== undefined || stackChecks?.has(fn.name)) continue;
|
||||
const tdz = new Set([...tdzGlobals, ...fn.locals.filter((l) => l.tdz).map((l) => l.id)]);
|
||||
const calls: string[] = [];
|
||||
const bigint = (e: IrExpr): boolean => e.type.kind === "bigint";
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
if (!CAPTURE_FREE_EXPRS.has(e.kind)) return false;
|
||||
switch (e.kind) {
|
||||
case "varRef":
|
||||
case "incDec":
|
||||
case "assignExpr":
|
||||
if (tdz.has(e.localId)) return false;
|
||||
break;
|
||||
case "bin":
|
||||
// BigInt division and remainder by zero throw RangeError.
|
||||
if (bigint(e) || bigint(e.left) || bigint(e.right)) return false;
|
||||
break;
|
||||
case "unary":
|
||||
if (bigint(e) || bigint(e.operand)) return false;
|
||||
break;
|
||||
case "libCall":
|
||||
// Math functions only: they neither throw nor call back.
|
||||
if (!e.fn.startsWith("math.") || MAY_THROW_LIB_FNS.has(e.fn)) return false;
|
||||
break;
|
||||
case "arrIntrinsic": {
|
||||
// Methods that cannot throw, over numbers, booleans or strings
|
||||
// (no element conversion can run program code) and without a
|
||||
// callback argument.
|
||||
const elem = e.receiver.type.kind === "array" ? e.receiver.type.elem.kind : null;
|
||||
if (
|
||||
MAY_THROW_ARR_METHODS.has(e.method) ||
|
||||
(elem !== "f64" && elem !== "bool" && elem !== "string") ||
|
||||
e.args.some((arg) => arg.type.kind === "func")
|
||||
)
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
case "call":
|
||||
calls.push(e.callee);
|
||||
break;
|
||||
case "new":
|
||||
calls.push(`%${e.className}.constructor`);
|
||||
break;
|
||||
case "virtualCall": {
|
||||
const impls = methodImpls.get(e.method);
|
||||
if (impls === undefined) return false;
|
||||
calls.push(...impls);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return everyExprChild(e, expr, stmt);
|
||||
};
|
||||
const stmt = (s: IrStmt): boolean => {
|
||||
if (!CAPTURE_FREE_STMTS.has(s.kind)) return false;
|
||||
if (s.kind === "assign" && tdz.has(s.localId)) return false;
|
||||
if (published !== null && writesPublished(published, s)) return false;
|
||||
return everyStmtChild(s, expr, stmt);
|
||||
};
|
||||
if (fn.body.every(stmt)) callees.set(fn.name, calls);
|
||||
}
|
||||
// Drop candidates that call a function outside the set until none do.
|
||||
for (const name of callees.keys()) free.add(name);
|
||||
let changed = true;
|
||||
while (changed) {
|
||||
changed = false;
|
||||
for (const [name, calls] of callees) {
|
||||
if (free.has(name) && !calls.every((callee) => free.has(callee))) {
|
||||
free.delete(name);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return free;
|
||||
}
|
||||
@@ -7,8 +7,10 @@ import {
|
||||
coverageEnvelope,
|
||||
coveragePasses,
|
||||
renderCoverage,
|
||||
renderExplainDynamic,
|
||||
type CoverageFailOn,
|
||||
} from "../coverage/report.js";
|
||||
import { TIER_MODES, type TierMode } from "../frontend/tier-analysis.js";
|
||||
import { buildEnvelope } from "../diagnostics/envelope.js";
|
||||
import { renderWarnings } from "../diagnostics/render.js";
|
||||
import { isNativeOptimization } from "../backend/optimization.js";
|
||||
@@ -45,8 +47,15 @@ function parseCli(
|
||||
): ReturnType<
|
||||
typeof parseArgs<{ options: typeof CLI_OPTIONS; allowPositionals: true; allowNegative: true }>
|
||||
> {
|
||||
// `--explain-dynamic` takes an optional value: alone it lists every unit.
|
||||
const normalized = args.map((arg) => (arg === "--explain-dynamic" ? "--explain-dynamic=" : arg));
|
||||
try {
|
||||
return parseArgs({ args, options: CLI_OPTIONS, allowPositionals: true, allowNegative: true });
|
||||
return parseArgs({
|
||||
args: normalized,
|
||||
options: CLI_OPTIONS,
|
||||
allowPositionals: true,
|
||||
allowNegative: true,
|
||||
});
|
||||
} catch (err) {
|
||||
// parseArgs appends a paragraph about `--` and positionals to the
|
||||
// unknown-option message; the first sentence is the part that names
|
||||
@@ -91,6 +100,8 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
externalTypeArgs.length > 0 ||
|
||||
values.out !== undefined ||
|
||||
values["emit-ir"] ||
|
||||
values.tier !== undefined ||
|
||||
values["explain-dynamic"] !== undefined ||
|
||||
!values["keep-llvm"]
|
||||
) {
|
||||
fail(
|
||||
@@ -152,10 +163,12 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
values["windows-subsystem"] !== undefined ||
|
||||
values["detached-exit"] ||
|
||||
(values["npm-static"] ?? []).length > 0 ||
|
||||
externalTypeArgs.length > 0
|
||||
externalTypeArgs.length > 0 ||
|
||||
values.tier !== undefined ||
|
||||
values["explain-dynamic"] !== undefined
|
||||
) {
|
||||
fail(
|
||||
"scriptc build --lib takes no --backend/--emit/--print/--optimization/--strip/--windows-subsystem/--detached-exit/--npm-static/--ffi/--external-types: the profile pins the emission and optimization, npm imports are judged automatically, outbound FFI belongs to executable builds, and external type mappings belong to coverage",
|
||||
"scriptc build --lib takes no --backend/--emit/--print/--optimization/--strip/--windows-subsystem/--detached-exit/--npm-static/--ffi/--external-types/--tier/--explain-dynamic: the profile pins the emission and optimization, npm imports are judged automatically, outbound FFI belongs to executable builds, external type mappings belong to coverage, and library artifacts are static-tier only",
|
||||
);
|
||||
}
|
||||
const profilePath = resolve(profileArg);
|
||||
@@ -223,6 +236,15 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
fail(`--fail-on is a coverage option (a build already fails on blockers)\n\n${USAGE}`);
|
||||
}
|
||||
const failOn = failOnRaw as CoverageFailOn | undefined;
|
||||
const tierRaw = values.tier;
|
||||
if (tierRaw !== undefined && !(TIER_MODES as readonly string[]).includes(tierRaw)) {
|
||||
fail(`unknown --tier value "${tierRaw}" (supported: ${TIER_MODES.join(", ")})\n\n${USAGE}`);
|
||||
}
|
||||
const tier = tierRaw as TierMode | undefined;
|
||||
const explainDynamic = values["explain-dynamic"];
|
||||
if (explainDynamic !== undefined && command !== "coverage") {
|
||||
fail(`--explain-dynamic is a coverage option\n\n${USAGE}`);
|
||||
}
|
||||
if (printNativeLinkInfo && command !== "build") {
|
||||
fail(`--print=native-link-info is a build option\n\n${USAGE}`);
|
||||
}
|
||||
@@ -263,6 +285,11 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
}
|
||||
externalTypes[specifier] = declarationPath;
|
||||
}
|
||||
const wasmArgs = values.wasm ?? [];
|
||||
if (wasmArgs.length > 0 && command !== "coverage") {
|
||||
fail(`--wasm is a coverage-only option\n\n${USAGE}`);
|
||||
}
|
||||
const wasmFiles = wasmArgs.map((file) => resolve(file));
|
||||
const ffiProfilePath = values.ffi !== undefined ? resolve(values.ffi) : undefined;
|
||||
if (values.backend !== undefined && values.backend !== "llvm") {
|
||||
fail(`unknown backend "${values.backend}" (supported: llvm)\n\n${USAGE}`);
|
||||
@@ -340,6 +367,9 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
...(npmStatic !== undefined ? { npmStatic } : {}),
|
||||
...(ffiProfilePath !== undefined ? { ffiProfilePath } : {}),
|
||||
...(Object.keys(externalTypes).length > 0 ? { externalTypes } : {}),
|
||||
...(tier !== undefined ? { tier } : {}),
|
||||
...(explainDynamic !== undefined ? { explainDynamic: true } : {}),
|
||||
...(wasmFiles.length > 0 ? { wasm: wasmFiles } : {}),
|
||||
});
|
||||
if (printDiagnostics) {
|
||||
// Keep stdout pure JSON for tooling.
|
||||
@@ -354,6 +384,18 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
process.stdout.write(
|
||||
renderCoverage(coverage, { color, sourceTexts, root: process.cwd() }) + "\n",
|
||||
);
|
||||
if (explainDynamic !== undefined && coverage.tiers !== undefined) {
|
||||
process.stdout.write(
|
||||
"\n" +
|
||||
renderExplainDynamic(coverage.tiers, {
|
||||
color,
|
||||
sourceTexts,
|
||||
root: process.cwd(),
|
||||
query: explainDynamic,
|
||||
}) +
|
||||
"\n",
|
||||
);
|
||||
}
|
||||
}
|
||||
return coveragePasses(coverage, failOn) ? 0 : 1;
|
||||
}
|
||||
@@ -386,6 +428,7 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
...(npmStatic !== undefined ? { npmStatic } : {}),
|
||||
...(ffiProfilePath !== undefined ? { ffiProfilePath } : {}),
|
||||
...(printNativeLinkInfo ? { nativeLinkInfo: true } : {}),
|
||||
...(tier !== undefined ? { tier } : {}),
|
||||
});
|
||||
if (!result.ok) {
|
||||
if (printDiagnostics) {
|
||||
|
||||
@@ -57,7 +57,8 @@ Options:
|
||||
compile the named npm packages' shipped JS statically as
|
||||
program modules (repeatable; "auto" opts in every eligible
|
||||
direct import: own .d.ts, unminified JS, no build-transform
|
||||
markers). Packages that fail preflight are refused with a
|
||||
markers; "pkg..." also names every package pkg depends
|
||||
on). Packages that fail preflight are refused with a
|
||||
diagnostic. Eligible dependencies compile automatically.
|
||||
--provenance-sources
|
||||
EXPERIMENTAL: compile npm dependencies from their
|
||||
@@ -69,6 +70,21 @@ Options:
|
||||
local declaration file. The declaration supplies types
|
||||
for analysis; the host module remains an explicit
|
||||
external-boundary blocker (repeatable)
|
||||
--tier <auto|static-only>
|
||||
tier policy (default: auto). static-only keeps every
|
||||
construct without a static lowering a compile error, so
|
||||
the program has no dynamic-tier demotions. Both modes
|
||||
currently compile identically
|
||||
--explain-dynamic[=<name>]
|
||||
coverage only: list the functions that operate on dynamic
|
||||
values and the bindings, fields, functions, classes and
|
||||
modules the dynamic tier would take over, with the
|
||||
refusal behind each; a name shows only that unit's chain
|
||||
--wasm <file.wasm>
|
||||
coverage only: report a WebAssembly module the program
|
||||
loads whose path its sources do not spell (repeatable;
|
||||
modules the sources name, inline modules and packaged
|
||||
.wasm files are found without it)
|
||||
-h, --help show this help
|
||||
-v, --version print the version
|
||||
`;
|
||||
@@ -90,6 +106,9 @@ export const CLI_OPTIONS = {
|
||||
"npm-static": { type: "string", multiple: true },
|
||||
"provenance-sources": { type: "boolean", default: false },
|
||||
"external-types": { type: "string", multiple: true },
|
||||
tier: { type: "string" },
|
||||
"explain-dynamic": { type: "string" },
|
||||
wasm: { type: "string", multiple: true },
|
||||
lib: { type: "boolean", default: false },
|
||||
profile: { type: "string" },
|
||||
help: { type: "boolean", short: "h", default: false },
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { NativeLinkInfo } from "./backend/native-link-info.js";
|
||||
import type { NativeOptimization } from "./backend/optimization.js";
|
||||
import type { ScrDiagnostic } from "./diagnostics/diagnostic.js";
|
||||
import type { CoverageInput } from "./coverage/report.js";
|
||||
import type { TierMode } from "./frontend/tier-analysis.js";
|
||||
|
||||
export type CompileOutputKind = "ir" | "llvm" | "asm" | "obj" | "exe";
|
||||
|
||||
@@ -45,6 +46,10 @@ export interface CompileBaseOptions {
|
||||
/** Attach the machine-readable external link recipe to an object result.
|
||||
* Valid only with outputKind "obj"; it never invokes a linker. */
|
||||
nativeLinkInfo?: boolean;
|
||||
/** `--tier` (frontend/tier-analysis.ts). "static-only" keeps every demotion
|
||||
* request a refusal (design invariant I2). Until demotion lands both modes
|
||||
* compile exactly as before. */
|
||||
tier?: TierMode;
|
||||
}
|
||||
|
||||
/** Executable compile options. This remains the compatibility type for the
|
||||
@@ -161,6 +166,14 @@ export interface AnalyzeOptions {
|
||||
* to analyze project code, but imported runtime values remain explicit
|
||||
* SC1010 blockers rather than being counted as executable. */
|
||||
externalTypes?: Readonly<Record<string, string>>;
|
||||
/** `--tier` (see CompileBaseOptions.tier). */
|
||||
tier?: TierMode;
|
||||
/** `--explain-dynamic`: attach the tier-assignment report
|
||||
* (CoverageInput.tiers). Such analyses bypass the verdict cache. */
|
||||
explainDynamic?: boolean;
|
||||
/** `--wasm <file>`: WebAssembly module files the program loads that its
|
||||
* sources do not name (absolute paths); reported with the discovered ones. */
|
||||
wasm?: readonly string[];
|
||||
}
|
||||
|
||||
export interface AnalyzeResult {
|
||||
|
||||
@@ -17,10 +17,14 @@ import {
|
||||
type DiagnosticsEnvelope,
|
||||
type LocatedDiagnostic,
|
||||
type SourcePosition,
|
||||
type TierUnitPosition,
|
||||
type TiersEnvelope,
|
||||
} from "../diagnostics/envelope.js";
|
||||
import { renderDiagnostics } from "../diagnostics/render.js";
|
||||
import type { LowerStats } from "../frontend/lowering/lowerer.js";
|
||||
import type { ProvenanceSources } from "../frontend/provenance-registry.js";
|
||||
import type { DemotionUnit, TierReport } from "../frontend/tier-analysis.js";
|
||||
import type { WasmModuleReport } from "../wasm/discover.js";
|
||||
|
||||
export interface CoverageInput {
|
||||
file: string;
|
||||
@@ -51,6 +55,12 @@ export interface CoverageInput {
|
||||
/** --provenance-sources: diagnostics of elided pure-annotated dead
|
||||
* consts (off the build; the provenance section names them). */
|
||||
provenanceElided?: ScrDiagnostic[];
|
||||
/** --explain-dynamic: the report-only tier assignment (which bindings,
|
||||
* fields, functions, classes and modules the dynamic tier would take). */
|
||||
tiers?: TierReport;
|
||||
/** The WebAssembly modules the build can see (wasm/discover.ts): their
|
||||
* size, validity and the proposals they use. */
|
||||
wasmModules?: WasmModuleReport[];
|
||||
/** True when tsc preflight failed — no coverage can be computed. */
|
||||
preflightFailed: boolean;
|
||||
}
|
||||
@@ -61,6 +71,8 @@ export interface NpmStaticStatus {
|
||||
status: "static" | "refused";
|
||||
/** The first refusal reason for a refused package. */
|
||||
detail?: string;
|
||||
/** How to act on the refusal, when there is a specific remedy. */
|
||||
hint?: string;
|
||||
}
|
||||
|
||||
const GREEN = "\x1b[32m";
|
||||
@@ -180,6 +192,12 @@ export function renderCoverage(input: CoverageInput, opts: CoverageRenderOptions
|
||||
|
||||
out.push(` statements analyzed ${total}`);
|
||||
out.push(` compile statically ${ok} ${c(pctColor, c(BOLD, `(${pct}%)`))}`);
|
||||
const dynamic = dynamicStatements(input);
|
||||
if (dynamic > 0) {
|
||||
out.push(
|
||||
` in dynamic tier ${dynamic} ${c(DIM, "(operate on values typed only at runtime; --explain-dynamic lists them)")}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (skipped > 0) {
|
||||
out.push(
|
||||
@@ -203,6 +221,51 @@ export function renderCoverage(input: CoverageInput, opts: CoverageRenderOptions
|
||||
out.push("");
|
||||
}
|
||||
|
||||
// The WebAssembly modules the build can see: what translating them would
|
||||
// take (wasm/discover.ts). No module compiles to native code yet; the
|
||||
// sites that load one are blockers or deferred fences above.
|
||||
const wasm = input.wasmModules ?? [];
|
||||
if (wasm.length > 0) {
|
||||
out.push(
|
||||
` ${c(DIM, "WebAssembly modules:")} ${c(DIM, "(found at build time; none compile to native code yet)")}`,
|
||||
);
|
||||
for (const m of wasm) {
|
||||
const name = m.path !== undefined ? displayPath(m.path, opts.root) : "inline module";
|
||||
const state =
|
||||
m.status === "valid"
|
||||
? c(GREEN, "valid")
|
||||
: m.status === "unsupported"
|
||||
? c(YELLOW, `uses ${m.unsupported.join(", ")}`)
|
||||
: c(RED, `${m.status}: ${m.error ?? ""}`);
|
||||
out.push(` ${name} ${c(DIM, `${groupDigits(m.size)} bytes`)} ${state}`);
|
||||
const imports = m.importModules.length > 0 ? ` (${m.importModules.join(", ")})` : "";
|
||||
out.push(
|
||||
` ${c(DIM, `${counted(m.functions, "function")}, ${counted(m.imports, "import")}${imports}, ${counted(m.exports, "export")}`)}`,
|
||||
);
|
||||
if (m.features.length > 0) out.push(` ${c(DIM, `features: ${m.features.join(", ")}`)}`);
|
||||
const shown = m.sources.slice(0, SITES_SHOWN);
|
||||
for (const s of shown) {
|
||||
const at =
|
||||
s.line !== undefined
|
||||
? `${displayPath(s.file, opts.root)}:${s.line}`
|
||||
: displayPath(s.file, opts.root);
|
||||
const how =
|
||||
s.kind === "path"
|
||||
? `path at ${at}`
|
||||
: s.kind === "inline"
|
||||
? `inline at ${at}`
|
||||
: s.kind === "package"
|
||||
? `shipped by the package at ${displayPath(dirOf(s.file), opts.root)}`
|
||||
: `configured by ${at}`;
|
||||
out.push(` ${c(DIM, `found: ${how}`)}`);
|
||||
}
|
||||
if (m.sources.length > shown.length) {
|
||||
out.push(` ${c(DIM, `found: +${m.sources.length - shown.length} more`)}`);
|
||||
}
|
||||
}
|
||||
out.push("");
|
||||
}
|
||||
|
||||
// --provenance-sources: per-package attribution — the answer to "did
|
||||
// the DEPENDENCY's statements compile static?", computed by summing
|
||||
// statsByFile entries under each package's source dir. Elided
|
||||
@@ -321,6 +384,27 @@ export function renderCoverage(input: CoverageInput, opts: CoverageRenderOptions
|
||||
return out.join("\n");
|
||||
}
|
||||
|
||||
/** "1 function", "1,698 functions". */
|
||||
function counted(n: number, noun: string): string {
|
||||
return `${groupDigits(n)} ${noun}${n === 1 ? "" : "s"}`;
|
||||
}
|
||||
|
||||
/** 637405 → "637,405". */
|
||||
function groupDigits(n: number): string {
|
||||
const digits = String(n);
|
||||
let out = "";
|
||||
for (let i = 0; i < digits.length; i++) {
|
||||
if (i > 0 && (digits.length - i) % 3 === 0) out += ",";
|
||||
out += digits[i];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function dirOf(file: string): string {
|
||||
const slash = file.lastIndexOf("/");
|
||||
return slash <= 0 ? file : file.slice(0, slash);
|
||||
}
|
||||
|
||||
function DIMPath(file: string, color: boolean): string {
|
||||
return color ? DIM + file + RESET : file;
|
||||
}
|
||||
@@ -432,13 +516,148 @@ export function coverageEnvelope(
|
||||
stats: {
|
||||
statementsTotal: total,
|
||||
statementsStatic: ok,
|
||||
statementsDynamic: 0,
|
||||
statementsDynamic: dynamicStatements(input),
|
||||
statementsFailed: failed,
|
||||
functionsSkipped: skipped,
|
||||
percentStatic: total === 0 ? 100 : Math.floor((ok / total) * 100),
|
||||
},
|
||||
}),
|
||||
...(input.tiers !== undefined ? { tiers: tiersEnvelope(input.tiers, locator) } : {}),
|
||||
...(input.wasmModules !== undefined ? { wasmModules: input.wasmModules } : {}),
|
||||
groups,
|
||||
diagnostics: located,
|
||||
};
|
||||
}
|
||||
|
||||
/** Reached statements in dynamic-tier functions, never more than the
|
||||
* compiled statements they are part of. */
|
||||
function dynamicStatements(input: CoverageInput): number {
|
||||
const compiled = input.stats.statementsTotal - input.stats.statementsFailed;
|
||||
return Math.max(0, Math.min(input.stats.statementsDynamic ?? 0, compiled));
|
||||
}
|
||||
|
||||
function tiersEnvelope(tiers: TierReport, locator: SourceLocator): TiersEnvelope {
|
||||
const at = (loc: { file: string; start: number }): SourcePosition => ({
|
||||
file: loc.file,
|
||||
...locator.position(loc.file, loc.start),
|
||||
});
|
||||
const unit = (u: DemotionUnit): TierUnitPosition => ({
|
||||
kind: u.kind,
|
||||
name: u.name,
|
||||
...(u.owner !== undefined ? { owner: u.owner } : {}),
|
||||
...at(u.loc),
|
||||
});
|
||||
return {
|
||||
mode: tiers.mode,
|
||||
statementsDynamic: tiers.statementsDynamic,
|
||||
dynamicFunctions: tiers.dynamicFunctions.map((fn) => ({
|
||||
name: fn.name,
|
||||
...at(fn.loc),
|
||||
operation: fn.operation,
|
||||
operationAt: at(fn.operationLoc),
|
||||
statements: fn.statements,
|
||||
})),
|
||||
requests: tiers.requests.map((r) => ({
|
||||
unit: unit(r.unit),
|
||||
enclosing: unit(r.enclosing),
|
||||
seed: r.seed,
|
||||
scope: r.scope,
|
||||
code: r.code,
|
||||
message: r.message,
|
||||
...at(r.loc),
|
||||
})),
|
||||
units: tiers.units.map((u) => ({ ...unit(u), requests: u.requests, sites: u.sites })),
|
||||
};
|
||||
}
|
||||
|
||||
/** IR function names as a user reads them: `%init.3` is a module's top
|
||||
* level, `%Point.norm` the method `Point.norm`; generated helpers keep
|
||||
* their `%`-prefixed names. */
|
||||
function functionDisplayName(name: string): string {
|
||||
if (name.startsWith("%init.")) return "<top level>";
|
||||
if (/^%[A-Za-z_$][\w$]*\.[A-Za-z_$#][\w$]*$/.test(name)) return name.slice(1);
|
||||
return name;
|
||||
}
|
||||
|
||||
function unitLabel(u: DemotionUnit, root: string | undefined): string {
|
||||
if (u.kind === "module") return `module ${displayPath(u.name, root)}`;
|
||||
if (u.kind === "binding" && u.owner !== undefined) return `binding ${u.name} in ${u.owner}`;
|
||||
return `${u.kind} ${u.name}`;
|
||||
}
|
||||
|
||||
function unitMatches(u: DemotionUnit, query: string): boolean {
|
||||
return (
|
||||
u.name === query ||
|
||||
u.name.endsWith(`.${query}`) ||
|
||||
u.name.split(" ")[0] === query ||
|
||||
`${u.owner ?? ""}:${u.name}` === query
|
||||
);
|
||||
}
|
||||
|
||||
/** `scriptc coverage --explain-dynamic[=<unit>]`: the dynamic tier today
|
||||
* and the demotion chains the report-only analysis computed. With a query,
|
||||
* only units whose name matches it (`x`, `Point.x`, `parse:x`). */
|
||||
export function renderExplainDynamic(
|
||||
tiers: TierReport,
|
||||
opts: CoverageRenderOptions & { query?: string } = {},
|
||||
): string {
|
||||
const c = (code: string, s: string) => (opts.color ? code + s + RESET : s);
|
||||
const locator = new SourceLocator(opts.sourceTexts);
|
||||
const where = (loc: { file: string; start: number }) => {
|
||||
const p = locator.position(loc.file, loc.start);
|
||||
return `${displayPath(loc.file, opts.root)}:${p.line}:${p.column}`;
|
||||
};
|
||||
const query = opts.query !== undefined && opts.query !== "" ? opts.query : undefined;
|
||||
const out: string[] = [];
|
||||
out.push(
|
||||
` ${c(BOLD, "dynamic tier")} ${c(DIM, `(tier assignment, report-only; --tier=${tiers.mode})`)}`,
|
||||
);
|
||||
if (query === undefined) {
|
||||
const fns = tiers.dynamicFunctions;
|
||||
out.push(
|
||||
` ${fns.length} function${fns.length === 1 ? "" : "s"} already operate${fns.length === 1 ? "s" : ""} on dynamic values ${c(DIM, `(${tiers.statementsDynamic} statement${tiers.statementsDynamic === 1 ? "" : "s"})`)}`,
|
||||
);
|
||||
for (const fn of fns) {
|
||||
out.push(
|
||||
` ${functionDisplayName(fn.name)} ${c(DIM, `${where(fn.loc)}: ${fn.operation} at ${where(fn.operationLoc)}`)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
const live = tiers.requests.filter((r) => r.scope !== "unreached" && r.seed !== "excluded");
|
||||
const excluded = tiers.requests.filter((r) => r.seed === "excluded").length;
|
||||
const unreached = tiers.requests.filter((r) => r.scope === "unreached").length;
|
||||
if (query === undefined) {
|
||||
out.push("");
|
||||
out.push(
|
||||
` ${live.length} demotion request${live.length === 1 ? "" : "s"} ${c(DIM, "(refusals and fences the dynamic tier would replace)")}` +
|
||||
(unreached > 0 ? c(DIM, `, ${unreached} more in unreached code`) : "") +
|
||||
(excluded > 0 ? c(DIM, `, ${excluded} excluded by policy`) : ""),
|
||||
);
|
||||
}
|
||||
const units = tiers.units.filter((u) => query === undefined || unitMatches(u, query));
|
||||
if (query !== undefined && units.length === 0) {
|
||||
out.push(` no demoted unit matches '${query}'`);
|
||||
return out.join("\n");
|
||||
}
|
||||
for (const u of units) {
|
||||
const counts = [
|
||||
u.requests > 0 ? `${u.requests} request${u.requests === 1 ? "" : "s"}` : "",
|
||||
u.sites > 0 ? `${u.sites} site${u.sites === 1 ? "" : "s"} inside` : "",
|
||||
]
|
||||
.filter((s) => s !== "")
|
||||
.join(", ");
|
||||
out.push(` ${unitLabel(u, opts.root)} ${c(DIM, `${where(u.loc)} (${counts})`)}`);
|
||||
if (query === undefined && u.requests === 0) continue;
|
||||
for (const r of live) {
|
||||
if (r.unit.kind !== u.kind || r.unit.name !== u.name || r.unit.loc.start !== u.loc.start)
|
||||
continue;
|
||||
if (r.unit.loc.file !== u.loc.file) continue;
|
||||
out.push(
|
||||
` ${c(DIM, "<-")} ${r.code} ${r.seed} at ${where(r.loc)}: ${capMessage(r.message)}`,
|
||||
);
|
||||
if (r.enclosing.name !== r.unit.name || r.enclosing.kind !== r.unit.kind)
|
||||
out.push(` ${c(DIM, "->")} ${unitLabel(r.enclosing, opts.root)} turns dynamic`);
|
||||
}
|
||||
}
|
||||
return out.join("\n");
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ import { InternalCompilerError } from "../errors.js";
|
||||
* harness asserts refusals match entry codes. renderSurfaceManifest is
|
||||
* byte-deterministic: entries sort by id, keys are emitted in one fixed
|
||||
* order, and the output carries no timestamps or absolute paths. */
|
||||
import { FENCE_CODES, UNSUPPORTED } from "../diagnostics/diagnostic.js";
|
||||
import { FENCE_CODES, UNSUPPORTED, webAssemblyDiag } from "../diagnostics/diagnostic.js";
|
||||
import { NODE24_FETCH_COMPAT_PROFILE } from "../compat/fetch-profile.js";
|
||||
import { SUPPORTED_BUILTIN_MODULES, SUPPORTED_NODE_MODULES } from "../frontend/builtin-modules.js";
|
||||
import {
|
||||
@@ -275,7 +275,7 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes
|
||||
status: "static",
|
||||
...(COLLECTION_ITERATOR_METHODS.has(name)
|
||||
? {
|
||||
note: "live native iterators support next(), for...of, and drains; iterator helper methods are refused",
|
||||
note: "live native iterators support next(), for...of, drains, and the iterator helper methods",
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
@@ -288,7 +288,7 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes
|
||||
status: "static",
|
||||
...(COLLECTION_ITERATOR_METHODS.has(name)
|
||||
? {
|
||||
note: "live native iterators support next(), for...of, and drains; iterator helper methods are refused",
|
||||
note: "live native iterators support next(), for...of, drains, and the iterator helper methods",
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
@@ -444,6 +444,19 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes
|
||||
}
|
||||
}
|
||||
|
||||
// ── the WebAssembly global: every reached use refuses (webAssemblyDiag) ─
|
||||
{
|
||||
const refusal = webAssemblyDiag("WebAssembly", { file: "", start: 0, end: 0 });
|
||||
add({
|
||||
id: "stdlib.webassembly",
|
||||
kind: "stdlib",
|
||||
name: "globalThis.WebAssembly",
|
||||
status: "unsupported",
|
||||
code: refusal.code,
|
||||
...(refusal.hint !== undefined ? { note: refusal.hint } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
// ── fetch/Web platform: one explicit, versioned compatibility profile ─
|
||||
const fetchTarget =
|
||||
`Node ${NODE24_FETCH_COMPAT_PROFILE.target.node} / ` +
|
||||
|
||||
@@ -14,6 +14,7 @@ import type { AnalyzeOptions, AnalyzeResult } from "../compile-types.js";
|
||||
import type { ScrDiagnostic } from "../diagnostics/diagnostic.js";
|
||||
import type { LowerStats } from "../frontend/lowering/lowerer.js";
|
||||
import type { NpmStaticStatus } from "./report.js";
|
||||
import type { WasmModuleReport } from "../wasm/discover.js";
|
||||
import {
|
||||
FrontendInputTracker,
|
||||
frontendInputsStillMatch,
|
||||
@@ -38,6 +39,7 @@ export interface CachedCoverage {
|
||||
unreached?: { stats: LowerStats; diagnostics: ScrDiagnostic[]; runtimeFences?: ScrDiagnostic[] };
|
||||
divergences?: ScrDiagnostic[];
|
||||
npmStatic?: NpmStaticStatus[];
|
||||
wasmModules?: WasmModuleReport[];
|
||||
preflightFailed: boolean;
|
||||
}
|
||||
|
||||
@@ -89,6 +91,9 @@ export function verdictKey(
|
||||
opts: AnalyzeOptions,
|
||||
cache: VerdictCacheOptions,
|
||||
): string | null {
|
||||
// The tier report carries source locations the semantic replay does not
|
||||
// move; explaining analyses always lower.
|
||||
if (opts.explainDynamic === true) return null;
|
||||
let ffi = "";
|
||||
if (opts.ffiProfilePath !== undefined) {
|
||||
try {
|
||||
@@ -120,6 +125,10 @@ export function verdictKey(
|
||||
opts.ffiProfilePath ?? "",
|
||||
ffi,
|
||||
external.join("\n"),
|
||||
// Demotion (stage S6) makes the tier mode part of the verdict.
|
||||
...(opts.tier === "static-only" ? ["tier=static-only"] : []),
|
||||
// The module files' contents are probes; their names are options.
|
||||
...(opts.wasm !== undefined && opts.wasm.length > 0 ? [`wasm=${opts.wasm.join("\n")}`] : []),
|
||||
].join("\0"),
|
||||
);
|
||||
}
|
||||
@@ -141,6 +150,7 @@ function cacheable(coverage: AnalyzeResult["coverage"]): CachedCoverage | null {
|
||||
if (coverage.unreached !== undefined) out.unreached = coverage.unreached;
|
||||
if (coverage.divergences !== undefined) out.divergences = coverage.divergences;
|
||||
if (coverage.npmStatic !== undefined) out.npmStatic = coverage.npmStatic;
|
||||
if (coverage.wasmModules !== undefined) out.wasmModules = coverage.wasmModules;
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -157,6 +167,7 @@ function restore(cached: CachedCoverage, sources: CachedSource[]): AnalyzeResult
|
||||
if (cached.unreached !== undefined) coverage.unreached = cached.unreached;
|
||||
if (cached.divergences !== undefined) coverage.divergences = cached.divergences;
|
||||
if (cached.npmStatic !== undefined) coverage.npmStatic = cached.npmStatic;
|
||||
if (cached.wasmModules !== undefined) coverage.wasmModules = cached.wasmModules;
|
||||
return { coverage, sourceTexts };
|
||||
}
|
||||
|
||||
|
||||
@@ -751,6 +751,20 @@ export function noLoweringDiag(
|
||||
};
|
||||
}
|
||||
|
||||
/** A REACHED use of the WebAssembly API (SC2020): scriptc.d.ts declares
|
||||
* its types so packages that mention them typecheck, but no module
|
||||
* compiles to native code yet. `surface` names what was used
|
||||
* ('WebAssembly.compile', 'new WebAssembly.Module', 'globalThis.WebAssembly'). */
|
||||
export function webAssemblyDiag(surface: string, loc: SrcLoc): ScrDiagnostic {
|
||||
return {
|
||||
code: "SC2020",
|
||||
message: `'${surface}' is not supported yet: scriptc does not compile WebAssembly modules to native code yet`,
|
||||
loc,
|
||||
milestone: "later",
|
||||
hint: "WebAssembly support is planned as translation of each module whose bytes the build can see (an inline literal, or a file read from a path the compiler resolves) into native code; 'scriptc coverage' lists the modules this program would load (https://scriptc.dev/limitations)",
|
||||
};
|
||||
}
|
||||
|
||||
/** A resolved native addon needs Node's embedding ABI. The migration uses the
|
||||
* underlying native operation through an ordinary C ABI declaration. */
|
||||
export function nativeAddonDiag(specifier: string, loc: SrcLoc): ScrDiagnostic {
|
||||
@@ -877,13 +891,20 @@ export function unsupportedPackageValueDiag(pkg: string, loc: SrcLoc): ScrDiagno
|
||||
);
|
||||
}
|
||||
|
||||
export function npmStaticRefusalDiag(pkg: string, detail: string, loc: SrcLoc): ScrDiagnostic {
|
||||
export function npmStaticRefusalDiag(
|
||||
pkg: string,
|
||||
detail: string,
|
||||
loc: SrcLoc,
|
||||
hint?: string,
|
||||
): ScrDiagnostic {
|
||||
return {
|
||||
code: "SC2013",
|
||||
message: `cannot compile npm package '${pkg}' statically: ${detail}`,
|
||||
loc,
|
||||
milestone: "later",
|
||||
hint: "use a dependency with supported source, or replace the unsupported operation; scriptc has no interpreter fallback",
|
||||
hint:
|
||||
hint ??
|
||||
"use a dependency with supported source, or replace the unsupported operation; scriptc has no interpreter fallback",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
* bumps it. */
|
||||
import { readFileSync } from "node:fs";
|
||||
import type { ScrDiagnostic } from "./diagnostic.js";
|
||||
import type { WasmModuleReport } from "../wasm/discover.js";
|
||||
|
||||
export const DIAGNOSTICS_SCHEMA = "scriptc-diagnostics";
|
||||
export const DIAGNOSTICS_SCHEMA_VERSION = 1;
|
||||
@@ -81,6 +82,34 @@ export interface DiagnosticGroup {
|
||||
sites: SourcePosition[];
|
||||
}
|
||||
|
||||
export interface TierUnitPosition extends SourcePosition {
|
||||
kind: "binding" | "field" | "function" | "class" | "module";
|
||||
name: string;
|
||||
owner?: string;
|
||||
}
|
||||
|
||||
/** `scriptc coverage --explain-dynamic --print=diagnostics`: the tier
|
||||
* assignment of frontend/tier-analysis.ts with source positions. */
|
||||
export interface TiersEnvelope {
|
||||
mode: "auto" | "static-only";
|
||||
statementsDynamic: number;
|
||||
dynamicFunctions: (SourcePosition & {
|
||||
name: string;
|
||||
operation: string;
|
||||
operationAt: SourcePosition;
|
||||
statements: number;
|
||||
})[];
|
||||
requests: (SourcePosition & {
|
||||
unit: TierUnitPosition;
|
||||
enclosing: TierUnitPosition;
|
||||
seed: string;
|
||||
scope: string;
|
||||
code: string;
|
||||
message: string;
|
||||
})[];
|
||||
units: (TierUnitPosition & { requests: number; sites: number })[];
|
||||
}
|
||||
|
||||
export interface DiagnosticsEnvelope {
|
||||
schema: typeof DIAGNOSTICS_SCHEMA;
|
||||
schemaVersion: typeof DIAGNOSTICS_SCHEMA_VERSION;
|
||||
@@ -97,8 +126,10 @@ export interface DiagnosticsEnvelope {
|
||||
stats?: {
|
||||
statementsTotal: number;
|
||||
statementsStatic: number;
|
||||
/** Always 0: every compiled statement is native. Kept so schema
|
||||
* version 1 consumers keep reading the same fields. */
|
||||
/** The compiled statements (counted in statementsStatic, which covers
|
||||
* every statement that compiles to native code) that belong to
|
||||
* dynamic-tier functions: code operating on values whose types are only
|
||||
* known at runtime. */
|
||||
statementsDynamic: number;
|
||||
statementsFailed: number;
|
||||
functionsSkipped: number;
|
||||
@@ -106,6 +137,11 @@ export interface DiagnosticsEnvelope {
|
||||
};
|
||||
/** build only: the produced artifact path. */
|
||||
artifact?: string;
|
||||
/** coverage --explain-dynamic only: the report-only tier assignment. */
|
||||
tiers?: TiersEnvelope;
|
||||
/** coverage only: the WebAssembly modules the build can see, with their
|
||||
* size, validity, proposals and the sources that found them. */
|
||||
wasmModules?: WasmModuleReport[];
|
||||
groups: DiagnosticGroup[];
|
||||
diagnostics: LocatedDiagnostic[];
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ export function prepareExecutableModule(
|
||||
entry.package,
|
||||
entry.detail ?? "its static compilation was refused",
|
||||
fe.npmImportSites.get(entry.package) ?? { file: entryPath, start: 0, end: 0 },
|
||||
entry.hint,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -69,6 +70,7 @@ export function prepareExecutableModule(
|
||||
lowered = fe.lower({
|
||||
targetPlatform: buildPlatform,
|
||||
...(ffi !== null ? { ffiImports: ffi.functions } : {}),
|
||||
...(opts.tier !== undefined ? { tierMode: opts.tier } : {}),
|
||||
});
|
||||
timing("lower");
|
||||
} catch (e) {
|
||||
|
||||
@@ -58,6 +58,7 @@ export function analyzeWithFrontend(
|
||||
entry.package,
|
||||
entry.detail ?? "its static compilation was refused",
|
||||
fe.npmImportSites.get(entry.package) ?? { file: entryPath, start: 0, end: 0 },
|
||||
entry.hint,
|
||||
),
|
||||
),
|
||||
];
|
||||
@@ -90,6 +91,9 @@ export function analyzeWithFrontend(
|
||||
coverage: true,
|
||||
targetPlatform: buildPlatform,
|
||||
...(ffi !== null ? { ffiImports: ffi.functions } : {}),
|
||||
...(opts.tier !== undefined ? { tierMode: opts.tier } : {}),
|
||||
...(opts.explainDynamic === true ? { tierReport: { preflight } } : {}),
|
||||
...(opts.wasm !== undefined && opts.wasm.length > 0 ? { wasmFiles: opts.wasm } : {}),
|
||||
});
|
||||
const provenance = provenanceSources();
|
||||
return {
|
||||
@@ -109,6 +113,8 @@ export function analyzeWithFrontend(
|
||||
...(provenance !== null ? { provenance } : {}),
|
||||
...(lowered.statsByFile ? { statsByFile: lowered.statsByFile } : {}),
|
||||
...(lowered.provenanceElided ? { provenanceElided: lowered.provenanceElided } : {}),
|
||||
...(lowered.tiers !== undefined ? { tiers: lowered.tiers } : {}),
|
||||
...(lowered.wasmModules !== undefined ? { wasmModules: lowered.wasmModules } : {}),
|
||||
preflightFailed: false,
|
||||
},
|
||||
sourceTexts: fe.sourceTexts(),
|
||||
|
||||
@@ -42,6 +42,8 @@ export const SUPPORTED_BUILTIN_MODULES = [
|
||||
"module",
|
||||
"console",
|
||||
"process",
|
||||
"punycode",
|
||||
"constants",
|
||||
] as const;
|
||||
|
||||
/** Builtins Node itself serves ONLY under the node: prefix —
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer } from "../lowerer.js";
|
||||
import { type ClassInfo } from "../lower-classes.js";
|
||||
import { isJsSourceFile } from "../../program.js";
|
||||
import { builtinMemberRequireDecl } from "./module-bindings.js";
|
||||
import { stripTypeCasts } from "./arguments.js";
|
||||
import { streamClassAliasDecl } from "../lower-stream.js";
|
||||
import { zlibClassName, zlibStreamModeOf } from "./compression.js";
|
||||
import { stdlibGlobalNameOf } from "../surfaces.js";
|
||||
|
||||
/** A builtin class an expression names at compile time: the error
|
||||
* classes, node:events EventEmitter, the node:stream classes (each a
|
||||
* runtime ClassInfo), the node:zlib stream classes (by node_zlib_mode),
|
||||
* and the Map and Set constructors. `name` is the class's `name`
|
||||
* property. */
|
||||
export type BuiltinClassRef =
|
||||
| { kind: "error" | "emitter" | "stream"; info: ClassInfo; name: string }
|
||||
| { kind: "zlib"; mode: number; name: string }
|
||||
| { kind: "collection"; which: "Map" | "Set"; name: string };
|
||||
|
||||
/** The builtin class `expr` names: a direct spelling (`EventEmitter`,
|
||||
* `stream.Readable`, `zlib.Gzip`, `Map`, `globalThis.TypeError`) or a
|
||||
* `const` binding of one (`const E = EventEmitter`, an alias of an alias,
|
||||
* an imported alias), through parentheses and type assertions. Null for
|
||||
* anything else. */
|
||||
export function builtinClassRefOf(lowerer: Lowerer, expr: ts.Expression): BuiltinClassRef | null {
|
||||
const e = stripTypeCasts(expr);
|
||||
// Checker lookups only: answering must not run the fences and deferred
|
||||
// diagnostics of a lowered reference (resolveValueSymbol).
|
||||
let symbol: ts.Symbol | undefined;
|
||||
if (ts.isIdentifier(e)) {
|
||||
symbol = lowerer.checker.getSymbolAtLocation(e);
|
||||
} else if (ts.isPropertyAccessExpression(e) && !e.questionDotToken) {
|
||||
symbol = lowerer.checker.getSymbolAtLocation(e.name);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
if (!symbol) return null;
|
||||
const alias = builtinClassAliasOf(lowerer, symbol);
|
||||
if (alias !== null) return alias;
|
||||
if (symbol.flags & ts.SymbolFlags.Alias) symbol = lowerer.checker.getAliasedSymbol(symbol);
|
||||
const which = stdlibGlobalNameOf(lowerer, e);
|
||||
if (which === "Map" || which === "Set") return { kind: "collection", which, name: which };
|
||||
const error = lowerer.builtinErrorInfoOf(symbol);
|
||||
if (error) return { kind: "error", info: error, name: runtimeClassName(error) };
|
||||
const emitter = lowerer.builtinEmitterInfoOf(symbol);
|
||||
if (emitter) return { kind: "emitter", info: emitter, name: runtimeClassName(emitter) };
|
||||
const stream = lowerer.builtinStreamInfoOf(symbol);
|
||||
if (stream) return { kind: "stream", info: stream, name: runtimeClassName(stream) };
|
||||
const mode = zlibStreamModeOf(lowerer, symbol);
|
||||
if (mode !== null) return { kind: "zlib", mode, name: zlibClassName(mode) };
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The builtin class a `const` binding names (`const E = EventEmitter`,
|
||||
* `const M = Map`, `const C = zlib.Gzip`, or an alias of such a binding),
|
||||
* or null. `let` and `var` bindings could be reassigned, and annotated
|
||||
* bindings give the value another type, so they are not aliases. Resolved from the declaration, so uses lowered before the
|
||||
* declaration statement (hoisted functions) see the same answer. */
|
||||
export function builtinClassAliasOf(lowerer: Lowerer, symbol: ts.Symbol): BuiltinClassRef | null {
|
||||
const cached = lowerer.builtinClassAliases.get(symbol);
|
||||
if (cached !== undefined) return cached;
|
||||
// An import of an exported alias names the exporting binding.
|
||||
if (symbol.flags & ts.SymbolFlags.Alias) {
|
||||
lowerer.builtinClassAliases.set(symbol, null);
|
||||
const target = lowerer.checker.getAliasedSymbol(symbol);
|
||||
const ref = target === symbol ? null : builtinClassAliasOf(lowerer, target);
|
||||
lowerer.builtinClassAliases.set(symbol, ref);
|
||||
return ref;
|
||||
}
|
||||
const init = constInitializerOf(lowerer, symbol);
|
||||
if (init === null) {
|
||||
lowerer.builtinClassAliases.set(symbol, null);
|
||||
return null;
|
||||
}
|
||||
// Marked first: a cycle through other bindings answers null.
|
||||
lowerer.builtinClassAliases.set(symbol, null);
|
||||
const ref = builtinClassRefOf(lowerer, init);
|
||||
lowerer.builtinClassAliases.set(symbol, ref);
|
||||
return ref;
|
||||
}
|
||||
|
||||
/** True when `symbol` is a builtin class alias whose declaration owns no
|
||||
* runtime slot: new, instanceof, typeof, .name and === lower at each use,
|
||||
* and other uses report SC1090. In JavaScript files, a binding whose
|
||||
* initializer has a value (`const E = EventEmitter`, `const M =
|
||||
* globalThis.Map`, both identity tokens) keeps its slot; only builtin
|
||||
* module members (`zlib.Gzip`) and copies of slotless aliases own none.
|
||||
* The `require("events").EventEmitter` and `stream.Writable` spellings
|
||||
* keep their own declaration handling. */
|
||||
export function builtinClassAliasOwnsNoSlot(lowerer: Lowerer, symbol: ts.Symbol): boolean {
|
||||
if (builtinClassAliasOf(lowerer, symbol) === null) return false;
|
||||
const binding =
|
||||
symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol;
|
||||
const decl = lowerer.checker.valueDeclarationOf(binding);
|
||||
if (decl === undefined || !ts.isVariableDeclaration(decl)) return false;
|
||||
if (builtinMemberRequireDecl(decl.name, decl.initializer)) return false;
|
||||
if (streamClassAliasDecl(lowerer, decl.name, decl.initializer)) return false;
|
||||
if (!isJsSourceFile(decl.getSourceFile())) return true;
|
||||
let init = decl.initializer;
|
||||
while (init !== undefined && ts.isParenthesizedExpression(init)) init = init.expression;
|
||||
if (init === undefined) return true;
|
||||
let source: ts.Symbol | undefined;
|
||||
if (ts.isPropertyAccessExpression(init)) {
|
||||
if (lowerer.builtinNamespaceModuleOf(init.expression) !== null) return true;
|
||||
source = lowerer.checker.getSymbolAtLocation(init.name);
|
||||
} else {
|
||||
source = lowerer.checker.getSymbolAtLocation(init);
|
||||
}
|
||||
return source !== undefined && builtinClassAliasOwnsNoSlot(lowerer, source);
|
||||
}
|
||||
|
||||
/** True for a `const` declaration that is a builtin class alias owning no
|
||||
* runtime slot (see builtinClassAliasOwnsNoSlot): both declaration walks
|
||||
* skip it. */
|
||||
export function registerBuiltinClassAlias(
|
||||
lowerer: Lowerer,
|
||||
nameNode: ts.Node,
|
||||
init: ts.Expression | undefined,
|
||||
): boolean {
|
||||
if (!ts.isIdentifier(nameNode) || init === undefined) return false;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(nameNode);
|
||||
return symbol !== undefined && builtinClassAliasOwnsNoSlot(lowerer, symbol);
|
||||
}
|
||||
|
||||
/** True when two references name the same builtin class. */
|
||||
export function sameBuiltinClass(a: BuiltinClassRef, b: BuiltinClassRef): boolean {
|
||||
if (a.kind === "zlib") return b.kind === "zlib" && a.mode === b.mode;
|
||||
if (a.kind === "collection") return b.kind === "collection" && a.which === b.which;
|
||||
return b.kind === a.kind && b.info === a.info;
|
||||
}
|
||||
|
||||
/** The initializer of an unannotated `const` binding whose initializer
|
||||
* can name a class (an identifier or a property access), or null. */
|
||||
function constInitializerOf(lowerer: Lowerer, symbol: ts.Symbol): ts.Expression | null {
|
||||
if ((symbol.flags & ts.SymbolFlags.BlockScopedVariable) === 0) return null;
|
||||
const decl = lowerer.checker.valueDeclarationOf(symbol);
|
||||
if (
|
||||
decl === undefined ||
|
||||
!ts.isVariableDeclaration(decl) ||
|
||||
!ts.isIdentifier(decl.name) ||
|
||||
decl.type !== undefined
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const list = decl.parent;
|
||||
if (!ts.isVariableDeclarationList(list) || (list.flags & ts.NodeFlags.Const) === 0) return null;
|
||||
let init = decl.initializer;
|
||||
while (init !== undefined && ts.isParenthesizedExpression(init)) init = init.expression;
|
||||
if (init === undefined) return null;
|
||||
if (ts.isIdentifier(init)) return init;
|
||||
if (ts.isPropertyAccessExpression(init) && !init.questionDotToken) return init;
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Node's constructor name of a runtime class (`%TypeError` → TypeError). */
|
||||
function runtimeClassName(info: ClassInfo): string {
|
||||
return info.def.name.startsWith("%") ? info.def.name.slice(1) : info.def.name;
|
||||
}
|
||||
@@ -132,8 +132,8 @@ export function zlibStreamModeOf(
|
||||
symbol: ts.Symbol | null | undefined,
|
||||
): number | null {
|
||||
if (!symbol) return null;
|
||||
const aliasMode = zlibClassAliasModeOf(lowerer, symbol, 0);
|
||||
if (aliasMode !== undefined) return aliasMode;
|
||||
const alias = lowerer.builtinClassAliasOf(symbol);
|
||||
if (alias !== null) return alias.kind === "zlib" ? alias.mode : null;
|
||||
const target =
|
||||
symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol;
|
||||
const mode = ZLIB_STREAM_MODES.get(target.name);
|
||||
@@ -499,80 +499,12 @@ export function lowerZlibModuleCall(
|
||||
throw new InternalCompilerError(`unhandled lowered zlib member ${bi.member}`);
|
||||
}
|
||||
|
||||
/** The zlib stream class an expression names (`zlib.Gzip`, an imported
|
||||
* `Gzip`, a `const C = zlib.Gzip` alias), as its node_zlib_mode, or null. */
|
||||
export function zlibClassModeOfExpr(lowerer: Lowerer, expr: ts.Expression): number | null {
|
||||
let e = expr;
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
if (ts.isIdentifier(e)) return zlibStreamModeOf(lowerer, lowerer.checker.getSymbolAtLocation(e));
|
||||
if (ts.isPropertyAccessExpression(e) && !e.questionDotToken) {
|
||||
return zlibStreamModeOf(lowerer, lowerer.checker.getSymbolAtLocation(e.name));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 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;
|
||||
return "Zlib";
|
||||
}
|
||||
|
||||
/** The mode of a `const C = zlib.Gzip` binding (or an alias of such a
|
||||
* binding), null when the symbol is a const binding of something else,
|
||||
* undefined when it is not a const binding. Resolved from the declaration,
|
||||
* so uses lowered before the declaration statement see the same answer. */
|
||||
function zlibClassAliasModeOf(
|
||||
lowerer: Lowerer,
|
||||
symbol: ts.Symbol,
|
||||
depth: number,
|
||||
): number | null | undefined {
|
||||
const cached = lowerer.zlibClassAliases.get(symbol);
|
||||
if (cached !== undefined) return cached;
|
||||
if ((symbol.flags & ts.SymbolFlags.BlockScopedVariable) === 0) return undefined;
|
||||
const decl = lowerer.checker.valueDeclarationOf(symbol);
|
||||
if (decl === undefined || !ts.isVariableDeclaration(decl) || !ts.isIdentifier(decl.name)) {
|
||||
return undefined;
|
||||
}
|
||||
// const only: a var or let binding could be reassigned
|
||||
const list = decl.parent;
|
||||
if (!ts.isVariableDeclarationList(list) || (list.flags & ts.NodeFlags.Const) === 0) {
|
||||
return undefined;
|
||||
}
|
||||
let init = decl.initializer;
|
||||
while (init !== undefined && ts.isParenthesizedExpression(init)) init = init.expression;
|
||||
if (init === undefined || depth > 16) return null;
|
||||
let mode: number | null = null;
|
||||
if (ts.isIdentifier(init)) {
|
||||
const next = lowerer.checker.getSymbolAtLocation(init);
|
||||
if (next !== undefined) {
|
||||
const nested = zlibClassAliasModeOf(lowerer, next, depth + 1);
|
||||
mode = nested === undefined ? zlibStreamModeOf(lowerer, next) : nested;
|
||||
}
|
||||
} else if (ts.isPropertyAccessExpression(init) && !init.questionDotToken) {
|
||||
mode = zlibStreamModeOf(lowerer, lowerer.checker.getSymbolAtLocation(init.name));
|
||||
}
|
||||
if (mode !== null) lowerer.zlibClassAliases.set(symbol, mode);
|
||||
return mode;
|
||||
}
|
||||
|
||||
/** True for `const C = zlib.Gzip` (or an alias of an alias): a
|
||||
* compile-time zlib class alias that owns no runtime slot, so the
|
||||
* statement lowering skips it. */
|
||||
export function registerZlibClassAlias(
|
||||
lowerer: Lowerer,
|
||||
nameNode: ts.Node,
|
||||
init: ts.Expression | undefined,
|
||||
): boolean {
|
||||
if (!ts.isIdentifier(nameNode) || init === undefined) return false;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(nameNode);
|
||||
return symbol !== undefined && zlibClassAliasOf(lowerer, symbol) !== null;
|
||||
}
|
||||
|
||||
/** The mode of a `const C = zlib.Gzip` alias symbol, or null. */
|
||||
export function zlibClassAliasOf(lowerer: Lowerer, symbol: ts.Symbol): number | null {
|
||||
return zlibClassAliasModeOf(lowerer, symbol, 0) ?? null;
|
||||
}
|
||||
|
||||
/** `value instanceof zlib.Gzip` (any spelling of the class, aliases
|
||||
* included). zlib streams are runtime Transforms: a Transform operand asks
|
||||
* the runtime for the stream's mode, and an operand typed as one of
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { lowerBuiltinByteInput, errorFirstBytesCallback, errorFirstCallback } from "./arguments.js";
|
||||
import { isKeyObjectValue } from "./key-object.js";
|
||||
import { boolLit, dynUndefinedExpr, varRef } from "../../../ir/build.js";
|
||||
import { boolLit, dynUndefinedExpr, nodeThrowExpr, varRef } from "../../../ir/build.js";
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer, own } from "../lowerer.js";
|
||||
import { locOf } from "../../program.js";
|
||||
@@ -941,9 +941,27 @@ export function lowerCryptoHashMethodCall(
|
||||
if (call.questionDotToken || access.questionDotToken) return null;
|
||||
const receiverType = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
|
||||
if (receiverType?.kind !== "cryptoHash" && receiverType?.kind !== "cryptoHmac") return null;
|
||||
if (!lowerer.isStdlibMember(access)) return null;
|
||||
const name = access.name.text;
|
||||
const loc = locOf(call);
|
||||
// Hmac has no copy(): Node reads the member as undefined and throws a
|
||||
// TypeError once the arguments are evaluated.
|
||||
if (receiverType.kind === "cryptoHmac" && name === "copy") {
|
||||
if (call.arguments.some(ts.isSpreadElement))
|
||||
lowerer.noLowering("Hmac.copy with spread arguments", call);
|
||||
const evaluated = [access.expression, ...call.arguments].map((node): IrStmt => ({
|
||||
kind: "exprStmt",
|
||||
expr: lowerer.lowerExpr(node),
|
||||
loc,
|
||||
}));
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: evaluated,
|
||||
result: nodeThrowExpr(1, "", `${access.getText()} is not a function`, DYN, loc),
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (!lowerer.isStdlibMember(access)) return null;
|
||||
const receiver = (): IrExpr => lowerer.lowerExprExpecting(access.expression, receiverType);
|
||||
const hash = receiverType.kind === "cryptoHash";
|
||||
const label = hash ? "Hash" : "Hmac";
|
||||
|
||||
@@ -8,6 +8,10 @@ import {
|
||||
isJsonStringifyDynamicType,
|
||||
BOOL,
|
||||
DYN,
|
||||
type IrType,
|
||||
classDynViewSupported,
|
||||
isClassOwnEnumerableFieldName,
|
||||
isDynTypedRefType,
|
||||
type IrExpr,
|
||||
STRING,
|
||||
type SrcLoc,
|
||||
@@ -157,6 +161,16 @@ export function lowerJsonMethodCall(
|
||||
(id) => lowerer.unions.get(id),
|
||||
)
|
||||
) {
|
||||
const unreadable = classFieldWithoutJsonView(lowerer, value.type, new Set());
|
||||
if (unreadable !== null) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
argNode,
|
||||
`JSON.stringify of '${lowerer.fmt(value.type)}' values (field '${unreadable.field}' of class ` +
|
||||
`'${unreadable.className}' has type '${lowerer.fmt(unreadable.type)}', which the runtime JSON ` +
|
||||
`traversal cannot ${unreadable.reason})`,
|
||||
);
|
||||
}
|
||||
// Keep the original containers live while the runtime visits them:
|
||||
// a toJSON callback can mutate a later field, and shared/cyclic
|
||||
// references must retain their identities throughout traversal.
|
||||
@@ -276,6 +290,100 @@ function lowerOptionalStringifyRoot(
|
||||
return { kind: "call", callee: helper, args: [value], type: resultT, loc };
|
||||
}
|
||||
|
||||
/** True when a class field type holds a RegExp or a Promise outside a
|
||||
* nested class instance (whose own fields are checked separately): Node
|
||||
* writes both as {}, and the runtime JSON traversal has no writer for them. */
|
||||
function holdsUnwritable(lowerer: Lowerer, t: IrType, visiting: Set<string>): boolean {
|
||||
switch (t.kind) {
|
||||
case "regex":
|
||||
case "promise":
|
||||
return true;
|
||||
case "array":
|
||||
return holdsUnwritable(lowerer, t.elem, visiting);
|
||||
case "record": {
|
||||
if (visiting.has(t.shapeId)) return false;
|
||||
visiting.add(t.shapeId);
|
||||
const shape = lowerer.shapes.get(t.shapeId);
|
||||
return (
|
||||
(shape?.fields ?? []).some((field) => holdsUnwritable(lowerer, field.type, visiting)) ||
|
||||
(shape?.indexValue !== undefined && holdsUnwritable(lowerer, shape.indexValue, visiting))
|
||||
);
|
||||
}
|
||||
case "union": {
|
||||
if (visiting.has(t.unionId)) return false;
|
||||
visiting.add(t.unionId);
|
||||
return (lowerer.unions.get(t.unionId)?.arms ?? []).some((arm) =>
|
||||
holdsUnwritable(lowerer, arm, visiting),
|
||||
);
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** The first class field reachable from a stringify input that the class's
|
||||
* checked-dynamic property view cannot represent, or that holds a RegExp or
|
||||
* a Promise, which the runtime JSON traversal cannot write. The traversal
|
||||
* reads class instances through that view, so such a class is refused at
|
||||
* compile time instead of throwing when the view is built or written. */
|
||||
function classFieldWithoutJsonView(
|
||||
lowerer: Lowerer,
|
||||
t: IrType,
|
||||
visiting: Set<string>,
|
||||
): { className: string; field: string; type: IrType; reason: "read" | "write" } | null {
|
||||
const getRecord = (id: string) => lowerer.shapes.get(id);
|
||||
const getUnion = (id: string) => lowerer.unions.get(id);
|
||||
switch (t.kind) {
|
||||
case "array":
|
||||
return classFieldWithoutJsonView(lowerer, t.elem, visiting);
|
||||
case "record": {
|
||||
if (visiting.has(t.shapeId)) return null;
|
||||
visiting.add(t.shapeId);
|
||||
const shape = getRecord(t.shapeId);
|
||||
for (const field of shape?.fields ?? []) {
|
||||
const found = classFieldWithoutJsonView(lowerer, field.type, visiting);
|
||||
if (found) return found;
|
||||
}
|
||||
return shape?.indexValue
|
||||
? classFieldWithoutJsonView(lowerer, shape.indexValue, visiting)
|
||||
: null;
|
||||
}
|
||||
case "union": {
|
||||
if (visiting.has(t.unionId)) return null;
|
||||
visiting.add(t.unionId);
|
||||
for (const arm of getUnion(t.unionId)?.arms ?? []) {
|
||||
const found = classFieldWithoutJsonView(lowerer, arm, visiting);
|
||||
if (found) return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
case "object": {
|
||||
if (!isDynTypedRefType(t) || visiting.has(t.className)) return null;
|
||||
visiting.add(t.className);
|
||||
const info = lowerer.classes.get(t.className);
|
||||
for (const field of info?.def.fields ?? []) {
|
||||
if (!isClassOwnEnumerableFieldName(field.name)) continue;
|
||||
const unreadable = !classDynViewSupported([field], getRecord, getUnion);
|
||||
if (unreadable || holdsUnwritable(lowerer, field.type, new Set())) {
|
||||
const className =
|
||||
info?.def.jsName ?? info?.decl?.name?.text ?? t.className.replace(/^%/, "");
|
||||
return {
|
||||
className,
|
||||
field: field.name,
|
||||
type: field.type,
|
||||
reason: unreadable ? "read" : "write",
|
||||
};
|
||||
}
|
||||
const found = classFieldWithoutJsonView(lowerer, field.type, visiting);
|
||||
if (found) return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Only explicit null/undefined select the no-callback path. */
|
||||
function jsonNullishArgument(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
if (ts.isParenthesizedExpression(node)) return jsonNullishArgument(lowerer, node.expression);
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer, own } from "../lowerer.js";
|
||||
import { lowerCheckedPredicateValue } from "../lower-builtin-values.js";
|
||||
import { locOf } from "../../program.js";
|
||||
import { BOOL, F64, type IrExpr, type IrLibFn } from "../../../ir/ir.js";
|
||||
import { BOOL, F64, STRING, type IrExpr, type IrLibFn } from "../../../ir/ir.js";
|
||||
import { lowerBuiltinValuePreservingUndefined, lowerOptionalNumberPredicate } from "./arguments.js";
|
||||
import { tryLowerExpression } from "../expressions/try-lower-expression.js";
|
||||
import { lowerStaticNumberGlobal } from "../lower-calls.js";
|
||||
@@ -88,6 +88,22 @@ export function lowerNumberStaticCall(
|
||||
const parsed = lowerStaticNumberGlobal(lowerer, member, call, call.arguments, loc);
|
||||
if (parsed !== null) return parsed;
|
||||
}
|
||||
// A checked-dynamic argument (`Number.parseInt(env.FORCE_COLOR, 10)`
|
||||
// over a JavaScript dictionary read): the parsers' own ToString,
|
||||
// exactly the global parseFloat's dyn form.
|
||||
if (probed?.type.kind === "dyn") {
|
||||
const text: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "dyn.toStringCoerce",
|
||||
args: [probed],
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
if (member === "parseFloat")
|
||||
return { kind: "libCall", fn: "num.parseFloat", args: [text], type: F64, loc };
|
||||
const radix = lowerer.lowerExprExpecting(call.arguments[1]!, F64);
|
||||
return { kind: "libCall", fn: "num.parseInt", args: [text, radix], type: F64, loc };
|
||||
}
|
||||
const argText =
|
||||
probed !== null
|
||||
? lowerer.fmt(probed.type)
|
||||
|
||||
@@ -233,6 +233,41 @@ export function lowerProcessStreamProperty(
|
||||
* read in source order). Method members referenced without a call are
|
||||
* rejected specifically. Null for non-process receivers (the chain keeps
|
||||
* trying other property lowerings). */
|
||||
/** The value of a DATA member of the process global taken off by
|
||||
* destructuring (`const { env, argv } = process` in JavaScript, the
|
||||
* supports-color opening): the same values the spelled `process.env` /
|
||||
* `process.argv` reads produce, typed by the binding. Null for members
|
||||
* this does not cover (methods keep the destructuring fence). */
|
||||
export function processDataMemberValue(
|
||||
lowerer: Lowerer,
|
||||
member: string,
|
||||
binding: ts.Node,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
switch (member) {
|
||||
case "env": {
|
||||
const mapped = lowerer.mapTypeOf(lowerer.typeOf(binding));
|
||||
if (mapped?.kind !== "record") return null;
|
||||
const helper = envSnapshotHelper(lowerer, mapped.shapeId, loc);
|
||||
return helper === null ? null : { kind: "call", callee: helper, args: [], type: mapped, loc };
|
||||
}
|
||||
case "argv":
|
||||
return { kind: "libCall", fn: "process.argv", args: [], type: arrayOf(STRING), loc };
|
||||
case "platform":
|
||||
return { kind: "libCall", fn: "process.platform", args: [], type: STRING, loc };
|
||||
case "arch":
|
||||
return { kind: "libCall", fn: "process.arch", args: [], type: STRING, loc };
|
||||
case "pid":
|
||||
return { kind: "libCall", fn: "process.pid", args: [], type: F64, loc };
|
||||
case "execPath":
|
||||
return { kind: "libCall", fn: "process.execPath", args: [], type: STRING, loc };
|
||||
case "versions":
|
||||
return { kind: "libCall", fn: "process.versions", args: [], type: DYN, loc };
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function lowerProcessProperty(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.PropertyAccessExpression,
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer, PoisonError } from "../lowerer.js";
|
||||
import { locOf } from "../../program.js";
|
||||
import { webAssemblyDiag } from "../../../diagnostics/diagnostic.js";
|
||||
import { STRING, type IrExpr } from "../../../ir/ir.js";
|
||||
import { stdlibGlobalNameOf } from "../surfaces.js";
|
||||
|
||||
/* The WebAssembly API. scriptc.d.ts declares its types (lib.dom's
|
||||
* namespace) so that packages whose declarations mention them typecheck,
|
||||
* and the type mapper gives those types the checked-dynamic representation.
|
||||
* No module compiles to native code yet, so every reached use of the API
|
||||
* value fences at its expression with webAssemblyDiag: a build error in
|
||||
* TypeScript sources, a located runtime fence in JavaScript ones. typeof
|
||||
* needs no value and folds to Node's answer. */
|
||||
|
||||
/** Strips the spellings that leave a value unchanged: parentheses, type
|
||||
* assertions, `satisfies` and non-null assertions. */
|
||||
function bare(expr: ts.Expression): ts.Expression {
|
||||
for (;;) {
|
||||
if (ts.isParenthesizedExpression(expr) || ts.isNonNullExpression(expr)) expr = expr.expression;
|
||||
else if (ts.isAsExpression(expr) || ts.isTypeAssertion(expr)) expr = expr.expression;
|
||||
else if (ts.isSatisfiesExpression(expr)) expr = expr.expression;
|
||||
else return expr;
|
||||
}
|
||||
}
|
||||
|
||||
/** The global object itself, as `globalThis` or `global` spells it. */
|
||||
function isGlobalThis(lowerer: Lowerer, expr: ts.Expression): boolean {
|
||||
const node = bare(expr);
|
||||
return (
|
||||
ts.isIdentifier(node) &&
|
||||
(node.text === "globalThis" || node.text === "global") &&
|
||||
stdlibGlobalNameOf(lowerer, node) === "globalThis"
|
||||
);
|
||||
}
|
||||
|
||||
/** THE WebAssembly namespace value: `WebAssembly` or `globalThis.WebAssembly`
|
||||
* (and the global object reached through a cast, as in
|
||||
* `(globalThis as any).WebAssembly`). */
|
||||
function isWebAssemblyGlobal(lowerer: Lowerer, expr: ts.Expression): boolean {
|
||||
expr = bare(expr);
|
||||
if (ts.isIdentifier(expr)) {
|
||||
return expr.text === "WebAssembly" && stdlibGlobalNameOf(lowerer, expr) === "WebAssembly";
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(expr)) {
|
||||
return expr.name.text === "WebAssembly" && isGlobalThis(lowerer, expr.expression);
|
||||
}
|
||||
if (ts.isElementAccessExpression(expr)) {
|
||||
const key = expr.argumentExpression;
|
||||
return (
|
||||
(ts.isStringLiteral(key) || ts.isNoSubstitutionTemplateLiteral(key)) &&
|
||||
key.text === "WebAssembly" &&
|
||||
isGlobalThis(lowerer, expr.expression)
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The surface a WebAssembly API expression names ('WebAssembly',
|
||||
* 'globalThis.WebAssembly', 'WebAssembly.compile'), or null. A cheap
|
||||
* spelling check runs first: almost every expression is rejected without a
|
||||
* checker query. */
|
||||
function webAssemblySurface(lowerer: Lowerer, expr: ts.Expression): string | null {
|
||||
// `globalThis as unknown as { WebAssembly: ... }`: the cast's target type
|
||||
// names the member, and the value it checks is the global object's
|
||||
// WebAssembly (the spelling pi's codemode uses).
|
||||
if (
|
||||
(ts.isAsExpression(expr) || ts.isTypeAssertion(expr)) &&
|
||||
isGlobalThis(lowerer, expr.expression)
|
||||
) {
|
||||
const target = lowerer.checker.getTypeAtLocation(expr.type);
|
||||
return lowerer.checker.getPropertyOfType(target, "WebAssembly") !== undefined
|
||||
? "globalThis.WebAssembly"
|
||||
: null;
|
||||
}
|
||||
const node = bare(expr);
|
||||
if (ts.isIdentifier(node)) {
|
||||
return node.text === "WebAssembly" && isWebAssemblyGlobal(lowerer, node) ? "WebAssembly" : null;
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(node)) {
|
||||
if (node.name.text === "WebAssembly") {
|
||||
return isWebAssemblyGlobal(lowerer, node) ? "globalThis.WebAssembly" : null;
|
||||
}
|
||||
// WebAssembly.<member>, and members of members
|
||||
// (WebAssembly.Module.customSections).
|
||||
const names = [node.name.text];
|
||||
let receiver = bare(node.expression);
|
||||
while (ts.isPropertyAccessExpression(receiver) && receiver.name.text !== "WebAssembly") {
|
||||
names.unshift(receiver.name.text);
|
||||
receiver = bare(receiver.expression);
|
||||
}
|
||||
const spelled =
|
||||
(ts.isIdentifier(receiver) && receiver.text === "WebAssembly") ||
|
||||
(ts.isPropertyAccessExpression(receiver) && receiver.name.text === "WebAssembly");
|
||||
return spelled && isWebAssemblyGlobal(lowerer, receiver)
|
||||
? `WebAssembly.${names.join(".")}`
|
||||
: null;
|
||||
}
|
||||
if (ts.isElementAccessExpression(node)) {
|
||||
return isWebAssemblyGlobal(lowerer, node) ? "globalThis.WebAssembly" : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Fences a reached use of the WebAssembly API. `prefix` qualifies a
|
||||
* construction ('new '). Returns normally when `expr` is anything else. */
|
||||
export function refuseWebAssemblyUse(lowerer: Lowerer, expr: ts.Expression, prefix = ""): void {
|
||||
const surface = webAssemblySurface(lowerer, expr);
|
||||
if (surface === null) return;
|
||||
lowerer.pushDiag(webAssemblyDiag(`${prefix}${surface}`, locOf(expr)));
|
||||
throw new PoisonError();
|
||||
}
|
||||
|
||||
/** `typeof WebAssembly` is "object" and `typeof WebAssembly.<member>` is
|
||||
* "function" for every member the namespace declares, as in Node. The
|
||||
* operand is an effect-free global read, so nothing else evaluates. */
|
||||
export function lowerWebAssemblyTypeof(lowerer: Lowerer, operand: ts.Expression): IrExpr | null {
|
||||
const surface = webAssemblySurface(lowerer, operand);
|
||||
if (surface === null) return null;
|
||||
const loc = locOf(operand);
|
||||
if (surface === "WebAssembly" || surface === "globalThis.WebAssembly") {
|
||||
return { kind: "strLit", value: "object", type: STRING, loc };
|
||||
}
|
||||
const t = lowerer.typeOf(operand);
|
||||
if (
|
||||
lowerer.checker.getCallSignatures(t).length > 0 ||
|
||||
lowerer.checker.getConstructSignatures(t).length > 0
|
||||
) {
|
||||
return { kind: "strLit", value: "function", type: STRING, loc };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -503,6 +503,24 @@ export function requireExactArityValue(
|
||||
? lowerer.mapTypeOf(lowerer.typeOf(contextual))
|
||||
: null;
|
||||
if (mapped && typeEquals(mapped, funcType)) return;
|
||||
// An object-literal METHOD has no contextual type of its own: the
|
||||
// literal's contextual type names the member slot it fills. An
|
||||
// overloaded member's slot holds its erased function, which spells the
|
||||
// completed parameters of an implementation like `m(a?: string)`.
|
||||
if (
|
||||
contextual === null &&
|
||||
ts.isMethodDeclaration(blame) &&
|
||||
blame.parent !== undefined &&
|
||||
ts.isObjectLiteralExpression(blame.parent) &&
|
||||
(ts.isIdentifier(blame.name) || ts.isStringLiteral(blame.name))
|
||||
) {
|
||||
const literalType = lowerer.checker.getContextualType(blame.parent);
|
||||
const member = literalType
|
||||
? lowerer.checker.getPropertyOfType(literalType, blame.name.text)
|
||||
: undefined;
|
||||
const slot = member ? lowerer.memberSlotTypeOf(lowerer.checker.getTypeOfSymbol(member)) : null;
|
||||
if (slot && typeEquals(slot, funcType)) return;
|
||||
}
|
||||
// A union-typed slot (`runner || defaultRunner` under a
|
||||
// `CommandRunner | undefined` context): the value can only inhabit
|
||||
// the union's one func arm — judge by it.
|
||||
|
||||
@@ -0,0 +1,430 @@
|
||||
/* A CommonJS module's own exports object, seen from inside the module.
|
||||
*
|
||||
* The compiled module system has no reflective exports object: each export
|
||||
* is a module global (collectGlobals) or alias plumbing to a declaration.
|
||||
* Inside the module that is enough for
|
||||
*
|
||||
* - member reads and member calls (`exports.f`, `exports.f(x)`,
|
||||
* `module.exports.f(x)`): they are the member's export global, the
|
||||
* storage every `exports.f =` statement assigns;
|
||||
* - identity comparisons of the object itself (`root === module.exports`,
|
||||
* `exports === module.exports`): while the module never replaces
|
||||
* `module.exports` and never rebinds `exports`, both name the module's
|
||||
* original exports object, which top-level `this` already represents
|
||||
* (lowerCjsTopLevelThis). `typeof` and truthiness keep their existing
|
||||
* lowerings.
|
||||
*
|
||||
* Every other use of the object as a value (passing it to a function,
|
||||
* storing it, reading a member through an alias) would need a real object
|
||||
* whose properties are the exports; those uses keep a named refusal. The
|
||||
* dynamic CommonJS module object is the two-tier design's S9 stage. */
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import {
|
||||
cjsExportAssignmentOf,
|
||||
cjsExportTargetLiteral,
|
||||
isCjsJsFile,
|
||||
isModuleExportsAccess,
|
||||
isRequireStatement,
|
||||
locOf,
|
||||
} from "../program.js";
|
||||
import { isCjsModuleGlobal } from "./lower-node-module.js";
|
||||
import { DYN, type IrExpr, type IrGlobal } from "../../ir/ir.js";
|
||||
|
||||
/** True for the CommonJS wrapper's `exports` binding (not shadowed by any
|
||||
* source declaration) in a CommonJS JS file. */
|
||||
export function isCjsExportsIdentifier(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
if (!ts.isIdentifier(node) || node.text !== "exports") return false;
|
||||
if (!isCjsJsFile(node.getSourceFile(), lowerer.program)) return false;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(node);
|
||||
const decls = symbol ? lowerer.checker.declarationsOf(symbol) : [];
|
||||
return !decls.some(
|
||||
(d) =>
|
||||
!d.getSourceFile().isDeclarationFile &&
|
||||
(ts.isVariableDeclaration(d) ||
|
||||
ts.isParameter(d) ||
|
||||
ts.isBindingElement(d) ||
|
||||
ts.isFunctionDeclaration(d) ||
|
||||
ts.isClassDeclaration(d) ||
|
||||
ts.isImportSpecifier(d) ||
|
||||
ts.isNamespaceImport(d) ||
|
||||
ts.isImportEqualsDeclaration(d)),
|
||||
);
|
||||
}
|
||||
|
||||
/** `module.exports` over the CommonJS `module` global in a CommonJS file. */
|
||||
export function isCjsModuleExports(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
return (
|
||||
isModuleExportsAccess(node) &&
|
||||
isCjsModuleGlobal(lowerer, node.expression) &&
|
||||
isCjsJsFile(node.getSourceFile(), lowerer.program)
|
||||
);
|
||||
}
|
||||
|
||||
/** The export global behind a same-module member access `exports.<name>` /
|
||||
* `module.exports.<name>`, or null (identifier-valued members are alias
|
||||
* plumbing with no storage, and other receivers are not the exports
|
||||
* object). */
|
||||
export function cjsInFileExportGlobalOf(
|
||||
lowerer: Lowerer,
|
||||
access: ts.PropertyAccessExpression,
|
||||
): IrGlobal | null {
|
||||
if (access.questionDotToken || !ts.isIdentifier(access.name)) return null;
|
||||
const sf = access.getSourceFile();
|
||||
const recv =
|
||||
isModuleExportsAccess(access.expression) ||
|
||||
(ts.isIdentifier(access.expression) &&
|
||||
access.expression.text === "exports" &&
|
||||
!lowerer.resolveLocal(access.expression) &&
|
||||
!lowerer.globalOf(access.expression));
|
||||
if (!recv || !isCjsJsFile(sf, lowerer.program)) return null;
|
||||
const sym =
|
||||
lowerer.checker.getSymbolAtLocation(access.name) ??
|
||||
lowerer.cjsModuleExportSymbol(sf, access.name.text);
|
||||
const resolved =
|
||||
sym !== undefined && sym.flags & ts.SymbolFlags.Alias
|
||||
? lowerer.checker.getAliasedSymbol(sym)
|
||||
: sym;
|
||||
return (
|
||||
(sym ? lowerer.globalsBySymbol.get(sym) : undefined) ??
|
||||
(resolved ? lowerer.globalsBySymbol.get(resolved) : undefined) ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
/** Whether `node` is an operand of an equality operator: only its identity
|
||||
* is observed. */
|
||||
function identityUse(node: ts.Expression): boolean {
|
||||
let child: ts.Node = node;
|
||||
let parent = node.parent;
|
||||
while (parent !== undefined && ts.isParenthesizedExpression(parent)) {
|
||||
child = parent;
|
||||
parent = parent.parent;
|
||||
}
|
||||
if (parent === undefined || !ts.isBinaryExpression(parent)) return false;
|
||||
const op = parent.operatorToken.kind;
|
||||
return (
|
||||
(op === ts.SyntaxKind.EqualsEqualsEqualsToken ||
|
||||
op === ts.SyntaxKind.ExclamationEqualsEqualsToken ||
|
||||
op === ts.SyntaxKind.EqualsEqualsToken ||
|
||||
op === ts.SyntaxKind.ExclamationEqualsToken) &&
|
||||
(parent.left === child || parent.right === child)
|
||||
);
|
||||
}
|
||||
|
||||
/** Uses whose answer does not need the object: `typeof`, `void`, and
|
||||
* truthiness tests (their existing lowerings answer them). */
|
||||
function answeredWithoutObject(node: ts.Expression): boolean {
|
||||
let child: ts.Node = node;
|
||||
let parent = node.parent;
|
||||
while (parent !== undefined && ts.isParenthesizedExpression(parent)) {
|
||||
child = parent;
|
||||
parent = parent.parent;
|
||||
}
|
||||
if (parent === undefined) return false;
|
||||
if (ts.isTypeOfExpression(parent) || ts.isVoidExpression(parent)) return true;
|
||||
return truthinessOnly(child as ts.Expression);
|
||||
}
|
||||
|
||||
function truthinessOnly(node: ts.Expression): boolean {
|
||||
let child: ts.Node = node;
|
||||
for (let parent = node.parent; parent !== undefined; child = parent, parent = parent.parent) {
|
||||
if (ts.isParenthesizedExpression(parent)) continue;
|
||||
if (ts.isPrefixUnaryExpression(parent))
|
||||
return parent.operator === ts.SyntaxKind.ExclamationToken;
|
||||
if (
|
||||
ts.isBinaryExpression(parent) &&
|
||||
(parent.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
|
||||
parent.operatorToken.kind === ts.SyntaxKind.BarBarToken)
|
||||
)
|
||||
continue;
|
||||
if (ts.isIfStatement(parent) || ts.isWhileStatement(parent) || ts.isDoStatement(parent))
|
||||
return parent.expression === child;
|
||||
if (ts.isForStatement(parent)) return parent.condition === child;
|
||||
if (ts.isConditionalExpression(parent)) return parent.condition === child;
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Which of the module's exports bindings the file ever assigns (any
|
||||
* nesting, compound operators included), cached per file on the lowerer
|
||||
* (Lowerer.cjsExportsAssignments). */
|
||||
function exportsAssignments(
|
||||
lowerer: Lowerer,
|
||||
sf: ts.SourceFile,
|
||||
): { exports: boolean; moduleExports: boolean } {
|
||||
const cached = lowerer.cjsExportsAssignments.get(sf);
|
||||
if (cached !== undefined) return cached;
|
||||
const out = { exports: false, moduleExports: false };
|
||||
lowerer.cjsExportsAssignments.set(sf, out);
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
// A fresh `exports = module.exports = {}` reset leaves both bindings
|
||||
// naming one (new, empty) exports object for the rest of the module.
|
||||
if (ts.isExpressionStatement(node) && isFreshExportsReset(lowerer, node)) return "skip";
|
||||
if (
|
||||
!ts.isBinaryExpression(node) ||
|
||||
node.operatorToken.kind < ts.SyntaxKind.FirstAssignment ||
|
||||
node.operatorToken.kind > ts.SyntaxKind.LastAssignment
|
||||
)
|
||||
return undefined;
|
||||
let target = node.left;
|
||||
while (ts.isParenthesizedExpression(target)) target = target.expression;
|
||||
if (isCjsModuleExports(lowerer, target)) out.moduleExports = true;
|
||||
if (isCjsExportsIdentifier(lowerer, target)) out.exports = true;
|
||||
return undefined;
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Whether `node` still names the module's ORIGINAL exports object wherever
|
||||
* it runs: `exports` unless the file rebinds it; `module.exports` unless the
|
||||
* file rebinds `exports` or replaces `module.exports`. */
|
||||
function namesOriginalExports(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
const assigned = exportsAssignments(lowerer, node.getSourceFile());
|
||||
if (assigned.exports) return false;
|
||||
return ts.isIdentifier(node) || !assigned.moduleExports;
|
||||
}
|
||||
|
||||
/** The uses this module answers with the original exports object: identity
|
||||
* comparisons and `typeof` of either spelling, and truthiness or `void` of
|
||||
* `exports` (`module.exports` truthiness keeps its existing fold). */
|
||||
function objectAnsweredUse(node: ts.Expression): boolean {
|
||||
if (identityUse(node)) return true;
|
||||
let parent = node.parent;
|
||||
while (parent !== undefined && ts.isParenthesizedExpression(parent)) parent = parent.parent;
|
||||
if (parent !== undefined && ts.isTypeOfExpression(parent)) return true;
|
||||
return ts.isIdentifier(node) && answeredWithoutObject(node);
|
||||
}
|
||||
|
||||
/** Whether `node` (`exports` or `module.exports`) is a use of the module's
|
||||
* exports object as a VALUE (not a member access receiver, not an
|
||||
* assignment target). */
|
||||
export function isCjsExportsObjectValue(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
if (!isCjsExportsIdentifier(lowerer, node) && !isCjsModuleExports(lowerer, node)) return false;
|
||||
let outer: ts.Node = node;
|
||||
while (outer.parent !== undefined && ts.isParenthesizedExpression(outer.parent))
|
||||
outer = outer.parent;
|
||||
const parent = outer.parent;
|
||||
if (parent === undefined) return false;
|
||||
if (
|
||||
(ts.isPropertyAccessExpression(parent) || ts.isElementAccessExpression(parent)) &&
|
||||
parent.expression === outer
|
||||
)
|
||||
return false;
|
||||
if (
|
||||
ts.isBinaryExpression(parent) &&
|
||||
parent.left === outer &&
|
||||
parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
|
||||
parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment
|
||||
)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Files whose exports object is observed by identity somewhere: their top-
|
||||
* level `this` object must exist (lowerFileInit creates it). */
|
||||
export function usesCjsExportsIdentity(lowerer: Lowerer, sf: ts.SourceFile): boolean {
|
||||
if (!isCjsJsFile(sf, lowerer.program)) return false;
|
||||
let found = false;
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
if (
|
||||
(ts.isIdentifier(node) || ts.isPropertyAccessExpression(node)) &&
|
||||
isCjsExportsObjectValue(lowerer, node) &&
|
||||
objectAnsweredUse(node) &&
|
||||
namesOriginalExports(lowerer, node)
|
||||
) {
|
||||
found = true;
|
||||
return "stop";
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
/** `exports` / `module.exports` used as a value inside its own module: the
|
||||
* original exports object for identity, typeof and truthiness, or a named
|
||||
* refusal. Null when `node` is not such a use. */
|
||||
export function lowerCjsExportsObjectValue(lowerer: Lowerer, node: ts.Expression): IrExpr | null {
|
||||
if (!isCjsExportsObjectValue(lowerer, node)) return null;
|
||||
const sf = node.getSourceFile();
|
||||
// A module that replaces module.exports answers `module.exports` reads
|
||||
// through the replacement's own machinery (the class-expression whole
|
||||
// export, the replaced-exports reads).
|
||||
if (!ts.isIdentifier(node) && exportsAssignments(lowerer, sf).moduleExports) return null;
|
||||
const spelled = ts.isIdentifier(node) ? "exports" : "module.exports";
|
||||
const original = lowerer.cjsExportsThis.get(sf);
|
||||
if (objectAnsweredUse(node) && original !== undefined && namesOriginalExports(lowerer, node)) {
|
||||
return { kind: "varRef", localId: original.id, type: DYN, loc: locOf(node) };
|
||||
}
|
||||
// `module.exports` truthiness keeps its existing fold (and anything the
|
||||
// object is not needed for keeps whatever answered it before).
|
||||
if (answeredWithoutObject(node)) return null;
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
node,
|
||||
identityUse(node)
|
||||
? `comparing '${spelled}' in a module that replaces module.exports or rebinds exports (the compiled exports are a static table, not an object with an identity)`
|
||||
: `'${spelled}' as a value other than a member access, comparison, typeof or truthiness test (the compiled exports are a static table, not an object: read ${spelled}.<name>)`,
|
||||
);
|
||||
}
|
||||
|
||||
/** `exports = module.exports = {}` (either order) as a top-level statement
|
||||
* that runs before anything could observe or extend the original exports
|
||||
* object: only directives, require statements and function declarations
|
||||
* precede it, and the module assigns neither binding anywhere else. Node
|
||||
* swaps the original empty exports object for another empty one that both
|
||||
* bindings name; the compiled module's static exports start out exactly
|
||||
* the same, so the statement is a no-op (semver's internal/re.js). */
|
||||
export function isFreshExportsReset(lowerer: Lowerer, stmt: ts.ExpressionStatement): boolean {
|
||||
const sf = stmt.parent;
|
||||
if (!ts.isSourceFile(sf) || !isCjsJsFile(sf, lowerer.program)) return false;
|
||||
let e: ts.Expression = stmt.expression;
|
||||
const targets: ts.Expression[] = [];
|
||||
for (;;) {
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
if (!ts.isBinaryExpression(e) || e.operatorToken.kind !== ts.SyntaxKind.EqualsToken) break;
|
||||
targets.push(e.left);
|
||||
e = e.right;
|
||||
}
|
||||
if (!ts.isObjectLiteralExpression(e) || e.properties.length !== 0) return false;
|
||||
if (targets.length !== 2) return false;
|
||||
const hasExports = targets.some((t) => isCjsExportsIdentifier(lowerer, t));
|
||||
const hasModuleExports = targets.some((t) => isCjsModuleExports(lowerer, t));
|
||||
if (!hasExports || !hasModuleExports) return false;
|
||||
for (const prior of sf.statements) {
|
||||
if (prior === stmt) break;
|
||||
if (ts.isFunctionDeclaration(prior) || isRequireStatement(prior)) continue;
|
||||
if (ts.isExpressionStatement(prior) && ts.isStringLiteral(prior.expression)) continue;
|
||||
return false;
|
||||
}
|
||||
let others = 0;
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
if (
|
||||
!ts.isBinaryExpression(node) ||
|
||||
node.operatorToken.kind < ts.SyntaxKind.FirstAssignment ||
|
||||
node.operatorToken.kind > ts.SyntaxKind.LastAssignment
|
||||
)
|
||||
return undefined;
|
||||
let target = node.left;
|
||||
while (ts.isParenthesizedExpression(target)) target = target.expression;
|
||||
if (isCjsModuleExports(lowerer, target) || isCjsExportsIdentifier(lowerer, target)) others++;
|
||||
return undefined;
|
||||
});
|
||||
return others === 2;
|
||||
}
|
||||
|
||||
/** The export global a WRITE `exports.<name> = v` / `module.exports.<name> =
|
||||
* v` updates when it is not a top-level export statement (inside a
|
||||
* function, or in value position): only while the module keeps its
|
||||
* original exports object, which is then the object every requirer reads
|
||||
* the member from. */
|
||||
export function cjsExportMemberWriteGlobalOf(
|
||||
lowerer: Lowerer,
|
||||
target: ts.PropertyAccessExpression,
|
||||
): IrGlobal | null {
|
||||
const sf = target.getSourceFile();
|
||||
if (!isCjsJsFile(sf, lowerer.program)) return null;
|
||||
const assigned = exportsAssignments(lowerer, sf);
|
||||
if (assigned.exports || assigned.moduleExports) return null;
|
||||
return cjsInFileExportGlobalOf(lowerer, target);
|
||||
}
|
||||
|
||||
/** Whether a function body reads `this` (arrow functions inherit it; other
|
||||
* nested functions bind their own). */
|
||||
function functionUsesThis(fn: ts.FunctionLikeDeclaration): boolean {
|
||||
if (fn.body === undefined) return false;
|
||||
let found = false;
|
||||
ts.walkPreorder(fn.body, (node) => {
|
||||
if (node !== fn.body && ts.isFunctionLike(node) && !ts.isArrowFunction(node)) return "skip";
|
||||
if (node.kind === ts.SyntaxKind.ThisKeyword) {
|
||||
found = true;
|
||||
return "stop";
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
/** A function declaration, or a variable initialized with a function or
|
||||
* arrow expression, that never reads `this`. */
|
||||
function symbolIsThisFreeFunction(lowerer: Lowerer, sym: ts.Symbol | undefined): boolean {
|
||||
const decls = sym ? lowerer.checker.declarationsOf(sym) : [];
|
||||
const fn = decls.find(ts.isFunctionDeclaration);
|
||||
if (fn !== undefined) return !functionUsesThis(fn);
|
||||
const v = decls.find(ts.isVariableDeclaration);
|
||||
let init = v?.initializer;
|
||||
while (init !== undefined && ts.isParenthesizedExpression(init)) init = init.expression;
|
||||
return (
|
||||
init !== undefined &&
|
||||
(ts.isFunctionExpression(init) || ts.isArrowFunction(init)) &&
|
||||
!functionUsesThis(init)
|
||||
);
|
||||
}
|
||||
|
||||
/** True when every value the module assigns to its export `name` is a
|
||||
* function that never reads `this`. A call through the exports object
|
||||
* (`exports.f(x)`) binds `this` to that object, which the static export
|
||||
* table cannot supply, so only these calls lower; the others keep a fence. */
|
||||
export function cjsExportCallIgnoresThis(
|
||||
lowerer: Lowerer,
|
||||
sf: ts.SourceFile,
|
||||
name: string,
|
||||
depth = 0,
|
||||
): boolean {
|
||||
if (depth > 16) return false;
|
||||
const values: ts.Expression[] = [];
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
if (
|
||||
ts.isBinaryExpression(node) &&
|
||||
node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
||||
ts.isPropertyAccessExpression(node.left) &&
|
||||
node.left.name.text === name &&
|
||||
(isCjsExportsIdentifier(lowerer, node.left.expression) ||
|
||||
isCjsModuleExports(lowerer, node.left.expression))
|
||||
) {
|
||||
values.push(node.right);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
// Entries of a `module.exports = { ... }` table (direct, or a const
|
||||
// literal the statement names).
|
||||
let methodEntry: ts.MethodDeclaration | null = null;
|
||||
let shorthandOnly = false;
|
||||
for (const stmt of sf.statements) {
|
||||
const cjs = cjsExportAssignmentOf(stmt);
|
||||
if (cjs?.kind !== "table") continue;
|
||||
const table = cjs.obj ?? cjsExportTargetLiteral(cjs.expr.right, sf);
|
||||
for (const prop of table?.properties ?? []) {
|
||||
if (prop.name === undefined || !ts.isIdentifier(prop.name) || prop.name.text !== name)
|
||||
continue;
|
||||
if (ts.isPropertyAssignment(prop)) values.push(prop.initializer);
|
||||
else if (ts.isShorthandPropertyAssignment(prop)) {
|
||||
const value = lowerer.checker.getShorthandAssignmentValueSymbol(prop);
|
||||
if (!symbolIsThisFreeFunction(lowerer, value)) return false;
|
||||
shorthandOnly = true;
|
||||
} else if (ts.isMethodDeclaration(prop)) methodEntry = prop;
|
||||
}
|
||||
}
|
||||
if (methodEntry !== null) return values.length === 0 && !functionUsesThis(methodEntry);
|
||||
if (values.length === 0) return shorthandOnly;
|
||||
return values.every((v) => {
|
||||
let e = v;
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
if (ts.isFunctionExpression(e) || ts.isArrowFunction(e)) return !functionUsesThis(e);
|
||||
if (ts.isIdentifier(e)) {
|
||||
return symbolIsThisFreeFunction(lowerer, lowerer.checker.getSymbolAtLocation(e));
|
||||
}
|
||||
// Another module's member (`exports.f = require("./lib").f`, or through
|
||||
// a module binding): that module's own export values decide.
|
||||
if (ts.isPropertyAccessExpression(e) && ts.isIdentifier(e.name)) {
|
||||
const dep = ts.isIdentifier(e.expression)
|
||||
? lowerer.cjsLocalModuleBindingOf(e.expression)
|
||||
? lowerer.cjsModuleBindingDepOf(e.expression)
|
||||
: null
|
||||
: lowerer.cjsModuleRefDepOf(e.expression);
|
||||
return dep !== null && cjsExportCallIgnoresThis(lowerer, dep, e.name.text, depth + 1);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
/* Inline CommonJS `require("./x")` of a program module in expression
|
||||
* position. A top-level `const m = require("./x")` binding is alias
|
||||
* plumbing: member reads resolve to the exporter's declarations, and the
|
||||
* statement lowers to the dependency's run-once %init call. An inline
|
||||
* require is the same thing without the binding: the %init call runs at
|
||||
* exactly the call's position (Node evaluates the module there, or
|
||||
* answers from its cache), and the value is then read statically:
|
||||
*
|
||||
* - `require("./x").f` and `require("./x").f(...)` over a module whose
|
||||
* exports are a table (member exports or `module.exports = {...}`)
|
||||
* read the member like a binding would;
|
||||
* - `require("./x")` as a value over a module whose whole export is one
|
||||
* value (`module.exports = fn` / `= class {}` / `= make()` / `= 42` /
|
||||
* `= require("./y")`) answers that value.
|
||||
*
|
||||
* A table module's exports object as a value has no static
|
||||
* representation; it keeps a named refusal (a first-class CommonJS module
|
||||
* object is the two-tier design's S9 stage). */
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import {
|
||||
canonicalBuiltinModule,
|
||||
cjsExportAssignmentOf,
|
||||
cjsExportDiscardReason,
|
||||
cjsExportTargetLiteral,
|
||||
isCjsJsFile,
|
||||
isJsSourceFile,
|
||||
isModuleExportsAccess,
|
||||
locOf,
|
||||
npmStaticDepSf7,
|
||||
requireSpecOf,
|
||||
resolveImport,
|
||||
} from "../program.js";
|
||||
import { createRequireCalleeFileOf } from "./builtins/module-bindings.js";
|
||||
import { isCjsRequireGlobal } from "./lower-node-module.js";
|
||||
import { DYN, type IrExpr } from "../../ir/ir.js";
|
||||
import { nodeThrowExpr } from "../../ir/build.js";
|
||||
import { probeNodeRequireRefusal } from "../npm.js";
|
||||
|
||||
export interface InlineRequire {
|
||||
call: ts.CallExpression;
|
||||
spec: string;
|
||||
dep: ts.SourceFile;
|
||||
}
|
||||
|
||||
/** A `require("<literal>")` call of the CommonJS global in a CommonJS JS
|
||||
* file whose target is a compiled program module (relative, aliased, or an
|
||||
* opted-in --npm-static package). Builtins, JSON documents and
|
||||
* createRequire bindings answer null: their own lowerings own them. */
|
||||
export function inlineProgramRequireOf(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.Expression,
|
||||
): InlineRequire | null {
|
||||
let e = expr;
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
if (!ts.isCallExpression(e) || !ts.isIdentifier(e.expression)) return null;
|
||||
const spec = requireSpecOf(e);
|
||||
if (spec === null || canonicalBuiltinModule(spec) !== null) return null;
|
||||
const sf = e.getSourceFile();
|
||||
if (!isCjsJsFile(sf, lowerer.program)) return null;
|
||||
if (!isCjsRequireGlobal(lowerer, e.expression)) return null;
|
||||
if (createRequireCalleeFileOf(lowerer, e.expression) !== null) return null;
|
||||
const dep =
|
||||
resolveImport(lowerer.program, sf, spec, "require") ??
|
||||
npmStaticDepSf7(lowerer.program, sf, spec, "require");
|
||||
if (!dep || dep.isDeclarationFile || dep.fileName.endsWith(".json")) return null;
|
||||
return { call: e, spec, dep };
|
||||
}
|
||||
|
||||
/** The kept (last, not discarded) top-level `module.exports = <value>` of a
|
||||
* CommonJS module whose whole export is a single value, or null when its
|
||||
* exports are a table: member exports only, or a literal / const-literal /
|
||||
* Proxy-over-literal table. */
|
||||
export function cjsWholeExportValueOf(
|
||||
sf: ts.SourceFile,
|
||||
): { assign: ts.BinaryExpression; rhs: ts.Expression } | null {
|
||||
let kept: ts.BinaryExpression | null = null;
|
||||
for (const stmt of sf.statements) {
|
||||
const cjs = cjsExportAssignmentOf(stmt);
|
||||
if (cjs?.kind === "table" && cjsExportDiscardReason(stmt) === null) kept = cjs.expr;
|
||||
}
|
||||
if (kept === null) return null;
|
||||
let rhs: ts.Expression = kept.right;
|
||||
while (ts.isParenthesizedExpression(rhs)) rhs = rhs.expression;
|
||||
if (ts.isObjectLiteralExpression(rhs) || cjsExportTargetLiteral(rhs, sf) !== null) return null;
|
||||
return { assign: kept, rhs };
|
||||
}
|
||||
|
||||
/** True when `dep`'s exports, through `module.exports = require("./y")`
|
||||
* re-export chains, are a table that member reads resolve against. */
|
||||
export function cjsNamespaceModule(lowerer: Lowerer, dep: ts.SourceFile, depth = 0): boolean {
|
||||
const whole = cjsWholeExportValueOf(dep);
|
||||
if (whole === null) return true;
|
||||
const inner = inlineProgramRequireOf(lowerer, whole.rhs);
|
||||
return inner !== null && depth < 32 && cjsNamespaceModule(lowerer, inner.dep, depth + 1);
|
||||
}
|
||||
|
||||
/** An inline program-module require over a table module (member reads and
|
||||
* member calls resolve through it), or null. */
|
||||
export function inlineNamespaceRequireOf(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.Expression,
|
||||
): InlineRequire | null {
|
||||
const req = inlineProgramRequireOf(lowerer, expr);
|
||||
return req !== null && cjsNamespaceModule(lowerer, req.dep) ? req : null;
|
||||
}
|
||||
|
||||
/** `value` evaluated after the required module's run-once %init (and its
|
||||
* module-registry link) at the require's own position. */
|
||||
export function withRequireInit(lowerer: Lowerer, req: InlineRequire, value: IrExpr): IrExpr {
|
||||
if (lowerer.inlineRequireCycleCalls.has(req.call)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
req.call,
|
||||
`require() of '${req.spec}' while it is still evaluating (a require cycle: Node answers its partial exports, which the static export table does not model)`,
|
||||
);
|
||||
}
|
||||
// Every inline program require is a preflight module edge, so its module
|
||||
// has an initializer; never read its exports without evaluating it.
|
||||
if (!lowerer.initNameOf.has(req.dep)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
req.call,
|
||||
`require() of '${req.spec}' here (the module is not part of the compiled module order)`,
|
||||
);
|
||||
}
|
||||
const init = lowerer.requireInitStmt(req.spec, req.call);
|
||||
if (init === null) return value;
|
||||
return { kind: "seqExpr", stmts: [init], result: value, type: value.type, loc: value.loc };
|
||||
}
|
||||
|
||||
/** The whole-export value of `dep` read from the requirer, following
|
||||
* `module.exports = require("./y")` re-exports (the inner module already
|
||||
* evaluated inside dep's own %init). Null for table modules and for
|
||||
* whole-export shapes with no represented value. */
|
||||
export function wholeExportValue(
|
||||
lowerer: Lowerer,
|
||||
dep: ts.SourceFile,
|
||||
blame: ts.Node,
|
||||
depth = 0,
|
||||
): IrExpr | null {
|
||||
const whole = cjsWholeExportValueOf(dep);
|
||||
if (whole === null || depth > 32) return null;
|
||||
const inner = inlineProgramRequireOf(lowerer, whole.rhs);
|
||||
if (inner !== null) return wholeExportValue(lowerer, inner.dep, blame, depth + 1);
|
||||
// An identifier names a declaration of the exporting module (or a
|
||||
// builtin global): reading it IS the export, exactly what requirer
|
||||
// bindings alias to.
|
||||
if (ts.isIdentifier(whole.rhs)) return lowerer.lowerExpr(whole.rhs);
|
||||
if (ts.isClassExpression(whole.rhs)) {
|
||||
return lowerer.classValueRef(lowerer.lowerClassExpressionInfo(whole.rhs), blame);
|
||||
}
|
||||
const g = lowerer.globalsByDeclNode.get(whole.assign);
|
||||
if (g === undefined) return null;
|
||||
return { kind: "varRef", localId: g.id, type: g.type, loc: locOf(blame) };
|
||||
}
|
||||
|
||||
/** `require("./x")` as a value (`require("debug")("ns")`, `new
|
||||
* (require("./C"))()`, `{ verify: require("./verify") }`): the module's
|
||||
* %init at this position, then its whole-export value. Null when `call`
|
||||
* is not an inline program-module require. */
|
||||
export function lowerInlineRequireValue(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null {
|
||||
const req = inlineProgramRequireOf(lowerer, call);
|
||||
if (req === null) return null;
|
||||
const value = wholeExportValue(lowerer, req.dep, call, 0);
|
||||
if (value === null) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
call,
|
||||
cjsNamespaceModule(lowerer, req.dep)
|
||||
? `the exports object of '${req.spec}' as a value (its exports compile to a static table: read require(${JSON.stringify(req.spec)}).member directly, or bind const m = require(${JSON.stringify(req.spec)}) at the module's top level)`
|
||||
: `the whole export of '${req.spec}' as a value (its 'module.exports =' value has no static representation)`,
|
||||
);
|
||||
}
|
||||
return withRequireInit(lowerer, req, value);
|
||||
}
|
||||
|
||||
/** Lowers `inner` (a member read or member call whose receiver is the
|
||||
* inline require) with that require marked as handled, so the receiver
|
||||
* resolves as a module binding, then sequences the module's %init first. */
|
||||
export function lowerWithInlineRequireInit(
|
||||
lowerer: Lowerer,
|
||||
req: InlineRequire,
|
||||
inner: () => IrExpr,
|
||||
): IrExpr {
|
||||
lowerer.inlineRequireInitDone.add(req.call);
|
||||
try {
|
||||
return withRequireInit(lowerer, req, inner());
|
||||
} finally {
|
||||
lowerer.inlineRequireInitDone.delete(req.call);
|
||||
}
|
||||
}
|
||||
|
||||
/** A `require(...)` call of the CommonJS global in a CommonJS file that no
|
||||
* other lowering claimed (not a builtin, JSON document, program module or
|
||||
* createRequire binding): a package nothing installed resolves throws
|
||||
* Node's catchable MODULE_NOT_FOUND at the call, and every other form is
|
||||
* refused by name instead of as an unlowerable `require` value. */
|
||||
export function lowerUncompiledRequireCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
): IrExpr | null {
|
||||
if (!ts.isIdentifier(call.expression) || call.expression.text !== "require") return null;
|
||||
const sf = call.getSourceFile();
|
||||
if (!isCjsJsFile(sf, lowerer.program) || !isCjsRequireGlobal(lowerer, call.expression))
|
||||
return null;
|
||||
if (createRequireCalleeFileOf(lowerer, call.expression) !== null) return null;
|
||||
const spec = requireSpecOf(call);
|
||||
if (spec === null) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
call,
|
||||
'require() with a computed specifier (the compiled module graph is fixed at build time: require("<string literal>") lowers)',
|
||||
);
|
||||
}
|
||||
if (canonicalBuiltinModule(spec) !== null) return null;
|
||||
if (!spec.startsWith(".") && !spec.startsWith("/") && !spec.startsWith("#")) {
|
||||
const refusal = probeNodeRequireRefusal(sf.fileName, spec);
|
||||
if (refusal !== null) {
|
||||
return nodeThrowExpr(0, "MODULE_NOT_FOUND", refusal.message, DYN, locOf(call));
|
||||
}
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
call,
|
||||
`require() of the package '${spec}' in an expression (the package is not compiled into this program)`,
|
||||
);
|
||||
}
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
call,
|
||||
`require() of '${spec}' in an expression (it does not resolve to a compiled program module)`,
|
||||
);
|
||||
}
|
||||
|
||||
/** The elements of a top-level `const { key: { ... } } = require("./x")`
|
||||
* declaration over a program module whose nested pattern destructures the
|
||||
* named export's value (preflight admits exactly these). Plain elements
|
||||
* stay alias plumbing. */
|
||||
export function requireNestedPatternsOf(decl: ts.VariableDeclaration): ts.BindingElement[] {
|
||||
if (!ts.isObjectBindingPattern(decl.name) || decl.initializer === undefined) return [];
|
||||
const spec = requireSpecOf(decl.initializer);
|
||||
if (spec === null || canonicalBuiltinModule(spec) !== null) return [];
|
||||
if (!isJsSourceFile(decl.getSourceFile())) return [];
|
||||
return decl.name.elements.filter(
|
||||
(e) =>
|
||||
e.name !== undefined &&
|
||||
ts.isObjectBindingPattern(e.name) &&
|
||||
e.propertyName !== undefined &&
|
||||
ts.isIdentifier(e.propertyName) &&
|
||||
e.initializer === undefined &&
|
||||
e.dotDotDotToken === undefined,
|
||||
);
|
||||
}
|
||||
|
||||
/** A plain method entry of a `module.exports = { ... }` table: identifier
|
||||
* name, a body, not a generator. Its value is the method's function. */
|
||||
export function cjsTableMethodEntry(
|
||||
prop: ts.ObjectLiteralElementLike,
|
||||
): (ts.MethodDeclaration & { name: ts.Identifier; body: ts.Block }) | null {
|
||||
if (!ts.isMethodDeclaration(prop) || !ts.isIdentifier(prop.name) || prop.body === undefined)
|
||||
return null;
|
||||
if (prop.asteriskToken !== undefined) return null;
|
||||
return prop as ts.MethodDeclaration & { name: ts.Identifier; body: ts.Block };
|
||||
}
|
||||
|
||||
/** `exports.<name> = value` / `module.exports.<name> = value` assignments in
|
||||
* VALUE position inside a top-level statement (`const src = exports.src =
|
||||
* []`), outside function bodies and outside the statement's own export
|
||||
* chain (`statementExport`, which the statement-level machinery owns). */
|
||||
export function nestedCjsExportAssignmentsOf(
|
||||
stmt: ts.Statement,
|
||||
statementExport: ts.BinaryExpression | undefined,
|
||||
): { name: ts.Identifier; value: ts.Expression }[] {
|
||||
if (!ts.isVariableStatement(stmt) && !ts.isExpressionStatement(stmt)) return [];
|
||||
const owned = new Set<ts.Node>();
|
||||
for (let e: ts.Expression | undefined = statementExport; e !== undefined;) {
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
if (!ts.isBinaryExpression(e) || e.operatorToken.kind !== ts.SyntaxKind.EqualsToken) break;
|
||||
owned.add(e);
|
||||
e = e.right;
|
||||
}
|
||||
const out: { name: ts.Identifier; value: ts.Expression }[] = [];
|
||||
ts.walkPreorder(stmt, (node) => {
|
||||
if (
|
||||
node !== stmt &&
|
||||
(ts.isFunctionLike(node) || ts.isClassDeclaration(node) || ts.isClassExpression(node))
|
||||
)
|
||||
return "skip";
|
||||
if (
|
||||
ts.isBinaryExpression(node) &&
|
||||
node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
||||
!owned.has(node) &&
|
||||
ts.isPropertyAccessExpression(node.left) &&
|
||||
node.left.questionDotToken === undefined &&
|
||||
ts.isIdentifier(node.left.name) &&
|
||||
((ts.isIdentifier(node.left.expression) && node.left.expression.text === "exports") ||
|
||||
isModuleExportsAccess(node.left.expression))
|
||||
) {
|
||||
out.push({ name: node.left.name, value: node.right });
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
/** `const { A, B: b } = ns` at a module's top level, where `ns` is a
|
||||
* CommonJS module binding over a table module (picomatch's `const {
|
||||
* MAX_LENGTH, ... } = constants`): each name takes a snapshot of the named
|
||||
* export's value at this statement, exactly like destructuring the exports
|
||||
* object would. Null for every other declaration; the plain elements and
|
||||
* their key names otherwise. */
|
||||
export function cjsNamespaceDestructureOf(
|
||||
lowerer: Lowerer,
|
||||
decl: ts.VariableDeclaration,
|
||||
): { dep: ts.SourceFile; elements: { name: ts.Identifier; key: string }[] } | null {
|
||||
if (!ts.isObjectBindingPattern(decl.name) || decl.initializer === undefined) return null;
|
||||
const stmt = decl.parent?.parent;
|
||||
if (stmt === undefined || !ts.isVariableStatement(stmt) || !ts.isSourceFile(stmt.parent))
|
||||
return null;
|
||||
if (!isJsSourceFile(stmt.parent)) return null;
|
||||
let init: ts.Expression = decl.initializer;
|
||||
while (ts.isParenthesizedExpression(init)) init = init.expression;
|
||||
if (!ts.isIdentifier(init) || !lowerer.cjsLocalModuleBindingOf(init)) return null;
|
||||
const dep = lowerer.cjsModuleBindingDepOf(init);
|
||||
if (dep === null) return null;
|
||||
const elements: { name: ts.Identifier; key: string }[] = [];
|
||||
for (const e of decl.name.elements) {
|
||||
if (e.initializer !== undefined || e.dotDotDotToken !== undefined) return null;
|
||||
if (e.name === undefined || !ts.isIdentifier(e.name)) return null;
|
||||
const key =
|
||||
e.propertyName === undefined
|
||||
? e.name.text
|
||||
: ts.isIdentifier(e.propertyName) || ts.isStringLiteral(e.propertyName)
|
||||
? e.propertyName.text
|
||||
: null;
|
||||
if (key === null) return null;
|
||||
elements.push({ name: e.name, key });
|
||||
}
|
||||
return { dep, elements };
|
||||
}
|
||||
|
||||
/** The value of `dep`'s export `key` read from another module: its export
|
||||
* global, or the function/class declaration it aliases. Null when it has no
|
||||
* static value. */
|
||||
export function cjsExportValueByName(
|
||||
lowerer: Lowerer,
|
||||
dep: ts.SourceFile,
|
||||
key: string,
|
||||
blame: ts.Node,
|
||||
): IrExpr | null {
|
||||
let sym = lowerer.cjsModuleExportSymbol(dep, key);
|
||||
if (sym === undefined) return null;
|
||||
if (sym.flags & ts.SymbolFlags.Alias) sym = lowerer.checker.getAliasedSymbol(sym);
|
||||
const direct = lowerer.globalsBySymbol.get(sym);
|
||||
if (direct) return { kind: "varRef", localId: direct.id, type: direct.type, loc: locOf(blame) };
|
||||
for (const d of lowerer.checker.declarationsOf(sym)) {
|
||||
const byDecl = lowerer.globalsByDeclNode.get(d);
|
||||
if (byDecl) return { kind: "varRef", localId: byDecl.id, type: byDecl.type, loc: locOf(blame) };
|
||||
// Alias plumbing: a shorthand/identifier table entry or a member
|
||||
// export naming a declaration. Reading that identifier IS the value.
|
||||
if (ts.isShorthandPropertyAssignment(d)) return lowerer.lowerShorthandValue(d);
|
||||
if (ts.isPropertyAssignment(d) && ts.isIdentifier(d.initializer)) {
|
||||
return lowerer.lowerExpr(d.initializer);
|
||||
}
|
||||
if ((ts.isFunctionDeclaration(d) || ts.isClassDeclaration(d)) && d.name !== undefined) {
|
||||
return lowerer.lowerExpr(d.name);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
/* tsc's CommonJS import interop: `const x_1 = __importDefault(require("./m"))`
|
||||
* and `const x_1 = __importStar(require("./m"))` at a module's top level,
|
||||
* with the helpers tsc emits at the top of the file:
|
||||
*
|
||||
* var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
* return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
* };
|
||||
*
|
||||
* For a CommonJS dependency the helper's answer is fixed by the
|
||||
* dependency's source: stamped with `__esModule` (tsc output), the binding
|
||||
* IS the module's exports; otherwise `x_1.default` is the module's exports
|
||||
* (and __importStar also copies every export onto x_1). Both are the
|
||||
* static alias model a plain `const x_1 = require("./m")` already has, so
|
||||
* the binding lowers as a require binding: the statement is the module's
|
||||
* %init, `x_1.f` / `x_1.default.f` read the exports statically, and
|
||||
* `x_1.default` over a single-value module is that value. The helper
|
||||
* declarations themselves are dead once every call is such a binding (the
|
||||
* self-probe `this.__importDefault` reads the pristine exports object,
|
||||
* which never has the name), so they lower to nothing. */
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import {
|
||||
isCjsJsFile,
|
||||
isEsModuleStamp,
|
||||
npmStaticDepSf7,
|
||||
requireSpecOf,
|
||||
resolveImport,
|
||||
} from "../program.js";
|
||||
import { cjsNamespaceModule } from "./cjs-inline-require.js";
|
||||
|
||||
const HELPERS = new Set([
|
||||
"__importDefault",
|
||||
"__importStar",
|
||||
"__createBinding",
|
||||
"__setModuleDefault",
|
||||
]);
|
||||
|
||||
export interface TscInteropBinding {
|
||||
helper: "__importDefault" | "__importStar";
|
||||
spec: string;
|
||||
dep: ts.SourceFile;
|
||||
/** The dependency stamps `__esModule`: the binding IS its exports. */
|
||||
stamped: boolean;
|
||||
}
|
||||
|
||||
/** `var <helper> = (this && this.<helper>) || <function>` at a module's top
|
||||
* level: the tsc helper declaration shape. */
|
||||
function tscHelperDeclName(stmt: ts.Statement): string | null {
|
||||
if (!ts.isVariableStatement(stmt)) return null;
|
||||
const decls = stmt.declarationList.declarations;
|
||||
if (decls.length !== 1) return null;
|
||||
const d = decls[0]!;
|
||||
if (!ts.isIdentifier(d.name) || !HELPERS.has(d.name.text) || d.initializer === undefined)
|
||||
return null;
|
||||
let init: ts.Expression = d.initializer;
|
||||
while (ts.isParenthesizedExpression(init)) init = init.expression;
|
||||
if (!ts.isBinaryExpression(init) || init.operatorToken.kind !== ts.SyntaxKind.BarBarToken)
|
||||
return null;
|
||||
let probe: ts.Expression = init.left;
|
||||
while (ts.isParenthesizedExpression(probe)) probe = probe.expression;
|
||||
if (
|
||||
!ts.isBinaryExpression(probe) ||
|
||||
probe.operatorToken.kind !== ts.SyntaxKind.AmpersandAmpersandToken ||
|
||||
probe.left.kind !== ts.SyntaxKind.ThisKeyword ||
|
||||
!ts.isPropertyAccessExpression(probe.right) ||
|
||||
probe.right.expression.kind !== ts.SyntaxKind.ThisKeyword ||
|
||||
probe.right.name.text !== d.name.text
|
||||
)
|
||||
return null;
|
||||
// The fallback is the helper body: a function, or (newer tsc output) a
|
||||
// conditional or IIFE producing one. It never runs when the helper is dead.
|
||||
return d.name.text;
|
||||
}
|
||||
|
||||
/** True when the dependency stamps itself as an ES module translation. */
|
||||
function stampsEsModule(dep: ts.SourceFile): boolean {
|
||||
return dep.statements.some((s) => {
|
||||
if (!ts.isExpressionStatement(s)) return false;
|
||||
if (isEsModuleStamp(s.expression)) return true;
|
||||
const e = s.expression;
|
||||
return (
|
||||
ts.isBinaryExpression(e) &&
|
||||
e.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
||||
ts.isPropertyAccessExpression(e.left) &&
|
||||
ts.isIdentifier(e.left.expression) &&
|
||||
e.left.expression.text === "exports" &&
|
||||
e.left.name.text === "__esModule" &&
|
||||
e.right.kind === ts.SyntaxKind.TrueKeyword
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/** The interop binding a top-level `const x = __importDefault(require(s))` /
|
||||
* `__importStar(require(s))` declaration makes, or null. The helper must be
|
||||
* the module's own tsc helper declaration and the dependency a compiled
|
||||
* CommonJS program module. */
|
||||
export function tscInteropBindingOfDecl(
|
||||
lowerer: Lowerer,
|
||||
decl: ts.VariableDeclaration,
|
||||
): TscInteropBinding | null {
|
||||
if (!ts.isIdentifier(decl.name) || decl.initializer === undefined) return null;
|
||||
const stmt = decl.parent?.parent;
|
||||
if (stmt === undefined || !ts.isVariableStatement(stmt) || !ts.isSourceFile(stmt.parent))
|
||||
return null;
|
||||
const sf = stmt.parent;
|
||||
if (!isCjsJsFile(sf, lowerer.program)) return null;
|
||||
let init: ts.Expression = decl.initializer;
|
||||
while (ts.isParenthesizedExpression(init)) init = init.expression;
|
||||
if (!ts.isCallExpression(init) || init.arguments.length !== 1) return null;
|
||||
const callee = init.expression;
|
||||
if (!ts.isIdentifier(callee)) return null;
|
||||
if (callee.text !== "__importDefault" && callee.text !== "__importStar") return null;
|
||||
if (!sf.statements.some((s) => tscHelperDeclName(s) === callee.text)) return null;
|
||||
const spec = requireSpecOf(init.arguments[0]!);
|
||||
if (spec === null) return null;
|
||||
const dep =
|
||||
resolveImport(lowerer.program, sf, spec, "require") ??
|
||||
npmStaticDepSf7(lowerer.program, sf, spec, "require");
|
||||
if (!dep || dep.isDeclarationFile || !isCjsJsFile(dep, lowerer.program)) return null;
|
||||
return {
|
||||
helper: callee.text as TscInteropBinding["helper"],
|
||||
spec,
|
||||
dep,
|
||||
stamped: stampsEsModule(dep),
|
||||
};
|
||||
}
|
||||
|
||||
/** The interop binding an identifier reads, or null. */
|
||||
export function tscInteropBindingOf(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.Expression,
|
||||
): TscInteropBinding | null {
|
||||
if (!ts.isIdentifier(expr)) return null;
|
||||
const sym = lowerer.checker.getSymbolAtLocation(expr);
|
||||
const decl = sym ? lowerer.checker.declarationsOf(sym).find(ts.isVariableDeclaration) : undefined;
|
||||
return decl ? tscInteropBindingOfDecl(lowerer, decl) : null;
|
||||
}
|
||||
|
||||
/** The module whose exports `expr` denotes through an interop binding:
|
||||
* the binding itself when the dependency is stamped (or for __importStar,
|
||||
* whose copy carries every export), and `binding.default` when it is not.
|
||||
* Null for anything else. */
|
||||
export function tscInteropModuleOf(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.Expression,
|
||||
): TscInteropBinding | null {
|
||||
let e = expr;
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
const direct = tscInteropBindingOf(lowerer, e);
|
||||
if (direct !== null) {
|
||||
return direct.stamped || direct.helper === "__importStar" ? direct : null;
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(e) && !e.questionDotToken && e.name.text === "default") {
|
||||
const via = tscInteropBindingOf(lowerer, e.expression);
|
||||
if (via !== null && !via.stamped) return via;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** `binding.default` over an unstamped single-value dependency: that value
|
||||
* (the caller lowers the dependency's whole export). */
|
||||
export function tscInteropDefaultValueOf(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.PropertyAccessExpression,
|
||||
): TscInteropBinding | null {
|
||||
if (expr.questionDotToken || expr.name.text !== "default") return null;
|
||||
const via = tscInteropBindingOf(lowerer, expr.expression);
|
||||
if (via === null || via.stamped) return null;
|
||||
return cjsNamespaceModule(lowerer, via.dep) ? null : via;
|
||||
}
|
||||
|
||||
/** True for a tsc helper declaration every reference to which is a
|
||||
* recognized interop binding's callee, its own self-probe, or a reference
|
||||
* from another such dead helper: the statement lowers to nothing. */
|
||||
export function isDeadTscHelperDecl(lowerer: Lowerer, stmt: ts.Statement): boolean {
|
||||
const name = tscHelperDeclName(stmt);
|
||||
if (name === null) return false;
|
||||
const sf = stmt.getSourceFile();
|
||||
const helperSpans = new Map<string, ts.Statement>();
|
||||
for (const s of sf.statements) {
|
||||
const n = tscHelperDeclName(s);
|
||||
if (n !== null) helperSpans.set(n, s);
|
||||
}
|
||||
const dead = new Set<string>();
|
||||
for (let changed = true; changed;) {
|
||||
changed = false;
|
||||
for (const [helper, decl] of helperSpans) {
|
||||
if (dead.has(helper)) continue;
|
||||
let live = false;
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
if (!ts.isIdentifier(node) || node.text !== helper) return undefined;
|
||||
const parent = node.parent;
|
||||
if (parent !== undefined && ts.isVariableDeclaration(parent) && parent.name === node)
|
||||
return undefined;
|
||||
const at = node.getStart(sf);
|
||||
if (at >= decl.getStart(sf) && at < decl.getEnd()) return undefined;
|
||||
for (const [other, otherDecl] of helperSpans) {
|
||||
if (other !== helper && dead.has(other)) {
|
||||
if (at >= otherDecl.getStart(sf) && at < otherDecl.getEnd()) return undefined;
|
||||
}
|
||||
}
|
||||
const call = parent;
|
||||
const binding = call?.parent;
|
||||
if (
|
||||
call !== undefined &&
|
||||
ts.isCallExpression(call) &&
|
||||
call.expression === node &&
|
||||
binding !== undefined &&
|
||||
ts.isVariableDeclaration(binding) &&
|
||||
tscInteropBindingOfDecl(lowerer, binding) !== null
|
||||
)
|
||||
return undefined;
|
||||
live = true;
|
||||
return "stop";
|
||||
});
|
||||
if (!live) {
|
||||
dead.add(helper);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return dead.has(name);
|
||||
}
|
||||
@@ -1159,7 +1159,9 @@ export class ClassDynamicDispatch {
|
||||
// Runtime-owned roots keep their dedicated property contracts. A
|
||||
// family whose fields all exist from construction and are never
|
||||
// deleted keeps a fixed layout, as TypeScript classes do.
|
||||
this.deletedNames ??= deletedPropertyNames(lowerer.moduleOrder);
|
||||
// null is a result too; undefined means not computed yet.
|
||||
if (this.deletedNames === undefined)
|
||||
this.deletedNames = deletedPropertyNames(lowerer.moduleOrder);
|
||||
if (!root.base?.def.runtime && !fixedFieldFamily(root, this.deletedNames)) track(root);
|
||||
}
|
||||
const loc = this.propertyBag.loc;
|
||||
|
||||
@@ -124,15 +124,6 @@ export function constructionEscape(inputs: ConstructionInputs): ConstructionEsca
|
||||
continue;
|
||||
}
|
||||
if (ts.isExpressionStatement(stmt) && !exposes(stmt.expression, depth)) continue;
|
||||
// A check that only reads assigned fields and throws or does nothing
|
||||
// with the receiver (`if (this.size < 0) throw new RangeError()`)
|
||||
// does not expose the instance; assignments in its branches are not
|
||||
// definite and still count as exposures.
|
||||
if (
|
||||
(ts.isIfStatement(stmt) || ts.isThrowStatement(stmt) || ts.isBlock(stmt)) &&
|
||||
!exposes(stmt, depth)
|
||||
)
|
||||
continue;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -1511,11 +1511,23 @@ function lowerBufferStaticValue(
|
||||
} else {
|
||||
list = lowerer.lowerExpr(argNode);
|
||||
if (!typeEquals(list.type, arrayOf(BYTES_U8))) {
|
||||
lowerer.noLowering(
|
||||
`Buffer.concat of '${lowerer.fmt(list.type)}' values`,
|
||||
argNode,
|
||||
"one Uint8Array[]/Buffer[] value (or literal) is the lowered list shape",
|
||||
);
|
||||
// Any other list (JavaScript's untyped `const chunks = []`, or a
|
||||
// union of arrays): the runtime checks it as Node does (an Array of
|
||||
// Uint8Arrays, the ERR_INVALID_ARG_TYPE errors otherwise) before
|
||||
// concatenating.
|
||||
const value: IrExpr =
|
||||
list.type.kind === "dyn" ? list : { kind: "dynFrom", value: list, type: DYN, loc };
|
||||
if (args.length === 2) {
|
||||
const total = lowerer.lowerExprExpecting(args[1]!, F64);
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "buffer.concatLenDyn",
|
||||
args: [value, total],
|
||||
type: BYTES_U8,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return { kind: "libCall", fn: "buffer.concatDyn", args: [value], type: BYTES_U8, loc };
|
||||
}
|
||||
}
|
||||
if (args.length === 2) {
|
||||
|
||||
@@ -747,7 +747,7 @@ function lowerForOfMapOrSet(
|
||||
* Recurses through every nested statement list; nested functions are
|
||||
* lifted elsewhere and never appear here, so every return found exits
|
||||
* THIS function through the active iteration. */
|
||||
function exitBeforeReturns(stmts: IrStmt[], make: () => IrStmt): IrStmt[] {
|
||||
export function exitBeforeReturns(stmts: IrStmt[], make: () => IrStmt): IrStmt[] {
|
||||
const walk = (list: IrStmt[]): IrStmt[] =>
|
||||
list.flatMap((s): IrStmt[] => {
|
||||
switch (s.kind) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -315,7 +315,13 @@ export function refreshErrorPropertyDispatch(
|
||||
info.def.name,
|
||||
member,
|
||||
value,
|
||||
[varRef("value", STRING, loc)],
|
||||
[
|
||||
// A JavaScript setter takes its value untyped.
|
||||
lowerer.coerceToExpected(
|
||||
varRef("value", STRING, loc),
|
||||
accessor.sig.params[0]?.type ?? STRING,
|
||||
),
|
||||
],
|
||||
VOID,
|
||||
loc,
|
||||
),
|
||||
|
||||
@@ -26,7 +26,8 @@ import type { Lowerer } from "../lowerer.js";
|
||||
* deeper — `x?.trim().toLowerCase()` reads nothing past the guard when x
|
||||
* is nullish, so the whole tail must lower inside it. Walks
|
||||
* property/element accesses and call steps only (parens and `!` break
|
||||
* the chain per the grammar: `(a?.b).c` throws on undefined in JS);
|
||||
* the chain per the grammar: `(a?.b).c` throws on undefined in JS, while
|
||||
* an unparenthesized `a?.b!.c` stays one chain, as TypeScript emits it);
|
||||
* the node's OWN token — and a call's immediate callee token — are the
|
||||
* plain entries' cases, not tails, and handled markers make the chain
|
||||
* lowering's re-dispatch walk past its own step. `f?.()` steps stay out
|
||||
@@ -41,6 +42,10 @@ function chainTailDot(
|
||||
// An active chain's bound receiver read is a LEAF — its own token
|
||||
// was consumed by the chain that bound it.
|
||||
if (lowerer.chainRecvByNode.has(cur)) return null;
|
||||
if (ts.isNonNullExpression(cur)) {
|
||||
cur = cur.expression;
|
||||
continue;
|
||||
}
|
||||
if (ts.isCallExpression(cur)) {
|
||||
// `f?.()` steps route through their own entry (or are being
|
||||
// handled); a deeper token under one is that chain's business.
|
||||
@@ -91,20 +96,29 @@ export function isOptionalChainTail(lowerer: Lowerer, expr: ts.Expression): bool
|
||||
if (!tail) return false;
|
||||
const recvT = lowerer.mapTypeOf(lowerer.typeOf(tail.expression));
|
||||
if (recvT?.kind === "dyn") return true;
|
||||
if (recvT?.kind !== "union") return false;
|
||||
const def = lowerer.unions.get(recvT.unionId);
|
||||
return !!def && def.arms.some(isUnitType);
|
||||
if (recvT?.kind === "union") {
|
||||
const def = lowerer.unions.get(recvT.unionId);
|
||||
if (def?.arms.some(isUnitType)) return true;
|
||||
}
|
||||
// The checker calls the receiver present, but an unchecked read (`a[0]`
|
||||
// past the end, a missing index-signature key) can still lower to an
|
||||
// undefined-armed value, and `a[0]?.b.c` must then short-circuit `.c`
|
||||
// too. The chain lowering inspects the lowered receiver: a present value
|
||||
// re-dispatches the plain lowering, so only genuinely optional receivers
|
||||
// change shape.
|
||||
return lowerer.isRuntimeOptionalOrigin(tail.expression);
|
||||
}
|
||||
|
||||
/** True when an EARLIER step of this access chain carries `?.` — JS
|
||||
* short-circuits the whole tail (`a?.b.c` reads nothing when a is
|
||||
* nullish), so a dyn tail read must answer undefined instead of
|
||||
* throwing. Walks the receiver spine only; the argument of the access
|
||||
* itself is not part of the guard. */
|
||||
* itself is not part of the guard. Parentheses end the chain
|
||||
* (`(a?.b).c` reads `.c` from undefined); `!` does not. */
|
||||
export function hasOptionalChainGuard(expr: ts.Expression): boolean {
|
||||
let cur: ts.Expression = expr;
|
||||
for (;;) {
|
||||
if (ts.isParenthesizedExpression(cur) || ts.isNonNullExpression(cur)) {
|
||||
if (ts.isNonNullExpression(cur)) {
|
||||
cur = cur.expression;
|
||||
continue;
|
||||
}
|
||||
@@ -393,7 +407,8 @@ export function lowerOptionalChain(
|
||||
for (let cur: ts.Expression = expr; cur !== dotNode;) {
|
||||
const next: ts.Expression = ts.isCallExpression(cur)
|
||||
? cur.expression
|
||||
: (cur as ts.PropertyAccessExpression | ts.ElementAccessExpression).expression;
|
||||
: (cur as ts.PropertyAccessExpression | ts.ElementAccessExpression | ts.NonNullExpression)
|
||||
.expression;
|
||||
if (next === dotNode) break;
|
||||
if (!lowerer.chainNarrowedType.has(next)) {
|
||||
lowerer.chainNarrowedType.set(
|
||||
|
||||
@@ -436,11 +436,14 @@ export function resolveInferredReturn(
|
||||
if (ice.length > 0) lowerer.diags.push(...ice);
|
||||
lowerer.runtimeFences.push(...captured.filter((d) => d.code !== "SC9001"));
|
||||
const first = captured.find((d) => d.code !== "SC9001");
|
||||
// Property writes, not literal element writes: the class method
|
||||
// census reads `x["kind"] = …` as a possible write to every method
|
||||
// named `kind`, which would make those methods dynamic in native builds.
|
||||
const mutable = e.stmt as unknown as Record<string, unknown>;
|
||||
delete mutable["value"];
|
||||
mutable["kind"] = "runtimeFence";
|
||||
mutable["code"] = first?.code ?? "SC1090";
|
||||
mutable["message"] = first
|
||||
delete mutable.value;
|
||||
mutable.kind = "runtimeFence";
|
||||
mutable.code = first?.code ?? "SC1090";
|
||||
mutable.message = first
|
||||
? `${first.message} [${first.code}]`
|
||||
: "this return value has no lowering onto the instance's settled return type [SC1090]";
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import { isJsSourceFile, locOf } from "../program.js";
|
||||
import { typeKey } from "../type-mapper.js";
|
||||
import { PoisonError, dynFallbackType, neverTaintedJsType, newFnCtx } from "./lowerer.js";
|
||||
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
|
||||
import { findGenericMethodOn, type ClassInfo } from "./lower-classes.js";
|
||||
import { findGenericMethodOn, storedClassValueType, type ClassInfo } from "./lower-classes.js";
|
||||
import { nsPathPrefix } from "./lower-namespaces.js";
|
||||
import { declSymbolOf } from "./lower-modules.js";
|
||||
import { npmStaticPackageOfPath } from "../npm-static.js";
|
||||
@@ -292,6 +292,8 @@ export function genericCallInstance(
|
||||
(_param, i) => expr.arguments[i] ?? expr,
|
||||
);
|
||||
const retTs = lowerer.checker.getReturnTypeOfSignature(rsig);
|
||||
const local = localClassResultInstance(lowerer, expr, info, rsig, params, retTs);
|
||||
if (local) return local;
|
||||
const mappedReturn = lowerer.mapTypeOf(retTs);
|
||||
if (!mappedReturn) {
|
||||
fenceGenericSignatureResult(lowerer, expr, retTs);
|
||||
@@ -339,6 +341,150 @@ export function genericCallInstance(
|
||||
);
|
||||
}
|
||||
|
||||
/** A generic factory whose result is one of its own local classes
|
||||
* (`function base<T>(v: T) { return class { value: T = v }; }`). Each
|
||||
* instance has its own class, so the instance is keyed on its bindings and
|
||||
* the result maps inside it, where the local class takes the instance's
|
||||
* name and bindings. */
|
||||
function localClassResultInstance(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.CallExpression,
|
||||
info: GenericFnInfo,
|
||||
rsig: ts.Signature,
|
||||
params: ParamShape[],
|
||||
retTs: ts.Type,
|
||||
): GenericInstance | null {
|
||||
if (!localClassOfResult(lowerer, info, retTs)) return null;
|
||||
const tsBindings = new Map<ts.Symbol, ts.Type>();
|
||||
const bindings = lowerer.inferTypeParamBindings(expr, info, rsig, tsBindings);
|
||||
const bindingKey = info.typeParams
|
||||
.map((tp) => {
|
||||
const bound = bindings.get(tp);
|
||||
return bound ? typeKey(bound) : "?";
|
||||
})
|
||||
.join(",");
|
||||
const pending: IrType = { kind: "void" };
|
||||
const inst = internGenericInstance(lowerer, expr, info, params, pending, () => bindings, {
|
||||
extraKey: `@class<${bindingKey}>`,
|
||||
tsBindings,
|
||||
});
|
||||
if (inst.returnType !== pending) return inst;
|
||||
const returnType = withInstanceScope(lowerer, info, inst, () => lowerer.mapTypeOf(retTs));
|
||||
if (!returnType) {
|
||||
fenceGenericSignatureResult(lowerer, expr, retTs);
|
||||
lowerer.badType(expr, retTs);
|
||||
}
|
||||
inst.returnType = returnType;
|
||||
// Values of the class outside the factory (`const b = new B()`, `b.read()`)
|
||||
// carry the instantiated checker type; record which class it names. A
|
||||
// call inside another generic instance can have a type that depends on
|
||||
// that instance's parameters, which no single class answers.
|
||||
if (lowerer.typeParamBindings !== null) return inst;
|
||||
const instance =
|
||||
returnType.kind === "classval"
|
||||
? lowerer.checker
|
||||
.getConstructSignatures(retTs)
|
||||
.map((sig) => lowerer.checker.getReturnTypeOfSignature(sig))
|
||||
: [retTs];
|
||||
const className =
|
||||
returnType.kind === "classval" || returnType.kind === "object" ? returnType.className : null;
|
||||
if (className !== null)
|
||||
for (const type of instance) lowerer.localClassInstanceTypes.set(type.id, className);
|
||||
return inst;
|
||||
}
|
||||
|
||||
/** The class value a call of a generic factory returns when the result is
|
||||
* one of the factory's local classes (`class C extends base(42) {}`), or
|
||||
* null for any other call. */
|
||||
export function genericLocalClassResult(lowerer: Lowerer, expr: ts.CallExpression): IrType | null {
|
||||
if (!ts.isIdentifier(expr.expression)) return null;
|
||||
const info = lowerer.genericFnOf(expr.expression);
|
||||
if (!info || info.implicitParams) return null;
|
||||
const rsig = lowerer.checker.getResolvedSignature(expr);
|
||||
if (!rsig || !localClassOfResult(lowerer, info, lowerer.checker.getReturnTypeOfSignature(rsig)))
|
||||
return null;
|
||||
return genericCallInstance(lowerer, expr, info).returnType;
|
||||
}
|
||||
|
||||
/** The type of a `const` binding whose initializer calls a generic factory
|
||||
* returning one of its local classes (`const B = base(7)`), or constructs
|
||||
* such a class (`const b = new B()`); null for any other binding. */
|
||||
export function genericLocalClassBinding(lowerer: Lowerer, node: ts.Node): IrType | null {
|
||||
const decl = node.parent;
|
||||
if (
|
||||
!ts.isIdentifier(node) ||
|
||||
!decl ||
|
||||
!ts.isVariableDeclaration(decl) ||
|
||||
decl.name !== node ||
|
||||
!decl.initializer ||
|
||||
(ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) === 0
|
||||
)
|
||||
return null;
|
||||
// An `any` binding keeps the checked-dynamic representation.
|
||||
if ((lowerer.typeOf(node).flags & ts.TypeFlags.Any) !== 0) return null;
|
||||
let init = decl.initializer;
|
||||
while (ts.isParenthesizedExpression(init)) init = init.expression;
|
||||
if (ts.isCallExpression(init)) return genericLocalClassResult(lowerer, init);
|
||||
if (!ts.isNewExpression(init)) return null;
|
||||
const constructed = storedClassValueType(lowerer, init.expression);
|
||||
if (constructed?.kind !== "classval") return null;
|
||||
const classDecl = lowerer.classes.get(constructed.className)?.decl;
|
||||
return classDecl && insideGenericFunction(classDecl)
|
||||
? { kind: "object", className: constructed.className }
|
||||
: null;
|
||||
}
|
||||
|
||||
/** Whether `node` is declared inside a function with type parameters. */
|
||||
function insideGenericFunction(node: ts.Node): boolean {
|
||||
for (let parent = node.parent; parent && !ts.isSourceFile(parent); parent = parent.parent)
|
||||
if (ts.isFunctionLike(parent) && (parent.typeParameters?.length ?? 0) > 0) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The class declared inside a generic function that `type` is the
|
||||
* constructor or instance type of, or null. */
|
||||
function localClassOfResult(
|
||||
lowerer: Lowerer,
|
||||
info: GenericFnInfo,
|
||||
type: ts.Type,
|
||||
): ts.ClassLikeDeclaration | null {
|
||||
const symbol = type.getSymbol();
|
||||
if (!symbol || !(symbol.flags & ts.SymbolFlags.Class)) return null;
|
||||
const decl = lowerer.checker.valueDeclarationOf(symbol);
|
||||
if (!decl || !(ts.isClassExpression(decl) || ts.isClassDeclaration(decl))) return null;
|
||||
for (let parent = decl.parent; parent && !ts.isSourceFile(parent); parent = parent.parent)
|
||||
if (parent === info.decl) return decl;
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Runs `fn` under an instance's type bindings and local-class names, as
|
||||
* its body lowers. */
|
||||
function withInstanceScope<T>(
|
||||
lowerer: Lowerer,
|
||||
info: GenericFnInfo,
|
||||
inst: GenericInstance,
|
||||
fn: () => T,
|
||||
): T {
|
||||
const prevBindings = lowerer.typeParamBindings;
|
||||
const prevTsBindings = lowerer.typeParamTsBindings;
|
||||
const previousPath = lowerer.genericInstantiationPath;
|
||||
const prevContext = lowerer.instantiationContext;
|
||||
lowerer.typeParamBindings = inst.bindings;
|
||||
lowerer.typeParamTsBindings = inst.tsBindings ?? null;
|
||||
lowerer.genericInstantiationPath = inst.path;
|
||||
lowerer.instantiationContext = `instantiating '${info.baseName}' with ${inst.typeArgsText}`;
|
||||
lowerer.localClassInstantiations.push({ owner: info.decl, name: inst.name });
|
||||
try {
|
||||
return fn();
|
||||
} finally {
|
||||
lowerer.localClassInstantiations.pop();
|
||||
lowerer.typeParamBindings = prevBindings;
|
||||
lowerer.typeParamTsBindings = prevTsBindings;
|
||||
lowerer.genericInstantiationPath = previousPath;
|
||||
lowerer.instantiationContext = prevContext;
|
||||
}
|
||||
}
|
||||
|
||||
/** The one instance table both instantiation routes share: key identity IS
|
||||
* signature identity, so a call (`identity(1)`) and a pinned VALUE
|
||||
* (`const f: (x: number) => number = identity`) reuse one compiled
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { afterAll, expect, test } from "vitest";
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { closeSourceParser, parseSourceFile } from "../ts7/source-parser-node.js";
|
||||
import { isValueReturningConstructorClass } from "./js-dynamic-objects.js";
|
||||
|
||||
afterAll(closeSourceParser);
|
||||
|
||||
function classify(source: string): boolean {
|
||||
const file = parseSourceFile("classes.js", source, "js");
|
||||
let decl: ts.ClassDeclaration | undefined;
|
||||
ts.walkPreorder(file, (node) => {
|
||||
if (!decl && ts.isClassDeclaration(node)) decl = node;
|
||||
});
|
||||
if (!decl) throw new Error("fixture has no class");
|
||||
return isValueReturningConstructorClass(decl);
|
||||
}
|
||||
|
||||
test("a constructor-only class returning a value is claimed", () => {
|
||||
expect(classify(`class Chalk { constructor(options) { return chalkFactory(options); } }`)).toBe(
|
||||
true,
|
||||
);
|
||||
expect(classify(`class C { constructor(a) { if (a) return { a }; return [a]; } ; }`)).toBe(true);
|
||||
expect(classify(`class C { constructor() { const f = () => 1; return { f }; } }`)).toBe(true);
|
||||
});
|
||||
|
||||
test("classes that keep their instance layout are left alone", () => {
|
||||
// No value return, or only nested functions return.
|
||||
expect(classify(`class C { constructor() { return; } }`)).toBe(false);
|
||||
expect(classify(`class C { constructor() { function f() { return 1; } f(); } }`)).toBe(false);
|
||||
expect(classify(`class C { constructor() { const f = () => { return 1; }; f(); } }`)).toBe(false);
|
||||
// Other members, heritage, or a constructor that observes `this`.
|
||||
expect(classify(`class C { constructor() { return {}; } m() {} }`)).toBe(false);
|
||||
expect(classify(`class C { x = 1; constructor() { return {}; } }`)).toBe(false);
|
||||
expect(classify(`class C extends B { constructor() { super(); return {}; } }`)).toBe(false);
|
||||
expect(classify(`class C { constructor() { this.a = 1; return {}; } }`)).toBe(false);
|
||||
expect(classify(`class C { constructor() { const g = () => this; return { g }; } }`)).toBe(false);
|
||||
expect(classify(`class C {}`)).toBe(false);
|
||||
});
|
||||
@@ -0,0 +1,312 @@
|
||||
/* JavaScript declarations whose values only the checked-dynamic
|
||||
* representation can hold faithfully. TypeScript infers a fixed object
|
||||
* type for each of them, but the program changes the value's shape or
|
||||
* identity at run time, so a static layout would answer stale members
|
||||
* (or throw on a value that no longer fits):
|
||||
*
|
||||
* - a non-empty object literal bound to a variable that the same file
|
||||
* grows or reshapes dynamically: a keyed write with a runtime key
|
||||
* (`o[k] = v`), a `delete`, or `Object.defineProperty(o, ...)` /
|
||||
* `Object.defineProperties(o, ...)` (ansi-styles' `assembleStyles`).
|
||||
* The object literals nested in its property values belong to the same
|
||||
* value graph and are marked with it;
|
||||
* - a class whose only member is a constructor that returns a value and
|
||||
* never reads `this` (chalk's `class Chalk { constructor(options) {
|
||||
* return chalkFactory(options); } }`): `new C()` answers whatever the
|
||||
* constructor returns, so its instance type has no fixed layout.
|
||||
*
|
||||
* The type mapper answers the checked-dynamic type for the instance and
|
||||
* object-literal types these declarations own (TypeMapperCtx
|
||||
* .jsDynamicDeclaration). The scan is syntactic, runs over JavaScript
|
||||
* files only, and an empty answer leaves every mapping unchanged, so
|
||||
* TypeScript programs and JavaScript that never uses these patterns
|
||||
* compile exactly as before. */
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { isJsSourceFile, locOf } from "../program.js";
|
||||
import { BOOL, DYN, type IrExpr, type IrStmt } from "../../ir/ir.js";
|
||||
import { dynUndefinedExpr, nodeThrowExpr } from "../../ir/build.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import type { ParamShape } from "./call-signatures.js";
|
||||
import { newFnCtx } from "./function-context.js";
|
||||
|
||||
const SCANNED_KINDS: ReadonlySet<ts.SyntaxKind> = new Set([
|
||||
ts.SyntaxKind.VariableDeclaration,
|
||||
ts.SyntaxKind.ClassDeclaration,
|
||||
ts.SyntaxKind.BinaryExpression,
|
||||
ts.SyntaxKind.DeleteExpression,
|
||||
ts.SyntaxKind.CallExpression,
|
||||
]);
|
||||
|
||||
function unparenthesized(node: ts.Expression): ts.Expression {
|
||||
while (ts.isParenthesizedExpression(node)) node = node.expression;
|
||||
return node;
|
||||
}
|
||||
|
||||
function isLiteralKey(node: ts.Expression): boolean {
|
||||
const key = unparenthesized(node);
|
||||
return (
|
||||
ts.isStringLiteral(key) || ts.isNumericLiteral(key) || ts.isNoSubstitutionTemplateLiteral(key)
|
||||
);
|
||||
}
|
||||
|
||||
function isAssignmentOperator(kind: ts.SyntaxKind): boolean {
|
||||
return kind >= ts.SyntaxKind.FirstAssignment && kind <= ts.SyntaxKind.LastAssignment;
|
||||
}
|
||||
|
||||
/** `class C { constructor(...) { ...; return value; } }` with nothing else:
|
||||
* no heritage, decorators or other members, a constructor body that
|
||||
* returns a value at its own level and never mentions `this`/`super`. */
|
||||
export function isValueReturningConstructorClass(decl: ts.ClassDeclaration): boolean {
|
||||
if (decl.heritageClauses !== undefined && decl.heritageClauses.length > 0) return false;
|
||||
if (decl.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.Decorator)) return false;
|
||||
let ctor: ts.ConstructorDeclaration | undefined;
|
||||
for (const member of decl.members) {
|
||||
if (ts.isSemicolonClassElement(member)) continue;
|
||||
if (!ts.isConstructorDeclaration(member) || ctor !== undefined) return false;
|
||||
ctor = member;
|
||||
}
|
||||
if (ctor?.body === undefined) return false;
|
||||
let returnsValue = false;
|
||||
let observesThis = false;
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (observesThis) return;
|
||||
if (node.kind === ts.SyntaxKind.ThisKeyword || node.kind === ts.SyntaxKind.SuperKeyword) {
|
||||
observesThis = true;
|
||||
return;
|
||||
}
|
||||
// Arrow functions share the constructor's `this`; other functions
|
||||
// have their own `this` and their own returns.
|
||||
if (ts.isFunctionLike(node) && !ts.isArrowFunction(node)) return;
|
||||
if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) return;
|
||||
if (ts.isReturnStatement(node) && node.expression !== undefined && !insideArrow(node, ctor!))
|
||||
returnsValue = true;
|
||||
node.forEachChild(visit);
|
||||
};
|
||||
ctor.body.forEachChild(visit);
|
||||
for (const parameter of ctor.parameters) parameter.forEachChild(visit);
|
||||
return returnsValue && !observesThis;
|
||||
}
|
||||
|
||||
function insideArrow(node: ts.Node, ctor: ts.ConstructorDeclaration): boolean {
|
||||
for (let p = node.parent; p !== undefined && p !== ctor; p = p.parent) {
|
||||
if (ts.isArrowFunction(p)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function markNested(literal: ts.ObjectLiteralExpression, out: Set<ts.Node>): void {
|
||||
if (out.has(literal)) return;
|
||||
out.add(literal);
|
||||
for (const property of literal.properties) {
|
||||
if (!ts.isPropertyAssignment(property)) continue;
|
||||
const value = unparenthesized(property.initializer);
|
||||
if (ts.isObjectLiteralExpression(value)) markNested(value, out);
|
||||
}
|
||||
}
|
||||
|
||||
export function jsDynamicDeclarations(
|
||||
files: readonly ts.SourceFile[],
|
||||
checker: ts.TypeChecker,
|
||||
): Set<ts.Node> {
|
||||
const out = new Set<ts.Node>();
|
||||
for (const file of files) {
|
||||
if (!isJsSourceFile(file) || file.isDeclarationFile) continue;
|
||||
const literals = new Map<ts.Symbol, ts.ObjectLiteralExpression>();
|
||||
const reshaped = new Set<ts.Symbol>();
|
||||
const receiver = (expression: ts.Expression | undefined): void => {
|
||||
if (expression === undefined) return;
|
||||
const target = unparenthesized(expression);
|
||||
if (!ts.isIdentifier(target)) return;
|
||||
const symbol = checker.getSymbolAtLocation(target);
|
||||
if (symbol !== undefined) reshaped.add(symbol);
|
||||
};
|
||||
ts.walkPreorder(
|
||||
file,
|
||||
(node) => {
|
||||
if (ts.isVariableDeclaration(node)) {
|
||||
if (!ts.isIdentifier(node.name) || node.initializer === undefined) return;
|
||||
const init = unparenthesized(node.initializer);
|
||||
// An empty literal's binding is already checked-dynamic
|
||||
// (jsOpenObjectType), and its type describes no layout.
|
||||
if (!ts.isObjectLiteralExpression(init) || init.properties.length === 0) return;
|
||||
const symbol = checker.getSymbolAtLocation(node.name);
|
||||
if (symbol !== undefined) literals.set(symbol, init);
|
||||
return;
|
||||
}
|
||||
if (ts.isClassDeclaration(node)) {
|
||||
if (isValueReturningConstructorClass(node)) out.add(node);
|
||||
return;
|
||||
}
|
||||
if (ts.isBinaryExpression(node)) {
|
||||
if (!isAssignmentOperator(node.operatorToken.kind)) return;
|
||||
const left = unparenthesized(node.left);
|
||||
if (ts.isElementAccessExpression(left) && !isLiteralKey(left.argumentExpression))
|
||||
receiver(left.expression);
|
||||
return;
|
||||
}
|
||||
if (ts.isDeleteExpression(node)) {
|
||||
const operand = unparenthesized(node.expression);
|
||||
if (ts.isElementAccessExpression(operand) || ts.isPropertyAccessExpression(operand))
|
||||
receiver(operand.expression);
|
||||
return;
|
||||
}
|
||||
if (ts.isCallExpression(node)) {
|
||||
const callee = unparenthesized(node.expression);
|
||||
if (
|
||||
ts.isPropertyAccessExpression(callee) &&
|
||||
ts.isIdentifier(callee.expression) &&
|
||||
callee.expression.text === "Object" &&
|
||||
(callee.name.text === "defineProperty" || callee.name.text === "defineProperties")
|
||||
)
|
||||
receiver(node.arguments[0]);
|
||||
}
|
||||
},
|
||||
SCANNED_KINDS,
|
||||
);
|
||||
for (const symbol of reshaped) {
|
||||
const literal = literals.get(symbol);
|
||||
if (literal !== undefined) markNested(literal, out);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** `new C(args)` for a value-returning constructor class (above): the
|
||||
* constructor body runs as an ordinary JavaScript function whose result
|
||||
* is the construction's value. A returned object or function is Node's
|
||||
* answer; a fall-through or primitive return would answer the bare
|
||||
* instance, which has no static home here, and throws the deferred
|
||||
* refusal instead. Null when the callee is not such a class. */
|
||||
export function lowerValueReturningConstructorNew(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.NewExpression,
|
||||
): IrExpr | null {
|
||||
const isDynamic = lowerer.typeCtx.jsDynamicDeclaration;
|
||||
if (isDynamic === undefined) return null;
|
||||
const callee = unparenthesized(expr.expression);
|
||||
if (!ts.isIdentifier(callee)) return null;
|
||||
let symbol = lowerer.resolveValueSymbol(callee);
|
||||
if (symbol !== null && (symbol.flags & ts.SymbolFlags.Alias) !== 0)
|
||||
symbol = lowerer.checker.getAliasedSymbol(symbol);
|
||||
const decl = symbol
|
||||
? lowerer.checker
|
||||
.declarationsOf(symbol)
|
||||
.find((node): node is ts.ClassDeclaration => ts.isClassDeclaration(node) && isDynamic(node))
|
||||
: undefined;
|
||||
if (decl === undefined) return null;
|
||||
const loc = locOf(expr);
|
||||
const ctor = decl.members.find(ts.isConstructorDeclaration)!;
|
||||
const simple = ctor.parameters.every(
|
||||
(p) => ts.isIdentifier(p.name) && p.dotDotDotToken === undefined && p.initializer === undefined,
|
||||
);
|
||||
if (!simple || !ts.isSourceFile(decl.parent)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
"constructing a class whose constructor returns a value, with parameter defaults, rest or patterns, or declared inside a function",
|
||||
);
|
||||
}
|
||||
const args = expr.arguments ?? [];
|
||||
if (args.some(ts.isSpreadElement)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
"spread arguments when constructing a class whose constructor returns a value",
|
||||
);
|
||||
}
|
||||
const key = `ctorValue:${decl.getSourceFile().fileName}:${decl.getStart()}`;
|
||||
let name = lowerer.valueHelpers.get(key);
|
||||
if (name === undefined) {
|
||||
name = `%ctorValue.${lowerer.valueHelpers.size}`;
|
||||
lowerer.valueHelpers.set(key, name);
|
||||
const savedClass = lowerer.currentClass;
|
||||
lowerer.currentClass = null;
|
||||
lowerer.fnStack.push(newFnCtx(false, null, null, DYN));
|
||||
try {
|
||||
const shapes: ParamShape[] = ctor.parameters.map(() => ({ type: DYN, mode: "required" }));
|
||||
const declared = lowerer.declareParams(ctor.parameters, shapes);
|
||||
const body: IrStmt[] = [
|
||||
...declared.prologue,
|
||||
...lowerer.lowerStmts(ctor.body!.statements),
|
||||
{
|
||||
kind: "runtimeFence",
|
||||
code: "SC1090",
|
||||
message: `constructors that return no object (the bare instance of '${decl.name?.text ?? "class"}' has no representation here) are not supported yet [SC1090 at ${decl.getSourceFile().fileName}:${ts.getLineAndCharacterOfPosition(decl.getSourceFile(), ctor.getStart()).line + 1}]`,
|
||||
loc: locOf(ctor),
|
||||
},
|
||||
];
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: declared.params,
|
||||
returnType: DYN,
|
||||
locals: lowerer.ctx.locals,
|
||||
body,
|
||||
loc: locOf(ctor),
|
||||
});
|
||||
} finally {
|
||||
lowerer.fnStack.pop();
|
||||
lowerer.currentClass = savedClass;
|
||||
}
|
||||
}
|
||||
const lowered = ctor.parameters.map((_, i) =>
|
||||
args[i] !== undefined ? lowerer.lowerExprExpecting(args[i]!, DYN) : dynUndefinedExpr(loc),
|
||||
);
|
||||
// Surplus arguments still evaluate, left to right, after the others.
|
||||
const surplus: IrStmt[] = args
|
||||
.slice(ctor.parameters.length)
|
||||
.map((arg) => ({ kind: "exprStmt", expr: lowerer.lowerExpr(arg), loc: locOf(arg) }));
|
||||
const result = lowerer.declareHiddenLocal("%constructed", DYN);
|
||||
const value: IrExpr = { kind: "varRef", localId: result.id, type: DYN, loc };
|
||||
const sf = expr.getSourceFile();
|
||||
const line = ts.getLineAndCharacterOfPosition(sf, expr.getStart()).line + 1;
|
||||
const objectLike: IrExpr = {
|
||||
kind: "logical",
|
||||
op: "||",
|
||||
left: { kind: "dynTest", test: "function", value, type: BOOL, loc },
|
||||
right: {
|
||||
kind: "logical",
|
||||
op: "&&",
|
||||
left: { kind: "dynTest", test: "object", value, type: BOOL, loc },
|
||||
right: { kind: "dynTest", test: "null", negated: true, value, type: BOOL, loc },
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
const args0: IrStmt[] = [];
|
||||
const argLocals = lowered.map((arg) => {
|
||||
const local = lowerer.declareHiddenLocal("%constructArg", DYN);
|
||||
args0.push({ kind: "varDecl", localId: local.id, init: arg, loc });
|
||||
return { kind: "varRef", localId: local.id, type: DYN, loc } as IrExpr;
|
||||
});
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
...args0,
|
||||
...surplus,
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: result.id,
|
||||
init: { kind: "call", callee: name, args: argLocals, type: DYN, loc },
|
||||
loc,
|
||||
},
|
||||
],
|
||||
result: {
|
||||
kind: "ternary",
|
||||
cond: objectLike,
|
||||
then: value,
|
||||
else_: nodeThrowExpr(
|
||||
0,
|
||||
"SC1090",
|
||||
`constructors that return no object are not supported yet [SC1090 at ${sf.fileName}:${line}]`,
|
||||
DYN,
|
||||
loc,
|
||||
),
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
builtinModulesArrayLit,
|
||||
stdlibGlobalNameOf,
|
||||
} from "./surfaces.js";
|
||||
import { CRYPTO_CONSTANTS } from "./crypto-tables.js";
|
||||
|
||||
const PROTOTYPE_METHODS: Record<string, readonly string[]> = {
|
||||
Date: ["getTime", "valueOf", "toISOString", "toJSON"],
|
||||
@@ -680,3 +681,34 @@ export function lowerBuiltinLoaderValue(lowerer: Lowerer, loc: SrcLoc): IrExpr {
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** The deprecated node:constants module object: the runtime merges
|
||||
* os.constants' groups and fs.constants with crypto.constants, which a
|
||||
* lifted function answers from the baked table the first time the object
|
||||
* is built. */
|
||||
export function builtinConstantsObject(lowerer: Lowerer, loc: SrcLoc): IrExpr {
|
||||
const name = "%builtin.constants.crypto";
|
||||
if (!lowerer.builtinCallableValueFns.has(name)) {
|
||||
lowerer.builtinCallableValueFns.set(name, name);
|
||||
const fields = Object.entries(CRYPTO_CONSTANTS).map(([key, value]) => ({
|
||||
key: strLit(key, loc),
|
||||
value: box(builtinConstLit(value, loc)),
|
||||
}));
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: [],
|
||||
returnType: DYN,
|
||||
locals: [],
|
||||
body: [{ kind: "return", value: { kind: "dynObjLit", fields, type: DYN, loc }, loc }],
|
||||
loc,
|
||||
});
|
||||
}
|
||||
const table: IrExpr = {
|
||||
kind: "closure",
|
||||
fnName: name,
|
||||
captures: [],
|
||||
type: { kind: "func", params: [], ret: DYN },
|
||||
loc,
|
||||
};
|
||||
return { kind: "libCall", fn: "constants.object", args: [box(table)], type: DYN, loc };
|
||||
}
|
||||
|
||||
@@ -24,6 +24,13 @@ import {
|
||||
lowerZlibStreamMethodCall,
|
||||
} from "./builtins/compression.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import {
|
||||
inlineNamespaceRequireOf,
|
||||
lowerInlineRequireValue,
|
||||
lowerUncompiledRequireCall,
|
||||
lowerWithInlineRequireInit,
|
||||
} from "./cjs-inline-require.js";
|
||||
import { cjsExportCallIgnoresThis, cjsInFileExportGlobalOf } from "./cjs-exports-object.js";
|
||||
import { lowerGenMethodCall } from "./lower-generators.js";
|
||||
import { lowerClassSymbolMethodCall } from "./symbol-methods.js";
|
||||
import { errorToStringCall } from "./error-methods.js";
|
||||
@@ -34,6 +41,7 @@ import {
|
||||
BOOL,
|
||||
CAUGHT,
|
||||
DYN,
|
||||
DYN_HANDLE_KINDS,
|
||||
F64,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
@@ -61,7 +69,11 @@ import { lowerProtocolMethodCall } from "./class-protocol-dispatch.js";
|
||||
import { isJsSourceFile, locOf, requireSpecOf } from "../program.js";
|
||||
import { lowerJsonRequireCall } from "./json-require.js";
|
||||
import { lowerAssertInstanceCall } from "./assert-instances.js";
|
||||
import { isGenericCallableMemberType, typeKey } from "../type-mapper.js";
|
||||
import {
|
||||
isGenericCallableMemberType,
|
||||
isOverloadedCallableMemberType,
|
||||
typeKey,
|
||||
} from "../type-mapper.js";
|
||||
import { dynFallbackType, jsFuncNameOf, newFnCtx, staticImportNamespaceType } from "./lowerer.js";
|
||||
import {
|
||||
NARROW_FIRST,
|
||||
@@ -79,7 +91,10 @@ import {
|
||||
import { lowerArrayFromAsyncCall } from "./containers/array-from-async.js";
|
||||
import { lowerDynArrayFilterCall, lowerDynArrayFlatMapCall } from "./containers/array-callbacks.js";
|
||||
import { lowerGroupByStaticCall } from "./containers/collection-grouping.js";
|
||||
import { lowerIteratorHelperCall } from "./containers/iterator-helpers.js";
|
||||
import {
|
||||
lowerGeneratorHelperCall,
|
||||
lowerIteratorHelperCall,
|
||||
} from "./containers/iterator-helpers.js";
|
||||
import {
|
||||
lowerObjectAssignIndexShape,
|
||||
lowerObjectConstructor,
|
||||
@@ -234,6 +249,32 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
const json = lowerJsonRequireCall(lowerer, expr);
|
||||
if (json) return json;
|
||||
|
||||
// Inline require() of a program module (cjs-inline-require.ts): a
|
||||
// member call over its export table resolves like a binding's member
|
||||
// call after the module's %init at this position; the require itself
|
||||
// as a value answers the module's whole-export value.
|
||||
if (ts.isPropertyAccessExpression(expr.expression) && !expr.expression.questionDotToken) {
|
||||
const req = inlineNamespaceRequireOf(lowerer, expr.expression.expression);
|
||||
if (req !== null && !lowerer.inlineRequireInitDone.has(req.call)) {
|
||||
// Node calls the member with `this` bound to the module's exports.
|
||||
if (
|
||||
ts.isIdentifier(expr.expression.name) &&
|
||||
!cjsExportCallIgnoresThis(lowerer, req.dep, expr.expression.name.text)
|
||||
) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
`calling '${expr.expression.name.text}' through require(${JSON.stringify(req.spec)}) when the function may read 'this' (Node binds it to the module's exports object, which compiles to a static table — bind the module and call a local function instead)`,
|
||||
);
|
||||
}
|
||||
return lowerWithInlineRequireInit(lowerer, req, () => lowerCall(lowerer, expr));
|
||||
}
|
||||
}
|
||||
{
|
||||
const inlineValue = lowerInlineRequireValue(lowerer, expr);
|
||||
if (inlineValue) return inlineValue;
|
||||
}
|
||||
|
||||
// A call whose chain ROOTS at an ambient-undefined name (`declare
|
||||
// const value: Y | undefined; value?.foo("a")`, `declare function
|
||||
// chain...; chain(o).mapValues(f).value()`, a trap binding's read):
|
||||
@@ -310,6 +351,12 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
lowerImportMetaResolveCall(lowerer, expr) ?? lowerRequireResolveCall(lowerer, expr);
|
||||
if (resolved) return resolved;
|
||||
}
|
||||
// Any other require() call of the CommonJS global: MODULE_NOT_FOUND for
|
||||
// packages nothing installed resolves, a named refusal otherwise.
|
||||
{
|
||||
const uncompiled = lowerUncompiledRequireCall(lowerer, expr);
|
||||
if (uncompiled) return uncompiled;
|
||||
}
|
||||
|
||||
// `process.getuid?.()` — intercepted BEFORE the optional-chain
|
||||
// machinery (the member always exists on a POSIX target, so the
|
||||
@@ -1274,6 +1321,32 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
const args = completeFuncValueArgs(lowerer, expr, callee.type, loc);
|
||||
return { kind: "callValue", callee, args, type: callee.type.ret, loc };
|
||||
}
|
||||
// `exports.f(x)` / `module.exports.f(x)` inside the module: a call of
|
||||
// the member's export global (cjs-exports-object.ts), the value every
|
||||
// `exports.f =` statement assigns.
|
||||
if (ts.isPropertyAccessExpression(expr.expression) && !expr.questionDotToken) {
|
||||
const g = cjsInFileExportGlobalOf(lowerer, expr.expression);
|
||||
if (
|
||||
g &&
|
||||
(g.type.kind === "func" || g.type.kind === "dyn") &&
|
||||
!cjsExportCallIgnoresThis(lowerer, expr.getSourceFile(), expr.expression.name.text)
|
||||
) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
`calling '${expr.expression.getText()}' through the module's exports object when the function may read 'this' (Node binds it to the exports object, which compiles to a static table — call a local function instead)`,
|
||||
);
|
||||
}
|
||||
if (g && (g.type.kind === "func" || g.type.kind === "dyn")) {
|
||||
const callee: IrExpr = { kind: "varRef", localId: g.id, type: g.type, loc };
|
||||
const checked = lowerCheckedCallableValue(lowerer, expr, callee, loc);
|
||||
if (checked) return checked;
|
||||
if (callee.type.kind === "func") {
|
||||
const args = completeFuncValueArgs(lowerer, expr, callee.type, loc);
|
||||
return { kind: "callValue", callee, args, type: callee.type.ret, loc };
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(expr.expression)) {
|
||||
const intrinsic =
|
||||
fenceNodeModuleMutationCall(lowerer, expr, expr.expression) ??
|
||||
@@ -1421,6 +1494,8 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
// array method paths (the terminal names collide with array
|
||||
// methods, but only iterator-typed receivers reach this).
|
||||
lowerIteratorHelperCall(lowerer, expr, expr.expression) ??
|
||||
// Helper calls on generators and other iterator objects.
|
||||
lowerGeneratorHelperCall(lowerer, expr, expr.expression) ??
|
||||
lowerIteratorStaticFence(lowerer, expr, expr.expression) ??
|
||||
lowerObjectStaticCall(lowerer, expr, expr.expression) ??
|
||||
lowerObjectFromEntriesCall(lowerer, expr, expr.expression) ??
|
||||
@@ -3074,8 +3149,9 @@ function lowerOptionalStringNumber(
|
||||
return { kind: "call", callee: helper, args: [arg], type: F64, loc };
|
||||
}
|
||||
|
||||
/** Iterator types whose helper calls have no lowering on any path (the
|
||||
* array iterator's chains lower in lowerIteratorHelperCall). */
|
||||
/** Iterator types with checked runtime receivers: Map/Set iterators'
|
||||
* helper calls lower in lowerGeneratorHelperCall; the string iterator's
|
||||
* have no lowering. */
|
||||
const UNLOWERED_ITERATOR_TYPES = new Set(["MapIterator", "SetIterator", "StringIterator"]);
|
||||
const ITERATOR_HELPER_METHODS = new Set([
|
||||
"map",
|
||||
@@ -3142,6 +3218,8 @@ function lowerDynReceiverMethodCall(
|
||||
lowerer.isStdlibMember(access)
|
||||
) {
|
||||
if (recvSymbol.name === "MapIterator" || recvSymbol.name === "SetIterator") {
|
||||
const helper = lowerGeneratorHelperCall(lowerer, call, access);
|
||||
if (helper) return helper;
|
||||
lowerer.noLowering(
|
||||
`collection iterator .${access.name.text}()`,
|
||||
call,
|
||||
@@ -3152,7 +3230,7 @@ function lowerDynReceiverMethodCall(
|
||||
lowerer.noLowering(
|
||||
`${recvSymbol.name}.${access.name.text}`,
|
||||
call,
|
||||
"iterator helpers lower on chains from arr.values() or arr.entries() that end in a terminal (toArray, forEach, reduce, some, every, find) — spread the string first: [...s].values()",
|
||||
"iterator helpers lower on generators and on array, Map and Set iterators — spread the string first: [...s].values()",
|
||||
);
|
||||
}
|
||||
const arrayReceiver = lowerer.checker.isArrayType(recvTs);
|
||||
@@ -3274,6 +3352,11 @@ function lowerDynReceiverMethodCall(
|
||||
lowerStringMethodCall(lowerer, call, access, () => recv)
|
||||
);
|
||||
}
|
||||
// A native handle read through `as any` (`(hash as any).update(x)`)
|
||||
// dispatches through the handle, as the same value held in an
|
||||
// 'unknown' slot does.
|
||||
if (checkerUntyped && DYN_HANDLE_KINDS.has(recv.type.kind))
|
||||
recv = lowerer.coerceToExpected(recv, DYN);
|
||||
if (recv.type.kind !== "dyn") return null;
|
||||
if (
|
||||
access.name.text === "normalize" &&
|
||||
@@ -3480,13 +3563,41 @@ function lowerDynReceiverMethodCall(
|
||||
// its name overlaps a primitive prototype (`colors.bold(...)`): the
|
||||
// source value was compiled from that object shape, while genuinely
|
||||
// untyped/any receivers retain the runtime-kind ambiguity and fence.
|
||||
if (DYN_PROTO_METHOD_NAMES.has(access.name.text) && !ownCallable) return null;
|
||||
// A name only String.prototype declares (`bold`, `italics`, `link`,
|
||||
// ...) on a checker-untyped receiver: every other dyn kind answers its
|
||||
// own or inherited member exactly like a non-prototype name (chalk's
|
||||
// `chalk.red.bold(...)` reads a prototype getter of a function), so
|
||||
// only a string receiver keeps the refusal, deferred to run time.
|
||||
const stringOnlyName =
|
||||
checkerUntyped && !ownCallable && DYN_STRING_PROTO_ONLY_NAMES.has(access.name.text);
|
||||
if (DYN_PROTO_METHOD_NAMES.has(access.name.text) && !ownCallable && !stringOnlyName) return null;
|
||||
// Optional forms (`obj.cb?.()`, `obj?.cb()`) belong to the chain
|
||||
// machinery's short-circuit semantics — not modeled here yet.
|
||||
if (call.questionDotToken || access.questionDotToken) return null;
|
||||
const loc = locOf(call);
|
||||
const receiverLocal = lowerer.declareHiddenLocal("%dynCallReceiver", DYN);
|
||||
const receiver: IrExpr = { kind: "varRef", localId: receiverLocal.id, type: DYN, loc };
|
||||
// The string refusal throws in place of the member read (a seqExpr
|
||||
// statement cannot leave the expression).
|
||||
const stringRefusal = (read: IrExpr): IrExpr => {
|
||||
if (!stringOnlyName) return read;
|
||||
const sf = call.getSourceFile();
|
||||
const line = ts.getLineAndCharacterOfPosition(sf, call.getStart(sf)).line + 1;
|
||||
return {
|
||||
kind: "ternary",
|
||||
cond: { kind: "dynTest", test: "string", value: receiver, type: BOOL, loc },
|
||||
then: nodeThrowExpr(
|
||||
0,
|
||||
"SC1090",
|
||||
`method calls like '${access.getText()}' on a string are not supported yet [SC1090 at ${sf.fileName}:${line}]`,
|
||||
DYN,
|
||||
loc,
|
||||
),
|
||||
else_: read,
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
};
|
||||
const member: IrExpr = {
|
||||
kind: "dynKeyGet",
|
||||
key: { kind: "strLit", value: access.name.text, type: STRING, loc: locOf(access) },
|
||||
@@ -3498,7 +3609,7 @@ function lowerDynReceiverMethodCall(
|
||||
const callee = varRef(calleeLocal.id, DYN, loc);
|
||||
const stmts: IrStmt[] = [
|
||||
{ kind: "varDecl", localId: receiverLocal.id, init: recv, loc },
|
||||
{ kind: "varDecl", localId: calleeLocal.id, init: member, loc },
|
||||
{ kind: "varDecl", localId: calleeLocal.id, init: stringRefusal(member), loc },
|
||||
];
|
||||
if (call.arguments.some(ts.isSpreadElement)) {
|
||||
const spread = lowerSpreadArgsCall(lowerer, call, callee, loc);
|
||||
@@ -3607,6 +3718,31 @@ const DYN_PROTO_METHOD_NAMES = new Set([
|
||||
"toPrecision",
|
||||
]);
|
||||
|
||||
/** The DYN_PROTO_METHOD_NAMES that only String.prototype declares among
|
||||
* the dyn-representable kinds' prototypes. */
|
||||
const DYN_STRING_PROTO_ONLY_NAMES = new Set([
|
||||
"anchor",
|
||||
"big",
|
||||
"blink",
|
||||
"bold",
|
||||
"codePointAt",
|
||||
"fixed",
|
||||
"fontcolor",
|
||||
"fontsize",
|
||||
"isWellFormed",
|
||||
"italics",
|
||||
"link",
|
||||
"localeCompare",
|
||||
"normalize",
|
||||
"small",
|
||||
"strike",
|
||||
"sub",
|
||||
"sup",
|
||||
"toLocaleLowerCase",
|
||||
"toLocaleUpperCase",
|
||||
"toWellFormed",
|
||||
]);
|
||||
|
||||
/** The SHARED prototype names scr_dyn_invoke dispatches at runtime (the
|
||||
* subset of DYN_PROTO_METHOD_NAMES with a receiver-kind dispatch): the
|
||||
* runtime runs the real method for the receiver's kind, throws Node's
|
||||
@@ -4117,10 +4253,32 @@ function lowerNumberToStringCall(
|
||||
}
|
||||
if (call.arguments.length === 1) {
|
||||
if (operand.type.kind !== "f64") return null;
|
||||
const arg = call.arguments[0]!;
|
||||
// A number radix needs no box: ToIntegerOrInfinity of a number is a
|
||||
// truncation, which the typed entry point applies itself.
|
||||
if (!ts.isSpreadElement(arg) && lowerer.mapTypeOf(lowerer.typeOf(arg))?.kind === "f64") {
|
||||
const radix = lowerer.lowerExpr(arg);
|
||||
if (radix.type.kind === "f64") {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "num.toStringRadixF64",
|
||||
args: [operand, radix],
|
||||
type: STRING,
|
||||
loc: locOf(call),
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "num.toStringRadix",
|
||||
args: [operand, lowerer.coerceToExpected(radix, DYN)],
|
||||
type: STRING,
|
||||
loc: locOf(call),
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "num.toStringRadix",
|
||||
args: [operand, lowerer.lowerExprExpecting(call.arguments[0]!, DYN)],
|
||||
args: [operand, lowerer.lowerExprExpecting(arg, DYN)],
|
||||
type: STRING,
|
||||
loc: locOf(call),
|
||||
};
|
||||
@@ -6467,9 +6625,9 @@ function recordAssignHelper(
|
||||
}
|
||||
|
||||
/** The `Iterator` global's statics (ES2025 — Iterator.from, and the
|
||||
* abstract constructor as a value): no first-class iterator objects
|
||||
* exist here, so every member fences with the working spelling named
|
||||
* instead of the generic-method fence's monomorphization wording. */
|
||||
* abstract constructor as a value) have no lowering: every member fences
|
||||
* with the working spelling named instead of the generic-method fence's
|
||||
* monomorphization wording. */
|
||||
function lowerIteratorStaticFence(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
@@ -6480,8 +6638,8 @@ function lowerIteratorStaticFence(
|
||||
lowerer.noLowering(
|
||||
`Iterator.${access.name.text}`,
|
||||
call,
|
||||
"first-class iterator objects have no lowering — iterator helpers compile as one chain on an " +
|
||||
"array iterator, consumed in place: arr.values().map(f).take(n).toArray()",
|
||||
"iterator helpers apply directly to generators and to array, Map and Set iterators: " +
|
||||
"arr.values().map(f).take(n).toArray()",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8132,6 +8290,30 @@ function lowerObjectAssignHybrid(lowerer: Lowerer, call: ts.CallExpression): IrE
|
||||
return { kind: "recordLit", fields, type: mapped, loc };
|
||||
}
|
||||
|
||||
/** The accessed member is an overload set (isOverloadedCallableMemberType),
|
||||
* also under an optional chain whose access still spells `| undefined`. */
|
||||
function isOverloadedMemberAccess(lowerer: Lowerer, access: ts.PropertyAccessExpression): boolean {
|
||||
return isOverloadedCallableMemberType(
|
||||
lowerer.checker.getNonNullableType(lowerer.typeOf(access)),
|
||||
lowerer.checker,
|
||||
);
|
||||
}
|
||||
|
||||
/** The result type of the overload a call through an overloaded member
|
||||
* resolved to: the erased slot's result converts to it. A non-generic
|
||||
* overload answers through its declaration, because the resolved signature
|
||||
* of an optional-chain call (`api?.m(x)`) also carries the chain's
|
||||
* undefined; a generic one answers through its instantiation. */
|
||||
function resolvedOverloadResultType(lowerer: Lowerer, call: ts.CallExpression): IrType {
|
||||
const { checker } = lowerer;
|
||||
const signature = checker.getResolvedSignature(call);
|
||||
const decl = signature ? checker.signatureDeclaration(signature) : undefined;
|
||||
const declared = decl ? checker.getSignatureFromDeclaration(decl) : undefined;
|
||||
const source = declared && declared.getTypeParameters().length === 0 ? declared : signature;
|
||||
const result = source ? lowerer.mapTypeOf(checker.getReturnTypeOfSignature(source)) : null;
|
||||
return result ?? lowerer.irTypeOf(call);
|
||||
}
|
||||
|
||||
export function lowerRecordFieldCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
@@ -8163,7 +8345,18 @@ export function lowerRecordFieldCall(
|
||||
const callee = lowerer.lowerUnionProperty(access);
|
||||
if (callee?.type.kind !== "func") return null;
|
||||
const args = completeFuncValueArgs(lowerer, call, callee.type, locOf(call));
|
||||
return { kind: "callValue", callee, args, type: callee.type.ret, loc: locOf(call) };
|
||||
const value: IrExpr = {
|
||||
kind: "callValue",
|
||||
callee,
|
||||
args,
|
||||
type: callee.type.ret,
|
||||
loc: locOf(call),
|
||||
};
|
||||
// An overloaded member's erased result converts to the resolved
|
||||
// overload's type, as on a single record receiver below.
|
||||
return isOverloadedMemberAccess(lowerer, access)
|
||||
? lowerer.coerceInto(call, value, resolvedOverloadResultType(lowerer, call))
|
||||
: value;
|
||||
}
|
||||
const target = lowerer.fieldTarget(access);
|
||||
const loc = locOf(call);
|
||||
@@ -8238,10 +8431,14 @@ export function lowerRecordFieldCall(
|
||||
type: callee.type.ret,
|
||||
loc,
|
||||
};
|
||||
// A generic member's field holds its erased instance: the result
|
||||
// converts to this call's instantiated return type at the call.
|
||||
if (isGenericCallableMemberType(lowerer.typeOf(access), lowerer.checker))
|
||||
// A generic member's field holds its erased instance, and an overloaded
|
||||
// member's field its erased function (the overloads joined): the result
|
||||
// converts to this call's instantiated or resolved return type.
|
||||
const memberType = lowerer.typeOf(access);
|
||||
if (isGenericCallableMemberType(memberType, lowerer.checker))
|
||||
result = lowerer.coerceInto(call, result, lowerer.irTypeOf(call));
|
||||
else if (isOverloadedMemberAccess(lowerer, access))
|
||||
result = lowerer.coerceInto(call, result, resolvedOverloadResultType(lowerer, call));
|
||||
return init ? { kind: "seqExpr", stmts: [init], result, type: result.type, loc } : result;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { lowerValueReturningConstructorNew } from "./js-dynamic-objects.js";
|
||||
import { dynUndefinedExpr, nodeThrowExpr, varRef } from "../../ir/build.js";
|
||||
import { checkedIterableSpread } from "./checked-iterable-spread.js";
|
||||
import { lowerReadlineUiNew } from "./builtins/readline.js";
|
||||
@@ -11,7 +12,13 @@ import { InternalCompilerError } from "../../errors.js";
|
||||
import { fsStreamClassOf, lowerFsStreamNew } from "./builtins/fs-streams.js";
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { lowerZlibStreamNew, zlibClassName, zlibStreamModeOf } from "./builtins/compression.js";
|
||||
import {
|
||||
type BuiltinClassRef,
|
||||
builtinClassAliasOf,
|
||||
builtinClassRefOf,
|
||||
} from "./builtins/class-values.js";
|
||||
import type { FnCtx, Lowerer } from "./lowerer.js";
|
||||
import { inlineNamespaceRequireOf, lowerWithInlineRequireInit } from "./cjs-inline-require.js";
|
||||
import {
|
||||
BOOL,
|
||||
DATE_T,
|
||||
@@ -44,6 +51,7 @@ import {
|
||||
implicitAnyParamSymbolsOf,
|
||||
implicitCallInstance,
|
||||
implicitMonoFile,
|
||||
genericLocalClassResult,
|
||||
type GenericFnInfo,
|
||||
} from "./generic-functions.js";
|
||||
import {
|
||||
@@ -143,7 +151,7 @@ import { lowerObjectFactoryNew } from "./object-factory-new.js";
|
||||
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
|
||||
import { lowerInstanceConstructorNew } from "./class-instance-constructor.js";
|
||||
import { classPrototypeData } from "./class-prototypes.js";
|
||||
import { constructionEscape, mentionsReceiver } from "./construction-escapes.js";
|
||||
import { constructionEscape } from "./construction-escapes.js";
|
||||
import { checkedClassConstruction, checkedClassConstructionPacked } from "./class-construction.js";
|
||||
import { initializeRuntimeStatics, hasRuntimeStatics } from "./class-runtime-statics.js";
|
||||
import { reflectedClassStaticMethodValue } from "./class-method-values.js";
|
||||
@@ -222,6 +230,11 @@ export function storedClassValueType(
|
||||
if (value?.kind === "classval") return value;
|
||||
}
|
||||
}
|
||||
// A generic factory's own local class has one class per instance.
|
||||
if (ts.isCallExpression(expression)) {
|
||||
const local = genericLocalClassResult(lowerer, expression);
|
||||
if (local?.kind === "classval") return local;
|
||||
}
|
||||
const type = lowerer.typeOf(expression);
|
||||
const mapped = lowerer.mapTypeOf(type);
|
||||
if (mapped?.kind === "classval" && !lowerer.classes.has(mapped.className)) {
|
||||
@@ -468,11 +481,6 @@ export interface ClassInfo {
|
||||
* unassigned read throws the catchable TypeError instead of yielding
|
||||
* an undefined the declared type cannot hold (SEMANTICS.md). */
|
||||
deferredInitFields?: Set<string>;
|
||||
/** Fields (inherited included) that code construction exposes the
|
||||
* instance to can read before their assignment: the slot is the
|
||||
* undefined-armed union, allocation writes undefined, and reads yield the
|
||||
* union value, so an early read sees undefined as in Node. */
|
||||
earlyReadFields?: Set<string>;
|
||||
/** True when construction may hand the instance to other code (a method
|
||||
* call, an argument, a callback, or a base constructor that does) — a
|
||||
* subclass's own fields can then be read before their assignment. */
|
||||
@@ -928,12 +936,15 @@ export function registerBuiltinStreamClasses(lowerer: Lowerer): void {
|
||||
* "stream" ambient module, EXCLUDING @types/node's (whose stream.Readable
|
||||
* also types child stdio — under @types/node the childStream mapping
|
||||
* keeps priority and the static stream classes stand down; the shipped
|
||||
* fallback declarations are the supported surface). */
|
||||
* fallback declarations are the supported surface). A const binding of
|
||||
* one (`const R = Readable`) names the same class (builtinClassAliasOf). */
|
||||
export function builtinStreamInfoOf(
|
||||
lowerer: Lowerer,
|
||||
symbol: ts.Symbol | null | undefined,
|
||||
): ClassInfo | null {
|
||||
if (!symbol) return null;
|
||||
const alias = builtinClassAliasOf(lowerer, symbol);
|
||||
if (alias !== null) return alias.kind === "stream" ? alias.info : null;
|
||||
if (!lowerer.isStdlibSymbol(symbol)) {
|
||||
// A const ALIAS of a namespace member (`const Writable =
|
||||
// stream.Writable` — the two-step spelling; the one-step
|
||||
@@ -1288,12 +1299,16 @@ function undefArmedFieldType(lowerer: Lowerer, p: ts.Symbol): IrType | null {
|
||||
* `extends EventEmitter`, `x instanceof EventEmitter`) — any import
|
||||
* spelling (named/default/namespace member, CJS require) resolves to
|
||||
* the ambient class. Provenance-checked like the error classes: only a
|
||||
* stdlib-file declaration inside the "events" ambient module counts. */
|
||||
* stdlib-file declaration inside the "events" ambient module counts. A
|
||||
* const binding of it (`const E = EventEmitter`) names the same class
|
||||
* (builtinClassAliasOf). */
|
||||
export function builtinEmitterInfoOf(
|
||||
lowerer: Lowerer,
|
||||
symbol: ts.Symbol | null | undefined,
|
||||
): ClassInfo | null {
|
||||
if (!symbol) return null;
|
||||
const alias = builtinClassAliasOf(lowerer, symbol);
|
||||
if (alias !== null) return alias.kind === "emitter" ? alias.info : null;
|
||||
if (!lowerer.isStdlibSymbol(symbol)) {
|
||||
// A const ALIAS of the emitter class member (`const EventEmitter =
|
||||
// require('node:events').EventEmitter` — commander's spelling; the
|
||||
@@ -1334,12 +1349,17 @@ export function builtinEmitterInfoOf(
|
||||
/** The builtin error ClassInfo a VALUE symbol refers to (`new Error`,
|
||||
* `extends TypeError`, `x instanceof RangeError`), or null. Provenance-
|
||||
* checked: only the standard library's declarations count — a user's own
|
||||
* `class Error` resolves through classBySymbol instead. */
|
||||
* `class Error` resolves through classBySymbol instead. A const binding
|
||||
* of one (`const E = TypeError`) names the same class
|
||||
* (builtinClassAliasOf). */
|
||||
export function builtinErrorInfoOf(
|
||||
lowerer: Lowerer,
|
||||
symbol: ts.Symbol | null | undefined,
|
||||
): ClassInfo | null {
|
||||
if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null;
|
||||
if (!symbol) return null;
|
||||
const alias = builtinClassAliasOf(lowerer, symbol);
|
||||
if (alias !== null) return alias.kind === "error" ? alias.info : null;
|
||||
if (!lowerer.isStdlibSymbol(symbol)) return null;
|
||||
for (const [irName, rec] of RUNTIME_ERROR_CLASSES) {
|
||||
if (rec.lib === symbol.name) return lowerer.classes.get(irName) ?? null;
|
||||
}
|
||||
@@ -3507,54 +3527,55 @@ export function collectClassShapeInner(
|
||||
const derived =
|
||||
decl.heritageClauses?.some((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword) ??
|
||||
false;
|
||||
// A generic family is not emitted; its instances and each mixin
|
||||
// application analyze their own construction with their actual base.
|
||||
const escape: { escapes: boolean; assigned: Set<string> | null } = familyMode
|
||||
? { escapes: derived, assigned: null }
|
||||
: constructionEscape({
|
||||
ctor,
|
||||
fields: fieldOrder.flatMap((f) =>
|
||||
paramProps.some((pp) => pp.name === f.name)
|
||||
? []
|
||||
: [{ name: f.name, initializer: f.initializer }],
|
||||
),
|
||||
paramProps: paramProps.map((pp) => pp.name),
|
||||
derived,
|
||||
baseEscapes:
|
||||
base === null ||
|
||||
callableBase !== undefined ||
|
||||
factoryBaseExpression !== undefined ||
|
||||
base.constructionEscapes === true,
|
||||
isField: (name) =>
|
||||
fields.has(name) &&
|
||||
!methods.has(`set:${name}`) &&
|
||||
!lowerer.findMethodOn(base, `set:${name}`),
|
||||
privateMethodBody: (call) => {
|
||||
const callee = call.expression;
|
||||
if (!ts.isPropertyAccessExpression(callee)) return null;
|
||||
const name = callee.name.text;
|
||||
const declaration = decl.members.find(
|
||||
(member): member is ts.MethodDeclaration =>
|
||||
ts.isMethodDeclaration(member) &&
|
||||
member.body !== undefined &&
|
||||
(ts.isIdentifier(member.name) || ts.isPrivateIdentifier(member.name)) &&
|
||||
member.name.text === name,
|
||||
);
|
||||
if (!declaration?.body || declaration.asteriskToken) return null;
|
||||
const modifiers = ts.getModifiers(declaration) ?? [];
|
||||
if (
|
||||
modifiers.some(
|
||||
(m) =>
|
||||
m.kind === ts.SyntaxKind.StaticKeyword || m.kind === ts.SyntaxKind.AsyncKeyword,
|
||||
// Generic instances and mixins share layouts across declarations, so
|
||||
// they only report whether construction exposes the instance.
|
||||
const escape: { escapes: boolean; assigned: Set<string> | null } =
|
||||
familyMode || mixin
|
||||
? { escapes: derived, assigned: null }
|
||||
: constructionEscape({
|
||||
ctor,
|
||||
fields: fieldOrder.flatMap((f) =>
|
||||
paramProps.some((pp) => pp.name === f.name)
|
||||
? []
|
||||
: [{ name: f.name, initializer: f.initializer }],
|
||||
),
|
||||
paramProps: paramProps.map((pp) => pp.name),
|
||||
derived,
|
||||
baseEscapes:
|
||||
base === null ||
|
||||
callableBase !== undefined ||
|
||||
factoryBaseExpression !== undefined ||
|
||||
base.constructionEscapes === true,
|
||||
isField: (name) =>
|
||||
fields.has(name) &&
|
||||
!methods.has(`set:${name}`) &&
|
||||
!lowerer.findMethodOn(base, `set:${name}`),
|
||||
privateMethodBody: (call) => {
|
||||
const callee = call.expression;
|
||||
if (!ts.isPropertyAccessExpression(callee)) return null;
|
||||
const name = callee.name.text;
|
||||
const declaration = decl.members.find(
|
||||
(member): member is ts.MethodDeclaration =>
|
||||
ts.isMethodDeclaration(member) &&
|
||||
member.body !== undefined &&
|
||||
(ts.isIdentifier(member.name) || ts.isPrivateIdentifier(member.name)) &&
|
||||
member.name.text === name,
|
||||
);
|
||||
if (!declaration?.body || declaration.asteriskToken) return null;
|
||||
const modifiers = ts.getModifiers(declaration) ?? [];
|
||||
if (
|
||||
modifiers.some(
|
||||
(m) =>
|
||||
m.kind === ts.SyntaxKind.StaticKeyword || m.kind === ts.SyntaxKind.AsyncKeyword,
|
||||
)
|
||||
)
|
||||
)
|
||||
return null;
|
||||
const isPrivate =
|
||||
ts.isPrivateIdentifier(declaration.name) ||
|
||||
modifiers.some((m) => m.kind === ts.SyntaxKind.PrivateKeyword);
|
||||
return isPrivate ? declaration.body : null;
|
||||
},
|
||||
});
|
||||
return null;
|
||||
const isPrivate =
|
||||
ts.isPrivateIdentifier(declaration.name) ||
|
||||
modifiers.some((m) => m.kind === ts.SyntaxKind.PrivateKeyword);
|
||||
return isPrivate ? declaration.body : null;
|
||||
},
|
||||
});
|
||||
// The deferred definite-assignment check: a field on the unguarded
|
||||
// list passes only with an unconditional `this.x = ...` at the
|
||||
// constructor's TOP LEVEL — the same standard the JS-class path
|
||||
@@ -3610,30 +3631,18 @@ export function collectClassShapeInner(
|
||||
}
|
||||
}
|
||||
|
||||
// Fields assigned after construction can expose the instance (a method
|
||||
// call, a base constructor that calls an override) can be read while
|
||||
// Node still holds undefined. Their slots admit undefined, allocation
|
||||
// writes it, and reads yield it, as in Node.
|
||||
const earlyReadFields = new Set<string>(base?.earlyReadFields ?? []);
|
||||
if (escape.assigned && escape.escapes) {
|
||||
if (escape.assigned && !inst) {
|
||||
for (const f of fieldOrder) {
|
||||
if (f.redeclared || escape.assigned.has(f.name) || deferredInitFields.has(f.name)) continue;
|
||||
if (paramProps.some((pp) => pp.name === f.name)) continue;
|
||||
const declared = fields.get(f.name);
|
||||
if (declared === undefined || !earlyReadable(lowerer, declared)) continue;
|
||||
// A declared `T | undefined` field already starts undefined.
|
||||
if (declared.kind === "union" && lowerer.armTag(declared.unionId, UNDEFINED_T) >= 0)
|
||||
if (declared?.kind !== "object" || lowerer.classes.get(declared.className)?.def.runtime)
|
||||
continue;
|
||||
const armed = lowerer.runtimeOptionalType(declared);
|
||||
if (armed.kind !== "union") continue;
|
||||
const armed = lowerer.withUndefinedArm(declared);
|
||||
if (armed === null || armed.kind !== "union") continue;
|
||||
fields.set(f.name, armed);
|
||||
f.type = armed;
|
||||
// Primitive and union slots yield the undefined value itself.
|
||||
// Reference slots keep the deferred-init reads: consumers that
|
||||
// observe undefined (printing, equality, typeof, nullish defaults)
|
||||
// see it, and member access on it throws a TypeError, as in Node.
|
||||
if (declared.kind === "union" || PRIMITIVE_EARLY_READS.has(declared.kind))
|
||||
earlyReadFields.add(f.name);
|
||||
else deferredInitFields.add(f.name);
|
||||
deferredInitFields.add(f.name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3738,43 +3747,6 @@ export function collectClassShapeInner(
|
||||
(name) => !hasAccessor(name) && !methods.has(name) && !lowerer.findMethodOn(base, name),
|
||||
)
|
||||
: null;
|
||||
// Constructor statements that run after the instance may have been
|
||||
// exposed (a method call, `this` passed along, a base constructor
|
||||
// that exposes it): a field they create can be read before its
|
||||
// assignment, while Node holds undefined.
|
||||
const exposedBefore = new Set<ts.Statement>();
|
||||
{
|
||||
let exposed = false;
|
||||
for (const stmt of ctor?.body?.statements ?? []) {
|
||||
const expression = ts.isExpressionStatement(stmt) ? stmt.expression : null;
|
||||
if (
|
||||
expression &&
|
||||
ts.isCallExpression(expression) &&
|
||||
expression.expression.kind === ts.SyntaxKind.SuperKeyword
|
||||
) {
|
||||
if (base?.constructionEscapes === true || expression.arguments.some(mentionsReceiver))
|
||||
exposed = true;
|
||||
continue;
|
||||
}
|
||||
const assigned =
|
||||
expression &&
|
||||
ts.isBinaryExpression(expression) &&
|
||||
expression.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
||||
ts.isPropertyAccessExpression(expression.left) &&
|
||||
expression.left.expression.kind === ts.SyntaxKind.ThisKeyword
|
||||
? expression
|
||||
: null;
|
||||
if (exposed || (assigned && mentionsReceiver(assigned.right))) exposedBefore.add(stmt);
|
||||
const plainAssignment =
|
||||
assigned !== null &&
|
||||
ts.isPropertyAccessExpression(assigned.left) &&
|
||||
!mentionsReceiver(assigned.right) &&
|
||||
!hasAccessor(assigned.left.name.text) &&
|
||||
!methods.has(assigned.left.name.text) &&
|
||||
!lowerer.findMethodOn(base, assigned.left.name.text);
|
||||
if (!plainAssignment && mentionsReceiver(stmt)) exposed = true;
|
||||
}
|
||||
}
|
||||
if (ctor?.body) {
|
||||
for (const stmt of ctor.body.statements) {
|
||||
const lhs =
|
||||
@@ -3845,15 +3817,8 @@ export function collectClassShapeInner(
|
||||
type = inheritedJsFieldType(lowerer, decl, name, type);
|
||||
// super() can invoke a subclass setter or method before its
|
||||
// constructor assignments run. These slots start undefined.
|
||||
const sealed = sealedPrefix?.created.has(name) === true;
|
||||
if (base && lowerer.dynConvertible(type) && !sealed) type = DYN;
|
||||
if (exposedBefore.has(stmt) && !sealed && earlyReadable(lowerer, type)) {
|
||||
const armed = lowerer.runtimeOptionalType(type);
|
||||
if (armed.kind === "union") {
|
||||
type = armed;
|
||||
earlyReadFields.add(name);
|
||||
}
|
||||
}
|
||||
if (base && lowerer.dynConvertible(type) && !sealedPrefix?.created.has(name))
|
||||
type = DYN;
|
||||
fields.set(name, type);
|
||||
fieldOrder.push({ name, type, initializer: undefined });
|
||||
continue;
|
||||
@@ -4114,7 +4079,6 @@ export function collectClassShapeInner(
|
||||
? { classDecorators: { nodes: classDecoratorNodes } }
|
||||
: {}),
|
||||
...(deferredInitFields.size > 0 ? { deferredInitFields } : {}),
|
||||
...(earlyReadFields.size > 0 ? { earlyReadFields } : {}),
|
||||
...(escape.escapes ? { constructionEscapes: true as const } : {}),
|
||||
};
|
||||
// GENERIC members get their declaring-class backlink now that the
|
||||
@@ -4419,6 +4383,7 @@ export function lowerStaticFieldInits(lowerer: Lowerer, info: ClassInfo): IrStmt
|
||||
// instantiation's bindings/context (a no-op for everything else —
|
||||
// generic FAMILIES own their statics and carry no genericInstance).
|
||||
const diagsBefore = lowerer.diags.length;
|
||||
const sinkMark = lowerer.diagSinkMark();
|
||||
try {
|
||||
return withInstanceBindings(lowerer, info, () => lowerStaticFieldInitsInner(lowerer, info));
|
||||
} catch (error) {
|
||||
@@ -4426,8 +4391,11 @@ export function lowerStaticFieldInits(lowerer: Lowerer, info: ClassInfo): IrStmt
|
||||
// Keep their refusal attached to the class declaration instead of
|
||||
// letting a poisoned initializer escape the module lowering pass.
|
||||
if (!(error instanceof PoisonError)) throw error;
|
||||
if (info.decl && isJsSourceFile(info.decl.getSourceFile()) && lowerer.diagSink === null) {
|
||||
const fence = lowerer.deferToRuntimeFence(diagsBefore, info.decl, { kind: "statement" });
|
||||
if (info.decl && isJsSourceFile(info.decl.getSourceFile())) {
|
||||
const fence =
|
||||
lowerer.diagSink === null
|
||||
? lowerer.deferToRuntimeFence(diagsBefore, info.decl, { kind: "statement" })
|
||||
: lowerer.capturedRuntimeFence(sinkMark, info.decl);
|
||||
if (fence) return [fence];
|
||||
}
|
||||
return [];
|
||||
@@ -4564,6 +4532,7 @@ function lowerStaticFieldInitsInner(lowerer: Lowerer, info: ClassInfo): IrStmt[]
|
||||
lowerer.stats.statementsTotal++;
|
||||
lowerer.bumpFileStat(locOf(f.initializer).file, "total");
|
||||
const diagsBefore = lowerer.diags.length;
|
||||
const sinkMark = lowerer.diagSinkMark();
|
||||
try {
|
||||
const previousThis = lowerer.ctx.thisLocal;
|
||||
const receiver =
|
||||
@@ -4599,10 +4568,11 @@ function lowerStaticFieldInitsInner(lowerer: Lowerer, info: ClassInfo): IrStmt[]
|
||||
if (!(e instanceof PoisonError)) throw e;
|
||||
lowerer.stats.statementsFailed++;
|
||||
lowerer.bumpFileStat(locOf(f.initializer).file, "failed");
|
||||
if (isJsSourceFile(f.initializer.getSourceFile()) && lowerer.diagSink === null) {
|
||||
const fence = lowerer.deferToRuntimeFence(diagsBefore, f.initializer, {
|
||||
kind: "statement",
|
||||
});
|
||||
if (isJsSourceFile(f.initializer.getSourceFile())) {
|
||||
const fence =
|
||||
lowerer.diagSink === null
|
||||
? lowerer.deferToRuntimeFence(diagsBefore, f.initializer, { kind: "statement" })
|
||||
: lowerer.capturedRuntimeFence(sinkMark, f.initializer);
|
||||
if (fence) out.push(fence);
|
||||
}
|
||||
}
|
||||
@@ -6289,33 +6259,6 @@ function overrideReturnAbi(
|
||||
return declared;
|
||||
}
|
||||
|
||||
/** Field kinds whose early reads yield the undefined value (earlyReadFields). */
|
||||
const PRIMITIVE_EARLY_READS: ReadonlySet<IrType["kind"]> = new Set([
|
||||
"f64",
|
||||
"bool",
|
||||
"string",
|
||||
"bigint",
|
||||
]);
|
||||
|
||||
/** Field types whose slot can admit undefined for reads before the field's
|
||||
* assignment. Date values are scalars, so their unassigned slot is memory
|
||||
* safe; checked-dynamic slots already hold undefined. */
|
||||
function earlyReadable(lowerer: Lowerer, type: IrType): boolean {
|
||||
if (isUnitType(type)) return false;
|
||||
switch (type.kind) {
|
||||
case "dyn":
|
||||
case "void":
|
||||
case "date":
|
||||
case "generator":
|
||||
case "caught":
|
||||
return false;
|
||||
case "object":
|
||||
return lowerer.classes.get(type.className)?.def.runtime !== true;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/** The nearest declaration of `name` at or above `info` — the method a
|
||||
* receiver of that static class runs when nothing below overrides it. */
|
||||
export function findMethodOn(
|
||||
@@ -6502,7 +6445,7 @@ export function collectInheritedJsFieldWrites(
|
||||
* hierarchy identifies the original declarations through aliases as well. */
|
||||
export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.SourceFile[]): void {
|
||||
const families: [ts.MethodDeclaration, ts.MethodDeclaration][] = [];
|
||||
collectClassMethodMutations(lowerer, files);
|
||||
lowerer.speculativeTypeQueries(() => collectClassMethodMutations(lowerer, files));
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) {
|
||||
const accessors = node.members.filter(
|
||||
@@ -7703,6 +7646,7 @@ export function fieldInitStmts(lowerer: Lowerer, info: ClassInfo, thisLocal: IrL
|
||||
lowerer.stats.statementsTotal++;
|
||||
lowerer.bumpFileStat(locOf(f.initializer).file, "total");
|
||||
const diagsBefore = lowerer.diags.length;
|
||||
const sinkMark = lowerer.diagSinkMark();
|
||||
try {
|
||||
const value = lowerer.lowerExprExpecting(f.initializer, f.type);
|
||||
out.push({
|
||||
@@ -7717,10 +7661,11 @@ export function fieldInitStmts(lowerer: Lowerer, info: ClassInfo, thisLocal: IrL
|
||||
if (!(e instanceof PoisonError)) throw e;
|
||||
lowerer.stats.statementsFailed++;
|
||||
lowerer.bumpFileStat(locOf(f.initializer).file, "failed");
|
||||
if (isJsSourceFile(f.initializer.getSourceFile()) && lowerer.diagSink === null) {
|
||||
const fence = lowerer.deferToRuntimeFence(diagsBefore, f.initializer, {
|
||||
kind: "statement",
|
||||
});
|
||||
if (isJsSourceFile(f.initializer.getSourceFile())) {
|
||||
const fence =
|
||||
lowerer.diagSink === null
|
||||
? lowerer.deferToRuntimeFence(diagsBefore, f.initializer, { kind: "statement" })
|
||||
: lowerer.capturedRuntimeFence(sinkMark, f.initializer);
|
||||
if (fence) out.push(fence);
|
||||
}
|
||||
}
|
||||
@@ -7753,11 +7698,7 @@ function paramPropInitStmts(lowerer: Lowerer, info: ClassInfo, thisLocal: IrLoca
|
||||
obj: { kind: "varRef", localId: thisLocal.id, type: thisType, loc },
|
||||
className: info.def.name,
|
||||
field: pp.name,
|
||||
// An early-readable field's slot admits undefined (earlyReadFields).
|
||||
value: lowerer.coerceToExpected(
|
||||
{ kind: "varRef", localId: local!.id, type: local!.type, loc },
|
||||
info.fields.get(pp.name) ?? local!.type,
|
||||
),
|
||||
value: { kind: "varRef", localId: local!.id, type: local!.type, loc },
|
||||
loc,
|
||||
});
|
||||
}
|
||||
@@ -8526,8 +8467,22 @@ function assignedThisFieldType(lowerer: Lowerer, expr: ts.NewExpression): IrType
|
||||
return lowerer.currentClass?.fields.get(parent.left.name.text) ?? null;
|
||||
}
|
||||
|
||||
/** Map, Set and the error classes construct in lowerNew's identifier
|
||||
* branch, which a module member alias of one (`new mod.M()`) also takes. */
|
||||
function isCollectionOrErrorClass(ref: BuiltinClassRef | null): boolean {
|
||||
return ref?.kind === "collection" || ref?.kind === "error";
|
||||
}
|
||||
|
||||
export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
const loc = locOf(expr);
|
||||
// `new (require("./x").C)()`: the module's %init at the require's
|
||||
// position, then construction through the member as for a binding.
|
||||
if (ts.isPropertyAccessExpression(expr.expression) && !expr.expression.questionDotToken) {
|
||||
const req = inlineNamespaceRequireOf(lowerer, expr.expression.expression);
|
||||
if (req !== null && !lowerer.inlineRequireInitDone.has(req.call)) {
|
||||
return lowerWithInlineRequireInit(lowerer, req, () => lowerNew(lowerer, expr));
|
||||
}
|
||||
}
|
||||
const worker = lowerWorkerNew(lowerer, expr);
|
||||
if (worker) return worker;
|
||||
const readlineUi = lowerReadlineUiNew(lowerer, expr);
|
||||
@@ -8551,6 +8506,8 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
undefined,
|
||||
lowerer.checker.getSymbolAtLocation(expr.expression),
|
||||
);
|
||||
const valueReturning = lowerValueReturningConstructorNew(lowerer, expr);
|
||||
if (valueReturning) return valueReturning;
|
||||
const instanceConstructor = lowerInstanceConstructorNew(lowerer, expr);
|
||||
if (instanceConstructor) return instanceConstructor;
|
||||
const objectFactory = lowerObjectFactoryNew(lowerer, expr);
|
||||
@@ -8818,7 +8775,10 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
ts.isIdentifier(expr.expression) ||
|
||||
(ts.isPropertyAccessExpression(expr.expression) &&
|
||||
ts.isIdentifier(expr.expression.name) &&
|
||||
lowerer.isStdlibGlobal(expr.expression.expression, "globalThis"))
|
||||
(lowerer.isStdlibGlobal(expr.expression.expression, "globalThis") ||
|
||||
// `new mod.M()` over an exported `const M = Map` or `const E =
|
||||
// TypeError`
|
||||
isCollectionOrErrorClass(builtinClassRefOf(lowerer, expr.expression))))
|
||||
) {
|
||||
// `import C = N.C; new C()` — the alias's own source-order guards
|
||||
// (a no-op for every non-import= binding).
|
||||
@@ -9183,7 +9143,8 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
if (symbol?.name === "Array" && lowerer.isStdlibSymbol(symbol)) {
|
||||
return lowerArrayConstructor(lowerer, expr, expr.arguments ?? []);
|
||||
}
|
||||
if (symbol?.name === "Map" && lowerer.isStdlibSymbol(symbol)) {
|
||||
// A const alias (`const M = Map`) constructs the same Map.
|
||||
if (ts.isIdentifier(identifier) && lowerer.isStdlibGlobal(identifier, "Map")) {
|
||||
const seedArg = (expr.arguments?.length ?? 0) === 1 ? expr.arguments![0]! : null;
|
||||
const isPairLit = (el: ts.Expression): el is ts.ArrayLiteralExpression =>
|
||||
ts.isArrayLiteralExpression(el) &&
|
||||
@@ -9315,7 +9276,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
}
|
||||
// `new Set<T>()`: construct + bulk add from supported values,
|
||||
// including live collection cursors. Duplicates collapse in insertion order.
|
||||
if (symbol?.name === "Set" && lowerer.isStdlibSymbol(symbol)) {
|
||||
if (ts.isIdentifier(identifier) && lowerer.isStdlibGlobal(identifier, "Set")) {
|
||||
const tsType = lowerer.typeOf(expr);
|
||||
let mapped = lowerer.mapTypeOf(tsType);
|
||||
const fieldType = assignedThisFieldType(lowerer, expr);
|
||||
|
||||
@@ -11,6 +11,7 @@ import { lowerWorkerMetadata } from "./builtins/workers.js";
|
||||
import { esmImportBindingKind } from "./builtins/module-bindings.js";
|
||||
import { lowerReadlineProperty } from "./builtins/readline.js";
|
||||
import { lowerBlobInstanceof } from "./builtins/blob.js";
|
||||
import { lowerWebAssemblyTypeof, refuseWebAssemblyUse } from "./builtins/webassembly.js";
|
||||
import { lowerKeyObjectInstanceof } from "./builtins/key-object.js";
|
||||
import { lowerNetAddressInstanceof, lowerNetAddressStaticRead } from "./builtins/net-address.js";
|
||||
import { isExecExceptionType, refuseExecExceptionCode } from "./builtins/errors.js";
|
||||
@@ -35,14 +36,28 @@ import {
|
||||
isZlibStreamMember,
|
||||
lowerZlibInstanceOf,
|
||||
lowerZlibStreamBytesWritten,
|
||||
zlibClassAliasOf,
|
||||
zlibClassModeOfExpr,
|
||||
zlibClassName,
|
||||
} from "./builtins/compression.js";
|
||||
import {
|
||||
builtinClassAliasOwnsNoSlot,
|
||||
builtinClassRefOf,
|
||||
sameBuiltinClass,
|
||||
} from "./builtins/class-values.js";
|
||||
import { constituentTypes } from "../ts7/checker.js";
|
||||
import { dirname } from "node:path";
|
||||
import * as posix from "node:path/posix";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import {
|
||||
inlineNamespaceRequireOf,
|
||||
lowerWithInlineRequireInit,
|
||||
wholeExportValue,
|
||||
} from "./cjs-inline-require.js";
|
||||
import { tscInteropDefaultValueOf } from "./cjs-tsc-interop.js";
|
||||
import {
|
||||
cjsExportMemberWriteGlobalOf,
|
||||
cjsInFileExportGlobalOf,
|
||||
isCjsExportsIdentifier,
|
||||
lowerCjsExportsObjectValue,
|
||||
} from "./cjs-exports-object.js";
|
||||
import { checkedClassAssertion, checkedClassUnionAssertion } from "./class-assertions.js";
|
||||
import { checkedArmExtraction } from "./coercions/unions.js";
|
||||
import { narrowClassUnion, narrowStoredClassValue } from "./class-unions.js";
|
||||
@@ -203,7 +218,9 @@ import { tryLowerExpression } from "./expressions/try-lower-expression.js";
|
||||
import {
|
||||
fenceNodeModuleMutation,
|
||||
isNodeModuleValue,
|
||||
lowerCjsTopLevelThis,
|
||||
lowerNodeModuleIdentifier,
|
||||
isCjsModuleGlobal,
|
||||
lowerNodeModuleProperty,
|
||||
lowerRequireCacheElement,
|
||||
lowerRequireCacheHas,
|
||||
@@ -227,6 +244,7 @@ import {
|
||||
} from "./class-prototypes.js";
|
||||
import { isClassCallback } from "./class-callbacks.js";
|
||||
import {
|
||||
builtinConstantsObject,
|
||||
builtinPrototypeMethod,
|
||||
lowerArrayIsArrayValue,
|
||||
lowerCheckedPredicateValue,
|
||||
@@ -441,6 +459,8 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
if (externalTypeSpecifier !== null) {
|
||||
lowerer.externalHostFence(externalTypeSpecifier, expr);
|
||||
}
|
||||
// Every reached use of the WebAssembly API fences at its expression.
|
||||
refuseWebAssemblyUse(lowerer, expr);
|
||||
|
||||
if (ts.isNumericLiteral(expr)) {
|
||||
const value = Number(expr.text.replace(/_/g, ""));
|
||||
@@ -606,6 +626,8 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
);
|
||||
}
|
||||
if (ts.isTypeOfExpression(expr)) {
|
||||
const webAssembly = lowerWebAssemblyTypeof(lowerer, expr.expression);
|
||||
if (webAssembly) return webAssembly;
|
||||
if (lowerer.isStdlibGlobal(expr.expression, "process"))
|
||||
return { kind: "strLit", value: "object", type: STRING, loc };
|
||||
if (lowerer.isStdlibGlobal(expr.expression, "fetch"))
|
||||
@@ -615,8 +637,9 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
if (isNodeModuleValue(lowerer, expr.expression)) {
|
||||
return { kind: "strLit", value: "object", type: STRING, loc };
|
||||
}
|
||||
// `typeof zlib.Gzip`, `typeof C` over a zlib class alias.
|
||||
if (zlibClassModeOfExpr(lowerer, expr.expression) !== null) {
|
||||
// `typeof EventEmitter`, `typeof zlib.Gzip`, `typeof C` over a
|
||||
// builtin class alias.
|
||||
if (builtinClassRefOf(lowerer, expr.expression) !== null) {
|
||||
return { kind: "strLit", value: "function", type: STRING, loc };
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(expr.expression)) {
|
||||
@@ -690,6 +713,30 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
if (!sym) {
|
||||
return { kind: "strLit", value: "undefined", type: STRING, loc };
|
||||
}
|
||||
// `const { inspect } = require("util")` / `import { inspect } from
|
||||
// "node:util"`: a binding of a builtin module's exported function,
|
||||
// class or object. JavaScript sources represent such a value without
|
||||
// its own lowering as an identity token (a string), so the operand's
|
||||
// representation cannot answer. Node's answer follows the export's
|
||||
// kind, and the binding read has no effects.
|
||||
const imported = isJsSourceFile(tested.getSourceFile())
|
||||
? lowerer.builtinImportOf(tested)
|
||||
: null;
|
||||
if (
|
||||
imported &&
|
||||
builtinModuleConstOf(lowerer, imported.module, imported.member) === undefined
|
||||
) {
|
||||
const t = lowerer.typeOf(tested);
|
||||
if (
|
||||
lowerer.checker.getCallSignatures(t).length > 0 ||
|
||||
lowerer.checker.getConstructSignatures(t).length > 0
|
||||
) {
|
||||
return { kind: "strLit", value: "function", type: STRING, loc };
|
||||
}
|
||||
if ((t.flags & ts.TypeFlags.Object) !== 0) {
|
||||
return { kind: "strLit", value: "object", type: STRING, loc };
|
||||
}
|
||||
}
|
||||
if (lowerer.isStdlibSymbol(sym)) {
|
||||
const t = lowerer.typeOf(expr.expression);
|
||||
if (
|
||||
@@ -833,6 +880,10 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
|
||||
const moduleValue = lowerNodeModuleIdentifier(lowerer, expr);
|
||||
if (moduleValue) return moduleValue;
|
||||
if (expr.text === "exports") {
|
||||
const exportsValue = lowerCjsExportsObjectValue(lowerer, expr);
|
||||
if (exportsValue) return exportsValue;
|
||||
}
|
||||
if (lowerer.isSelfReference(expr)) {
|
||||
return { kind: "selfRef", type: lowerer.ctx.selfType!, loc };
|
||||
}
|
||||
@@ -877,13 +928,13 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
`a builtin function alias as an escaping value (call '${expr.text}' directly)`,
|
||||
);
|
||||
}
|
||||
// `const C = zlib.Gzip` owns no runtime slot: construction,
|
||||
// `const E = EventEmitter` owns no runtime slot: construction,
|
||||
// instanceof, typeof and .name lower at their use sites.
|
||||
if (sym && zlibClassAliasOf(lowerer, sym) !== null) {
|
||||
if (sym && builtinClassAliasOwnsNoSlot(lowerer, sym)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
`a zlib class alias as an escaping value (use '${expr.text}' with new, instanceof, typeof or .name)`,
|
||||
`a builtin class alias as an escaping value (use '${expr.text}' with new, instanceof, typeof or .name)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1228,6 +1279,17 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
if (bi.module === "os" && bi.member === "constants") {
|
||||
return { kind: "libCall", fn: "os.constants", args: [], type: DYN, loc };
|
||||
}
|
||||
// `import { O_RDONLY } from "constants"`: the member of the frozen
|
||||
// module object.
|
||||
if (bi.module === "constants") {
|
||||
return {
|
||||
kind: "dynKeyGet",
|
||||
value: builtinConstantsObject(lowerer, loc),
|
||||
key: { kind: "strLit", value: bi.member, type: STRING, loc },
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// tls.rootCertificates: a runtime-valued module constant (the
|
||||
// cached bundled-CA array) — the one member read that lowers
|
||||
// to a libCall instead of a baked literal.
|
||||
@@ -1270,6 +1332,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
// their access sites only. The CommonJS namespace binding
|
||||
// (`const lib = require("./lib.js")`) gets the same fence.
|
||||
const builtinNamespace = lowerer.builtinNamespaceModuleOf(expr);
|
||||
if (builtinNamespace === "constants") return builtinConstantsObject(lowerer, loc);
|
||||
if (builtinNamespace !== null) {
|
||||
const type: IrType = { kind: "moduleNs", moduleId: `builtin:${builtinNamespace}` };
|
||||
return { kind: "moduleNsRef", moduleId: type.moduleId, type, loc };
|
||||
@@ -1559,6 +1622,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
return { kind: "moduleNsRef", moduleId: moduleNs.moduleId, type: moduleNs, loc };
|
||||
}
|
||||
const builtinNs = lowerer.builtinNamespaceModuleOf(expr);
|
||||
if (builtinNs === "constants") return builtinConstantsObject(lowerer, loc);
|
||||
if (builtinNs !== null) {
|
||||
const type: IrType = { kind: "moduleNs", moduleId: `builtin:${builtinNs}` };
|
||||
return { kind: "moduleNsRef", moduleId: type.moduleId, type, loc };
|
||||
@@ -1661,6 +1725,9 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
// constant. TypeScript keeps the fence (noImplicitThis makes it a
|
||||
// compile-time story there).
|
||||
if (isJsSourceFile(expr.getSourceFile())) {
|
||||
// Module scope of a CommonJS file: `this` is module.exports.
|
||||
const exportsThis = lowerCjsTopLevelThis(lowerer, expr);
|
||||
if (exportsThis) return exportsThis;
|
||||
return { kind: "libCall", fn: "dyn.this", args: [], type: DYN, loc };
|
||||
}
|
||||
lowerer.unsupported("SC1080", expr);
|
||||
@@ -1808,6 +1875,28 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
if (ts.isConditionalExpression(expr)) return lowerTernary(lowerer, expr);
|
||||
|
||||
if (ts.isPropertyAccessExpression(expr)) {
|
||||
// `require("./x").member` over a program module's export table: the
|
||||
// module's %init at this position, then the member read exactly like
|
||||
// a `const m = require("./x")` binding's (cjs-inline-require.ts).
|
||||
if (!expr.questionDotToken) {
|
||||
const req = inlineNamespaceRequireOf(lowerer, expr.expression);
|
||||
if (req !== null && !lowerer.inlineRequireInitDone.has(req.call)) {
|
||||
return lowerWithInlineRequireInit(lowerer, req, () => lowerExpr(lowerer, expr));
|
||||
}
|
||||
// `module.exports` compared by identity inside its own module, or
|
||||
// refused by name as any other value (cjs-exports-object.ts).
|
||||
if (expr.name.text === "exports") {
|
||||
const exportsValue = lowerCjsExportsObjectValue(lowerer, expr);
|
||||
if (exportsValue) return exportsValue;
|
||||
}
|
||||
// tsc's `x_1.default` over a dependency whose whole export is one
|
||||
// value: that value (cjs-tsc-interop.ts).
|
||||
const interop = tscInteropDefaultValueOf(lowerer, expr);
|
||||
if (interop !== null) {
|
||||
const value = wholeExportValue(lowerer, interop.dep, expr);
|
||||
if (value !== null) return value;
|
||||
}
|
||||
}
|
||||
{
|
||||
const member = lifetimeRegionMemberOf(lowerer.program, expr, lowerer.experimentalFiles);
|
||||
if (member !== null)
|
||||
@@ -1821,10 +1910,11 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
const table = lowerInternTableProperty(lowerer, expr);
|
||||
if (table) return table;
|
||||
}
|
||||
// `zlib.Gzip.name`, `C.name` over a zlib class alias.
|
||||
// `EventEmitter.name`, `zlib.Gzip.name`, `C.name` over a builtin
|
||||
// class alias.
|
||||
if (expr.name.text === "name" && !expr.questionDotToken) {
|
||||
const mode = zlibClassModeOfExpr(lowerer, expr.expression);
|
||||
if (mode !== null) return { kind: "strLit", value: zlibClassName(mode), type: STRING, loc };
|
||||
const ref = builtinClassRefOf(lowerer, expr.expression);
|
||||
if (ref !== null) return { kind: "strLit", value: ref.name, type: STRING, loc };
|
||||
}
|
||||
// Expando statics may still be absent even when JS inference sees
|
||||
// the eventual assignment's scalar type. Preserve the checked value.
|
||||
@@ -2153,35 +2243,12 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
// global (pure alias plumbing) and fall through unchanged; write
|
||||
// positions belong to the export-assignment machinery.
|
||||
{
|
||||
const sf = expr.getSourceFile();
|
||||
const cjsMemberRecv =
|
||||
isModuleExportsAccess(expr.expression) ||
|
||||
(ts.isIdentifier(expr.expression) &&
|
||||
expr.expression.text === "exports" &&
|
||||
!lowerer.resolveLocal(expr.expression) &&
|
||||
!lowerer.globalOf(expr.expression));
|
||||
const writePos =
|
||||
ts.isBinaryExpression(expr.parent) &&
|
||||
expr.parent.left === expr &&
|
||||
expr.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken;
|
||||
if (
|
||||
!expr.questionDotToken &&
|
||||
cjsMemberRecv &&
|
||||
!writePos &&
|
||||
isCjsJsFile(sf, lowerer.program)
|
||||
) {
|
||||
const sym =
|
||||
lowerer.checker.getSymbolAtLocation(expr.name) ??
|
||||
lowerer.cjsModuleExportSymbol(sf, expr.name.text);
|
||||
const resolved =
|
||||
sym !== undefined && sym.flags & ts.SymbolFlags.Alias
|
||||
? lowerer.checker.getAliasedSymbol(sym)
|
||||
: sym;
|
||||
const g =
|
||||
(sym ? lowerer.globalsBySymbol.get(sym) : undefined) ??
|
||||
(resolved ? lowerer.globalsBySymbol.get(resolved) : undefined);
|
||||
if (g) return { kind: "varRef", localId: g.id, type: g.type, loc };
|
||||
}
|
||||
const g = cjsInFileExportGlobalOf(lowerer, expr);
|
||||
if (g && !writePos) return { kind: "varRef", localId: g.id, type: g.type, loc };
|
||||
}
|
||||
// Namespace-qualified reads (`N.x`, `A.B.C.f`, import= alias
|
||||
// chains): the member is a compile-time-known declaration of a
|
||||
@@ -2777,6 +2844,23 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
);
|
||||
}
|
||||
}
|
||||
// A member a JavaScript module attached to one of its functions
|
||||
// (`status.message = codes; module.exports = status`), read from
|
||||
// TypeScript: JavaScript stores such members on the function's own
|
||||
// property table (`fn.member = v` writes through the function's
|
||||
// checked-dynamic face), so the read takes the same path and enters
|
||||
// the static code at the type inference gave the member.
|
||||
if (recvLowered.type.kind === "func" && jsFunctionExpandoMember(lowerer, expr)) {
|
||||
const read: IrExpr = {
|
||||
kind: "dynKeyGet",
|
||||
value: lowerer.coerceToExpected(recvLowered, DYN),
|
||||
key: { kind: "strLit", value: expr.name.text, type: STRING, loc: locOf(expr.name) },
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
const mapped = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
||||
return mapped && mapped.kind !== "void" ? lowerer.coerceInto(expr, read, mapped) : read;
|
||||
}
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
@@ -3161,51 +3245,6 @@ export function lowerTernary(
|
||||
* the same union (unnarrowed use), to a SUB-union (partial narrowing —
|
||||
* unrepresentable without a re-tag), or to nothing (`never` in an
|
||||
* exhaustive default) leaves the expression union-typed. */
|
||||
/** Uses that read an early-read field's value as Node does when it is
|
||||
* undefined: printing, templates, operators, tests, member access, calls of
|
||||
* program functions (whose parameters the optional-value analysis widens),
|
||||
* and stores into bindings, fields and returns. Other uses, such as library
|
||||
* arguments, extract the declared type with a check. */
|
||||
function observesEarlyRead(node: ts.Node): boolean {
|
||||
let child = node;
|
||||
let parent = node.parent;
|
||||
while (parent && ts.isParenthesizedExpression(parent)) {
|
||||
child = parent;
|
||||
parent = parent.parent;
|
||||
}
|
||||
if (!parent) return false;
|
||||
if (
|
||||
ts.isTemplateSpan(parent) ||
|
||||
ts.isBinaryExpression(parent) ||
|
||||
ts.isPrefixUnaryExpression(parent) ||
|
||||
ts.isPostfixUnaryExpression(parent) ||
|
||||
ts.isTypeOfExpression(parent) ||
|
||||
ts.isConditionalExpression(parent) ||
|
||||
ts.isIfStatement(parent) ||
|
||||
ts.isWhileStatement(parent) ||
|
||||
ts.isReturnStatement(parent) ||
|
||||
ts.isVariableDeclaration(parent) ||
|
||||
ts.isPropertyAssignment(parent) ||
|
||||
ts.isArrayLiteralExpression(parent) ||
|
||||
(ts.isPropertyAccessExpression(parent) && parent.expression === child) ||
|
||||
(ts.isElementAccessExpression(parent) && parent.expression === child)
|
||||
)
|
||||
return true;
|
||||
if (ts.isCallExpression(parent) && parent.arguments.includes(child as ts.Expression)) {
|
||||
const callee = parent.expression;
|
||||
if (
|
||||
ts.isPropertyAccessExpression(callee) &&
|
||||
ts.isIdentifier(callee.expression) &&
|
||||
callee.expression.text === "console"
|
||||
)
|
||||
return true;
|
||||
return (
|
||||
!ts.isPropertyAccessExpression(callee) || callee.expression.kind === ts.SyntaxKind.ThisKeyword
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrExpr {
|
||||
if (
|
||||
(expr.type.kind === "union" || expr.type.kind === "dyn") &&
|
||||
@@ -3416,25 +3455,6 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx
|
||||
if (expr.type.kind !== "union") return expr;
|
||||
if (ts.isPropertyAccessExpression(node) && isOptionalProcessStreamProperty(lowerer, node))
|
||||
return expr;
|
||||
// A field read before its assignment holds undefined although its
|
||||
// declared type omits it; the declared type is no narrowing proof. A flow
|
||||
// narrowing past the declared type (`if (this.x !== null)`) extracts the
|
||||
// narrowed arm with a check, since the slot may still hold undefined.
|
||||
if (
|
||||
expr.kind === "fieldGet" &&
|
||||
lowerer.classes.get(expr.className)?.earlyReadFields?.has(expr.field) === true
|
||||
) {
|
||||
const symbol = ts.isPropertyAccessExpression(node)
|
||||
? lowerer.checker.getSymbolAtLocation(node.name)
|
||||
: undefined;
|
||||
const declared = symbol ? lowerer.mapTypeOf(lowerer.checker.getTypeOfSymbol(symbol)) : null;
|
||||
const narrowedArm = lowerer.mapTypeOf(lowerer.typeOf(node));
|
||||
if (narrowedArm === null || narrowedArm.kind === "union") return expr;
|
||||
if (declared !== null && typeEquals(declared, narrowedArm) && observesEarlyRead(node))
|
||||
return expr;
|
||||
const helper = lowerer.narrowedArmHelper(expr.type.unionId, narrowedArm, expr.loc);
|
||||
return helper ? lowerer.checkedNarrowCall(helper, expr, narrowedArm, expr.loc) : expr;
|
||||
}
|
||||
|
||||
// Represented fields and index values can include undefined even when
|
||||
// checker inference or lib.d.ts declares strings (RegExp captures).
|
||||
@@ -5082,6 +5102,34 @@ export function lowerIntrinsicProperty(
|
||||
return { kind: "libCall", fn: "dyn.arrayPrototype", args: [], type: DYN, loc: locOf(expr) };
|
||||
if (lowerer.stdlibGlobalMember(expr, "Object") === "prototype")
|
||||
return { kind: "libCall", fn: "dyn.objectPrototype", args: [], type: DYN, loc: locOf(expr) };
|
||||
// Function.prototype as a VALUE (a prototype-chain link:
|
||||
// `Object.setPrototypeOf(F.prototype, Function.prototype)`): one
|
||||
// anonymous function answering undefined, length 0 — a zero-capture
|
||||
// closure of a compiler helper, so every read is the same function
|
||||
// (closure identity) and shares its property table. Reads through it
|
||||
// find the function members the checked runtime models for every
|
||||
// function (apply, constructor, name, length). Member forms keep their
|
||||
// own lowerings.
|
||||
if (lowerer.stdlibGlobalMember(expr, "Function") === "prototype") {
|
||||
const loc = locOf(expr);
|
||||
const name = "%dyn.functionPrototype";
|
||||
if (!lowerer.liftedFns.some((fn) => fn.name === name))
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: [],
|
||||
locals: [],
|
||||
returnType: DYN,
|
||||
body: [{ kind: "return", value: dynUndefinedExpr(loc), loc }],
|
||||
loc,
|
||||
});
|
||||
return {
|
||||
kind: "dynFrom",
|
||||
value: { kind: "closure", fnName: name, captures: [], type: funcOf([], DYN), loc },
|
||||
fnName: "",
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (lowerer.stdlibGlobalMember(expr, "Array") === "isArray")
|
||||
return lowerArrayIsArrayValue(lowerer, locOf(expr));
|
||||
if (lowerer.stdlibGlobalMember(expr, "Array") === "from")
|
||||
@@ -10159,6 +10207,35 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
// RHS value is the expression's value — its OWN type, so the
|
||||
// consumer sees exactly what the checker typed.
|
||||
if (ts.isPropertyAccessExpression(expr.left) && !expr.left.questionDotToken) {
|
||||
// `const src = exports.src = []`: an export member assigned in
|
||||
// value position writes its export global and yields the value.
|
||||
{
|
||||
let inFunction = false;
|
||||
for (let p: ts.Node | undefined = expr.parent; p !== undefined; p = p.parent) {
|
||||
if (ts.isFunctionLike(p) || ts.isClassDeclaration(p) || ts.isClassExpression(p)) {
|
||||
inFunction = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const g = inFunction
|
||||
? cjsExportMemberWriteGlobalOf(lowerer, expr.left)
|
||||
: cjsInFileExportGlobalOf(lowerer, expr.left);
|
||||
if (g) {
|
||||
const value = lowerer.lowerExprExpecting(expr.right, g.type);
|
||||
const tmp = lowerer.declareHiddenLocal("%exportValue", value.type);
|
||||
const result = varRef(tmp.id, value.type, loc);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{ kind: "varDecl", localId: tmp.id, init: value, loc },
|
||||
{ kind: "assign", localId: g.id, value: result, loc },
|
||||
],
|
||||
result,
|
||||
type: value.type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
}
|
||||
const recv = tryLowerExpression(lowerer, expr.left.expression);
|
||||
if (recv && recv.type.kind === "dyn") return lowerDynMemberAssignment(lowerer, expr, recv);
|
||||
const unionWrite = lowerUnionFieldWrite(lowerer, expr.left, expr.right);
|
||||
@@ -10268,23 +10345,25 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
op === ts.SyntaxKind.ExclamationEqualsEqualsToken
|
||||
) {
|
||||
const test =
|
||||
lowerWebAssemblyTypeofTest(lowerer, expr, loc) ??
|
||||
lowerErrorCodeTypeofTest(lowerer, expr, loc) ??
|
||||
lowerer.lowerCaughtTypeofTest(expr, loc) ??
|
||||
lowerDynTypeofTest(lowerer, expr, loc) ??
|
||||
lowerUnionTypeofTest(lowerer, expr, loc);
|
||||
if (test) return test;
|
||||
}
|
||||
// `C === zlib.Gzip` between two zlib class references: the classes
|
||||
// are compile-time values, so the answer folds.
|
||||
// `C === EventEmitter` between two builtin class references: the
|
||||
// classes are compile-time values, so the answer folds.
|
||||
if (
|
||||
op === ts.SyntaxKind.EqualsEqualsEqualsToken ||
|
||||
op === ts.SyntaxKind.ExclamationEqualsEqualsToken
|
||||
) {
|
||||
const leftClass = zlibClassModeOfExpr(lowerer, expr.left);
|
||||
const rightClass = leftClass === null ? null : zlibClassModeOfExpr(lowerer, expr.right);
|
||||
const leftClass = builtinClassRefOf(lowerer, expr.left);
|
||||
const rightClass = leftClass === null ? null : builtinClassRefOf(lowerer, expr.right);
|
||||
if (leftClass !== null && rightClass !== null) {
|
||||
const negated = op === ts.SyntaxKind.ExclamationEqualsEqualsToken;
|
||||
return { kind: "boolLit", value: (leftClass === rightClass) !== negated, type: BOOL, loc };
|
||||
const same = sameBuiltinClass(leftClass, rightClass);
|
||||
return { kind: "boolLit", value: same !== negated, type: BOOL, loc };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11538,6 +11617,34 @@ function lowerLogicalPair(lowerer: Lowerer, expr: ts.BinaryExpression, left: IrE
|
||||
// same lift the union-operand path above takes, arriving here
|
||||
// with two plain arm values instead.
|
||||
const target = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
||||
// An always-truthy left operand (a record, object, array or function
|
||||
// value: every JS object is truthy): `o && x` evaluates `o` for its
|
||||
// effects and answers `x`; `o || x` answers `o` and never evaluates
|
||||
// `x`. supports-color's `stream && stream.isTTY` is the shape. Ahead
|
||||
// of the union lift below exactly where that lift would store the
|
||||
// object in a result union with no arm for it; a union holding the
|
||||
// operand keeps the lift.
|
||||
if (
|
||||
REF_TRUTHY_KINDS.has(kind) &&
|
||||
(target?.kind !== "union" ||
|
||||
(op === ts.SyntaxKind.AmpersandAmpersandToken &&
|
||||
lowerer.armTag(target.unionId, left.type) < 0))
|
||||
) {
|
||||
const and = op === ts.SyntaxKind.AmpersandAmpersandToken;
|
||||
const answer = and ? right : left;
|
||||
const answerT = target ?? answer.type;
|
||||
if (answerT.kind !== "void") {
|
||||
const value = lowerer.coerceInto(and ? expr.right : expr.left, answer, answerT);
|
||||
if (!and) return value;
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: left, loc }],
|
||||
result: value,
|
||||
type: answerT,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (target?.kind === "union") {
|
||||
lowerer.requireTruthyUnion(target.unionId, expr);
|
||||
return {
|
||||
@@ -11631,6 +11738,24 @@ function errorRootedObjectOf(lowerer: Lowerer, t: ts.Type): IrType | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** `typeof WebAssembly === "object"` and `typeof WebAssembly.<member> ===
|
||||
* "function"`: the operand needs no value, so the test folds as bare typeof
|
||||
* does (builtins/webassembly.ts) before any dyn-typed operand lowers. */
|
||||
function lowerWebAssemblyTypeofTest(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.BinaryExpression,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
const negated = expr.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsEqualsToken;
|
||||
for (const [a, b] of operandPairs(expr)) {
|
||||
if (!ts.isTypeOfExpression(a) || !ts.isStringLiteral(b)) continue;
|
||||
const folded = lowerWebAssemblyTypeof(lowerer, a.expression);
|
||||
if (folded?.kind !== "strLit") continue;
|
||||
return { kind: "boolLit", value: (folded.value === b.text) !== negated, type: BOOL, loc };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** `typeof e.code === "string"` / `"undefined"` (and the `!==` forms)
|
||||
* where e — through parens and as-casts — is a catch binding or a value
|
||||
* of %Error-rooted type: the runtime error's code slot is a string
|
||||
@@ -12328,15 +12453,16 @@ export function caughtLocalOf(lowerer: Lowerer, node: ts.Expression): IrLocal |
|
||||
* Folding is limited to side-effect-free operands — dropping a computed
|
||||
* operand would skip its effects, so those are rejected instead. */
|
||||
export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc: SrcLoc): IrExpr {
|
||||
refuseWebAssemblyUse(lowerer, expr.right);
|
||||
// `x instanceof net.Socket` over a union with a netSocket arm — the
|
||||
// h2 compat 'connect' narrowing (lower-server.ts): a union tag test.
|
||||
const sockTest = lowerSocketInstanceOf(lowerer, expr, loc);
|
||||
if (sockTest !== null) return sockTest;
|
||||
const zlibMode = zlibClassModeOfExpr(lowerer, expr.right);
|
||||
if (zlibMode !== null) return lowerZlibInstanceOf(lowerer, expr, zlibMode, loc);
|
||||
const rightClass = builtinClassRefOf(lowerer, expr.right);
|
||||
if (rightClass?.kind === "zlib") return lowerZlibInstanceOf(lowerer, expr, rightClass.mode, loc);
|
||||
|
||||
if (lowerer.isStdlibGlobal(expr.right, "Set") || lowerer.isStdlibGlobal(expr.right, "Map")) {
|
||||
const collection = lowerer.isStdlibGlobal(expr.right, "Map") ? "map" : "set";
|
||||
if (rightClass?.kind === "collection") {
|
||||
const collection = rightClass.which === "Map" ? "map" : "set";
|
||||
const value = lowerer.lowerExpr(expr.left);
|
||||
if (value.type.kind === "union") {
|
||||
const union = lowerer.unions.get(value.type.unionId);
|
||||
@@ -12684,42 +12810,68 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
|
||||
"'instanceof' right-hand sides other than classes declared in the program",
|
||||
);
|
||||
}
|
||||
// `x instanceof Writable`: Node's Writable[Symbol.hasInstance] also
|
||||
// answers true for streams with a writable side that do not inherit from
|
||||
// Writable, which are Duplex and its subclasses (Transform, PassThrough,
|
||||
// the zlib streams). Subclasses of Writable keep the prototype test.
|
||||
const duplex =
|
||||
target.def.name === "%Writable" && target.builtinStream !== undefined
|
||||
? lowerer.classes.get("%Duplex")
|
||||
: undefined;
|
||||
// The classes whose interval test can answer true for a value of static
|
||||
// class `className`: Writable and Duplex are disjoint, so a value typed
|
||||
// as one side needs only that side's test.
|
||||
const runtimeTests = (className: string): ClassInfo[] => {
|
||||
if (!duplex) return [target];
|
||||
const mayHold = (info: ClassInfo): boolean =>
|
||||
className === info.def.name ||
|
||||
lowerer.isSubclassOf(className, info.def.name) ||
|
||||
lowerer.isSubclassOf(info.def.name, className);
|
||||
const tests = [target, duplex].filter(mayHold);
|
||||
return tests.length > 0 ? tests : [target];
|
||||
};
|
||||
// A catch binding on the left: the runtime test against the class's
|
||||
// preorder interval (false for non-hierarchy-object payloads). tsc's
|
||||
// narrowing then types the branches; reads bridge through caughtRead.
|
||||
const caughtLocal = lowerer.caughtLocalOf(expr.left);
|
||||
if (caughtLocal) {
|
||||
if (!lowerer.inHierarchy(target)) {
|
||||
const value = varRef(caughtLocal.id, CAUGHT, loc);
|
||||
return {
|
||||
kind: "ternary",
|
||||
cond: {
|
||||
kind: "caughtTest",
|
||||
value,
|
||||
test: "instanceof",
|
||||
className: target.def.name,
|
||||
const caughtInstanceOf = (info: ClassInfo): IrExpr => {
|
||||
if (!lowerer.inHierarchy(info)) {
|
||||
const value = varRef(caughtLocal.id, CAUGHT, loc);
|
||||
return {
|
||||
kind: "ternary",
|
||||
cond: {
|
||||
kind: "caughtTest",
|
||||
value,
|
||||
test: "instanceof",
|
||||
className: info.def.name,
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: { kind: "boolLit", value: true, type: BOOL, loc },
|
||||
else_: classInstanceOf(
|
||||
lowerer,
|
||||
{ kind: "caughtToDyn", value, type: DYN, loc },
|
||||
info,
|
||||
loc,
|
||||
),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: { kind: "boolLit", value: true, type: BOOL, loc },
|
||||
else_: classInstanceOf(
|
||||
lowerer,
|
||||
{ kind: "caughtToDyn", value, type: DYN, loc },
|
||||
target,
|
||||
loc,
|
||||
),
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: "caughtTest",
|
||||
value: { kind: "varRef", localId: caughtLocal.id, type: CAUGHT, loc },
|
||||
test: "instanceof",
|
||||
className: info.def.name,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: "caughtTest",
|
||||
value: { kind: "varRef", localId: caughtLocal.id, type: CAUGHT, loc },
|
||||
test: "instanceof",
|
||||
className: target.def.name,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
const test = caughtInstanceOf(target);
|
||||
return duplex
|
||||
? { kind: "logical", op: "||", left: test, right: caughtInstanceOf(duplex), type: BOOL, loc }
|
||||
: test;
|
||||
}
|
||||
const left = lowerer.lowerExpr(expr.left);
|
||||
// `u instanceof Error` on an `unknown` value: the checked-dynamic tree's error encoding
|
||||
@@ -12799,23 +12951,33 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
|
||||
const lhsInfo = lowerer.classes.get(arm.className);
|
||||
if (!lhsInfo)
|
||||
throw new InternalCompilerError(`lowerer bug: unknown class ${arm.className}`);
|
||||
const result: IrExpr =
|
||||
lowerer.inHierarchy(lhsInfo) && lowerer.inHierarchy(target)
|
||||
const armTest = (info: ClassInfo): IrExpr =>
|
||||
lowerer.inHierarchy(lhsInfo) && lowerer.inHierarchy(info)
|
||||
? {
|
||||
kind: "instanceOf",
|
||||
value: { kind: "unionNarrow", unionId, tag, value, type: arm, loc },
|
||||
className: target.def.name,
|
||||
className: info.def.name,
|
||||
type: BOOL,
|
||||
loc,
|
||||
}
|
||||
: {
|
||||
kind: "boolLit",
|
||||
value:
|
||||
arm.className === target.def.name ||
|
||||
lowerer.isSubclassOf(arm.className, target.def.name),
|
||||
arm.className === info.def.name ||
|
||||
lowerer.isSubclassOf(arm.className, info.def.name),
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
const result = runtimeTests(arm.className)
|
||||
.map(armTest)
|
||||
.reduce((left: IrExpr, right: IrExpr): IrExpr => ({
|
||||
kind: "logical",
|
||||
op: "||",
|
||||
left,
|
||||
right,
|
||||
type: BOOL,
|
||||
loc,
|
||||
}));
|
||||
body.push({
|
||||
kind: "if",
|
||||
cond: { kind: "unionIsTag", unionId, tag, negated: false, value, type: BOOL, loc },
|
||||
@@ -12861,11 +13023,39 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
|
||||
if (!lhsInfo)
|
||||
throw new InternalCompilerError(`lowerer bug: unknown class ${left.type.className}`);
|
||||
if (lowerer.inHierarchy(lhsInfo) && lowerer.inHierarchy(target)) {
|
||||
return { kind: "instanceOf", value: left, className: target.def.name, type: BOOL, loc };
|
||||
const tests = runtimeTests(left.type.className);
|
||||
if (tests.length === 1) {
|
||||
return { kind: "instanceOf", value: left, className: tests[0]!.def.name, type: BOOL, loc };
|
||||
}
|
||||
const local = lowerer.declareHiddenLocal("%writableTest", left.type);
|
||||
const ref = varRef(local.id, left.type, loc);
|
||||
const test = (className: string): IrExpr => ({
|
||||
kind: "instanceOf",
|
||||
value: ref,
|
||||
className,
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "varDecl", localId: local.id, init: left, loc }],
|
||||
result: {
|
||||
kind: "logical",
|
||||
op: "||",
|
||||
left: test(tests[0]!.def.name),
|
||||
right: test(tests[1]!.def.name),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
const leftClass = left.type.className;
|
||||
const isInstance = (className: string): boolean =>
|
||||
leftClass === className || lowerer.isSubclassOf(leftClass, className);
|
||||
const value =
|
||||
left.type.className === target.def.name ||
|
||||
lowerer.isSubclassOf(left.type.className, target.def.name);
|
||||
isInstance(target.def.name) || (duplex !== undefined && isInstance(duplex.def.name));
|
||||
if (left.kind === "varRef") {
|
||||
return readsClassAfter(lowerer, target, expr.right, [], {
|
||||
kind: "boolLit",
|
||||
@@ -14957,6 +15147,9 @@ export function fieldTarget(
|
||||
access: ts.PropertyAccessExpression,
|
||||
): FieldTarget | null {
|
||||
if (lowerer.cjsLocalModuleBindingOf(access.expression)) return null;
|
||||
// `module.exports` / `module.id`: CommonJS module surface, never a field.
|
||||
if (isCjsModuleGlobal(lowerer, access.expression)) return null;
|
||||
if (isCjsExportsIdentifier(lowerer, access.expression)) return null;
|
||||
if (lowerer.chainBlocked(access)) return null;
|
||||
if (
|
||||
access.name.text === "stackTraceLimit" &&
|
||||
@@ -15465,20 +15658,6 @@ export function fieldGetExpr(
|
||||
const helper = lowerer.deferredReadHelper(target.fieldType.unionId, inner, loc);
|
||||
if (helper) return { kind: "call", callee: helper, args: [read], type: inner, loc };
|
||||
}
|
||||
// An early-read field (ClassInfo.earlyReadFields) yields its undefined
|
||||
// where the use reads it as Node does; elsewhere the declared type is
|
||||
// extracted with a check.
|
||||
if (
|
||||
target.fieldType.kind === "union" &&
|
||||
lowerer.classes.get(target.className)?.earlyReadFields?.has(target.field) === true &&
|
||||
!observesEarlyRead(blame)
|
||||
) {
|
||||
const declared = lowerer.mapTypeOf(lowerer.typeOf(blame));
|
||||
if (declared && declared.kind !== "union") {
|
||||
const helper = lowerer.narrowedArmHelper(target.fieldType.unionId, declared, loc);
|
||||
if (helper) return lowerer.checkedNarrowCall(helper, read, declared, loc);
|
||||
}
|
||||
}
|
||||
return read;
|
||||
}
|
||||
return {
|
||||
@@ -16019,22 +16198,6 @@ function lowerFieldCompoundValue(
|
||||
if (op === "+" && read.type.kind === "string") {
|
||||
appended = lowerer.ensureString(rhs, rhsNode ?? access);
|
||||
value = { kind: "strConcat", left: read, right: appended, type: STRING, loc };
|
||||
} else if (
|
||||
op === "+" &&
|
||||
read.type.kind === "union" &&
|
||||
lowerer.stripUndefinedArm(read.type).kind === "string" &&
|
||||
target.container === "class" &&
|
||||
lowerer.classes.get(target.className)?.earlyReadFields?.has(target.field) === true
|
||||
) {
|
||||
// A string field read before its assignment holds undefined, which
|
||||
// concatenates as "undefined".
|
||||
value = {
|
||||
kind: "strConcat",
|
||||
left: lowerer.ensureString(read, access),
|
||||
right: lowerer.ensureString(rhs, rhsNode ?? access),
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
} else if (numericRead.type.kind === "f64" && numericRhs.type.kind === "f64") {
|
||||
value = { kind: "bin", op, left: numericRead, right: numericRhs, type: F64, loc };
|
||||
} else if (bigintValue) {
|
||||
@@ -16273,3 +16436,28 @@ function isBuiltinModuleValueDecl(decl: ts.Node): boolean {
|
||||
ts.isVariableDeclaration(decl))
|
||||
);
|
||||
}
|
||||
|
||||
/** True when `access` reads a member that JavaScript sources attach to a
|
||||
* function by assignment (`fn.member = value`): the receiver's type is
|
||||
* callable and every declaration of the member is such an assignment in a
|
||||
* JS file. */
|
||||
function jsFunctionExpandoMember(lowerer: Lowerer, access: ts.PropertyAccessExpression): boolean {
|
||||
if (access.questionDotToken || !ts.isIdentifier(access.name)) return false;
|
||||
const recvType = lowerer.typeOf(access.expression);
|
||||
if (lowerer.checker.getCallSignatures(recvType).length === 0) return false;
|
||||
const member = lowerer.checker.getPropertyOfType(recvType, access.name.text);
|
||||
const decls = member ? lowerer.checker.declarationsOf(member) : [];
|
||||
return (
|
||||
decls.length > 0 &&
|
||||
decls.every((d) => {
|
||||
if (!isJsSourceFile(d.getSourceFile())) return false;
|
||||
const assign = ts.isBinaryExpression(d) ? d : d.parent;
|
||||
return (
|
||||
assign !== undefined &&
|
||||
ts.isBinaryExpression(assign) &&
|
||||
assign.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
||||
ts.isPropertyAccessExpression(assign.left)
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -115,6 +115,16 @@ export function lowerGenMethodCall(
|
||||
if (call.arguments.some(ts.isSpreadElement)) {
|
||||
lowerer.unsupported("SC1090", call, "spread arguments");
|
||||
}
|
||||
// Iterator helpers and the built-in iterators have no throw method; an
|
||||
// iterator-typed value may hold one, so only a generator's own member
|
||||
// lowers.
|
||||
if (name === "throw" && !isGeneratorMember(lowerer, access)) {
|
||||
lowerer.noLowering(
|
||||
`${lowerer.checker.typeToString(lowerer.typeOf(access.expression))}.throw`,
|
||||
call,
|
||||
"throw() lowers on values typed as generators (iterator helpers and built-in iterators have no throw method)",
|
||||
);
|
||||
}
|
||||
const loc = locOf(call);
|
||||
const gen = lowerer.lowerExpr(access.expression);
|
||||
if (gen.type.kind !== "generator") return null;
|
||||
@@ -187,6 +197,23 @@ export function lowerGenMethodCall(
|
||||
return { kind: "genResume", mode: name, gen, arg, type: resultT, loc };
|
||||
}
|
||||
|
||||
/** Whether `access` names a member the lib declares on Generator or
|
||||
* AsyncGenerator itself. */
|
||||
function isGeneratorMember(lowerer: Lowerer, access: ts.PropertyAccessExpression): boolean {
|
||||
const member = lowerer.checker.getSymbolAtLocation(access.name);
|
||||
return (
|
||||
member !== undefined &&
|
||||
lowerer.checker
|
||||
.declarationsOf(member)
|
||||
.some(
|
||||
(d) =>
|
||||
ts.isInterfaceDeclaration(d.parent) &&
|
||||
(d.parent.name.text === "Generator" || d.parent.name.text === "AsyncGenerator") &&
|
||||
lowerer.isStdlibFile(d.getSourceFile()),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/** Extraction of a suspended resume's yield value out of the result
|
||||
* record's V slot, typed back to the yield channel: V IS the channel for
|
||||
* dyn (and for a union channel already equal to V); a non-union channel
|
||||
@@ -319,7 +346,7 @@ export function genValue(gen: IrExpr & { type: GenType }, loc: SrcLoc): IrExpr {
|
||||
return { kind: "genResume", mode: "value", gen, arg: null, type: gen.type.yieldT, loc };
|
||||
}
|
||||
|
||||
function generatorLoopBinding(
|
||||
export function generatorLoopBinding(
|
||||
lowerer: Lowerer,
|
||||
stmt: ts.ForOfStatement,
|
||||
value: IrExpr,
|
||||
@@ -572,7 +599,9 @@ export function lowerForOfGenerator(
|
||||
catchBody: null,
|
||||
catchLocalId: null,
|
||||
// IteratorClose: every exit that leaves the generator unfinished
|
||||
// closes it — finallys run; the .return() result is dropped.
|
||||
// closes it — finallys run; the .return() result is dropped, and
|
||||
// after a throw from the body an error from the close is too.
|
||||
dropFinallyErrors: true,
|
||||
finallyBody: [
|
||||
{
|
||||
kind: "if",
|
||||
|
||||
@@ -18,7 +18,9 @@ import { isRelativeSpecifier } from "../workspace-registry.js";
|
||||
import {
|
||||
canonicalBuiltinModule,
|
||||
cjsExportAssignmentOf,
|
||||
cjsExportChainOf,
|
||||
cjsExportDiscardReason,
|
||||
cjsExportTargetLiteral,
|
||||
entryPackageFilePredicate,
|
||||
isCjsJsFile,
|
||||
isJsSourceFile,
|
||||
@@ -26,6 +28,7 @@ import {
|
||||
locOf,
|
||||
makeCycleAdmission,
|
||||
orderedImportsOf,
|
||||
requireSpecOf,
|
||||
resolveImport,
|
||||
resolveNpmImport,
|
||||
} from "../program.js";
|
||||
@@ -36,6 +39,13 @@ import {
|
||||
withCycleInitFlags,
|
||||
} from "./cycle-initialization.js";
|
||||
import { markRequireEarlyBindings } from "./require-initialization.js";
|
||||
import {
|
||||
cjsTableMethodEntry,
|
||||
inlineProgramRequireOf,
|
||||
nestedCjsExportAssignmentsOf,
|
||||
requireNestedPatternsOf,
|
||||
} from "./cjs-inline-require.js";
|
||||
import { isDeadTscHelperDecl, tscInteropBindingOfDecl } from "./cjs-tsc-interop.js";
|
||||
import { collectJsonRequires, isJsonRequireDecl } from "./json-require.js";
|
||||
import {
|
||||
invalidJsonModuleDiag,
|
||||
@@ -80,7 +90,7 @@ import {
|
||||
createRequireProgramModuleDecl,
|
||||
} from "./builtins/module-bindings.js";
|
||||
import { isPromisifyCall, registerBuiltinCallableAlias } from "./builtins/callable-aliases.js";
|
||||
import { registerZlibClassAlias } from "./builtins/compression.js";
|
||||
import { registerBuiltinClassAlias } from "./builtins/class-values.js";
|
||||
import { stripTypeCasts } from "./builtins/arguments.js";
|
||||
import {
|
||||
bindingContextualGenericFnNodeOf,
|
||||
@@ -1391,6 +1401,11 @@ export function collectGlobals(
|
||||
deferJsCollectionDiags(lowerer, sf, diagsBefore);
|
||||
}
|
||||
};
|
||||
// Export members assigned in VALUE position at the module's top
|
||||
// level (`const src = exports.src = []`) are exports too.
|
||||
for (const nested of nestedCjsExportAssignmentsOf(stmt, cjs?.expr)) {
|
||||
registerExport(nested.name, nested.name.text, nested.value);
|
||||
}
|
||||
if (cjs?.kind === "member" && ts.isIdentifier(cjs.name)) {
|
||||
// `exports.C = C` naming a CLASS declaration: alias plumbing —
|
||||
// no storage (a class VALUE global would fence for
|
||||
@@ -1398,6 +1413,13 @@ export function collectGlobals(
|
||||
// symbol (cjsMemberExportClassSymbol) and the class registry
|
||||
// applies unchanged.
|
||||
if (lowerer.cjsMemberExportClassSymbol(cjs.expr) !== null) continue;
|
||||
// A chain `exports.a = exports.b = value`: every member is an
|
||||
// export holding the chain's one value.
|
||||
const chain = cjsExportChainOf(cjs.expr);
|
||||
if (chain !== null) {
|
||||
for (const nameNode of chain.names) registerExport(nameNode, nameNode.text, chain.value);
|
||||
continue;
|
||||
}
|
||||
registerExport(cjs.name, cjs.name.text, cjs.value);
|
||||
continue;
|
||||
}
|
||||
@@ -1429,7 +1451,22 @@ export function collectGlobals(
|
||||
// statement lowering evaluates the call exactly once at its
|
||||
// source position and assigns that slot. Unmappable calls retain
|
||||
// the existing statement-level fence.
|
||||
const factoryValued = ts.isCallExpression(rhs);
|
||||
// Any other computed value (`os.homedir || function () {…}`,
|
||||
// `cond ? a : b`, `new Registry()`, `ns.member`) is the same single
|
||||
// snapshot: Node stores whatever the expression produced. A
|
||||
// require() of a program module is not: the exporter re-exports
|
||||
// that module's exports, which requirers resolve through the
|
||||
// checker's alias (the statement lowers to the dep's %init).
|
||||
const factoryValued =
|
||||
!ts.isIdentifier(rhs) &&
|
||||
!ts.isClassExpression(rhs) &&
|
||||
!ts.isObjectLiteralExpression(rhs) &&
|
||||
!(
|
||||
requireSpecOf(rhs) !== null &&
|
||||
inlineProgramRequireOf(lowerer, rhs) !== null &&
|
||||
!sf.statements.some((s) => cjsExportAssignmentOf(s)?.kind === "member")
|
||||
) &&
|
||||
cjsExportTargetLiteral(rhs, sf) === null;
|
||||
if (cjsScalarLiteral(rhs) || fnValued || factoryValued) {
|
||||
const diagsBefore = lowerer.diags.length;
|
||||
try {
|
||||
@@ -1464,6 +1501,10 @@ export function collectGlobals(
|
||||
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
|
||||
registerExport(prop.name, prop.name.text, prop.initializer);
|
||||
}
|
||||
// A METHOD entry (`module.exports = { f(x) { … } }`) is an
|
||||
// expression-valued export whose value is the method's function.
|
||||
const method = cjsTableMethodEntry(prop);
|
||||
if (method !== null) registerExport(method.name, method.name.text, method.name);
|
||||
// other property forms fence in the statement lowering
|
||||
}
|
||||
continue;
|
||||
@@ -1527,6 +1568,16 @@ export function collectGlobals(
|
||||
collectNestedVarGlobals(lowerer, sf, stmt, tag);
|
||||
continue;
|
||||
}
|
||||
// tsc's interop bindings and its then-dead helper declarations own no
|
||||
// storage either (cjs-tsc-interop.ts).
|
||||
if (
|
||||
isJsSourceFile(sf) &&
|
||||
(isDeadTscHelperDecl(lowerer, stmt) ||
|
||||
(stmt.declarationList.declarations.length === 1 &&
|
||||
tscInteropBindingOfDecl(lowerer, stmt.declarationList.declarations[0]!) !== null))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
// CommonJS require declarations are alias plumbing (preflight owns
|
||||
// the module edges; resolveValueSymbol routes reads through to the
|
||||
// exporter's declarations): no storage, and the statement lowers to
|
||||
@@ -1549,6 +1600,38 @@ export function collectGlobals(
|
||||
stdlibGlobalAliasDecl(lowerer, decl.name, decl.initializer);
|
||||
}
|
||||
}
|
||||
// `const { codes: { ERR_X } } = require("./errors")`: the outer
|
||||
// keys stay alias plumbing, but a nested pattern destructures the
|
||||
// export's value at the require, so its bindings own storage like
|
||||
// any destructured declaration (lowerVarDeclarator assigns them).
|
||||
if (ts.isVariableStatement(stmt)) {
|
||||
const isLetList =
|
||||
(stmt.declarationList.flags & ts.NodeFlags.Let) !== 0 ||
|
||||
(stmt.declarationList.flags & ts.NodeFlags.BlockScoped) === 0;
|
||||
for (const decl of stmt.declarationList.declarations) {
|
||||
for (const element of requireNestedPatternsOf(decl)) {
|
||||
for (const nameNode of boundIdentifiersOf(element.name!)) {
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(nameNode);
|
||||
if (!symbol || lowerer.globalsBySymbol.has(symbol)) continue;
|
||||
const mapped = lowerer.mapTypeOf(lowerer.typeOf(nameNode));
|
||||
const type = mapped && mapped.kind !== "void" ? mapped : DYN;
|
||||
const g: IrGlobal = {
|
||||
id: `%g.${tag}${nameNode.text}`,
|
||||
name: nameNode.text,
|
||||
type,
|
||||
mutable: isLetList,
|
||||
source: bindingSource(nameNode),
|
||||
...((type.kind === "func" || type.kind === "dyn") && !isVarDeclared(decl)
|
||||
? { tdz: true as const }
|
||||
: {}),
|
||||
};
|
||||
lowerer.globalsBySymbol.set(symbol, g);
|
||||
lowerer.globalsList.push(g);
|
||||
if (isVarDeclared(decl)) noteVarGlobalEntryInit(lowerer, sf, g);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// `declare const __VERSION__: string` — AMBIENT: no storage exists
|
||||
@@ -1701,7 +1784,7 @@ export function collectGlobals(
|
||||
// resolution and value reads reuse the source's interned closure.
|
||||
if (isConst && registerOverloadedCallableAlias(lowerer, decl)) continue;
|
||||
if (isConst && registerBuiltinCallableAlias(lowerer, decl.name, decl.initializer)) continue;
|
||||
if (registerZlibClassAlias(lowerer, decl.name, decl.initializer)) continue;
|
||||
if (registerBuiltinClassAlias(lowerer, decl.name, decl.initializer)) continue;
|
||||
// `const f = (x) => ...` at file scope of an npm-static JS file
|
||||
// whose params are implicit-any: the implicit-monomorphization
|
||||
// twin of the generic binding above — no global exists; calls and
|
||||
@@ -2664,6 +2747,18 @@ export function lowerFileInit(
|
||||
loc: loc0,
|
||||
});
|
||||
}
|
||||
if (sf === lowerer.entry) header.push(...lowerer.punycodeLinkWarning());
|
||||
// Top-level `this` of a CommonJS module: its object exists before the
|
||||
// module's first statement (lowerCjsTopLevelThis).
|
||||
const exportsThis = lowerer.cjsExportsThis.get(sf);
|
||||
if (exportsThis) {
|
||||
header.push({
|
||||
kind: "assign",
|
||||
localId: exportsThis.id,
|
||||
value: { kind: "dynObjLit", fields: [], type: DYN, loc: loc0 },
|
||||
loc: loc0,
|
||||
});
|
||||
}
|
||||
for (const { stmt, dep } of orderedImportsOf(lowerer.program, sf)) {
|
||||
const npm = lowerer.npmInitActions.get(sf)?.get(stmt);
|
||||
if (npm !== undefined && npm.length > 0) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user