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:
@@ -140,7 +140,6 @@ Never hand-edit these files:
|
||||
- `internal/compatibility/generated/node-v24-internal.json`
|
||||
- `internal/compatibility/generated/node-v24-backlog.json`
|
||||
- `docs/src/generated/node-v24-compatibility.json`
|
||||
- `docs/src/generated/node-v24-compatibility-meta.json`
|
||||
|
||||
Change compiler decision sources or the implementation-owned compatibility manifests, then regenerate. Use `pnpm manifest` when compiler decision tables change and `pnpm node-compat` whenever the Node pin, compatibility manifests, compiler surface manifest, changes.
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
|
||||
| `numeric-kernels` | cpu | sieve, hashing, `number[][]` matmul, `Float64Array` |
|
||||
| `typed-array-scan`, `typed-array-scan-workers` | cpu | element-by-element scan loops over `Uint8Array`, `Uint16Array`, `Int32Array` and `Float64Array`; the second entry is a worker-enabled executable (it can start a worker), so each access also checks for shared storage |
|
||||
| `intern-table`, `intern-table-map`, `intern-table-1`, `intern-table-map-1` | cpu | worker threads interning the same type-like objects under string keys and records under number keys, then hits and misses: one shared `InternTable` per kind of key, or a module-level `Map` per thread (the `-map` entries), with 8 threads or 1 (the `-1` entries); under Node.js every thread has its own tables |
|
||||
| `thread-slots`, `thread-slots-cache` | cpu | worker threads reading one published type graph from an `InternTable` and keeping per-thread state for each type they visit: a `ThreadSlots` family, or a direct-mapped cache by id in front of a `Map` by id (the `-cache` entry); under Node.js both are per thread |
|
||||
| `atomics-contended` | cpu | four worker threads updating `SharedArrayBuffer` counters with `Atomics`: one shared counter, one counter per worker, a `compareExchange` loop, and `Atomics.wait`/`notify` handoffs |
|
||||
| `value-objects` | cpu | small immutable records and class instances: vector helpers, method chains, particle updates |
|
||||
| `value-records` | cpu | records that hold strings, spread state updates and destructured tuples: token helpers, an immutable reducer, tuple returns |
|
||||
|
||||
@@ -127,6 +127,26 @@
|
||||
"100000"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "thread-slots",
|
||||
"entry": "workloads/thread-slots.ts",
|
||||
"args": [
|
||||
"slots",
|
||||
"8",
|
||||
"50000",
|
||||
"20"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "thread-slots-cache",
|
||||
"entry": "workloads/thread-slots.ts",
|
||||
"args": [
|
||||
"cache",
|
||||
"8",
|
||||
"50000",
|
||||
"20"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "atomics-contended",
|
||||
"entry": "workloads/atomics-contended.ts",
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
// Per-thread state for shared objects: worker threads read the same
|
||||
// published type graph from an InternTable and keep per-thread state for
|
||||
// each type they visit (a resolved flag and a visit count), then walk the
|
||||
// graph many times. Arguments: the store ("slots" for a ThreadSlots family,
|
||||
// "cache" for a direct-mapped cache by id in front of a Map by id, the
|
||||
// layout programs use without slots), the thread count, the type count and
|
||||
// the number of walks. Under Node.js both stores are per-thread.
|
||||
import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
|
||||
import { InternTable, ThreadSlots } from "@scriptc/experimental";
|
||||
|
||||
class Type {
|
||||
readonly id: number;
|
||||
readonly members: readonly Type[];
|
||||
constructor(id: number, members: readonly Type[]) {
|
||||
this.id = id;
|
||||
this.members = members;
|
||||
}
|
||||
}
|
||||
|
||||
class TypeState {
|
||||
resolved = false;
|
||||
visits = 0;
|
||||
}
|
||||
|
||||
const types = new InternTable<number, Type>("bench.slot-types");
|
||||
const states = new ThreadSlots<Type, TypeState>("bench.type-states");
|
||||
|
||||
const CACHE = 1024;
|
||||
const cacheIds: number[] = new Array<number>(CACHE).fill(-1);
|
||||
const cacheStates: (TypeState | undefined)[] = new Array<TypeState | undefined>(CACHE).fill(
|
||||
undefined,
|
||||
);
|
||||
const byId = new Map<number, TypeState>();
|
||||
|
||||
function slotState(t: Type): TypeState {
|
||||
let state = states.get(t);
|
||||
if (state === undefined) {
|
||||
state = new TypeState();
|
||||
states.set(t, state);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function cachedState(t: Type): TypeState {
|
||||
const slot = t.id & (CACHE - 1);
|
||||
if (cacheIds[slot] === t.id) return cacheStates[slot]!;
|
||||
let state = byId.get(t.id);
|
||||
if (state === undefined) {
|
||||
state = new TypeState();
|
||||
byId.set(t.id, state);
|
||||
}
|
||||
cacheIds[slot] = t.id;
|
||||
cacheStates[slot] = state;
|
||||
return state;
|
||||
}
|
||||
|
||||
function typeOf(id: number): Type {
|
||||
return types.getOrInsertComputed(id, () => {
|
||||
const members: Type[] = [];
|
||||
if (id >= 3) members.push(typeOf(Math.floor(id / 3)));
|
||||
if (id >= 7) members.push(typeOf(id - 7));
|
||||
return new Type(id, members);
|
||||
});
|
||||
}
|
||||
|
||||
function run(useSlots: boolean, count: number, walks: number): string {
|
||||
const all: Type[] = [];
|
||||
for (let id = 0; id < count; id++) all.push(typeOf(id));
|
||||
// Visits follow member edges from a pseudo-random start, as a checker's
|
||||
// relation checks do: neighbours are often visited together.
|
||||
let seed = 1;
|
||||
let resolved = 0;
|
||||
let visits = 0;
|
||||
for (let w = 0; w < walks; w++) {
|
||||
for (let i = 0; i < count; i++) {
|
||||
seed = (seed * 48271) % 2147483647;
|
||||
let t = all[seed % count]!;
|
||||
for (let depth = 0; depth < 4; depth++) {
|
||||
const state = useSlots ? slotState(t) : cachedState(t);
|
||||
if (!state.resolved) {
|
||||
state.resolved = true;
|
||||
resolved++;
|
||||
}
|
||||
state.visits++;
|
||||
visits++;
|
||||
if (t.members.length === 0) break;
|
||||
t = t.members[depth % t.members.length]!;
|
||||
}
|
||||
}
|
||||
}
|
||||
return `${resolved} resolved, ${visits} visits`;
|
||||
}
|
||||
|
||||
if (isMainThread) {
|
||||
const store = process.argv[2] ?? "slots";
|
||||
const threads = Number(process.argv[3] ?? "8");
|
||||
const count = Number(process.argv[4] ?? "50000");
|
||||
const walks = Number(process.argv[5] ?? "20");
|
||||
const results: string[] = [];
|
||||
let pending = threads;
|
||||
for (let i = 0; i < threads; i++) {
|
||||
const worker = new Worker(new URL(import.meta.url), {
|
||||
workerData: { useSlots: store === "slots", count, walks },
|
||||
});
|
||||
worker.on("message", (text: string) => {
|
||||
results[i] = text;
|
||||
});
|
||||
worker.on("exit", () => {
|
||||
if (--pending === 0) {
|
||||
const same = results.every((r) => r === results[0]);
|
||||
console.log(`${threads} threads: ${results[0]}, same in every thread ${same}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const { useSlots, count, walks } = workerData as {
|
||||
useSlots: boolean;
|
||||
count: number;
|
||||
walks: number;
|
||||
};
|
||||
parentPort!.postMessage(run(useSlots, count, walks));
|
||||
}
|
||||
@@ -238,8 +238,99 @@ No lock is held while a callback runs. Two threads that miss the same key at the
|
||||
2. It does not compare identities or ids across threads.
|
||||
3. A thread does not depend on another thread's inserts: `size` and `has` reflect every thread's inserts in scriptc and the calling thread's under Node.js.
|
||||
|
||||
State that a thread computes about a shared value later (lazily resolved members, caches) belongs in thread-local state, such as a module-level `Map` keyed by the value or an array indexed by an id the value carries: module state is per thread in both runtimes. In scriptc an array indexed by a dense id is the fastest of these.
|
||||
State that a thread computes about a shared value later (lazily resolved members, caches) belongs in thread-local state, such as a [`ThreadSlots`](#threadslots) family, a module-level `Map` keyed by the value or an array indexed by an id the value carries: module state is per thread in both runtimes. In scriptc an array indexed by a dense id is the fastest of these.
|
||||
|
||||
### Memory
|
||||
|
||||
Lookups take no lock and write nothing. Inserts lock one of 64 segments of the table for a probe and a few stores. Stored values are immortal in scriptc builds: they stay allocated until the process exits, including a value a callback computed for a key that another thread stored first. Programs that never construct an `InternTable` do not run any of its code.
|
||||
|
||||
## ThreadSlots
|
||||
|
||||
Threads that share values through an `InternTable` often compute more about each shared value later: resolved members, cached results, flags. That state differs per thread, so it cannot live in the shared value, which is immutable. A `ThreadSlots` family holds one value per key and thread, like a `WeakMap` that each thread has its own copy of. A thread sees only the values it set, including for keys that every thread shares.
|
||||
|
||||
```ts:slots.ts
|
||||
import { Worker, isMainThread, parentPort, threadId } from "node:worker_threads";
|
||||
import { InternTable, ThreadSlots } from "@scriptc/experimental";
|
||||
|
||||
class Shape {
|
||||
readonly name: string;
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
class ShapeState {
|
||||
uses = 0;
|
||||
readonly thread: number;
|
||||
constructor(thread: number) {
|
||||
this.thread = thread;
|
||||
}
|
||||
}
|
||||
|
||||
const shapes = new InternTable<string, Shape>("example.slot-shapes");
|
||||
// Module scope: every thread that evaluates this line opens the same family.
|
||||
const states = new ThreadSlots<Shape, ShapeState>("example.shape-states");
|
||||
|
||||
function use(name: string): ShapeState {
|
||||
const shape = shapes.getOrInsertComputed(name, () => new Shape(name));
|
||||
let state = states.get(shape);
|
||||
if (state === undefined) {
|
||||
state = new ShapeState(threadId);
|
||||
states.set(shape, state);
|
||||
}
|
||||
state.uses++;
|
||||
return state;
|
||||
}
|
||||
|
||||
if (isMainThread) {
|
||||
for (let i = 0; i < 2; i++) {
|
||||
new Worker(new URL(import.meta.url)).on("message", (text: string) => console.log(text));
|
||||
}
|
||||
} else {
|
||||
use("square");
|
||||
const state = use("square");
|
||||
parentPort!.postMessage(`worker: ${state.uses} uses, own state ${state.thread === threadId}`);
|
||||
}
|
||||
```
|
||||
|
||||
In the scriptc executable both workers read the same `Shape` object from the table, and each keeps its own `ShapeState` for it. Both runtimes print the same:
|
||||
|
||||
```console
|
||||
$ node slots.ts
|
||||
worker: 2 uses, own state true
|
||||
worker: 2 uses, own state true
|
||||
$ scriptc build slots.ts -o slots >/dev/null
|
||||
$ ./slots
|
||||
worker: 2 uses, own state true
|
||||
worker: 2 uses, own state true
|
||||
```
|
||||
|
||||
### Slot methods
|
||||
|
||||
<dl>
|
||||
<dt><code>new ThreadSlots<K, V>(name)</code></dt>
|
||||
<dd>Opens the family called <code>name</code>. Within one thread every construction with the same name refers to the same family.</dd>
|
||||
<dt><code>slots.get(key)</code>, <code>slots.has(key)</code></dt>
|
||||
<dd>Read the calling thread's value for <code>key</code>.</dd>
|
||||
<dt><code>slots.set(key, value)</code></dt>
|
||||
<dd>Sets the calling thread's value for <code>key</code> and returns the slots. It never writes the key, so it works on published keys.</dd>
|
||||
<dt><code>slots.delete(key)</code></dt>
|
||||
<dd>Removes the calling thread's value for <code>key</code> and returns whether there was one.</dd>
|
||||
</dl>
|
||||
|
||||
Under Node.js each family is a `WeakMap` per thread.
|
||||
|
||||
### Keys and values
|
||||
|
||||
scriptc requires:
|
||||
|
||||
- the name to be a string literal, and every construction of one name to use the same type arguments;
|
||||
- the key type to be a class declared in the program; keys may be instances of its subclasses;
|
||||
- the value type to be a class, an object type, an array, a `Map` or a `Set`;
|
||||
- the methods to be called through a module-level `const` initialized with `new ThreadSlots<K, V>("name")`, as in the example. A family's storage is found by its name when the program is compiled.
|
||||
|
||||
### Slot cost
|
||||
|
||||
Each instance of a key class, and of every class in its hierarchy, carries a 4-byte slot index, 0 until a value is set for it. The first `set` for a key assigns the index; indices are never reused. `get` and `has` read the index, a page of the thread's family and the entry, with no lock, hash or atomic update. A family stores values in pages of 1024 entries, allocated as the thread sets values in them.
|
||||
|
||||
A family holds a reference to each of the thread's values. A value is released when it is replaced or deleted, when its key is freed, and when the thread exits. Keys that are freed with a [`LifetimeRegion`](#lifetimeregion) keep their values until the thread exits, a value that belongs to a region keeps the region allocated while the family holds it, and a value that refers to its own key keeps the key alive. Programs that never construct a `ThreadSlots` do not run any of its code, and their classes have no slot index.
|
||||
|
||||
@@ -1 +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.
|
||||
- **Iterator helpers:** `map`, `filter`, `take`, `drop`, `flatMap`, `toArray`, `forEach`, `reduce`, `some`, `every` and `find` compile on generators, on array, typed array, string, Map and Set iterators, and on the objects the helpers return. Helper objects support `.next()`, `.return()`, `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. In TypeScript, `h.return!()` and `h.return?.()` compile when `h` is a helper call or a `const` initialized with one. `flatMap` callbacks return arrays, generators, Sets or Maps; a string result throws Node's TypeError. `Iterator.from` accepts arrays, strings, typed arrays, Sets, Maps and iterator objects such as generators. In JavaScript, untyped array, string, Map and Set iterators answer the same methods. `Iterator.from` over an object with its own `next` method or over an untyped value, the `Iterator` global as a value, and printing typed helper objects 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`. 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.
|
||||
- **Map and Set:** Keys and elements support numbers and strings by value, and records, classes, arrays, functions, symbols, server handles, and unions of these reference types by identity. Map values may also be functions. `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.
|
||||
|
||||
@@ -3,3 +3,4 @@
|
||||
- **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.
|
||||
- **Timeouts:** when a test's `{ timeout }` elapses, its running subtests are cancelled, as in Node. Subtests do not take their parent's `timeout` as their own, where Node's do, and the subtests of a timed-out test with `{ concurrency }` above 1 run to completion before the test reports. A body that keeps running after its timeout and calls `t.test` again runs that subtest, where Node reports it as not started.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
`process.argv[0]` is `"scriptc"`, `process.argv[1]` is the binary's path, and program arguments begin at index 2. In programs that use `node:test`, a leading `--test-update-snapshots` argument is Node's snapshot update flag: it is kept out of `process.argv` and appears in `process.execArgv`, which is otherwise `[]`. `process.version` and `process.versions.node` report the Node.js release the runtime is verified against.
|
||||
|
||||
Uncaught exceptions print `Uncaught <value>` to stderr instead of Node.js's source excerpt and stack-trace block; thrown strings and numbers print their value. Exit codes and output written before the exception are preserved. Runtime errors expose `message` and Node's `code`, but not `errno`, `syscall`, or `path`.
|
||||
Uncaught exceptions print `Uncaught <value>` to stderr instead of Node.js's source excerpt and stack-trace block; thrown strings and numbers print their value. Exit codes and output written before the exception are preserved. Runtime errors expose `message` and Node's `code` as typed members. The other own properties Node gives them, such as a system error's `errno`, `syscall`, and `path`, a child process error's `status`, `stdout`, and `stderr`, or an assertion error's `actual` and `expected`, are readable through an untyped view of the error (for example a cast of a caught value) and appear in `util.inspect` output.
|
||||
|
||||
Errors carry no stack frames. `console.log` and `util.inspect` print an Error in Node's stackless form, `[TypeError: message]`, where Node prints `TypeError: message` followed by stack frames; the two agree when `Error.stackTraceLimit` is `0`.
|
||||
|
||||
@@ -1 +1 @@
|
||||
- **Null and optional values:** `undefined` and `null` compile as standalone values and union members. Optional record and class fields and optional, default, and rest parameters also compile.
|
||||
- **Null and optional values:** `undefined` and `null` compile as standalone values and union members. Optional record and class fields and optional, default, and rest parameters also compile, including rest parameters typed `unknown[]` or `any[]`, whose functions take any number of arguments. A function value with fixed parameters cannot be stored where such a variadic function type is expected.
|
||||
|
||||
@@ -1 +1 @@
|
||||
- **Unions:** Narrow a union before an operation whose members require incompatible native representations, such as reading `u.length` on `string | string[]`. Some union-to-union widening and unions combining function and data members are unsupported. Same-representation methods on class unions, `map` and `forEach` on array unions, and shared or joinable field reads compile. `console.log(u)` supports printing a whole union.
|
||||
- **Unions:** Narrow a union before an operation whose members require incompatible native representations, such as reading `u.length` on `string | string[]`. Some union-to-union widening and unions combining function and data members are unsupported. Same-representation methods on class unions, `map` and `forEach` on array unions, and shared or joinable field reads compile. `console.log(u)` supports printing a whole union. A union may hold one promise beside other members, such as `T | Promise<T>` or `Promise<R | void> | R | void`: `await`, an async function's `return`, `Promise.resolve`, and `instanceof Promise` act on the member it holds, with Node's microtask timing. A promise member beside a member that declares `then`, two promise members in one union, and `console.log` of a union holding a promise are unsupported.
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { createGeistdocs } from "@vercel/geistdocs/next";
|
||||
|
||||
const withGeistdocs = createGeistdocs();
|
||||
const legacySlugs = ["quickstart", "coverage", "dependencies", "ffi", "native-objects", "wasm", "platforms", "cli", "how-it-works", "limitations"];
|
||||
|
||||
// The compatibility page fetches its data under a content hash of the
|
||||
// committed artifact, so a deploy never serves a cached older copy, and
|
||||
// checks the row count it expects. Both are derived here at build time.
|
||||
const compatibilityArtifact = readFileSync(new URL("./src/generated/node-v24-compatibility.json", import.meta.url), "utf8");
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
env: {
|
||||
COMPATIBILITY_ARTIFACT_VERSION: createHash("sha256").update(compatibilityArtifact).digest("hex").slice(0, 20),
|
||||
COMPATIBILITY_ROW_COUNT: String(JSON.parse(compatibilityArtifact).rows.length),
|
||||
},
|
||||
agentRules: false,
|
||||
allowedDevOrigins: ["127.0.0.1"],
|
||||
distDir: process.env.NEXT_DIST_DIR || ".next",
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
// @refresh reset
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import compatibilityMeta from "@/generated/node-v24-compatibility-meta.json";
|
||||
|
||||
// Set by next.config.mjs from the committed artifact.
|
||||
const artifactVersion = process.env.COMPATIBILITY_ARTIFACT_VERSION;
|
||||
const rowCount = Number(process.env.COMPATIBILITY_ROW_COUNT);
|
||||
|
||||
type Status =
|
||||
| "supported"
|
||||
@@ -211,14 +214,14 @@ export function NodeCompatibilityMatrix() {
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
fetch(`/compatibility/data?v=${compatibilityMeta.artifactVersion}`, { cache: "no-store", signal: controller.signal })
|
||||
fetch(`/compatibility/data?v=${artifactVersion}`, { cache: "no-store", signal: controller.signal })
|
||||
.then((response) => {
|
||||
if (!response.ok) throw new Error(`compatibility data: ${response.status}`);
|
||||
return response.json() as Promise<Snapshot>;
|
||||
})
|
||||
.then((data) => {
|
||||
if (!Array.isArray(data.rows) || data.rows.length !== compatibilityMeta.rowCount) {
|
||||
throw new Error(`compatibility data is stale or malformed (expected ${compatibilityMeta.rowCount} rows)`);
|
||||
if (!Array.isArray(data.rows) || data.rows.length !== rowCount) {
|
||||
throw new Error(`compatibility data is stale or malformed (expected ${rowCount} rows)`);
|
||||
}
|
||||
setSnapshot(data);
|
||||
})
|
||||
@@ -281,7 +284,7 @@ export function NodeCompatibilityMatrix() {
|
||||
return <div className="flex h-full items-center justify-center p-6"><div className="rounded-xl border border-red-500/40 bg-red-100 p-4 text-sm text-red-900">The compatibility dataset could not be loaded.</div></div>;
|
||||
}
|
||||
if (!snapshot) {
|
||||
return <div className="flex h-full items-center justify-center p-6 text-sm text-gray-900">Loading {compatibilityMeta.rowCount.toLocaleString()} Node.js API rows…</div>;
|
||||
return <div className="flex h-full items-center justify-center p-6 text-sm text-gray-900">Loading {rowCount.toLocaleString()} Node.js API rows…</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"schemaVersion": 3,
|
||||
"nodeVersion": "24.22.0",
|
||||
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
|
||||
"artifactVersion": "efc18bcd8a7a03fd8622",
|
||||
"rowCount": 3880
|
||||
}
|
||||
+16
@@ -300,6 +300,22 @@
|
||||
{
|
||||
"text": "Memory",
|
||||
"id": "memory"
|
||||
},
|
||||
{
|
||||
"text": "ThreadSlots",
|
||||
"id": "threadslots"
|
||||
},
|
||||
{
|
||||
"text": "Slot methods",
|
||||
"id": "slot-methods"
|
||||
},
|
||||
{
|
||||
"text": "Keys and values",
|
||||
"id": "keys-and-values"
|
||||
},
|
||||
{
|
||||
"text": "Slot cost",
|
||||
"id": "slot-cost"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -151,8 +151,11 @@ test("compatibility page and data endpoints keep their existing contracts", asyn
|
||||
const response = await get("/compatibility/data");
|
||||
assert.equal(response.status, 200);
|
||||
const dataset = await response.json();
|
||||
const metadata = JSON.parse(await readFile(new URL("../src/generated/node-v24-compatibility-meta.json", import.meta.url), "utf8"));
|
||||
assert.equal(dataset.rows.length, metadata.rowCount);
|
||||
const artifact = JSON.parse(await readFile(new URL("../src/generated/node-v24-compatibility.json", import.meta.url), "utf8"));
|
||||
assert.equal(dataset.rows.length, artifact.rows.length);
|
||||
// The row count next.config.mjs derives reaches the page (React separates
|
||||
// adjacent text nodes with empty comments).
|
||||
assert.ok(html.replaceAll("<!-- -->", "").includes(`Loading ${artifact.rows.length.toLocaleString()} Node.js API rows`));
|
||||
assert.equal((await get("/node-compatibility")).status, 308);
|
||||
assert.equal((await get("/node-compatibility/data")).status, 308);
|
||||
});
|
||||
|
||||
@@ -24,7 +24,6 @@ const root = fileURLToPath(new URL("../../../", import.meta.url));
|
||||
const internalOutputPath = `${packageRoot}generated/node-v24-internal.json`;
|
||||
const backlogOutputPath = `${packageRoot}generated/node-v24-backlog.json`;
|
||||
const publicOutputPath = `${root}docs/src/generated/node-v24-compatibility.json`;
|
||||
const publicMetaOutputPath = `${root}docs/src/generated/node-v24-compatibility-meta.json`;
|
||||
const compilerManifestPath = `${root}packages/compiler/surface-manifest.json`;
|
||||
const compilerCompatPath = `${packageRoot}static-support.json`;
|
||||
const pinPath = `${packageRoot}node-v24.json`;
|
||||
@@ -1009,17 +1008,6 @@ function publicSnapshot(snapshot) {
|
||||
};
|
||||
}
|
||||
|
||||
function publicMetadata(publicRendered, snapshot) {
|
||||
const matrix = projectPublicMatrix(snapshot);
|
||||
return {
|
||||
schemaVersion: 3,
|
||||
nodeVersion: snapshot.nodeVersion,
|
||||
nodeCommit: snapshot.nodeCommit,
|
||||
artifactVersion: sha(publicRendered).slice(0, 20),
|
||||
rowCount: matrix.rows.length,
|
||||
};
|
||||
}
|
||||
|
||||
function checkLocal() {
|
||||
const snapshot = JSON.parse(readFileSync(internalOutputPath, "utf8"));
|
||||
if (snapshot.nodeVersion !== pin.version || snapshot.nodeCommit !== pin.commit) {
|
||||
@@ -1071,11 +1059,8 @@ function checkLocal() {
|
||||
if (readFileSync(backlogOutputPath, "utf8") !== render(buildBacklog(snapshot))) {
|
||||
throw new Error("the Node compatibility backlog is stale; run 'pnpm node-compat'");
|
||||
}
|
||||
const publicRendered = render(publicSnapshot(snapshot));
|
||||
if (
|
||||
readFileSync(publicMetaOutputPath, "utf8") !== render(publicMetadata(publicRendered, snapshot))
|
||||
) {
|
||||
throw new Error("the docs compatibility metadata is stale; run 'pnpm node-compat'");
|
||||
if (readFileSync(publicOutputPath, "utf8") !== render(publicSnapshot(snapshot))) {
|
||||
throw new Error("the docs compatibility artifact is stale; run 'pnpm node-compat'");
|
||||
}
|
||||
for (const row of snapshot.rows) {
|
||||
for (const key of ["static"]) {
|
||||
@@ -1141,7 +1126,6 @@ async function main() {
|
||||
const internalRendered = render(snapshot);
|
||||
const backlogRendered = render(buildBacklog(snapshot));
|
||||
const publicRendered = render(publicSnapshot(snapshot));
|
||||
const publicMetaRendered = render(publicMetadata(publicRendered, snapshot));
|
||||
|
||||
if (args.has("--check")) {
|
||||
if (readFileSync(internalOutputPath, "utf8") !== internalRendered)
|
||||
@@ -1150,8 +1134,6 @@ async function main() {
|
||||
throw new Error("Node compatibility backlog is stale; run 'pnpm node-compat'");
|
||||
if (readFileSync(publicOutputPath, "utf8") !== publicRendered)
|
||||
throw new Error("public Node compatibility snapshot is stale; run 'pnpm node-compat'");
|
||||
if (readFileSync(publicMetaOutputPath, "utf8") !== publicMetaRendered)
|
||||
throw new Error("public Node compatibility metadata is stale; run 'pnpm node-compat'");
|
||||
console.log(
|
||||
`Node compatibility snapshot matches ${pin.tag} (${snapshot.rows.length} API rows, commit ${pin.commit.slice(0, 12)})`,
|
||||
);
|
||||
@@ -1160,7 +1142,6 @@ async function main() {
|
||||
writeFileSync(internalOutputPath, internalRendered);
|
||||
writeFileSync(backlogOutputPath, backlogRendered);
|
||||
writeFileSync(publicOutputPath, publicRendered);
|
||||
writeFileSync(publicMetaOutputPath, publicMetaRendered);
|
||||
console.log(
|
||||
`wrote compatibility ledger, backlog, and public artifact (${snapshot.rows.length} source rows, ${projectPublicMatrix(snapshot).rows.length} public matrix rows, ${pin.tag})`,
|
||||
);
|
||||
|
||||
+7
-5
@@ -4868,17 +4868,16 @@ declare module "http" {
|
||||
/* The Agent lowers to a checked-dynamic HANDLE: option parsing at the
|
||||
* literal construction site, getName, destroy, the sockets/requests/
|
||||
* freeSockets snapshots, and REAL maxSockets accounting (over-limit
|
||||
* requests defer their dial and queue). keepAlive: true throws the
|
||||
* named pooling fence — this client dials one connection per request
|
||||
* and closes it with the response, so freeSockets is always
|
||||
* empty. */
|
||||
* requests defer their dial and queue). keepAlive: true keeps finished
|
||||
* connections in freeSockets for the next request to the same name,
|
||||
* taken in `scheduling` order. */
|
||||
export interface AgentOptions {
|
||||
keepAlive?: boolean;
|
||||
keepAliveMsecs?: number;
|
||||
maxSockets?: number;
|
||||
maxFreeSockets?: number;
|
||||
maxTotalSockets?: number;
|
||||
scheduling?: string;
|
||||
scheduling?: "fifo" | "lifo";
|
||||
timeout?: number;
|
||||
port?: number;
|
||||
[option: string]: unknown;
|
||||
@@ -4908,6 +4907,7 @@ declare module "http" {
|
||||
readonly freeSockets: { [key: string]: unknown[] };
|
||||
readonly requests: { [key: string]: unknown[] };
|
||||
}
|
||||
/* Node's keepAlive global agent for requests without an agent option. */
|
||||
export const globalAgent: Agent;
|
||||
export interface ClientRequest {
|
||||
readonly destroyed: boolean;
|
||||
@@ -5111,6 +5111,8 @@ declare module "https" {
|
||||
import { Server } from "http";
|
||||
import { Agent, ClientRequest, IncomingMessage, ServerResponse } from "http";
|
||||
export { Agent, Server };
|
||||
/* Node's keepAlive global agent for requests without an agent option. */
|
||||
export const globalAgent: Agent;
|
||||
export interface ServerOptions {
|
||||
cert: string | Uint8Array;
|
||||
key: string | Uint8Array;
|
||||
|
||||
@@ -97,16 +97,20 @@ export function computeCycleMutability(
|
||||
// can store; a commit rewrites every field, reference fields possibly
|
||||
// with fresh conversions). Without any such store in the program,
|
||||
// crossing into checked-dynamic code writes nothing. FFI calls and
|
||||
// intrinsics other than publish (which only marks objects immortal) and
|
||||
// the intern-table operations (which store published values) stay
|
||||
// conservative.
|
||||
// intrinsics other than publish (which only marks objects immortal), the
|
||||
// intern-table operations (which store published values) and ThreadSlots
|
||||
// reads stay conservative.
|
||||
const dynStores = dynamicFieldStores(mod);
|
||||
const dynamicStores = dynStores.all || dynStores.names.size > 0;
|
||||
result.emptyPropertyBags = !dynamicStores;
|
||||
const boundaryWrites = (e: IrExpr): boolean =>
|
||||
dynamicStores ||
|
||||
e.kind === "ffiCall" ||
|
||||
(e.kind === "intrinsic" && e.name !== "threads.publish" && !e.name.startsWith("itable."));
|
||||
(e.kind === "intrinsic" &&
|
||||
e.name !== "threads.publish" &&
|
||||
!e.name.startsWith("itable.") &&
|
||||
e.name !== "slots.get" &&
|
||||
e.name !== "slots.has");
|
||||
|
||||
// Types that can become checked-dynamic values: mark their whole closure.
|
||||
const exposedTypes = new Set<string>();
|
||||
|
||||
@@ -152,6 +152,97 @@ function declareGlobal(host: AllocHost, decl: string): void {
|
||||
if (host.threadLocalAlloc !== true) host.declare(decl);
|
||||
}
|
||||
|
||||
/** `scr_cyc_buffers` (scr_rc.h ScrCycBuffers): the three candidate buffers
|
||||
* `{ v, n, cap, used }`, the inline buffering window and the allocation
|
||||
* trigger. */
|
||||
const CYC_BUFFERS_TYPE = "{ [3 x { ptr, i64, i64, i64 }], i64, i64 }";
|
||||
/** Byte offsets in it (asserted in scr_rc.h). */
|
||||
const CYC_NURSERY_COUNT = 8;
|
||||
const CYC_WINDOW = 96;
|
||||
const CYC_ALLOC_TRIGGER = 104;
|
||||
|
||||
/** Whether emitted release paths can append nursery candidates to the
|
||||
* collector's buffer themselves (through {@link cycleAppendHelper}): where
|
||||
* the runtime keeps that state in ordinary globals (executables without
|
||||
* workers and classic libraries, on the 64-bit targets with inline
|
||||
* allocation). */
|
||||
export function inlineBufferingSupported(host: AllocHost): boolean {
|
||||
return host.inlineAlloc === true && host.threadLocalAlloc !== true && host.sizeType === "i64";
|
||||
}
|
||||
|
||||
/** The emitted symbol of {@link cycleAppendHelper}. */
|
||||
export const CYC_APPEND = "sc_cyc_append";
|
||||
|
||||
/** The runtime symbols {@link cycleAppendHelper} uses. The helper is
|
||||
* defined after the module's extern block, so its caller declares them. */
|
||||
export function cycleAppendDecls(): string[] {
|
||||
return [
|
||||
`@scr_cyc_buffers = external global ${CYC_BUFFERS_TYPE}`,
|
||||
`@scr_cyc_live = external global i64`,
|
||||
`declare void @scr_cyc_on_release(ptr)`,
|
||||
];
|
||||
}
|
||||
|
||||
/** The candidate-buffering step of every emitted release that leaves a
|
||||
* headered object alive and unbuffered: scr_rc.h's scr_cyc_append, then
|
||||
* scr_cyc_on_release when it declines. A nursery object goes onto the
|
||||
* nursery buffer while the buffer holds fewer entries than the runtime's
|
||||
* window and the live count is below the allocation trigger.
|
||||
*
|
||||
* One out-of-line definition per module keeps the release fast paths, which
|
||||
* LLVM inlines into their callers, as small as the plain call they had. It
|
||||
* uses preserve_mostcc, so the call clobbers almost no caller registers:
|
||||
* the callers' code around a release stays what it was, and the append
|
||||
* itself saves only the few registers it uses. Its slow path enters the
|
||||
* runtime through a second preserve_mostcc function, which saves the rest. */
|
||||
export function cycleAppendHelper(): string[] {
|
||||
return [
|
||||
`define internal preserve_mostcc void @${CYC_APPEND}(ptr %o) noinline #0 { ; scr_cyc_append, else scr_cyc_on_release`,
|
||||
`entry:`,
|
||||
` %genp = getelementptr i8, ptr %o, i64 -${CYC_HDR - CYC_GEN}`,
|
||||
` %gen = load i8, ptr %genp ; ScrCycHdr.gen`,
|
||||
` %young = icmp eq i8 %gen, 0`,
|
||||
` br i1 %young, label %window, label %call`,
|
||||
`window:`,
|
||||
` %np = getelementptr inbounds i8, ptr @scr_cyc_buffers, i64 ${CYC_NURSERY_COUNT}`,
|
||||
` %n = load i64, ptr %np ; nursery buffer entries`,
|
||||
` %wp = getelementptr inbounds i8, ptr @scr_cyc_buffers, i64 ${CYC_WINDOW}`,
|
||||
` %w = load i64, ptr %wp ; window`,
|
||||
` %room = icmp ult i64 %n, %w`,
|
||||
` br i1 %room, label %live, label %call`,
|
||||
`live:`,
|
||||
` %lv = load i64, ptr @scr_cyc_live`,
|
||||
` %tp = getelementptr inbounds i8, ptr @scr_cyc_buffers, i64 ${CYC_ALLOC_TRIGGER}`,
|
||||
` %t = load i64, ptr %tp ; allocation trigger`,
|
||||
` %due = icmp uge i64 %lv, %t`,
|
||||
` br i1 %due, label %call, label %append`,
|
||||
`append:`,
|
||||
` %v = load ptr, ptr @scr_cyc_buffers`,
|
||||
` %slot = getelementptr inbounds ptr, ptr %v, i64 %n`,
|
||||
` store ptr %o, ptr %slot`,
|
||||
` %n1 = add i64 %n, 1`,
|
||||
` store i64 %n1, ptr %np`,
|
||||
` %i = trunc i64 %n to i32`,
|
||||
` %b = or i32 %i, -2147483648 ; SCR_CYC_BUFFERED_BIT | position`,
|
||||
` %bufp = getelementptr i8, ptr %o, i64 -${CYC_HDR - CYC_BUF}`,
|
||||
` store i32 %b, ptr %bufp`,
|
||||
` ret void`,
|
||||
`call:`,
|
||||
` tail call preserve_mostcc void @${CYC_APPEND}_call(ptr %o)`,
|
||||
` ret void`,
|
||||
`}`,
|
||||
// The runtime call clobbers every caller-saved register, which this
|
||||
// preserve_mostcc function must restore: saving them here keeps those
|
||||
// saves out of the append path above.
|
||||
`define internal preserve_mostcc void @${CYC_APPEND}_call(ptr %o) noinline #0 {`,
|
||||
`entry:`,
|
||||
` call void @scr_cyc_on_release(ptr %o) ; buffer the candidate; may collect`,
|
||||
` ret void`,
|
||||
`}`,
|
||||
``,
|
||||
];
|
||||
}
|
||||
|
||||
/** A collector header to install: the object type's constant ScrCycType
|
||||
* descriptor (its trace and teardown; see cycTypeDescriptor). */
|
||||
export interface CycHeader {
|
||||
|
||||
@@ -75,6 +75,10 @@ export class LlClassMeta {
|
||||
membership: { pre: number; post: number }[] = [];
|
||||
/** Type-only references retain layouts without retaining construction. */
|
||||
constructorRetained = false;
|
||||
/** The hierarchy holds ThreadSlots keys (IrModule.threadSlotKeys): every
|
||||
* member's struct carries the i32 slot word before its fields
|
||||
* (classSlotWordIndex). */
|
||||
threadSlot = false;
|
||||
|
||||
constructor(readonly def: IrClassDef) {
|
||||
this.root = this;
|
||||
@@ -116,6 +120,12 @@ export function buildClassGraph(
|
||||
if (meta.base === null) number(meta, meta);
|
||||
meta.hierarchy = meta.base !== null || meta.children.length > 0;
|
||||
}
|
||||
const slotRoots = new Set<LlClassMeta>();
|
||||
for (const key of mod.threadSlotKeys ?? []) {
|
||||
const meta = metaMap.get(key);
|
||||
if (meta !== undefined) slotRoots.add(meta.root);
|
||||
}
|
||||
for (const meta of metaMap.values()) meta.threadSlot = slotRoots.has(meta.root);
|
||||
const declares = (m: LlClassMeta, method: string): boolean =>
|
||||
m.def.methods?.includes(method) ?? false;
|
||||
const declaredBelow = (m: LlClassMeta, method: string): boolean =>
|
||||
@@ -323,16 +333,31 @@ function runtimePrefixOf(meta: LlClassMeta): RuntimePrefix | null {
|
||||
return RUNTIME_PREFIXES.get(meta.root.def.name) ?? null;
|
||||
}
|
||||
|
||||
/** The GEP index where a class's own fields start: rc at 0, the vtable
|
||||
* word at 1 on hierarchy members, then a runtime prefix (state + display
|
||||
* name) on classes rooted at a runtime base, then the stream-state slot
|
||||
* on stream-rooted ones. */
|
||||
function fieldBase(meta: LlClassMeta): number {
|
||||
/** The GEP index after a class's header words: rc at 0, the vtable word
|
||||
* at 1 on hierarchy members, then a runtime prefix (state + display name)
|
||||
* on classes rooted at a runtime base, then the stream-state slot on
|
||||
* stream-rooted ones. */
|
||||
function headerEnd(meta: LlClassMeta): number {
|
||||
if (!meta.hierarchy) return 1;
|
||||
if (streamRooted(meta)) return 5;
|
||||
return runtimePrefixOf(meta) ? 4 : 2;
|
||||
}
|
||||
|
||||
/** The GEP index of the ThreadSlots slot word (an i32) in a class whose
|
||||
* hierarchy holds ThreadSlots keys: right after the header words, so every
|
||||
* member of the hierarchy has it at the same offset. */
|
||||
export function classSlotWordIndex(meta: LlClassMeta): number {
|
||||
if (!meta.threadSlot)
|
||||
throw new InternalCompilerError(`llvm emitter bug: class ${meta.def.name} has no slot word`);
|
||||
return headerEnd(meta);
|
||||
}
|
||||
|
||||
/** The GEP index where a class's own fields start: after the header words
|
||||
* and the slot word. */
|
||||
export function classFieldBase(meta: LlClassMeta): number {
|
||||
return headerEnd(meta) + (meta.threadSlot ? 1 : 0);
|
||||
}
|
||||
|
||||
/** Append the lexical environment after inherited fields to preserve base layouts. */
|
||||
export function classEnvironmentIndex(meta: LlClassMeta): number {
|
||||
return classFieldIndex(meta, `%classEnvironment:${meta.def.name}`).index;
|
||||
@@ -346,7 +371,28 @@ export function classFieldIndex(meta: LlClassMeta, field: string): { index: numb
|
||||
throw new InternalCompilerError(
|
||||
`llvm emitter bug: unknown field ${field} on class ${meta.def.name}`,
|
||||
);
|
||||
return { index: fieldBase(meta) + idx, type: meta.def.fields[idx]!.type };
|
||||
return { index: classFieldBase(meta) + idx, type: meta.def.fields[idx]!.type };
|
||||
}
|
||||
|
||||
/** Lines that release the ThreadSlots values of the object `%o` before it
|
||||
* is freed: its slot word is 0 unless a value was ever set for it. */
|
||||
function slotForgetLines(
|
||||
host: ClassHost,
|
||||
struct: string,
|
||||
meta: LlClassMeta,
|
||||
tag: string,
|
||||
): string[] {
|
||||
host.declare(`declare void @scr_slots_forget(i32)`);
|
||||
return [
|
||||
` %${tag}p = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${classSlotWordIndex(meta)}`,
|
||||
` %${tag} = load i32, ptr %${tag}p, align 4`,
|
||||
` %${tag}.set = icmp ne i32 %${tag}, 0`,
|
||||
` br i1 %${tag}.set, label %${tag}.forget, label %${tag}.done`,
|
||||
`${tag}.forget: ; release its ThreadSlots values`,
|
||||
` call void @scr_slots_forget(i32 %${tag})`,
|
||||
` br label %${tag}.done`,
|
||||
`${tag}.done:`,
|
||||
];
|
||||
}
|
||||
|
||||
/** What class emission needs beyond ShapeHost: interned unit instances for
|
||||
@@ -483,7 +529,7 @@ export function emitClassShapes(
|
||||
? ["ptr", "ptr", "ptr"]
|
||||
: ["ptr"]
|
||||
: [];
|
||||
const members = [...prefix, ...fieldTys];
|
||||
const members = [...prefix, ...(meta.threadSlot ? ["i32"] : []), ...fieldTys];
|
||||
typeDefs.push(
|
||||
`%${mangleClassStruct(cls.name)} = type { ${host.sizeType}${members.length ? ", " + members.join(", ") : ""} } ` +
|
||||
`; class ${cls.name}${meta.hierarchy ? " (vt at 1)" : ""}${streamRooted(meta) ? " (ScrStream prefix at 2)" : runtimePrefix ? ` (${runtimePrefix.struct} prefix at 2)` : ""} { ${cls.fields.map((f) => llvmCommentText(f.name)).join("; ")} }`,
|
||||
@@ -569,7 +615,7 @@ export function emitClassShapes(
|
||||
const traced = host.tracedShapes.has(`object:${cls.name}`);
|
||||
const runtimePrefix = runtimePrefixOf(meta);
|
||||
const isStreamRooted = streamRooted(meta);
|
||||
const fieldIndex = (i: number): number => fieldBase(meta) + i;
|
||||
const fieldIndex = (i: number): number => classFieldBase(meta) + i;
|
||||
// Teardown, trace and gcFree see each slot's storage type: a nullable
|
||||
// class field holds the instance pointer (NULL for the unit arm), and
|
||||
// every class retain/release/visit entry point is NULL-tolerant.
|
||||
@@ -640,6 +686,7 @@ export function emitClassShapes(
|
||||
});
|
||||
if (runtimePrefix) lines.push(...regCall("td", runtimePrefix.drop, ""));
|
||||
if (isStreamRooted) lines.push(...stCall("tds", "scr_stream_st_release", ""));
|
||||
if (meta.threadSlot) lines.push(...slotForgetLines(host, struct, meta, "tdw"));
|
||||
lines.push(...emitObjectFree(host, traced));
|
||||
};
|
||||
|
||||
@@ -841,6 +888,7 @@ export function emitClassShapes(
|
||||
` call void ${releaseSym(host, f.type)}(ptr %v${i}) ; ${llvmCommentText(f.name)} (acyclic)`,
|
||||
);
|
||||
});
|
||||
if (meta.threadSlot) gf.push(...slotForgetLines(host, struct, meta, "gfw"));
|
||||
gf.push(...emitObjectFree(host, true), ` ret void`, `}`, ``);
|
||||
defs.push(...gf);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { expect, test } from "vitest";
|
||||
import { DYN_HANDLE_KINDS } from "../../ir/ir.js";
|
||||
import { DYN_HANDLE_TAG_NUM } from "./dyn.js";
|
||||
|
||||
// ScrDynHandleTag in scr_dyn.h lists its tags by name, each with an explicit
|
||||
// value, so changes that add tags edit different lines. These checks stand
|
||||
// in for what the enum's order used to guarantee.
|
||||
const header = readFileSync(new URL("../../../../runtime/src/scr_dyn.h", import.meta.url), "utf8");
|
||||
const body = /typedef enum \{\n([\s\S]*?)\} ScrDynHandleTag;/.exec(header)![1]!;
|
||||
const tags = [...body.matchAll(/^\s*(SCR_DYNH_[A-Z0-9_]+) = (\d+),/gm)].map(([, name, value]) => ({
|
||||
name: name!,
|
||||
value: Number(value),
|
||||
}));
|
||||
const capacity = Number(/#define SCR_DYNH_CAPACITY (\d+)/.exec(header)![1]);
|
||||
|
||||
test("every ScrDynHandleTag has an explicit value, in name order", () => {
|
||||
const lines = body.split("\n").filter((line) => line.trim().startsWith("SCR_DYNH_"));
|
||||
expect(tags.length).toBe(lines.length);
|
||||
expect(tags.map((tag) => tag.name)).toEqual(tags.map((tag) => tag.name).toSorted());
|
||||
});
|
||||
|
||||
test("ScrDynHandleTag values are unique and below SCR_DYNH_CAPACITY", () => {
|
||||
const owners = new Map<number, string>();
|
||||
for (const { name, value } of tags) {
|
||||
expect(owners.get(value), `${name} = ${value}`).toBeUndefined();
|
||||
owners.set(value, name);
|
||||
expect(value, name).toBeLessThan(capacity);
|
||||
}
|
||||
});
|
||||
|
||||
test("the backend's handle tag numbers match scr_dyn.h", () => {
|
||||
const values = new Map(tags.map((tag) => [tag.name, tag.value]));
|
||||
for (const [kind, num] of Object.entries(DYN_HANDLE_TAG_NUM)) {
|
||||
const tag = DYN_HANDLE_KINDS.get(kind)?.tag;
|
||||
expect(tag, kind).toBeDefined();
|
||||
expect(values.get(tag!), `${kind} (${tag})`).toBe(num);
|
||||
}
|
||||
});
|
||||
@@ -72,8 +72,8 @@ export function recordDynKeepsIdentity(shape: IrRecordShape): boolean {
|
||||
}
|
||||
|
||||
/** ScrDynKind values (scr_runtime.h). */
|
||||
/** ScrDynHandleTag numeric values (scr_runtime.h's enum order). */
|
||||
const DYN_HANDLE_TAG_NUM: Record<string, number> = {
|
||||
/** ScrDynHandleTag values (scr_dyn.h's explicit values; dyn-handle-tags.test.ts checks them). */
|
||||
export const DYN_HANDLE_TAG_NUM: Record<string, number> = {
|
||||
httpReq: 0,
|
||||
httpRes: 1,
|
||||
netSocket: 2,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { jsTruthy } from "./jsval.js";
|
||||
import { type ArraySearchKind, emitArraySearchHelpers } from "./array-search.js";
|
||||
import { emitStringInputs } from "./string-lifetimes.js";
|
||||
import { emitLlvmLayouts } from "./layouts.js";
|
||||
import { emitThreadSlotsIntrinsic, threadSlotsBlockDefinitions } from "./thread-slots.js";
|
||||
import { compileRegexMatcher, type RegexMatcherCode } from "./regex-matchers.js";
|
||||
import {
|
||||
CYC_BUF,
|
||||
@@ -116,6 +117,7 @@ import {
|
||||
} from "../../ir/integer-ranges.js";
|
||||
import { unshareNodes } from "../../ir/unshare.js";
|
||||
import { copyStableCaptures } from "../../ir/capture-values.js";
|
||||
import { resolveSelfCaptures } from "../../ir/self-captures.js";
|
||||
import { analyzeInt32Slots, dynamicFieldStores, type Int32Slots } from "../../ir/int32-slots.js";
|
||||
import { analyzeFinalFields, constructorClass, type FinalFields } from "../../ir/final-fields.js";
|
||||
import { foldConstantGlobals } from "../../ir/constant-globals.js";
|
||||
@@ -515,7 +517,9 @@ export function emitLlvmModule(input: IrModule, options: LlvmTargetOptions = {})
|
||||
specializeNumericCalls(
|
||||
foldConstantGlobals(
|
||||
copyStableCaptures(
|
||||
hoistFinalFieldReads(freezeInitOnlyGlobals(dropUnwrittenPropertyBags(mod))),
|
||||
resolveSelfCaptures(
|
||||
hoistFinalFieldReads(freezeInitOnlyGlobals(dropUnwrittenPropertyBags(mod))),
|
||||
),
|
||||
),
|
||||
),
|
||||
mangleFunction,
|
||||
@@ -543,7 +547,9 @@ export function emitLlvmModuleSource(
|
||||
specializeNumericCalls(
|
||||
foldConstantGlobals(
|
||||
copyStableCaptures(
|
||||
hoistFinalFieldReads(freezeInitOnlyGlobals(dropUnwrittenPropertyBags(mod))),
|
||||
resolveSelfCaptures(
|
||||
hoistFinalFieldReads(freezeInitOnlyGlobals(dropUnwrittenPropertyBags(mod))),
|
||||
),
|
||||
),
|
||||
),
|
||||
mangleFunction,
|
||||
@@ -910,6 +916,8 @@ export class LlEmitter {
|
||||
/** Constant-key reads on checked-dynamic objects, each with its own
|
||||
* zero-initialized ScrDynReadIc (scr_runtime.h). */
|
||||
private dynReadIcSites = 0;
|
||||
/** ThreadSlots blocks by family name (thread-slots.ts). */
|
||||
private readonly threadSlotsBlocks = new Map<string, string>();
|
||||
/** May-throw analysis (computeMayThrow): pending
|
||||
* checks are emitted only after calls that can actually raise. */
|
||||
readonly mayThrow: Set<string>;
|
||||
@@ -2503,6 +2511,7 @@ export class LlEmitter {
|
||||
{
|
||||
const tl =
|
||||
this.mod.workers === true || this.mod.lib?.threadInstances === true ? "thread_local " : "";
|
||||
out.push(...threadSlotsBlockDefinitions(this.threadSlotsBlocks, tl !== ""));
|
||||
for (let site = 0; site < this.dynReadIcSites; site++)
|
||||
out.push(
|
||||
`@sc_dyn_read_ic_${site} = internal ${tl}global { ptr, ${this.sizeType}, ${this.sizeType}, ptr } zeroinitializer`,
|
||||
@@ -10177,9 +10186,20 @@ export class LlEmitter {
|
||||
)
|
||||
return this.emitRegionIntrinsic(e);
|
||||
if (e.name.startsWith("itable.")) return this.emitInternTableIntrinsic(e);
|
||||
if (e.name.startsWith("slots.")) return emitThreadSlotsIntrinsic(this, e);
|
||||
return emitIntrinsicExpr(this, e);
|
||||
}
|
||||
|
||||
/** The ScrSlots block of the ThreadSlots family `name`. */
|
||||
threadSlotsBlock(name: string): string {
|
||||
let sym = this.threadSlotsBlocks.get(name);
|
||||
if (sym === undefined) {
|
||||
sym = `@sc_slots_${this.threadSlotsBlocks.size}`;
|
||||
this.threadSlotsBlocks.set(name, sym);
|
||||
}
|
||||
return sym;
|
||||
}
|
||||
|
||||
/** InternTable's runtime calls (scr_itable.c). Table and entry handles
|
||||
* are f64 values carrying pointer bits; entry values are 64-bit slots
|
||||
* (double bits, a bool, or a pointer to an immortal value) that the
|
||||
|
||||
@@ -316,6 +316,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"date.getUTCDate": "scr_date_get_date_utc",
|
||||
"date.getDay": "scr_date_get_day_local",
|
||||
"date.getUTCDay": "scr_date_get_day_utc",
|
||||
"date.toUTCString": "scr_date_to_utc_string",
|
||||
"date.getHours": "scr_date_get_hours_local",
|
||||
"date.getUTCHours": "scr_date_get_hours_utc",
|
||||
"date.getMinutes": "scr_date_get_minutes_local",
|
||||
@@ -820,6 +821,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
// ambient-this read never throw. The fs dyn read is the sync-fs story.
|
||||
"fetch.start": "scr_fetch_static",
|
||||
"fetch.responseNew": "scr_fetch_response_new",
|
||||
"fetch.responseJsonStatic": "scr_fetch_response_json_static",
|
||||
"fetch.input": "scr_fetch_input",
|
||||
"abort.controllerNew": "scr_fetch_abort_controller_new",
|
||||
"event.targetNew": "scr_event_target_new",
|
||||
@@ -1242,9 +1244,11 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"http.reqSetTimeout": "scr_http_req_set_timeout_plain",
|
||||
"http.clientDestroy": "scr_http_client_destroy",
|
||||
"http.clientDestroyError": "scr_http_client_destroy_error",
|
||||
"http.globalAgent": "scr_http_global_agent_value",
|
||||
"http.serverMaxHeaderSize": "scr_http_server_max_header_size",
|
||||
"http.clientMaxHeaderSize": "scr_http_client_max_header_size",
|
||||
"https.clientServername": "scr_https_client_servername",
|
||||
"http.clientSignalCheck": "scr_http_client_signal_check",
|
||||
"https.nextServername": "scr_https_set_next_servername",
|
||||
"http.clientAbort": "scr_http_client_abort",
|
||||
"http.clientAborted": "scr_http_client_aborted",
|
||||
"http.clientDestroyed": "scr_http_client_destroyed",
|
||||
|
||||
@@ -164,13 +164,8 @@ export function emitTlsLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValu
|
||||
]);
|
||||
B.br(done);
|
||||
B.startBlock(plain);
|
||||
const insecure = callRuntime(host, "scr_http_request_ex", "ptr", [
|
||||
...a.slice(1, 8),
|
||||
...handler,
|
||||
{ type: "i32", name: "80" },
|
||||
ptr(),
|
||||
ptr(),
|
||||
]);
|
||||
// The plain arm is http.request: Node's global agent serves it.
|
||||
const insecure = callRuntime(host, "scr_http_request", "ptr", [...a.slice(1, 8), ...handler]);
|
||||
B.br(done);
|
||||
B.startBlock(done);
|
||||
const result = B.tmp();
|
||||
|
||||
@@ -483,11 +483,14 @@ export class ReferenceWrites {
|
||||
return;
|
||||
case "intrinsic":
|
||||
// A TDZ test reads; publication only makes objects immortal; intern
|
||||
// tables store published values and write no script object.
|
||||
// tables store published values and write no script object;
|
||||
// ThreadSlots reads write nothing.
|
||||
if (
|
||||
e.name !== "module.tdzCheck" &&
|
||||
e.name !== "threads.publish" &&
|
||||
!e.name.startsWith("itable.")
|
||||
!e.name.startsWith("itable.") &&
|
||||
e.name !== "slots.get" &&
|
||||
e.name !== "slots.has"
|
||||
)
|
||||
sink.all();
|
||||
return;
|
||||
|
||||
@@ -36,7 +36,7 @@ import {
|
||||
mangleRecordStruct,
|
||||
} from "../mangle.js";
|
||||
import { threadHotAddressLines } from "./alloc.js";
|
||||
import type { LlClassMeta } from "./classes.js";
|
||||
import { classFieldBase, type LlClassMeta } from "./classes.js";
|
||||
import { llvmCommentText } from "./common.js";
|
||||
import type { NullableRefFields } from "./nullable-fields.js";
|
||||
import { FN_ATTRS, rcMembers, releaseSym, vAdapters, type ShapeHost } from "./shapes.js";
|
||||
@@ -84,7 +84,7 @@ export class LifetimeRegions {
|
||||
|
||||
/** The reference-holding fields; inline scalar unions own nothing. */
|
||||
private classFields(def: IrClassDef, meta: LlClassMeta): Field[] {
|
||||
const base = meta.hierarchy ? 2 : 1;
|
||||
const base = classFieldBase(meta);
|
||||
return def.fields.flatMap((f, i) =>
|
||||
this.host.inlineUnions.classField(def.name, f.name)
|
||||
? []
|
||||
|
||||
@@ -205,6 +205,43 @@ describe("LLVM runtime RC symbols", () => {
|
||||
expect(fn("sc_rc_retain_promise")).not.toContain("store i32");
|
||||
});
|
||||
|
||||
test("optimized builds buffer candidates through the module's append helper", () => {
|
||||
const emit = (inlineRc: true | "release", extra: Partial<ShapeHost>) => {
|
||||
const { host } = declarationHost(64, inlineRc);
|
||||
const configured: ShapeHost = { ...host, inlineAlloc: true, ...extra };
|
||||
releaseSym(configured, { kind: "dyn" });
|
||||
return {
|
||||
ir: emitInlineRcHelpers(configured).join("\n"),
|
||||
decls: inlineRcDecls(configured),
|
||||
};
|
||||
};
|
||||
for (const mode of [true, "release"] as const) {
|
||||
const { ir, decls } = emit(mode, {});
|
||||
expect(ir).toContain("call preserve_mostcc void @sc_cyc_append(ptr %o)");
|
||||
expect(ir).toContain("define internal preserve_mostcc void @sc_cyc_append(ptr %o) noinline");
|
||||
// The young test reads the generation byte at obj-5; the append fills
|
||||
// scr_cyc_buffers' nursery buffer and the header's position at obj-4.
|
||||
expect(ir).toContain("getelementptr i8, ptr %o, i64 -5");
|
||||
expect(ir).toContain("getelementptr inbounds i8, ptr @scr_cyc_buffers, i64 96");
|
||||
expect(ir).toContain("getelementptr inbounds i8, ptr @scr_cyc_buffers, i64 104");
|
||||
expect(ir).toContain("load i64, ptr @scr_cyc_live");
|
||||
expect(ir).toContain("or i32 %i, -2147483648");
|
||||
expect(ir).toContain("call void @scr_cyc_on_release(ptr %o)");
|
||||
expect(decls).toContain(
|
||||
"@scr_cyc_buffers = external global { [3 x { ptr, i64, i64, i64 }], i64, i64 }",
|
||||
);
|
||||
expect(decls).toContain("@scr_cyc_live = external global i64");
|
||||
}
|
||||
// Worker executables and targets without inline allocation call
|
||||
// scr_cyc_on_release, which tries the same append first.
|
||||
for (const extra of [{ threadLocalAlloc: true }, { inlineAlloc: false }]) {
|
||||
const { ir, decls } = emit(true, extra);
|
||||
expect(ir).not.toContain("sc_cyc_append");
|
||||
expect(ir).toContain("call void @scr_cyc_on_release(ptr %o)");
|
||||
expect(decls.join("\n")).not.toContain("scr_cyc_buffers");
|
||||
}
|
||||
});
|
||||
|
||||
test("uses the wasm32 header offsets", () => {
|
||||
const { host } = declarationHost(32, true);
|
||||
releaseSym(host, { kind: "union", unionId: "u0" } as never);
|
||||
|
||||
@@ -29,7 +29,15 @@ import {
|
||||
mangleRecordStruct,
|
||||
mangleRecordTrace,
|
||||
} from "../mangle.js";
|
||||
import { cycTypeDescriptor, emitObjectAlloc, emitObjectFree } from "./alloc.js";
|
||||
import {
|
||||
cycTypeDescriptor,
|
||||
emitObjectAlloc,
|
||||
emitObjectFree,
|
||||
CYC_APPEND,
|
||||
cycleAppendDecls,
|
||||
cycleAppendHelper,
|
||||
inlineBufferingSupported,
|
||||
} from "./alloc.js";
|
||||
import type { BlockBuilder } from "./blocks.js";
|
||||
import { SCR_ARR_FIELD, llvmCommentText } from "./common.js";
|
||||
import {
|
||||
@@ -479,6 +487,10 @@ export const INLINE_RC_FAMILIES: Readonly<Record<string, InlineRcFamily>> = {
|
||||
|
||||
type RcOp = "retain" | "release";
|
||||
|
||||
/** The helper key (with the "family:op" keys) of the module's candidate
|
||||
* append helper (alloc.ts cycleAppendHelper). */
|
||||
const CYC_APPEND_KEY = "cyc:append";
|
||||
|
||||
/** The mirrored family for an RC operation on a runtime stem (`scr_str`),
|
||||
* or null when it stays a call. */
|
||||
export function inlineRcFamily(host: ShapeHost, stem: string, op: RcOp): InlineRcFamily | null {
|
||||
@@ -547,11 +559,17 @@ export function boxReleaseSym(host: ShapeHost): string {
|
||||
/** Record the helper and declare the runtime symbols it calls now, so they
|
||||
* land in the extern block. */
|
||||
function requestRcHelper(host: ShapeHost, family: string, op: RcOp): string {
|
||||
if (host.rcHelpers === null || mirroredFamily(host, family, op) === null) {
|
||||
const row = host.rcHelpers === null ? null : mirroredFamily(host, family, op);
|
||||
if (host.rcHelpers === null || row === null) {
|
||||
throw new LlvmUnsupportedError(`inline rc:${family}:${op}`);
|
||||
}
|
||||
host.rcHelpers.add(`${family}:${op}`);
|
||||
for (const decl of rcHelperDecls(family, op)) host.declare(decl);
|
||||
// Its body is written last (emitInlineRcHelpers), after the requests are
|
||||
// collected: a release that can buffer a candidate requests the append
|
||||
// helper now. The jsval row (JS_RC_ROW) has no collector header; its
|
||||
// cells release through dyn's helper, which makes its own request.
|
||||
if (op === "release" && row.cycle === "always") requestCycleAppend(host);
|
||||
return `@${inlineRcName(family, op)}`;
|
||||
}
|
||||
|
||||
@@ -653,6 +671,10 @@ function rcHelperKeys(host: ShapeHost): string[] {
|
||||
export function inlineRcDecls(host: ShapeHost): string[] {
|
||||
const decls = new Set<string>();
|
||||
for (const key of rcHelperKeys(host)) {
|
||||
if (key === CYC_APPEND_KEY) {
|
||||
for (const decl of cycleAppendDecls()) decls.add(decl);
|
||||
continue;
|
||||
}
|
||||
const [family, op] = key.split(":") as [string, RcOp];
|
||||
for (const decl of rcHelperDecls(family, op)) decls.add(decl);
|
||||
}
|
||||
@@ -798,6 +820,10 @@ function releaseHelper(host: ShapeHost, family: string, row: InlineRcFamily): st
|
||||
export function emitInlineRcHelpers(host: ShapeHost): string[] {
|
||||
const out: string[] = [];
|
||||
for (const key of rcHelperKeys(host).sort()) {
|
||||
if (key === CYC_APPEND_KEY) {
|
||||
out.push(...cycleAppendHelper());
|
||||
continue;
|
||||
}
|
||||
const [family, op] = key.split(":") as [string, RcOp];
|
||||
if (family === "js") {
|
||||
out.push(...jsvalRcHelper(host, op, rcHelperAttrs(host)));
|
||||
@@ -1454,12 +1480,6 @@ export function releaseFastPath(
|
||||
];
|
||||
}
|
||||
|
||||
/** The possible-cycle-root step of a release that left a headered object
|
||||
* alive: scr_cyc_on_release with its early return inlined. Test the sign
|
||||
* of `buf` (the i32 at obj-4; negative for a queued candidate or a tenured
|
||||
* object) and call the runtime only to enqueue a new candidate.
|
||||
* Every emitted release of a headered object shares this sequence
|
||||
* (scr_runtime.h asserts the offset); it ends by branching to `done`. */
|
||||
/** One traced field of an emitted trace function: load the slot and visit
|
||||
* it unless it is null. Every collector visitor ignores null children, and
|
||||
* most traced slots of a typical heap are empty (optional links, unset
|
||||
@@ -1483,16 +1503,39 @@ export function traceFieldVisitLines(
|
||||
];
|
||||
}
|
||||
|
||||
/** Whether candidate buffering goes through the module's append helper
|
||||
* (alloc.ts cycleAppendHelper), requesting its definition if so: in
|
||||
* optimized builds (those with inline RC helpers) that support it. */
|
||||
function requestCycleAppend(host: ShapeHost): boolean {
|
||||
if (host.rcHelpers === null || !inlineBufferingSupported(host)) return false;
|
||||
if (!host.rcHelpers.has(CYC_APPEND_KEY)) {
|
||||
host.rcHelpers.add(CYC_APPEND_KEY);
|
||||
for (const decl of cycleAppendDecls()) host.declare(decl);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** The possible-cycle-root step of a release that left a headered object
|
||||
* alive: scr_cyc_on_release with its early return inlined. Test the sign
|
||||
* of `buf` (the i32 at obj-4; negative for a queued candidate or a tenured
|
||||
* object), and buffer a new candidate: through the module's
|
||||
* cycleAppendHelper in optimized builds that support it, through
|
||||
* scr_cyc_on_release otherwise. Every emitted release of a headered object
|
||||
* shares this sequence (scr_runtime.h asserts the offsets); it ends by
|
||||
* branching to `done`. */
|
||||
export function cycleRootLines(host: ShapeHost, done: string): string[] {
|
||||
const S = host.sizeType;
|
||||
host.declare(`declare void @scr_cyc_on_release(ptr)`);
|
||||
const append = requestCycleAppend(host);
|
||||
return [
|
||||
` %bufp = getelementptr i8, ptr %o, ${S} -${host.cycleBufOffset}`,
|
||||
` %buf = load i32, ptr %bufp`,
|
||||
` %queued = icmp slt i32 %buf, 0`,
|
||||
` br i1 %queued, label %${done}, label %enqueue`,
|
||||
`enqueue:`,
|
||||
` call void @scr_cyc_on_release(ptr %o) ; buffer the candidate; may collect`,
|
||||
append
|
||||
? ` call preserve_mostcc void @${CYC_APPEND}(ptr %o) ; buffer the candidate; may collect`
|
||||
: ` call void @scr_cyc_on_release(ptr %o) ; buffer the candidate; may collect`,
|
||||
` br label %${done}`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -47,6 +47,10 @@ export type StackObjectFieldOwner = (fields: readonly StackObjectField[]) => voi
|
||||
* (stackObjectFields). Published shapes keep heap storage: their field
|
||||
* guards treat the immortal header as a frozen object. */
|
||||
export function canStackObject(host: LlvmEmitterContext, e: IrExpr): e is StackObjectSource {
|
||||
// A ThreadSlots key keeps its slot index for its lifetime and has its
|
||||
// values released when it is freed: it lives on the heap.
|
||||
if (e.type.kind === "object" && host.classMeta.get(e.type.className)?.threadSlot === true)
|
||||
return false;
|
||||
if (e.type.kind === "record" && host.publishedTypes?.recordGuarded(e.type.shapeId) === true)
|
||||
return false;
|
||||
// A producer with a result-slot entry builds its fresh result in the
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { BOOL, F64, STRING, VOID, type IrExpr, type IrModule, type IrType } from "../../ir/ir.js";
|
||||
import { validateModule } from "../../ir/validate.js";
|
||||
import { emitLlvmModule } from "./emitter.js";
|
||||
import { mangleClassStruct } from "../mangle.js";
|
||||
|
||||
const loc = { file: "thread-slots.ts", start: 0, end: 1 };
|
||||
const slotsType: IrType = { kind: "object", className: "%ThreadSlots" };
|
||||
const node: IrType = { kind: "object", className: "Node" };
|
||||
const leaf: IrType = { kind: "object", className: "Leaf" };
|
||||
const plain: IrType = { kind: "object", className: "Plain" };
|
||||
const state: IrType = { kind: "object", className: "State" };
|
||||
const maybeState: IrType = { kind: "union", unionId: "u0" };
|
||||
|
||||
/** A key hierarchy (Node, Leaf), a class that is not a key (Plain), and the
|
||||
* value class (State), with `slots` a module global of the slots class. */
|
||||
function module(body: IrExpr[]): IrModule {
|
||||
const cls = (name: string, fields: [string, IrType][], base?: string) => ({
|
||||
name,
|
||||
...(base !== undefined ? { base } : {}),
|
||||
fields: fields.map(([field, type]) => ({ name: field, type })),
|
||||
methods: [],
|
||||
loc,
|
||||
});
|
||||
return {
|
||||
irVersion: 16,
|
||||
sourceFile: loc.file,
|
||||
entry: "main",
|
||||
threadSlotKeys: ["Node"],
|
||||
globals: [
|
||||
["slots", slotsType],
|
||||
["node", node],
|
||||
["leaf", leaf],
|
||||
["plain", plain],
|
||||
["state", state],
|
||||
].map(([name, type]) => ({ id: `%g.${name as string}`, name, type, mutable: false })),
|
||||
functions: [
|
||||
{
|
||||
name: "main",
|
||||
params: [],
|
||||
returnType: VOID,
|
||||
locals: [],
|
||||
body: body.map((expr) => ({ kind: "exprStmt" as const, expr, loc })),
|
||||
loc,
|
||||
},
|
||||
],
|
||||
classes: [
|
||||
cls("%ThreadSlots", []),
|
||||
cls("Node", [["name", STRING]]),
|
||||
cls(
|
||||
"Leaf",
|
||||
[
|
||||
["name", STRING],
|
||||
["weight", F64],
|
||||
],
|
||||
"Node",
|
||||
),
|
||||
cls("Plain", [["name", STRING]]),
|
||||
cls("State", [["count", F64]]),
|
||||
],
|
||||
unions: [{ id: "u0", arms: [state, { kind: "undefinedT" }] }],
|
||||
} as unknown as IrModule;
|
||||
}
|
||||
|
||||
function body(llvm: string, name: string): string {
|
||||
const start = llvm.indexOf(`@${name}(`);
|
||||
return start < 0 ? "" : llvm.slice(start, llvm.indexOf("\n}", start));
|
||||
}
|
||||
|
||||
const intrinsic = (name: string, args: IrExpr[], type: IrType): IrExpr =>
|
||||
({ kind: "intrinsic", name, args, type, loc }) as IrExpr;
|
||||
const str = (value: string): IrExpr => ({ kind: "strLit", value, type: STRING, loc });
|
||||
const global = (id: string, type: IrType): IrExpr =>
|
||||
({ kind: "varRef", localId: `%g.${id}`, type, loc }) as IrExpr;
|
||||
const slots = global("slots", slotsType);
|
||||
const fresh = (type: IrType & { kind: "object" }): IrExpr =>
|
||||
global(type.className.toLowerCase(), type);
|
||||
|
||||
test("slots.get reads the slot word, the page and the entry inline", () => {
|
||||
const calls = [
|
||||
intrinsic("slots.get", [str("s"), slots, fresh(leaf)], maybeState),
|
||||
intrinsic("slots.has", [str("s"), slots, fresh(node)], BOOL),
|
||||
intrinsic("slots.set", [str("s"), slots, fresh(leaf), fresh(state)], VOID),
|
||||
intrinsic("slots.delete", [str("t"), slots, fresh(node)], BOOL),
|
||||
];
|
||||
expect(validateModule(module(calls))).toEqual([]);
|
||||
const llvm = emitLlvmModule(module(calls), { pointerBits: 64 });
|
||||
// The slot word sits after the header words of every hierarchy member.
|
||||
expect(llvm).toMatch(
|
||||
new RegExp(`%${mangleClassStruct("Node")} = type \\{ i64, ptr, i32, ptr \\}`),
|
||||
);
|
||||
expect(llvm).toMatch(
|
||||
new RegExp(`%${mangleClassStruct("Leaf")} = type \\{ i64, ptr, i32, ptr, (i32|double) \\}`),
|
||||
);
|
||||
expect(llvm).toMatch(new RegExp(`%${mangleClassStruct("Plain")} = type \\{ i64, ptr \\}`));
|
||||
// One block per family name.
|
||||
expect(llvm).toMatch(
|
||||
/@sc_slots_0 = internal global \{ ptr, i32, i32, ptr, ptr \} zeroinitializer/,
|
||||
);
|
||||
expect(llvm).toMatch(
|
||||
/@sc_slots_1 = internal global \{ ptr, i32, i32, ptr, ptr \} zeroinitializer/,
|
||||
);
|
||||
const main = body(llvm, "sc_f_main");
|
||||
expect(main).toMatch(
|
||||
/getelementptr inbounds %\S+, ptr %\S+, i64 0, i32 2\n {2}(%\S+) = load atomic i32, ptr %\S+ monotonic, align 4\n {2}%\S+ = lshr i32 \1, 10/,
|
||||
);
|
||||
expect(main).not.toMatch(/call ptr @scr_slots_get/);
|
||||
expect(main).toMatch(
|
||||
/call void @scr_slots_set\(ptr @sc_slots_0, ptr %\S+, ptr %\S+, ptr %\S+, ptr @\S+\)/,
|
||||
);
|
||||
expect(main).toMatch(/call zeroext i1 @scr_slots_delete\(ptr @sc_slots_1, ptr %\S+\)/);
|
||||
});
|
||||
|
||||
test("freeing a key releases its values when its slot word is set", () => {
|
||||
const llvm = emitLlvmModule(
|
||||
module([intrinsic("slots.has", [str("s"), slots, fresh(node)], BOOL)]),
|
||||
{ pointerBits: 64 },
|
||||
);
|
||||
expect(llvm).toMatch(/icmp ne i32 %tdw, 0\n {2}br i1 %tdw\.set, label %tdw\.forget/);
|
||||
expect(llvm).toMatch(/call void @scr_slots_forget\(i32 %tdw\)/);
|
||||
});
|
||||
|
||||
test("the validator checks ThreadSlots intrinsic operands", () => {
|
||||
const bad = [
|
||||
intrinsic("slots.get", [str("s"), slots], maybeState),
|
||||
intrinsic("slots.has", [slots, slots, fresh(node)], BOOL),
|
||||
intrinsic("slots.has", [str("s"), slots, fresh(plain)], BOOL),
|
||||
intrinsic(
|
||||
"slots.set",
|
||||
[str("s"), slots, fresh(node), { kind: "numLit", value: 1, type: F64, loc }],
|
||||
VOID,
|
||||
),
|
||||
intrinsic("slots.get", [str("s"), slots, fresh(node)], state),
|
||||
];
|
||||
expect(validateModule(module(bad)).map((e) => e.message)).toEqual([
|
||||
"in main: slots.get takes 3 arguments",
|
||||
"in main: slots.has takes a name literal and a %ThreadSlots value",
|
||||
"in main: slots.has key object:Plain is not a ThreadSlots key class",
|
||||
"in main: slots.set value f64 is not a ThreadSlots value type",
|
||||
"in main: slots.get has a V | undefined union type",
|
||||
]);
|
||||
});
|
||||
@@ -0,0 +1,170 @@
|
||||
/* ThreadSlots (@scriptc/experimental): the slots.* intrinsics (scr_slots.c,
|
||||
* frontend/lowering/thread-slots.ts).
|
||||
*
|
||||
* Each family name has one `{ ptr, i32, i32, ptr, ptr }` ScrSlots block,
|
||||
* an internal global that is thread-local where module globals are. A key
|
||||
* class's hierarchy carries an i32 slot word after its header words
|
||||
* (classSlotWordIndex). A read is inline: the slot word, then the page
|
||||
* and the entry,
|
||||
*
|
||||
* page = word >> 10 < block.npages ? block.pages[word >> 10] : none
|
||||
* value = page[word & 1023]
|
||||
*
|
||||
* and absent pages are a shared page of NULL entries (scr_slots_none), so
|
||||
* a key without a value reads NULL, the undefined of `V | undefined`. Set
|
||||
* and delete call the runtime, which holds one reference to each value and
|
||||
* releases the value it replaces or removes. */
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { isUnitType, typeKey, type IrType } from "../../ir/ir.js";
|
||||
import { classSlotWordIndex, classStructSym } from "./classes.js";
|
||||
import { llvmCommentText } from "./common.js";
|
||||
import type { ExprOf, LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import { releaseSym } from "./shapes.js";
|
||||
|
||||
const BLOCK = "{ ptr, i32, i32, ptr, ptr }";
|
||||
|
||||
/** The slots.* intrinsics. */
|
||||
export function emitThreadSlotsIntrinsic(
|
||||
host: LlvmEmitterContext,
|
||||
e: ExprOf<"intrinsic">,
|
||||
): LlValue {
|
||||
const B = host.B;
|
||||
const [nameArg, slotsArg, keyArg, valueArg] = e.args;
|
||||
if (nameArg?.kind !== "strLit" || slotsArg === undefined || keyArg === undefined)
|
||||
throw new InternalCompilerError(`llvm emitter bug: ${e.name} operands`);
|
||||
const block = host.threadSlotsBlock(nameArg.value);
|
||||
// The slots value is read for its effects (a module const's TDZ check).
|
||||
const slots = host.emitReadReceiver(slotsArg);
|
||||
const key = host.emitReadReceiver(keyArg);
|
||||
const value = valueArg !== undefined ? host.emitExpr(valueArg) : null;
|
||||
if (B.isTerminated()) return { name: "", type: e.type };
|
||||
if (keyArg.type.kind !== "object")
|
||||
throw new InternalCompilerError(`llvm emitter bug: ${e.name} key of kind ${keyArg.type.kind}`);
|
||||
const className = keyArg.type.className;
|
||||
const word = B.tmp();
|
||||
B.line(
|
||||
`${word} = getelementptr inbounds %${classStructSym(className)}, ptr ${key.name}, i64 0, i32 ${classSlotWordIndex(host.classMetaOf(className))}`,
|
||||
);
|
||||
switch (e.name) {
|
||||
case "slots.get": {
|
||||
const read = readSlot(host, block, word);
|
||||
if (host.nullableUnions.of(e.type) !== null)
|
||||
return host.own({ name: host.retainValue(read, e.type), type: e.type });
|
||||
return boxedRead(host, read, e.type);
|
||||
}
|
||||
case "slots.has": {
|
||||
const read = readSlot(host, block, word);
|
||||
const has = B.tmp();
|
||||
B.line(`${has} = icmp ne ptr ${read}, null`);
|
||||
return { name: has, type: e.type };
|
||||
}
|
||||
case "slots.delete": {
|
||||
host.declare(`declare zeroext i1 @scr_slots_delete(ptr, ptr)`);
|
||||
const had = B.tmp();
|
||||
B.line(`${had} = call zeroext i1 @scr_slots_delete(ptr ${block}, ptr ${word})`);
|
||||
return { name: had, type: e.type };
|
||||
}
|
||||
case "slots.set": {
|
||||
if (value === null) throw new InternalCompilerError("llvm emitter bug: slots.set value");
|
||||
// The value moves into the block.
|
||||
host.moveTemp(value);
|
||||
host.declare(`declare void @scr_slots_set(ptr, ptr, ptr, ptr, ptr)`);
|
||||
B.line(
|
||||
`call void @scr_slots_set(ptr ${block}, ptr ${key.name}, ptr ${word}, ptr ${value.name}, ptr ${releaseSym(host.shapeHost, value.type)})`,
|
||||
);
|
||||
if (e.type.kind === "void") return { name: "", type: e.type };
|
||||
return host.own({ name: host.retainValue(slots.name, e.type), type: e.type });
|
||||
}
|
||||
default:
|
||||
throw new InternalCompilerError(`llvm emitter bug: unknown intrinsic ${e.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** A read as a tagged union box, for `V | undefined` types that the module
|
||||
* keeps boxed (Map keys and Set elements of that type): the value's arm, or
|
||||
* the interned undefined instance for NULL. */
|
||||
function boxedRead(host: LlvmEmitterContext, read: string, type: IrType): LlValue {
|
||||
const B = host.B;
|
||||
const def = type.kind === "union" ? host.unionsById.get(type.unionId) : undefined;
|
||||
const refTag = def?.arms.findIndex((a) => !isUnitType(a)) ?? -1;
|
||||
const unitTag = def?.arms.findIndex((a) => a.kind === "undefinedT") ?? -1;
|
||||
if (def === undefined || refTag < 0 || unitTag < 0 || def.arms.length !== 2)
|
||||
throw new InternalCompilerError(`llvm emitter bug: slots.get of type ${typeKey(type)}`);
|
||||
const result = B.slot();
|
||||
B.entryAllocas.push(`${result} = alloca ptr`);
|
||||
const has = B.tmp();
|
||||
B.line(`${has} = icmp ne ptr ${read}, null`);
|
||||
const some = B.newLabel("slots.some"),
|
||||
none = B.newLabel("slots.none"),
|
||||
done = B.newLabel("slots.box");
|
||||
B.condBr(has, some, none);
|
||||
B.startBlock(some);
|
||||
const arm = def.arms[refTag]!;
|
||||
const box = host.unionNewOwned(def.id, refTag, { name: host.retainValue(read, arm), type: arm });
|
||||
B.line(`store ptr ${box}, ptr ${result}`);
|
||||
B.br(done);
|
||||
B.startBlock(none);
|
||||
B.line(`store ptr ${host.unitInstanceRef(def.id, unitTag)}, ptr ${result}`);
|
||||
B.br(done);
|
||||
B.startBlock(done);
|
||||
const out = B.tmp();
|
||||
B.line(`${out} = load ptr, ptr ${result}`);
|
||||
return host.own({ name: out, type });
|
||||
}
|
||||
|
||||
/** This thread's value for the slot word at `word`, borrowed, or NULL. */
|
||||
function readSlot(host: LlvmEmitterContext, block: string, word: string): string {
|
||||
const B = host.B;
|
||||
host.declare(`@scr_slots_none = external constant [1024 x ptr]`);
|
||||
const index = B.tmp(),
|
||||
page = B.tmp(),
|
||||
pages = B.tmp(),
|
||||
count = B.tmp(),
|
||||
inRange = B.tmp();
|
||||
B.line(`${index} = load atomic i32, ptr ${word} monotonic, align 4`);
|
||||
B.line(`${page} = lshr i32 ${index}, 10`);
|
||||
B.line(`${count} = getelementptr inbounds ${BLOCK}, ptr ${block}, i64 0, i32 1`);
|
||||
const npages = B.tmp();
|
||||
B.line(`${npages} = load i32, ptr ${count}`);
|
||||
B.line(`${inRange} = icmp ult i32 ${page}, ${npages}`);
|
||||
const result = B.slot();
|
||||
B.entryAllocas.push(`${result} = alloca ptr`);
|
||||
B.line(`store ptr null, ptr ${result}`);
|
||||
const found = B.newLabel("slots.page"),
|
||||
done = B.newLabel("slots.j");
|
||||
B.condBr(inRange, found, done);
|
||||
B.startBlock(found);
|
||||
B.line(`${pages} = load ptr, ptr ${block}`);
|
||||
const pageIndex = B.tmp(),
|
||||
pagePtr = B.tmp(),
|
||||
entries = B.tmp(),
|
||||
entryIndex = B.tmp(),
|
||||
entryWide = B.tmp(),
|
||||
entry = B.tmp(),
|
||||
v = B.tmp();
|
||||
B.line(`${pageIndex} = zext i32 ${page} to i64`);
|
||||
B.line(`${pagePtr} = getelementptr inbounds ptr, ptr ${pages}, i64 ${pageIndex}`);
|
||||
B.line(`${entries} = load ptr, ptr ${pagePtr}`);
|
||||
B.line(`${entryIndex} = and i32 ${index}, 1023`);
|
||||
B.line(`${entryWide} = zext i32 ${entryIndex} to i64`);
|
||||
B.line(`${entry} = getelementptr inbounds ptr, ptr ${entries}, i64 ${entryWide}`);
|
||||
B.line(`${v} = load ptr, ptr ${entry}`);
|
||||
B.line(`store ptr ${v}, ptr ${result}`);
|
||||
B.br(done);
|
||||
B.startBlock(done);
|
||||
const out = B.tmp();
|
||||
B.line(`${out} = load ptr, ptr ${result}`);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The ScrSlots block definitions of the families a module uses. */
|
||||
export function threadSlotsBlockDefinitions(
|
||||
blocks: ReadonlyMap<string, string>,
|
||||
threadLocal: boolean,
|
||||
): string[] {
|
||||
const tl = threadLocal ? "thread_local " : "";
|
||||
return [...blocks].map(
|
||||
([name, sym]) =>
|
||||
`${sym} = internal ${tl}global ${BLOCK} zeroinitializer ; ThreadSlots ${llvmCommentText(JSON.stringify(name))}`,
|
||||
);
|
||||
}
|
||||
@@ -70,6 +70,7 @@ export const EXECUTABLE_RUNTIME_SOURCES = [
|
||||
"scr_publish.c",
|
||||
"scr_region.c",
|
||||
"scr_itable.c",
|
||||
"scr_slots.c",
|
||||
"scr_alloc.c",
|
||||
] as const;
|
||||
|
||||
|
||||
@@ -518,6 +518,15 @@ export const NODE24_FETCH_COMPAT_PROFILE = {
|
||||
evidence: [fixture("static")],
|
||||
}),
|
||||
),
|
||||
{
|
||||
id: "stdlib.response.static.json",
|
||||
name: "Response.json",
|
||||
kind: "static-method",
|
||||
facets: ["webidl-conversion", "error-shape"],
|
||||
scope:
|
||||
"data that crosses the checked-dynamic boundary, serialized with JSON.stringify (toJSON hooks included); ResponseInit as for the constructor",
|
||||
evidence: [fixture("static-response-json")],
|
||||
},
|
||||
...["json", "text", "bytes", "arrayBuffer"].map((member): FetchCompatOperation => ({
|
||||
id: `stdlib.response.${member}`,
|
||||
name: `Response.${member}`,
|
||||
@@ -681,7 +690,8 @@ export const NODE24_FETCH_COMPAT_PROFILE = {
|
||||
),
|
||||
|
||||
staticEntry("stdlib.response.constructor", "Response", "constructor", "constructor"),
|
||||
...["error", "json", "redirect"].map((member) =>
|
||||
staticEntry("stdlib.response.static.json", "Response", "json", "static"),
|
||||
...["error", "redirect"].map((member) =>
|
||||
unsupportedEntry(
|
||||
`stdlib.response.static.${member}`,
|
||||
"Response",
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
/* Provenance of the @scriptc/experimental exports. The package is scriptc's
|
||||
* own (packages/experimental): Node.js runs its JavaScript implementation,
|
||||
* scriptc builds lower its exports (lowering/lifetime-regions.ts,
|
||||
* lowering/intern-tables.ts) and never compile its body. */
|
||||
* lowering/intern-tables.ts, lowering/thread-slots.ts) and never compile
|
||||
* its body. */
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import * as ts from "./ts7/adapter.js";
|
||||
|
||||
export const EXPERIMENTAL_MODULE = "@scriptc/experimental";
|
||||
|
||||
export type ExperimentalMember = "LifetimeRegion" | "InternTable";
|
||||
export type ExperimentalMember = "LifetimeRegion" | "InternTable" | "ThreadSlots";
|
||||
|
||||
/** The @scriptc/experimental export an identifier is a named-import binding of. */
|
||||
export function experimentalImportOf(
|
||||
@@ -27,7 +28,9 @@ export function experimentalImportOf(
|
||||
)
|
||||
return null;
|
||||
const member = decl.propertyName?.text ?? decl.name.text;
|
||||
return member === "LifetimeRegion" || member === "InternTable" ? member : null;
|
||||
return member === "LifetimeRegion" || member === "InternTable" || member === "ThreadSlots"
|
||||
? member
|
||||
: null;
|
||||
}
|
||||
|
||||
/** Whether `sf` is the package's own declaration file: its directory's
|
||||
@@ -114,3 +117,27 @@ export function internTableMemberOf(
|
||||
? name
|
||||
: null;
|
||||
}
|
||||
|
||||
export type ThreadSlotsMember = "get" | "has" | "set" | "delete";
|
||||
|
||||
/** The ThreadSlots member a property access names (`slots.get`), by the
|
||||
* declaration it resolves to. */
|
||||
export function threadSlotsMemberOf(
|
||||
program: ts.Program,
|
||||
access: ts.PropertyAccessExpression,
|
||||
known: Map<string, boolean>,
|
||||
): ThreadSlotsMember | null {
|
||||
const checker = program.getTypeChecker();
|
||||
const symbol = checker.getSymbolAtLocation(access.name);
|
||||
const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined;
|
||||
if (decl === undefined || !ts.isMethodDeclaration(decl)) return null;
|
||||
const owner = decl.parent;
|
||||
if (
|
||||
!ts.isClassDeclaration(owner) ||
|
||||
owner.name?.text !== "ThreadSlots" ||
|
||||
!isExperimentalDeclarationFile(owner.getSourceFile(), known)
|
||||
)
|
||||
return null;
|
||||
const name = access.name.text;
|
||||
return name === "get" || name === "has" || name === "set" || name === "delete" ? name : null;
|
||||
}
|
||||
|
||||
@@ -102,6 +102,8 @@ export function lowerExecSyncCall(
|
||||
let stdoutMode = 1;
|
||||
let stderrMode = 0;
|
||||
let stdinInherit = false;
|
||||
// Without an encoding option, a failure's error carries Buffer outputs.
|
||||
let encodingGiven = false;
|
||||
// A conditional env spread (`...(c ? { env: ... } : {})`): the call
|
||||
// itself splits into a ternary of the two env-nesses at the tail.
|
||||
let condEnvSpread: { cond: IrExpr; pairs: IrExpr; whenTrue: boolean } | null = null;
|
||||
@@ -134,6 +136,8 @@ export function lowerExecSyncCall(
|
||||
runtime.timeout,
|
||||
runtime.stdoutMode,
|
||||
runtime.stderrMode,
|
||||
// The runtime options carry a utf8 encoding (others throw).
|
||||
boolLit(false, loc),
|
||||
],
|
||||
type: STRING,
|
||||
loc,
|
||||
@@ -251,6 +255,7 @@ export function lowerExecSyncCall(
|
||||
'outputs are captured as utf8 — pass { encoding: "utf8" }',
|
||||
);
|
||||
}
|
||||
encodingGiven = true;
|
||||
break;
|
||||
}
|
||||
case "cwd":
|
||||
@@ -395,6 +400,7 @@ export function lowerExecSyncCall(
|
||||
timeout,
|
||||
numLit(stdoutMode + (stdinInherit ? 4 : 0), loc),
|
||||
numLit(stderrMode, loc),
|
||||
boolLit(!encodingGiven, loc),
|
||||
],
|
||||
type: STRING,
|
||||
loc,
|
||||
|
||||
@@ -34,7 +34,7 @@ const DATE_GETTER_FNS: Readonly<Record<string, IrLibFn>> = {
|
||||
};
|
||||
|
||||
const DATE_METHOD_HINT =
|
||||
"getTime(), valueOf(), toISOString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering";
|
||||
"getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering";
|
||||
|
||||
/** The Date slice: statics plus read-only Date values backed by one
|
||||
* TimeClip'd millisecond scalar. Construction lives in lowerNew;
|
||||
@@ -113,6 +113,9 @@ export function lowerDateCall(
|
||||
if (name === "toISOString") {
|
||||
return { kind: "libCall", fn: "date.toISOStringValue", args: [receiver], type: STRING, loc };
|
||||
}
|
||||
if (name === "toUTCString") {
|
||||
return { kind: "libCall", fn: "date.toUTCString", args: [receiver], type: STRING, loc };
|
||||
}
|
||||
const fn = own(DATE_GETTER_FNS, name);
|
||||
if (fn !== undefined) {
|
||||
return { kind: "libCall", fn, args: [receiver], type: F64, loc };
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
VOID,
|
||||
arrayOf,
|
||||
funcOf,
|
||||
typeEquals,
|
||||
} from "../../../ir/ir.js";
|
||||
import { lowerBuiltinOptionalDefault, optionMember, literalBoolOptions } from "./arguments.js";
|
||||
import {
|
||||
@@ -603,6 +604,14 @@ export function lowerFsReadDescriptorCall(
|
||||
return { kind: "libCall", fn: "fs.readFdSync", args: [fd, enc], type: STRING, loc };
|
||||
}
|
||||
|
||||
/** The `{ encoding }` options record. A literal whose value is checked-
|
||||
* dynamic (an encoding variable reassigned from an untyped source) lowers
|
||||
* to a dynamic object: check it into the record the checker describes. */
|
||||
function encodingOptions(lowerer: Lowerer, options: ts.Expression, type: IrType): IrExpr {
|
||||
const value = lowerer.lowerExpr(options);
|
||||
return typeEquals(value.type, type) ? value : lowerer.coerceInto(options, value, type);
|
||||
}
|
||||
|
||||
/** Encoding-bearing reads share Buffer's decoder and runtime alias checks.
|
||||
* Keep the read and encoding evaluation in source order in one helper. */
|
||||
export function lowerFsEncodedRead(
|
||||
@@ -631,7 +640,7 @@ export function lowerFsEncodedRead(
|
||||
? lowerer.lowerExprExpecting(options, STRING)
|
||||
: {
|
||||
kind: "recordGet",
|
||||
obj: lowerer.lowerExpr(options),
|
||||
obj: encodingOptions(lowerer, options, optionsType),
|
||||
shapeId: optionsType.shapeId,
|
||||
field: "encoding",
|
||||
type: STRING,
|
||||
@@ -692,7 +701,7 @@ export function lowerFsPromisesEncodedRead(
|
||||
? lowerer.lowerExprExpecting(options, STRING)
|
||||
: {
|
||||
kind: "recordGet",
|
||||
obj: lowerer.lowerExpr(options),
|
||||
obj: encodingOptions(lowerer, options, optionsType),
|
||||
shapeId: optionsType.shapeId,
|
||||
field: "encoding",
|
||||
type: STRING,
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
isUnitType,
|
||||
} from "../../../ir/ir.js";
|
||||
import { optionalStringTags } from "./arguments.js";
|
||||
import { constituentTypes } from "../../ts7/checker.js";
|
||||
|
||||
/** JSON parse produces checked-dynamic data; a native reviver walks it
|
||||
* bottom-up before any checked cast. Stringify without a callback keeps
|
||||
@@ -152,9 +153,18 @@ export function lowerJsonMethodCall(
|
||||
}
|
||||
const optionalString = lowerOptionalStringifyRoot(lowerer, value, indent, loc);
|
||||
if (optionalString) return optionalString;
|
||||
// A program class instance whose class (or a base) declares toJSON:
|
||||
// JSON.stringify serializes whatever toJSON(key) returns, so the
|
||||
// instance crosses into the runtime walker, which calls the hook
|
||||
// through the checked-dynamic class view.
|
||||
const toJsonClass =
|
||||
value.type.kind === "object" &&
|
||||
lowerer.findMethodOn(lowerer.classes.get(value.type.className) ?? null, "toJSON") !== null &&
|
||||
lowerer.dynConvertible(value.type);
|
||||
if (
|
||||
value.type.kind === "url" ||
|
||||
value.type.kind === "dyn" ||
|
||||
toJsonClass ||
|
||||
isJsonStringifyDynamicType(
|
||||
value.type,
|
||||
(id) => lowerer.shapes.get(id),
|
||||
@@ -191,9 +201,11 @@ export function lowerJsonMethodCall(
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
// Preserve the existing dyn-root string ABI, including its textual
|
||||
// "undefined" result, while letting the runtime honor toJSON hooks.
|
||||
return value.type.kind === "dyn"
|
||||
// A dyn root can serialize to undefined (undefined, a function, a
|
||||
// symbol). Where the program can observe that (jsonResultObserved),
|
||||
// keep the actual undefined; elsewhere the dyn-root string ABI
|
||||
// stays, whose undefined result prints the same text.
|
||||
return (value.type.kind === "dyn" || toJsonClass) && !jsonResultObserved(lowerer, call)
|
||||
? { kind: "toString", operand: raw, type: STRING, loc }
|
||||
: { kind: "dynCheck", value: raw, type: lowerer.withUndefinedArm(STRING), loc };
|
||||
}
|
||||
@@ -290,6 +302,106 @@ function lowerOptionalStringifyRoot(
|
||||
return { kind: "call", callee: helper, args: [value], type: resultT, loc };
|
||||
}
|
||||
|
||||
/** True when a `JSON.stringify` result is used where JavaScript can tell
|
||||
* undefined from the text "undefined": compared with undefined/null,
|
||||
* `typeof`, `??`, `?.`, truth tests, a return from a function declaring
|
||||
* an undefined result, or an unannotated local binding whose references
|
||||
* do one of these. The lib types say `string`, so other uses (concatenation,
|
||||
* printing, string parameters) cannot tell the difference. */
|
||||
function jsonResultObserved(lowerer: Lowerer, call: ts.CallExpression): boolean {
|
||||
let node: ts.Node = call;
|
||||
while (ts.isParenthesizedExpression(node.parent)) node = node.parent;
|
||||
if (observesUndefined(node as ts.Expression)) return true;
|
||||
const parent = node.parent;
|
||||
if (ts.isReturnStatement(parent)) {
|
||||
let fn: ts.Node | undefined = parent.parent;
|
||||
while (fn && !ts.isFunctionLike(fn)) fn = fn.parent;
|
||||
const annotation = fn && ts.isFunctionLike(fn) ? fn.type : undefined;
|
||||
if (!annotation) return false;
|
||||
const type = lowerer.checker.getTypeAtLocation(annotation);
|
||||
const parts = type.flags & ts.TypeFlags.Union ? constituentTypes(type) : [type];
|
||||
return parts.some((part) => (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0);
|
||||
}
|
||||
// Module-scope bindings are globals whose slot type comes from the
|
||||
// checker's `string`; only function- and block-scoped locals adopt the
|
||||
// initializer's checked `string | undefined`.
|
||||
if (
|
||||
!ts.isVariableDeclaration(parent) ||
|
||||
parent.initializer !== node ||
|
||||
parent.type ||
|
||||
!ts.isIdentifier(parent.name) ||
|
||||
(ts.isVariableDeclarationList(parent.parent) &&
|
||||
ts.isVariableStatement(parent.parent.parent) &&
|
||||
ts.isSourceFile(parent.parent.parent.parent))
|
||||
)
|
||||
return false;
|
||||
const name = parent.name;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(name);
|
||||
if (!symbol) return false;
|
||||
let scope: ts.Node = parent;
|
||||
while (!ts.isBlock(scope.parent) && !ts.isSourceFile(scope.parent) && scope.parent)
|
||||
scope = scope.parent;
|
||||
let observed = false;
|
||||
const visit = (current: ts.Node): void => {
|
||||
if (observed) return;
|
||||
if (ts.isIdentifier(current)) {
|
||||
if (
|
||||
current !== name &&
|
||||
current.text === name.text &&
|
||||
lowerer.checker.getSymbolAtLocation(current) === symbol &&
|
||||
observesUndefined(current)
|
||||
)
|
||||
observed = true;
|
||||
return;
|
||||
}
|
||||
ts.forEachChild(current, visit);
|
||||
};
|
||||
visit(scope.parent ?? scope);
|
||||
return observed;
|
||||
}
|
||||
|
||||
function observesUndefined(expr: ts.Expression): boolean {
|
||||
let node: ts.Node = expr;
|
||||
while (ts.isParenthesizedExpression(node.parent)) node = node.parent;
|
||||
const parent = node.parent;
|
||||
if (ts.isTypeOfExpression(parent)) return true;
|
||||
if (ts.isPrefixUnaryExpression(parent) && parent.operator === ts.SyntaxKind.ExclamationToken)
|
||||
return true;
|
||||
if (
|
||||
(ts.isPropertyAccessExpression(parent) || ts.isElementAccessExpression(parent)) &&
|
||||
parent.expression === node &&
|
||||
parent.questionDotToken
|
||||
)
|
||||
return true;
|
||||
if (
|
||||
(ts.isIfStatement(parent) || ts.isWhileStatement(parent) || ts.isDoStatement(parent)) &&
|
||||
parent.expression === node
|
||||
)
|
||||
return true;
|
||||
if (ts.isConditionalExpression(parent) && parent.condition === node) return true;
|
||||
if (ts.isBinaryExpression(parent)) {
|
||||
const op = parent.operatorToken.kind;
|
||||
if (op === ts.SyntaxKind.QuestionQuestionToken) return parent.left === node;
|
||||
if (op === ts.SyntaxKind.AmpersandAmpersandToken || op === ts.SyntaxKind.BarBarToken)
|
||||
return parent.left === node;
|
||||
if (
|
||||
op === ts.SyntaxKind.EqualsEqualsEqualsToken ||
|
||||
op === ts.SyntaxKind.ExclamationEqualsEqualsToken ||
|
||||
op === ts.SyntaxKind.EqualsEqualsToken ||
|
||||
op === ts.SyntaxKind.ExclamationEqualsToken
|
||||
) {
|
||||
let other: ts.Node = parent.left === node ? parent.right : parent.left;
|
||||
while (ts.isParenthesizedExpression(other)) other = other.expression;
|
||||
return (
|
||||
other.kind === ts.SyntaxKind.NullKeyword ||
|
||||
(ts.isIdentifier(other) && other.text === "undefined") ||
|
||||
ts.isVoidExpression(other)
|
||||
);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 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. */
|
||||
|
||||
@@ -2,6 +2,7 @@ import { checkedPromiseAll } from "../checked-promise-all.js";
|
||||
import { InternalCompilerError } from "../../../errors.js";
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer } from "../lowerer.js";
|
||||
import { maybePromiseResolve, maybePromiseUnionOf } from "../maybe-promise.js";
|
||||
import { isJsSourceFile, locOf } from "../../program.js";
|
||||
import {
|
||||
DYN,
|
||||
@@ -402,6 +403,22 @@ export function lowerPromiseStaticCall(
|
||||
if (v.type.kind !== "promise") lowerer.badType(argNode, argT);
|
||||
return v;
|
||||
}
|
||||
const maybePromise = argIr ? maybePromiseUnionOf(lowerer, argIr) : null;
|
||||
if (maybePromise) {
|
||||
const resultT = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
const v = lowerer.lowerExpr(argNode);
|
||||
const mp = maybePromiseUnionOf(lowerer, v.type);
|
||||
const resolved =
|
||||
resultT?.kind === "promise" && mp
|
||||
? maybePromiseResolve(lowerer, v, mp, resultT, loc)
|
||||
: null;
|
||||
if (resolved) return resolved;
|
||||
lowerer.noLowering(
|
||||
`Promise.resolve over '${lowerer.fmt(v.type)}' at this result type`,
|
||||
argNode,
|
||||
"the promise arm passes through identically, so it must already be the result's promise type",
|
||||
);
|
||||
}
|
||||
if (
|
||||
argIr?.kind === "union" &&
|
||||
(lowerer.unions.get(argIr.unionId)?.arms ?? []).some((a) => a.kind === "promise")
|
||||
@@ -423,7 +440,17 @@ export function lowerPromiseStaticCall(
|
||||
}
|
||||
}
|
||||
}
|
||||
const resultT = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
let resultT = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
// A maybe-promise context (`return Promise.resolve(v)` from a function
|
||||
// returning `Promise<R | void> | R | void`) names its promise arm: the
|
||||
// fresh promise takes that payload type directly.
|
||||
if (argNode !== undefined && resultT?.kind === "promise") {
|
||||
const ctxT = lowerer.checker.getContextualType(call);
|
||||
const contextual = ctxT?.isUnionType() ? lowerer.mapTypeOf(ctxT) : null;
|
||||
const mp = contextual ? maybePromiseUnionOf(lowerer, contextual) : null;
|
||||
if (mp && !typeEquals(mp.promise, resultT) && mp.promise.inner.kind !== "void")
|
||||
resultT = mp.promise;
|
||||
}
|
||||
if (resultT?.kind !== "promise") {
|
||||
lowerer.noLowering(
|
||||
"Promise.resolve at this type",
|
||||
|
||||
@@ -546,7 +546,13 @@ export function lowerSpreadArgsCall(
|
||||
callee: IrExpr,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
if (!isJsSourceFile(expr.getSourceFile())) return null;
|
||||
// TypeScript reaches this lane only for checked-dynamic variadic callees
|
||||
// (`(...args: unknown[]) => R`), whose ABI is the JS one.
|
||||
if (
|
||||
!isJsSourceFile(expr.getSourceFile()) &&
|
||||
!(callee.type.kind === "func" && callee.type.rest === true && callee.type.restAbi === undefined)
|
||||
)
|
||||
return null;
|
||||
if (!expr.arguments.some((a) => ts.isSpreadElement(a))) return null;
|
||||
if (callee.type.kind !== "dyn" && callee.type.kind !== "func") return null;
|
||||
const calleeName =
|
||||
|
||||
@@ -13,7 +13,13 @@ import {
|
||||
typeEquals,
|
||||
} from "../../ir/ir.js";
|
||||
import { isJsSourceFile, isNodeEsmFile } from "../program.js";
|
||||
import { genResultRecord, jsOpenObjectType } from "../type-mapper.js";
|
||||
import {
|
||||
genResultRecord,
|
||||
isAnyArrayType,
|
||||
isUnitOnlyTsType,
|
||||
jsOpenObjectType,
|
||||
unitOnlyUnion,
|
||||
} from "../type-mapper.js";
|
||||
import { dynFallbackType } from "./lowerer.js";
|
||||
import { builtinModuleFnOf } from "./surfaces.js";
|
||||
import { mixinFnShapeOf } from "./lower-mixins.js";
|
||||
@@ -286,10 +292,17 @@ export function paramShape(lowerer: Lowerer, param: ts.ParameterDeclaration): Pa
|
||||
return { type: DYN, mode: "dynRest" };
|
||||
}
|
||||
}
|
||||
// `...args: unknown[]` / `...args: any[]` in TypeScript: the binding is
|
||||
// the checked-dynamic array (how `unknown[]` maps everywhere), so the
|
||||
// ABI is the JS variadic dyn form above — the function type marks it
|
||||
// with `rest` and no typed slot (the type mapper agrees).
|
||||
const restTs = lowerer.typeOf(param.name);
|
||||
if (isAnyArrayType(restTs, lowerer.checker)) return { type: DYN, mode: "dynRest" };
|
||||
const type = lowerer.runtimeOptionalBindingType(param.name, lowerer.irTypeOf(param.name));
|
||||
if (type.kind === "dyn") return { type: DYN, mode: "dynRest" };
|
||||
// Tuple-typed rest params don't map to an array; generic rest is the
|
||||
// generic path's business. Anything non-array here is unmappable.
|
||||
if (type.kind !== "array") lowerer.badType(param.name, lowerer.typeOf(param.name));
|
||||
if (type.kind !== "array") lowerer.badType(param.name, restTs);
|
||||
return { type, mode: "rest" };
|
||||
}
|
||||
if (param.initializer) {
|
||||
@@ -373,7 +386,7 @@ export function paramShapes(
|
||||
const tsType = symbol
|
||||
? lowerer.checker.getTypeOfSymbol(symbol)
|
||||
: lowerer.typeOf(declParam.name);
|
||||
const mapped = jsOpenObjectType(
|
||||
let mapped = jsOpenObjectType(
|
||||
declParam,
|
||||
lowerer.runtimeOptionalBindingType(
|
||||
declParam.name,
|
||||
@@ -382,6 +395,11 @@ export function paramShapes(
|
||||
lowerer.shapes,
|
||||
lowerer.unions,
|
||||
);
|
||||
// An instantiation with a literal `undefined` argument (`id(undefined)`
|
||||
// binds T = undefined): standalone undefined maps to VOID (the return
|
||||
// convention), and a parameter is a value position, so it takes the
|
||||
// unit-only slot like a declared `v: undefined` parameter.
|
||||
if (mapped?.kind === "void" && isUnitOnlyTsType(tsType)) mapped = unitOnlyUnion(lowerer.unions);
|
||||
if (!mapped || mapped.kind === "void")
|
||||
lowerer.badType(blameOf?.(declParam, i) ?? declParam.name, tsType);
|
||||
if (declParam.dotDotDotToken) {
|
||||
@@ -861,13 +879,34 @@ export function lambdaSignature(
|
||||
ctxDecl && !ctxDecl.getSourceFile().isDeclarationFile
|
||||
? lowerer.mapTypeOf(lowerer.checker.getReturnTypeOfSignature(ctxSigs[0]!))
|
||||
: null;
|
||||
const ctxRet = isAsyncLike && ctxRetRaw?.kind === "promise" ? ctxRetRaw.inner : ctxRetRaw;
|
||||
// An async lambda always produces a promise, so a slot returning a
|
||||
// promise-armed union (`Promise<R | void> | R | void`, `Promise<void>
|
||||
// | void`) is met through that promise arm: its payload is the ABI.
|
||||
const ctxPromiseArm =
|
||||
isAsyncLike && ctxRetRaw?.kind === "union"
|
||||
? lowerer.unions.get(ctxRetRaw.unionId)?.arms.filter((arm) => arm.kind === "promise")
|
||||
: undefined;
|
||||
const ctxRet =
|
||||
isAsyncLike && ctxRetRaw?.kind === "promise"
|
||||
? ctxRetRaw.inner
|
||||
: ctxPromiseArm?.length === 1 && ctxPromiseArm[0]!.kind === "promise"
|
||||
? ctxPromiseArm[0]!.inner
|
||||
: ctxRetRaw;
|
||||
// A TypeScript variadic lambda (`(...args) => handler(...args)`) in a
|
||||
// checked-dynamic variadic slot returning `unknown` adopts that result
|
||||
// too: rest closures have no function-value adapter, so the closure
|
||||
// must match the slot's ABI exactly.
|
||||
const dynVariadicSlot =
|
||||
ctxRet?.kind === "dyn" &&
|
||||
!isJsSourceFile(node.getSourceFile()) &&
|
||||
shapes.some((shape) => shape.mode === "dynRest");
|
||||
if (
|
||||
(ctxRet?.kind === "union" || ctxRet?.kind === "array" || ctxRet?.kind === "record") &&
|
||||
(innerRet.kind !== "void" ||
|
||||
(ctxRet.kind === "union" && lowerer.armTag(ctxRet.unionId, UNDEFINED_T) >= 0))
|
||||
dynVariadicSlot ||
|
||||
((ctxRet?.kind === "union" || ctxRet?.kind === "array" || ctxRet?.kind === "record") &&
|
||||
(innerRet.kind !== "void" ||
|
||||
(ctxRet.kind === "union" && lowerer.armTag(ctxRet.unionId, UNDEFINED_T) >= 0)))
|
||||
) {
|
||||
ret = isAsyncLike ? { kind: "promise", inner: ctxRet } : ctxRet;
|
||||
ret = isAsyncLike ? { kind: "promise", inner: ctxRet! } : ctxRet!;
|
||||
}
|
||||
// A VOID slot discards the callback's result (TS's void-returning
|
||||
// assignability rule; JS ignores the value), so an UNANNOTATED
|
||||
|
||||
@@ -830,6 +830,12 @@ export class ClassDynamicDispatch {
|
||||
break;
|
||||
case "dynCheck":
|
||||
if (isDynTypedRefType(expr.type)) rewrite.add(fn);
|
||||
// A dynamic function checked into a native function type
|
||||
// receives its arguments as dynamic values: class instances
|
||||
// passed to it cross the boundary when it is called (a JS
|
||||
// `list.map((rec) => rec.match(x))` callback with an untyped
|
||||
// parameter).
|
||||
if (expr.type.kind === "func") expr.type.params.forEach(discover);
|
||||
break;
|
||||
case "libCall":
|
||||
if (
|
||||
|
||||
@@ -379,12 +379,48 @@ function reflectedMethodValue(
|
||||
needsNativeReceiver = true;
|
||||
});
|
||||
if (needsNativeReceiver) return null;
|
||||
// The method reads itself as a value inside its own body: it is the
|
||||
// closure its lowering in progress produces.
|
||||
if (lowerer.methodLambdasInProgress.has(member)) {
|
||||
const pending = lowerer.methodLambdasInProgress.get(member);
|
||||
if (!pending) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
member,
|
||||
"class methods that read their own value while their signature is computed",
|
||||
);
|
||||
}
|
||||
pending.selfReads++;
|
||||
return {
|
||||
kind: "closure",
|
||||
fnName: pending.fnName,
|
||||
captures: [],
|
||||
type: pending.type,
|
||||
loc: locOf(member),
|
||||
};
|
||||
}
|
||||
const context = newFnCtx(false, null, null, DYN);
|
||||
const previousClass = lowerer.currentClass;
|
||||
lowerer.currentClass = info;
|
||||
lowerer.fnStack.push(context);
|
||||
lowerer.methodLambdasInProgress.set(member, null);
|
||||
try {
|
||||
const value = lowerer.lowerLambda(member);
|
||||
const pending = lowerer.methodLambdasInProgress.get(member);
|
||||
if (
|
||||
pending &&
|
||||
pending.selfReads > 0 &&
|
||||
(value.kind !== "closure" ||
|
||||
value.fnName !== pending.fnName ||
|
||||
value.captures.length > 0 ||
|
||||
!typeEquals(value.type, pending.type))
|
||||
) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
member,
|
||||
"class methods that read their own value inside their body when the method's value type is only settled by its body",
|
||||
);
|
||||
}
|
||||
if (value.kind === "closure" && value.captures.length === 0) {
|
||||
const body = lowerer.liftedFns.find((fn) => fn.name === value.fnName);
|
||||
if (body?.captures?.length === 0) delete body.captures;
|
||||
@@ -395,6 +431,7 @@ function reflectedMethodValue(
|
||||
cache.set(method, value);
|
||||
return value;
|
||||
} finally {
|
||||
lowerer.methodLambdasInProgress.delete(member);
|
||||
lowerer.fnStack.pop();
|
||||
lowerer.currentClass = previousClass;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,325 @@
|
||||
import { varRef } from "../../ir/build.js";
|
||||
import { BOOL, DYN, F64, STRING, type IrExpr, type IrStmt, type IrType } from "../../ir/ir.js";
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { accessorCall, findMethodOn, type ClassInfo } from "./lower-classes.js";
|
||||
import { isClassCallback } from "./class-callbacks.js";
|
||||
import { isSafeToRepeat } from "./expressions/evaluation-safety.js";
|
||||
|
||||
/** The ToPrimitive hint of a conversion: `String(x)` and template literals
|
||||
* take "string", `+` "default", `Number(x)` "number". */
|
||||
export type ConversionHint = "string" | "default" | "number";
|
||||
export type StringConversionHint = "string" | "default";
|
||||
|
||||
type ConversionMethod = "sym:toPrimitive" | "valueOf" | "toString";
|
||||
|
||||
/** How an instance of one class converts: through one of its methods, or
|
||||
* through Object.prototype.toString ("[object Object]"). */
|
||||
type Plan = { method: ConversionMethod; ret: IrType } | { method: null };
|
||||
|
||||
const PRIMITIVE_KINDS = new Set(["f64", "string", "bool", "bigint", "undefinedT", "nullT"]);
|
||||
|
||||
/** ToString(ToPrimitive(value, hint)) of a program class instance, as Node
|
||||
* computes it: `[Symbol.toPrimitive](hint)` when the class defines it, then
|
||||
* `toString` (hint "string") or `valueOf` and then `toString` (hint
|
||||
* "default"), and "[object Object]" when the class defines neither. The
|
||||
* methods dispatch like calls of them; subclasses that convert through a
|
||||
* different method are tested first. Null for values whose class is not a
|
||||
* plain program class (builtin classes and their subclasses keep their own
|
||||
* conversions). */
|
||||
export function classToStringConversion(
|
||||
lowerer: Lowerer,
|
||||
value: IrExpr,
|
||||
hint: StringConversionHint,
|
||||
node: ts.Node,
|
||||
): IrExpr | null {
|
||||
return classConversion(lowerer, value, hint, node, STRING, (primitive) =>
|
||||
lowerer.ensureString(primitive, node),
|
||||
);
|
||||
}
|
||||
|
||||
/** ToNumber(ToPrimitive(value, "number")) of a program class instance
|
||||
* (`Number(x)`): `[Symbol.toPrimitive]("number")`, then `valueOf` and then
|
||||
* `toString`, and NaN for "[object Object]". Null as for
|
||||
* classToStringConversion. */
|
||||
export function classToNumberConversion(
|
||||
lowerer: Lowerer,
|
||||
value: IrExpr,
|
||||
node: ts.Node,
|
||||
operator: "Number" | "unary plus" = "Number",
|
||||
): IrExpr | null {
|
||||
return classConversion(lowerer, value, "number", node, F64, (primitive) =>
|
||||
numberOfPrimitive(lowerer, primitive, node, operator),
|
||||
);
|
||||
}
|
||||
|
||||
/** ToNumber of a primitive value: `Number(primitive)`, or unary plus,
|
||||
* which throws for a BigInt instead of converting it. */
|
||||
function numberOfPrimitive(
|
||||
lowerer: Lowerer,
|
||||
value: IrExpr,
|
||||
node: ts.Node,
|
||||
operator: "Number" | "unary plus",
|
||||
): IrExpr {
|
||||
const loc = value.loc;
|
||||
if (operator === "unary plus" && mayBeBigint(lowerer, value.type))
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
node,
|
||||
"unary plus of class instances whose conversion can return a bigint",
|
||||
);
|
||||
switch (value.type.kind) {
|
||||
case "f64":
|
||||
return value;
|
||||
case "string":
|
||||
return { kind: "libCall", fn: "num.fromString", args: [value], type: F64, loc };
|
||||
case "bool":
|
||||
return {
|
||||
kind: "ternary",
|
||||
cond: value,
|
||||
then: { kind: "numLit", value: 1, type: F64, loc },
|
||||
else_: { kind: "numLit", value: 0, type: F64, loc },
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
case "bigint":
|
||||
return { kind: "libCall", fn: "bigint.toF64", args: [value], type: F64, loc };
|
||||
default:
|
||||
if (!lowerer.dynConvertible(value.type))
|
||||
lowerer.unsupported("SC1090", node, "number conversions of this class's primitive value");
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "dyn.numberConstructor",
|
||||
args: [lowerer.coerceToExpected(value, DYN)],
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function mayBeBigint(lowerer: Lowerer, type: IrType): boolean {
|
||||
if (type.kind === "bigint") return true;
|
||||
return (
|
||||
type.kind === "union" &&
|
||||
lowerer.unions.get(type.unionId)?.arms.some((arm) => arm.kind === "bigint") === true
|
||||
);
|
||||
}
|
||||
|
||||
function classConversion(
|
||||
lowerer: Lowerer,
|
||||
value: IrExpr,
|
||||
hint: ConversionHint,
|
||||
node: ts.Node,
|
||||
resultType: IrType,
|
||||
finish: (primitive: IrExpr) => IrExpr,
|
||||
): IrExpr | null {
|
||||
if (value.type.kind !== "object") return null;
|
||||
const info = lowerer.classes.get(value.type.className);
|
||||
if (!info || !plainProgramClass(info)) return null;
|
||||
const classes = subtree(lowerer, info);
|
||||
for (const cls of classes) refuseReplacedConversions(lowerer, cls, node);
|
||||
const plans = new Map(classes.map((cls) => [cls, planOf(lowerer, cls, hint, node)]));
|
||||
// A class whose plan differs from its base's starts a group; the deepest
|
||||
// groups are tested first, so each instance takes its own class's plan.
|
||||
const tops = classes
|
||||
.filter((cls) => {
|
||||
if (cls === info) return false;
|
||||
const base = cls.base;
|
||||
return base !== null && plans.has(base) && !samePlan(plans.get(base)!, plans.get(cls)!);
|
||||
})
|
||||
.sort((a, b) => depth(b) - depth(a));
|
||||
const loc = value.loc;
|
||||
const prefix: IrStmt[] = [];
|
||||
let receiver = value;
|
||||
if (tops.length > 0 && !isSafeToRepeat(value)) {
|
||||
const local = lowerer.declareHiddenLocal("%converted", value.type);
|
||||
prefix.push({ kind: "varDecl", localId: local.id, init: value, loc });
|
||||
receiver = varRef(local.id, value.type, loc);
|
||||
}
|
||||
const convert = (cls: ClassInfo, object: IrExpr): IrExpr => {
|
||||
const plan = plans.get(cls)!;
|
||||
if (plan.method === null)
|
||||
return finish({ kind: "strLit", value: "[object Object]", type: STRING, loc });
|
||||
const args: IrExpr[] =
|
||||
plan.method === "sym:toPrimitive" ? [{ kind: "strLit", value: hint, type: STRING, loc }] : [];
|
||||
return finish(accessorCall(lowerer, cls.def.name, plan.method, object, args, plan.ret, loc));
|
||||
};
|
||||
let result = convert(info, receiver);
|
||||
for (let i = tops.length - 1; i >= 0; i--) {
|
||||
const top = tops[i]!;
|
||||
const narrowed: IrExpr = {
|
||||
kind: "downcast",
|
||||
value: receiver,
|
||||
type: { kind: "object", className: top.def.name },
|
||||
loc,
|
||||
};
|
||||
result = {
|
||||
kind: "ternary",
|
||||
cond: { kind: "instanceOf", value: receiver, className: top.def.name, type: BOOL, loc },
|
||||
then: convert(top, narrowed),
|
||||
else_: result,
|
||||
type: resultType,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return prefix.length > 0
|
||||
? { kind: "seqExpr", stmts: prefix, result, type: resultType, loc }
|
||||
: result;
|
||||
}
|
||||
|
||||
/** A class of the program whose chain reaches no builtin base: its
|
||||
* instances inherit Object.prototype's conversions. */
|
||||
function plainProgramClass(info: ClassInfo): boolean {
|
||||
for (let cls: ClassInfo | null = info; cls; cls = cls.base) {
|
||||
if (
|
||||
cls.def.runtime ||
|
||||
cls.builtinError ||
|
||||
cls.builtinEmitter ||
|
||||
cls.builtinStream ||
|
||||
cls.callableBase !== undefined
|
||||
)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function subtree(lowerer: Lowerer, info: ClassInfo): ClassInfo[] {
|
||||
const out: ClassInfo[] = [];
|
||||
const visit = (cls: ClassInfo): void => {
|
||||
out.push(cls);
|
||||
for (const child of cls.subclasses) visit(child);
|
||||
};
|
||||
visit(info);
|
||||
return out.filter(
|
||||
(cls) => cls === info || lowerer.classCanBeConstructed(cls) || cls.subclasses.length > 0,
|
||||
);
|
||||
}
|
||||
|
||||
function depth(info: ClassInfo): number {
|
||||
let n = 0;
|
||||
for (let cls = info.base; cls; cls = cls.base) n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
function samePlan(a: Plan, b: Plan): boolean {
|
||||
return a.method === b.method;
|
||||
}
|
||||
|
||||
/** A conversion method the class declares or inherits (a field of that
|
||||
* name is an instance property the program sets, refused separately). */
|
||||
function declared(
|
||||
lowerer: Lowerer,
|
||||
info: ClassInfo,
|
||||
method: ConversionMethod,
|
||||
): { ret: IrType } | null {
|
||||
if (info.fields.has(method)) return null;
|
||||
const found = findMethodOn(lowerer, info, method);
|
||||
return found ? { ret: found.sig.ret } : null;
|
||||
}
|
||||
|
||||
function planOf(lowerer: Lowerer, info: ClassInfo, hint: ConversionHint, node: ts.Node): Plan {
|
||||
const exotic = declared(lowerer, info, "sym:toPrimitive");
|
||||
if (exotic) {
|
||||
if (!primitive(lowerer, exotic.ret))
|
||||
refuse(lowerer, node, info, "a [Symbol.toPrimitive] method that can return an object");
|
||||
return { method: "sym:toPrimitive", ret: exotic.ret };
|
||||
}
|
||||
// OrdinaryToPrimitive: "string" tries toString first, which every object
|
||||
// has (Object.prototype.toString answers a string); "default" and
|
||||
// "number" try valueOf first, whose Object.prototype version answers the
|
||||
// object itself.
|
||||
const order: ConversionMethod[] = hint === "string" ? ["toString"] : ["valueOf", "toString"];
|
||||
for (const method of order) {
|
||||
const found = declared(lowerer, info, method);
|
||||
if (!found) continue;
|
||||
if (!primitive(lowerer, found.ret))
|
||||
refuse(lowerer, node, info, `a '${method}' method that can return an object`);
|
||||
return { method, ret: found.ret };
|
||||
}
|
||||
return { method: null };
|
||||
}
|
||||
|
||||
function primitive(lowerer: Lowerer, type: IrType): boolean {
|
||||
if (type.kind === "union")
|
||||
return (
|
||||
lowerer.unions.get(type.unionId)?.arms.every((arm) => PRIMITIVE_KINDS.has(arm.kind)) === true
|
||||
);
|
||||
return PRIMITIVE_KINDS.has(type.kind);
|
||||
}
|
||||
|
||||
/** Conversions the program can change at run time (an instance property
|
||||
* of a method's name, a prototype write, a Symbol.toStringTag) would not be
|
||||
* the class's declared methods. */
|
||||
function refuseReplacedConversions(lowerer: Lowerer, info: ClassInfo, node: ts.Node): void {
|
||||
for (const method of ["toString", "valueOf"] as const) {
|
||||
if (info.fields.has(method) || isClassCallback(lowerer, info, method))
|
||||
refuse(lowerer, node, info, `a '${method}' instance property`);
|
||||
if (lowerer.prototypeMethodAccesses.has(method))
|
||||
refuse(lowerer, node, info, `a '${method}' the program reaches through a prototype`);
|
||||
}
|
||||
if (findMethodOn(lowerer, info, "get:sym:toStringTag") || info.fields.has("sym:toStringTag"))
|
||||
refuse(lowerer, node, info, "a Symbol.toStringTag");
|
||||
}
|
||||
|
||||
function refuse(lowerer: Lowerer, node: ts.Node, info: ClassInfo, what: string): never {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
node,
|
||||
`conversions of '${info.def.jsName ?? info.def.name}' instances to primitives (the class has ${what})`,
|
||||
);
|
||||
}
|
||||
|
||||
/** ToString(ToPrimitive(value, hint)) of a union whose arms are program
|
||||
* class instances and primitives (`Box | undefined`): each arm converts as
|
||||
* it would alone. Null for other unions. */
|
||||
export function classUnionToString(
|
||||
lowerer: Lowerer,
|
||||
value: IrExpr,
|
||||
hint: StringConversionHint,
|
||||
node: ts.Node,
|
||||
): IrExpr | null {
|
||||
if (value.type.kind !== "union") return null;
|
||||
const unionId = value.type.unionId;
|
||||
const arms = lowerer.unions.get(unionId)?.arms;
|
||||
if (!arms || !arms.some((arm) => arm.kind === "object")) return null;
|
||||
const convertible = (arm: IrType): boolean => {
|
||||
if (PRIMITIVE_KINDS.has(arm.kind)) return true;
|
||||
if (arm.kind !== "object") return false;
|
||||
const info = lowerer.classes.get(arm.className);
|
||||
return info !== undefined && plainProgramClass(info);
|
||||
};
|
||||
if (!arms.every(convertible)) return null;
|
||||
const loc = value.loc;
|
||||
const prefix: IrStmt[] = [];
|
||||
let stable = value;
|
||||
if (!isSafeToRepeat(value)) {
|
||||
const local = lowerer.declareHiddenLocal("%converted", value.type);
|
||||
prefix.push({ kind: "varDecl", localId: local.id, init: value, loc });
|
||||
stable = varRef(local.id, value.type, loc);
|
||||
}
|
||||
const convertArm = (tag: number): IrExpr => {
|
||||
const arm = arms[tag]!;
|
||||
if (arm.kind === "undefinedT" || arm.kind === "nullT")
|
||||
return {
|
||||
kind: "strLit",
|
||||
value: arm.kind === "nullT" ? "null" : "undefined",
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
const narrowed: IrExpr = { kind: "unionNarrow", unionId, tag, value: stable, type: arm, loc };
|
||||
if (arm.kind === "object") return classToStringConversion(lowerer, narrowed, hint, node)!;
|
||||
return lowerer.ensureString(narrowed, node);
|
||||
};
|
||||
let result = convertArm(arms.length - 1);
|
||||
for (let tag = arms.length - 2; tag >= 0; tag--) {
|
||||
result = {
|
||||
kind: "ternary",
|
||||
cond: { kind: "unionIsTag", unionId, tag, negated: false, value: stable, type: BOOL, loc },
|
||||
then: convertArm(tag),
|
||||
else_: result,
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return prefix.length > 0 ? { kind: "seqExpr", stmts: prefix, result, type: STRING, loc } : result;
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from "../../../ir/ir.js";
|
||||
import { typeKey } from "../../type-mapper.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
import { maybePromiseUnionOf } from "../maybe-promise.js";
|
||||
|
||||
/** Wrap a zero-argument function whose array result needs an element
|
||||
* conversion. Each invocation creates a fresh converted array; the factory
|
||||
@@ -96,6 +97,11 @@ export function coercibleValue(lowerer: Lowerer, src: IrType, dst: IrType): bool
|
||||
if (dst.kind === "union") {
|
||||
if (src.kind === "union") return lowerer.unionRetagMappable(src.unionId, dst.unionId);
|
||||
if (src.kind === "void") return lowerer.armTag(dst.unionId, UNDEFINED_T) >= 0;
|
||||
if (src.kind === "promise") {
|
||||
// A maybe-promise union's promise arm accepts payload widening.
|
||||
const mp = maybePromiseUnionOf(lowerer, dst);
|
||||
if (mp) return lowerer.promisePayloadWidens(src, mp.promise);
|
||||
}
|
||||
return !isUnitType(src) && lowerer.armTag(dst.unionId, src) >= 0;
|
||||
}
|
||||
if (src.kind === "union") {
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
} from "../optional-arguments.js";
|
||||
import { lowerCheckedArrayFrom } from "./array-construction.js";
|
||||
import { lowerToNumberArgument } from "../lower-exprs.js";
|
||||
import { lowerBytesIteratorValue } from "../iterator-adapters.js";
|
||||
|
||||
/** TypedArray.prototype.sort and toSorted. The receiver and comparator
|
||||
* expressions are evaluated before entering the helper (buildBytesSortFn).
|
||||
@@ -462,6 +463,21 @@ export function lowerBytesMethodCall(
|
||||
if (name === "sort" || name === "toSorted") {
|
||||
return lowerBytesSortCall(lowerer, call, access, receiverIr);
|
||||
}
|
||||
// values()/keys()/entries() as iterator objects (for...of heads and
|
||||
// stored numeric iterators lower before this).
|
||||
if ((name === "values" || name === "keys" || name === "entries") && nArgs === 0) {
|
||||
const type = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
if (type?.kind === "generator") {
|
||||
const value = lowerBytesIteratorValue(
|
||||
lowerer,
|
||||
lowerer.lowerExpr(access.expression),
|
||||
name,
|
||||
type,
|
||||
loc,
|
||||
);
|
||||
if (value) return value;
|
||||
}
|
||||
}
|
||||
if (name === "toReversed") {
|
||||
if (nArgs !== 0) {
|
||||
lowerer.noLowering(`.toReversed with ${nArgs} arguments on typed arrays`, call);
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { Lowerer } from "../lowerer.js";
|
||||
import {
|
||||
BOOL,
|
||||
CAUGHT,
|
||||
DYN,
|
||||
F64,
|
||||
type IrMapIntrinsicMethod,
|
||||
type IrSetIntrinsicMethod,
|
||||
@@ -28,12 +29,18 @@ import { concatStrings, throwTypeError } from "./dynamic-receivers.js";
|
||||
import {
|
||||
type GenType,
|
||||
generatorLoopBinding,
|
||||
resultRecordOf,
|
||||
genStep,
|
||||
genValue,
|
||||
stepDrivable,
|
||||
} from "../lower-generators.js";
|
||||
import { exitBeforeReturns } from "./for-of.js";
|
||||
import { iteratorSlotAdapter } from "../iterator-adapters.js";
|
||||
import {
|
||||
iteratorSlotAdapter,
|
||||
lowerArrayIteratorValue,
|
||||
lowerBytesIteratorValue,
|
||||
lowerStringIteratorValue,
|
||||
} from "../iterator-adapters.js";
|
||||
import { lowerArrayCallback } from "./callback-arguments.js";
|
||||
|
||||
/* ── iterator helpers (ES2025) ─────────────────────────────────────────── */
|
||||
@@ -189,6 +196,24 @@ export function lowerGeneratorHelperCall(
|
||||
collectionSeed(lowerer, source),
|
||||
);
|
||||
}
|
||||
// Iterator.from(arr) walks the array by index.
|
||||
const arrayRoot = arrayIteratorCall(lowerer, cur);
|
||||
if (arrayRoot !== null) {
|
||||
const items = lowerer.lowerExpr(arrayRoot.receiver);
|
||||
if (items.type.kind !== "array")
|
||||
lowerer.badType(arrayRoot.receiver, lowerer.typeOf(arrayRoot.receiver));
|
||||
const elem =
|
||||
arrayRoot.proj === "entries" ? entryPairType(lowerer, items.type.elem) : items.type.elem;
|
||||
return lowerFusedChain(
|
||||
lowerer,
|
||||
call,
|
||||
name,
|
||||
stages,
|
||||
{ kind: "array", type: items.type, proj: arrayRoot.proj },
|
||||
items,
|
||||
elem,
|
||||
);
|
||||
}
|
||||
const src = lowerHelperSource(lowerer, cur);
|
||||
return lowerFusedChain(
|
||||
lowerer,
|
||||
@@ -201,6 +226,128 @@ export function lowerGeneratorHelperCall(
|
||||
);
|
||||
}
|
||||
|
||||
/** `h.return!()` or `h.return?.()` on a helper object. The lib declares
|
||||
* return() as optional on iterator objects, and a helper object always
|
||||
* has one, so both spellings are the call: the helper completes and closes
|
||||
* its source, and the result is `{ value: undefined, done: true }`. The
|
||||
* receiver must be provably a helper object — a helper stage call, or a
|
||||
* const binding initialized with one — since other iterator objects (the
|
||||
* array, string, Map and Set iterators) have no return(). Null otherwise,
|
||||
* and for the plain `h.return()` spelling (lowerGenMethodCall's). */
|
||||
export function lowerHelperReturnCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null {
|
||||
let callee: ts.Expression = call.expression;
|
||||
const nonNull = ts.isNonNullExpression(callee);
|
||||
if (nonNull) callee = (callee as ts.NonNullExpression).expression;
|
||||
if (!nonNull && call.questionDotToken === undefined) return null;
|
||||
if (!ts.isPropertyAccessExpression(callee) || callee.questionDotToken) return null;
|
||||
if (callee.name.text !== "return" || call.arguments.length !== 0) return null;
|
||||
if (!isHelperObject(lowerer, callee.expression) || !lowerer.isStdlibMember(callee)) return null;
|
||||
const callT = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
const gen = lowerer.lowerExpr(callee.expression);
|
||||
if (gen.type.kind !== "generator" || gen.type.async) return null;
|
||||
const loc = locOf(call);
|
||||
const recT = resultRecordOf(lowerer, gen.type);
|
||||
const resume: IrExpr = { kind: "genResume", mode: "return", gen, arg: null, type: recT, loc };
|
||||
// `?.` types the call `IteratorResult | undefined`, which has no mapping
|
||||
// as a whole; the result is never undefined, so it is the record itself.
|
||||
if (callT === null || typeEquals(callT, recT)) return resume;
|
||||
const result = lowerer.coerceToExpected(resume, callT);
|
||||
if (!typeEquals(result.type, callT)) lowerer.badType(call, lowerer.typeOf(call));
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Whether `expr` is a helper stage call, or a const binding initialized
|
||||
* with one. */
|
||||
function isHelperObject(lowerer: Lowerer, expr: ts.Expression): boolean {
|
||||
while (ts.isParenthesizedExpression(expr)) expr = expr.expression;
|
||||
if (isStageCall(lowerer, expr)) return true;
|
||||
if (!ts.isIdentifier(expr)) return false;
|
||||
const symbol = lowerer.resolveValueSymbol(expr);
|
||||
const decl = symbol ? lowerer.checker.valueDeclarationOf(symbol) : undefined;
|
||||
if (
|
||||
!decl ||
|
||||
!ts.isVariableDeclaration(decl) ||
|
||||
decl.initializer === undefined ||
|
||||
!ts.isVariableDeclarationList(decl.parent) ||
|
||||
(decl.parent.flags & ts.NodeFlags.Const) === 0
|
||||
)
|
||||
return false;
|
||||
let init: ts.Expression = decl.initializer;
|
||||
while (ts.isParenthesizedExpression(init)) init = init.expression;
|
||||
return isStageCall(lowerer, init);
|
||||
}
|
||||
|
||||
/** `Iterator.from(value)` over a typed built-in iterable: the value's own
|
||||
* iterator, which already inherits from Iterator.prototype, so Node returns
|
||||
* it rather than a wrapper. A generator (including a helper object or an
|
||||
* array iterator) is itself; an array, string or typed array gives its
|
||||
* values iterator; a Set its values and a Map its entries, adapted into a
|
||||
* generator over the live collection. The result converts into the call's
|
||||
* IteratorObject type. Null for other arguments (objects with their own
|
||||
* next method, untyped values), which keep the Iterator fence. */
|
||||
export function lowerIteratorFromCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
access: ts.PropertyAccessExpression,
|
||||
): IrExpr | null {
|
||||
if (access.name.text !== "from" || call.arguments.length !== 1) return null;
|
||||
const argNode = call.arguments[0]!;
|
||||
if (ts.isSpreadElement(argNode)) return null;
|
||||
const resultT = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
const argT = lowerer.mapTypeOf(lowerer.typeOf(argNode));
|
||||
if (resultT?.kind !== "generator" || resultT.async || argT === null) return null;
|
||||
const iterableKinds = ["generator", "array", "string", "bytes", "set", "map"];
|
||||
if (!iterableKinds.includes(argT.kind) || (argT.kind === "generator" && argT.async)) return null;
|
||||
const loc = locOf(call);
|
||||
const value = lowerer.lowerExpr(argNode);
|
||||
const own = (yieldT: IrType): GenType => ({
|
||||
kind: "generator",
|
||||
yieldT,
|
||||
retT: VOID,
|
||||
nextT: UNDEFINED_T,
|
||||
});
|
||||
let iterator: IrExpr | null = null;
|
||||
const t = value.type;
|
||||
if (t.kind === "generator") iterator = value;
|
||||
else if (t.kind === "array")
|
||||
iterator = lowerArrayIteratorValue(lowerer, value, "values", own(t.elem), loc);
|
||||
else if (t.kind === "string")
|
||||
iterator = lowerStringIteratorValue(lowerer, value, own(STRING), loc);
|
||||
else if (t.kind === "bytes")
|
||||
iterator = lowerBytesIteratorValue(lowerer, value, "values", own(F64), loc);
|
||||
else if (t.kind === "set" || t.kind === "map") {
|
||||
const elem = t.kind === "set" ? t.elem : entryPairTypeOf(lowerer, t.key, t.value);
|
||||
const native: IrExpr = {
|
||||
kind: "dynInvoke",
|
||||
recv: lowerer.coerceToExpected(value, DYN),
|
||||
method: t.kind === "set" ? "values" : "entries",
|
||||
calleeName: t.kind === "set" ? "set.values" : "map.entries",
|
||||
args: [],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
iterator = iteratorSlotAdapter(lowerer, native, own(elem));
|
||||
}
|
||||
if (iterator === null) lowerer.badType(argNode, lowerer.typeOf(argNode));
|
||||
const result = lowerer.coerceToExpected(iterator, resultT);
|
||||
if (!typeEquals(result.type, resultT)) lowerer.badType(call, lowerer.typeOf(call));
|
||||
return result;
|
||||
}
|
||||
|
||||
/** The interned [K, V] pair a Map's entries yield. */
|
||||
function entryPairTypeOf(lowerer: Lowerer, keyT: IrType, valueT: IrType): IrType {
|
||||
return {
|
||||
kind: "record",
|
||||
shapeId: lowerer.shapes.intern(
|
||||
[
|
||||
{ name: "0", type: keyT },
|
||||
{ name: "1", type: valueT },
|
||||
],
|
||||
true,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
const ITER_STAGES = new Set(["map", "filter", "take", "drop", "flatMap"]);
|
||||
const ITER_TERMINALS = new Set(["toArray", "forEach", "reduce", "some", "every", "find"]);
|
||||
/** Native iterators that ride checked runtime values and adapt into
|
||||
@@ -353,12 +500,30 @@ export function lowerForOfHelperChain(
|
||||
}
|
||||
}
|
||||
|
||||
/** `arr.values()` or `arr.entries()` on an array value, as the root of a
|
||||
* helper chain. */
|
||||
/** The argument of an `Iterator.from(x)` call, or null. As a chain's root,
|
||||
* the call is the argument's own iterator, so the chain can walk the
|
||||
* argument directly. */
|
||||
function iteratorFromArgument(lowerer: Lowerer, expr: ts.Expression): ts.Expression | null {
|
||||
if (!ts.isCallExpression(expr) || !ts.isPropertyAccessExpression(expr.expression)) return null;
|
||||
if (expr.questionDotToken || expr.expression.questionDotToken || expr.arguments.length !== 1)
|
||||
return null;
|
||||
const arg = expr.arguments[0]!;
|
||||
if (ts.isSpreadElement(arg) || expr.expression.name.text !== "from") return null;
|
||||
if (!lowerer.isStdlibGlobal(expr.expression.expression, "Iterator")) return null;
|
||||
return lowerer.isStdlibMember(expr.expression) ? arg : null;
|
||||
}
|
||||
|
||||
/** `arr.values()`, `arr.entries()` or `Iterator.from(arr)` on an array value,
|
||||
* as the root of a helper chain. */
|
||||
function arrayIteratorCall(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.Expression,
|
||||
): { receiver: ts.Expression; proj: "values" | "entries" } | null {
|
||||
const fromArg = iteratorFromArgument(lowerer, expr);
|
||||
if (fromArg !== null)
|
||||
return lowerer.mapTypeOf(lowerer.typeOf(fromArg))?.kind === "array"
|
||||
? { receiver: fromArg, proj: "values" }
|
||||
: null;
|
||||
if (!ts.isCallExpression(expr) || !ts.isPropertyAccessExpression(expr.expression)) return null;
|
||||
if (expr.questionDotToken || expr.expression.questionDotToken || expr.arguments.length !== 0)
|
||||
return null;
|
||||
@@ -401,6 +566,17 @@ function collectionIteratorCall(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.Expression,
|
||||
): { receiver: ts.Expression; proj: "keys" | "values" | "entries" } | null {
|
||||
// Iterator.from(set) is the Set's values iterator, Iterator.from(map) the
|
||||
// Map's entries iterator.
|
||||
const fromArg = iteratorFromArgument(lowerer, expr);
|
||||
if (fromArg !== null) {
|
||||
const kind = lowerer.mapTypeOf(lowerer.typeOf(fromArg))?.kind;
|
||||
return kind === "set"
|
||||
? { receiver: fromArg, proj: "values" }
|
||||
: kind === "map"
|
||||
? { receiver: fromArg, proj: "entries" }
|
||||
: null;
|
||||
}
|
||||
if (!ts.isCallExpression(expr) || !ts.isPropertyAccessExpression(expr.expression)) return null;
|
||||
if (expr.questionDotToken || expr.expression.questionDotToken || expr.arguments.length !== 0)
|
||||
return null;
|
||||
@@ -480,6 +656,11 @@ function lowerHelperSource(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.Expression,
|
||||
): { gen: IrExpr & { type: GenType }; elem: IrType } {
|
||||
// Iterator.from(gen) is the generator itself; the call's IteratorObject
|
||||
// type would add an adapter generator over it.
|
||||
const fromArg = iteratorFromArgument(lowerer, expr);
|
||||
if (fromArg !== null && lowerer.mapTypeOf(lowerer.typeOf(fromArg))?.kind === "generator")
|
||||
return lowerHelperSource(lowerer, fromArg);
|
||||
const tsType = lowerer.typeOf(expr);
|
||||
const mapped = lowerer.mapTypeOf(tsType);
|
||||
const arg = lowerer.checker.getTypeArguments(tsType as ts.TypeReference)[0];
|
||||
@@ -577,10 +758,17 @@ function lowerStage(
|
||||
if (ret.kind === "array") return { stage: { kind: "flatMap", fn }, out: ret.elem };
|
||||
if (ret.kind === "generator" && ret.async !== true && stepDrivable(ret))
|
||||
return { stage: { kind: "flatMap", fn }, out: ret.yieldT };
|
||||
// A Set flattens its values and a Map its entries, walked live. A string
|
||||
// is rejected at run time with Node's TypeError (strings are not
|
||||
// iterator objects).
|
||||
if (ret.kind === "set") return { stage: { kind: "flatMap", fn }, out: ret.elem };
|
||||
if (ret.kind === "map")
|
||||
return { stage: { kind: "flatMap", fn }, out: entryPairTypeOf(lowerer, ret.key, ret.value) };
|
||||
if (ret.kind === "string") return { stage: { kind: "flatMap", fn }, out: STRING };
|
||||
lowerer.noLowering(
|
||||
`.flatMap over a callback returning '${lowerer.fmt(ret)}'`,
|
||||
s.argNode,
|
||||
"callbacks returning an array or a generator are the supported forms",
|
||||
"callbacks returning an array, a generator, a Set or a Map are the supported forms",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1245,8 +1433,9 @@ function walkCollection(
|
||||
}
|
||||
|
||||
/** A flatMap stage's inner walk over the callback's result in local
|
||||
* `innerId`: an array by index, or a generator driven (and closed on
|
||||
* early exit) like the source. `stop` ends the walk early. */
|
||||
* `innerId`: an array by index, a Set's values or a Map's entries walked
|
||||
* live, or a generator driven (and closed on early exit) like the source.
|
||||
* A string result throws Node's TypeError. `stop` ends the walk early. */
|
||||
function walkInner(
|
||||
lowerer: Lowerer,
|
||||
frame: ChainFrame,
|
||||
@@ -1257,6 +1446,27 @@ function walkInner(
|
||||
loc: SrcLoc,
|
||||
): IrStmt[] {
|
||||
if (innerT.kind === "array") return [walkArray(frame, innerId, innerT, stop, body, loc)];
|
||||
if (innerT.kind === "set" || innerT.kind === "map") {
|
||||
const source: ChainSource = {
|
||||
kind: "collection",
|
||||
type: innerT,
|
||||
proj: innerT.kind === "set" ? "values" : "entries",
|
||||
};
|
||||
return walkCollection(lowerer, frame, innerId, source, stop ?? boolLit(false, loc), body, loc);
|
||||
}
|
||||
if (innerT.kind === "string") {
|
||||
return [
|
||||
throwTypeError(
|
||||
{
|
||||
kind: "strLit",
|
||||
value: "Iterator.prototype.flatMap called on non-object",
|
||||
type: STRING,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
),
|
||||
];
|
||||
}
|
||||
if (innerT.kind !== "generator")
|
||||
throw new InternalCompilerError("lowerer bug: flatMap over a non-iterable result");
|
||||
const pre: IrStmt[] =
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import type { IrExpr, IrFunction } from "../../ir/ir.js";
|
||||
import {
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrType,
|
||||
callSiteReturnType,
|
||||
typeEquals,
|
||||
} from "../../ir/ir.js";
|
||||
import { everyStmtList, transformStmtList } from "../../ir/traverse.js";
|
||||
import type { ClassInfo } from "./lower-classes.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
@@ -56,6 +62,9 @@ export function devirtualizeVirtualCalls(
|
||||
|
||||
class ConstructedClasses {
|
||||
private readonly lowered = new Set<string>();
|
||||
/** Lowered bodies' return types: an override may return a subclass
|
||||
* (`set(): this` resolves per declaring class). */
|
||||
private readonly returnTypes = new Map<string, IrType>();
|
||||
/** Classes some retained IR allocates or exposes as a class value. */
|
||||
private readonly allocated = new Set<string>();
|
||||
private readonly exportedNames: ReadonlySet<string>;
|
||||
@@ -74,6 +83,7 @@ class ConstructedClasses {
|
||||
};
|
||||
for (const fn of functions) {
|
||||
this.lowered.add(fn.name);
|
||||
this.returnTypes.set(fn.name, callSiteReturnType(fn));
|
||||
everyStmtList(fn.body, {
|
||||
stmt: () => true,
|
||||
expr: (e) => {
|
||||
@@ -102,7 +112,7 @@ class ConstructedClasses {
|
||||
this.facts.reachable.has(`%${candidate.def.name}.${e.method}`),
|
||||
)
|
||||
)
|
||||
return this.direct(e, info, nearest.declarer);
|
||||
return this.direct(e, info, nearest.declarer) ?? e;
|
||||
if (this.open(info)) return e;
|
||||
const targets: ClassInfo[] = [];
|
||||
for (const candidate of this.descendants(info)) {
|
||||
@@ -117,7 +127,7 @@ class ConstructedClasses {
|
||||
const callee = `%${target.def.name}.${e.method}`;
|
||||
if (!this.facts.reachable.has(callee) || !this.lowered.has(callee)) return e;
|
||||
}
|
||||
if (targets.length === 1) return this.direct(e, info, targets[0]!);
|
||||
if (targets.length === 1) return this.direct(e, info, targets[0]!) ?? e;
|
||||
// A small closed set: the backend tests the loaded slot against each
|
||||
// implementation and calls it directly (no indirect fallback; every
|
||||
// instance that can reach this site resolves to one of them).
|
||||
@@ -125,7 +135,19 @@ class ConstructedClasses {
|
||||
return { ...e, targets: targets.map((t) => t.def.name) };
|
||||
}
|
||||
|
||||
private direct(e: VirtualCall, info: ClassInfo, target: ClassInfo): IrExpr {
|
||||
/** The direct call, or null when the target's return type cannot stand
|
||||
* for the site's: an override returning a subclass object (a polymorphic
|
||||
* `this` return) upcasts to the site's class; any other mismatch keeps
|
||||
* the virtual call. */
|
||||
private direct(e: VirtualCall, info: ClassInfo, target: ClassInfo): IrExpr | null {
|
||||
const callee = `%${target.def.name}.${e.method}`;
|
||||
const ret = this.returnTypes.get(callee) ?? e.type;
|
||||
const upcast =
|
||||
!typeEquals(ret, e.type) &&
|
||||
ret.kind === "object" &&
|
||||
e.type.kind === "object" &&
|
||||
this.lowerer.isSubclassOf(ret.className, e.type.className);
|
||||
if (!typeEquals(ret, e.type) && !upcast) return null;
|
||||
const receiver = e.args[0]!;
|
||||
const typed: IrExpr =
|
||||
target === info || this.lowerer.isSubclassOf(info.def.name, target.def.name)
|
||||
@@ -139,13 +161,16 @@ class ConstructedClasses {
|
||||
type: { kind: "object", className: target.def.name },
|
||||
loc: receiver.loc,
|
||||
};
|
||||
return {
|
||||
const call: IrExpr = {
|
||||
kind: "call",
|
||||
callee: `%${target.def.name}.${e.method}`,
|
||||
callee,
|
||||
args: [typed, ...e.args.slice(1)],
|
||||
type: e.type,
|
||||
type: ret,
|
||||
loc: e.loc,
|
||||
};
|
||||
return upcast && e.type.kind === "object"
|
||||
? this.lowerer.upcastTo(call, e.type.className)
|
||||
: call;
|
||||
}
|
||||
|
||||
/** The class and every class below it, in the isSubclassOf sense
|
||||
|
||||
@@ -823,7 +823,7 @@ export function lowerObjectLiteral(
|
||||
// `k?: ...` — which desugar to one conditional field at the
|
||||
// spread's own position (any position: they introduce one fresh
|
||||
// name, collision-fenced below). Everything else stays fenced.
|
||||
const cs = conditionalSpreadOf(prop.expression);
|
||||
const cs = conditionalSpreadOf(prop.expression) ?? andSpreadOf(lowerer, prop.expression);
|
||||
if (cs === "unsupported" || (cs && cs.props.length !== 1)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
@@ -1264,6 +1264,7 @@ export function lowerObjectLiteral(
|
||||
expr.properties.length >= 2 &&
|
||||
ts.isSpreadAssignment(expr.properties[0]!) &&
|
||||
!conditionalSpreadOf(expr.properties[0]!.expression) &&
|
||||
!andSpreadOf(lowerer, expr.properties[0]!.expression) &&
|
||||
expr.properties
|
||||
.slice(1)
|
||||
.every(
|
||||
@@ -1340,7 +1341,7 @@ export function lowerObjectLiteral(
|
||||
let mergeable = true;
|
||||
for (const prop of expr.properties) {
|
||||
if (ts.isSpreadAssignment(prop)) {
|
||||
const cs = conditionalSpreadOf(prop.expression);
|
||||
const cs = conditionalSpreadOf(prop.expression) ?? andSpreadOf(lowerer, prop.expression);
|
||||
if (cs === "unsupported" || (cs && cs.props.length !== 1)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
@@ -1481,7 +1482,12 @@ export function lowerObjectLiteral(
|
||||
!shape.tuple &&
|
||||
shape.fields.length > 0 &&
|
||||
expr.properties.length > 0 &&
|
||||
expr.properties.every((p) => ts.isSpreadAssignment(p) && !conditionalSpreadOf(p.expression)) &&
|
||||
expr.properties.every(
|
||||
(p) =>
|
||||
ts.isSpreadAssignment(p) &&
|
||||
!conditionalSpreadOf(p.expression) &&
|
||||
!andSpreadOf(lowerer, p.expression),
|
||||
) &&
|
||||
expr.properties.some((p) => {
|
||||
const t = lowerer.mapTypeOf(lowerer.typeOf((p as ts.SpreadAssignment).expression));
|
||||
return (
|
||||
@@ -1599,7 +1605,7 @@ export function lowerObjectLiteral(
|
||||
}
|
||||
}
|
||||
if (ts.isSpreadAssignment(prop)) {
|
||||
const cs = conditionalSpreadOf(prop.expression);
|
||||
const cs = conditionalSpreadOf(prop.expression) ?? andSpreadOf(lowerer, prop.expression);
|
||||
if (cs && cs !== "unsupported") {
|
||||
// `...(c ? { k: v } : {})` — ONE conditional field at the
|
||||
// spread's own position: cond evaluates exactly once, `v` only
|
||||
@@ -1728,7 +1734,8 @@ export function lowerObjectLiteral(
|
||||
const laterNames = new Set<string>();
|
||||
for (const later of expr.properties.slice(expr.properties.indexOf(prop) + 1)) {
|
||||
if (ts.isSpreadAssignment(later)) {
|
||||
if (conditionalSpreadOf(later.expression)) continue;
|
||||
if (conditionalSpreadOf(later.expression) || andSpreadOf(lowerer, later.expression))
|
||||
continue;
|
||||
const lt = laterSpreadType(lowerer, later.expression);
|
||||
if (lt?.kind === "record") {
|
||||
// A field the later source may lack defines nothing.
|
||||
@@ -1963,7 +1970,8 @@ export function lowerObjectLiteral(
|
||||
const laterNames = new Set<string>();
|
||||
for (const later of expr.properties.slice(expr.properties.indexOf(prop) + 1)) {
|
||||
if (ts.isSpreadAssignment(later)) {
|
||||
if (conditionalSpreadOf(later.expression)) continue;
|
||||
if (conditionalSpreadOf(later.expression) || andSpreadOf(lowerer, later.expression))
|
||||
continue;
|
||||
const lt = laterSpreadType(lowerer, later.expression);
|
||||
if (lt?.kind === "record") {
|
||||
// A field the later source may lack defines nothing.
|
||||
@@ -2517,6 +2525,54 @@ function lowerUnionRecordClone(lowerer: Lowerer, expr: ts.ObjectLiteralExpressio
|
||||
};
|
||||
}
|
||||
|
||||
/** `...(c && { k: v })`: spreading a falsy primitive (false, 0, "", NaN)
|
||||
* copies nothing, so the idiom is `...(c ? { k: v } : {})` with `c`
|
||||
* evaluated once. Recognized only where the spread's own type mixes the
|
||||
* record with primitive arms; a record or record-or-unit spread keeps the
|
||||
* ordinary source-copy path. */
|
||||
export function andSpreadOf(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.Expression,
|
||||
): { cond: ts.Expression; props: CondSpreadProp[]; whenTrue: boolean } | "unsupported" | null {
|
||||
let e: ts.Expression = expr;
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
if (!ts.isBinaryExpression(e) || e.operatorToken.kind !== ts.SyntaxKind.AmpersandAmpersandToken)
|
||||
return null;
|
||||
let right: ts.Expression = e.right;
|
||||
while (ts.isParenthesizedExpression(right)) right = right.expression;
|
||||
if (!ts.isObjectLiteralExpression(right)) return null;
|
||||
const spreadType = lowerer.mapTypeOf(lowerer.typeOf(e));
|
||||
if (spreadType?.kind !== "union") return null;
|
||||
const arms = lowerer.unions.get(spreadType.unionId)?.arms ?? [];
|
||||
if (
|
||||
arms.filter((arm) => arm.kind === "record").length !== 1 ||
|
||||
!arms.some((arm) => arm.kind === "bool" || arm.kind === "f64" || arm.kind === "string") ||
|
||||
!arms.every(
|
||||
(arm) =>
|
||||
arm.kind === "record" ||
|
||||
arm.kind === "bool" ||
|
||||
arm.kind === "f64" ||
|
||||
arm.kind === "string" ||
|
||||
isUnitType(arm),
|
||||
)
|
||||
)
|
||||
return null;
|
||||
if (right.properties.length === 0) return "unsupported";
|
||||
const props: CondSpreadProp[] = [];
|
||||
for (const p of right.properties) {
|
||||
if (
|
||||
(ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) &&
|
||||
p.name &&
|
||||
(ts.isIdentifier(p.name) || ts.isStringLiteral(p.name))
|
||||
) {
|
||||
props.push(p as CondSpreadProp);
|
||||
} else {
|
||||
return "unsupported";
|
||||
}
|
||||
}
|
||||
return { cond: e.left, props, whenTrue: true };
|
||||
}
|
||||
|
||||
/** A conditional spread's carrier property: `name: value` or shorthand,
|
||||
* with an identifier/string-literal name. */
|
||||
export type CondSpreadProp = (ts.PropertyAssignment | ts.ShorthandPropertyAssignment) & {
|
||||
|
||||
@@ -4,6 +4,8 @@ import { DYN, STRING, UNDEFINED_T, VOID, isUnitType, typeEquals } from "../../..
|
||||
import type { IrExpr, IrStmt, SrcLoc } from "../../../ir/ir.js";
|
||||
import { isNodeEsmFile, locOf } from "../../program.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
import { tryLowerExpression } from "./try-lower-expression.js";
|
||||
import { constituentTypes } from "../../ts7/checker.js";
|
||||
|
||||
/** One optional-chain STEP: `a?.b`, `a?.m(...)`, `a?.[i]` (the token on
|
||||
* the member access) and `f?.()` (the token on the call). The receiver
|
||||
@@ -135,6 +137,31 @@ export function hasOptionalChainGuard(expr: ts.Expression): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
/** `recv.m` where `m` is a required method the standard library declares
|
||||
* and `recv` itself is a non-optional, never-nullish typed receiver. */
|
||||
function alwaysPresentStdlibMethod(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
if (!ts.isPropertyAccessExpression(node) || node.questionDotToken) return false;
|
||||
if (hasOptionalChainGuard(node.expression)) return false;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(node.name);
|
||||
if (!symbol || !(symbol.flags & ts.SymbolFlags.Method) || symbol.flags & ts.SymbolFlags.Optional)
|
||||
return false;
|
||||
if (!lowerer.isStdlibSymbol(symbol)) return false;
|
||||
const nullable =
|
||||
ts.TypeFlags.Any |
|
||||
ts.TypeFlags.Unknown |
|
||||
ts.TypeFlags.Undefined |
|
||||
ts.TypeFlags.Null |
|
||||
ts.TypeFlags.Void;
|
||||
const receiverType = lowerer.checker.getTypeAtLocation(node.expression);
|
||||
const calleeType = lowerer.checker.getTypeAtLocation(node);
|
||||
const parts = (type: ts.Type): readonly ts.Type[] =>
|
||||
type.flags & ts.TypeFlags.Union ? constituentTypes(type) : [type];
|
||||
return (
|
||||
parts(receiverType).every((part) => !(part.flags & nullable)) &&
|
||||
parts(calleeType).every((part) => !(part.flags & nullable))
|
||||
);
|
||||
}
|
||||
|
||||
export function lowerOptionalChain(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.CallExpression | ts.PropertyAccessExpression | ts.ElementAccessExpression,
|
||||
@@ -181,7 +208,25 @@ export function lowerOptionalChain(
|
||||
dotNode = tail;
|
||||
recvNode = tail.expression;
|
||||
}
|
||||
const loweredReceiver = lowerer.lowerExpr(recvNode);
|
||||
// `h.getSetCookie?.()`: a required standard-library METHOD on a
|
||||
// never-nullish receiver always exists, so `?.()` is the plain call.
|
||||
// Native handle methods have no first-class value to guard; when the
|
||||
// method read itself has no lowering, re-dispatch the plain call (which
|
||||
// reports its own diagnostics) instead of failing on the value read.
|
||||
let loweredReceiver: IrExpr | null;
|
||||
if (dotNode === expr && alwaysPresentStdlibMethod(lowerer, recvNode)) {
|
||||
loweredReceiver = tryLowerExpression(lowerer, recvNode);
|
||||
if (loweredReceiver === null) {
|
||||
lowerer.chainHandled.add(dotNode);
|
||||
try {
|
||||
return lowerer.lowerExpr(expr);
|
||||
} finally {
|
||||
lowerer.chainHandled.delete(dotNode);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
loweredReceiver = lowerer.lowerExpr(recvNode);
|
||||
}
|
||||
const receiver = lowerer.runtimeOptionalSourceValue(recvNode, loweredReceiver) ?? loweredReceiver;
|
||||
if (receiver.type.kind === "dyn") {
|
||||
if (dotNode === expr && ts.isCallExpression(expr)) {
|
||||
|
||||
@@ -157,6 +157,13 @@ export function declaredReturnType(
|
||||
}
|
||||
if (mappedReturn && isJsSourceFile(decl.getSourceFile()) && functionCanReturnUndefined(decl))
|
||||
mappedReturn = lowerer.withUndefinedArmOf(mappedReturn) ?? mappedReturn;
|
||||
// A generic-class method body that can return an indexed read that
|
||||
// misses returns undefined there, whatever the declared result says.
|
||||
if (mappedReturn && lowerer.runtimeOptionalGenericReturns.has(decl))
|
||||
mappedReturn =
|
||||
mappedReturn.kind === "promise"
|
||||
? { kind: "promise", inner: lowerer.runtimeOptionalType(mappedReturn.inner) }
|
||||
: lowerer.runtimeOptionalType(mappedReturn);
|
||||
if (isJsSourceFile(decl.getSourceFile()) && mappedReturn?.kind === "void") {
|
||||
let returnsValue = false;
|
||||
const scan = (node: ts.Node): void => {
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from "../../ir/ir.js";
|
||||
import { isJsSourceFile, locOf } from "../program.js";
|
||||
import { typeKey } from "../type-mapper.js";
|
||||
import { functionCanReturnUndefined } from "../function-completion.js";
|
||||
import { PoisonError, dynFallbackType, neverTaintedJsType, newFnCtx } from "./lowerer.js";
|
||||
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
|
||||
import { findGenericMethodOn, storedClassValueType, type ClassInfo } from "./lower-classes.js";
|
||||
@@ -294,11 +295,19 @@ export function genericCallInstance(
|
||||
const retTs = lowerer.checker.getReturnTypeOfSignature(rsig);
|
||||
const local = localClassResultInstance(lowerer, expr, info, rsig, params, retTs);
|
||||
if (local) return local;
|
||||
const returnType = lowerer.mapTypeOf(retTs);
|
||||
if (!returnType) {
|
||||
const mappedReturn = lowerer.mapTypeOf(retTs);
|
||||
if (!mappedReturn) {
|
||||
fenceGenericSignatureResult(lowerer, expr, retTs);
|
||||
lowerer.badType(expr, retTs);
|
||||
}
|
||||
// A body that can return an indexed read that misses returns undefined
|
||||
// there, as in JavaScript, whatever the declared result says. An async
|
||||
// body's promise is always present and settles with undefined there.
|
||||
const returnType = lowerer.runtimeOptionalGenericReturns.has(info.decl)
|
||||
? mappedReturn.kind === "promise"
|
||||
? { kind: "promise" as const, inner: lowerer.runtimeOptionalType(mappedReturn.inner) }
|
||||
: lowerer.runtimeOptionalType(mappedReturn)
|
||||
: mappedReturn;
|
||||
|
||||
// keyof-constrained type parameters (`K extends keyof T`): the bound
|
||||
// LITERAL is semantic — the instance body reads the named field — so
|
||||
@@ -1574,6 +1583,16 @@ function implicitDeclaredReturn(lowerer: Lowerer, info: GenericFnInfo): IrType |
|
||||
return null;
|
||||
if (mapped !== null && promiseCarriesDyn(lowerer, mapped) && !hasExplicitJsDocReturn(info.decl))
|
||||
return null;
|
||||
// A JavaScript body that can complete without returning yields
|
||||
// undefined there, whatever its JSDoc claims (bcryptjs's `hash`
|
||||
// returns a promise only when no callback is passed), exactly as
|
||||
// declaredReturnType widens non-generic JavaScript functions.
|
||||
if (
|
||||
mapped !== null &&
|
||||
isJsSourceFile(info.decl.getSourceFile()) &&
|
||||
functionCanReturnUndefined(info.decl)
|
||||
)
|
||||
return lowerer.withUndefinedArmOf(mapped) ?? mapped;
|
||||
return mapped;
|
||||
} catch (e) {
|
||||
if (!(e instanceof PoisonError)) throw e;
|
||||
|
||||
@@ -265,6 +265,222 @@ function dynIteratorAdapter(
|
||||
return { kind: "call", callee: name, args: [expr], type: expected, loc };
|
||||
}
|
||||
|
||||
/** `ta.values()`, `ta.keys()` or `ta.entries()` on a typed array as a
|
||||
* value: an interned generator over the indices, reading the length and
|
||||
* the element at each step, as the typed array iterator does. Null when
|
||||
* `type` is not the matching number (or [number, number]) generator. */
|
||||
export function lowerBytesIteratorValue(
|
||||
lowerer: Lowerer,
|
||||
receiver: IrExpr,
|
||||
method: "values" | "keys" | "entries",
|
||||
type: IrType,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
const bytesT = receiver.type;
|
||||
if (bytesT.kind !== "bytes" || type.kind !== "generator") return null;
|
||||
if (type.retT.kind !== "void" || type.nextT.kind !== "undefinedT") return null;
|
||||
const pair = type.yieldT.kind === "record" ? lowerer.shapes.get(type.yieldT.shapeId) : undefined;
|
||||
const pairOk =
|
||||
pair?.tuple === true &&
|
||||
pair.fields.length === 2 &&
|
||||
pair.fields.every((f) => (f.name === "0" || f.name === "1") && f.type.kind === "f64");
|
||||
if (method === "entries" ? !pairOk : type.yieldT.kind !== "f64") return null;
|
||||
const resultType = genResultRecord(type.yieldT, type.retT, lowerer.shapes, lowerer.unions);
|
||||
if (!resultType) return null;
|
||||
const key = `bytesIter:${method}:${typeKey(bytesT)}:${typeKey(type)}`;
|
||||
let name = lowerer.valueHelpers.get(key);
|
||||
if (!name) {
|
||||
name = `%bytes.iter.${lowerer.valueHelpers.size}`;
|
||||
lowerer.valueHelpers.set(key, name);
|
||||
const list = varRef("list.0", bytesT, loc);
|
||||
const index = varRef("index.0", F64, loc);
|
||||
const element: IrExpr = {
|
||||
kind: "bytesIntrinsic",
|
||||
method: "get",
|
||||
receiver: list,
|
||||
args: [index],
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
const yielded: IrExpr =
|
||||
method === "keys"
|
||||
? index
|
||||
: method === "values"
|
||||
? element
|
||||
: {
|
||||
kind: "recordLit",
|
||||
fields: [
|
||||
{ name: "0", value: index },
|
||||
{ name: "1", value: element },
|
||||
],
|
||||
type: type.yieldT,
|
||||
loc,
|
||||
};
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: [{ localId: "list.0", name: "list", type: bytesT }],
|
||||
returnType: VOID,
|
||||
locals: [
|
||||
{ id: "list.0", name: "list", type: bytesT, mutable: false },
|
||||
{ id: "index.0", name: "index", type: F64, mutable: true },
|
||||
],
|
||||
body: [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "index.0",
|
||||
init: { kind: "numLit", value: 0, type: F64, loc },
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "while",
|
||||
cond: {
|
||||
kind: "bin",
|
||||
op: "<",
|
||||
left: index,
|
||||
right: {
|
||||
kind: "bytesIntrinsic",
|
||||
method: "length",
|
||||
receiver: list,
|
||||
args: [],
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
body: [
|
||||
{ kind: "exprStmt", expr: { kind: "yieldExpr", value: yielded, type: VOID, loc }, loc },
|
||||
{
|
||||
kind: "assign",
|
||||
localId: "index.0",
|
||||
value: {
|
||||
kind: "bin",
|
||||
op: "+",
|
||||
left: index,
|
||||
right: { kind: "numLit", value: 1, type: F64, loc },
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
},
|
||||
{ kind: "return", value: null, loc },
|
||||
],
|
||||
generator: { yieldT: type.yieldT, nextT: UNDEFINED_T, resultType },
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return { kind: "call", callee: name, args: [receiver], type, loc };
|
||||
}
|
||||
|
||||
/** `s[Symbol.iterator]()` on a string value: an interned generator over
|
||||
* its code points, the string iterator's steps (a lone surrogate is one
|
||||
* step). Null when `type` is not the plain string-yielding generator. */
|
||||
export function lowerStringIteratorValue(
|
||||
lowerer: Lowerer,
|
||||
receiver: IrExpr,
|
||||
type: IrType,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
if (receiver.type.kind !== "string" || type.kind !== "generator") return null;
|
||||
if (
|
||||
type.yieldT.kind !== "string" ||
|
||||
type.retT.kind !== "void" ||
|
||||
type.nextT.kind !== "undefinedT"
|
||||
)
|
||||
return null;
|
||||
const resultType = genResultRecord(type.yieldT, type.retT, lowerer.shapes, lowerer.unions);
|
||||
if (!resultType) return null;
|
||||
const key = `strIter:${typeKey(type)}`;
|
||||
let name = lowerer.valueHelpers.get(key);
|
||||
if (!name) {
|
||||
name = `%str.iter.${lowerer.valueHelpers.size}`;
|
||||
lowerer.valueHelpers.set(key, name);
|
||||
const text = varRef("text.0", STRING, loc);
|
||||
const index = varRef("index.0", F64, loc);
|
||||
const point = varRef("point.0", STRING, loc);
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: [{ localId: "text.0", name: "text", type: STRING }],
|
||||
returnType: VOID,
|
||||
locals: [
|
||||
{ id: "text.0", name: "text", type: STRING, mutable: false },
|
||||
{ id: "index.0", name: "index", type: F64, mutable: true },
|
||||
{ id: "point.0", name: "point", type: STRING, mutable: false },
|
||||
],
|
||||
body: [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "index.0",
|
||||
init: { kind: "numLit", value: 0, type: F64, loc },
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "while",
|
||||
cond: {
|
||||
kind: "bin",
|
||||
op: "<",
|
||||
left: index,
|
||||
right: {
|
||||
kind: "strIntrinsic",
|
||||
method: "length",
|
||||
receiver: text,
|
||||
args: [],
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
body: [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "point.0",
|
||||
init: {
|
||||
kind: "strIntrinsic",
|
||||
method: "cpAt",
|
||||
receiver: text,
|
||||
args: [index],
|
||||
type: STRING,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "assign",
|
||||
localId: "index.0",
|
||||
value: {
|
||||
kind: "bin",
|
||||
op: "+",
|
||||
left: index,
|
||||
right: {
|
||||
kind: "strIntrinsic",
|
||||
method: "length",
|
||||
receiver: point,
|
||||
args: [],
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
{ kind: "exprStmt", expr: { kind: "yieldExpr", value: point, type: VOID, loc }, loc },
|
||||
],
|
||||
loc,
|
||||
},
|
||||
{ kind: "return", value: null, loc },
|
||||
],
|
||||
generator: { yieldT: type.yieldT, nextT: UNDEFINED_T, resultType },
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return { kind: "call", callee: name, args: [receiver], type, loc };
|
||||
}
|
||||
|
||||
/** A generator flowing into a slot with other channel types (a
|
||||
* `Generator<number, void, undefined>` returned through an
|
||||
* `IterableIterator<number>` annotation). The interned adapter generator
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { guardNullProto, nullProtoRecordShape } from "./null-prototype.js";
|
||||
import { lowerUnionEquality, tagEqualityMayMissAlias } from "./strict-equality.js";
|
||||
import { lowerHashStreamCall } from "./builtins/hash-stream.js";
|
||||
import { lowerResponseStaticJson } from "./lower-stdlib.js";
|
||||
import { lowerArrayCallbackCall } from "./containers/array-callback-call.js";
|
||||
/** Dispatch source calls, builtin members and receiver-specific operations.
|
||||
* Signature collection, argument completion and function bodies have their
|
||||
@@ -93,6 +94,8 @@ import { lowerDynArrayFilterCall, lowerDynArrayFlatMapCall } from "./containers/
|
||||
import { lowerGroupByStaticCall } from "./containers/collection-grouping.js";
|
||||
import {
|
||||
lowerGeneratorHelperCall,
|
||||
lowerHelperReturnCall,
|
||||
lowerIteratorFromCall,
|
||||
lowerIteratorHelperCall,
|
||||
} from "./containers/iterator-helpers.js";
|
||||
import {
|
||||
@@ -157,6 +160,7 @@ import {
|
||||
} from "./lower-exprs.js";
|
||||
import { isSafeToDiscard } from "./expressions/evaluation-safety.js";
|
||||
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
|
||||
import { classToNumberConversion } from "./class-to-primitive.js";
|
||||
import { hasRuntimeStatics } from "./class-runtime-statics.js";
|
||||
import {
|
||||
httpClientFnBindingOf,
|
||||
@@ -221,7 +225,9 @@ import {
|
||||
completeArgs,
|
||||
} from "./call-arguments.js";
|
||||
import { isThisParameter, type ParamShape } from "./call-signatures.js";
|
||||
import { lowerStringIteratorValue } from "./iterator-adapters.js";
|
||||
import { checkedArmExtraction } from "./coercions/unions.js";
|
||||
import { maybePromiseReturnStmt, promiseArmedUnionOf } from "./maybe-promise.js";
|
||||
import {
|
||||
requireObjLitGenericReceiver,
|
||||
objLitGenericFnNodeOf,
|
||||
@@ -362,6 +368,9 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
// machinery (the member always exists on a POSIX target, so the
|
||||
// optional call IS the call; `process.getuid` itself has no value
|
||||
// lowering for the chain to guard).
|
||||
// `h.return!()` / `h.return?.()` on a helper object: the method exists.
|
||||
const helperReturn = lowerHelperReturnCall(lowerer, expr);
|
||||
if (helperReturn) return helperReturn;
|
||||
const processOptional = lowerer.lowerProcessOptionalMethodCall(expr);
|
||||
if (processOptional) return processOptional;
|
||||
const reqStreamOptional = lowerCompatReqStreamOptionalCall(lowerer, expr);
|
||||
@@ -1462,6 +1471,8 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
// compile-time-known, so a literal key folds to a constant.
|
||||
lowerClassHasOwnPropertyCall(lowerer, expr, expr.expression) ??
|
||||
lowerObjectOwnMethodCall(lowerer, expr, expr.expression) ??
|
||||
// Static Response.json(data, init?) builds a native Response.
|
||||
lowerResponseStaticJson(lowerer, expr) ??
|
||||
// Refuse Response constructor operations before generic-call
|
||||
// fallbacks can report an unrelated SC1090 diagnostic.
|
||||
lowerer.fenceUnsupportedFetchConstructorMember(expr.expression) ??
|
||||
@@ -1611,6 +1622,17 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
lowerer.unsupported("SC1090", expr, `method calls like '${expr.expression.getText()}'`);
|
||||
}
|
||||
|
||||
// `s[Symbol.iterator]()` on a string: the string iterator as a value.
|
||||
if (
|
||||
ts.isElementAccessExpression(expr.expression) &&
|
||||
!expr.expression.questionDotToken &&
|
||||
!expr.questionDotToken &&
|
||||
expr.arguments.length === 0
|
||||
) {
|
||||
const iterator = lowerStringIteratorCall(lowerer, expr, expr.expression);
|
||||
if (iterator) return iterator;
|
||||
}
|
||||
|
||||
// The element spelling of an unsupported native Web method must fence
|
||||
// before lowering the callee into a checked-dynamic keyed read.
|
||||
if (ts.isElementAccessExpression(expr.expression)) {
|
||||
@@ -2133,6 +2155,9 @@ function lowerNumberConstructorValue(
|
||||
};
|
||||
const optionalNumber = lowerOptionalStringNumber(lowerer, arg, loc);
|
||||
if (optionalNumber) return optionalNumber;
|
||||
// A program class instance converts through its own methods.
|
||||
const classNumber = classToNumberConversion(lowerer, arg, argNode);
|
||||
if (classNumber) return classNumber;
|
||||
const scalarUnionNumber = lowerScalarUnionNumber(lowerer, arg, argNode, loc);
|
||||
if (scalarUnionNumber) return scalarUnionNumber;
|
||||
return lowerer.noLowering(
|
||||
@@ -3149,10 +3174,32 @@ function lowerOptionalStringNumber(
|
||||
return { kind: "call", callee: helper, args: [arg], type: F64, loc };
|
||||
}
|
||||
|
||||
/** 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"]);
|
||||
/** `s[Symbol.iterator]()` on a string-typed receiver: the generator over
|
||||
* its code points. Null for other element calls. */
|
||||
function lowerStringIteratorCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
access: ts.ElementAccessExpression,
|
||||
): IrExpr | null {
|
||||
let key = access.argumentExpression;
|
||||
while (ts.isParenthesizedExpression(key)) key = key.expression;
|
||||
if (
|
||||
!ts.isPropertyAccessExpression(key) ||
|
||||
lowerer.stdlibGlobalMember(key, "Symbol") !== "iterator"
|
||||
)
|
||||
return null;
|
||||
if (lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind !== "string") return null;
|
||||
const type = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
if (type?.kind !== "generator") return null;
|
||||
const receiver = lowerer.lowerExpr(access.expression);
|
||||
const value = lowerStringIteratorValue(lowerer, receiver, type, locOf(call));
|
||||
if (!value) lowerer.unsupported("SC1090", call, "this string iterator type");
|
||||
return value;
|
||||
}
|
||||
|
||||
/** Iterator types with checked runtime receivers, whose helper calls lower
|
||||
* in lowerGeneratorHelperCall. */
|
||||
const CHECKED_ITERATOR_TYPES = new Set(["MapIterator", "SetIterator"]);
|
||||
const ITERATOR_HELPER_METHODS = new Set([
|
||||
"map",
|
||||
"filter",
|
||||
@@ -3212,25 +3259,17 @@ function lowerDynReceiverMethodCall(
|
||||
const recvSymbol = recvTs.getSymbol();
|
||||
if (
|
||||
recvSymbol &&
|
||||
UNLOWERED_ITERATOR_TYPES.has(recvSymbol.name) &&
|
||||
CHECKED_ITERATOR_TYPES.has(recvSymbol.name) &&
|
||||
lowerer.isStdlibSymbol(recvSymbol) &&
|
||||
ITERATOR_HELPER_METHODS.has(access.name.text) &&
|
||||
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,
|
||||
"stored Map/Set iterators support .next() and iteration; iterator helper methods have no lowering",
|
||||
);
|
||||
}
|
||||
// The checked runtime's string iterator has no helper methods.
|
||||
const helper = lowerGeneratorHelperCall(lowerer, call, access);
|
||||
if (helper) return helper;
|
||||
lowerer.noLowering(
|
||||
`${recvSymbol.name}.${access.name.text}`,
|
||||
`collection iterator .${access.name.text}()`,
|
||||
call,
|
||||
"iterator helpers lower on generators and on array, Map and Set iterators — spread the string first: [...s].values()",
|
||||
"iterator helpers on Map/Set iterators are called directly on the iterator, without optional chaining",
|
||||
);
|
||||
}
|
||||
const arrayReceiver = lowerer.checker.isArrayType(recvTs);
|
||||
@@ -4315,8 +4354,15 @@ function lowerUnionToStringCall(
|
||||
(a.kind === "bytes" && a.elem === "u8"),
|
||||
);
|
||||
if (!stringable) return null;
|
||||
const operand = lowerer.lowerExpr(access.expression);
|
||||
let operand = lowerer.lowerExpr(access.expression);
|
||||
if (operand.type.kind !== "union") return null;
|
||||
// A flow-narrowed read keeps its wider storage union (`string | number`
|
||||
// after `!Array.isArray(h.host)` over a `string | number | string[]`
|
||||
// slot): retag it to the proven union before the per-union helper.
|
||||
if (operand.type.unionId !== recvT.unionId) {
|
||||
operand = lowerer.coerceInto(access.expression, operand, recvT);
|
||||
if (operand.type.kind !== "union" || operand.type.unionId !== recvT.unionId) return null;
|
||||
}
|
||||
return { kind: "toString", operand, type: STRING, loc: locOf(call) };
|
||||
}
|
||||
|
||||
@@ -5322,6 +5368,8 @@ export function lowerPromiseMethodCall(
|
||||
const resultOf = (value: IrExpr, returnsPromise = false): IrStmt[] => {
|
||||
if (value.type.kind === "promise")
|
||||
return [lowerer.adoptingReturn(value, R, (e) => lowerer.coerceInto(call, e, R))];
|
||||
const flattened = maybePromiseHandlerResult(lowerer, call, value, R);
|
||||
if (flattened) return [flattened];
|
||||
if (returnsPromise) {
|
||||
// A boxed handler's promise result adopts too: the then job's
|
||||
// turn after the call, then the await of the returned value.
|
||||
@@ -5672,7 +5720,10 @@ export function lowerPromiseMethodCall(
|
||||
// The handler's result: promise returns flatten exactly like
|
||||
// `return p` in any async body (the spec's thenable adoption);
|
||||
// everything else coerces into R.
|
||||
if (handlerCall.type.kind === "promise" && R.kind !== "promise") {
|
||||
const flattened = maybePromiseHandlerResult(lowerer, call, handlerCall, R);
|
||||
if (flattened) {
|
||||
body.push(flattened);
|
||||
} else if (handlerCall.type.kind === "promise" && R.kind !== "promise") {
|
||||
body.push(lowerer.adoptingReturn(handlerCall, R, (e) => lowerer.coerceInto(call, e, R)));
|
||||
} else if (R.kind === "void") {
|
||||
body.push({ kind: "exprStmt", expr: handlerCall, loc });
|
||||
@@ -6624,10 +6675,10 @@ function recordAssignHelper(
|
||||
return helper;
|
||||
}
|
||||
|
||||
/** The `Iterator` global's statics (ES2025 — Iterator.from, and the
|
||||
* 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. */
|
||||
/** The `Iterator` global's statics (ES2025): `Iterator.from` over a typed
|
||||
* built-in iterable lowers (lowerIteratorFromCall); every other member,
|
||||
* and other arguments, fence with the working spelling named instead of
|
||||
* the generic-method fence's monomorphization wording. */
|
||||
function lowerIteratorStaticFence(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
@@ -6635,11 +6686,15 @@ function lowerIteratorStaticFence(
|
||||
): IrExpr | null {
|
||||
if (!lowerer.isStdlibGlobal(access.expression, "Iterator")) return null;
|
||||
if (!lowerer.isStdlibMember(access)) return null;
|
||||
const from = lowerIteratorFromCall(lowerer, call, access);
|
||||
if (from) return from;
|
||||
lowerer.noLowering(
|
||||
`Iterator.${access.name.text}`,
|
||||
call,
|
||||
"iterator helpers apply directly to generators and to array, Map and Set iterators: " +
|
||||
"arr.values().map(f).take(n).toArray()",
|
||||
access.name.text === "from"
|
||||
? "Iterator.from accepts arrays, strings, typed arrays, Sets, Maps and iterator objects such as generators"
|
||||
: "iterator helpers apply directly to generators and to array, Map and Set iterators: " +
|
||||
"arr.values().map(f).take(n).toArray()",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9778,3 +9833,26 @@ export function lowerStaticNumberGlobal(
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** A then handler's promise-armed result (`.then(() => cached ?? load())`
|
||||
* typed `T | Promise<T>`, or a `Promise<void> | void` callback's result):
|
||||
* the lifted async body adopts the promise arm and fulfills with the
|
||||
* others, like `return u` in an async function. Null for other results. */
|
||||
function maybePromiseHandlerResult(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
value: IrExpr,
|
||||
R: IrType,
|
||||
): IrStmt | null {
|
||||
const mp = promiseArmedUnionOf(lowerer, value.type);
|
||||
if (!mp || promiseArmedUnionOf(lowerer, R)) return null;
|
||||
const stmt = maybePromiseReturnStmt(lowerer, value, mp, R, (e) => lowerer.coerceInto(call, e, R));
|
||||
if (stmt === null) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
call,
|
||||
`then handlers returning '${lowerer.fmt(value.type)}' into '${lowerer.fmt(R)}' (an arm does not convert to the result)`,
|
||||
);
|
||||
}
|
||||
return stmt;
|
||||
}
|
||||
|
||||
@@ -517,6 +517,9 @@ export interface ClassInfo {
|
||||
/** The synthetic InternTable class (@scriptc/experimental,
|
||||
* intern-tables.ts): synthesized like lifetimeRegion. */
|
||||
internTable?: true;
|
||||
/** The synthetic ThreadSlots class (@scriptc/experimental,
|
||||
* thread-slots.ts): synthesized like lifetimeRegion. */
|
||||
threadSlots?: true;
|
||||
/** This class's own `emit` override in the FORWARDING SHAPE (the one
|
||||
* EventEmitter member a subclass may re-declare): never in `methods` —
|
||||
* emit calls keep routing through the emitter spoke, which lowers the
|
||||
@@ -1573,7 +1576,14 @@ function inferredEmptyCollectionFieldType(
|
||||
const mappedArgument = (argument: ts.Expression): IrType | null => {
|
||||
const type = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(argument));
|
||||
const mapped = lowerer.mapTypeOf(type);
|
||||
if (!mapped || mapped.kind === "dyn" || mapped.kind === "void" || isUnitType(mapped))
|
||||
// Inferred JS collection fields keep functions in checked slots.
|
||||
if (
|
||||
!mapped ||
|
||||
mapped.kind === "dyn" ||
|
||||
mapped.kind === "void" ||
|
||||
mapped.kind === "func" ||
|
||||
isUnitType(mapped)
|
||||
)
|
||||
return null;
|
||||
return mapped;
|
||||
};
|
||||
@@ -2651,9 +2661,15 @@ export function collectClassShapeInner(
|
||||
? String(Number(member.name.text))
|
||||
: member.name.text;
|
||||
const fieldName = symbolKey?.fieldName ?? stringKey;
|
||||
// A TypeScript field without an initializer takes its declared
|
||||
// type and the identifier fields' definite-assignment rule (an
|
||||
// unconditional `this[key] = ...` at the constructor's top
|
||||
// level), checked with the unguarded fields below.
|
||||
const declaredOnly =
|
||||
!member.initializer && !isJsSourceFile(member.getSourceFile()) && !!member.type;
|
||||
if (
|
||||
fieldName === null ||
|
||||
(!member.initializer && !isJsSourceFile(member.getSourceFile()))
|
||||
(!member.initializer && !isJsSourceFile(member.getSourceFile()) && !declaredOnly)
|
||||
) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
@@ -2666,9 +2682,10 @@ export function collectClassShapeInner(
|
||||
member.type || !member.initializer
|
||||
? declared
|
||||
: lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(member.initializer));
|
||||
const mapped = member.initializer
|
||||
? (lowerer.mapTypeOf(inferred) ?? dynFallbackType(lowerer, member, inferred))
|
||||
: DYN;
|
||||
const mapped =
|
||||
member.initializer || declaredOnly
|
||||
? (lowerer.mapTypeOf(inferred) ?? dynFallbackType(lowerer, member, inferred))
|
||||
: DYN;
|
||||
let type =
|
||||
mapped?.kind === "record" && isJsSourceFile(member.getSourceFile()) && !member.type
|
||||
? DYN
|
||||
@@ -2680,7 +2697,7 @@ export function collectClassShapeInner(
|
||||
type.kind === "void" ||
|
||||
(type.kind === "dyn" &&
|
||||
!isJsSourceFile(member.getSourceFile()) &&
|
||||
!lowerer.classImplementedProtocol(inferred))
|
||||
!lowerer.classFieldStoresDyn(inferred))
|
||||
)
|
||||
lowerer.badType(member, inferred);
|
||||
if (isJsSourceFile(member.getSourceFile())) {
|
||||
@@ -2688,7 +2705,24 @@ export function collectClassShapeInner(
|
||||
if (base?.fields.get(fieldName)?.kind === "dyn") type = DYN;
|
||||
if (base && !fields.has(fieldName) && lowerer.dynConvertible(type)) type = DYN;
|
||||
}
|
||||
const undefinedInitializer = !member.initializer ? locOf(member) : undefined;
|
||||
const undefinedInitializer =
|
||||
!member.initializer && (!declaredOnly || type.kind === "dyn")
|
||||
? locOf(member)
|
||||
: undefined;
|
||||
if (
|
||||
declaredOnly &&
|
||||
type.kind !== "dyn" &&
|
||||
!(
|
||||
type.kind === "union" &&
|
||||
lowerer.unions.get(type.unionId)?.arms.some((a) => a.kind === "undefinedT")
|
||||
)
|
||||
) {
|
||||
unguardedFields.push({
|
||||
node: member.name,
|
||||
name: fieldName,
|
||||
why: `initializer-less computed fields not assigned at the constructor's top level (nothing guarantees '${symbolKey ? symbolFieldDisplayName(fieldName) : fieldName}' is assigned before a read — assign it unconditionally at the top of the constructor, give it an initializer, or include undefined in its type)`,
|
||||
});
|
||||
}
|
||||
const previous = fields.get(fieldName);
|
||||
if (previous) {
|
||||
if (symbolKey && !symbolFields.has(symbolKey.identity)) {
|
||||
@@ -3448,7 +3482,17 @@ export function collectClassShapeInner(
|
||||
}
|
||||
let sig: { params: ParamShape[]; ret: IrType };
|
||||
if (isGet) {
|
||||
const ret = lowerer.declaredReturnType(member, member.name);
|
||||
let ret = lowerer.declaredReturnType(member, member.name);
|
||||
// A getter that never returns a value: typed `never` (it only
|
||||
// throws, Next.js's removed `page`/`ua` getters) or, unannotated
|
||||
// with no return statement, inferred `void`. A read answers
|
||||
// undefined or throws, so the property takes the checked-dynamic
|
||||
// slot instead of VOID.
|
||||
if (
|
||||
ret.kind === "void" &&
|
||||
(lowerer.typeOf(member.name).flags & (ts.TypeFlags.Never | ts.TypeFlags.Void)) !== 0
|
||||
)
|
||||
ret = DYN;
|
||||
if (ret.kind === "void") lowerer.badType(member.name, lowerer.typeOf(member.name));
|
||||
sig = { params: [], ret };
|
||||
} else {
|
||||
@@ -3595,6 +3639,16 @@ export function collectClassShapeInner(
|
||||
stmt.expression.left.expression.kind === ts.SyntaxKind.ThisKeyword
|
||||
) {
|
||||
topAssigned.add(stmt.expression.left.name.text);
|
||||
} else if (
|
||||
ts.isExpressionStatement(stmt) &&
|
||||
ts.isBinaryExpression(stmt.expression) &&
|
||||
stmt.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
||||
ts.isElementAccessExpression(stmt.expression.left) &&
|
||||
stmt.expression.left.expression.kind === ts.SyntaxKind.ThisKeyword
|
||||
) {
|
||||
const key = stmt.expression.left.argumentExpression;
|
||||
const name = classSymbolKeyOf(lowerer, key)?.fieldName ?? lowerer.foldedStringKeyOf(key);
|
||||
if (name !== null) topAssigned.add(name);
|
||||
}
|
||||
}
|
||||
for (const f of unguardedFields) {
|
||||
@@ -3632,11 +3686,26 @@ export function collectClassShapeInner(
|
||||
}
|
||||
|
||||
if (escape.assigned && !inst) {
|
||||
let earlyReads: Set<string> | null = null;
|
||||
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?.kind !== "object" || lowerer.classes.get(declared.className)?.def.runtime)
|
||||
// Records, arrays and strings are pointer slots that start empty
|
||||
// too, but only take the checked slot when a method the
|
||||
// constructor calls actually reads them: their other early
|
||||
// exposures keep the plain slot.
|
||||
if (
|
||||
declared?.kind === "record" ||
|
||||
declared?.kind === "array" ||
|
||||
declared?.kind === "string"
|
||||
) {
|
||||
earlyReads ??= constructorMethodReads(decl, base, ctor ?? undefined);
|
||||
if (!earlyReads.has(f.name)) continue;
|
||||
} else if (
|
||||
declared?.kind !== "object" ||
|
||||
lowerer.classes.get(declared.className)?.def.runtime
|
||||
)
|
||||
continue;
|
||||
const armed = lowerer.withUndefinedArm(declared);
|
||||
if (armed === null || armed.kind !== "union") continue;
|
||||
@@ -4868,6 +4937,58 @@ export function staticShadowBelow(info: ClassInfo, name: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/** The `this` members read by the methods and accessors a constructor
|
||||
* calls through `this` (transitively, over the class and its bases),
|
||||
* outside nested functions. The constructor's own reads are not included:
|
||||
* they run where the constructor's statements put them. */
|
||||
function constructorMethodReads(
|
||||
decl: ts.ClassLikeDeclaration,
|
||||
base: ClassInfo | null,
|
||||
ctor: ts.ConstructorDeclaration | undefined,
|
||||
): Set<string> {
|
||||
const reads = new Set<string>();
|
||||
const visited = new Set<string>();
|
||||
const bodies = (name: string): ts.Node[] => {
|
||||
const out: ts.Node[] = [];
|
||||
let owner: ts.ClassLikeDeclaration | undefined = decl;
|
||||
for (let next = base; owner; owner = next?.decl ?? undefined, next = next?.base ?? null) {
|
||||
for (const member of owner.members) {
|
||||
if (
|
||||
(ts.isMethodDeclaration(member) || ts.isAccessor(member)) &&
|
||||
member.body &&
|
||||
(ts.isIdentifier(member.name) || ts.isPrivateIdentifier(member.name)) &&
|
||||
member.name.text === name
|
||||
)
|
||||
out.push(member.body);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
};
|
||||
const enter = (name: string): void => {
|
||||
if (visited.has(name)) return;
|
||||
visited.add(name);
|
||||
for (const body of bodies(name)) scan(body, true);
|
||||
};
|
||||
// Nested functions (callbacks handed elsewhere) are not followed: only
|
||||
// the straight-line code of the constructor and the methods it calls.
|
||||
const scan = (node: ts.Node, inMethod: boolean): void => {
|
||||
ts.walkPreorder(node, (n) => {
|
||||
if (n !== node && ts.isFunctionLike(n)) return "skip";
|
||||
if (
|
||||
ts.isPropertyAccessExpression(n) &&
|
||||
n.expression.kind === ts.SyntaxKind.ThisKeyword &&
|
||||
(ts.isIdentifier(n.name) || ts.isPrivateIdentifier(n.name))
|
||||
) {
|
||||
if (inMethod) reads.add(n.name.text);
|
||||
enter(n.name.text);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
};
|
||||
if (ctor?.body) scan(ctor.body, false);
|
||||
return reads;
|
||||
}
|
||||
|
||||
/** The nearest GENERIC static declaration of `name` at/above `info` —
|
||||
* findStaticOn's twin over the genericStatics tables. */
|
||||
export function findGenericStaticOn(
|
||||
@@ -7705,6 +7826,24 @@ function paramPropInitStmts(lowerer: Lowerer, info: ClassInfo, thisLocal: IrLoca
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Whether a super() call's value is discarded by its statement: the call
|
||||
* is the expression statement itself, or an operand of a comma chain that
|
||||
* is (`super(m, o), this.code = c;`, as minifiers join statements). The
|
||||
* statement lowering runs each comma operand as its own statement in
|
||||
* source order, so the chain lowers exactly like `super(m, o); this.code =
|
||||
* c;`. Parentheses are transparent to both. */
|
||||
function superCallInStatementPosition(call: ts.CallExpression): boolean {
|
||||
let node: ts.Node = call;
|
||||
for (;;) {
|
||||
const parent = node.parent;
|
||||
if (ts.isExpressionStatement(parent)) return true;
|
||||
const comma =
|
||||
ts.isBinaryExpression(parent) && parent.operatorToken.kind === ts.SyntaxKind.CommaToken;
|
||||
if (!comma && !ts.isParenthesizedExpression(parent)) return false;
|
||||
node = parent;
|
||||
}
|
||||
}
|
||||
|
||||
/** Super statements initialize the base and then the derived fields on
|
||||
* whichever control-flow path executes. Repeated calls are explicitly
|
||||
* refused until native layouts can allocate a second receiver. */
|
||||
@@ -7722,7 +7861,7 @@ export function lowerDerivedCtorBody(
|
||||
const previousInitialized = context.superInitialized;
|
||||
context.superInitialized = initialized;
|
||||
context.superCall = (superCall) => {
|
||||
if (!ts.isExpressionStatement(superCall.parent))
|
||||
if (!superCallInStatementPosition(superCall))
|
||||
lowerer.unsupported("SC1090", superCall, "super() in an expression position");
|
||||
superSeen = true;
|
||||
const loc = locOf(superCall);
|
||||
@@ -9246,7 +9385,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
"SC1090",
|
||||
expr,
|
||||
`Map values of type '${lowerer.checker.typeToString(targs[1])}' ` +
|
||||
`(Map values must be number, string, boolean, records, class instances, ` +
|
||||
`(Map values must be number, string, boolean, records, class instances, functions, ` +
|
||||
`typed arrays, arrays, promises, Maps, Sets, or supported unions and checked values)`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import { STATS_TYPE_TESTS } from "./builtins/filesystem.js";
|
||||
import { threadsImportOf } from "../threads-import.js";
|
||||
import { experimentalImportOf, lifetimeRegionMemberOf } from "../experimental-import.js";
|
||||
import { lowerInternTableProperty } from "./intern-tables.js";
|
||||
import { lowerThreadSlotsProperty } from "./thread-slots.js";
|
||||
import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../ir/build.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { SYMBOL_T } from "../../ir/ir.js";
|
||||
@@ -58,6 +59,11 @@ import {
|
||||
isCjsExportsIdentifier,
|
||||
lowerCjsExportsObjectValue,
|
||||
} from "./cjs-exports-object.js";
|
||||
import {
|
||||
lowerMaybePromiseAwait,
|
||||
maybePromiseInstanceOf,
|
||||
maybePromiseUnionOf,
|
||||
} from "./maybe-promise.js";
|
||||
import { checkedClassAssertion, checkedClassUnionAssertion } from "./class-assertions.js";
|
||||
import { checkedArmExtraction } from "./coercions/unions.js";
|
||||
import { narrowClassUnion, narrowStoredClassValue } from "./class-unions.js";
|
||||
@@ -141,6 +147,7 @@ import {
|
||||
} from "../../diagnostics/diagnostic.js";
|
||||
import { PoisonError, dynFallbackType, jsFuncNameOf, neverTaintedJsType, own } from "./lowerer.js";
|
||||
import { lowerCollectionSpread } from "./containers/collection-methods.js";
|
||||
import { lowerHelperReturnCall } from "./containers/iterator-helpers.js";
|
||||
import {
|
||||
arrayNumberRead,
|
||||
arrayValueRead,
|
||||
@@ -215,6 +222,11 @@ import {
|
||||
} from "./expressions/optional-chains.js";
|
||||
import { foldedStringKeyOf } from "./expressions/object-literals.js";
|
||||
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
|
||||
import {
|
||||
classToNumberConversion,
|
||||
classToStringConversion,
|
||||
classUnionToString,
|
||||
} from "./class-to-primitive.js";
|
||||
import {
|
||||
fenceNodeModuleMutation,
|
||||
isNodeModuleValue,
|
||||
@@ -1241,6 +1253,12 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
expr,
|
||||
'open tables with new InternTable("name")',
|
||||
);
|
||||
if (experimentalImportOf(lowerer.program, expr) === "ThreadSlots")
|
||||
lowerer.noLowering(
|
||||
"ThreadSlots (@scriptc/experimental) as a value",
|
||||
expr,
|
||||
'open slots with new ThreadSlots("name")',
|
||||
);
|
||||
}
|
||||
{
|
||||
const bi = lowerer.builtinImportOf(expr);
|
||||
@@ -1273,6 +1291,15 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
if (bi.module === "http" && bi.member === "METHODS") {
|
||||
return { kind: "libCall", fn: "http.methods", args: [], type: arrayOf(STRING), loc };
|
||||
}
|
||||
if ((bi.module === "http" || bi.module === "https") && bi.member === "globalAgent") {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "http.globalAgent",
|
||||
args: [{ kind: "boolLit", value: bi.module === "https", type: BOOL, loc }],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (bi.module === "http" && bi.member === "STATUS_CODES") {
|
||||
return { kind: "libCall", fn: "http.statusCodes", args: [], type: DYN, loc };
|
||||
}
|
||||
@@ -1761,6 +1788,18 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
const promiseArm = def.arms[promiseTag]!;
|
||||
const inner = promiseArm.kind === "promise" ? promiseArm.inner : VOID;
|
||||
const units = def.arms.filter(isUnitType);
|
||||
// A checked-dynamic inner: the whole operand is a dyn value (the
|
||||
// promise boxes by reference, the unit as itself), and the dyn
|
||||
// await adopts the promise or hops once for the unit.
|
||||
if (inner.kind === "dyn" && lowerer.dynConvertible(value.type)) {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "async.awaitDyn",
|
||||
args: [lowerer.coerceToExpected(value, DYN)],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (inner.kind === "union" || inner.kind === "dyn") {
|
||||
// A union inner would need an arm-wise re-tag into the result
|
||||
|
||||
@@ -1788,6 +1827,8 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
}
|
||||
return { kind: "awaitUnionExpr", value, promiseTag, type, loc };
|
||||
}
|
||||
const maybePromise = maybePromiseUnionOf(lowerer, value.type);
|
||||
if (maybePromise) return lowerMaybePromiseAwait(lowerer, expr, value, maybePromise, loc);
|
||||
}
|
||||
if (value.type.kind !== "promise") {
|
||||
// A CHECKED-DYNAMIC operand (`await v` where v rode an untyped
|
||||
@@ -1864,6 +1905,10 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
if (ts.isPostfixUnaryExpression(expr)) return lowerIncDec(lowerer, expr, false);
|
||||
if (ts.isBinaryExpression(expr)) return lowerer.lowerBinary(expr);
|
||||
if (ts.isCallExpression(expr)) {
|
||||
// `h.return?.()` on a helper object: the method exists, so the
|
||||
// optional call is the call.
|
||||
const helperReturn = lowerHelperReturnCall(lowerer, expr);
|
||||
if (helperReturn) return helperReturn;
|
||||
// The tail of an optional chain whose `?.` sits deeper
|
||||
// (`x?.trim().toLowerCase()`): the whole tail short-circuits together.
|
||||
if (isOptionalChainTail(lowerer, expr)) return lowerer.lowerOptionalChain(expr);
|
||||
@@ -1910,6 +1955,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
const table = lowerInternTableProperty(lowerer, expr);
|
||||
if (table) return table;
|
||||
}
|
||||
lowerThreadSlotsProperty(lowerer, expr);
|
||||
// `EventEmitter.name`, `zlib.Gzip.name`, `C.name` over a builtin
|
||||
// class alias.
|
||||
if (expr.name.text === "name" && !expr.questionDotToken) {
|
||||
@@ -4737,12 +4783,61 @@ export function lowerCondition(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
);
|
||||
}
|
||||
|
||||
/** `typeof (u as any).then` / `if ((u as Promise<T>).then)` over a
|
||||
* maybe-promise union `u` (the cast is erasure in JS): a promise arm has
|
||||
* the prototype's `then` function, and no data arm is thenable (the type
|
||||
* mapper's rule), so the union tag answers. Null for every other shape. */
|
||||
function maybePromiseThenPresence(
|
||||
lowerer: Lowerer,
|
||||
access: ts.PropertyAccessExpression,
|
||||
result: IrExpr,
|
||||
): IrExpr | null {
|
||||
let receiver: ts.Expression = access.expression;
|
||||
let cast = false;
|
||||
for (;;) {
|
||||
if (ts.isParenthesizedExpression(receiver)) receiver = receiver.expression;
|
||||
else if (ts.isAsExpression(receiver) || ts.isTypeAssertion(receiver)) {
|
||||
receiver = receiver.expression;
|
||||
cast = true;
|
||||
} else break;
|
||||
}
|
||||
if (!cast) return null;
|
||||
const mapped = lowerer.mapTypeOf(lowerer.typeOf(receiver));
|
||||
if (!mapped || !maybePromiseUnionOf(lowerer, mapped)) return null;
|
||||
const value = lowerer.lowerExpr(receiver);
|
||||
const mp = maybePromiseUnionOf(lowerer, value.type);
|
||||
if (!mp) return null;
|
||||
const loc = locOf(access);
|
||||
const absent: IrExpr =
|
||||
result.type.kind === "bool"
|
||||
? { kind: "boolLit", value: false, type: BOOL, loc }
|
||||
: { kind: "strLit", value: "undefined", type: STRING, loc };
|
||||
return {
|
||||
kind: "ternary",
|
||||
cond: {
|
||||
kind: "unionIsTag",
|
||||
unionId: mp.def.id,
|
||||
tag: mp.promiseTag,
|
||||
negated: false,
|
||||
value,
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: result,
|
||||
else_: absent,
|
||||
type: result.type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
function lowerPromiseThenPresence(
|
||||
lowerer: Lowerer,
|
||||
access: ts.PropertyAccessExpression,
|
||||
result: IrExpr,
|
||||
): IrExpr | null {
|
||||
if (access.name.text !== "then") return null;
|
||||
const maybePromise = maybePromiseThenPresence(lowerer, access, result);
|
||||
if (maybePromise !== null) return maybePromise;
|
||||
const mapped = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
|
||||
const promiseType =
|
||||
mapped?.kind === "promise"
|
||||
@@ -5926,6 +6021,11 @@ export function lowerArrayLiteral(
|
||||
const elem = mapped.elem;
|
||||
const arms = elem.kind === "union" ? lowerer.unions.get(elem.unionId)?.arms : [elem];
|
||||
if (arms?.length && arms.every((arm) => arm.kind === "classval")) mapped = DYN;
|
||||
// Inferred JavaScript literals mixing promises and plain values
|
||||
// (`[Promise.resolve(1), 2]`) keep the checked-dynamic array: their
|
||||
// consumers (Readable.from, Promise.all over iterators) inspect each
|
||||
// element at run time.
|
||||
if (maybePromiseUnionOf(lowerer, elem)) mapped = DYN;
|
||||
}
|
||||
if (
|
||||
!expected &&
|
||||
@@ -7229,25 +7329,38 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre
|
||||
/** A record receiver that an outer keyed read left optional
|
||||
* (`nested[a][b]`, where `nested[a]` can miss). The receiver evaluates
|
||||
* once into `stmts`; `present` is its record arm, valid only once the
|
||||
* access has tested `stable` against the undefined arm. */
|
||||
* access has tested `stable` against the undefined (or, for reads, null)
|
||||
* arm. */
|
||||
interface OptionalRecordReceiver {
|
||||
stmts: IrStmt[];
|
||||
stable: IrExpr;
|
||||
present: IrExpr;
|
||||
unionId: string;
|
||||
undefTag: number;
|
||||
nullish: "undefined" | "null";
|
||||
}
|
||||
|
||||
function optionalRecordReceiver(
|
||||
lowerer: Lowerer,
|
||||
value: IrExpr,
|
||||
loc: SrcLoc,
|
||||
allowNull = false,
|
||||
): OptionalRecordReceiver | null {
|
||||
if (value.type.kind !== "union") return null;
|
||||
const unionId = value.type.unionId;
|
||||
const undefTag = lowerer.armTag(unionId, UNDEFINED_T);
|
||||
if (undefTag < 0 || lowerer.unions.get(unionId)?.arms.length !== 2) return null;
|
||||
const record = lowerer.stripUndefinedArm(value.type);
|
||||
const arms = lowerer.unions.get(unionId)?.arms;
|
||||
if (arms?.length !== 2) return null;
|
||||
let undefTag = lowerer.armTag(unionId, UNDEFINED_T);
|
||||
let nullish: OptionalRecordReceiver["nullish"] = "undefined";
|
||||
// A null-initialized slot assigned a record later (iconv-lite's
|
||||
// `encodings = null` table, filled on first use): the checker narrows
|
||||
// the read to the record, the storage keeps the null arm.
|
||||
if (undefTag < 0 && allowNull) {
|
||||
undefTag = lowerer.armTag(unionId, NULL_T);
|
||||
nullish = "null";
|
||||
}
|
||||
if (undefTag < 0) return null;
|
||||
const record = arms[1 - undefTag]!;
|
||||
if (record.kind !== "record") return null;
|
||||
const stmts: IrStmt[] = [];
|
||||
const stable = stabilized(lowerer, value, "%recordReceiver", stmts);
|
||||
@@ -7259,7 +7372,7 @@ function optionalRecordReceiver(
|
||||
type: record,
|
||||
loc,
|
||||
};
|
||||
return { stmts, stable, present, unionId, undefTag };
|
||||
return { stmts, stable, present, unionId, undefTag, nullish };
|
||||
}
|
||||
|
||||
/** `value`, evaluated once at this point of `out`. */
|
||||
@@ -7282,14 +7395,16 @@ function undefinedReceiver(receiver: OptionalRecordReceiver, loc: SrcLoc): IrExp
|
||||
};
|
||||
}
|
||||
|
||||
/** Node's TypeError for a keyed read or write on an undefined receiver. */
|
||||
/** Node's TypeError for a keyed read or write on an undefined (or null)
|
||||
* receiver. */
|
||||
function undefinedReceiverThrow(
|
||||
key: IrExpr,
|
||||
action: "read" | "set",
|
||||
type: IrType,
|
||||
loc: SrcLoc,
|
||||
nullish: OptionalRecordReceiver["nullish"] = "undefined",
|
||||
): IrExpr {
|
||||
const head = `Cannot ${action} properties of undefined (${action === "read" ? "reading" : "setting"} '`;
|
||||
const head = `Cannot ${action} properties of ${nullish} (${action === "read" ? "reading" : "setting"} '`;
|
||||
const message: IrExpr = {
|
||||
kind: "strConcat",
|
||||
left: {
|
||||
@@ -7319,8 +7434,9 @@ function guardedRecordAccess(
|
||||
lowerer: Lowerer,
|
||||
access: ts.ElementAccessExpression,
|
||||
rawObj: IrExpr,
|
||||
allowNull = false,
|
||||
): { receiver: OptionalRecordReceiver; key: IrExpr; name: IrExpr } | null {
|
||||
const receiver = optionalRecordReceiver(lowerer, rawObj, locOf(access.expression));
|
||||
const receiver = optionalRecordReceiver(lowerer, rawObj, locOf(access.expression), allowNull);
|
||||
if (!receiver) return null;
|
||||
const keyNode = access.argumentExpression;
|
||||
const litKey = recordKeyLiteralText(keyNode) ?? recordKeyTypeLiteralText(lowerer, keyNode);
|
||||
@@ -7384,6 +7500,89 @@ function lowerGuardedRecordWrite(
|
||||
};
|
||||
}
|
||||
|
||||
/** `b[i] = v` where the checker's flow type of `b` is a typed array but
|
||||
* its storage is wider. A hoisted JavaScript `var` (or a field written
|
||||
* after construction) holds `bytes | undefined`: the receiver, index and
|
||||
* value evaluate in order, then an undefined receiver throws Node's
|
||||
* TypeError and a present one takes the typed store. Any other storage (a
|
||||
* `var` later reassigned a string, a checked-dynamic value) takes the
|
||||
* checked-dynamic store, which applies whatever value the receiver holds. */
|
||||
function lowerWiderBytesWrite(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.BinaryExpression,
|
||||
target: ts.ElementAccessExpression,
|
||||
recv: IrExpr,
|
||||
): IrStmt {
|
||||
const loc = locOf(expr);
|
||||
if (recv.type.kind === "union") {
|
||||
const unionId = recv.type.unionId;
|
||||
const arms = lowerer.unions.get(unionId)?.arms;
|
||||
const undefTag = lowerer.armTag(unionId, UNDEFINED_T);
|
||||
const bytes = arms?.length === 2 && undefTag >= 0 ? arms[1 - undefTag]! : null;
|
||||
const keyType = lowerer.mapTypeOf(lowerer.typeOf(target.argumentExpression));
|
||||
const index =
|
||||
bytes?.kind === "bytes" && keyType?.kind === "f64"
|
||||
? lowerer.lowerExpr(target.argumentExpression)
|
||||
: null;
|
||||
// An index stored wider than a number (a hoisted `var` counter) keeps
|
||||
// the checked-dynamic store below, which converts the key exactly.
|
||||
if (bytes?.kind === "bytes" && index?.type.kind === "f64") {
|
||||
const stmts: IrStmt[] = [];
|
||||
const stable = stabilized(lowerer, recv, "%bytesReceiver", stmts);
|
||||
const key = stabilized(lowerer, index, "%bytesIndex", stmts);
|
||||
const value = stabilized(
|
||||
lowerer,
|
||||
lowerer.lowerExprExpecting(expr.right, F64),
|
||||
"%bytesValue",
|
||||
stmts,
|
||||
);
|
||||
const name = lowerRecordPropertyKey(lowerer, key, target.argumentExpression);
|
||||
const present: IrExpr = {
|
||||
kind: "unionNarrow",
|
||||
unionId,
|
||||
tag: 1 - undefTag,
|
||||
value: stable,
|
||||
type: bytes,
|
||||
loc,
|
||||
};
|
||||
return {
|
||||
kind: "block",
|
||||
body: [
|
||||
...stmts,
|
||||
{
|
||||
kind: "if",
|
||||
cond: {
|
||||
kind: "unionIsTag",
|
||||
unionId,
|
||||
tag: undefTag,
|
||||
negated: false,
|
||||
value: stable,
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: [{ kind: "exprStmt", expr: undefinedReceiverThrow(name, "set", VOID, loc), loc }],
|
||||
else_: [{ kind: "bytesSet", arr: present, index: key, value, loc }],
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (!lowerer.dynConvertible(recv.type)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
target.expression,
|
||||
`typed-array element writes through '${lowerer.fmt(recv.type)}' storage`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
kind: "exprStmt",
|
||||
expr: lowerDynMemberAssignment(lowerer, expr, lowerer.coerceInto(target.expression, recv, DYN)),
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `r[k]` over a (non-tuple) record shape. A LITERAL key naming a declared
|
||||
* field is the bracket spelling of field access (the ONLY spelling tsc
|
||||
* permits for signature-declared fields under
|
||||
@@ -7423,7 +7622,15 @@ export function lowerRecordKeyRead(
|
||||
// evaluates the receiver before the key).
|
||||
const obj = lowerer.lowerExpr(expr.expression);
|
||||
{
|
||||
const guarded = guardedRecordAccess(lowerer, expr, obj);
|
||||
// A null arm is guarded only for string and number keys: other keys
|
||||
// already take the checked-dynamic read, which handles null itself.
|
||||
// Only a receiver with a null arm asks about the key.
|
||||
let allowNull = false;
|
||||
if (obj.type.kind === "union" && lowerer.armTag(obj.type.unionId, NULL_T) >= 0) {
|
||||
const keyType = litKey !== null ? STRING : lowerer.mapTypeOf(lowerer.typeOf(keyNode));
|
||||
allowNull = keyType?.kind === "string" || keyType?.kind === "f64";
|
||||
}
|
||||
const guarded = guardedRecordAccess(lowerer, expr, obj, allowNull);
|
||||
if (guarded) {
|
||||
const { receiver, key, name } = guarded;
|
||||
const read = lowerer.withExpressionOverride(expr.expression, receiver.present, () =>
|
||||
@@ -7439,7 +7646,7 @@ export function lowerRecordKeyRead(
|
||||
result: {
|
||||
kind: "ternary",
|
||||
cond: undefinedReceiver(receiver, loc),
|
||||
then: undefinedReceiverThrow(name, "read", read.type, loc),
|
||||
then: undefinedReceiverThrow(name, "read", read.type, loc, receiver.nullish),
|
||||
else_: read,
|
||||
type: read.type,
|
||||
loc,
|
||||
@@ -8029,7 +8236,7 @@ function lowerElementCompoundParts(
|
||||
const appended: IrExpr = {
|
||||
kind: "strConcat",
|
||||
left: oldValue,
|
||||
right: lowerer.ensureString(rhs, expr.right),
|
||||
right: concatOperand(lowerer, rhs, expr.right),
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
@@ -8047,7 +8254,7 @@ function lowerElementCompoundParts(
|
||||
computed = {
|
||||
kind: "strConcat",
|
||||
left: lowerer.ensureString(oldRef(), target),
|
||||
right: lowerer.ensureString(rhs, expr.right),
|
||||
right: concatOperand(lowerer, rhs, expr.right),
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
@@ -8430,6 +8637,8 @@ export function lowerElementWrite(lowerer: Lowerer, expr: ts.BinaryExpression):
|
||||
// the runtime coerces JS-exactly; invalid indices are ignored.
|
||||
if (receiverIr?.kind === "bytes") {
|
||||
const recv = lowerer.lowerExpr(target.expression);
|
||||
// The checker's flow type is a typed array, but the storage is wider.
|
||||
if (recv.type.kind !== "bytes") return lowerWiderBytesWrite(lowerer, expr, target, recv);
|
||||
const index = lowerer.lowerExpr(target.argumentExpression);
|
||||
if (index.type.kind === "dyn" && recv.type.kind === "bytes") {
|
||||
return {
|
||||
@@ -8798,6 +9007,16 @@ export function lowerEnvironmentKey(lowerer: Lowerer, node: ts.Expression): IrEx
|
||||
return lowerRecordPropertyKey(lowerer, key, node);
|
||||
}
|
||||
|
||||
/** An operand of `+` with a string: ToString of ToPrimitive with the
|
||||
* default hint, which tries a class instance's valueOf before toString. */
|
||||
function concatOperand(lowerer: Lowerer, e: IrExpr, node: ts.Node): IrExpr {
|
||||
return (
|
||||
classToStringConversion(lowerer, e, "default", node) ??
|
||||
classUnionToString(lowerer, e, "default", node) ??
|
||||
lowerer.ensureString(e, node)
|
||||
);
|
||||
}
|
||||
|
||||
export function ensureString(lowerer: Lowerer, e: IrExpr, node: ts.Node): IrExpr {
|
||||
if (e.type.kind === "string") return e;
|
||||
if (e.type.kind === "url")
|
||||
@@ -8841,6 +9060,9 @@ export function ensureString(lowerer: Lowerer, e: IrExpr, node: ts.Node): IrExpr
|
||||
return errorToStringCall(lowerer, e);
|
||||
}
|
||||
}
|
||||
// A program class instance converts through its own methods.
|
||||
const converted = classToStringConversion(lowerer, e, "string", node);
|
||||
if (converted) return converted;
|
||||
}
|
||||
if (e.type.kind === "union") {
|
||||
// `${u}` — the arm's ToString via a per-union interned helper
|
||||
@@ -8899,6 +9121,8 @@ export function ensureString(lowerer: Lowerer, e: IrExpr, node: ts.Node): IrExpr
|
||||
node,
|
||||
);
|
||||
}
|
||||
const viaClasses = classUnionToString(lowerer, e, "string", node);
|
||||
if (viaClasses) return viaClasses;
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
node,
|
||||
@@ -9372,6 +9596,8 @@ export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpressio
|
||||
if (raw.type.kind === "string") {
|
||||
return { kind: "libCall", fn: "num.fromString", args: [raw], type: F64, loc };
|
||||
}
|
||||
const classNumber = classToNumberConversion(lowerer, raw, expr.operand, "unary plus");
|
||||
if (classNumber) return classNumber;
|
||||
const operand = lowerOptionalNumber(lowerer, raw, loc, expr.operand);
|
||||
if (operand.type.kind !== "f64") operatorUnsupported(lowerer, expr, "+", [raw.type]);
|
||||
return operand;
|
||||
@@ -9840,7 +10066,7 @@ export function lowerCompoundValueToTarget(
|
||||
{
|
||||
kind: "strConcat",
|
||||
left: lowerer.ensureString(read, expr.left),
|
||||
right: lowerer.ensureString(rhs, expr.right),
|
||||
right: concatOperand(lowerer, rhs, expr.right),
|
||||
type: STRING,
|
||||
loc,
|
||||
},
|
||||
@@ -10903,8 +11129,8 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
if (left.type.kind === "string" || right.type.kind === "string") {
|
||||
return {
|
||||
kind: "strConcat",
|
||||
left: lowerer.ensureString(left, expr.left),
|
||||
right: lowerer.ensureString(right, expr.right),
|
||||
left: concatOperand(lowerer, left, expr.left),
|
||||
right: concatOperand(lowerer, right, expr.right),
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
@@ -12694,6 +12920,38 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
|
||||
};
|
||||
}
|
||||
}
|
||||
// `x instanceof Promise` over a promise-armed union (the maybe-promise
|
||||
// `T | Promise<T>` dispatch): a union tag test — the data arms are
|
||||
// never promises (scriptc promises are all native, and no data arm
|
||||
// can hold one). A promise-typed operand answers true after its
|
||||
// evaluation; a checked-dynamic one asks the runtime.
|
||||
if (
|
||||
ts.isIdentifier(expr.right) &&
|
||||
lowerer.isStdlibGlobal(expr.right, "Promise") &&
|
||||
!lowerer.caughtLocalOf(expr.left)
|
||||
) {
|
||||
const left = lowerer.lowerExpr(expr.left);
|
||||
const tagTest = maybePromiseInstanceOf(lowerer, left, loc);
|
||||
if (tagTest) return tagTest;
|
||||
if (left.type.kind === "dyn")
|
||||
return { kind: "dynTest", test: "promise", value: left, type: BOOL, loc };
|
||||
if (left.type.kind === "promise") {
|
||||
const yes: IrExpr = { kind: "boolLit", value: true, type: BOOL, loc };
|
||||
if (isSafeToDiscard(left)) return yes;
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: left, loc }],
|
||||
result: yes,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
`'instanceof Promise' on '${lowerer.fmt(left.type)}' values (only promises, promise-armed unions, and checked-dynamic values have a lowering)`,
|
||||
);
|
||||
}
|
||||
// `x instanceof RegExp` over a union with a regex arm (the
|
||||
// skip-utility `string | RegExp` dispatch): a union tag test — the
|
||||
// socket-narrowing shape; the checker types the branches and
|
||||
@@ -15932,7 +16190,11 @@ export function lowerPromiseRejectCall(
|
||||
);
|
||||
}
|
||||
const ctxT = lowerer.checker.getContextualType(call);
|
||||
const type = ctxT ? lowerer.mapTypeOf(ctxT) : null;
|
||||
const contextual = ctxT ? lowerer.mapTypeOf(ctxT) : null;
|
||||
// A maybe-promise context (`return Promise.reject(e)` from a function
|
||||
// returning `T | Promise<T>`) names its one promise arm.
|
||||
const maybePromise = contextual ? maybePromiseUnionOf(lowerer, contextual) : null;
|
||||
const type = maybePromise ? maybePromise.promise : contextual;
|
||||
const reasonNode = call.arguments[0]!;
|
||||
// A CHECKED-DYNAMIC reason (`Promise.reject(value)` where value rode
|
||||
// an untyped binding — the tracingChannel suite's shape): the checked-dynamic tree
|
||||
@@ -16196,7 +16458,7 @@ function lowerFieldCompoundValue(
|
||||
const numericRead = lowerOptionalNumber(lowerer, read, loc, access);
|
||||
const bigintValue = rhsNode ? bigintCompoundValue(op, read, rhs, loc) : null;
|
||||
if (op === "+" && read.type.kind === "string") {
|
||||
appended = lowerer.ensureString(rhs, rhsNode ?? access);
|
||||
appended = concatOperand(lowerer, rhs, rhsNode ?? access);
|
||||
value = { kind: "strConcat", left: read, right: appended, type: STRING, loc };
|
||||
} else if (numericRead.type.kind === "f64" && numericRhs.type.kind === "f64") {
|
||||
value = { kind: "bin", op, left: numericRead, right: numericRhs, type: F64, loc };
|
||||
|
||||
@@ -270,6 +270,8 @@ export function lowerLambda(
|
||||
: lowerer.bodyReturnType(isAsync, funcType.ret);
|
||||
|
||||
const fnCtx = newFnCtx(true, selfSymbol, funcType, bodyReturn);
|
||||
if (lowerer.methodLambdasInProgress.has(node))
|
||||
lowerer.methodLambdasInProgress.set(node, { fnName, type: funcType, selfReads: 0 });
|
||||
const inferredSignature =
|
||||
isJsSourceFile(node.getSourceFile()) && !node.type
|
||||
? lowerer.checker.getSignatureFromDeclaration(node)
|
||||
@@ -345,9 +347,12 @@ export function lowerLambda(
|
||||
} else {
|
||||
const value = lowerer.lowerExpr(bodyExpr);
|
||||
// `async () => p` with a void result still resolves the
|
||||
// function's promise with p (its settlement, rejections included).
|
||||
body =
|
||||
value.kind === "unitLit"
|
||||
// function's promise with p (its settlement, rejections included);
|
||||
// a maybe-promise value adopts only its promise arm.
|
||||
const flattened = isAsync ? lowerer.maybePromiseReturn(bodyExpr, value, VOID) : null;
|
||||
body = flattened
|
||||
? [flattened]
|
||||
: value.kind === "unitLit"
|
||||
? []
|
||||
: isAsync && value.type.kind === "promise"
|
||||
? [lowerer.adoptingReturn(value, VOID, (e) => e)]
|
||||
@@ -367,8 +372,13 @@ export function lowerLambda(
|
||||
if (isAsync && value.type.kind === "promise" && bodyReturn.kind !== "promise") {
|
||||
value = lowerer.returnedPromiseAwait(value);
|
||||
}
|
||||
body =
|
||||
value.type.kind === "void" && lowerer.wrappedUndefined(bodyReturn, locOf(node.body!))
|
||||
const flattened =
|
||||
isAsync && !fnCtx.inferReturn
|
||||
? lowerer.maybePromiseReturn(bodyExpr, value, bodyReturn)
|
||||
: null;
|
||||
body = flattened
|
||||
? [flattened]
|
||||
: value.type.kind === "void" && lowerer.wrappedUndefined(bodyReturn, locOf(node.body!))
|
||||
? [{ kind: "exprStmt", expr: value, loc: locOf(node.body!) }]
|
||||
: [
|
||||
{
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
canonicalBuiltinModule,
|
||||
cjsExportAssignmentOf,
|
||||
cjsExportChainOf,
|
||||
cjsVoidInitPreambleReals,
|
||||
cjsExportDiscardReason,
|
||||
cjsExportTargetLiteral,
|
||||
entryPackageFilePredicate,
|
||||
@@ -1420,6 +1421,11 @@ export function collectGlobals(
|
||||
for (const nameNode of chain.names) registerExport(nameNode, nameNode.text, chain.value);
|
||||
continue;
|
||||
}
|
||||
// A dropped tsc void-init preamble whose real assignment is such a
|
||||
// class alias registers nothing either: storage here would never
|
||||
// be assigned, and importers would read it instead of the class.
|
||||
const real = cjsVoidInitPreambleReals(cjs)?.get(cjs.name.text);
|
||||
if (real && lowerer.cjsMemberExportClassSymbol(real.expr) !== null) continue;
|
||||
registerExport(cjs.name, cjs.name.text, cjs.value);
|
||||
continue;
|
||||
}
|
||||
@@ -2875,7 +2881,16 @@ export function lowerFileInit(
|
||||
const wrapped =
|
||||
g.type.kind === "dyn" ? dynUndefinedExpr(loc0) : lowerer.unassignedSlotInit(g.type, loc0);
|
||||
if (wrapped) {
|
||||
header.push({ kind: "assign", localId: g.id, value: wrapped, loc: loc0 });
|
||||
// A let global whose early reads are checked (initFlag) only
|
||||
// gets its storage filled here: this is not the source write, so
|
||||
// it must neither run the TDZ check nor set the flag.
|
||||
header.push({
|
||||
kind: "assign",
|
||||
localId: g.id,
|
||||
value: wrapped,
|
||||
...(g.initFlag !== undefined ? { initializes: true as const } : {}),
|
||||
loc: loc0,
|
||||
});
|
||||
}
|
||||
}
|
||||
const prelude = lowerer.jsonInitActions.get(sf) ?? [];
|
||||
|
||||
@@ -4310,12 +4310,12 @@ function lowerHttpCreateServerForms(
|
||||
* getName/destroy and the sockets/requests/freeSockets counters dispatch
|
||||
* through the dyn handle ops, and the request path threads it (the
|
||||
* requestAgent rows). Options parse at the LITERAL construction site:
|
||||
* keepAlive (a boolean value — TRUE throws the runtime's named pooling
|
||||
* fence: this client dials one connection per request), keepAliveMsecs /
|
||||
* maxSockets / maxFreeSockets / timeout / port as numbers, scheduling
|
||||
* DROPS (no free pool exists, so selection order cannot observe), other
|
||||
* documented keys fence by name, and undocumented keys drop like Node
|
||||
* drops them. Null when the callee isn't the http/https Agent. */
|
||||
* keepAlive (a boolean value: finished connections return to the free
|
||||
* list), keepAliveMsecs / maxSockets / maxFreeSockets / timeout / port as
|
||||
* numbers, scheduling as a value the runtime validates ('fifo' or
|
||||
* 'lifo'), other documented keys fence by name, and undocumented keys
|
||||
* drop like Node drops them. Null when the callee isn't the http/https
|
||||
* Agent. */
|
||||
export function lowerHttpAgentNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null {
|
||||
const callee = expr.expression;
|
||||
const bi =
|
||||
@@ -4334,6 +4334,7 @@ export function lowerHttpAgentNew(lowerer: Lowerer, expr: ts.NewExpression): IrE
|
||||
let maxFree: IrExpr = numLit(-1, loc);
|
||||
let timeout: IrExpr = numLit(-1, loc);
|
||||
let port: IrExpr = numLit(-1, loc);
|
||||
let scheduling: IrExpr = lowerer.coerceToExpected(strLit("lifo", loc), DYN);
|
||||
const args = expr.arguments ?? [];
|
||||
if (args.length > 1) {
|
||||
lowerer.noLowering(
|
||||
@@ -4416,11 +4417,15 @@ export function lowerHttpAgentNew(lowerer: Lowerer, expr: ts.NewExpression): IrE
|
||||
// the settable defaultPort carries this one.
|
||||
port = lowerVal("f64");
|
||||
break;
|
||||
case "scheduling":
|
||||
// Free-socket selection order: no free pool exists here (no
|
||||
// keep-alive reuse), so the choice cannot observe — dropped.
|
||||
if (initializer !== null) lowerer.lowerExpr(initializer); // evaluate, like Node
|
||||
case "scheduling": {
|
||||
// 'fifo' | 'lifo' (falsy = 'lifo'), validated by the runtime
|
||||
const v =
|
||||
initializer !== null
|
||||
? lowerer.lowerExpr(initializer)
|
||||
: lowerer.lowerShorthandValue(prop as ts.ShorthandPropertyAssignment);
|
||||
scheduling = lowerer.coerceToExpected(v, DYN);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
fenceOrDropOptionKey(
|
||||
lowerer,
|
||||
@@ -4446,6 +4451,7 @@ export function lowerHttpAgentNew(lowerer: Lowerer, expr: ts.NewExpression): IrE
|
||||
maxFree,
|
||||
timeout,
|
||||
port,
|
||||
scheduling,
|
||||
],
|
||||
type: DYN,
|
||||
loc,
|
||||
@@ -6042,6 +6048,13 @@ function lowerHttpClientCall(
|
||||
maxHeaderSize = lowerVal("f64");
|
||||
break;
|
||||
case "servername":
|
||||
if (binding !== null) {
|
||||
lowerer.noLowering(
|
||||
`${member} option 'servername' through a module-function binding`,
|
||||
prop,
|
||||
"call https.request directly when passing servername",
|
||||
);
|
||||
}
|
||||
if (!secureish) {
|
||||
lowerer.noLowering(
|
||||
`${member} option 'servername'`,
|
||||
@@ -6278,14 +6291,48 @@ function lowerHttpClientCall(
|
||||
timeout ??= numLit(0, loc);
|
||||
headers ??= { kind: "arrayLit", elems: [], type: arrayOf(STRING), loc };
|
||||
const autoEnd = boolLit(member === "get", loc);
|
||||
// Options applied to the built request: servername before the TLS
|
||||
// handshake starts, then options.signal (an already aborted signal
|
||||
// destroys the request at once, like Node's constructor).
|
||||
// options.signal is checked first: Node's constructor rejects a value
|
||||
// that is not an AbortSignal before it dials. servername is stored just
|
||||
// before construction (it selects the TLS name and which pooled sockets
|
||||
// the request may reuse); maxHeaderSize and the signal then apply to the
|
||||
// built request (an already aborted signal destroys it at once, like
|
||||
// Node's constructor).
|
||||
const withSignal = (request: IrExpr): IrExpr => {
|
||||
if (signal === null && servername === null && maxHeaderSize === null) return request;
|
||||
const local = lowerer.declareHiddenLocal("%requestOptions", HTTPCLIENTREQ_T);
|
||||
const ref = (): IrExpr => varRef(local.id, HTTPCLIENTREQ_T, loc);
|
||||
const steps: IrStmt[] = [{ kind: "varDecl", localId: local.id, init: request, loc }];
|
||||
const steps: IrStmt[] = [];
|
||||
let signalRef: (() => IrExpr) | null = null;
|
||||
if (signal !== null) {
|
||||
const signalLocal = lowerer.declareHiddenLocal("%requestSignal", DYN);
|
||||
signalRef = () => varRef(signalLocal.id, DYN, loc);
|
||||
steps.push({ kind: "varDecl", localId: signalLocal.id, init: signal, loc });
|
||||
steps.push({
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "libCall",
|
||||
fn: "http.clientSignalCheck",
|
||||
args: [signalRef()],
|
||||
type: VOID,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
});
|
||||
}
|
||||
if (servername !== null) {
|
||||
steps.push({
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "libCall",
|
||||
fn: "https.nextServername",
|
||||
args: [servername],
|
||||
type: VOID,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
});
|
||||
}
|
||||
steps.push({ kind: "varDecl", localId: local.id, init: request, loc });
|
||||
if (maxHeaderSize !== null) {
|
||||
steps.push({
|
||||
kind: "exprStmt",
|
||||
@@ -6299,26 +6346,19 @@ function lowerHttpClientCall(
|
||||
loc,
|
||||
});
|
||||
}
|
||||
if (servername !== null) {
|
||||
if (signalRef !== null) {
|
||||
steps.push({
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "libCall",
|
||||
fn: "https.clientServername",
|
||||
args: [ref(), servername],
|
||||
fn: "http.clientSignal",
|
||||
args: [ref(), signalRef()],
|
||||
type: VOID,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
});
|
||||
}
|
||||
if (signal !== null) {
|
||||
steps.push({
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "libCall", fn: "http.clientSignal", args: [ref(), signal], type: VOID, loc },
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return { kind: "seqExpr", stmts: steps, result: ref(), type: HTTPCLIENTREQ_T, loc };
|
||||
};
|
||||
if (connCb !== null) {
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
type IrExpr,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
type SrcLoc,
|
||||
STRING,
|
||||
VOID,
|
||||
canConvertToDyn,
|
||||
@@ -1932,11 +1933,39 @@ export function lowerResponseNew(lowerer: Lowerer, expr: ts.NewExpression): IrEx
|
||||
if (stdlibGlobalNameOf(lowerer, expr.expression) !== "Response") return null;
|
||||
const args = expr.arguments ?? [];
|
||||
if (args.some(ts.isSpreadElement)) return null;
|
||||
const loc = locOf(expr);
|
||||
return lowerResponseConstruction(lowerer, args, locOf(expr), false);
|
||||
}
|
||||
|
||||
/** Static `Response.json(data, init?)`: the data crosses into the runtime's
|
||||
* checked-dynamic JSON serializer (toJSON hooks included) after ResponseInit
|
||||
* converts, then the response initializes as the constructor does. */
|
||||
export function lowerResponseStaticJson(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null {
|
||||
if (call.questionDotToken || !ts.isPropertyAccessExpression(call.expression)) return null;
|
||||
const access = call.expression;
|
||||
if (access.questionDotToken || access.name.text !== "json") return null;
|
||||
if (!lowerer.isStdlibGlobal(access.expression, "Response")) return null;
|
||||
if (call.arguments.some(ts.isSpreadElement) || call.arguments.length === 0) return null;
|
||||
return lowerResponseConstruction(lowerer, call.arguments, locOf(call), true);
|
||||
}
|
||||
|
||||
function lowerResponseConstruction(
|
||||
lowerer: Lowerer,
|
||||
args: readonly ts.Expression[],
|
||||
loc: SrcLoc,
|
||||
json: boolean,
|
||||
): IrExpr {
|
||||
const toDyn = (node: ts.Expression | undefined, value: IrExpr, what: string): IrExpr => {
|
||||
if (node === undefined) return dynUndefinedExpr(loc);
|
||||
if (value.type.kind === "dyn") return value;
|
||||
if (what === "JSON data") {
|
||||
const boxed = lowerer.coerceToExpected(value, DYN);
|
||||
if (boxed.type.kind === "dyn") return boxed;
|
||||
lowerer.noLowering(
|
||||
`Response.json of '${lowerer.fmt(value.type)}' values`,
|
||||
node,
|
||||
"the data must cross the checked-dynamic boundary (number, string, boolean, null, records, arrays, and 'unknown')",
|
||||
);
|
||||
}
|
||||
/* A user class can satisfy ResponseInit structurally through plain
|
||||
* fields. Project just the declared dictionary members into a record,
|
||||
* then use the ordinary checked-dynamic copy. objRecordWidthHelper
|
||||
@@ -2059,8 +2088,11 @@ export function lowerResponseNew(lowerer: Lowerer, expr: ts.NewExpression): IrEx
|
||||
}
|
||||
const result: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "fetch.responseNew",
|
||||
args: [toDyn(bodyNode, bodyRef, "a body"), toDyn(initNode, initRef, "an init value")],
|
||||
fn: json ? "fetch.responseJsonStatic" : "fetch.responseNew",
|
||||
args: [
|
||||
toDyn(bodyNode, bodyRef, json ? "JSON data" : "a body"),
|
||||
toDyn(initNode, initRef, "an init value"),
|
||||
],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { dynUndefinedExpr, numLit, varRef } from "../../ir/build.js";
|
||||
import { lowerCollectionSpread } from "./containers/collection-methods.js";
|
||||
import { lowerEventObjectPropertyWrite } from "./lower-event-target.js";
|
||||
import { lowerDynObjectLiteral } from "./expressions/object-literals.js";
|
||||
import { importMetaBindingSource } from "./import-meta.js";
|
||||
@@ -74,6 +75,7 @@ import { enforceLibBoundary, lowerSurplusCalls } from "./lib-boundary.js";
|
||||
import {
|
||||
cjsExportAssignmentOf,
|
||||
cjsExportChainOf,
|
||||
cjsVoidInitPreambleReals,
|
||||
cjsExportDiscardReason,
|
||||
cjsExportTargetLiteral,
|
||||
isCjsJsFile,
|
||||
@@ -1012,12 +1014,23 @@ export function predeclareForwardFnDecl(lowerer: Lowerer, symbol: ts.Symbol): bo
|
||||
entry.ctx.hoistedFnDecls.add(decl);
|
||||
const fnTail = lowerer.fnStack.splice(depth + 1);
|
||||
const scopeTail = entry.ctx.scopes.splice(frameIdx + 1);
|
||||
// The hoisted function is kept even when the reference lowers inside
|
||||
// a speculative attempt (a probe's temporary diagnostic sink): lower
|
||||
// it without that sink, so its failing JS statements keep their
|
||||
// runtime fences instead of vanishing from a body that is kept. A
|
||||
// declaration that fails as a whole is not kept: its diagnostics go
|
||||
// back to the probe.
|
||||
const previousSink = lowerer.diagSink;
|
||||
const diagsBefore = lowerer.diags.length;
|
||||
lowerer.diagSink = null;
|
||||
try {
|
||||
entry.out.push(lowerer.lowerNestedFunctionDecl(decl));
|
||||
} catch (e) {
|
||||
entry.ctx.hoistedFnDecls.delete(decl);
|
||||
if (previousSink !== null) previousSink.push(...lowerer.diags.splice(diagsBefore));
|
||||
throw e;
|
||||
} finally {
|
||||
lowerer.diagSink = previousSink;
|
||||
entry.ctx.scopes.push(...scopeTail);
|
||||
lowerer.fnStack.push(...fnTail);
|
||||
}
|
||||
@@ -2503,6 +2516,39 @@ export function lowerBindingPattern(
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Map and Set sources (`const [[k, v], ...rest] = map`): the iterator
|
||||
// yields the collection's entries (Map pairs) or values in order, so
|
||||
// the source snapshots ONCE into that array — the spread's own
|
||||
// machinery — and the pattern lowers as an array pattern over it,
|
||||
// like the string case above. Element defaults keep a fence: they
|
||||
// run between iterator steps and could mutate the live collection.
|
||||
if (srcType.kind === "map" || srcType.kind === "set") {
|
||||
const defaulted = pattern.elements.find(
|
||||
(el): el is ts.BindingElement =>
|
||||
ts.isBindingElement(el) && el.initializer !== undefined && !el.dotDotDotToken,
|
||||
);
|
||||
if (defaulted)
|
||||
lowerer.unsupported("SC1031", defaulted, "binding defaults over Map/Set sources");
|
||||
const snapshot = lowerCollectionSpread(
|
||||
lowerer,
|
||||
srcRef(),
|
||||
pattern as unknown as ts.Expression,
|
||||
);
|
||||
if (snapshot && snapshot.type.kind === "array") {
|
||||
const loc = locOf(pattern);
|
||||
const snapshotT = snapshot.type;
|
||||
const entries = lowerer.declareHiddenLocal("%dentries", snapshotT);
|
||||
out.push({ kind: "varDecl", localId: entries.id, init: snapshot, loc });
|
||||
lowerer.lowerBindingPattern(
|
||||
pattern,
|
||||
() => ({ kind: "varRef", localId: entries.id, type: snapshotT, loc }),
|
||||
snapshotT,
|
||||
isLet,
|
||||
out,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (srcType.kind !== "array") {
|
||||
lowerer.unsupported(
|
||||
"SC1031",
|
||||
@@ -6130,47 +6176,7 @@ function lowerCjsExportStatement(
|
||||
// the real assignment's position already orders. Preambles with no
|
||||
// later real assignment keep the ordinary path (the export IS
|
||||
// undefined then).
|
||||
if (ts.isIdentifier(cjs.name)) {
|
||||
const isExportsMember = (e: ts.Expression): e is ts.PropertyAccessExpression =>
|
||||
ts.isPropertyAccessExpression(e) &&
|
||||
ts.isIdentifier(e.name) &&
|
||||
((ts.isIdentifier(e.expression) && e.expression.text === "exports") ||
|
||||
(ts.isPropertyAccessExpression(e.expression) &&
|
||||
ts.isIdentifier(e.expression.expression) &&
|
||||
e.expression.expression.text === "module" &&
|
||||
ts.isIdentifier(e.expression.name) &&
|
||||
e.expression.name.text === "exports"));
|
||||
const isVoid = (e: ts.Expression): boolean => {
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
return (
|
||||
e.kind === ts.SyntaxKind.VoidExpression || (ts.isIdentifier(e) && e.text === "undefined")
|
||||
);
|
||||
};
|
||||
// chains: `exports.a = exports.b = void 0;` collects every name
|
||||
const names: string[] = [cjs.name.text];
|
||||
let tail: ts.Expression = cjs.value;
|
||||
while (
|
||||
ts.isBinaryExpression(tail) &&
|
||||
tail.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
||||
isExportsMember(tail.left)
|
||||
) {
|
||||
names.push((tail.left.name as ts.Identifier).text);
|
||||
tail = tail.right;
|
||||
}
|
||||
if (isVoid(tail)) {
|
||||
const sfHere = cjs.expr.getSourceFile();
|
||||
const myPos = cjs.expr.getStart(sfHere);
|
||||
const laterRealOf = (name: string): boolean =>
|
||||
sfHere.statements.some((s) => {
|
||||
const other = cjsExportAssignmentOf(s);
|
||||
if (other?.kind !== "member" || !ts.isIdentifier(other.name)) return false;
|
||||
if (other.name.text !== name) return false;
|
||||
if (other.expr.getStart(sfHere) <= myPos) return false;
|
||||
return !isVoid(other.value);
|
||||
});
|
||||
if (names.every(laterRealOf)) return { kind: "block", body: [], loc };
|
||||
}
|
||||
}
|
||||
if (cjsVoidInitPreambleReals(cjs) !== null) return { kind: "block", body: [], loc };
|
||||
const sf = cjs.expr.getSourceFile();
|
||||
let root: IrGlobal | undefined;
|
||||
for (const stmt of sf.statements) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
canBoxFuncIntoDyn,
|
||||
DYN,
|
||||
F64,
|
||||
isUnitType,
|
||||
VOID,
|
||||
type IrExpr,
|
||||
type IrType,
|
||||
@@ -299,6 +300,19 @@ function tolerantClearNoop(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLo
|
||||
return null;
|
||||
}
|
||||
|
||||
/** A clear's handle as the f64 id it passes to the runtime. A handle that
|
||||
* may be undefined or null (`let timer: Timeout | undefined`) passes 0
|
||||
* when absent: no timer has id 0, so the clear does nothing, as Node's
|
||||
* clearTimeout(undefined) does. Other handle types are returned as they
|
||||
* are. */
|
||||
function optionalTimerHandle(lowerer: Lowerer, handle: IrExpr, loc: SrcLoc): IrExpr {
|
||||
if (handle.type.kind !== "union") return handle;
|
||||
const arms = lowerer.unions.get(handle.type.unionId)?.arms ?? [];
|
||||
const present = arms.filter((arm) => !isUnitType(arm));
|
||||
if (present.length !== 1 || present[0]!.kind !== "f64") return handle;
|
||||
return { kind: "nullish", left: handle, right: numLit(0, loc), type: F64, loc };
|
||||
}
|
||||
|
||||
const MOCKABLE_TIMER_FNS: ReadonlySet<string> = new Set([
|
||||
"timers.setTimeout",
|
||||
"timers.setTimeoutHandle",
|
||||
@@ -392,8 +406,7 @@ function lowerTimerCall(
|
||||
return { kind: "libCall", fn: "timers.setTimeout", args: [cb, ms], type: VOID, loc };
|
||||
}
|
||||
// clearTimeout(handle): shares the interval clear (the handle ids
|
||||
// share one space). A `Timeout | null` handle narrows first, like
|
||||
// clearInterval.
|
||||
// share one space). An optional handle clears nothing when absent.
|
||||
if (member === "clearTimeout") {
|
||||
const noop = tolerantClearNoop(lowerer, expr, loc);
|
||||
if (noop) return noop;
|
||||
@@ -419,11 +432,12 @@ function lowerTimerCall(
|
||||
loc,
|
||||
};
|
||||
}
|
||||
handle = optionalTimerHandle(lowerer, handle, loc);
|
||||
if (handle.type.kind !== "f64") {
|
||||
lowerer.noLowering(
|
||||
`clearTimeout of '${lowerer.fmt(handle.type)}' handles`,
|
||||
expr.arguments[0]!,
|
||||
"the handle is the Timeout setTimeout returned (narrow 'Timeout | null' first)",
|
||||
"the handle is the Timeout setTimeout returned, or undefined or null",
|
||||
);
|
||||
}
|
||||
return { kind: "libCall", fn: "timers.clearTimeout", args: [handle], type: VOID, loc };
|
||||
@@ -434,12 +448,12 @@ function lowerTimerCall(
|
||||
if (expr.arguments.length !== 1) {
|
||||
lowerer.noLowering(`clearInterval with ${expr.arguments.length} arguments`, expr);
|
||||
}
|
||||
const handle = lowerer.lowerExpr(expr.arguments[0]!);
|
||||
const handle = optionalTimerHandle(lowerer, lowerer.lowerExpr(expr.arguments[0]!), loc);
|
||||
if (handle.type.kind !== "f64") {
|
||||
lowerer.noLowering(
|
||||
`clearInterval of '${lowerer.fmt(handle.type)}' handles`,
|
||||
expr.arguments[0]!,
|
||||
"the handle is the number setInterval returned (narrow `number | null` first)",
|
||||
"the handle is the Timeout setInterval returned, or undefined or null",
|
||||
);
|
||||
}
|
||||
return { kind: "libCall", fn: "timers.clearInterval", args: [handle], type: VOID, loc };
|
||||
@@ -478,12 +492,12 @@ function lowerTimerCall(
|
||||
if (expr.arguments.length !== 1) {
|
||||
lowerer.noLowering(`clearImmediate with ${expr.arguments.length} arguments`, expr);
|
||||
}
|
||||
const handle = lowerer.lowerExpr(expr.arguments[0]!);
|
||||
const handle = optionalTimerHandle(lowerer, lowerer.lowerExpr(expr.arguments[0]!), loc);
|
||||
if (handle.type.kind !== "f64") {
|
||||
lowerer.noLowering(
|
||||
`clearImmediate of '${lowerer.fmt(handle.type)}' handles`,
|
||||
expr.arguments[0]!,
|
||||
"the handle is the Immediate setImmediate returned (narrow 'Immediate | undefined' first)",
|
||||
"the handle is the Immediate setImmediate returned, or undefined or null",
|
||||
);
|
||||
}
|
||||
return { kind: "libCall", fn: "timers.clearImmediate", args: [handle], type: VOID, loc };
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
lowerLifetimeRegionNew,
|
||||
} from "./lifetime-regions.js";
|
||||
import { internTableType, lowerInternTableCall, lowerInternTableNew } from "./intern-tables.js";
|
||||
import { lowerThreadSlotsCall, lowerThreadSlotsNew, threadSlotsType } from "./thread-slots.js";
|
||||
import { isExperimentalDeclarationFile } from "../experimental-import.js";
|
||||
import {
|
||||
checkCycleBindingAccesses,
|
||||
@@ -242,6 +243,7 @@ import {
|
||||
isOverloadedCallableMemberType,
|
||||
isUnitOnlyTsType,
|
||||
isWorkerHandleType,
|
||||
isFetchHandleType,
|
||||
jsOpenObjectType,
|
||||
mapType,
|
||||
ShapeRegistry,
|
||||
@@ -610,6 +612,11 @@ import { lowerRecordFieldCall, lowerObjectMethodCall } from "./lower-calls.js";
|
||||
import { fenceCrossBlockNsRef, nsPathPrefix } from "./lower-namespaces.js";
|
||||
import { isSafeToRepeat } from "./expressions/evaluation-safety.js";
|
||||
import { lowerTernary } from "./lower-exprs.js";
|
||||
import {
|
||||
maybePromiseReturnStmt,
|
||||
maybePromiseUnionOf,
|
||||
promiseArmedUnionOf,
|
||||
} from "./maybe-promise.js";
|
||||
|
||||
/** Entry function name. '%' cannot appear in a TS identifier, so a user
|
||||
* function can never collide with it (mangling is injective per prefix). */
|
||||
@@ -1318,17 +1325,20 @@ export function dynFallbackType(lowerer: Lowerer, node: ts.Node, t: ts.Type): Ir
|
||||
if (args.length === 2) {
|
||||
const key = lowerer.mapTypeOf(args[0]!);
|
||||
const value = lowerer.mapTypeOf(args[1]!);
|
||||
// Inferred JS function types keep the checked-dynamic slot (their
|
||||
// closures carry JS arity, not a typed closure ABI).
|
||||
return {
|
||||
kind: "map",
|
||||
key:
|
||||
key &&
|
||||
key.kind !== "func" &&
|
||||
isSupportedMapKey(
|
||||
key,
|
||||
key.kind === "union" ? lowerer.unions.get(key.unionId)?.arms : undefined,
|
||||
)
|
||||
? key
|
||||
: DYN,
|
||||
value: value && isSupportedMapValue(value) ? value : DYN,
|
||||
value: value && value.kind !== "func" && isSupportedMapValue(value) ? value : DYN,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1997,6 +2007,10 @@ export class Lowerer {
|
||||
* declared optional binding to that read's bare type, so presence tests
|
||||
* on these bindings consult the stored tag instead. */
|
||||
readonly runtimeOptionalAssignedSymbols = new Set<ts.Symbol>();
|
||||
/** Generic function declarations whose body can return an indexed read
|
||||
* that misses.
|
||||
* Each instance's result admits undefined (generic-functions.ts). */
|
||||
readonly runtimeOptionalGenericReturns = new Set<ts.Node>();
|
||||
/** Concise callback/lambda returns promoted by the HOF callback prepass;
|
||||
* arrows have no declaration symbol to key in fnSigsBySymbol. */
|
||||
readonly runtimeOptionalFunctionReturns = new Map<ts.Node, IrType>();
|
||||
@@ -2455,6 +2469,15 @@ export class Lowerer {
|
||||
/** Method adapter names are owned by class-method-values. Looking them
|
||||
* up must not scan and retain every unrelated lifted function. */
|
||||
readonly classMethodValueHelpers = new Set<string>();
|
||||
/** Reflected class methods whose lambda is lowering right now (null
|
||||
* until lowerLambda named it), with the closure it will produce: a
|
||||
* method that reads itself as a value inside its own body
|
||||
* (`o.create = K.create`) takes this closure instead of lowering the
|
||||
* method again. */
|
||||
readonly methodLambdasInProgress = new Map<
|
||||
ts.Node,
|
||||
{ fnName: string; type: IrType; selfReads: number } | null
|
||||
>();
|
||||
readonly classMethodValueSelections = new Map<
|
||||
string,
|
||||
import("./class-method-values.js").ClassMethodValueSelection
|
||||
@@ -2655,6 +2678,15 @@ export class Lowerer {
|
||||
readonly internTableHelpers = new Map<string, string>();
|
||||
/** Each InternTable name's type arguments and first construction site. */
|
||||
readonly internTableNames = new Map<string, { key: string; node: ts.Node }>();
|
||||
/** Each ThreadSlots name's type arguments (thread-slots.ts). */
|
||||
readonly threadSlotNames = new Map<string, string>();
|
||||
/** The family each `new ThreadSlots(...)` opens. */
|
||||
readonly threadSlotFamilies = new Map<
|
||||
ts.NewExpression,
|
||||
{ name: string; key: IrType; value: IrType }
|
||||
>();
|
||||
/** Classes used as ThreadSlots keys: their hierarchies carry a slot word. */
|
||||
readonly threadSlotKeys = new Set<string>();
|
||||
readonly forkTargetIdByPath = new Map<string, number>();
|
||||
/** Namespace ModuleBlocks this program lowers, filled by splitFiles:
|
||||
* "flattened" — an instantiated namespace whose body joined the file's
|
||||
@@ -3035,6 +3067,10 @@ export class Lowerer {
|
||||
isExperimentalDeclarationFile(decl.getSourceFile(), this.experimentalFiles)
|
||||
? internTableType(this)
|
||||
: null,
|
||||
threadSlotsClass: (decl) =>
|
||||
isExperimentalDeclarationFile(decl.getSourceFile(), this.experimentalFiles)
|
||||
? threadSlotsType(this)
|
||||
: null,
|
||||
moduleNamespaceId: (type) => this.moduleNamespaceIdOfType(type),
|
||||
};
|
||||
// Modules reachable only through literal import() joined
|
||||
@@ -3079,12 +3115,14 @@ export class Lowerer {
|
||||
/** Declared class fields may store a DYN value only when its static type
|
||||
* is `unknown` or `object` (also with `null` or `undefined`, or an array
|
||||
* of those), names checked protocol identity, or is an owned runtime
|
||||
* handle (a Worker or MessagePort); `any` fields stay refused. */
|
||||
* handle (a Worker or MessagePort, or a fetch Headers/Request/Response);
|
||||
* `any` fields stay refused. */
|
||||
classFieldStoresDyn(type: ts.Type): boolean {
|
||||
return (
|
||||
this.opaqueValueType(type) ||
|
||||
this.classImplementedProtocol(type) ||
|
||||
isWorkerHandleType(type, this.typeCtx)
|
||||
isWorkerHandleType(type, this.typeCtx) ||
|
||||
isFetchHandleType(type, this.typeCtx)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4552,6 +4590,79 @@ export class Lowerer {
|
||||
const read = this.mapTypeOf(this.typeOf(e));
|
||||
return !(read?.kind === "union" && this.armTag(read.unionId, UNDEFINED_T) >= 0);
|
||||
};
|
||||
/** An optional value in a generic body, where element types are type
|
||||
* parameters with no IR mapping yet: an element read of an array or a
|
||||
* read of a string index signature, judged on checker types. */
|
||||
const genericClassMethods: ts.MethodDeclaration[] = [];
|
||||
for (const sf of sourceFiles) {
|
||||
if (isJsSourceFile(sf) || sf.isDeclarationFile) continue;
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
if (!ts.isClassDeclaration(node)) return undefined;
|
||||
const genericClass = (node.typeParameters?.length ?? 0) > 0;
|
||||
for (const member of node.members)
|
||||
if (
|
||||
ts.isMethodDeclaration(member) &&
|
||||
member.body &&
|
||||
(genericClass || (member.typeParameters?.length ?? 0) > 0)
|
||||
)
|
||||
genericClassMethods.push(member);
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
const genericMayBeOptional = (node: ts.Expression): boolean => {
|
||||
if (explicitlyNonNull(node)) return false;
|
||||
if (mayBeOptional(node)) return true;
|
||||
const e = peel(node);
|
||||
if (ts.isConditionalExpression(e))
|
||||
return genericMayBeOptional(e.whenTrue) || genericMayBeOptional(e.whenFalse);
|
||||
if (ts.isBinaryExpression(e)) {
|
||||
const op = e.operatorToken.kind;
|
||||
if (op === ts.SyntaxKind.BarBarToken || op === ts.SyntaxKind.QuestionQuestionToken)
|
||||
return genericMayBeOptional(e.right);
|
||||
if (op === ts.SyntaxKind.AmpersandAmpersandToken)
|
||||
return genericMayBeOptional(e.left) || genericMayBeOptional(e.right);
|
||||
return false;
|
||||
}
|
||||
if (ts.isIdentifier(e)) {
|
||||
// A binding initialized from such a read and never reassigned.
|
||||
const symbol = symbolOf(e);
|
||||
const decl = symbol ? this.checker.valueDeclarationOf(symbol) : undefined;
|
||||
return (
|
||||
symbol !== null &&
|
||||
!reassigned.has(symbol) &&
|
||||
decl !== undefined &&
|
||||
ts.isVariableDeclaration(decl) &&
|
||||
decl.initializer !== undefined &&
|
||||
ts.isIdentifier(decl.name) &&
|
||||
!provenPresent(e, symbol) &&
|
||||
genericIndexedRead(decl.initializer)
|
||||
);
|
||||
}
|
||||
return genericIndexedRead(e);
|
||||
};
|
||||
const genericIndexedRead = (node: ts.Expression): boolean => {
|
||||
const e = peel(node);
|
||||
if (!ts.isElementAccessExpression(e) || explicitlyNonNull(node)) return false;
|
||||
const read = typeOrNull(e);
|
||||
if (!read || (read.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Any)) !== 0) return false;
|
||||
if (
|
||||
read.isUnionType() &&
|
||||
ts.constituentTypes(read).some((t) => (t.flags & ts.TypeFlags.Undefined) !== 0)
|
||||
)
|
||||
return false;
|
||||
const receiver = typeOrNull(e.expression);
|
||||
if (!receiver || this.checker.isTupleType(receiver)) return false;
|
||||
if (this.checker.isArrayType(receiver)) return true;
|
||||
const key = e.argumentExpression;
|
||||
if (
|
||||
(ts.isStringLiteral(key) || ts.isNumericLiteral(key)) &&
|
||||
this.checker.getPropertyOfType(receiver, key.text) !== undefined
|
||||
)
|
||||
return false;
|
||||
return this.checker
|
||||
.getIndexInfosOfType(receiver)
|
||||
.some((info) => (info.keyType.flags & ts.TypeFlags.StringLike) !== 0);
|
||||
};
|
||||
const isDynamicObjectEntryRead = (node: ts.Expression): boolean => {
|
||||
const read = peel(node);
|
||||
if (!ts.isElementAccessExpression(read) || !ts.isIdentifier(read.expression)) return false;
|
||||
@@ -4574,7 +4685,11 @@ export class Lowerer {
|
||||
const addUndefined = (t: IrType): IrType => {
|
||||
if (t.kind === "union")
|
||||
return this.armTag(t.unionId, UNDEFINED_T) >= 0 ? t : (this.withUndefinedArmOf(t) ?? t);
|
||||
if (t.kind === "void" || t.kind === "dyn" || t.kind === "generator") return t;
|
||||
// A Date has no undefined-armed union (it is not a union arm kind):
|
||||
// like a generator it keeps its type, and the site that would store
|
||||
// undefined into it keeps its own diagnostic.
|
||||
if (t.kind === "void" || t.kind === "dyn" || t.kind === "generator" || t.kind === "date")
|
||||
return t;
|
||||
return this.withUndefinedArm(t);
|
||||
};
|
||||
const fieldName = (node: ts.PropertyAccessExpression): string => node.name.text;
|
||||
@@ -6078,6 +6193,21 @@ export class Lowerer {
|
||||
for (const sf of sourceFiles) if (scanFile(sf)) changed = true;
|
||||
for (const [symbol, decl] of functionDeclBySymbol)
|
||||
if (scanReturns(symbol, decl)) changed = true;
|
||||
const noteGenericReturn = (symbol: ts.Symbol, decl: ts.FunctionLikeDeclaration): void => {
|
||||
if (optionalReturns.has(symbol) || isJsSourceFile(decl.getSourceFile())) return;
|
||||
if (!returnsOf(decl).some(genericMayBeOptional)) return;
|
||||
optionalReturns.add(symbol);
|
||||
this.runtimeOptionalGenericReturns.add(decl);
|
||||
provenance?.note("return", decl.name ?? decl, decl);
|
||||
changed = true;
|
||||
};
|
||||
for (const [symbol, info] of this.genericFnsBySymbol) noteGenericReturn(symbol, info.decl);
|
||||
// Generic methods and methods of a generic class collect with their
|
||||
// class, after this analysis; their result admits undefined there.
|
||||
for (const method of genericClassMethods) {
|
||||
const symbol = method.name ? symbolOf(method.name) : null;
|
||||
if (symbol) noteGenericReturn(symbol, method);
|
||||
}
|
||||
for (const fn of tsLambdas) {
|
||||
// An annotated return is the author's contract, as for parameters,
|
||||
// except that a var read before its first store is undefined
|
||||
@@ -6719,6 +6849,9 @@ export class Lowerer {
|
||||
entry: ENTRY_NAME,
|
||||
...(this.usesWorkers ? { workers: true } : {}),
|
||||
...(this.usesLifetimeRegions ? { lifetimeRegions: true as const } : {}),
|
||||
...(this.threadSlotKeys.size > 0
|
||||
? { threadSlotKeys: [...this.threadSlotKeys].sort() }
|
||||
: {}),
|
||||
...(isNodeEsmFile(this.entry, this.program) ? {} : { commonJsEntry: true }),
|
||||
...(this.ffiImports.length > 0 ? { ffiImports: [...this.ffiImports] } : {}),
|
||||
};
|
||||
@@ -6931,7 +7064,7 @@ export class Lowerer {
|
||||
}
|
||||
for (const info of this.classes.values()) {
|
||||
if (info.builtinError) continue; // runtime-provided; nothing lowers
|
||||
if (info.lifetimeRegion || info.internTable) continue; // synthesized with the class
|
||||
if (info.lifetimeRegion || info.internTable || info.threadSlots) continue; // synthesized with the class
|
||||
// Generic-class INSTANTIATIONS (and mixin instantiations) are
|
||||
// demand-driven, not units: their members lower unconditionally in
|
||||
// the instance drain below — the generic-fn instance rule.
|
||||
@@ -8016,7 +8149,13 @@ export class Lowerer {
|
||||
if (part.isUnionType()) return ts.constituentTypes(part).every(visit);
|
||||
return false;
|
||||
};
|
||||
return visit(t);
|
||||
// A union typescript-go cannot describe is not proven an array.
|
||||
try {
|
||||
return visit(t);
|
||||
} catch (e) {
|
||||
if (ts.isUnreadableConstituentsPanic(e)) return false;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/** The lowered static array/tuple value behind checkerAnyArray's
|
||||
@@ -8322,6 +8461,18 @@ export class Lowerer {
|
||||
// undefined/null literals store the dyn unit values. Types outside the
|
||||
// dyn's domain (bytes, classes, Maps, ...) fall through to
|
||||
// requireExactShape's SC1101 fence.
|
||||
// A promise entering a maybe-promise union whose promise arm has a
|
||||
// wider payload (`Promise<R>` into `Promise<R | void> | R | void`)
|
||||
// takes the payload widening below first, then wraps into that arm.
|
||||
if (expected.kind === "union" && expr.type.kind === "promise") {
|
||||
const mp = maybePromiseUnionOf(this, expected);
|
||||
if (
|
||||
mp &&
|
||||
!typeEquals(mp.promise, expr.type) &&
|
||||
this.promisePayloadWidens(expr.type, mp.promise)
|
||||
)
|
||||
expr = this.coerceToExpected(expr, mp.promise);
|
||||
}
|
||||
// Promise payloads may widen at a checked return boundary, including
|
||||
// arrays inferred as tuple results by Promise.all. Keep both payload
|
||||
// ABIs explicit: box settlement, then validate the destination payload.
|
||||
@@ -9520,6 +9671,25 @@ export class Lowerer {
|
||||
return type;
|
||||
}
|
||||
|
||||
/** Whether a promise of `from` converts to a promise of `to` through the
|
||||
* checked payload widening (coerceToExpected's boxed-settlement rule,
|
||||
* which keeps the promise's identity and settlement turn). */
|
||||
promisePayloadWidens(
|
||||
from: IrType & { kind: "promise" },
|
||||
to: IrType & { kind: "promise" },
|
||||
): boolean {
|
||||
if (typeEquals(from, to)) return true;
|
||||
if (to.inner.kind === "void") return from.inner.kind !== "void";
|
||||
return (
|
||||
this.dynConvertible(from) &&
|
||||
canDynCheckTo(
|
||||
to,
|
||||
(id) => this.shapes.get(id),
|
||||
(id) => this.unions.get(id),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/** True when a static type converts to a dyn value (the dynFrom
|
||||
* walker's domain): JSON-safe, bytes<u8> (Uint8Array/Buffer — the checked-dynamic tree's
|
||||
* bytes kind, payload copied; stdin chunks into unknown-typed helpers),
|
||||
@@ -9559,16 +9729,26 @@ export class Lowerer {
|
||||
// storage directly, just like arguments and variable initializers.
|
||||
// Contextual types (such as Proxy descriptors' symbol-valued fields)
|
||||
// must not force an intermediate record with incompatible field slots.
|
||||
if (this.returnsLiteralIntoDyn(node, expected)) return this.lowerExprExpecting(node, expected);
|
||||
return this.asyncReturnValueOf(node, this.lowerExpr(node), expected);
|
||||
}
|
||||
|
||||
/** A fresh JS object literal returned into a checked-native (dyn) result,
|
||||
* which lowers straight into that storage. */
|
||||
private returnsLiteralIntoDyn(node: ts.Expression, expected: IrType): boolean {
|
||||
let literal = node;
|
||||
while (ts.isParenthesizedExpression(literal)) literal = literal.expression;
|
||||
if (
|
||||
return (
|
||||
expected.kind === "dyn" &&
|
||||
isJsSourceFile(node.getSourceFile()) &&
|
||||
ts.isObjectLiteralExpression(literal)
|
||||
) {
|
||||
return this.lowerExprExpecting(node, expected);
|
||||
}
|
||||
const e = this.lowerExpr(node);
|
||||
);
|
||||
}
|
||||
|
||||
/** The value of `return <e>` (already lowered untyped) in an async or
|
||||
* void context: a promise flattens through the returned-promise await,
|
||||
* everything else converts into the declared result. */
|
||||
private asyncReturnValueOf(node: ts.Expression, e: IrExpr, expected: IrType): IrExpr | null {
|
||||
if (expected.kind === "void" && e.kind === "unitLit") return null;
|
||||
if (this.ctx.isAsync && e.type.kind === "promise" && expected.kind !== "promise") {
|
||||
const awaited = this.returnedPromiseAwait(e);
|
||||
@@ -9634,10 +9814,32 @@ export class Lowerer {
|
||||
return s;
|
||||
}
|
||||
|
||||
/** `return u` of a promise-armed union (maybe-promise, or promise-or-
|
||||
* unit) in an async function: adopt the promise arm, fulfill with the
|
||||
* others (maybe-promise.ts). Null when the return is not that shape. */
|
||||
maybePromiseReturn(node: ts.Expression, e: IrExpr, expected: IrType): IrStmt | null {
|
||||
if (!this.adoptsReturnedPromise()) return null;
|
||||
const mp = promiseArmedUnionOf(this, e.type);
|
||||
if (!mp || promiseArmedUnionOf(this, expected)) return null;
|
||||
const stmt = maybePromiseReturnStmt(this, e, mp, expected, (x) =>
|
||||
this.coerceInto(node, x, expected),
|
||||
);
|
||||
if (stmt === null) {
|
||||
this.unsupported(
|
||||
"SC1090",
|
||||
node,
|
||||
`returning '${this.fmt(e.type)}' from an async function whose result is '${this.fmt(expected)}' (an arm does not convert to the result)`,
|
||||
);
|
||||
}
|
||||
return stmt;
|
||||
}
|
||||
|
||||
private lowerReturnOf(node: ts.Expression, loc: SrcLoc): IrStmt {
|
||||
const expected = this.ctx.returnType;
|
||||
if (expected.kind === "void") {
|
||||
let e = this.lowerExpr(node);
|
||||
const flattened = this.maybePromiseReturn(node, e, expected);
|
||||
if (flattened) return flattened;
|
||||
if (this.ctx.isAsync && e.type.kind === "promise") {
|
||||
if (this.adoptsReturnedPromise()) return this.adoptingReturn(e, VOID, (x) => x);
|
||||
e = this.returnedPromiseAwait(e);
|
||||
@@ -9653,6 +9855,15 @@ export class Lowerer {
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (this.adoptsReturnedPromise() && !this.returnsLiteralIntoDyn(node, expected)) {
|
||||
// The async return lowers its operand untyped first (a promise
|
||||
// adopts before its payload converts); a maybe-promise union needs
|
||||
// the statement form, which branches between adoption and fulfillment.
|
||||
const e = this.lowerExpr(node);
|
||||
const flattened = this.maybePromiseReturn(node, e, expected);
|
||||
if (flattened) return flattened;
|
||||
return { kind: "return", value: this.asyncReturnValueOf(node, e, expected), loc };
|
||||
}
|
||||
return { kind: "return", value: this.lowerReturnValue(node), loc };
|
||||
}
|
||||
|
||||
@@ -11438,6 +11649,7 @@ export class Lowerer {
|
||||
return (
|
||||
lowerLifetimeRegionCall(this, expr) ??
|
||||
lowerInternTableCall(this, expr) ??
|
||||
lowerThreadSlotsCall(this, expr) ??
|
||||
lowerFfiCall(this, expr) ??
|
||||
lowerFetchCall(this, expr) ??
|
||||
lowerStaticFetchCompanionCall(this, expr) ??
|
||||
@@ -11664,6 +11876,7 @@ export class Lowerer {
|
||||
const builtin =
|
||||
lowerLifetimeRegionNew(this, expr) ??
|
||||
lowerInternTableNew(this, expr) ??
|
||||
lowerThreadSlotsNew(this, expr) ??
|
||||
lowerAbortControllerNew(this, expr) ??
|
||||
lowerDisposableStackNew(this, expr) ??
|
||||
lowerEventTargetNew(this, expr) ??
|
||||
@@ -12029,6 +12242,15 @@ export class Lowerer {
|
||||
if (bi.module === "http" && bi.member === "METHODS") {
|
||||
return { kind: "libCall", fn: "http.methods", args: [], type: arrayOf(STRING), loc };
|
||||
}
|
||||
if ((bi.module === "http" || bi.module === "https") && bi.member === "globalAgent") {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "http.globalAgent",
|
||||
args: [{ kind: "boolLit", value: bi.module === "https", type: BOOL, loc }],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (bi.module === "http" && bi.member === "STATUS_CODES") {
|
||||
return { kind: "libCall", fn: "http.statusCodes", args: [], type: DYN, loc };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
import { varRef } from "../../ir/build.js";
|
||||
import { BOOL, isUnitType, typeEquals, VOID } from "../../ir/ir.js";
|
||||
import type { IrExpr, IrStmt, IrType, IrUnionDef, SrcLoc } from "../../ir/ir.js";
|
||||
import type * as ts from "../ts7/adapter.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
|
||||
/** A MAYBE-PROMISE union (`T | Promise<T>`, `Promise<R | void> | R |
|
||||
* void`): one promise arm beside at least one data arm. The type mapper
|
||||
* mints these only when no data arm is thenable, so the union tag decides
|
||||
* exactly what JS's await decides: the promise arm settles, every other
|
||||
* arm is a non-thenable that answers itself. Promise-or-unit unions keep
|
||||
* their own lowerings (awaitUnionExpr). */
|
||||
export interface MaybePromiseUnion {
|
||||
def: IrUnionDef;
|
||||
promiseTag: number;
|
||||
promise: IrType & { kind: "promise" };
|
||||
}
|
||||
|
||||
export function maybePromiseUnionOf(lowerer: Lowerer, t: IrType): MaybePromiseUnion | null {
|
||||
const armed = promiseArmedUnionOf(lowerer, t);
|
||||
if (!armed || armed.def.arms.every((a, i) => i === armed.promiseTag || isUnitType(a)))
|
||||
return null;
|
||||
return armed;
|
||||
}
|
||||
|
||||
/** Any union with exactly one promise arm: a maybe-promise union or a
|
||||
* promise-or-unit union (`Promise<void> | void`). An async `return`
|
||||
* resolves both the same way: adopt the promise arm, fulfill otherwise. */
|
||||
export function promiseArmedUnionOf(lowerer: Lowerer, t: IrType): MaybePromiseUnion | null {
|
||||
if (t.kind !== "union") return null;
|
||||
const def = lowerer.unions.get(t.unionId);
|
||||
if (!def) return null;
|
||||
const tags = def.arms.flatMap((a, i) => (a.kind === "promise" ? [i] : []));
|
||||
if (tags.length !== 1) return null;
|
||||
const promiseTag = tags[0]!;
|
||||
const promise = def.arms[promiseTag]!;
|
||||
if (promise.kind !== "promise") return null;
|
||||
return { def, promiseTag, promise };
|
||||
}
|
||||
|
||||
function unionTagTest(mp: MaybePromiseUnion, value: IrExpr, tag: number, loc: SrcLoc): IrExpr {
|
||||
return {
|
||||
kind: "unionIsTag",
|
||||
unionId: mp.def.id,
|
||||
tag,
|
||||
negated: false,
|
||||
value,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** The value of `ref` (a repeatable read of the maybe-promise union) as
|
||||
* `target`, given that it does not hold the promise arm: each remaining
|
||||
* arm extracts and converts. Null when some arm cannot convert. */
|
||||
function nonPromiseArmsAs(
|
||||
lowerer: Lowerer,
|
||||
mp: MaybePromiseUnion,
|
||||
ref: IrExpr,
|
||||
target: IrType,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
const pieces: { tag: number; value: IrExpr }[] = [];
|
||||
for (let tag = 0; tag < mp.def.arms.length; tag++) {
|
||||
if (tag === mp.promiseTag) continue;
|
||||
const arm = mp.def.arms[tag]!;
|
||||
const source: IrExpr =
|
||||
arm.kind === "undefinedT" || arm.kind === "nullT"
|
||||
? {
|
||||
kind: "unitLit",
|
||||
unit: arm.kind === "undefinedT" ? "undefined" : "null",
|
||||
type: arm,
|
||||
loc,
|
||||
}
|
||||
: { kind: "unionNarrow", unionId: mp.def.id, tag, value: ref, type: arm, loc };
|
||||
const value = lowerer.coerceToExpected(source, target);
|
||||
if (!typeEquals(value.type, target)) return null;
|
||||
pieces.push({ tag, value });
|
||||
}
|
||||
let result: IrExpr | null = null;
|
||||
for (let i = pieces.length - 1; i >= 0; i--) {
|
||||
const piece = pieces[i]!;
|
||||
result =
|
||||
result === null
|
||||
? piece.value
|
||||
: {
|
||||
kind: "ternary",
|
||||
cond: unionTagTest(mp, ref, piece.tag, loc),
|
||||
then: piece.value,
|
||||
else_: result,
|
||||
type: target,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function hopStmt(loc: SrcLoc): IrStmt {
|
||||
return {
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "libCall", fn: "async.hop", args: [], type: VOID, loc },
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `await u` over a maybe-promise union. JS's await resolves the operand:
|
||||
* a native promise is awaited directly (resumption one turn after it
|
||||
* settles; a rejection throws here), and any other non-thenable value
|
||||
* resumes after exactly one microtask turn with the value itself. The
|
||||
* result type is the checker's type of the await expression (`Awaited`
|
||||
* of the union), which holds the promise's settled type and every data
|
||||
* arm. */
|
||||
export function lowerMaybePromiseAwait(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.AwaitExpression,
|
||||
value: IrExpr,
|
||||
mp: MaybePromiseUnion,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const resultT = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
||||
if (!resultT || resultT.kind === "void" || resultT.kind === "dyn" || isUnitType(resultT)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
`awaiting '${lowerer.fmt(value.type)}' (the awaited result type has no compiled representation)`,
|
||||
);
|
||||
}
|
||||
const local = lowerer.declareHiddenLocal("%maybePromise", value.type);
|
||||
const ref = varRef(local.id, value.type, loc);
|
||||
const inner = mp.promise.inner;
|
||||
const awaited: IrExpr = {
|
||||
kind: "awaitExpr",
|
||||
value: {
|
||||
kind: "unionNarrow",
|
||||
unionId: mp.def.id,
|
||||
tag: mp.promiseTag,
|
||||
value: ref,
|
||||
type: mp.promise,
|
||||
loc,
|
||||
},
|
||||
type: inner,
|
||||
loc,
|
||||
};
|
||||
let settled: IrExpr | null;
|
||||
if (inner.kind === "void") {
|
||||
const undef = lowerer.wrappedUndefined(resultT, loc);
|
||||
settled =
|
||||
undef === null
|
||||
? null
|
||||
: {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: awaited, loc }],
|
||||
result: undef,
|
||||
type: resultT,
|
||||
loc,
|
||||
};
|
||||
} else {
|
||||
settled = lowerer.coerceToExpected(awaited, resultT);
|
||||
if (!typeEquals(settled.type, resultT)) settled = null;
|
||||
}
|
||||
const rest = nonPromiseArmsAs(lowerer, mp, ref, resultT, loc);
|
||||
if (settled === null || rest === null) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
`awaiting '${lowerer.fmt(value.type)}' (its arms do not all convert to the awaited result '${lowerer.fmt(resultT)}')`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "varDecl", localId: local.id, init: value, loc }],
|
||||
result: {
|
||||
kind: "ternary",
|
||||
cond: unionTagTest(mp, ref, mp.promiseTag, loc),
|
||||
then: settled,
|
||||
else_: { kind: "seqExpr", stmts: [hopStmt(loc)], result: rest, type: resultT, loc },
|
||||
type: resultT,
|
||||
loc,
|
||||
},
|
||||
type: resultT,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `return u` of a promise-armed union from an async function (or a then
|
||||
* handler's lifted body) whose settled type is `R`: the promise arm is
|
||||
* adopted exactly like `return p` (the spec's thenable resolution, with
|
||||
* its then-job turn), and the other arms fulfill directly. Null when an
|
||||
* arm cannot convert to R. */
|
||||
export function maybePromiseReturnStmt(
|
||||
lowerer: Lowerer,
|
||||
value: IrExpr,
|
||||
mp: MaybePromiseUnion,
|
||||
R: IrType,
|
||||
coerce: (awaited: IrExpr) => IrExpr,
|
||||
): IrStmt | null {
|
||||
const loc = value.loc;
|
||||
const local = lowerer.declareHiddenLocal("%maybePromise", value.type);
|
||||
const ref = varRef(local.id, value.type, loc);
|
||||
const promise: IrExpr = {
|
||||
kind: "unionNarrow",
|
||||
unionId: mp.def.id,
|
||||
tag: mp.promiseTag,
|
||||
value: ref,
|
||||
type: mp.promise,
|
||||
loc,
|
||||
};
|
||||
let otherwise: IrStmt[];
|
||||
if (R.kind === "void") {
|
||||
otherwise = [{ kind: "return", value: null, loc }];
|
||||
} else {
|
||||
const rest = nonPromiseArmsAs(lowerer, mp, ref, R, loc);
|
||||
if (rest === null) return null;
|
||||
otherwise = [{ kind: "return", value: rest, loc }];
|
||||
}
|
||||
return {
|
||||
kind: "block",
|
||||
body: [
|
||||
{ kind: "varDecl", localId: local.id, init: value, loc },
|
||||
{
|
||||
kind: "if",
|
||||
cond: unionTagTest(mp, ref, mp.promiseTag, loc),
|
||||
then: [lowerer.adoptingReturn(promise, R, coerce)],
|
||||
else_: otherwise,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `Promise.resolve(u)` over a maybe-promise union: a promise argument is
|
||||
* returned as is (every scriptc promise is native, so the spec's
|
||||
* constructor check answers identity), any other value fulfills a fresh
|
||||
* promise. Null when the promise arm is not already the result type (no
|
||||
* identity-preserving payload adapter exists) or a data arm cannot convert. */
|
||||
export function maybePromiseResolve(
|
||||
lowerer: Lowerer,
|
||||
value: IrExpr,
|
||||
mp: MaybePromiseUnion,
|
||||
resultT: IrType & { kind: "promise" },
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
const local = lowerer.declareHiddenLocal("%maybePromise", value.type);
|
||||
const ref = varRef(local.id, value.type, loc);
|
||||
let promise: IrExpr = {
|
||||
kind: "unionNarrow",
|
||||
unionId: mp.def.id,
|
||||
tag: mp.promiseTag,
|
||||
value: ref,
|
||||
type: mp.promise,
|
||||
loc,
|
||||
};
|
||||
if (!typeEquals(mp.promise, resultT)) {
|
||||
if (resultT.inner.kind !== "void") return null;
|
||||
promise = { kind: "promiseVoidWiden", value: promise, type: resultT, loc };
|
||||
}
|
||||
let fresh: IrExpr;
|
||||
if (resultT.inner.kind === "void") {
|
||||
fresh = { kind: "intrinsic", name: "promise.resolve", args: [], type: resultT, loc };
|
||||
} else {
|
||||
const rest = nonPromiseArmsAs(lowerer, mp, ref, resultT.inner, loc);
|
||||
if (rest === null) return null;
|
||||
fresh = { kind: "intrinsic", name: "promise.resolve", args: [rest], type: resultT, loc };
|
||||
}
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "varDecl", localId: local.id, init: value, loc }],
|
||||
result: {
|
||||
kind: "ternary",
|
||||
cond: unionTagTest(mp, ref, mp.promiseTag, loc),
|
||||
then: promise,
|
||||
else_: fresh,
|
||||
type: resultT,
|
||||
loc,
|
||||
},
|
||||
type: resultT,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `u instanceof Promise` over a maybe-promise (or promise-or-unit) union:
|
||||
* the union tag test. */
|
||||
export function maybePromiseInstanceOf(
|
||||
lowerer: Lowerer,
|
||||
value: IrExpr,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
if (value.type.kind !== "union") return null;
|
||||
const def = lowerer.unions.get(value.type.unionId);
|
||||
const tag = def ? def.arms.findIndex((a) => a.kind === "promise") : -1;
|
||||
if (!def || tag < 0) return null;
|
||||
return { kind: "unionIsTag", unionId: def.id, tag, negated: false, value, type: BOOL, loc };
|
||||
}
|
||||
@@ -276,8 +276,8 @@ export const HTTP_SERVER_DOCUMENTED_OPTIONS: ReadonlySet<string> = new Set([
|
||||
/** new http.Agent(options)'s documented keys (Node v24 — the Agent
|
||||
* constructor table plus the socket.connect() options it forwards into
|
||||
* each dial). The lowered set is keepAlive/keepAliveMsecs/maxSockets/
|
||||
* maxFreeSockets/timeout/port (+ scheduling, dropped: no free pool
|
||||
* exists); the rest fence by name and unknown keys drop like Node. */
|
||||
* maxFreeSockets/timeout/port/scheduling; the rest fence by name and
|
||||
* unknown keys drop like Node. */
|
||||
export const AGENT_DOCUMENTED_OPTIONS: ReadonlySet<string> = new Set([
|
||||
"family",
|
||||
"hints",
|
||||
@@ -2224,6 +2224,13 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [
|
||||
fns: ["date.toISOString", "date.toISOStringValue"],
|
||||
note: "UTC ISO formatting over constructed and stored Date values",
|
||||
},
|
||||
{
|
||||
id: "stdlib.date.toUTCString",
|
||||
kind: "stdlib",
|
||||
name: "Date.prototype.toUTCString",
|
||||
fns: ["date.toUTCString"],
|
||||
note: "RFC 7231 IMF-fixdate formatting over stored Date values",
|
||||
},
|
||||
{
|
||||
id: "stdlib.date.getFullYear",
|
||||
kind: "stdlib",
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
/* ThreadSlots (@scriptc/experimental): named families of per-thread values
|
||||
* keyed by class instances (scr_slots.c, backend/llvm/thread-slots.ts).
|
||||
*
|
||||
* A ThreadSlots value is an instance of the synthetic class `%ThreadSlots`
|
||||
* with no fields, so it has the identity, typeof and inspection of the
|
||||
* Node.js class (whose state is private). A family's per-thread storage is
|
||||
* found by its name at compile time, so its methods are called through a
|
||||
* module-level const initialized with `new ThreadSlots<K, V>("name")`, and
|
||||
* lower at their call sites to slots.get, slots.has, slots.set and
|
||||
* slots.delete, with the family name, the const's value and the key:
|
||||
* - the name must be a string literal, and every construction of one name
|
||||
* must use the same type arguments;
|
||||
* - the key type must be a class (its hierarchy carries the slot word);
|
||||
* - the value type must be a class, an object type, an array, a Map or a
|
||||
* Set, so `V | undefined` is a nullable reference.
|
||||
* Any other use of the class or its members as values is refused. */
|
||||
import { STRING, VOID, typeKey } from "../../ir/ir.js";
|
||||
import type { IrExpr, IrType, SrcLoc } from "../../ir/ir.js";
|
||||
import {
|
||||
type ThreadSlotsMember,
|
||||
experimentalImportOf,
|
||||
threadSlotsMemberOf,
|
||||
} from "../experimental-import.js";
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import type { ClassInfo } from "./lower-classes.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { locOf } from "../program.js";
|
||||
|
||||
export const THREAD_SLOTS_CLASS = "%ThreadSlots";
|
||||
|
||||
const BUILTIN_LOC: SrcLoc = { file: "<builtin>", start: 0, end: 0 };
|
||||
|
||||
/** Value types a family stores: references that `V | undefined` holds as a
|
||||
* nullable pointer. */
|
||||
const VALUE_KINDS = new Set<IrType["kind"]>(["object", "record", "array", "map", "set"]);
|
||||
|
||||
/** The ThreadSlots instance type, registering the class (and its
|
||||
* constructor) on first use. */
|
||||
export function threadSlotsType(lowerer: Lowerer): IrType {
|
||||
const type: IrType = { kind: "object", className: THREAD_SLOTS_CLASS };
|
||||
if (lowerer.classes.has(THREAD_SLOTS_CLASS)) return type;
|
||||
const info: ClassInfo = {
|
||||
def: { name: THREAD_SLOTS_CLASS, fields: [], loc: BUILTIN_LOC },
|
||||
fields: new Map(),
|
||||
fieldOrder: [],
|
||||
methods: new Map(),
|
||||
decl: null,
|
||||
threadSlots: true,
|
||||
ctor: null,
|
||||
ctorParams: [],
|
||||
base: null,
|
||||
subclasses: [],
|
||||
throwingSetters: [],
|
||||
staticFields: [],
|
||||
};
|
||||
lowerer.classes.set(THREAD_SLOTS_CLASS, info);
|
||||
lowerer.liftedFns.push({
|
||||
name: `%${THREAD_SLOTS_CLASS}.constructor`,
|
||||
params: [{ localId: "this.0", name: "this", type }],
|
||||
returnType: VOID,
|
||||
locals: [{ id: "this.0", name: "this", type, mutable: false }],
|
||||
body: [],
|
||||
loc: BUILTIN_LOC,
|
||||
});
|
||||
return type;
|
||||
}
|
||||
|
||||
interface Family {
|
||||
name: string;
|
||||
key: IrType;
|
||||
value: IrType;
|
||||
}
|
||||
|
||||
/** The root of a class's hierarchy. */
|
||||
function rootOf(info: ClassInfo): ClassInfo {
|
||||
let root = info;
|
||||
while (root.base !== null) root = root.base;
|
||||
return root;
|
||||
}
|
||||
|
||||
/** The family a `new ThreadSlots<K, V>("name")` expression opens,
|
||||
* validated and registered once per expression. */
|
||||
function familyOf(lowerer: Lowerer, expr: ts.NewExpression): Family {
|
||||
const known = lowerer.threadSlotFamilies.get(expr);
|
||||
if (known !== undefined) return known;
|
||||
const args = expr.arguments ?? [];
|
||||
const arg = args[0];
|
||||
if (args.length !== 1 || arg === undefined || ts.isSpreadElement(arg))
|
||||
lowerer.noLowering(`new ThreadSlots with ${args.length} arguments`, expr);
|
||||
if (!ts.isStringLiteral(arg!) && !ts.isNoSubstitutionTemplateLiteral(arg!))
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
arg!,
|
||||
"ThreadSlots names other than string literals",
|
||||
"name the slots with a string literal: every construction of the name opens the same slots",
|
||||
);
|
||||
const name = (arg as ts.StringLiteral).text;
|
||||
const instance = lowerer.typeOf(expr);
|
||||
const typeArgs = lowerer.checker.getTypeArguments(instance as ts.TypeReference);
|
||||
if (typeArgs.length !== 2) lowerer.noLowering("ThreadSlots without type arguments", expr);
|
||||
const key = lowerer.mapTypeOf(typeArgs[0]!);
|
||||
const keyInfo = key?.kind === "object" ? lowerer.classes.get(key.className) : undefined;
|
||||
if (
|
||||
key?.kind !== "object" ||
|
||||
keyInfo === undefined ||
|
||||
keyInfo.decl === null ||
|
||||
rootOf(keyInfo).def.runtime === true
|
||||
)
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
`'${lowerer.checker.typeToString(typeArgs[0]!)}' as a ThreadSlots key type`,
|
||||
"key the slots by instances of a class declared in the program",
|
||||
);
|
||||
const value = lowerer.mapTypeOf(typeArgs[1]!);
|
||||
if (value === null || !VALUE_KINDS.has(value.kind))
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
`'${lowerer.checker.typeToString(typeArgs[1]!)}' as a ThreadSlots value type`,
|
||||
"store class instances, objects, arrays, Maps or Sets",
|
||||
);
|
||||
const signature = `${typeKey(key!)}|${typeKey(value!)}`;
|
||||
const first = lowerer.threadSlotNames.get(name);
|
||||
if (first === undefined) lowerer.threadSlotNames.set(name, signature);
|
||||
else if (first !== signature)
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
`opening ThreadSlots ${JSON.stringify(name)} with different type arguments than its first construction`,
|
||||
"every construction of one slots name must use the same key and value types",
|
||||
);
|
||||
lowerer.threadSlotKeys.add((key as IrType & { kind: "object" }).className);
|
||||
const family = { name, key: key!, value: value! };
|
||||
lowerer.threadSlotFamilies.set(expr, family);
|
||||
return family;
|
||||
}
|
||||
|
||||
/** `new ThreadSlots<K, V>("name")`. */
|
||||
export function lowerThreadSlotsNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null {
|
||||
if (!ts.isIdentifier(expr.expression)) return null;
|
||||
if (experimentalImportOf(lowerer.program, expr.expression) !== "ThreadSlots") return null;
|
||||
familyOf(lowerer, expr);
|
||||
return {
|
||||
kind: "new",
|
||||
className: THREAD_SLOTS_CLASS,
|
||||
args: [],
|
||||
type: threadSlotsType(lowerer),
|
||||
loc: locOf(expr),
|
||||
};
|
||||
}
|
||||
|
||||
/** The `new ThreadSlots(...)` initializer of the module-level const that
|
||||
* `receiver` names (through imports and namespace imports), or of the
|
||||
* slots whose `set` call
|
||||
* `receiver` is (`slots.set(a, x).set(b, y)`), or null. */
|
||||
function declaringNew(lowerer: Lowerer, receiver: ts.Expression): ts.NewExpression | null {
|
||||
if (
|
||||
ts.isCallExpression(receiver) &&
|
||||
ts.isPropertyAccessExpression(receiver.expression) &&
|
||||
threadSlotsMemberOf(lowerer.program, receiver.expression, lowerer.experimentalFiles) === "set"
|
||||
)
|
||||
return declaringNew(lowerer, receiver.expression.expression);
|
||||
// `slots`, or `ns.slots` through a namespace import.
|
||||
const ref = ts.isPropertyAccessExpression(receiver) ? receiver.name : receiver;
|
||||
if (!ts.isIdentifier(ref)) return null;
|
||||
const checker = lowerer.checker;
|
||||
let symbol = checker.getSymbolAtLocation(ref);
|
||||
if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0)
|
||||
symbol = checker.getAliasedSymbol(symbol);
|
||||
const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined;
|
||||
if (decl === undefined || !ts.isVariableDeclaration(decl)) return null;
|
||||
const list = decl.parent;
|
||||
if (
|
||||
!ts.isVariableDeclarationList(list) ||
|
||||
(list.flags & ts.NodeFlags.Const) === 0 ||
|
||||
!ts.isVariableStatement(list.parent) ||
|
||||
!ts.isSourceFile(list.parent.parent)
|
||||
)
|
||||
return null;
|
||||
const init = decl.initializer;
|
||||
if (init === undefined || !ts.isNewExpression(init) || !ts.isIdentifier(init.expression))
|
||||
return null;
|
||||
return experimentalImportOf(lowerer.program, init.expression) === "ThreadSlots" ? init : null;
|
||||
}
|
||||
|
||||
/** The refusal of ThreadSlots members used as values. */
|
||||
export function lowerThreadSlotsProperty(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.PropertyAccessExpression,
|
||||
): IrExpr | null {
|
||||
const member = threadSlotsMemberOf(lowerer.program, expr, lowerer.experimentalFiles);
|
||||
if (member === null) return null;
|
||||
return lowerer.noLowering(
|
||||
`ThreadSlots.${member} (@scriptc/experimental) as a value`,
|
||||
expr,
|
||||
`call slots.${member}(...) directly`,
|
||||
);
|
||||
}
|
||||
|
||||
/** `slots.get(k)`, `has`, `set` and `delete`. */
|
||||
export function lowerThreadSlotsCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr | null {
|
||||
const callee = expr.expression;
|
||||
if (!ts.isPropertyAccessExpression(callee)) return null;
|
||||
const member: ThreadSlotsMember | null = threadSlotsMemberOf(
|
||||
lowerer.program,
|
||||
callee,
|
||||
lowerer.experimentalFiles,
|
||||
);
|
||||
if (member === null) return null;
|
||||
if (callee.questionDotToken !== undefined || expr.questionDotToken !== undefined)
|
||||
lowerer.noLowering(`ThreadSlots.${member} through ?.`, expr, `call slots.${member}() directly`);
|
||||
const arity = member === "set" ? 2 : 1;
|
||||
if (expr.arguments.length !== arity || expr.arguments.some((a) => ts.isSpreadElement(a)))
|
||||
lowerer.noLowering(`ThreadSlots.${member} with ${expr.arguments.length} arguments`, expr);
|
||||
const init = declaringNew(lowerer, callee.expression);
|
||||
if (init === null)
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
callee.expression,
|
||||
"ThreadSlots calls through a value other than a module-level const",
|
||||
'call the methods through `const slots = new ThreadSlots<K, V>("name")` declared at module scope',
|
||||
);
|
||||
const family = familyOf(lowerer, init!);
|
||||
const loc = locOf(expr);
|
||||
const slotsType = threadSlotsType(lowerer);
|
||||
const receiver = lowerer.coerceInto(
|
||||
callee.expression,
|
||||
lowerer.lowerExpr(callee.expression),
|
||||
slotsType,
|
||||
);
|
||||
const keyNode = expr.arguments[0]!;
|
||||
const key = lowerer.coerceInto(keyNode, lowerer.lowerExpr(keyNode), family.key);
|
||||
const name: IrExpr = { kind: "strLit", value: family.name, type: STRING, loc };
|
||||
const args = [name, receiver, key];
|
||||
switch (member) {
|
||||
case "get": {
|
||||
const resultTs = lowerer.typeOf(expr);
|
||||
const result = lowerer.mapTypeOf(resultTs);
|
||||
if (result === null) lowerer.badType(expr, resultTs);
|
||||
return { kind: "intrinsic", name: "slots.get", args, type: result!, loc };
|
||||
}
|
||||
case "has":
|
||||
case "delete":
|
||||
return { kind: "intrinsic", name: `slots.${member}`, args, type: { kind: "bool" }, loc };
|
||||
case "set": {
|
||||
const valueNode = expr.arguments[1]!;
|
||||
const value = lowerer.coerceInto(valueNode, lowerer.lowerExpr(valueNode), family.value);
|
||||
// `set` returns the slots; a statement discards them.
|
||||
const type = ts.isExpressionStatement(expr.parent) ? VOID : slotsType;
|
||||
return { kind: "intrinsic", name: "slots.set", args: [...args, value], type, loc };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5980,6 +5980,67 @@ export function cjsExportAssignmentOf(
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The tsc VOID-INIT PREAMBLE (`exports.a = exports.b = void 0;` with the
|
||||
* real `exports.a = a;` further down; every tsc-emitted dist file opens its
|
||||
* exports this way): when every name in the chain has a later real
|
||||
* assignment, the export takes its value there and the preamble lowers to
|
||||
* nothing. Returns those later assignments by name, or null when this is
|
||||
* not such a preamble (a preamble with no later real assignment keeps the
|
||||
* ordinary path: the export IS undefined then). */
|
||||
export function cjsVoidInitPreambleReals(
|
||||
cjs: Extract<ReturnType<typeof cjsExportAssignmentOf>, { kind: "member" }>,
|
||||
): Map<string, Extract<ReturnType<typeof cjsExportAssignmentOf>, { kind: "member" }>> | null {
|
||||
if (!ts.isIdentifier(cjs.name)) return null;
|
||||
const isExportsMember = (e: ts.Expression): e is ts.PropertyAccessExpression =>
|
||||
ts.isPropertyAccessExpression(e) &&
|
||||
ts.isIdentifier(e.name) &&
|
||||
((ts.isIdentifier(e.expression) && e.expression.text === "exports") ||
|
||||
(ts.isPropertyAccessExpression(e.expression) &&
|
||||
ts.isIdentifier(e.expression.expression) &&
|
||||
e.expression.expression.text === "module" &&
|
||||
ts.isIdentifier(e.expression.name) &&
|
||||
e.expression.name.text === "exports"));
|
||||
const isVoid = (e: ts.Expression): boolean => {
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
return (
|
||||
e.kind === ts.SyntaxKind.VoidExpression || (ts.isIdentifier(e) && e.text === "undefined")
|
||||
);
|
||||
};
|
||||
// chains: `exports.a = exports.b = void 0;` collects every name
|
||||
const names: string[] = [cjs.name.text];
|
||||
let tail: ts.Expression = cjs.value;
|
||||
while (
|
||||
ts.isBinaryExpression(tail) &&
|
||||
tail.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
||||
isExportsMember(tail.left)
|
||||
) {
|
||||
names.push((tail.left.name as ts.Identifier).text);
|
||||
tail = tail.right;
|
||||
}
|
||||
if (!isVoid(tail)) return null;
|
||||
const sf = cjs.expr.getSourceFile();
|
||||
const myPos = cjs.expr.getStart(sf);
|
||||
const reals = new Map<
|
||||
string,
|
||||
Extract<ReturnType<typeof cjsExportAssignmentOf>, { kind: "member" }>
|
||||
>();
|
||||
for (const name of names) {
|
||||
const real = sf.statements
|
||||
.map((s) => cjsExportAssignmentOf(s))
|
||||
.find(
|
||||
(other) =>
|
||||
other?.kind === "member" &&
|
||||
ts.isIdentifier(other.name) &&
|
||||
other.name.text === name &&
|
||||
other.expr.getStart(sf) > myPos &&
|
||||
!isVoid(other.value),
|
||||
);
|
||||
if (real?.kind !== "member") return null;
|
||||
reals.set(name, real);
|
||||
}
|
||||
return reals;
|
||||
}
|
||||
|
||||
/** The members and shared value of a CHAINED member export
|
||||
* `exports.a = exports.b = module.exports.c = value` (outermost name
|
||||
* first), or null when the assignment is not a chain of at least two
|
||||
|
||||
@@ -120,6 +120,16 @@ export function constituentTypes(type: Type): readonly Type[] {
|
||||
return type.getTypes() ?? [];
|
||||
}
|
||||
|
||||
/** True for the panic typescript-go's API raises serializing a union's
|
||||
* constituents (an empty tuple cloned as an array literal keeps the tuple
|
||||
* flag without tuple data: `s.match(re) || []`). The union itself is
|
||||
* readable; its members are not. */
|
||||
export function isUnreadableConstituentsPanic(e: unknown): boolean {
|
||||
return (
|
||||
e instanceof Error && (e as { unreadableConstituents?: true }).unreadableConstituents === true
|
||||
);
|
||||
}
|
||||
|
||||
/** Function-like declarations whose body is deferred until reachability
|
||||
* asks for it. Header prefetch walks their names, type parameters, params,
|
||||
* and return types but leaves the body for a later explicit body wave. */
|
||||
|
||||
@@ -408,6 +408,7 @@ export class SemanticType {
|
||||
private trueType: number | false = false;
|
||||
private falseType: number | false = false;
|
||||
private constituents: SemanticType[] | undefined;
|
||||
private constituentsPanic: Error | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly data: TypeResponse,
|
||||
@@ -497,12 +498,27 @@ export class SemanticType {
|
||||
this.project.ensureActive();
|
||||
if ((this.flags & (TypeFlags.UnionOrIntersection | TypeFlags.TemplateLiteral)) === 0)
|
||||
return undefined;
|
||||
if (this.constituents === undefined)
|
||||
this.constituents = this.project.fetchTypes(this.id, "getTypesOfType");
|
||||
if (this.constituentsPanic !== undefined) throw this.constituentsPanic;
|
||||
if (this.constituents === undefined) {
|
||||
try {
|
||||
this.constituents = this.project.fetchTypes(this.id, "getTypesOfType");
|
||||
} catch (e) {
|
||||
// typescript-go panics serializing some constituents (an empty
|
||||
// tuple cloned as an array literal): answer every later read of
|
||||
// this snapshot's type with the same panic instead of a new
|
||||
// request.
|
||||
if (e instanceof Error && e.message.startsWith("panic:")) {
|
||||
(e as { unreadableConstituents?: true }).unreadableConstituents = true;
|
||||
this.constituentsPanic = e;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return this.constituents;
|
||||
}
|
||||
dispose(): void {
|
||||
this.constituents = undefined;
|
||||
this.constituentsPanic = undefined;
|
||||
}
|
||||
getBaseTypes(): SemanticType[] | undefined {
|
||||
this.project.ensureActive();
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
SYMBOL_T,
|
||||
typeEquals,
|
||||
typeKey,
|
||||
maybePromiseUnionArmsOk,
|
||||
unionContainerArmsOk,
|
||||
UNDEFINED_T,
|
||||
VOID,
|
||||
@@ -1099,6 +1100,9 @@ export interface TypeMapperCtx {
|
||||
/** The InternTable instance type (@scriptc/experimental), when `decl` is
|
||||
* that package's class declaration (lowering/intern-tables.ts). */
|
||||
internTableClass?: (decl: ts.ClassDeclaration) => IrType | null;
|
||||
/** The ThreadSlots instance type (@scriptc/experimental), when `decl` is
|
||||
* that package's class declaration (lowering/thread-slots.ts). */
|
||||
threadSlotsClass?: (decl: ts.ClassDeclaration) => IrType | null;
|
||||
}
|
||||
/** A generator type's normalized value channels (Generator<T, TReturn,
|
||||
* TNext> and the IteratorResult alias share this):
|
||||
@@ -1333,6 +1337,40 @@ export function isWorkerHandleType(
|
||||
return handle;
|
||||
}
|
||||
|
||||
/** The native fetch handles (Headers, Request, Response), optionally in
|
||||
* arrays or with null/undefined: checked-dynamic handles wherever they are
|
||||
* stored, like the worker handles above. */
|
||||
export function isFetchHandleType(
|
||||
type: ts.Type,
|
||||
ctx: Pick<TypeMapperCtx, "checker" | "isStdlibFile">,
|
||||
): boolean {
|
||||
const members = type.isUnionType() ? ts.constituentTypes(type) : [type];
|
||||
let handle = false;
|
||||
for (const member of members) {
|
||||
if (member.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) continue;
|
||||
if (ctx.checker.isArrayType(member)) {
|
||||
const element = ctx.checker.getTypeArguments(member as ts.TypeReference)[0];
|
||||
if (element === undefined || !isFetchHandleType(element, ctx)) return false;
|
||||
} else {
|
||||
const symbol = member.getSymbol();
|
||||
if (
|
||||
!symbol ||
|
||||
(symbol.name !== "Headers" && symbol.name !== "Request" && symbol.name !== "Response") ||
|
||||
!ctx.checker
|
||||
.declarationsOf(symbol)
|
||||
.some(
|
||||
(d) =>
|
||||
(ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
|
||||
ctx.isStdlibFile(d.getSourceFile()),
|
||||
)
|
||||
)
|
||||
return false;
|
||||
}
|
||||
handle = true;
|
||||
}
|
||||
return handle;
|
||||
}
|
||||
|
||||
export function mapType(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
if (mapTypeDepth >= MAP_TYPE_MAX_DEPTH) return null;
|
||||
// These intrinsic domains cannot depend on generic bindings, declaration
|
||||
@@ -1356,7 +1394,16 @@ export function mapType(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
const sensitivityAtEntry = contextResolutions;
|
||||
mapTypeDepth++;
|
||||
try {
|
||||
const mapped = mapTypeInner(type, ctx);
|
||||
let mapped: IrType | null;
|
||||
try {
|
||||
mapped = mapTypeInner(type, ctx);
|
||||
} catch (e) {
|
||||
// typescript-go's API can panic serializing a union's members (an
|
||||
// empty tuple cloned as an array literal, `s.match(re) || []`): a
|
||||
// union the checker cannot describe has no representation.
|
||||
if (!topLevel || !ts.isUnreadableConstituentsPanic(e)) throw e;
|
||||
mapped = null;
|
||||
}
|
||||
if (
|
||||
mapped !== null &&
|
||||
memoKey !== null &&
|
||||
@@ -1712,16 +1759,21 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
return hasStr ? STRING : F64;
|
||||
}
|
||||
}
|
||||
// @scriptc/experimental's LifetimeRegion and InternTable: synthetic
|
||||
// program classes, provenance-checked by the package's declaration file.
|
||||
// @scriptc/experimental's LifetimeRegion, InternTable and ThreadSlots:
|
||||
// synthetic program classes, provenance-checked by the package's
|
||||
// declaration file.
|
||||
{
|
||||
const classSym = widened.getSymbol();
|
||||
const name = classSym?.name;
|
||||
if (name === "LifetimeRegion" || name === "InternTable") {
|
||||
if (name === "LifetimeRegion" || name === "InternTable" || name === "ThreadSlots") {
|
||||
for (const d of checker.declarationsOf(classSym!)) {
|
||||
if (!ts.isClassDeclaration(d)) continue;
|
||||
const synthetic =
|
||||
name === "LifetimeRegion" ? ctx.lifetimeRegionClass?.(d) : ctx.internTableClass?.(d);
|
||||
name === "LifetimeRegion"
|
||||
? ctx.lifetimeRegionClass?.(d)
|
||||
: name === "InternTable"
|
||||
? ctx.internTableClass?.(d)
|
||||
: ctx.threadSlotsClass?.(d);
|
||||
if (synthetic) return synthetic;
|
||||
}
|
||||
}
|
||||
@@ -3216,8 +3268,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
// Native collection iterators use checked runtime iterator objects.
|
||||
if (isStdlibInterface("MapIterator") || isStdlibInterface("SetIterator")) return DYN;
|
||||
// Array iterators (`list.values()` and friends) are generators over the
|
||||
// live array: each step reads the current length and element.
|
||||
if (isStdlibInterface("ArrayIterator")) {
|
||||
// live array: each step reads the current length and element. String
|
||||
// iterators (`s[Symbol.iterator]()`) are generators over the code points.
|
||||
if (isStdlibInterface("ArrayIterator") || isStdlibInterface("StringIterator")) {
|
||||
const arg = checker.getTypeArguments(widened as ts.TypeReference)[0];
|
||||
const yieldT = arg ? mapType(arg, ctx) : null;
|
||||
if (!yieldT || yieldT.kind === "void" || isUnitType(yieldT)) return null;
|
||||
@@ -4196,7 +4249,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
a.kind === "dyn" ||
|
||||
a.kind === "generator",
|
||||
) ||
|
||||
!unionContainerArmsOk(arms)
|
||||
!maybePromiseUnionOk(parts, arms, ctx)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
@@ -4254,6 +4307,7 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null
|
||||
}
|
||||
const params: IrType[] = [];
|
||||
let typedRest = false;
|
||||
let dynRest = false;
|
||||
for (const p of sig.getParameters()) {
|
||||
const decl = checker.valueDeclarationOf(p);
|
||||
const rest = decl !== undefined && ts.isParameter(decl) && decl.dotDotDotToken !== undefined;
|
||||
@@ -4316,6 +4370,21 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null
|
||||
if (!armed) return null;
|
||||
pt = armed;
|
||||
}
|
||||
// `...args: unknown[]` / `...args: any[]` declared in program source:
|
||||
// the checked-dynamic variadic ABI (no typed slot; the callee's hidden
|
||||
// trailing dyn array receives every surplus argument, as for JS rest
|
||||
// params). Declaration files keep their existing paths: library
|
||||
// callback slots such as String#replace's `(substring, ...args: any[])`
|
||||
// have intrinsic lowerings that inspect the unmapped type.
|
||||
if (
|
||||
rest &&
|
||||
decl !== undefined &&
|
||||
!decl.getSourceFile().isDeclarationFile &&
|
||||
(pt?.kind === "dyn" || (!pt && isAnyArrayType(checker.getTypeOfSymbol(p), checker)))
|
||||
) {
|
||||
dynRest = true;
|
||||
continue;
|
||||
}
|
||||
if (!pt) return null;
|
||||
if (rest) {
|
||||
if (pt.kind !== "array") return null;
|
||||
@@ -4376,7 +4445,9 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null
|
||||
if (!ret) return null;
|
||||
return typedRest
|
||||
? { kind: "func", params, ret, rest: true, restAbi: "typed" }
|
||||
: funcOf(params, ret);
|
||||
: dynRest
|
||||
? { kind: "func", params, ret, rest: true }
|
||||
: funcOf(params, ret);
|
||||
}
|
||||
|
||||
/** Retain a literal discriminator before widening erases its values. A
|
||||
@@ -6280,9 +6351,49 @@ function dynSubsumableUnionArm(arm: IrType, ctx: TypeMapperCtx): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
/** `any[]` (or `readonly any[]`): an array type whose element is `any`. */
|
||||
export function isAnyArrayType(t: ts.Type, checker: ts.TypeChecker): boolean {
|
||||
if (!checker.isArrayType(t)) return false;
|
||||
const elem = checker.getTypeArguments(t as ts.TypeReference)[0];
|
||||
return elem !== undefined && (elem.flags & ts.TypeFlags.Any) !== 0;
|
||||
}
|
||||
|
||||
/** The maybe-promise union rule (mapTypeInner's union rule; IR
|
||||
* maybePromiseUnionArmsOk): at most one promise arm, and when it has data
|
||||
* siblings none of them may be thenable — `await` and async `return`
|
||||
* dispatch on the union tag, so a data arm whose type declares `then`
|
||||
* would skip the thenable adoption JS performs. */
|
||||
function maybePromiseUnionOk(
|
||||
parts: readonly ts.Type[],
|
||||
arms: IrType[],
|
||||
ctx: TypeMapperCtx,
|
||||
): boolean {
|
||||
if (unionContainerArmsOk(arms)) return true;
|
||||
if (!maybePromiseUnionArmsOk(arms)) return false;
|
||||
// A function beside a promise (`Promise<T> | (() => Promise<T>)`, the
|
||||
// assert.rejects argument) keeps its existing representation: callers
|
||||
// dispatch on callability, not on a then-able promise.
|
||||
if (arms.some((arm) => arm.kind === "func")) return false;
|
||||
return maybePromiseThenableArm(parts, ctx) === null;
|
||||
}
|
||||
|
||||
/** The first non-promise constituent of a promise-armed union whose type
|
||||
* has a `then` member (a thenable data arm), or null. */
|
||||
function maybePromiseThenableArm(parts: readonly ts.Type[], ctx: TypeMapperCtx): ts.Type | null {
|
||||
const { checker } = ctx;
|
||||
for (const part of parts) {
|
||||
if (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void | ts.TypeFlags.Null)) continue;
|
||||
if (mapType(part, ctx)?.kind === "promise") continue;
|
||||
// Primitive arms answer through their wrapper prototypes, none of
|
||||
// which has `then`; object arms answer through their own members.
|
||||
if (checker.getPropertyOfType(part, "then")) return part;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Arm kinds with no home in a compiled union (mapTypeInner's union rule):
|
||||
* no runtime narrowing test exists against sibling data arms. */
|
||||
function armHasUnionHome(arm: IrType, siblingCount: number): boolean {
|
||||
function armHasUnionHome(arm: IrType): boolean {
|
||||
switch (arm.kind) {
|
||||
case "void":
|
||||
case "union":
|
||||
@@ -6291,9 +6402,6 @@ function armHasUnionHome(arm: IrType, siblingCount: number): boolean {
|
||||
case "generator":
|
||||
case "dyn":
|
||||
return false;
|
||||
// Promise data siblings still lack a supported narrowing contract.
|
||||
case "promise":
|
||||
return siblingCount === 0;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
@@ -6418,10 +6526,24 @@ export function describeComponentBlocker(widened: ts.Type, ctx: TypeMapperCtx):
|
||||
if (!mapped) {
|
||||
return `the union shape is supported, but its arm '${text(part)}' does not compile`;
|
||||
}
|
||||
if (!armHasUnionHome(mapped, dataArms.length - 1)) {
|
||||
if (!armHasUnionHome(mapped)) {
|
||||
return `the union shape is supported, but '${text(part)}' arms have no home in a compiled union yet (no runtime narrowing test exists against sibling arms)`;
|
||||
}
|
||||
}
|
||||
const promiseArms = mappedArms.filter(({ mapped }) => mapped?.kind === "promise");
|
||||
if (promiseArms.length > 1) {
|
||||
return `the union shape is supported, but it has several promise arms ('${text(promiseArms[0]!.p)}' and '${text(promiseArms[1]!.p)}'), and a compiled union holds at most one (the union tag is the 'instanceof Promise' test)`;
|
||||
}
|
||||
if (promiseArms.length === 1 && dataArms.length > 1) {
|
||||
const callable = mappedArms.find(({ mapped }) => mapped?.kind === "func");
|
||||
if (callable) {
|
||||
return `the union shape is supported, but its function arm '${text(callable.p)}' sits beside a promise arm, and a maybe-promise union holds only data arms beside its promise`;
|
||||
}
|
||||
const thenable = maybePromiseThenableArm(parts, ctx);
|
||||
if (thenable) {
|
||||
return `the union shape is supported, but its arm '${text(thenable)}' declares 'then' beside a promise arm, so 'await' would have to adopt it as a thenable (a maybe-promise union's data arms must not be thenable)`;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -119,6 +119,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"fetch.requestNew",
|
||||
"fetch.headersNew",
|
||||
"fetch.responseNew",
|
||||
"fetch.responseJsonStatic",
|
||||
"fetch.abortTimeout",
|
||||
"fetch.abortAny",
|
||||
"abort.timeout",
|
||||
@@ -167,6 +168,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"http.resWriteHeadDyn",
|
||||
"http.resSetHeaderDyn",
|
||||
"http.clientSignal",
|
||||
"http.clientSignalCheck",
|
||||
"http.serverMaxHeaderSize",
|
||||
"http.clientMaxHeaderSize",
|
||||
"http.resAppendHeader",
|
||||
|
||||
@@ -65,6 +65,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
* are opaque checked-dynamic handles. */
|
||||
"fetch.start": { argTypes: [STRING, DYN], result: { kind: "promise", inner: DYN } },
|
||||
"fetch.responseNew": { argTypes: [DYN, DYN], result: DYN },
|
||||
"fetch.responseJsonStatic": { argTypes: [DYN, DYN], result: DYN },
|
||||
"fetch.input": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } },
|
||||
"abort.controllerNew": { argTypes: [], result: DYN },
|
||||
/** EventTarget / Event / CustomEvent construction (scr_event_target.c):
|
||||
@@ -1798,15 +1799,21 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
* socket POOLING is not modeled (one dial per request); maxSockets
|
||||
* accounting is real: over-limit requests defer their dial and queue.
|
||||
* MAY THROW. */
|
||||
"http.agentNew": { argTypes: [BOOL, BOOL, F64, F64, F64, F64, F64], result: DYN },
|
||||
"http.agentNew": { argTypes: [BOOL, BOOL, F64, F64, F64, F64, F64, DYN], result: DYN },
|
||||
/** http.globalAgent / https.globalAgent (true): Node's keepAlive global
|
||||
* agent, the one requests without an agent option use. */
|
||||
"http.globalAgent": { argTypes: [BOOL], result: DYN },
|
||||
/** http.request's `signal` option over the fresh request: an aborted
|
||||
* signal destroys it with Node's AbortError now, a later abort event
|
||||
* does the same. A falsy value is ignored; a non-signal throws
|
||||
* ERR_INVALID_ARG_TYPE. */
|
||||
"http.clientSignal": { argTypes: [HTTPCLIENTREQ_T, DYN], result: VOID },
|
||||
/** https.request's servername option: the TLS name sent and verified.
|
||||
* The "https." prefix keeps the TLS unit linked. */
|
||||
"https.clientServername": { argTypes: [HTTPCLIENTREQ_T, STRING], result: VOID },
|
||||
/** options.signal checked before the request is built (ERR_INVALID_ARG_TYPE). */
|
||||
"http.clientSignalCheck": { argTypes: [DYN], result: VOID },
|
||||
/** https.request's servername option, stored for the construction call
|
||||
* that follows: the TLS name sent and verified, and part of the pooled
|
||||
* socket key. The "https." prefix keeps the TLS unit linked. */
|
||||
"https.nextServername": { argTypes: [STRING], result: VOID },
|
||||
/** The maxHeaderSize option of createServer / http.request: Node's
|
||||
* validateInteger (ERR_OUT_OF_RANGE) then the header size bound.
|
||||
* MAY THROW. */
|
||||
@@ -2280,11 +2287,13 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
result: VOID,
|
||||
},
|
||||
/** execFileSync/execSync as ONE entry (args: cmd, argv, shell, input,
|
||||
* cwd, hasEnv, envPairs, timeoutMs, stdoutMode, stderrMode — see
|
||||
* scr_runtime.h). Throws Node's exact errors: "Command failed: <cmd>"
|
||||
* cwd, hasEnv, envPairs, timeoutMs, stdoutMode, stderrMode, buffers —
|
||||
* see scr_child.h). Throws Node's exact errors: "Command failed: <cmd>"
|
||||
* (+ captured stderr) on non-zero exit or signal death, "spawnSync
|
||||
* <file> ENOENT" on spawn failure, "spawnSync <file> ETIMEDOUT" after
|
||||
* the SIGTERM timeout. Result is the captured utf8 stdout (+1). */
|
||||
* the SIGTERM timeout, with Node's status, signal, output, pid, stdout
|
||||
* and stderr properties (Buffer outputs when buffers is set: no
|
||||
* encoding option). Result is the captured utf8 stdout (+1). */
|
||||
"cp.execSync": {
|
||||
argTypes: [
|
||||
STRING,
|
||||
@@ -2298,6 +2307,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
F64,
|
||||
F64,
|
||||
F64,
|
||||
BOOL,
|
||||
],
|
||||
result: STRING,
|
||||
},
|
||||
@@ -4215,6 +4225,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
"date.valueOf": { argTypes: [DATE_T], result: F64 },
|
||||
"date.toISOString": { argTypes: [F64], result: STRING },
|
||||
"date.toISOStringValue": { argTypes: [DATE_T], result: STRING },
|
||||
/** Date.prototype.toUTCString over a stored Date ("Invalid Date" for
|
||||
* NaN; never throws). */
|
||||
"date.toUTCString": { argTypes: [DATE_T], result: STRING },
|
||||
"date.getFullYear": { argTypes: [DATE_T], result: F64 },
|
||||
"date.getUTCFullYear": { argTypes: [DATE_T], result: F64 },
|
||||
"date.getMonth": { argTypes: [DATE_T], result: F64 },
|
||||
|
||||
@@ -209,6 +209,7 @@ const MODULE_FIELDS: Record<StringFields<Omit<IrModule, "functions">>, Role> = {
|
||||
"contracts.acyclic[]": "other",
|
||||
"contracts.immortalGlobals[]": "other",
|
||||
"contracts.programLifetime[]": "other",
|
||||
"threadSlotKeys[]": "other",
|
||||
};
|
||||
|
||||
const loc = { file: "references.ts", start: 0, end: 0 };
|
||||
|
||||
@@ -681,7 +681,12 @@ export function isIdentityCollectionKey(t: IrType): boolean {
|
||||
t.kind === "object" ||
|
||||
t.kind === "array" ||
|
||||
t.kind === "netServer" ||
|
||||
t.kind === "symbol"
|
||||
t.kind === "symbol" ||
|
||||
// A function value is one closure for its whole life (top-level
|
||||
// functions intern one immortal closure, inner closures allocate once
|
||||
// per evaluation), so pointer identity is JS function identity.
|
||||
// Closures are cycle-headered and traced like records.
|
||||
t.kind === "func"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -727,8 +732,8 @@ export function isSupportedSetElem(t: IrType, unionArms?: IrType[]): boolean {
|
||||
return isSupportedMapKey(t, unionArms);
|
||||
}
|
||||
|
||||
/** The Map VALUE fence: scalars, supported native references and checked
|
||||
* values. Functions require checked-value boxing.
|
||||
/** The Map VALUE fence: scalars, supported native references, closures
|
||||
* and checked values.
|
||||
* Record/object/union values can point back at the map holding them, which
|
||||
* is exactly why ref-valued maps are cycle-capable (see the backend's
|
||||
* cycle analysis and docs/memory.md). Shared frontend/validator. */
|
||||
@@ -782,6 +787,12 @@ export function isSupportedMapValue(t: IrType): boolean {
|
||||
// a regex holds no references), map form.
|
||||
case "regex":
|
||||
return true;
|
||||
// A closure (Map<string, (x: T) => U> — the factory/handler registry):
|
||||
// the index-signature overflow story (scr_closure adapters; closures
|
||||
// are cycle-headered and traced, so a handler capturing its own
|
||||
// registry collects).
|
||||
case "func":
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -797,7 +808,7 @@ export function isSupportedMapValue(t: IrType): boolean {
|
||||
* Nested index-signature RECORDS ride the record kind like any other.
|
||||
* Shared frontend (mapType) / validator. */
|
||||
export function isSupportedIndexValue(t: IrType): boolean {
|
||||
return t.kind === "dyn" || t.kind === "func" || isSupportedMapValue(t);
|
||||
return t.kind === "dyn" || isSupportedMapValue(t);
|
||||
}
|
||||
|
||||
export function funcOf(params: IrType[], ret: IrType): IrType {
|
||||
@@ -805,14 +816,25 @@ export function funcOf(params: IrType[], ret: IrType): IrType {
|
||||
}
|
||||
|
||||
/** Maps and sets use tagged union storage and retain their native identity.
|
||||
* Promises still require nullable-only unions until their data-arm narrowing
|
||||
* and await boundary have a matching contract. */
|
||||
* Joined unions (inferred joins of member reads and array elements) keep
|
||||
* promises nullable-only; declared maybe-promise unions are admitted by
|
||||
* maybePromiseUnionArmsOk. */
|
||||
export function unionContainerArmsOk(arms: IrType[]): boolean {
|
||||
return arms.every(
|
||||
(a, i) => a.kind !== "promise" || arms.every((b, j) => j === i || isUnitType(b)),
|
||||
);
|
||||
}
|
||||
|
||||
/** The MAYBE-PROMISE union (`T | Promise<T>`, `Promise<R | void> | R |
|
||||
* void`): at most one promise arm, beside unit or data arms. The union tag
|
||||
* is the `instanceof Promise` test, and `await` / async `return` branch on
|
||||
* it (the promise arm awaits or adopts, the other arms take the
|
||||
* non-thenable path). Data arms must not be thenable; the type mapper
|
||||
* checks that against the checker types before minting such a union. */
|
||||
export function maybePromiseUnionArmsOk(arms: IrType[]): boolean {
|
||||
return arms.filter((a) => a.kind === "promise").length <= 1;
|
||||
}
|
||||
|
||||
/** Canonical, injective text form of an IrType — the building block of
|
||||
* shape/union identity keys, generic-function instantiation keys, and the
|
||||
* backend's per-type helper interning (jsonStringify/dynCheck walkers).
|
||||
@@ -872,6 +894,33 @@ export function typeKey(t: IrType): string {
|
||||
}
|
||||
}
|
||||
|
||||
/** The type a CALL SITE (or function value) receives from a module
|
||||
* function: an async body's returnType is the promise's INNER type, so
|
||||
* calls and closures over it see Promise<T>; a generator body's is the
|
||||
* TReturn channel, so call sites see the generator type. The one place
|
||||
* the body/call-site split is spelled out, for the validator and lowering. */
|
||||
export function callSiteReturnType(fn: IrFunction): IrType {
|
||||
if (fn.async && fn.generator !== undefined) {
|
||||
return {
|
||||
kind: "generator",
|
||||
async: true,
|
||||
yieldT: fn.generator.yieldT,
|
||||
retT: fn.returnType,
|
||||
nextT: fn.generator.nextT,
|
||||
};
|
||||
}
|
||||
if (fn.async) return { kind: "promise", inner: fn.returnType };
|
||||
if (fn.generator !== undefined) {
|
||||
return {
|
||||
kind: "generator",
|
||||
yieldT: fn.generator.yieldT,
|
||||
retT: fn.returnType,
|
||||
nextT: fn.generator.nextT,
|
||||
};
|
||||
}
|
||||
return fn.returnType;
|
||||
}
|
||||
|
||||
export function typeEquals(a: IrType, b: IrType): boolean {
|
||||
// Dispatch once: a chain of negative narrowing checks repeatedly retags
|
||||
// the remaining variants when this comparator runs in the native compiler.
|
||||
@@ -970,6 +1019,10 @@ export interface IrModule {
|
||||
* ProgramLifetime instance lives in a counted region (the region.*
|
||||
* intrinsics; backend/llvm/regions.ts). */
|
||||
lifetimeRegions?: true;
|
||||
/** Classes used as ThreadSlots keys (@scriptc/experimental): the root of
|
||||
* each one's hierarchy carries a slot word (the slots.* intrinsics;
|
||||
* backend/llvm/thread-slots.ts). */
|
||||
threadSlotKeys?: string[];
|
||||
/** The entry module is CommonJS under Node's classification. Node runs a
|
||||
* CommonJS main synchronously and drains process.nextTick callbacks
|
||||
* before promise jobs at the first checkpoint; an ES-module main's
|
||||
@@ -3607,7 +3660,19 @@ export type IrExpr =
|
||||
| "itable.find"
|
||||
| "itable.insert"
|
||||
| "itable.value"
|
||||
| "itable.size";
|
||||
| "itable.size"
|
||||
/* ThreadSlots (@scriptc/experimental; scr_slots.c). Every one takes
|
||||
* the family name (a string literal), the family's %ThreadSlots
|
||||
* value (read for its effects only) and the key, a class instance
|
||||
* whose hierarchy is in IrModule.threadSlotKeys:
|
||||
* slots.get → V | undefined, a nullable union, owned (+1);
|
||||
* slots.has → bool; slots.delete → bool, releasing the value;
|
||||
* slots.set (…, V value) → void, or the %ThreadSlots value when the
|
||||
* result is used. Nothing throws. */
|
||||
| "slots.get"
|
||||
| "slots.has"
|
||||
| "slots.set"
|
||||
| "slots.delete";
|
||||
args: IrExpr[];
|
||||
type: IrType;
|
||||
loc: SrcLoc;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { IrExpr, IrModule, IrRecordShape, IrType, IrUnionDef } from "./ir.js";
|
||||
import type { IrClassDef, IrExpr, IrModule, IrRecordShape, IrType, IrUnionDef } from "./ir.js";
|
||||
import type { IrLibFn } from "./builtin-signatures.js";
|
||||
import {
|
||||
isDirentShape,
|
||||
@@ -137,10 +137,11 @@ export function moduleRuntimeFeatures(mod: IrModule): RuntimeFeatures {
|
||||
return scanRuntimeFeatures(mod);
|
||||
}
|
||||
|
||||
/** The module's record shapes and unions by id. */
|
||||
/** The module's record shapes, unions and class layouts by id. */
|
||||
interface TypeTables {
|
||||
records: Map<string, IrRecordShape>;
|
||||
unions: Map<string, IrUnionDef>;
|
||||
classes: Map<string, IrClassDef>;
|
||||
}
|
||||
|
||||
/** Whether boxing a value of type `t` as dyn boxes a promise, which goes
|
||||
@@ -171,6 +172,15 @@ function boxesPromise(t: IrType, tables: TypeTables, seen: Set<string>): boolean
|
||||
const def = tables.unions.get(t.unionId);
|
||||
return def?.arms.some((a) => boxesPromise(a, tables, seen)) ?? false;
|
||||
}
|
||||
case "object": {
|
||||
// A boxed class instance exposes its fields as dyn values (Object.keys,
|
||||
// spread, inspection), boxing a promise any field holds.
|
||||
const key = `class:${t.className}`;
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
const def = tables.classes.get(t.className);
|
||||
return def?.fields.some((f) => boxesPromise(f.type, tables, seen)) ?? false;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -228,6 +238,7 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run
|
||||
(tables ??= {
|
||||
records: new Map((mod.records ?? []).map((r) => [r.id, r])),
|
||||
unions: new Map((mod.unions ?? []).map((u) => [u.id, u])),
|
||||
classes: new Map((mod.classes ?? []).map((c) => [c.name, c])),
|
||||
});
|
||||
const expr = (node: IrExpr): boolean => {
|
||||
if (node.kind === "libCall") {
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
import { expect, test } from "vitest";
|
||||
import {
|
||||
F64,
|
||||
funcOf,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrLocal,
|
||||
type IrModule,
|
||||
type IrStmt,
|
||||
} from "./ir.js";
|
||||
import { resolveSelfCaptures } from "./self-captures.js";
|
||||
|
||||
const loc = { file: "self.ts", start: 0, end: 0 };
|
||||
const FN = funcOf([], F64);
|
||||
const ref = (localId: string, type = FN): IrExpr => ({ kind: "varRef", localId, type, loc });
|
||||
const call = (localId: string): IrExpr => ({
|
||||
kind: "callValue",
|
||||
callee: ref(localId),
|
||||
args: [],
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
const self: IrExpr = { kind: "selfRef", type: FN, loc };
|
||||
const closure = (fnName: string, captures: string[], extra: object = {}): IrExpr => ({
|
||||
kind: "closure",
|
||||
fnName,
|
||||
captures,
|
||||
type: FN,
|
||||
loc,
|
||||
...extra,
|
||||
});
|
||||
const decl = (localId: string, init: IrExpr | null): IrStmt => ({
|
||||
kind: "varDecl",
|
||||
localId,
|
||||
init,
|
||||
loc,
|
||||
});
|
||||
const assign = (localId: string, value: IrExpr, initializes = false): IrStmt => ({
|
||||
kind: "assign",
|
||||
localId,
|
||||
value,
|
||||
...(initializes ? { initializes: true as const } : {}),
|
||||
loc,
|
||||
});
|
||||
const ret = (e: IrExpr): IrStmt => ({ kind: "return", value: e, loc });
|
||||
const use = (e: IrExpr): IrStmt => ({ kind: "exprStmt", expr: e, loc });
|
||||
const local = (id: string, extra: Partial<IrLocal> = {}): IrLocal => ({
|
||||
id,
|
||||
name: id.split(".")[0]!,
|
||||
type: FN,
|
||||
mutable: false,
|
||||
boxed: true,
|
||||
...extra,
|
||||
});
|
||||
|
||||
function outer(body: IrStmt[], locals: IrLocal[]): IrFunction {
|
||||
return { name: "outer", params: [], returnType: F64, locals, body, loc };
|
||||
}
|
||||
function lifted(name: string, body: IrStmt[], captured: IrLocal[]): IrFunction {
|
||||
return {
|
||||
name,
|
||||
params: [],
|
||||
returnType: F64,
|
||||
captures: captured.map((l) => ({ localId: l.id, name: l.name, type: l.type })),
|
||||
locals: captured,
|
||||
body,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
function run(...functions: IrFunction[]): Map<string, IrFunction> {
|
||||
const mod = { functions } as unknown as IrModule;
|
||||
return new Map(resolveSelfCaptures(mod).functions.map((fn) => [fn.name, fn]));
|
||||
}
|
||||
|
||||
test("a closure stored in the dead-zone binding it captures reads itself", () => {
|
||||
const fns = run(
|
||||
outer(
|
||||
[decl("f.0", null), assign("f.0", closure("walk", ["f.0"]), true), ret(call("f.0"))],
|
||||
[local("f.0", { mutable: true, tdz: true })],
|
||||
),
|
||||
lifted("walk", [ret(call("f.0"))], [local("f.0", { tdz: true })]),
|
||||
);
|
||||
const walk = fns.get("walk")!;
|
||||
expect(walk.captures).toEqual([]);
|
||||
expect(walk.locals).toEqual([]);
|
||||
expect(walk.body).toEqual([ret({ kind: "callValue", callee: self, args: [], type: F64, loc })]);
|
||||
const origin = fns.get("outer")!;
|
||||
expect(origin.body[1]).toEqual(assign("f.0", closure("walk", []), true));
|
||||
// The dead-zone box still represents the binding's uninitialized state.
|
||||
expect(origin.locals[0]).toMatchObject({ boxed: true, tdz: true });
|
||||
});
|
||||
|
||||
test("a binding that only held the closure loses its box", () => {
|
||||
const n = local("n.0", { type: F64 });
|
||||
const fns = run(
|
||||
outer(
|
||||
[
|
||||
decl("n.0", { kind: "numLit", value: 1, type: F64, loc }),
|
||||
decl("f.0", closure("walk", ["n.0", "f.0"])),
|
||||
ret(call("f.0")),
|
||||
],
|
||||
[n, local("f.0")],
|
||||
),
|
||||
lifted("walk", [use(ref("n.0", F64)), ret(call("f.0"))], [n, local("f.0")]),
|
||||
);
|
||||
expect(fns.get("walk")!.captures!.map((c) => c.localId)).toEqual(["n.0"]);
|
||||
const origin = fns.get("outer")!;
|
||||
expect(origin.body[1]).toEqual(decl("f.0", closure("walk", ["n.0"])));
|
||||
expect(origin.locals.find((l) => l.id === "f.0")!.boxed).toBeUndefined();
|
||||
expect(origin.locals.find((l) => l.id === "n.0")!.boxed).toBe(true);
|
||||
});
|
||||
|
||||
test("hoisted declarations qualify; later writes and second sites do not", () => {
|
||||
const body = (extra: IrStmt[]): IrStmt[] => [
|
||||
decl("f.0", null),
|
||||
assign("f.0", closure("walk", ["f.0"])),
|
||||
...extra,
|
||||
ret(call("f.0")),
|
||||
];
|
||||
const walk = lifted("walk", [ret(call("f.0"))], [local("f.0")]);
|
||||
const hoisted = run(outer(body([]), [local("f.0", { mutable: true })]), walk);
|
||||
expect(hoisted.get("walk")!.captures).toEqual([]);
|
||||
expect(hoisted.get("outer")!.locals[0]!.boxed).toBeUndefined();
|
||||
|
||||
const rewritten = run(
|
||||
outer(body([assign("f.0", closure("other", []))]), [local("f.0", { mutable: true })]),
|
||||
walk,
|
||||
lifted("other", [ret({ kind: "numLit", value: 0, type: F64, loc })], []),
|
||||
);
|
||||
expect(rewritten.get("walk")).toBe(walk);
|
||||
|
||||
// The same body stored somewhere else: its environment must keep the box.
|
||||
const elsewhere = run(
|
||||
outer(
|
||||
[...body([]).slice(0, 2), use(closure("walk", ["f.0"])), ret(call("f.0"))],
|
||||
[local("f.0", { mutable: true })],
|
||||
),
|
||||
walk,
|
||||
);
|
||||
expect(elsewhere.get("walk")).toBe(walk);
|
||||
});
|
||||
|
||||
test("an assignment in another statement list, an adapter or a nested capture keeps the box", () => {
|
||||
const walk = lifted("walk", [ret(call("f.0"))], [local("f.0")]);
|
||||
const branch = run(
|
||||
outer(
|
||||
[
|
||||
decl("f.0", null),
|
||||
{ kind: "block", body: [assign("f.0", closure("walk", ["f.0"]))], loc },
|
||||
ret(call("f.0")),
|
||||
],
|
||||
[local("f.0", { mutable: true })],
|
||||
),
|
||||
walk,
|
||||
);
|
||||
expect(branch.get("walk")).toBe(walk);
|
||||
|
||||
const adapter = run(
|
||||
outer(
|
||||
[decl("f.0", closure("walk", ["f.0"], { adapts: true })), ret(call("f.0"))],
|
||||
[local("f.0")],
|
||||
),
|
||||
walk,
|
||||
);
|
||||
expect(adapter.get("walk")).toBe(walk);
|
||||
|
||||
const nested = lifted("walk", [use(closure("inner", ["f.0"])), ret(call("f.0"))], [local("f.0")]);
|
||||
const recaptured = run(
|
||||
outer([decl("f.0", closure("walk", ["f.0"])), ret(call("f.0"))], [local("f.0")]),
|
||||
nested,
|
||||
lifted("inner", [ret(call("f.1"))], [local("f.1")]),
|
||||
);
|
||||
expect(recaptured.get("walk")).toBe(nested);
|
||||
});
|
||||
|
||||
test("mutually recursive closures only drop their own capture", () => {
|
||||
const fns = run(
|
||||
outer(
|
||||
[
|
||||
decl("a.0", null),
|
||||
decl("b.0", null),
|
||||
assign("a.0", closure("fa", ["a.0", "b.0"]), true),
|
||||
assign("b.0", closure("fb", ["a.0", "b.0"]), true),
|
||||
ret(call("a.0")),
|
||||
],
|
||||
[local("a.0", { mutable: true, tdz: true }), local("b.0", { mutable: true, tdz: true })],
|
||||
),
|
||||
lifted(
|
||||
"fa",
|
||||
[use(call("b.0")), ret(call("a.0"))],
|
||||
[local("a.0", { tdz: true }), local("b.0", { tdz: true })],
|
||||
),
|
||||
lifted(
|
||||
"fb",
|
||||
[use(call("a.0")), ret(call("b.0"))],
|
||||
[local("a.0", { tdz: true }), local("b.0", { tdz: true })],
|
||||
),
|
||||
);
|
||||
expect(fns.get("fa")!.captures!.map((c) => c.localId)).toEqual(["b.0"]);
|
||||
expect(fns.get("fb")!.captures!.map((c) => c.localId)).toEqual(["a.0"]);
|
||||
expect(fns.get("outer")!.body.slice(2)).toEqual([
|
||||
assign("a.0", closure("fa", ["b.0"]), true),
|
||||
assign("b.0", closure("fb", ["a.0"]), true),
|
||||
ret(call("a.0")),
|
||||
]);
|
||||
});
|
||||
@@ -0,0 +1,229 @@
|
||||
import { CaptureFlow } from "./capture-values.js";
|
||||
import {
|
||||
typeEquals,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrLocal,
|
||||
type IrModule,
|
||||
type IrStmt,
|
||||
} from "./ir.js";
|
||||
import { everyExprChild, everyStmtChild, mapExprChildren, mapStmtChildren } from "./traverse.js";
|
||||
|
||||
/** Closures that capture the binding they are stored in read themselves
|
||||
* through `selfRef` instead of through that binding's box.
|
||||
*
|
||||
* `const visit = (node) => { ... visit(child) ... }` lowers to a closure
|
||||
* over the box of `visit`, and the box then holds the closure: a two-object
|
||||
* reference cycle that reference counting cannot free. Each closure made
|
||||
* this way is buffered as a cycle candidate and reclaimed only when a
|
||||
* collector pass walks it. Nested function declarations already read their
|
||||
* own name through `selfRef`; this pass gives arrows and function
|
||||
* expressions bound to a binding the same form once the IR proves that the
|
||||
* binding always holds the closure itself while the closure can run:
|
||||
*
|
||||
* - every creation site of the closure body stores the new closure, as is,
|
||||
* into a boxed local of the creating function: either the initializer of
|
||||
* that local's only declaration, or the only assignment of a local whose
|
||||
* only declaration is empty and precedes the assignment in the same
|
||||
* statement list (so each box is filled once, and a closure can only run
|
||||
* after the statement that stores it);
|
||||
* - nothing else writes the binding, in the creating function or in any
|
||||
* body that shares its box (CaptureFlow.unchanged), and no local class
|
||||
* captures it;
|
||||
* - the closure body is synchronous, is not a generator, and does not pass
|
||||
* the binding on to a nested closure (its environment would need a box of
|
||||
* its own, allocated on every call).
|
||||
*
|
||||
* The capture then disappears from the body and from every creation site,
|
||||
* and each read of it becomes `selfRef` (the running closure, retained),
|
||||
* which is the same pointer the box would have produced. The creating
|
||||
* function keeps its binding; it loses its box when no other closure
|
||||
* captures it, except for temporal-dead-zone bindings, whose box still
|
||||
* carries the empty state its own reads test. */
|
||||
export function resolveSelfCaptures(mod: IrModule): IrModule {
|
||||
const functions = new Map(mod.functions.map((fn) => [fn.name, fn]));
|
||||
const sites = new Map<string, { owner: string; closure: IrExpr & { kind: "closure" } }[]>();
|
||||
/** Closure nodes stored into a local by a qualifying definition, with that
|
||||
* local and the number of writes the definition itself makes. */
|
||||
const definitions = new Map<IrExpr, { local: string; writes: number }>();
|
||||
const declarations = new Map<string, Map<string, number>>();
|
||||
for (const fn of mod.functions) {
|
||||
const counts = new Map<string, number>();
|
||||
declarations.set(fn.name, counts);
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
if (e.kind === "closure") {
|
||||
let list = sites.get(e.fnName);
|
||||
if (list === undefined) sites.set(e.fnName, (list = []));
|
||||
list.push({ owner: fn.name, closure: e });
|
||||
}
|
||||
return everyExprChild(e, expr, stmt);
|
||||
};
|
||||
const stmt = (s: IrStmt): boolean => {
|
||||
if (s.kind === "varDecl") counts.set(s.localId, (counts.get(s.localId) ?? 0) + 1);
|
||||
return everyStmtChild(s, expr, stmt);
|
||||
};
|
||||
fn.body.every(stmt);
|
||||
for (const list of statementLists(fn.body)) {
|
||||
list.forEach((s, i) => {
|
||||
if (s.kind !== "varDecl") return;
|
||||
if (s.init !== null) {
|
||||
if (s.init.kind === "closure") definitions.set(s.init, { local: s.localId, writes: 0 });
|
||||
return;
|
||||
}
|
||||
for (let j = i + 1; j < list.length; j++) {
|
||||
const next = list[j]!;
|
||||
if (next.kind === "assign" && next.localId === s.localId) {
|
||||
if (next.value.kind === "closure")
|
||||
definitions.set(next.value, { local: s.localId, writes: 1 });
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let flow: CaptureFlow | null = null;
|
||||
/** Per closure body, the capture index that names its own closure. */
|
||||
const resolved = new Map<string, number>();
|
||||
for (const fn of mod.functions) {
|
||||
const captures = fn.captures;
|
||||
if (captures === undefined || captures.length === 0) continue;
|
||||
if (fn.async === true || fn.generator !== undefined || fn.classCaptures !== undefined) continue;
|
||||
const list = sites.get(fn.name);
|
||||
if (list === undefined || list.length === 0) continue;
|
||||
const recaptured = capturedIds(fn.body);
|
||||
for (let index = 0; index < captures.length; index++) {
|
||||
const capture = captures[index]!;
|
||||
if (capture.type.kind !== "func" || recaptured.has(capture.localId)) continue;
|
||||
const qualifies = list.every(({ owner, closure }) => {
|
||||
if (closure.adapts === true || !typeEquals(closure.type, capture.type)) return false;
|
||||
const definition = definitions.get(closure);
|
||||
const source = closure.captures[index];
|
||||
if (definition === undefined || source !== definition.local) return false;
|
||||
const creator = functions.get(owner)!;
|
||||
const local = creator.locals.find((l) => l.id === source);
|
||||
if (local?.boxed !== true || !typeEquals(local.type, closure.type)) return false;
|
||||
if (creator.params.some((p) => p.localId === source)) return false;
|
||||
if (creator.captures?.some((c) => c.localId === source)) return false;
|
||||
if (creator.classCaptures?.some((c) => c.localId === source)) return false;
|
||||
if (declarations.get(owner)!.get(source) !== 1) return false;
|
||||
flow ??= new CaptureFlow(mod.functions);
|
||||
return flow.unchanged(owner, source, definition.writes);
|
||||
});
|
||||
if (qualifies) {
|
||||
resolved.set(fn.name, index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (resolved.size === 0) return mod;
|
||||
|
||||
const rewritten = mod.functions.map((fn) => {
|
||||
const own = resolved.get(fn.name);
|
||||
const self = own === undefined ? null : fn.captures![own]!;
|
||||
let changed = false;
|
||||
const expr = (e: IrExpr): IrExpr => {
|
||||
if (self !== null && e.kind === "varRef" && e.localId === self.localId) {
|
||||
changed = true;
|
||||
return { kind: "selfRef", type: self.type, loc: e.loc };
|
||||
}
|
||||
if (e.kind === "closure") {
|
||||
const drop = resolved.get(e.fnName);
|
||||
if (drop !== undefined) {
|
||||
changed = true;
|
||||
return { ...e, captures: e.captures.filter((_, i) => i !== drop) };
|
||||
}
|
||||
}
|
||||
return mapExprChildren(e, expr, stmt);
|
||||
};
|
||||
const stmt = (s: IrStmt): IrStmt => mapStmtChildren(s, expr, stmt);
|
||||
const body = fn.body.map(stmt);
|
||||
if (!changed && self === null) return fn;
|
||||
// A binding no environment captures any more needs no box. A dead-zone
|
||||
// binding keeps the box that represents its uninitialized state.
|
||||
const stillCaptured = capturedIds(body);
|
||||
// The callback returns IrLocal explicitly: inferred from both returns,
|
||||
// its type would be the unboxed copy's, and the self-hosted compiler
|
||||
// would narrow every kept local to that shape, dropping `boxed`.
|
||||
const locals = fn.locals
|
||||
.filter((local) => self === null || local.id !== self.localId)
|
||||
.map((local): IrLocal => {
|
||||
if (local.boxed !== true || local.tdz === true || stillCaptured.has(local.id)) return local;
|
||||
if (fn.captures?.some((c) => c.localId === local.id)) return local;
|
||||
if (fn.classCaptures?.some((c) => c.localId === local.id)) return local;
|
||||
if (!wasSource(fn, local.id, resolved)) return local;
|
||||
const { boxed: _boxed, ...plain } = local;
|
||||
return plain;
|
||||
});
|
||||
const next: IrFunction = { ...fn, locals, body };
|
||||
if (self !== null) next.captures = fn.captures!.filter((_, i) => i !== own);
|
||||
return next;
|
||||
});
|
||||
return { ...mod, functions: rewritten };
|
||||
}
|
||||
|
||||
/** Whether `fn` stored a resolved closure into `id`, the only case in which
|
||||
* this pass may unbox a binding (others stay exactly as lowered). */
|
||||
function wasSource(fn: IrFunction, id: string, resolved: ReadonlyMap<string, number>): boolean {
|
||||
let found = false;
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
if (e.kind === "closure") {
|
||||
const index = resolved.get(e.fnName);
|
||||
if (index !== undefined && e.captures[index] === id) found = true;
|
||||
}
|
||||
return !found && everyExprChild(e, expr, stmt);
|
||||
};
|
||||
const stmt = (s: IrStmt): boolean => everyStmtChild(s, expr, stmt);
|
||||
fn.body.every(stmt);
|
||||
return found;
|
||||
}
|
||||
|
||||
/** Locals some closure or class created in `body` captures. */
|
||||
function capturedIds(body: readonly IrStmt[]): Set<string> {
|
||||
const ids = new Set<string>();
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
if (e.kind === "closure" || e.kind === "classRef")
|
||||
for (const id of e.captures ?? []) ids.add(id);
|
||||
return everyExprChild(e, expr, stmt);
|
||||
};
|
||||
const stmt = (s: IrStmt): boolean => everyStmtChild(s, expr, stmt);
|
||||
body.every(stmt);
|
||||
return ids;
|
||||
}
|
||||
|
||||
/** `body` and every statement list nested in its statements (not the ones
|
||||
* inside expressions). Statements of one list run in order, each at most
|
||||
* once per execution of the list. */
|
||||
function statementLists(body: readonly IrStmt[]): (readonly IrStmt[])[] {
|
||||
const lists: (readonly IrStmt[])[] = [];
|
||||
const visit = (list: readonly IrStmt[]): void => {
|
||||
lists.push(list);
|
||||
for (const s of list) {
|
||||
switch (s.kind) {
|
||||
case "block":
|
||||
case "while":
|
||||
case "doWhile":
|
||||
case "for":
|
||||
case "forOf":
|
||||
visit(s.body);
|
||||
break;
|
||||
case "if":
|
||||
visit(s.then);
|
||||
if (s.else_ !== null) visit(s.else_);
|
||||
break;
|
||||
case "switch":
|
||||
for (const c of s.cases) visit(c.body);
|
||||
break;
|
||||
case "tryCatch":
|
||||
visit(s.tryBody);
|
||||
if (s.catchBody !== null) visit(s.catchBody);
|
||||
if (s.finallyBody !== null) visit(s.finallyBody);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
visit(body);
|
||||
return lists;
|
||||
}
|
||||
@@ -288,17 +288,25 @@ test.each([mapOf(STRING, F64), setOf(STRING), { kind: "promise", inner: F64 } as
|
||||
},
|
||||
);
|
||||
|
||||
test.each([{ kind: "promise", inner: F64 } as IrType])(
|
||||
"%j payloads still refuse unrelated data siblings",
|
||||
(type) => {
|
||||
const mod = expressionModule({ kind: "numLit", value: 0, type: F64, loc }, [
|
||||
{ id: "mixed", arms: [type, STRING, UNDEFINED_T] },
|
||||
]);
|
||||
expect(validateModule(mod).map((error) => error.message)).toContain(
|
||||
`union mixed: ${type.kind} arm 0 beside non-unit arms`,
|
||||
);
|
||||
},
|
||||
);
|
||||
test("a maybe-promise union holds one promise arm beside data arms", () => {
|
||||
const mod = expressionModule({ kind: "numLit", value: 0, type: F64, loc }, [
|
||||
{ id: "maybe", arms: [{ kind: "promise", inner: F64 }, STRING, UNDEFINED_T] },
|
||||
]);
|
||||
expect(validateModule(mod)).toEqual([]);
|
||||
expect(deserializeModule(serializeModule(mod))).toEqual(mod);
|
||||
});
|
||||
|
||||
test("a union refuses a second promise arm", () => {
|
||||
const mod = expressionModule({ kind: "numLit", value: 0, type: F64, loc }, [
|
||||
{
|
||||
id: "mixed",
|
||||
arms: [BOOL, { kind: "promise", inner: F64 }, { kind: "promise", inner: STRING }],
|
||||
},
|
||||
]);
|
||||
expect(validateModule(mod).map((error) => error.message)).toContain(
|
||||
"union mixed: promise arm 1 beside another promise arm",
|
||||
);
|
||||
});
|
||||
|
||||
test("differently typed collection payloads preserve separate union tags", () => {
|
||||
const mod = expressionModule({ kind: "numLit", value: 0, type: F64, loc }, [
|
||||
|
||||
@@ -53,10 +53,10 @@ import {
|
||||
STRING,
|
||||
typeEquals,
|
||||
typeKey,
|
||||
unionContainerArmsOk,
|
||||
maybePromiseUnionArmsOk,
|
||||
VOID,
|
||||
} from "./ir.js";
|
||||
import { DYN_CLASS_PROPERTIES } from "./ir.js";
|
||||
import { DYN_CLASS_PROPERTIES, callSiteReturnType } from "./ir.js";
|
||||
import { unionWideningTags } from "./analysis.js";
|
||||
import { alwaysReturns } from "./control-flow.js";
|
||||
import { dynamicOperationOf, functionTier } from "./tiers.js";
|
||||
@@ -85,33 +85,6 @@ export interface IrValidationError {
|
||||
loc: SrcLoc;
|
||||
}
|
||||
|
||||
/** The type a CALL SITE (or function value) receives from a module
|
||||
* function: an async body's returnType is the promise's INNER type, so
|
||||
* calls and closures over it see Promise<T>; a generator body's is the
|
||||
* TReturn channel, so call sites see the generator type. The one place
|
||||
* the body/call-site split is spelled out in the validator. */
|
||||
function callSiteReturnType(fn: IrFunction): IrType {
|
||||
if (fn.async && fn.generator !== undefined) {
|
||||
return {
|
||||
kind: "generator",
|
||||
async: true,
|
||||
yieldT: fn.generator.yieldT,
|
||||
retT: fn.returnType,
|
||||
nextT: fn.generator.nextT,
|
||||
};
|
||||
}
|
||||
if (fn.async) return { kind: "promise", inner: fn.returnType };
|
||||
if (fn.generator !== undefined) {
|
||||
return {
|
||||
kind: "generator",
|
||||
yieldT: fn.generator.yieldT,
|
||||
retT: fn.returnType,
|
||||
nextT: fn.generator.nextT,
|
||||
};
|
||||
}
|
||||
return fn.returnType;
|
||||
}
|
||||
|
||||
interface VirtualCallInfo {
|
||||
declared: boolean;
|
||||
hasOverride: boolean;
|
||||
@@ -821,7 +794,7 @@ export function validateModule(mod: IrModule): IrValidationError[] {
|
||||
u.arms.forEach((arm, i) => {
|
||||
// The unit kinds (undefinedT/nullT) are valid arms — union membership
|
||||
// is the ONLY place they may appear. Container payloads preserve
|
||||
// identity; promise data siblings retain their separate fence.
|
||||
// identity; a union carries at most one promise arm (maybe-promise).
|
||||
if (
|
||||
arm.kind === "void" ||
|
||||
arm.kind === "union" ||
|
||||
@@ -831,9 +804,9 @@ export function validateModule(mod: IrModule): IrValidationError[] {
|
||||
) {
|
||||
errors.push({ message: `union ${u.id}: arm ${i} is ${arm.kind}`, loc: noLoc });
|
||||
}
|
||||
if (arm.kind === "promise" && !unionContainerArmsOk(u.arms)) {
|
||||
if (arm.kind === "promise" && !maybePromiseUnionArmsOk(u.arms)) {
|
||||
errors.push({
|
||||
message: `union ${u.id}: ${arm.kind} arm ${i} beside non-unit arms`,
|
||||
message: `union ${u.id}: ${arm.kind} arm ${i} beside another promise arm`,
|
||||
loc: noLoc,
|
||||
});
|
||||
}
|
||||
@@ -998,6 +971,7 @@ export function validateModule(mod: IrModule): IrValidationError[] {
|
||||
errors,
|
||||
mod.workers === true,
|
||||
mod.lifetimeRegions === true,
|
||||
new Set(mod.threadSlotKeys ?? []),
|
||||
);
|
||||
}
|
||||
return errors;
|
||||
@@ -1016,6 +990,7 @@ function validateFunction(
|
||||
errors: IrValidationError[],
|
||||
workers: boolean,
|
||||
lifetimeRegions: boolean,
|
||||
threadSlotKeys: ReadonlySet<string>,
|
||||
): void {
|
||||
const locals = new Map(fn.locals.map((l) => [l.id, l]));
|
||||
const err = (message: string, loc: SrcLoc) =>
|
||||
@@ -3894,6 +3869,44 @@ function validateFunction(
|
||||
} else if (e.type.kind !== "f64") err(`${e.name} has type f64`, e.loc);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
e.name === "slots.get" ||
|
||||
e.name === "slots.has" ||
|
||||
e.name === "slots.set" ||
|
||||
e.name === "slots.delete"
|
||||
) {
|
||||
for (const a of e.args) checkExpr(a);
|
||||
const [name, slots, key, value] = e.args;
|
||||
const arity = e.name === "slots.set" ? 4 : 3;
|
||||
const isSlots = (t: IrType): boolean => t.kind === "object" && t.className === "%ThreadSlots";
|
||||
// The key's class or one of its bases is a declared key.
|
||||
const keyed = (className: string): boolean => {
|
||||
for (let c: string | undefined = className; c !== undefined; c = classes.get(c)?.base)
|
||||
if (threadSlotKeys.has(c)) return true;
|
||||
return false;
|
||||
};
|
||||
if (e.args.length !== arity) err(`${e.name} takes ${arity} arguments`, e.loc);
|
||||
else if (name!.kind !== "strLit" || !isSlots(slots!.type))
|
||||
err(`${e.name} takes a name literal and a %ThreadSlots value`, e.loc);
|
||||
else if (key!.type.kind !== "object" || !keyed(key!.type.className))
|
||||
err(`${e.name} key ${typeKey(key!.type)} is not a ThreadSlots key class`, e.loc);
|
||||
else if (e.name === "slots.set") {
|
||||
const kind = value!.type.kind;
|
||||
if (
|
||||
kind !== "object" &&
|
||||
kind !== "record" &&
|
||||
kind !== "array" &&
|
||||
kind !== "map" &&
|
||||
kind !== "set"
|
||||
)
|
||||
err(`slots.set value ${typeKey(value!.type)} is not a ThreadSlots value type`, e.loc);
|
||||
if (e.type.kind !== "void" && !isSlots(e.type))
|
||||
err("slots.set has type void or %ThreadSlots", e.loc);
|
||||
} else if (e.name === "slots.get") {
|
||||
if (e.type.kind !== "union") err("slots.get has a V | undefined union type", e.loc);
|
||||
} else if (e.type.kind !== "bool") err(`${e.name} has type bool`, e.loc);
|
||||
return;
|
||||
}
|
||||
if (e.name === "threads.publish") {
|
||||
if (e.args.length !== 1) err("threads.publish takes exactly one argument", e.loc);
|
||||
for (const a of e.args) {
|
||||
|
||||
@@ -4216,6 +4216,13 @@
|
||||
"status": "static",
|
||||
"note": "UTC ISO formatting over constructed and stored Date values"
|
||||
},
|
||||
{
|
||||
"id": "stdlib.date.toUTCString",
|
||||
"kind": "stdlib",
|
||||
"name": "Date.prototype.toUTCString",
|
||||
"status": "static",
|
||||
"note": "RFC 7231 IMF-fixdate formatting over stored Date values"
|
||||
},
|
||||
{
|
||||
"id": "stdlib.date.valueOf",
|
||||
"kind": "stdlib",
|
||||
@@ -5293,9 +5300,8 @@
|
||||
"id": "stdlib.response.static.json",
|
||||
"kind": "stdlib",
|
||||
"name": "Response.json",
|
||||
"status": "unsupported",
|
||||
"code": "SC2020",
|
||||
"note": "Node 24.22.0 / Undici 7.29.1; Response static constructor-object operations have no compiler lowering"
|
||||
"status": "static",
|
||||
"note": "Node 24.22.0 / Undici 7.29.1; facets: webidl-conversion, error-shape; supported scope: data that crosses the checked-dynamic boundary, serialized with JSON.stringify (toJSON hooks included); ResponseInit as for the constructor; differential evidence: fixture:static-response-json"
|
||||
},
|
||||
{
|
||||
"id": "stdlib.response.static.redirect",
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile, deserializeModule } from "../src/index.js";
|
||||
import type { IrModule } from "../src/ir/ir.js";
|
||||
|
||||
// bignumber.js's clone(): `P = BigNumber.prototype = {...}` in a `var`
|
||||
// list above the constructor. The initializer lowers speculatively first,
|
||||
// which hoists the constructor and the helper it calls. Those bodies are
|
||||
// kept, so their failing statements must keep runtime fences; dropping
|
||||
// them left `isNum` without its return (an SC9002 trap at load).
|
||||
const source = `
|
||||
function clone(config) {
|
||||
var P = (Num.prototype = { constructor: Num, toString: null }),
|
||||
unit = config ? 2 : 1;
|
||||
function Num(v) {
|
||||
var x = this;
|
||||
if (!(x instanceof Num)) return new Num(v);
|
||||
x.v = isNum(v) ? 0 : v * unit;
|
||||
}
|
||||
function isNum(v) {
|
||||
return v instanceof Num || (!!v && v._isNum === true);
|
||||
}
|
||||
P._isNum = true;
|
||||
return Num;
|
||||
}
|
||||
const Num = clone();
|
||||
console.log(typeof Num);
|
||||
`;
|
||||
|
||||
async function lower(): Promise<IrModule> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-speculative-hoisted-"));
|
||||
try {
|
||||
const entry = join(dir, "main.mjs");
|
||||
const outPath = join(dir, "main.ir.json");
|
||||
await writeFile(entry, source);
|
||||
const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" });
|
||||
if (!result.ok)
|
||||
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
||||
return deserializeModule(await readFile(outPath, "utf8"));
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function fenceCodes(node: unknown, out: string[] = []): string[] {
|
||||
if (Array.isArray(node)) {
|
||||
for (const item of node) fenceCodes(item, out);
|
||||
} else if (node !== null && typeof node === "object") {
|
||||
const record = node as Record<string, unknown>;
|
||||
if (record["kind"] === "runtimeFence") out.push(String(record["code"]));
|
||||
for (const [key, value] of Object.entries(record)) if (key !== "loc") fenceCodes(value, out);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
test("speculatively hoisted JavaScript functions keep their statement fences", async () => {
|
||||
const mod = await lower();
|
||||
const isNum = mod.functions.find((fn) => fn.name.endsWith("_isNum"));
|
||||
expect(isNum).toBeDefined();
|
||||
expect(fenceCodes(isNum!.body)).toEqual(["SC1090"]);
|
||||
expect(mod.functions.flatMap((fn) => fenceCodes(fn.body))).not.toContain("SC9002");
|
||||
});
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5593-http-agent-total-socket-count.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5594-http-agent-header-limit.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5596-http-agent-keep-alive.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5597-http-agent-idle-sockets.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5598-http-global-agent.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5634-error-extra-props-inspect.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5635-error-extra-props-retry-loop.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5636-child-error-props.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5684-clear-optional-timer-handles.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5727-thread-slots-basics.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5728-thread-slots-workers.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5729-thread-slots-values.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5954-generic-optional-returns.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5985-self-capturing-closures.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6053-class-to-primitive.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6054-js-class-to-primitive.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6070-iterator-helpers-untyped.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user