diff --git a/benchmarks/regions/README.md b/benchmarks/regions/README.md index 457f059bea..0cc450abc1 100644 --- a/benchmarks/regions/README.md +++ b/benchmarks/regions/README.md @@ -7,6 +7,7 @@ Programs that build generations of `ProgramLifetime` objects and free each gener | `generations.ts` | Per-file syntax trees and binder symbols in one region per file, checker state (types, signatures, node and symbol links in paged tables, literal and union caches) in one region per generation. `edit` re-parses one file per generation, `all` re-parses every file. | | `compiler.ts` | A tokenizer, recursive-descent parser, binder and checker over generated sources, rebuilt from scratch in one region per build, like a watch mode that does not reuse syntax trees. | | `emit.ts` | A large live graph of ordinary objects with parent links, standing in for a type checker, and a per-file emit that builds its output in a region released right after. | +| `requests.ts` | Batches of concurrent async request handlers, each in its own region across its awaits: tokenize a JSON body, parse it into a syntax tree that points at its tokens, validate it into issues and an index, render a response, release the region. | ```console $ node benchmarks/regions/run.mjs --baseline=../scriptc-main --runs=7 diff --git a/benchmarks/regions/emit.ts b/benchmarks/regions/emit.ts index 630708361e..9fd15b8605 100644 --- a/benchmarks/regions/emit.ts +++ b/benchmarks/regions/emit.ts @@ -54,7 +54,7 @@ let level: Type[] = [new Type(id++, undefined)]; while (id < typeCount) { const next: Type[] = []; for (const t of level) for (let k = 0; k < 4 && id < typeCount; k++) next.push(new Type(id++, t)); - roots.push(...next); + for (const t of next) roots.push(t); level = next; } diff --git a/benchmarks/regions/requests.ts b/benchmarks/regions/requests.ts new file mode 100644 index 0000000000..6148eb25df --- /dev/null +++ b/benchmarks/regions/requests.ts @@ -0,0 +1,221 @@ +// An async request handler: each request tokenizes a JSON body, awaits (as +// if reading more of it), parses the tokens into a syntax tree whose nodes +// point back at their tokens, awaits again (as if loading reference data), +// validates the tree into issues held in an index, and renders a response. +// Requests run in batches of concurrent handlers, so the handlers of one +// batch interleave at every await. +// +// requests [plain] +// +// Each request runs in its own LifetimeRegion, released when its response +// is rendered; `plain` never constructs a region. +import { readFileSync } from "node:fs"; +import type { ProgramLifetime } from "@scriptc/types"; +import { LifetimeRegion } from "@scriptc/experimental"; + +const T_PUNCT = 0; +const T_STR = 1; +const T_NUM = 2; +const T_WORD = 3; + +class Token implements ProgramLifetime { + readonly kind: number; + readonly text: string; + readonly pos: number; + constructor(kind: number, text: string, pos: number) { + this.kind = kind; + this.text = text; + this.pos = pos; + } +} + +const N_OBJECT = 0; +const N_ARRAY = 1; +const N_STRING = 2; +const N_NUMBER = 3; +const N_LITERAL = 4; + +class JsonNode implements ProgramLifetime { + readonly kind: number; + readonly token: Token; + parent: JsonNode | undefined = undefined; + readonly keys: string[] = []; + readonly children: JsonNode[] = []; + constructor(kind: number, token: Token) { + this.kind = kind; + this.token = token; + } +} + +class Issue implements ProgramLifetime { + readonly path: string; + readonly node: JsonNode; + readonly message: string; + constructor(path: string, node: JsonNode, message: string) { + this.path = path; + this.node = node; + this.message = message; + } +} + +// ---- request bodies (deterministic) ---- + +function body(id: number, items: number): string { + let seed = 7 + id * 7919; + const rand = (n: number): number => { + seed = (seed * 1103515245 + 12345) % 2147483648; + return seed % n; + }; + const parts: string[] = []; + for (let i = 0; i < items; i++) { + const qty = rand(12) - 1; + const sku = rand(9) === 0 ? "" : `sku-${rand(5000)}`; + const tags = Array.from({ length: rand(4) }, (_, k) => `"t${k}${rand(10)}"`).join(","); + parts.push(`{"sku":"${sku}","qty":${qty},"price":${rand(10000) / 100},"gift":${rand(2) === 0},"tags":[${tags}]}`); + } + return `{"order":${id},"customer":"c${id % 97}","items":[${parts.join(",")}]}`; +} + +// ---- handler ---- + +function tokenize(text: string): Token[] { + const tokens: Token[] = []; + let i = 0; + while (i < text.length) { + const c = text.charCodeAt(i); + const start = i; + if (c === 34) { + i++; + while (text.charCodeAt(i) !== 34) i++; + i++; + tokens.push(new Token(T_STR, text.slice(start + 1, i - 1), start)); + } else if (c === 45 || (c >= 48 && c <= 57)) { + i++; + while (i < text.length && (text.charCodeAt(i) === 46 || (text.charCodeAt(i) >= 48 && text.charCodeAt(i) <= 57))) i++; + tokens.push(new Token(T_NUM, text.slice(start, i), start)); + } else if (c >= 97 && c <= 122) { + while (i < text.length && text.charCodeAt(i) >= 97 && text.charCodeAt(i) <= 122) i++; + tokens.push(new Token(T_WORD, text.slice(start, i), start)); + } else { + i++; + tokens.push(new Token(T_PUNCT, text[start]!, start)); + } + } + return tokens; +} + +class Parser { + private at = 0; + private readonly tokens: Token[]; + constructor(tokens: Token[]) { + this.tokens = tokens; + } + value(): JsonNode { + const token = this.tokens[this.at++]!; + if (token.kind === T_STR) return new JsonNode(N_STRING, token); + if (token.kind === T_NUM) return new JsonNode(N_NUMBER, token); + if (token.kind === T_WORD) return new JsonNode(N_LITERAL, token); + const node = new JsonNode(token.text === "{" ? N_OBJECT : N_ARRAY, token); + const close = token.text === "{" ? "}" : "]"; + while (this.tokens[this.at]!.text !== close) { + if (node.kind === N_OBJECT) { + node.keys.push(this.tokens[this.at]!.text); + this.at += 2; // the key and the colon + } + const child = this.value(); + child.parent = node; + node.children.push(child); + if (this.tokens[this.at]!.text === ",") this.at++; + } + this.at++; + return node; + } +} + +function member(node: JsonNode, key: string): JsonNode | undefined { + const i = node.keys.indexOf(key); + return i < 0 ? undefined : node.children[i]; +} + +function validate(root: JsonNode, issues: Issue[], index: Map): number { + let total = 0; + const items = member(root, "items")!; + for (let i = 0; i < items.children.length; i++) { + const item = items.children[i]!; + const path = `items[${i}]`; + const sku = member(item, "sku")!; + if (sku.token.text === "") issues.push(new Issue(`${path}.sku`, sku, "empty sku")); + else if (index.has(sku.token.text)) issues.push(new Issue(`${path}.sku`, sku, `duplicate of ${index.get(sku.token.text)!.token.pos}`)); + else index.set(sku.token.text, item); + const qty = Number(member(item, "qty")!.token.text); + if (qty <= 0) issues.push(new Issue(`${path}.qty`, member(item, "qty")!, "quantity must be positive")); + else total += qty * Number(member(item, "price")!.token.text); + const tags = member(item, "tags")!; + for (const tag of tags.children) if (tag.token.text.endsWith("0")) issues.push(new Issue(`${path}.tags`, tag, "reserved tag")); + } + return total; +} + +const hop = (): Promise => new Promise((resolve) => setImmediate(resolve)); + +async function handle(text: string): Promise { + const tokens = tokenize(text); + await hop(); + const root = new Parser(tokens).value(); + await hop(); + const issues: Issue[] = []; + const index = new Map(); + const total = validate(root, issues, index); + await hop(); + const first = issues[0]; + return `${member(root, "order")!.token.text} ${tokens.length} ${index.size} ${issues.length} ${total.toFixed(2)}` + + (first === undefined ? "" : ` ${first.path}@${first.node.token.pos}`); +} + +// ---- driver ---- + +function rssMb(key: string): number { + const m = new RegExp(`${key}:\\s*(\\d+) kB`).exec(readFileSync("/proc/self/status", "utf8")); + return m === null ? 0 : Number(m[1]) / 1024; +} + +const args = process.argv.slice(2); +const requests = Number(args[0] ?? "4000"); +const concurrency = Number(args[1] ?? "16"); +const items = Number(args[2] ?? "100"); +const useRegions = args[3] !== "plain"; + +async function serve(id: number): Promise { + const text = body(id, items); + if (!useRegions) return await handle(text); + const region = new LifetimeRegion(); + const response = await region.run(() => handle(text)); + region.release(); + return response; +} + +async function main(): Promise { + let hash = 0; + let lines = 0; + let sample = ""; + let rssFirst = 0; + const t0 = performance.now(); + for (let next = 0; next < requests; next += concurrency) { + const batch: Promise[] = []; + for (let k = next; k < next + concurrency && k < requests; k++) batch.push(serve(k)); + for (const response of await Promise.all(batch)) { + for (let i = 0; i < response.length; i++) hash = (hash * 31 + response.charCodeAt(i)) % 1000000007; + lines++; + if (lines === 1) sample = response; + } + if (next === 0) rssFirst = rssMb("VmRSS"); + } + const ms = performance.now() - t0; + console.log(`${lines} responses, hash ${hash}, first ${sample}`); + console.log( + `total ${ms.toFixed(1)} ms per-request ${((ms * 1000) / requests).toFixed(1)} us` + + ` rss-first ${rssFirst.toFixed(1)} MB rss-last ${rssMb("VmRSS").toFixed(1)} MB peak ${rssMb("VmHWM").toFixed(1)} MB`, + ); +} + +main(); diff --git a/benchmarks/regions/run.mjs b/benchmarks/regions/run.mjs index 1f1c4609f4..e698dc47d4 100644 --- a/benchmarks/regions/run.mjs +++ b/benchmarks/regions/run.mjs @@ -39,6 +39,8 @@ const CONFIGS = [ { name: "compiler-one-shot", program: "compiler.ts", args: ["400", "60", "1"] }, // Per-file emit regions next to a large live heap of ordinary objects. { name: "emit-files", program: "emit.ts", args: ["400000", "200", "2000"] }, + // Concurrent async request handlers, one region per request across its awaits. + { name: "requests-async", program: "requests.ts", args: ["4000", "16", "100"] }, ]; const work = join(tmpdir(), `scriptc-region-bench-${process.pid}`); @@ -93,7 +95,7 @@ for (const config of CONFIGS) { const lines = r.stdout.trim().split("\n"); outputs.set(name, lines[0].replace(/ gen0 .*/, "")); const metrics = {}; - for (const [, key, value] of lines.at(-1).matchAll(/([a-z0-9-]+) ([0-9.]+) (?:ms|MB)/g)) + for (const [, key, value] of lines.at(-1).matchAll(/([a-z0-9-]+) ([0-9.]+) (?:ms|us|MB)/g)) metrics[key] = Number(value); samples[name].push(metrics); } diff --git a/benchmarks/runtime/README.md b/benchmarks/runtime/README.md index ed9ee0532b..9635ee1f7a 100644 --- a/benchmarks/runtime/README.md +++ b/benchmarks/runtime/README.md @@ -19,6 +19,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea | `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 | | `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 | | `region-builds` | cpu | a watch-mode build loop over `LifetimeRegion`: each build binds a program in a new region and releases the previous one, which a cache keeps referenced for two builds | | `optional-fields` | cpu | records and class instances with `number \| undefined` and other scalar-union fields | | `record-boundaries` | cpu | records built from unchecked array reads passed to and returned from annotated functions | diff --git a/benchmarks/runtime/workloads.json b/benchmarks/runtime/workloads.json index c0d643a583..b95791a1d1 100644 --- a/benchmarks/runtime/workloads.json +++ b/benchmarks/runtime/workloads.json @@ -141,6 +141,13 @@ "2" ] }, + { + "name": "value-records", + "entry": "workloads/value-records.ts", + "args": [ + "1" + ] + }, { "name": "region-builds", "entry": "workloads/region-builds.ts", diff --git a/benchmarks/runtime/workloads/value-records.ts b/benchmarks/runtime/workloads/value-records.ts new file mode 100644 index 0000000000..922fb8e439 --- /dev/null +++ b/benchmarks/runtime/workloads/value-records.ts @@ -0,0 +1,93 @@ +// Records that hold references: a tokenizer that builds token records with +// string fields and passes them to helpers, an immutable reducer that +// updates its state with object spread, and tuple-returning helpers whose +// results are destructured at once. None of these temporaries outlives +// the statement or loop iteration that builds it. +interface Token { + kind: string; + text: string; + pos: number; +} +const KEYWORDS = ["let", "const", "if", "else", "for", "while", "return", "function"]; +function classify(text: string): string { + return KEYWORDS.includes(text) ? "keyword" : "ident"; +} +function token(kind: string, text: string, pos: number): Token { + return { kind, text, pos }; +} +function weight(t: Token): number { + return t.kind === "keyword" ? t.text.length * 3 : t.text.length + (t.pos & 7); +} +function tokenize(source: string): number { + let sum = 0; + let start = -1; + for (let i = 0; i <= source.length; i++) { + const c = i < source.length ? source.charCodeAt(i) : 32; + const word = (c >= 97 && c <= 122) || (c >= 48 && c <= 57) || c === 95; + if (word) { + if (start < 0) start = i; + continue; + } + if (start >= 0) { + const text = source.slice(start, i); + const t = token(classify(text), text, start); + sum += weight(t); + start = -1; + } + if (c !== 32 && c !== 10) { + const punct = { kind: "punct", text: source[i]!, pos: i }; + sum += weight(punct) + (punct.text === ";" ? 1 : 0); + } + } + return sum; +} + +interface State { + mode: string; + depth: number; + count: number; + last: string; +} +function step(s: State, text: string): State { + if (text === "{") return { ...s, depth: s.depth + 1, last: text }; + if (text === "}") return { ...s, depth: s.depth - 1, last: text }; + return s.mode === "code" ? { ...s, count: s.count + 1 } : { ...s, mode: "code", last: text }; +} +function reduce(texts: string[], rounds: number): number { + let total = 0; + for (let r = 0; r < rounds; r++) { + let state: State = { mode: "start", depth: 0, count: 0, last: "" }; + for (const text of texts) { + state = step(state, text); + if (state.depth > 3) state = { ...state, depth: 3 }; + } + total += state.count + state.depth + state.last.length + state.mode.length; + } + return total; +} + +function divmod(a: number, b: number): [number, number] { + return [Math.floor(a / b), a % b]; +} +function split(text: string, at: number): [string, string] { + return [text.slice(0, at), text.slice(at)]; +} +function tuples(n: number, texts: string[]): number { + let sum = 0; + for (let i = 0; i < n; i++) { + const [q, r] = divmod(i * 31, 17); + const [head, tail] = split(texts[i % texts.length]!, r % 3); + sum += q + r + head.length * 2 + tail.length; + } + return sum; +} + +const scaleArg = Number(process.argv[2] ?? "1"); +const line = "function step ( s , text ) { if ( text ) { return s ; } let x = s ; while ( x ) { x = x ; } }\n"; +const source = line.repeat(Math.floor(400 * scaleArg)); +const texts = source.split(" ").filter((t) => t.length > 0); +let lexed = 0; +for (let i = 0; i < 240; i++) lexed += tokenize(source); +const reduced = reduce(texts, Math.floor(600 * scaleArg)); +const pairs = tuples(Math.floor(2_000_000 * scaleArg), texts); +console.log("tokens", lexed, "reduce", reduced, "tuples", pairs); diff --git a/docs/content/docs/contracts.mdx b/docs/content/docs/contracts.mdx index 11737da590..d1bfb8cada 100644 --- a/docs/content/docs/contracts.mdx +++ b/docs/content/docs/contracts.mdx @@ -331,7 +331,7 @@ $ scriptc build graph.ts ## ProgramLifetime -`ProgramLifetime` marks a class whose instances a program keeps until it exits, such as the nodes of a syntax tree, the symbols of a symbol table or interned types. scriptc then never reference counts, collects or frees instances of the class and of its subclasses: they are never freed, every retain and release on them compiles to nothing, and the cycle collector neither walks them nor needs to trace the objects that point to them. Node.js behavior is unchanged; only memory use differs, because an instance the program drops is never reclaimed. +`ProgramLifetime` marks a class whose instances a program keeps until it exits, such as the nodes of a syntax tree, the symbols of a symbol table or interned types. scriptc then never reference counts, collects or frees instances of the class and of its subclasses: they are never freed, every retain and release on them compiles to nothing, and the cycle collector neither walks them nor needs to trace the objects that point to them. Node.js behavior is unchanged; only memory use differs, because an instance the program drops is never reclaimed. On Linux, the instances a worker thread creates are returned with the rest of its memory after it exits, unless the worker shared objects with other threads (see [cost](/docs/threads#cost)). ```ts:tree.ts import type { ProgramLifetime } from "@scriptc/types"; diff --git a/docs/content/docs/dependencies.mdx b/docs/content/docs/dependencies.mdx index f939522217..5ecc144c60 100644 --- a/docs/content/docs/dependencies.mdx +++ b/docs/content/docs/dependencies.mdx @@ -69,6 +69,10 @@ $ scriptc build app.ts --npm-static=three -o app >/dev/null Naming a package skips the declaration requirement above, but the package still has to compile. `--npm-static=auto` uses the default automatic selection. The option accepts a comma-separated list and can be repeated. +## CommonJS packages + +CommonJS packages compile with Node.js's module semantics. Each `require()` evaluates its module at the call, once, and later calls return the cached exports. A package can assign `module.exports` and run code before it requires its dependencies. If that code reads a `const` or `let` require binding before its `require` runs, the read throws the same `ReferenceError` as Node.js. A `var` binding that earlier code can reach is reported at build time. A `require()` of a JSON file, such as a package's own `package.json`, returns one object parsed at build time, shared by every module that requires it. A require cycle compiles when each of its modules assigns its exports before requiring anything, as semver's classes do, and uses the other modules only inside functions that its top-level code can't call. Other require cycles are reported at build time. The [limitations](/docs/limitations) page lists the supported `require` forms. + ## Unsupported code inside a package A JavaScript package can contain operations scriptc can't compile, often in functions your program never calls. These don't fail the build: each one compiles to code that throws a runtime error if it ever runs. Coverage reports list them as deferred sites, so review the list and test the parts of the package your program uses. diff --git a/docs/content/docs/experimental.mdx b/docs/content/docs/experimental.mdx index ac5891ec97..6a0fb53efe 100644 --- a/docs/content/docs/experimental.mdx +++ b/docs/content/docs/experimental.mdx @@ -73,9 +73,9 @@ scriptc region heap passes: 0
new LifetimeRegion()
Creates a region. Creating a region also retries the released regions that could not be freed yet.
region.run(fn)
-
Calls fn with the region as the current region and returns its result. ProgramLifetime instances created during the call belong to the region. Calls nest: a run inside the callback makes another region current until it returns, and the previous region is current again however the callback finishes, including when it throws.
+
Calls fn with the region as the current region and returns its result. ProgramLifetime instances created during the call belong to the region, and so do those that async functions called during it create after an await (see Async work). Calls nest: a run inside the callback makes another region current until it returns, and the previous region is current again however the callback finishes, including when it throws.
region.release()
-
Requests that the region be freed. scriptc frees it as soon as nothing outside the region refers to any of its instances, which may be at once, at a later release or region creation, or never. A region is never freed while a run call on it is active. After release(), run still calls its callback, and the instances it creates belong to the default region.
+
Requests that the region be freed. scriptc frees it as soon as nothing outside the region refers to any of its instances, which may be at once, at a later release or region creation, or never. A region is never freed while a run call on it is active, or while async work started inside it has not finished. After release(), run still calls its callback, and the instances it creates belong to the default region.
`ProgramLifetime` instances created outside every `run` call belong to the default region, which is never freed. Ordinary objects (instances of classes without the contract, arrays, maps, closures) are reference counted as always; a region frees the ones that only its instances refer to. @@ -92,7 +92,62 @@ A released region stays allocated, and its instances stay usable, while anything - an instance of the default region, which is never freed; - a promise, generator or `Error` that holds an instance, until it is no longer referenced. -Work that resumes after an `await` inside the callback runs outside the region, so the instances it creates belong to the default region and keep the instances they refer to allocated. +### Async work + +An async function called inside `run` keeps the region for its whole run: the instances it creates after each `await` belong to the region too, as do those of the async functions it calls. A server can give every request its own region, even while requests interleave at their awaits: + +```ts:requests.ts +import type { ProgramLifetime } from "@scriptc/types"; +import { LifetimeRegion } from "@scriptc/experimental"; + +class Part implements ProgramLifetime { + readonly text: string; + previous: Part | undefined = undefined; + constructor(text: string) { + this.text = text; + } +} + +const read = (): Promise => new Promise((resolve) => setImmediate(resolve)); + +async function handle(id: number): Promise { + let last: Part | undefined; + for (let i = 0; i < 1000; i++) { + if (i % 100 === 0) await read(); // the next chunk of the request body + const part = new Part(`${id}.${i}`); + part.previous = last; + last = part; + } + return `request ${id}: last part ${last!.text}`; +} + +async function serve(id: number): Promise { + const region = new LifetimeRegion(); + try { + return await region.run(() => handle(id)); + } finally { + region.release(); + } +} + +Promise.all([1, 2, 3].map(serve)).then((responses) => { + for (const response of responses) console.log(response); +}); +``` + +Each request's 1000 parts are freed when the request releases its region: + +```console +$ scriptc build requests.ts -o requests >/dev/null +$ SCRIPTC_REGION_STATS=1 ./requests >/dev/null +scriptc region 1: freed 1000 objects, 0 owned objects, 1 chunks (2.0 MiB); cycles 0.0 ms, walk 0.0 ms, free 0.0 ms +scriptc region 2: freed 1000 objects, 0 owned objects, 1 chunks (2.0 MiB); cycles 0.0 ms, walk 0.0 ms, free 0.0 ms +scriptc region 3: freed 1000 objects, 0 owned objects, 1 chunks (2.0 MiB); cycles 0.0 ms, walk 0.0 ms, free 0.0 ms +scriptc regions: 3 created, 3 released, 3 freed, 0 pinned, 0 kept +scriptc region heap passes: 0 +``` + +A region released while async work started inside it is still pending stays allocated until that work has finished, and the work keeps creating its instances in the region. Callbacks that the event loop calls (timers, `queueMicrotask`, event listeners) and the bodies of async generators run outside the region; synchronous generators run in the region of the code that resumes them. ### Threads @@ -102,7 +157,7 @@ Each thread has its own regions and its own default region. A region belongs to Programs that never construct a `LifetimeRegion` compile exactly as they would without the package. In programs that do, every retain and release of a `ProgramLifetime` instance that belongs to a region updates a count of the region chunk that holds it. Instances of the default region are not counted, and until the program creates its first region, a retain or release only tests whether counting has started. -Freeing a region visits each of its instances and the ordinary objects they own. A region that the check would keep may be kept only by unreachable cycles of ordinary objects that refer into it, so before keeping it scriptc runs the cycle collector over the thread's heap and checks again. +Freeing a region visits each of its instances and the ordinary objects they own. Each thread keeps up to 8 MiB of the memory of freed regions for its next regions, so a program that frees a region per request or per batch does not map and fault in new pages every time. A region that the check would keep may be kept only by unreachable cycles of ordinary objects that refer into it, so before keeping it scriptc runs the cycle collector over the thread's heap and checks again. `SCRIPTC_REGION_STATS=1` prints one line per release attempt, freed, `kept` (with the number of references the check could not explain) or `pinned` (published), and at exit a summary per thread and the number of those cycle-collector passes over the thread's heap. diff --git a/docs/content/docs/limitations.mdx b/docs/content/docs/limitations.mdx index ed2c5b1ec3..544bd2b2dd 100644 --- a/docs/content/docs/limitations.mdx +++ b/docs/content/docs/limitations.mdx @@ -32,6 +32,8 @@ title: "Limitations" ../limitations/language-features/dynamic-imports.mdx +../limitations/language-features/commonjs-require.mdx + ### Types and class definitions ../limitations/types-and-class-definitions/bigint.mdx diff --git a/docs/content/docs/threads.mdx b/docs/content/docs/threads.mdx index 7b394e09fd..6f9b5f062d 100644 --- a/docs/content/docs/threads.mdx +++ b/docs/content/docs/threads.mdx @@ -234,6 +234,8 @@ Without `@scriptc/threads`, a scriptc executable also shares a message of untype Starting a worker thread takes tens of microseconds plus the worker's own module initialization, since every worker initializes the program's modules again, as in Node.js. A worker that exits frees its heap first, which takes time proportional to what it still holds. `SCRIPTC_WORKER_STATS=1` prints, as each worker exits, how long it took to start, to run its script and to exit. +On Linux, a worker that exits without having shared an object it allocated with another thread returns all of its memory, including objects it never freed, such as `ProgramLifetime` instances, once no other worker thread is running. A worker shares objects when, for example, it sends a published value or frozen plain data, which other threads read by reference, or stores a value in an `InternTable`. The memory of a worker that shared stays allocated, as on other platforms. Set `SCR_SA_DISCARD=0` in the environment to keep the memory of every worker. + `publish` visits each object of the graph once and marks it immortal, so publication takes time proportional to the graph. Published objects then cost nothing on later sends, and reference counting and the cycle collector skip them. Stores into the types a program publishes carry one extra load and branch. Threads that add to one shared collection of published values while they run can use an [`InternTable`](/docs/experimental#interntable) from `@scriptc/experimental`. diff --git a/docs/content/docs/wasm.mdx b/docs/content/docs/wasm.mdx index a39c31e34a..b498eccf79 100644 --- a/docs/content/docs/wasm.mdx +++ b/docs/content/docs/wasm.mdx @@ -96,11 +96,13 @@ After initialization, counter_step(2) returns 2, and a i64, u64JavaScript BigInt. The existing library integer checks and proofs restrict values to the exact JavaScript number range. string, bytes parametersTwo i32 arguments: byte offset and byte length. Strings use UTF-8. Entry parameters are copied into runtime-owned values. string, bytes resultsTwo extra i32 arguments identify writable four-byte output slots. The function writes the result pointer and byte length there, in little-endian order. + cstring parametersOne i32 argument: the byte offset of NUL-terminated UTF-8 text. The text is copied into a runtime-owned string; malformed UTF-8 becomes U+FFFD. A zero pointer traps with SC4012. + cstring resultsAn i32 result: the byte offset of NUL-terminated UTF-8 text in the result arena. A result containing U+0000 traps with SC4015 instead of being truncated. voidNo result. -The native library rules about directions still apply: u8/u32/i32 are export parameters and callback scalars; i64/u64 are export-only. Callback results must be scalars or void. Callback string/byte parameters are borrowed only for the duration of that callback; copy them before retaining them in the host. +The native library rules about directions still apply: u8/u32/i32 are export parameters and callback scalars; i64/u64 are export-only. Callback results must be scalars or void. Callback string/byte parameters are borrowed only for the duration of that callback; copy them before retaining them in the host. A cstring callback parameter is one i32 pointer to NUL-terminated text, borrowed the same way; a value containing U+0000 traps with SC4015. The cstring class requires "profile_format": 2. In native libraries it is a const char * with the same rules. Allocate host input buffers and output slots with scriptc_alloc(size); a zero result indicates allocation failure. Release each allocation exactly once with scriptc_free(pointer). Only free pointers returned by scriptc_alloc. These allocations remain live across application calls, initialization, and collection until freed. diff --git a/docs/content/limitations/builtin-apis/arraybuffers-and-typed-arrays.mdx b/docs/content/limitations/builtin-apis/arraybuffers-and-typed-arrays.mdx index 81dfded259..59a61518d9 100644 --- a/docs/content/limitations/builtin-apis/arraybuffers-and-typed-arrays.mdx +++ b/docs/content/limitations/builtin-apis/arraybuffers-and-typed-arrays.mdx @@ -1 +1 @@ -- **ArrayBuffers and typed arrays:** Fixed-length ArrayBuffers support shared typed-array and DataView storage. Resizing, transferring, Float16 arrays, and BigInt arrays are unsupported. Numeric typed arrays and Buffers preserve their kind and shared storage through `unknown`, including when nested in records and arrays. Numeric typed-array constructors can be stored and used with `new`, including constructors read from an array's `constructor` property. JavaScript numeric-array parameters and returns preserve typed-array storage. `Buffer.from`, `Buffer.allocUnsafe`, and `Buffer.concat` return views into a per-thread pool of `Buffer.poolSize` bytes (64 KiB by default) when the size is below `Buffer.poolSize >>> 1`, as in Node; a pool slab stays in memory while any of its Buffers does. Node's own startup code allocates from the first slab, so the `byteOffset` of Buffers in that slab differs from Node's. Buffers returned by other Node APIs, such as `fs.readFileSync` and `zlib`, own their `ArrayBuffer`, where Node allocates small ones from the pool. +- **ArrayBuffers and typed arrays:** Fixed-length ArrayBuffers support shared typed-array and DataView storage. Resizing, transferring, and BigInt arrays are unsupported. `Float16Array` stores IEEE 754 half-precision values, rounding each stored number to nearest-even as Node does; `Math.f16round`, `DataView.prototype.getFloat16` and `setFloat16`, and `util.types.isFloat16Array` compile. Numeric typed arrays and Buffers preserve their kind and shared storage through `unknown`, including when nested in records and arrays. Numeric typed-array constructors can be stored and used with `new`, including constructors read from an array's `constructor` property. JavaScript numeric-array parameters and returns preserve typed-array storage. `Buffer.from`, `Buffer.allocUnsafe`, and `Buffer.concat` return views into a per-thread pool of `Buffer.poolSize` bytes (64 KiB by default) when the size is below `Buffer.poolSize >>> 1`, as in Node; a pool slab stays in memory while any of its Buffers does. Node's own startup code allocates from the first slab, so the `byteOffset` of Buffers in that slab differs from Node's. Buffers returned by other Node APIs, such as `fs.readFileSync` and `zlib`, own their `ArrayBuffer`, where Node allocates small ones from the pool. diff --git a/docs/content/limitations/builtin-apis/date.mdx b/docs/content/limitations/builtin-apis/date.mdx index 5c465b315c..db5278c536 100644 --- a/docs/content/limitations/builtin-apis/date.mdx +++ b/docs/content/limitations/builtin-apis/date.mdx @@ -1 +1 @@ -- **Date:** Date unions, year/month constructors, setters, statically typed TypeScript identity comparisons, thrown Date values, and locale/string formatters are unsupported. Zero-argument and single number/string constructors, storage and passing, `getTime`, `valueOf`, `toISOString`, calendar getters, `getTimezoneOffset`, and single-string `Date.parse` compile. JavaScript Dates retain identity through checked storage and support Date-copy construction and JSON serialization. See [Dates and timezones](#dates-and-timezones) for parsing restrictions. +- **Date:** Date unions, year/month constructors, setters, statically typed TypeScript identity comparisons, thrown Date values, and locale/string formatters are unsupported. Zero-argument and single number/string constructors, storage and passing, `getTime`, `valueOf`, `toISOString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), calendar getters, `getTimezoneOffset`, and single-string `Date.parse` compile. JavaScript Dates retain identity through checked storage and support Date-copy construction and JSON serialization. A TypeScript Date converted to `unknown` becomes a Date with the same time value; each conversion creates a new Date, so two `unknown` values converted from one TypeScript Date are not `===`. See [Dates and timezones](#dates-and-timezones) for parsing restrictions. diff --git a/docs/content/limitations/builtin-apis/eventemitter.mdx b/docs/content/limitations/builtin-apis/eventemitter.mdx index 0b44cd5493..c4435f4c70 100644 --- a/docs/content/limitations/builtin-apis/eventemitter.mdx +++ b/docs/content/limitations/builtin-apis/eventemitter.mdx @@ -1 +1 @@ -- **EventEmitter:** Literal event names use fixed argument tuples. Computed string names require a constant prefix or suffix that excludes `error`, meta events, and stream-internal events; their arguments must match the exact arity and support checked conversion. Unrestricted names, symbols, names that may reach internal events, and typed `listeners()`/`rawListeners()` results for names with possible computed registrations are unsupported. +- **EventEmitter:** Literal event names use fixed argument tuples. Computed string names require a constant prefix or suffix that excludes `error`, meta events, and stream-internal events; their arguments must match the exact arity and support checked conversion. Unrestricted names, symbols, names that may reach internal events, and typed `listeners()`/`rawListeners()` results for names with possible computed registrations are unsupported. Reading `defaultMaxListeners` through a CommonJS destructured or member binding (`const { defaultMaxListeners } = require("events")`) is unsupported. diff --git a/docs/content/limitations/builtin-apis/exit-status.mdx b/docs/content/limitations/builtin-apis/exit-status.mdx index e2b681daf8..f13782ff2c 100644 --- a/docs/content/limitations/builtin-apis/exit-status.mdx +++ b/docs/content/limitations/builtin-apis/exit-status.mdx @@ -1 +1 @@ -- **Exit status:** Numeric `process.exitCode = value` writes in statement position set the implicit exit status, which `process.exit()` uses when called without an argument. Reads, resets, and assignments used as values are unsupported. +- **Exit status:** Numeric `process.exitCode = value` writes in statement position set the implicit exit status, which `process.exit()` uses when called without an argument. `process.exit(code)` and `process.exitCode` throw a `RangeError` (`ERR_OUT_OF_RANGE`) for a code that is not a safe integer, and convert other codes to int32, as Node does. Reads, resets, and assignments used as values are unsupported. diff --git a/docs/content/limitations/builtin-apis/regular-expressions.mdx b/docs/content/limitations/builtin-apis/regular-expressions.mdx index 4578cfc937..a765bbe3cf 100644 --- a/docs/content/limitations/builtin-apis/regular-expressions.mdx +++ b/docs/content/limitations/builtin-apis/regular-expressions.mdx @@ -1 +1 @@ -- **Regular expressions:** Native checked storage preserves identity, `source`, `flags`, flag properties, and stateful `test`. Static `exec`, global `match`, and numeric `lastIndex` access use the native matcher. Runtime constructors accept checked strings and native regex copies; `replace` and `replaceAll` accept native regex values with string templates or callbacks, including captures, named groups, and UTF-16 offsets. Non-numeric `lastIndex`, detached methods, and `exec` through an untyped receiver are unsupported. The `d` (match indices) and `v` (unicode sets) flags are unsupported: regex literals and `new RegExp` calls with a literal flag string containing them fail to compile with SC1120, and `new RegExp` with a computed flag string containing them throws an error naming the flag. +- **Regular expressions:** Native checked storage preserves identity, `source`, `flags`, flag properties, and stateful `test`, and the flag getters (`global`, `unicodeSets`, and the rest) compile on statically typed regexes. Static `exec`, global `match`, and numeric `lastIndex` access use the native matcher. Runtime constructors accept checked strings and native regex copies; `replace` and `replaceAll` accept native regex values with string templates or callbacks, including captures, named groups, and UTF-16 offsets. Non-numeric `lastIndex`, detached methods, and `exec` through an untyped receiver are unsupported. The `v` (unicode sets) flag supports class set operations (`--`, `&&`), nested classes, `\q{...}` string literals, and properties of strings such as `\p{RGI_Emoji}`. `new RegExp` with an invalid pattern throws a `SyntaxError` whose message comes from the native engine and can differ from Node's. The `d` (match indices) flag is unsupported: regex literals and `new RegExp` calls with a literal flag string containing it fail to compile with SC1120, and `new RegExp` with a computed flag string containing it throws an error naming the flag. diff --git a/docs/content/limitations/builtin-apis/test-mocks.mdx b/docs/content/limitations/builtin-apis/test-mocks.mdx index c9e84a80d5..a0c63cd980 100644 --- a/docs/content/limitations/builtin-apis/test-mocks.mdx +++ b/docs/content/limitations/builtin-apis/test-mocks.mdx @@ -1 +1,2 @@ -- **Test mocks:** `mock.fn`, `mock.method`, `mock.restoreAll` and `mock.reset` compile, on the module's `mock` and on `t.mock`. `mock.method` mocks a function-valued field of a record, such as an object literal or an interface-typed object. Class instances, builtin objects, and object literals whose methods use `this` throw `SC2020`. Calls through the object record it as `this`, including calls of a method read off the object and called on its own, where Node records `undefined`. `object.method === mock` is `false`, because the field holds a wrapper of the mock. `mock.fn()` without an original, `mock.getter`, `mock.setter`, `mock.property`, `mock.module`, `mock.timers` and mock options throw `SC2020`. +- **Test mocks:** `mock.fn`, `mock.method`, `mock.restoreAll` and `mock.reset` compile, on the module's `mock` and on `t.mock`. `mock.method` mocks a function-valued field of a record, such as an object literal or an interface-typed object. Class instances, builtin objects, and object literals whose methods use `this` throw `SC2020`. Calls through the object record it as `this`, including calls of a method read off the object and called on its own, where Node records `undefined`. `object.method === mock` is `false`, because the field holds a wrapper of the mock. `mock.fn()` without an original, `mock.getter`, `mock.setter`, `mock.property`, `mock.module` and mock options throw `SC2020`. +- **Mock timers:** `mock.timers.enable`, `tick`, `runAll`, `reset`, `setTime` and `[Symbol.dispose]()` compile, on the module's `mock` and on `t.mock`, for the `setTimeout`, `setInterval`, `setImmediate`, `Date`, `scheduler.wait` and `AbortSignal.timeout` APIs. `enable()` takes no options or an object literal whose `apis`, when given, is an array literal. Options and `apis` lists that are not literals, and the `MockTimers` object used as a value, throw `SC2020`. As in Node, the global timer functions, `Date.now()`, `new Date()` and `scheduler.wait` are mocked, and other calls through named and namespace imports of `node:timers` and `node:timers/promises` use real timers. As in Node, once a reset gives `scheduler.wait` back, calling it throws `ERR_INVALID_THIS`. While `scheduler.wait` is mocked, calling it with a `signal` option throws an `SC2020` error. A mock timer's handle converts to a negative number, where Node's mock `Timeout` converts to `NaN`. When two trackers have timers enabled, clearing a timer through the tracker that did not create it leaves the timer queued, where Node removes the entry at the same queue position in the clearing tracker. diff --git a/docs/content/limitations/builtin-apis/test-runner.mdx b/docs/content/limitations/builtin-apis/test-runner.mdx index be138b3ca1..7a50901db2 100644 --- a/docs/content/limitations/builtin-apis/test-runner.mdx +++ b/docs/content/limitations/builtin-apis/test-runner.mdx @@ -1 +1,2 @@ - **Concurrent tests:** with `{ concurrency }` above 1, subtests run on their own fibers, and results are reported in declaration order, as in Node. Concurrent bodies resume in Node's order around `beforeEach` hooks and nested suites. Other runner steps, such as `afterEach` and `after` hooks, take no microtask turns between bodies, so code that observes the interleaving of concurrent bodies across those steps can see a different order than Node. +- **Snapshots:** `t.assert.snapshot` and `t.assert.fileSnapshot` compile, with Node's default serializer, `JSON.stringify(value, null, 2)`, or an `options.serializers` array literal. A compiled test reads the snapshot file Node writes beside the entry file, `.snapshot`, at the entry file's path when the program was compiled. Running the binary with Node's flag `--test-update-snapshots` as its first argument writes the snapshot files instead; as in Node, the flag is kept out of `process.argv` and appears in `process.execArgv`. Snapshot files are read in the form Node writes them, `` exports[`key`] = `value`; `` statements. The `snapshot.setDefaultSnapshotSerializers` and `snapshot.setResolveSnapshotPath` module functions throw `SC2020`. diff --git a/docs/content/limitations/language-features/async-iteration.mdx b/docs/content/limitations/language-features/async-iteration.mdx index ee570f17dd..f183772995 100644 --- a/docs/content/limitations/language-features/async-iteration.mdx +++ b/docs/content/limitations/language-features/async-iteration.mdx @@ -1 +1 @@ -- **Async iteration:** Async `yield*` delegation, stored Web or Node iterator handles, and general structural async-iterator objects are unsupported. Typed async generators support lazy execution, `await`, direct `yield`, queued `.next()`/`.return()`/`.throw()` requests, and `for await`. `for await` also accepts Node `Readable` streams, Web `ReadableStream` values, and compiled class iterators whose zero-parameter `next()` returns a promise of a `{ value, done? }` record. Abrupt completion runs cleanup, including literal `destroyOnReturn` and `preventCancel` options. The `node:timers/promises` `setInterval(delay, value)` iterator compiles without AbortSignal options. `Array.fromAsync` accepts arrays of values or of promises with an optional mapping function; async generator sources, a `thisArg`, and arrays that mix values and promises are unsupported. +- **Async iteration:** Async `yield*` delegation, stored Web or Node iterator handles, and general structural async-iterator objects are unsupported. Typed async generators support lazy execution, `await`, direct `yield`, queued `.next()`/`.return()`/`.throw()` requests, and `for await`. `for await` also accepts Node `Readable` streams, Web `ReadableStream` values, and compiled class iterators whose zero-parameter `next()` returns a promise of a `{ value, done? }` record. Abrupt completion runs cleanup, including literal `destroyOnReturn` and `preventCancel` options. The `node:timers/promises` `setInterval(delay, value)` iterator compiles without AbortSignal options. `Array.fromAsync` accepts arrays of values or of promises and typed async generators, with an optional mapping function; a `thisArg` and arrays that mix values and promises are unsupported. diff --git a/docs/content/limitations/language-features/commonjs-require.mdx b/docs/content/limitations/language-features/commonjs-require.mdx new file mode 100644 index 0000000000..5784994103 --- /dev/null +++ b/docs/content/limitations/language-features/commonjs-require.mdx @@ -0,0 +1 @@ +- **CommonJS `require`:** Literal `require()` calls of compiled modules, supported builtins, and JSON documents compile. Reading a top-level `const` or `let` require binding before its `require` runs throws the same `ReferenceError` as Node.js. A `var` binding that code above its `require` can reach is unsupported, and so is a read that compiles to no checkable expression, such as exporting the binding before the `require`. JSON documents are parsed at build time, and every `require()` of a document returns the same object. Destructuring the exports of a module supports plain and renamed properties, plus nested patterns for builtin sub-modules such as `util.types`. Any pattern works on a JSON document. Defaults, rest elements, and nested patterns over other modules are unsupported. A require cycle compiles when each of its modules assigns all of its exports before its first `require` and uses the other modules of the cycle only inside functions that its top-level code can't call. Other require cycles are rejected with `SC1016`. diff --git a/docs/content/limitations/language-features/resource-disposal.mdx b/docs/content/limitations/language-features/resource-disposal.mdx index 5163d53226..d3df4138ee 100644 --- a/docs/content/limitations/language-features/resource-disposal.mdx +++ b/docs/content/limitations/language-features/resource-disposal.mdx @@ -1 +1 @@ -- **Resource disposal:** Top-level and switch-clause `using` declarations, and `DisposableStack` constructors are unsupported. Block- and function-scoped `using` and `await using` support compiled classes with zero-parameter disposal methods, object literals and other `Disposable`- or `AsyncDisposable`-typed values, and supported FileHandle, timer, immediate, child-process, and readline handles. A class instance reached through a `Disposable`- or `unknown`-typed value throws an SC2020 error when its declaration runs; declare the variable with the class type. Array `for (using ... of ...)` loops also compile. `SuppressedError` values expose `error` and `suppressed`; `extends SuppressedError` and casting `unknown` to `SuppressedError` are unsupported (narrow with `instanceof`). +- **Resource disposal:** Top-level and switch-clause `using` declarations, and `AsyncDisposableStack`, are unsupported. Block- and function-scoped `using` and `await using` support compiled classes with zero-parameter disposal methods, object literals and other `Disposable`- or `AsyncDisposable`-typed values, and supported FileHandle, timer, immediate, child-process, and readline handles. A class instance reached through a `Disposable`- or `unknown`-typed value throws an SC2020 error when its declaration runs; declare the variable with the class type. Array `for (using ... of ...)` loops also compile. `SuppressedError` values expose `error` and `suppressed`; `extends SuppressedError` and casting `unknown` to `SuppressedError` are unsupported (narrow with `instanceof`). `DisposableStack` supports `use`, `adopt`, `defer`, `move`, `dispose`, `disposed` and `[Symbol.dispose]`, and works with `using`; `use` accepts class instances, object literals and other `Disposable`-typed values, and refuses unions that include class instances (narrow the value first). Disposal runs in reverse order and raises Node's `SuppressedError` chain when more than one callback throws. diff --git a/docs/content/limitations/native-worker-threads.mdx b/docs/content/limitations/native-worker-threads.mdx index 0527c24b02..0510936d69 100644 --- a/docs/content/limitations/native-worker-threads.mdx +++ b/docs/content/limitations/native-worker-threads.mdx @@ -1,6 +1,6 @@ -`node:worker_threads` runs workers as native threads. Worker entry points must resolve at build time: use a source-relative `new URL("./worker.ts", import.meta.url)`, the current module URL, an absolute path, or CommonJS `__filename`. Each worker initializes its own module state, event loop, environment snapshot, and reference-counted heap. Nested workers inherit their parent's environment snapshot. Supported constructor options are `workerData`, `argv`, and a `transferList` of MessagePorts; runtime paths, `eval`, loaders, custom stdio, shared environments, and resource limits are unsupported. Worker argument strings containing null bytes are also unsupported. +`node:worker_threads` runs workers as native threads. Worker entry points must resolve at build time: use a source-relative `new URL("./worker.ts", import.meta.url)`, the current module URL, an absolute path, or CommonJS `__filename`. Each worker initializes its own module state, event loop, environment snapshot, and reference-counted heap. Nested workers inherit their parent's environment snapshot. Supported constructor options are `workerData`, `argv`, a `transferList` of MessagePorts, `name` (reported by `threadName` and `worker.threadName`), `stdout`, `stderr` and `stdin`, and `resourceLimits.stackSizeMb`, which sets the worker thread's stack size (at least 192 KiB; without it a worker's stack is 8 MiB, where Node's is 4 MiB). The `resourceLimits` heap limits (`maxOldGenerationSizeMb`, `maxYoungGenerationSizeMb`, and `codeRangeSizeMb`), `worker.resourceLimits` while the worker runs, and `resourceLimits` read inside a worker are refused; in the main thread `resourceLimits` is `{}`, and after a worker exits `worker.resourceLimits` is `{}`, as in Node. With the `stdout` or `stderr` option, the worker's writes to that stream reach `worker.stdout` or `worker.stderr`, a Readable that ends after the worker's `'exit'` event; its chunks arrive in order with the worker's other events. Without the option a worker writes to the process's own output, and reading `worker.stdout` or `worker.stderr` is refused. With the `stdin` option, `worker.stdin` is a Writable whose chunks the worker reads from `process.stdin`, in order with the worker's other events, and `process.stdin` ends when `worker.stdin` ends; without it `worker.stdin` is `null` and the worker's `process.stdin` ends without data, as in Node. Runtime paths, `eval`, loaders, and shared environments are unsupported. `setEnvironmentData` and `getEnvironmentData` keep a map per thread with SameValueZero keys; each new Worker starts from a clone of its parent's entries taken by the constructor, which throws `DataCloneError` when an entry cannot be cloned. Worker argument strings containing null bytes are also unsupported. -Workers support `online`, `message`, `error`, and `exit` events, `postMessage`, `terminate`, `ref`, and `unref`. `new MessageChannel()` from `node:worker_threads` makes two entangled MessagePorts, in one thread or across threads once a port is transferred, with `receiveMessageOnPort`. Port messages arrive in Node's order: each port's queued messages as one batch before `setImmediate` callbacks, a port's own `'close'` after the poll phase, and the other port's `'close'` after its queued messages while the loop keeps running. A port's `onmessage` property, `addEventListener`, `receiveMessageOnPort(parentPort)`, and `BroadcastChannel` are unsupported. The parent port supports message listeners, `postMessage`, `close`, `start`, `ref`, `unref`, and `hasRef`. Messages clone primitives, plain object graphs, arrays, errors, ArrayBuffers, typed arrays, and DataViews, preserving cycles and aliases. Program class instances arrive as plain objects with their own data properties, without their prototype, methods, accessors, or private fields, as in Node. A MessagePort travels with a message when the message's transfer list (an array or `{ transfer }`) names it, and the sending port is unusable afterwards; a port in a message without that listing throws `DataCloneError`. Other transferable values, functions, proxies, and other native handles are refused. SharedArrayBuffers share their backing storage while retaining separate wrappers in each thread. Graphs made immutable with `@scriptc/threads` are passed by reference instead of cloned; see [Sharing Data Between Workers](/docs/threads). +Workers support `online`, `message`, `error`, and `exit` events, `postMessage`, `terminate`, `ref`, and `unref`. `worker[Symbol.asyncDispose]()` terminates the worker and resolves with `undefined` after its `exit` event, so `await using` disposes a Worker at the end of its block. A worker's uncaught exception reaches its `error` listeners as Node's error serializer sends it: an Error keeps the nearest of `Error` and its six native subclasses (an `AggregateError` arrives as an `Error` named `AggregateError`) along with its name, message, cause, and other data properties and their enumerability, and other thrown values clone as messages do. An unhandled rejection whose reason is not error-like arrives as an `UnhandledPromiseRejection` Error with code `ERR_UNHANDLED_REJECTION` quoting the reason. A thrown value that does not clone, such as a function, arrives as an `ERR_WORKER_UNSERIALIZABLE_ERROR` Error, where Node sends its `util.inspect` string. `new MessageChannel()` from `node:worker_threads` makes two entangled MessagePorts, in one thread or across threads once a port is transferred, with `receiveMessageOnPort`. Port messages arrive in Node's order: each port's queued messages as one batch before `setImmediate` callbacks, a port's own `'close'` after the poll phase, and the other port's `'close'` after its queued messages while the loop keeps running. `receiveMessageOnPort(parentPort)` in a worker takes the parent's next message. A port's `onmessage` property and `addEventListener` are unsupported. `BroadcastChannel` delivers each message to every other open channel with the same name, in any thread, as a `'message'` `MessageEvent` through `onmessage` and `addEventListener`; an open channel keeps the process alive until `close()` or `unref()`. A `messageerror` event is never dispatched. The parent port supports message listeners, `postMessage`, `close`, `start`, `ref`, `unref`, and `hasRef`. Messages clone primitives, plain object graphs, arrays, errors, ArrayBuffers, typed arrays, and DataViews, preserving cycles and aliases. Program class instances arrive as plain objects with their own data properties, without their prototype, methods, accessors, or private fields, as in Node. `markAsUntransferable` marks objects for the current thread, and a marked ArrayBuffer or MessagePort in a transfer list throws `DataCloneError`, as does the ArrayBuffer of a pooled Buffer; marked objects other than ArrayBuffers stay alive until their thread exits. `markAsUncloneable` marks a plain object, an error, or a class instance, and `structuredClone` and `postMessage` then throw `DataCloneError` for it, also where it is nested; as in Node, the mark has no effect on primitives, arrays, typed arrays, and ArrayBuffers. A plain object that the compiler copies into a container with a fixed element type, such as an array literal whose elements all have one shape, is a separate object in the copy, which is not marked. Marked objects stay alive until their thread exits. A MessagePort travels with a message when the message's transfer list (an array or `{ transfer }`) names it, and the sending port is unusable afterwards; a port in a message without that listing throws `DataCloneError`. Other transferable values, functions, proxies, and other native handles are refused. SharedArrayBuffers share their backing storage while retaining separate wrappers in each thread. Graphs made immutable with `@scriptc/threads` are passed by reference instead of cloned; see [Sharing Data Between Workers](/docs/threads). SharedArrayBuffer storage is fixed-length. Atomics supports integer `load`, `store`, `exchange`, `compareExchange`, `add`, `sub`, `and`, `or`, and `xor` over 8-, 16-, and 32-bit integer arrays. `wait` and `notify` use Int32Array views; BigInt atomics, `waitAsync`, and growable buffers are unsupported. Atomics operations run as sequentially consistent atomic instructions and take no lock. Other reads and writes of shared storage access each element atomically; a copy, fill, or conversion that runs while another thread writes the same storage can observe some of those writes, as in Node. `terminate` unwinds at runtime checkpoints, including loops and parked atomic waits, without running JavaScript catch or finally blocks. Blocking native calls finish before the next checkpoint. diff --git a/docs/content/limitations/process-arguments-and-errors.mdx b/docs/content/limitations/process-arguments-and-errors.mdx index 4cca7a808e..d048b4154f 100644 --- a/docs/content/limitations/process-arguments-and-errors.mdx +++ b/docs/content/limitations/process-arguments-and-errors.mdx @@ -1,4 +1,4 @@ -`process.argv[0]` is `"scriptc"`, `process.argv[1]` is the binary's path, and program arguments begin at index 2. `process.version` and `process.versions.node` report the Node.js release the runtime is verified against. +`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 ` 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`. diff --git a/docs/content/limitations/runtime-type-checks.mdx b/docs/content/limitations/runtime-type-checks.mdx index 5514fb41ee..ee7b953ced 100644 --- a/docs/content/limitations/runtime-type-checks.mdx +++ b/docs/content/limitations/runtime-type-checks.mdx @@ -3,3 +3,5 @@ Casts from values whose type is only known at runtime validate the value. For ex Casts between program classes are checked too: casting an instance to a class it does not extend throws a `TypeError`, where Node.js reads the members structurally. `SC6004` reports calls that pass such an instance to a function that casts it. A Promise converted from `unknown` to a typed result validates its fulfilled value. A mismatch rejects with a `TypeError`. Rejection reasons and reference equality are preserved across conversions. + +A non-null assertion (`value!`) checks the value when it extracts an object, array or other non-nullable member from a union. If the value holds `null` or `undefined`, the assertion throws a `TypeError`, even when the result is never used; Node.js continues until a property of the value is read. diff --git a/docs/src/generated/node-v24-compatibility-meta.json b/docs/src/generated/node-v24-compatibility-meta.json index 93149c51c0..f2bb89f6ef 100644 --- a/docs/src/generated/node-v24-compatibility-meta.json +++ b/docs/src/generated/node-v24-compatibility-meta.json @@ -2,6 +2,6 @@ "schemaVersion": 3, "nodeVersion": "24.22.0", "nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93", - "artifactVersion": "a1ff929d49885bd8c92e", + "artifactVersion": "be8d5b094695385e34ea", "rowCount": 3880 } diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index 352c211b5f..3c0350dc3d 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -164,9 +164,9 @@ "apiEntries": 39, "static": { "supported": 0, - "partial": 17, + "partial": 18, "refused": 0, - "not-implemented": 22, + "not-implemented": 21, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -509,9 +509,9 @@ "apiEntries": 24, "static": { "supported": 0, - "partial": 23, + "partial": 24, "refused": 0, - "not-implemented": 1, + "not-implemented": 0, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -569,9 +569,9 @@ "apiEntries": 120, "static": { "supported": 0, - "partial": 50, + "partial": 51, "refused": 0, - "not-implemented": 70, + "not-implemented": 69, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -689,9 +689,9 @@ "apiEntries": 117, "static": { "supported": 0, - "partial": 45, + "partial": 53, "refused": 0, - "not-implemented": 72, + "not-implemented": 64, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -779,9 +779,9 @@ "apiEntries": 62, "static": { "supported": 0, - "partial": 39, + "partial": 42, "refused": 0, - "not-implemented": 23, + "not-implemented": 20, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -794,9 +794,9 @@ "apiEntries": 109, "static": { "supported": 0, - "partial": 53, + "partial": 72, "refused": 0, - "not-implemented": 56, + "not-implemented": 37, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -884,9 +884,9 @@ "apiEntries": 66, "static": { "supported": 0, - "partial": 26, + "partial": 46, "refused": 0, - "not-implemented": 40, + "not-implemented": 20, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -9312,9 +9312,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -52503,9 +52503,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -57593,9 +57593,9 @@ "anchor": "", "nodeStability": null, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -65550,9 +65550,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -65573,9 +65573,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -65596,9 +65596,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -65619,9 +65619,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -65642,9 +65642,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -65665,9 +65665,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -66401,9 +66401,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -66424,9 +66424,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -71599,9 +71599,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -71714,9 +71714,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -71806,9 +71806,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -73324,9 +73324,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -73347,9 +73347,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -73508,9 +73508,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -74060,9 +74060,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.", + "verification": "test-backed" } }, { @@ -74083,9 +74083,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.", + "verification": "test-backed" } }, { @@ -74106,9 +74106,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.", + "verification": "test-backed" } }, { @@ -74129,9 +74129,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.", + "verification": "test-backed" } }, { @@ -74152,9 +74152,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.", + "verification": "test-backed" } }, { @@ -74175,9 +74175,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.", + "verification": "test-backed" } }, { @@ -74221,9 +74221,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.", + "verification": "test-backed" } }, { @@ -74244,9 +74244,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.", + "verification": "test-backed" } }, { @@ -74267,9 +74267,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.", + "verification": "test-backed" } }, { @@ -74451,9 +74451,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.", + "verification": "test-backed" } }, { @@ -74497,9 +74497,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.", + "verification": "test-backed" } }, { @@ -74566,9 +74566,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.", + "verification": "test-backed" } }, { @@ -74681,9 +74681,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.", + "verification": "test-backed" } }, { @@ -74704,9 +74704,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.", + "verification": "test-backed" } }, { @@ -74727,9 +74727,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.", + "verification": "test-backed" } }, { @@ -74750,9 +74750,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.", + "verification": "test-backed" } }, { @@ -82920,9 +82920,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -82943,9 +82943,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -82966,9 +82966,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -82989,9 +82989,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -83012,9 +83012,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -83035,9 +83035,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -83564,9 +83564,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -83633,9 +83633,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -83656,9 +83656,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -83679,9 +83679,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -83702,9 +83702,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -83748,9 +83748,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -83886,9 +83886,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -83909,9 +83909,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -83932,9 +83932,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -83955,9 +83955,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -84047,9 +84047,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -84070,9 +84070,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -84139,9 +84139,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -84208,9 +84208,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { diff --git a/docs/test/fixtures/migration-baseline.json b/docs/test/fixtures/migration-baseline.json index 6aaab5f70c..dd4320e03a 100644 --- a/docs/test/fixtures/migration-baseline.json +++ b/docs/test/fixtures/migration-baseline.json @@ -273,6 +273,10 @@ "text": "When a region is freed", "id": "when-a-region-is-freed" }, + { + "text": "Async work", + "id": "async-work" + }, { "text": "Threads", "id": "threads" diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index fff784adc8..24821377ea 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -7,8 +7,8 @@ "tierItemCount": 3456, "tiers": { "static": { - "audit-partial": 1091, - "verify-gap": 2238, + "audit-partial": 1144, + "verify-gap": 2185, "replace-refusal": 40, "classify": 2, "implement": 85 @@ -3941,12 +3941,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:child_process", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:child_process.child_process.exec", + "tests": [ + "tests/corpus/5627-exec-callback.ts", + "tests/fixtures/node-types/child-exec.ts", + "tests/harness/project-config.test.ts" + ] } } }, @@ -5885,7 +5889,8 @@ "source": "compiler-dedicated:console.assert", "tests": [ "tests/corpus/5096-console-assert-dir.ts", - "tests/corpus/5099-console-methods-dynamic.cjs" + "tests/corpus/5099-console-methods-dynamic.cjs", + "tests/corpus/5884-inspect-layout-options.ts" ] } } @@ -6029,7 +6034,8 @@ "source": "compiler-dedicated:console.dir", "tests": [ "tests/corpus/5096-console-assert-dir.ts", - "tests/corpus/5099-console-methods-dynamic.cjs" + "tests/corpus/5099-console-methods-dynamic.cjs", + "tests/corpus/5884-inspect-layout-options.ts" ] } } @@ -6057,7 +6063,8 @@ "source": "compiler-dedicated:console.dirxml", "tests": [ "tests/corpus/5096-console-assert-dir.ts", - "tests/corpus/5099-console-methods-dynamic.cjs" + "tests/corpus/5099-console-methods-dynamic.cjs", + "tests/corpus/5884-inspect-layout-options.ts" ] } } @@ -25006,7 +25013,9 @@ "confidence": "high", "source": "compiler-dedicated:events.getEventListeners", "tests": [ - "tests/corpus/5885-events-max-listener-helpers.ts" + "tests/corpus/5885-events-max-listener-helpers.ts", + "tests/corpus/5846-events-esm-default-max.ts", + "tests/corpus/5847-events-cjs-default-max.cjs" ] } } @@ -25033,7 +25042,9 @@ "confidence": "high", "source": "compiler-dedicated:events.getMaxListeners", "tests": [ - "tests/corpus/5885-events-max-listener-helpers.ts" + "tests/corpus/5885-events-max-listener-helpers.ts", + "tests/corpus/5846-events-esm-default-max.ts", + "tests/corpus/5847-events-cjs-default-max.cjs" ] } } @@ -25088,7 +25099,9 @@ "confidence": "high", "source": "compiler-dedicated:events.listenerCount", "tests": [ - "tests/corpus/5885-events-max-listener-helpers.ts" + "tests/corpus/5885-events-max-listener-helpers.ts", + "tests/corpus/5846-events-esm-default-max.ts", + "tests/corpus/5847-events-cjs-default-max.cjs" ] } } @@ -25140,7 +25153,9 @@ "confidence": "high", "source": "compiler-dedicated:events.setMaxListeners", "tests": [ - "tests/corpus/5885-events-max-listener-helpers.ts" + "tests/corpus/5885-events-max-listener-helpers.ts", + "tests/corpus/5846-events-esm-default-max.ts", + "tests/corpus/5847-events-cjs-default-max.cjs" ] } } @@ -25194,7 +25209,9 @@ "confidence": "high", "source": "compiler-dedicated:events.defaultMaxListeners", "tests": [ - "tests/corpus/5885-events-max-listener-helpers.ts" + "tests/corpus/5885-events-max-listener-helpers.ts", + "tests/corpus/5846-events-esm-default-max.ts", + "tests/corpus/5847-events-cjs-default-max.cjs" ] } } @@ -48427,7 +48444,8 @@ "confidence": "high", "source": "compiler-feature:module.module.isBuiltin", "tests": [ - "tests/corpus/2932-create-require-program/main.ts" + "tests/corpus/2932-create-require-program/main.ts", + "tests/corpus/5846-events-esm-default-max.ts" ] } } @@ -48524,7 +48542,8 @@ "confidence": "high", "source": "compiler-feature:module.module.syncBuiltinESMExports", "tests": [ - "tests/corpus/2932-create-require-program/main.ts" + "tests/corpus/2932-create-require-program/main.ts", + "tests/corpus/5846-events-esm-default-max.ts" ] } } @@ -51739,12 +51758,14 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:os", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "surface-manifest:node-builtin.os.setPriority", + "tests": [ + "tests/corpus/5883-os-set-priority.ts" + ] } } }, @@ -57502,12 +57523,15 @@ "priority": "normal", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:process", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "surface-manifest:node-builtin.process.execArgv", + "tests": [ + "tests/corpus/990-process-basics.ts", + "tests/corpus/2314-process-introspection.ts" + ] } } }, @@ -57551,6 +57575,7 @@ "confidence": "high", "source": "surface-manifest:node-builtin.process.exitCode", "tests": [ + "tests/corpus/5809-process-exit-and-rejections.cjs", "tests/corpus/2941-process-exit-code.ts", "tests/corpus/2942-process-exit-code-override.ts", "tests/corpus/2943-process-exit-code-invalid.ts", @@ -65617,7 +65642,8 @@ "source": "compiler-dedicated:StringDecoder", "tests": [ "tests/corpus/1662-string-decoder-encodings.ts", - "tests/corpus/node24-string-decoder.ts" + "tests/corpus/node24-string-decoder.ts", + "tests/corpus/5882-string-decoder-runtime-encoding.ts" ] } } @@ -65645,7 +65671,8 @@ "source": "compiler-dedicated:stringDecoder.end", "tests": [ "tests/corpus/1662-string-decoder-encodings.ts", - "tests/corpus/node24-string-decoder.ts" + "tests/corpus/node24-string-decoder.ts", + "tests/corpus/5882-string-decoder-runtime-encoding.ts" ] } } @@ -65673,7 +65700,8 @@ "source": "compiler-dedicated:stringDecoder.write", "tests": [ "tests/corpus/1662-string-decoder-encodings.ts", - "tests/corpus/node24-string-decoder.ts" + "tests/corpus/node24-string-decoder.ts", + "tests/corpus/5882-string-decoder-runtime-encoding.ts" ] } } @@ -66670,12 +66698,18 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:test", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:test.timers.enable", + "tests": [ + "tests/fixtures/node-test/cases/mock-timers.ts", + "tests/fixtures/node-test/cases/mock-timers-date.ts", + "tests/corpus/5813-mock-timers-module.ts", + "tests/corpus/5814-mock-timers-date.ts", + "tests/harness/node-test.test.ts" + ] } } }, @@ -66695,12 +66729,18 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:test", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:test.timers.reset", + "tests": [ + "tests/fixtures/node-test/cases/mock-timers.ts", + "tests/fixtures/node-test/cases/mock-timers-date.ts", + "tests/corpus/5813-mock-timers-module.ts", + "tests/corpus/5814-mock-timers-date.ts", + "tests/harness/node-test.test.ts" + ] } } }, @@ -66720,12 +66760,15 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:test", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:test.timers", + "tests": [ + "tests/fixtures/node-test/cases/mock-timers-date.ts", + "tests/harness/node-test.test.ts" + ] } } }, @@ -66745,12 +66788,18 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:test", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:test.timers.tick", + "tests": [ + "tests/fixtures/node-test/cases/mock-timers.ts", + "tests/fixtures/node-test/cases/mock-timers-date.ts", + "tests/corpus/5813-mock-timers-module.ts", + "tests/corpus/5814-mock-timers-date.ts", + "tests/harness/node-test.test.ts" + ] } } }, @@ -66770,12 +66819,18 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:test", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:test.timers.runAll", + "tests": [ + "tests/fixtures/node-test/cases/mock-timers.ts", + "tests/fixtures/node-test/cases/mock-timers-date.ts", + "tests/corpus/5813-mock-timers-module.ts", + "tests/corpus/5814-mock-timers-date.ts", + "tests/harness/node-test.test.ts" + ] } } }, @@ -66795,12 +66850,18 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:test", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:test.timers.setTime", + "tests": [ + "tests/fixtures/node-test/cases/mock-timers.ts", + "tests/fixtures/node-test/cases/mock-timers-date.ts", + "tests/corpus/5813-mock-timers-module.ts", + "tests/corpus/5814-mock-timers-date.ts", + "tests/harness/node-test.test.ts" + ] } } }, @@ -67625,12 +67686,15 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:test", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:test.context.assert.fileSnapshot", + "tests": [ + "tests/fixtures/node-test/cases/snapshot.ts", + "tests/harness/node-test.test.ts" + ] } } }, @@ -67650,12 +67714,15 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:test", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:test.context.assert.snapshot", + "tests": [ + "tests/fixtures/node-test/cases/snapshot.ts", + "tests/harness/node-test.test.ts" + ] } } }, @@ -73455,12 +73522,15 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:url", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:url.urlSearchParams.entries", + "tests": [ + "tests/corpus/5833-url-search-params-iterators.ts", + "tests/corpus/5834-url-search-params-iterators-dynamic.mjs" + ] } } }, @@ -73580,12 +73650,15 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:url", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:url.urlSearchParams.keys", + "tests": [ + "tests/corpus/5833-url-search-params-iterators.ts", + "tests/corpus/5834-url-search-params-iterators-dynamic.mjs" + ] } } }, @@ -73680,12 +73753,15 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:url", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:url.urlSearchParams.values", + "tests": [ + "tests/corpus/5833-url-search-params-iterators.ts", + "tests/corpus/5834-url-search-params-iterators-dynamic.mjs" + ] } } }, @@ -75499,12 +75575,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:util", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:util.util.format", + "tests": [ + "tests/corpus/1636-util-format.ts", + "tests/corpus/2440-console-inspect-args.ts", + "tests/corpus/5835-util-format-integer-specifiers.ts" + ] } } }, @@ -75524,12 +75604,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:util", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:util.util.formatWithOptions", + "tests": [ + "tests/corpus/1636-util-format.ts", + "tests/corpus/2440-console-inspect-args.ts", + "tests/corpus/5835-util-format-integer-specifiers.ts" + ] } } }, @@ -75718,12 +75802,18 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:util", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:util.util.inspect", + "tests": [ + "tests/corpus/1630-inspect-scalars.ts", + "tests/corpus/1632-inspect-records.ts", + "tests/corpus/1637-inspect-dyn.ts", + "tests/corpus/2485-inspect-circular-refs.ts", + "tests/corpus/5884-inspect-layout-options.ts" + ] } } }, @@ -76402,12 +76492,19 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:util", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:util.util.types.isBigInt64Array", + "tests": [ + "tests/corpus/5831-util-types-brands.ts", + "tests/corpus/5832-util-types-brands-dynamic.js", + "tests/corpus/node24-util-types.ts", + "tests/corpus/node24-util-types.js", + "tests/corpus/node24-util-types.cjs", + "tests/fixtures/node-types/util-types.ts" + ] } } }, @@ -76427,12 +76524,19 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:util", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:util.util.types.isBigIntObject", + "tests": [ + "tests/corpus/5831-util-types-brands.ts", + "tests/corpus/5832-util-types-brands-dynamic.js", + "tests/corpus/node24-util-types.ts", + "tests/corpus/node24-util-types.js", + "tests/corpus/node24-util-types.cjs", + "tests/fixtures/node-types/util-types.ts" + ] } } }, @@ -76452,12 +76556,19 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:util", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:util.util.types.isBigUint64Array", + "tests": [ + "tests/corpus/5831-util-types-brands.ts", + "tests/corpus/5832-util-types-brands-dynamic.js", + "tests/corpus/node24-util-types.ts", + "tests/corpus/node24-util-types.js", + "tests/corpus/node24-util-types.cjs", + "tests/fixtures/node-types/util-types.ts" + ] } } }, @@ -76477,12 +76588,19 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:util", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:util.util.types.isBooleanObject", + "tests": [ + "tests/corpus/5831-util-types-brands.ts", + "tests/corpus/5832-util-types-brands-dynamic.js", + "tests/corpus/node24-util-types.ts", + "tests/corpus/node24-util-types.js", + "tests/corpus/node24-util-types.cjs", + "tests/fixtures/node-types/util-types.ts" + ] } } }, @@ -76502,12 +76620,19 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:util", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:util.util.types.isBoxedPrimitive", + "tests": [ + "tests/corpus/5831-util-types-brands.ts", + "tests/corpus/5832-util-types-brands-dynamic.js", + "tests/corpus/node24-util-types.ts", + "tests/corpus/node24-util-types.js", + "tests/corpus/node24-util-types.cjs", + "tests/fixtures/node-types/util-types.ts" + ] } } }, @@ -76527,12 +76652,19 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:util", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:util.util.types.isCryptoKey", + "tests": [ + "tests/corpus/5831-util-types-brands.ts", + "tests/corpus/5832-util-types-brands-dynamic.js", + "tests/corpus/node24-util-types.ts", + "tests/corpus/node24-util-types.js", + "tests/corpus/node24-util-types.cjs", + "tests/fixtures/node-types/util-types.ts" + ] } } }, @@ -76582,12 +76714,19 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:util", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:util.util.types.isDate", + "tests": [ + "tests/corpus/5831-util-types-brands.ts", + "tests/corpus/5832-util-types-brands-dynamic.js", + "tests/corpus/node24-util-types.ts", + "tests/corpus/node24-util-types.js", + "tests/corpus/node24-util-types.cjs", + "tests/fixtures/node-types/util-types.ts" + ] } } }, @@ -76607,12 +76746,19 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:util", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:util.util.types.isExternal", + "tests": [ + "tests/corpus/5831-util-types-brands.ts", + "tests/corpus/5832-util-types-brands-dynamic.js", + "tests/corpus/node24-util-types.ts", + "tests/corpus/node24-util-types.js", + "tests/corpus/node24-util-types.cjs", + "tests/fixtures/node-types/util-types.ts" + ] } } }, @@ -76632,12 +76778,19 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:util", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:util.util.types.isFloat16Array", + "tests": [ + "tests/corpus/5831-util-types-brands.ts", + "tests/corpus/5832-util-types-brands-dynamic.js", + "tests/corpus/node24-util-types.ts", + "tests/corpus/node24-util-types.js", + "tests/corpus/node24-util-types.cjs", + "tests/fixtures/node-types/util-types.ts" + ] } } }, @@ -76867,12 +77020,19 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:util", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:util.util.types.isKeyObject", + "tests": [ + "tests/corpus/5831-util-types-brands.ts", + "tests/corpus/5832-util-types-brands-dynamic.js", + "tests/corpus/node24-util-types.ts", + "tests/corpus/node24-util-types.js", + "tests/corpus/node24-util-types.cjs", + "tests/fixtures/node-types/util-types.ts" + ] } } }, @@ -76922,12 +77082,19 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:util", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:util.util.types.isMapIterator", + "tests": [ + "tests/corpus/5831-util-types-brands.ts", + "tests/corpus/5832-util-types-brands-dynamic.js", + "tests/corpus/node24-util-types.ts", + "tests/corpus/node24-util-types.js", + "tests/corpus/node24-util-types.cjs", + "tests/fixtures/node-types/util-types.ts" + ] } } }, @@ -77002,12 +77169,19 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:util", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:util.util.types.isNumberObject", + "tests": [ + "tests/corpus/5831-util-types-brands.ts", + "tests/corpus/5832-util-types-brands-dynamic.js", + "tests/corpus/node24-util-types.ts", + "tests/corpus/node24-util-types.js", + "tests/corpus/node24-util-types.cjs", + "tests/fixtures/node-types/util-types.ts" + ] } } }, @@ -77147,12 +77321,19 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:util", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:util.util.types.isSetIterator", + "tests": [ + "tests/corpus/5831-util-types-brands.ts", + "tests/corpus/5832-util-types-brands-dynamic.js", + "tests/corpus/node24-util-types.ts", + "tests/corpus/node24-util-types.js", + "tests/corpus/node24-util-types.cjs", + "tests/fixtures/node-types/util-types.ts" + ] } } }, @@ -77172,12 +77353,19 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:util", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:util.util.types.isSharedArrayBuffer", + "tests": [ + "tests/corpus/5831-util-types-brands.ts", + "tests/corpus/5832-util-types-brands-dynamic.js", + "tests/corpus/node24-util-types.ts", + "tests/corpus/node24-util-types.js", + "tests/corpus/node24-util-types.cjs", + "tests/fixtures/node-types/util-types.ts" + ] } } }, @@ -77197,12 +77385,19 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:util", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:util.util.types.isStringObject", + "tests": [ + "tests/corpus/5831-util-types-brands.ts", + "tests/corpus/5832-util-types-brands-dynamic.js", + "tests/corpus/node24-util-types.ts", + "tests/corpus/node24-util-types.js", + "tests/corpus/node24-util-types.cjs", + "tests/fixtures/node-types/util-types.ts" + ] } } }, @@ -77222,12 +77417,19 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:util", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:util.util.types.isSymbolObject", + "tests": [ + "tests/corpus/5831-util-types-brands.ts", + "tests/corpus/5832-util-types-brands-dynamic.js", + "tests/corpus/node24-util-types.ts", + "tests/corpus/node24-util-types.js", + "tests/corpus/node24-util-types.cjs", + "tests/fixtures/node-types/util-types.ts" + ] } } }, @@ -83656,12 +83858,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:worker_threads", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:worker_threads.BroadcastChannel extends EventTarget", + "tests": [ + "tests/corpus/5800-broadcast-channel.ts", + "tests/corpus/5801-broadcast-channel.cjs", + "tests/corpus/5802-broadcast-channel-workers.ts" + ] } } }, @@ -83681,12 +83887,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:worker_threads", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:worker_threads.broadcastChannel.close", + "tests": [ + "tests/corpus/5800-broadcast-channel.ts", + "tests/corpus/5801-broadcast-channel.cjs", + "tests/corpus/5802-broadcast-channel-workers.ts" + ] } } }, @@ -83706,12 +83916,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:worker_threads", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:worker_threads.broadcastChannel.postMessage", + "tests": [ + "tests/corpus/5800-broadcast-channel.ts", + "tests/corpus/5801-broadcast-channel.cjs", + "tests/corpus/5802-broadcast-channel-workers.ts" + ] } } }, @@ -83731,12 +83945,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:worker_threads", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:worker_threads.broadcastChannel.ref", + "tests": [ + "tests/corpus/5800-broadcast-channel.ts", + "tests/corpus/5801-broadcast-channel.cjs", + "tests/corpus/5802-broadcast-channel-workers.ts" + ] } } }, @@ -83756,12 +83974,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:worker_threads", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:worker_threads.broadcastChannel.unref", + "tests": [ + "tests/corpus/5800-broadcast-channel.ts", + "tests/corpus/5801-broadcast-channel.cjs", + "tests/corpus/5802-broadcast-channel-workers.ts" + ] } } }, @@ -83781,12 +84003,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:worker_threads", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:worker_threads.onmessage", + "tests": [ + "tests/corpus/5800-broadcast-channel.ts", + "tests/corpus/5801-broadcast-channel.cjs", + "tests/corpus/5802-broadcast-channel-workers.ts" + ] } } }, @@ -83839,7 +84065,8 @@ "tests": [ "tests/corpus/5793-worker-message-channel.ts", "tests/corpus/5796-worker-message-channel.cjs", - "tests/corpus/5797-worker-message-port-transfer.ts" + "tests/corpus/5797-worker-message-port-transfer.ts", + "tests/corpus/5805-worker-receive-message-parent-port.ts" ] } } @@ -84404,12 +84631,14 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:worker_threads", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:worker_threads.worker", + "tests": [ + "tests/corpus/5991-worker-async-dispose.mjs" + ] } } }, @@ -84479,12 +84708,14 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:worker_threads", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:worker_threads.resourceLimits", + "tests": [ + "tests/corpus/5806-worker-resource-limits.ts" + ] } } }, @@ -84504,12 +84735,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:worker_threads", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:worker_threads.stderr", + "tests": [ + "tests/corpus/5874-worker-stdio-streams.ts", + "tests/corpus/5875-worker-stdout-pipe.cjs", + "tests/corpus/5879-worker-stdin.ts" + ] } } }, @@ -84529,12 +84764,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:worker_threads", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:worker_threads.stdin", + "tests": [ + "tests/corpus/5874-worker-stdio-streams.ts", + "tests/corpus/5875-worker-stdout-pipe.cjs", + "tests/corpus/5879-worker-stdin.ts" + ] } } }, @@ -84554,12 +84793,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:worker_threads", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:worker_threads.stdout", + "tests": [ + "tests/corpus/5874-worker-stdio-streams.ts", + "tests/corpus/5875-worker-stdout-pipe.cjs", + "tests/corpus/5879-worker-stdin.ts" + ] } } }, @@ -84608,12 +84851,15 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:worker_threads", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:worker_threads.threadName", + "tests": [ + "tests/corpus/5870-worker-thread-names.ts", + "tests/corpus/5871-worker-untransferable.cjs" + ] } } }, @@ -84641,7 +84887,9 @@ "tests": [ "tests/corpus/native-worker-references.ts", "tests/corpus/native-worker-error.ts", - "tests/corpus/native-worker-exit-listeners.ts" + "tests/corpus/native-worker-exit-listeners.ts", + "tests/corpus/5807-worker-error-values.ts", + "tests/corpus/5808-worker-unhandled-rejections.ts" ] } } @@ -84670,7 +84918,9 @@ "tests": [ "tests/corpus/native-worker-references.ts", "tests/corpus/native-worker-error.ts", - "tests/corpus/native-worker-exit-listeners.ts" + "tests/corpus/native-worker-exit-listeners.ts", + "tests/corpus/5807-worker-error-values.ts", + "tests/corpus/5808-worker-unhandled-rejections.ts" ] } } @@ -84699,7 +84949,9 @@ "tests": [ "tests/corpus/native-worker-references.ts", "tests/corpus/native-worker-error.ts", - "tests/corpus/native-worker-exit-listeners.ts" + "tests/corpus/native-worker-exit-listeners.ts", + "tests/corpus/5807-worker-error-values.ts", + "tests/corpus/5808-worker-unhandled-rejections.ts" ] } } @@ -84753,7 +85005,9 @@ "tests": [ "tests/corpus/native-worker-references.ts", "tests/corpus/native-worker-error.ts", - "tests/corpus/native-worker-exit-listeners.ts" + "tests/corpus/native-worker-exit-listeners.ts", + "tests/corpus/5807-worker-error-values.ts", + "tests/corpus/5808-worker-unhandled-rejections.ts" ] } } @@ -84774,12 +85028,15 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:worker_threads", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:worker_threads.worker_threads.getEnvironmentData", + "tests": [ + "tests/corpus/5803-worker-environment-data.ts", + "tests/corpus/5804-worker-environment-data.cjs" + ] } } }, @@ -84799,12 +85056,15 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:worker_threads", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:worker_threads.worker_threads.markAsUntransferable", + "tests": [ + "tests/corpus/5870-worker-thread-names.ts", + "tests/corpus/5871-worker-untransferable.cjs" + ] } } }, @@ -84824,12 +85084,15 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:worker_threads", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:worker_threads.worker_threads.isMarkedAsUntransferable", + "tests": [ + "tests/corpus/5870-worker-thread-names.ts", + "tests/corpus/5871-worker-untransferable.cjs" + ] } } }, @@ -84849,12 +85112,14 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:worker_threads", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:worker_threads.worker_threads.markAsUncloneable", + "tests": [ + "tests/corpus/5990-worker-uncloneable.mjs" + ] } } }, @@ -84932,7 +85197,8 @@ "tests": [ "tests/corpus/5793-worker-message-channel.ts", "tests/corpus/5796-worker-message-channel.cjs", - "tests/corpus/5797-worker-message-port-transfer.ts" + "tests/corpus/5797-worker-message-port-transfer.ts", + "tests/corpus/5805-worker-receive-message-parent-port.ts" ] } } @@ -84953,12 +85219,15 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:worker_threads", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:worker_threads.worker_threads.setEnvironmentData", + "tests": [ + "tests/corpus/5803-worker-environment-data.ts", + "tests/corpus/5804-worker-environment-data.cjs" + ] } } }, @@ -84978,12 +85247,14 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:worker_threads", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:worker_threads.isInternalThread", + "tests": [ + "tests/corpus/5990-worker-uncloneable.mjs" + ] } } }, @@ -85061,12 +85332,14 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:worker_threads", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:worker_threads.resourceLimits", + "tests": [ + "tests/corpus/5806-worker-resource-limits.ts" + ] } } }, @@ -85140,12 +85413,15 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:worker_threads", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:worker_threads.threadName", + "tests": [ + "tests/corpus/5870-worker-thread-names.ts", + "tests/corpus/5871-worker-untransferable.cjs" + ] } } }, diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 559bd48ca9..3587ae2cb8 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -131,9 +131,9 @@ "apiEntries": 39, "static": { "supported": 0, - "partial": 17, + "partial": 18, "refused": 0, - "not-implemented": 22, + "not-implemented": 21, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -536,9 +536,9 @@ "apiEntries": 24, "static": { "supported": 0, - "partial": 23, + "partial": 24, "refused": 0, - "not-implemented": 1, + "not-implemented": 0, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -596,9 +596,9 @@ "apiEntries": 120, "static": { "supported": 0, - "partial": 50, + "partial": 51, "refused": 0, - "not-implemented": 70, + "not-implemented": 69, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -746,9 +746,9 @@ "apiEntries": 117, "static": { "supported": 0, - "partial": 45, + "partial": 53, "refused": 0, - "not-implemented": 72, + "not-implemented": 64, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -836,9 +836,9 @@ "apiEntries": 62, "static": { "supported": 0, - "partial": 39, + "partial": 42, "refused": 0, - "not-implemented": 23, + "not-implemented": 20, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -851,9 +851,9 @@ "apiEntries": 109, "static": { "supported": 0, - "partial": 53, + "partial": 72, "refused": 0, - "not-implemented": 56, + "not-implemented": 37, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -941,9 +941,9 @@ "apiEntries": 66, "static": { "supported": 0, - "partial": 26, + "partial": 46, "refused": 0, - "not-implemented": 40, + "not-implemented": 20, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -11663,7 +11663,10 @@ "tests/corpus/3127-spawn-runtime-options.ts", "tests/corpus/3130-child-generic-streams.ts", "tests/corpus/3131-child-rest-listeners.js", - "tests/corpus/3132-child-generic-stream-completion.ts" + "tests/corpus/3132-child-generic-stream-completion.ts", + "tests/corpus/5627-exec-callback.ts", + "tests/fixtures/node-types/child-exec.ts", + "tests/harness/project-config.test.ts" ] }, "anchorSource": "chapter" @@ -11732,8 +11735,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:child_process" + "status": "partial", + "evidence": "compiler-feature:child_process.child_process.exec", + "tests": [ + "tests/corpus/5627-exec-callback.ts", + "tests/fixtures/node-types/child-exec.ts", + "tests/harness/project-config.test.ts" + ] }, "anchorSource": "exact" }, @@ -19141,6 +19149,7 @@ "tests/corpus/5097-console-table.ts", "tests/corpus/5098-console-time-warnings.ts", "tests/corpus/5099-console-methods-dynamic.cjs", + "tests/corpus/5884-inspect-layout-options.ts", "tests/corpus/console-native-value.js", "tests/harness/npm-static.test.ts" ] @@ -19297,7 +19306,8 @@ "evidence": "compiler-dedicated:console.assert", "tests": [ "tests/corpus/5096-console-assert-dir.ts", - "tests/corpus/5099-console-methods-dynamic.cjs" + "tests/corpus/5099-console-methods-dynamic.cjs", + "tests/corpus/5884-inspect-layout-options.ts" ] }, "anchorSource": "exact" @@ -19436,7 +19446,8 @@ "evidence": "compiler-dedicated:console.dir", "tests": [ "tests/corpus/5096-console-assert-dir.ts", - "tests/corpus/5099-console-methods-dynamic.cjs" + "tests/corpus/5099-console-methods-dynamic.cjs", + "tests/corpus/5884-inspect-layout-options.ts" ] }, "anchorSource": "exact" @@ -19463,7 +19474,8 @@ "evidence": "compiler-dedicated:console.dirxml", "tests": [ "tests/corpus/5096-console-assert-dir.ts", - "tests/corpus/5099-console-methods-dynamic.cjs" + "tests/corpus/5099-console-methods-dynamic.cjs", + "tests/corpus/5884-inspect-layout-options.ts" ] }, "anchorSource": "exact" @@ -41815,6 +41827,8 @@ "tests/corpus/5245-event-target-subclass-signals.ts", "tests/corpus/5247-event-target-subclass-lifetimes.ts", "tests/corpus/5248-event-target-subclass-warnings.ts", + "tests/corpus/5846-events-esm-default-max.ts", + "tests/corpus/5847-events-cjs-default-max.cjs", "tests/corpus/5885-events-max-listener-helpers.ts" ] }, @@ -43735,7 +43749,9 @@ "status": "partial", "evidence": "compiler-dedicated:events.getEventListeners", "tests": [ - "tests/corpus/5885-events-max-listener-helpers.ts" + "tests/corpus/5885-events-max-listener-helpers.ts", + "tests/corpus/5846-events-esm-default-max.ts", + "tests/corpus/5847-events-cjs-default-max.cjs" ] }, "anchorSource": "exact" @@ -43761,7 +43777,9 @@ "status": "partial", "evidence": "compiler-dedicated:events.getMaxListeners", "tests": [ - "tests/corpus/5885-events-max-listener-helpers.ts" + "tests/corpus/5885-events-max-listener-helpers.ts", + "tests/corpus/5846-events-esm-default-max.ts", + "tests/corpus/5847-events-cjs-default-max.cjs" ] }, "anchorSource": "exact" @@ -43837,7 +43855,9 @@ "status": "partial", "evidence": "compiler-dedicated:events.listenerCount", "tests": [ - "tests/corpus/5885-events-max-listener-helpers.ts" + "tests/corpus/5885-events-max-listener-helpers.ts", + "tests/corpus/5846-events-esm-default-max.ts", + "tests/corpus/5847-events-cjs-default-max.cjs" ] }, "anchorSource": "exact" @@ -43886,7 +43906,9 @@ "status": "partial", "evidence": "compiler-dedicated:events.setMaxListeners", "tests": [ - "tests/corpus/5885-events-max-listener-helpers.ts" + "tests/corpus/5885-events-max-listener-helpers.ts", + "tests/corpus/5846-events-esm-default-max.ts", + "tests/corpus/5847-events-cjs-default-max.cjs" ] }, "anchorSource": "exact" @@ -43938,7 +43960,9 @@ "status": "partial", "evidence": "compiler-dedicated:events.defaultMaxListeners", "tests": [ - "tests/corpus/5885-events-max-listener-helpers.ts" + "tests/corpus/5885-events-max-listener-helpers.ts", + "tests/corpus/5846-events-esm-default-max.ts", + "tests/corpus/5847-events-cjs-default-max.cjs" ] }, "anchorSource": "exact" @@ -69412,6 +69436,7 @@ "tests/corpus/2637-create-require-bare/main.ts", "tests/corpus/2675-create-require-nested-esm.js", "tests/corpus/2932-create-require-program/main.ts", + "tests/corpus/5846-events-esm-default-max.ts", "tests/harness/npm-static.test.ts" ] }, @@ -69499,7 +69524,8 @@ "status": "partial", "evidence": "compiler-feature:module.module.isBuiltin", "tests": [ - "tests/corpus/2932-create-require-program/main.ts" + "tests/corpus/2932-create-require-program/main.ts", + "tests/corpus/5846-events-esm-default-max.ts" ] }, "anchorSource": "exact" @@ -69589,7 +69615,8 @@ "status": "partial", "evidence": "compiler-feature:module.module.syncBuiltinESMExports", "tests": [ - "tests/corpus/2932-create-require-program/main.ts" + "tests/corpus/2932-create-require-program/main.ts", + "tests/corpus/5846-events-esm-default-max.ts" ] }, "anchorSource": "exact" @@ -74097,7 +74124,8 @@ "tests/corpus/2917-builtin-callable-widths.ts", "tests/corpus/4598-os-available-parallelism.ts", "tests/corpus/5140-os-host-facts.ts", - "tests/corpus/5141-os-constants.ts" + "tests/corpus/5141-os-constants.ts", + "tests/corpus/5883-os-set-priority.ts" ] }, "anchorSource": "chapter" @@ -74646,8 +74674,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:os" + "status": "partial", + "evidence": "surface-manifest:node-builtin.os.setPriority", + "tests": [ + "tests/corpus/5883-os-set-priority.ts" + ] }, "anchorSource": "exact" }, @@ -79289,6 +79320,7 @@ "tests/corpus/5194-process-emit-warning-tick.cjs", "tests/corpus/5210-process-memory-usage.ts", "tests/corpus/5211-process-identity.ts", + "tests/corpus/5809-process-exit-and-rejections.cjs", "tests/corpus/990-process-basics.ts", "tests/corpus/node24-env-files-errors.ts", "tests/corpus/node24-env-files.ts", @@ -80940,8 +80972,12 @@ "anchor": "", "nodeStability": null, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:process" + "status": "partial", + "evidence": "surface-manifest:node-builtin.process.execArgv", + "tests": [ + "tests/corpus/990-process-basics.ts", + "tests/corpus/2314-process-introspection.ts" + ] }, "anchorSource": "chapter" }, @@ -80983,6 +81019,7 @@ "status": "partial", "evidence": "surface-manifest:node-builtin.process.exitCode", "tests": [ + "tests/corpus/5809-process-exit-and-rejections.cjs", "tests/corpus/2941-process-exit-code.ts", "tests/corpus/2942-process-exit-code-override.ts", "tests/corpus/2943-process-exit-code-invalid.ts", @@ -90813,6 +90850,7 @@ "evidence": "derived:descendants", "tests": [ "tests/corpus/1662-string-decoder-encodings.ts", + "tests/corpus/5882-string-decoder-runtime-encoding.ts", "tests/corpus/node24-string-decoder.ts" ] }, @@ -90840,7 +90878,8 @@ "evidence": "compiler-dedicated:StringDecoder", "tests": [ "tests/corpus/1662-string-decoder-encodings.ts", - "tests/corpus/node24-string-decoder.ts" + "tests/corpus/node24-string-decoder.ts", + "tests/corpus/5882-string-decoder-runtime-encoding.ts" ] }, "anchorSource": "exact" @@ -90867,7 +90906,8 @@ "evidence": "compiler-dedicated:stringDecoder.end", "tests": [ "tests/corpus/1662-string-decoder-encodings.ts", - "tests/corpus/node24-string-decoder.ts" + "tests/corpus/node24-string-decoder.ts", + "tests/corpus/5882-string-decoder-runtime-encoding.ts" ] }, "anchorSource": "exact" @@ -90894,7 +90934,8 @@ "evidence": "compiler-dedicated:stringDecoder.write", "tests": [ "tests/corpus/1662-string-decoder-encodings.ts", - "tests/corpus/node24-string-decoder.ts" + "tests/corpus/node24-string-decoder.ts", + "tests/corpus/5882-string-decoder-runtime-encoding.ts" ] }, "anchorSource": "exact" @@ -90928,11 +90969,16 @@ "tests/corpus/5515-assert-deep-strict-typed-messages.ts", "tests/corpus/5810-mock-method-records.ts", "tests/corpus/5811-mock-tracker-restore.ts", + "tests/corpus/5813-mock-timers-module.ts", + "tests/corpus/5814-mock-timers-date.ts", "tests/fixtures/node-test/cases/ctx-hooks.ts", "tests/fixtures/node-test/cases/ctx-plan.ts", "tests/fixtures/node-test/cases/mock-fn.ts", "tests/fixtures/node-test/cases/mock-method.ts", + "tests/fixtures/node-test/cases/mock-timers-date.ts", + "tests/fixtures/node-test/cases/mock-timers.ts", "tests/fixtures/node-test/cases/only-nested.ts", + "tests/fixtures/node-test/cases/snapshot.ts", "tests/fixtures/node-test/cases/suite-variants.ts", "tests/fixtures/node-test/cases/todo-suites.ts", "tests/harness/node-test.test.ts" @@ -92562,8 +92608,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:test" + "status": "partial", + "evidence": "compiler-feature:test.timers.enable", + "tests": [ + "tests/fixtures/node-test/cases/mock-timers.ts", + "tests/fixtures/node-test/cases/mock-timers-date.ts", + "tests/corpus/5813-mock-timers-module.ts", + "tests/corpus/5814-mock-timers-date.ts", + "tests/harness/node-test.test.ts" + ] }, "anchorSource": "exact" }, @@ -92585,8 +92638,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:test" + "status": "partial", + "evidence": "compiler-feature:test.timers.reset", + "tests": [ + "tests/fixtures/node-test/cases/mock-timers.ts", + "tests/fixtures/node-test/cases/mock-timers-date.ts", + "tests/corpus/5813-mock-timers-module.ts", + "tests/corpus/5814-mock-timers-date.ts", + "tests/harness/node-test.test.ts" + ] }, "anchorSource": "exact" }, @@ -92608,8 +92668,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:test" + "status": "partial", + "evidence": "compiler-feature:test.timers", + "tests": [ + "tests/fixtures/node-test/cases/mock-timers-date.ts", + "tests/harness/node-test.test.ts" + ] }, "anchorSource": "exact" }, @@ -92631,8 +92695,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:test" + "status": "partial", + "evidence": "compiler-feature:test.timers.tick", + "tests": [ + "tests/fixtures/node-test/cases/mock-timers.ts", + "tests/fixtures/node-test/cases/mock-timers-date.ts", + "tests/corpus/5813-mock-timers-module.ts", + "tests/corpus/5814-mock-timers-date.ts", + "tests/harness/node-test.test.ts" + ] }, "anchorSource": "exact" }, @@ -92700,8 +92771,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:test" + "status": "partial", + "evidence": "compiler-feature:test.timers.runAll", + "tests": [ + "tests/fixtures/node-test/cases/mock-timers.ts", + "tests/fixtures/node-test/cases/mock-timers-date.ts", + "tests/corpus/5813-mock-timers-module.ts", + "tests/corpus/5814-mock-timers-date.ts", + "tests/harness/node-test.test.ts" + ] }, "anchorSource": "exact" }, @@ -92723,8 +92801,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:test" + "status": "partial", + "evidence": "compiler-feature:test.timers.setTime", + "tests": [ + "tests/fixtures/node-test/cases/mock-timers.ts", + "tests/fixtures/node-test/cases/mock-timers-date.ts", + "tests/corpus/5813-mock-timers-module.ts", + "tests/corpus/5814-mock-timers-date.ts", + "tests/harness/node-test.test.ts" + ] }, "anchorSource": "exact" }, @@ -93545,8 +93630,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:test" + "status": "partial", + "evidence": "compiler-feature:test.context.assert.fileSnapshot", + "tests": [ + "tests/fixtures/node-test/cases/snapshot.ts", + "tests/harness/node-test.test.ts" + ] }, "anchorSource": "exact" }, @@ -93568,8 +93657,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:test" + "status": "partial", + "evidence": "compiler-feature:test.context.assert.snapshot", + "tests": [ + "tests/fixtures/node-test/cases/snapshot.ts", + "tests/harness/node-test.test.ts" + ] }, "anchorSource": "exact" }, @@ -99004,6 +99097,8 @@ "tests/corpus/5227-url-legacy-dependency/main.ts", "tests/corpus/5228-url-domain-conversion.ts", "tests/corpus/5229-url-legacy-errors.cjs", + "tests/corpus/5833-url-search-params-iterators.ts", + "tests/corpus/5834-url-search-params-iterators-dynamic.mjs", "tests/corpus/node24-file-url-buffer.ts", "tests/corpus/node24-file-url-errors.js", "tests/corpus/node24-file-url-options.ts", @@ -99788,8 +99883,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:url" + "status": "partial", + "evidence": "compiler-feature:url.urlSearchParams.entries", + "tests": [ + "tests/corpus/5833-url-search-params-iterators.ts", + "tests/corpus/5834-url-search-params-iterators-dynamic.mjs" + ] }, "anchorSource": "exact" }, @@ -99903,8 +100002,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:url" + "status": "partial", + "evidence": "compiler-feature:url.urlSearchParams.keys", + "tests": [ + "tests/corpus/5833-url-search-params-iterators.ts", + "tests/corpus/5834-url-search-params-iterators-dynamic.mjs" + ] }, "anchorSource": "exact" }, @@ -99995,8 +100098,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:url" + "status": "partial", + "evidence": "compiler-feature:url.urlSearchParams.values", + "tests": [ + "tests/corpus/5833-url-search-params-iterators.ts", + "tests/corpus/5834-url-search-params-iterators-dynamic.mjs" + ] }, "anchorSource": "exact" }, @@ -100874,6 +100981,12 @@ "packages/compiler/test/deep-equality.test.ts", "tests/corpus/1473-promisify-execfile.ts", "tests/corpus/1573-promisify-execfile-env-spread.ts", + "tests/corpus/1630-inspect-scalars.ts", + "tests/corpus/1632-inspect-records.ts", + "tests/corpus/1636-util-format.ts", + "tests/corpus/1637-inspect-dyn.ts", + "tests/corpus/2440-console-inspect-args.ts", + "tests/corpus/2485-inspect-circular-refs.ts", "tests/corpus/2678-util-parseargs.ts", "tests/corpus/2912-promisify-fs-readfile.ts", "tests/corpus/3133-text-decoder-streaming.ts", @@ -100896,6 +101009,10 @@ "tests/corpus/5628-promisify-timers.ts", "tests/corpus/5629-promisify-stream.ts", "tests/corpus/5630-promisify-zlib.ts", + "tests/corpus/5831-util-types-brands.ts", + "tests/corpus/5832-util-types-brands-dynamic.js", + "tests/corpus/5835-util-format-integer-specifiers.ts", + "tests/corpus/5884-inspect-layout-options.ts", "tests/corpus/node24-deep-equality-properties.js", "tests/corpus/node24-deep-equality.cjs", "tests/corpus/node24-deep-equality.ts", @@ -101992,8 +102109,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "compiler-feature:util.util.format", + "tests": [ + "tests/corpus/1636-util-format.ts", + "tests/corpus/2440-console-inspect-args.ts", + "tests/corpus/5835-util-format-integer-specifiers.ts" + ] }, "anchorSource": "exact" }, @@ -102015,8 +102137,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "compiler-feature:util.util.formatWithOptions", + "tests": [ + "tests/corpus/1636-util-format.ts", + "tests/corpus/2440-console-inspect-args.ts", + "tests/corpus/5835-util-format-integer-specifiers.ts" + ] }, "anchorSource": "exact" }, @@ -102199,8 +102326,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "compiler-feature:util.util.inspect", + "tests": [ + "tests/corpus/1630-inspect-scalars.ts", + "tests/corpus/1632-inspect-records.ts", + "tests/corpus/1637-inspect-dyn.ts", + "tests/corpus/2485-inspect-circular-refs.ts", + "tests/corpus/5884-inspect-layout-options.ts" + ] }, "anchorSource": "exact" }, @@ -102963,8 +103097,16 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "compiler-feature:util.util.types.isBigInt64Array", + "tests": [ + "tests/corpus/5831-util-types-brands.ts", + "tests/corpus/5832-util-types-brands-dynamic.js", + "tests/corpus/node24-util-types.ts", + "tests/corpus/node24-util-types.js", + "tests/corpus/node24-util-types.cjs", + "tests/fixtures/node-types/util-types.ts" + ] }, "anchorSource": "exact" }, @@ -102986,8 +103128,16 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "compiler-feature:util.util.types.isBigIntObject", + "tests": [ + "tests/corpus/5831-util-types-brands.ts", + "tests/corpus/5832-util-types-brands-dynamic.js", + "tests/corpus/node24-util-types.ts", + "tests/corpus/node24-util-types.js", + "tests/corpus/node24-util-types.cjs", + "tests/fixtures/node-types/util-types.ts" + ] }, "anchorSource": "exact" }, @@ -103009,8 +103159,16 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "compiler-feature:util.util.types.isBigUint64Array", + "tests": [ + "tests/corpus/5831-util-types-brands.ts", + "tests/corpus/5832-util-types-brands-dynamic.js", + "tests/corpus/node24-util-types.ts", + "tests/corpus/node24-util-types.js", + "tests/corpus/node24-util-types.cjs", + "tests/fixtures/node-types/util-types.ts" + ] }, "anchorSource": "exact" }, @@ -103032,8 +103190,16 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "compiler-feature:util.util.types.isBooleanObject", + "tests": [ + "tests/corpus/5831-util-types-brands.ts", + "tests/corpus/5832-util-types-brands-dynamic.js", + "tests/corpus/node24-util-types.ts", + "tests/corpus/node24-util-types.js", + "tests/corpus/node24-util-types.cjs", + "tests/fixtures/node-types/util-types.ts" + ] }, "anchorSource": "exact" }, @@ -103055,8 +103221,16 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "compiler-feature:util.util.types.isBoxedPrimitive", + "tests": [ + "tests/corpus/5831-util-types-brands.ts", + "tests/corpus/5832-util-types-brands-dynamic.js", + "tests/corpus/node24-util-types.ts", + "tests/corpus/node24-util-types.js", + "tests/corpus/node24-util-types.cjs", + "tests/fixtures/node-types/util-types.ts" + ] }, "anchorSource": "exact" }, @@ -103078,8 +103252,16 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "compiler-feature:util.util.types.isCryptoKey", + "tests": [ + "tests/corpus/5831-util-types-brands.ts", + "tests/corpus/5832-util-types-brands-dynamic.js", + "tests/corpus/node24-util-types.ts", + "tests/corpus/node24-util-types.js", + "tests/corpus/node24-util-types.cjs", + "tests/fixtures/node-types/util-types.ts" + ] }, "anchorSource": "exact" }, @@ -103130,8 +103312,16 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "compiler-feature:util.util.types.isDate", + "tests": [ + "tests/corpus/5831-util-types-brands.ts", + "tests/corpus/5832-util-types-brands-dynamic.js", + "tests/corpus/node24-util-types.ts", + "tests/corpus/node24-util-types.js", + "tests/corpus/node24-util-types.cjs", + "tests/fixtures/node-types/util-types.ts" + ] }, "anchorSource": "exact" }, @@ -103153,8 +103343,16 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "compiler-feature:util.util.types.isExternal", + "tests": [ + "tests/corpus/5831-util-types-brands.ts", + "tests/corpus/5832-util-types-brands-dynamic.js", + "tests/corpus/node24-util-types.ts", + "tests/corpus/node24-util-types.js", + "tests/corpus/node24-util-types.cjs", + "tests/fixtures/node-types/util-types.ts" + ] }, "anchorSource": "exact" }, @@ -103176,8 +103374,16 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "compiler-feature:util.util.types.isFloat16Array", + "tests": [ + "tests/corpus/5831-util-types-brands.ts", + "tests/corpus/5832-util-types-brands-dynamic.js", + "tests/corpus/node24-util-types.ts", + "tests/corpus/node24-util-types.js", + "tests/corpus/node24-util-types.cjs", + "tests/fixtures/node-types/util-types.ts" + ] }, "anchorSource": "exact" }, @@ -103402,8 +103608,16 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "compiler-feature:util.util.types.isKeyObject", + "tests": [ + "tests/corpus/5831-util-types-brands.ts", + "tests/corpus/5832-util-types-brands-dynamic.js", + "tests/corpus/node24-util-types.ts", + "tests/corpus/node24-util-types.js", + "tests/corpus/node24-util-types.cjs", + "tests/fixtures/node-types/util-types.ts" + ] }, "anchorSource": "exact" }, @@ -103454,8 +103668,16 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "compiler-feature:util.util.types.isMapIterator", + "tests": [ + "tests/corpus/5831-util-types-brands.ts", + "tests/corpus/5832-util-types-brands-dynamic.js", + "tests/corpus/node24-util-types.ts", + "tests/corpus/node24-util-types.js", + "tests/corpus/node24-util-types.cjs", + "tests/fixtures/node-types/util-types.ts" + ] }, "anchorSource": "exact" }, @@ -103529,8 +103751,16 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "compiler-feature:util.util.types.isNumberObject", + "tests": [ + "tests/corpus/5831-util-types-brands.ts", + "tests/corpus/5832-util-types-brands-dynamic.js", + "tests/corpus/node24-util-types.ts", + "tests/corpus/node24-util-types.js", + "tests/corpus/node24-util-types.cjs", + "tests/fixtures/node-types/util-types.ts" + ] }, "anchorSource": "exact" }, @@ -103668,8 +103898,16 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "compiler-feature:util.util.types.isSetIterator", + "tests": [ + "tests/corpus/5831-util-types-brands.ts", + "tests/corpus/5832-util-types-brands-dynamic.js", + "tests/corpus/node24-util-types.ts", + "tests/corpus/node24-util-types.js", + "tests/corpus/node24-util-types.cjs", + "tests/fixtures/node-types/util-types.ts" + ] }, "anchorSource": "exact" }, @@ -103691,8 +103929,16 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "compiler-feature:util.util.types.isSharedArrayBuffer", + "tests": [ + "tests/corpus/5831-util-types-brands.ts", + "tests/corpus/5832-util-types-brands-dynamic.js", + "tests/corpus/node24-util-types.ts", + "tests/corpus/node24-util-types.js", + "tests/corpus/node24-util-types.cjs", + "tests/fixtures/node-types/util-types.ts" + ] }, "anchorSource": "exact" }, @@ -103714,8 +103960,16 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "compiler-feature:util.util.types.isStringObject", + "tests": [ + "tests/corpus/5831-util-types-brands.ts", + "tests/corpus/5832-util-types-brands-dynamic.js", + "tests/corpus/node24-util-types.ts", + "tests/corpus/node24-util-types.js", + "tests/corpus/node24-util-types.cjs", + "tests/fixtures/node-types/util-types.ts" + ] }, "anchorSource": "exact" }, @@ -103737,8 +103991,16 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "compiler-feature:util.util.types.isSymbolObject", + "tests": [ + "tests/corpus/5831-util-types-brands.ts", + "tests/corpus/5832-util-types-brands-dynamic.js", + "tests/corpus/node24-util-types.ts", + "tests/corpus/node24-util-types.js", + "tests/corpus/node24-util-types.cjs", + "tests/fixtures/node-types/util-types.ts" + ] }, "anchorSource": "exact" }, @@ -112848,6 +113110,22 @@ "tests/corpus/5793-worker-message-channel.ts", "tests/corpus/5796-worker-message-channel.cjs", "tests/corpus/5797-worker-message-port-transfer.ts", + "tests/corpus/5800-broadcast-channel.ts", + "tests/corpus/5801-broadcast-channel.cjs", + "tests/corpus/5802-broadcast-channel-workers.ts", + "tests/corpus/5803-worker-environment-data.ts", + "tests/corpus/5804-worker-environment-data.cjs", + "tests/corpus/5805-worker-receive-message-parent-port.ts", + "tests/corpus/5806-worker-resource-limits.ts", + "tests/corpus/5807-worker-error-values.ts", + "tests/corpus/5808-worker-unhandled-rejections.ts", + "tests/corpus/5870-worker-thread-names.ts", + "tests/corpus/5871-worker-untransferable.cjs", + "tests/corpus/5874-worker-stdio-streams.ts", + "tests/corpus/5875-worker-stdout-pipe.cjs", + "tests/corpus/5879-worker-stdin.ts", + "tests/corpus/5990-worker-uncloneable.mjs", + "tests/corpus/5991-worker-async-dispose.mjs", "tests/corpus/native-worker-clone-errors.ts", "tests/corpus/native-worker-commonjs.cjs", "tests/corpus/native-worker-delayed-messages.ts", @@ -112955,8 +113233,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:worker_threads" + "status": "partial", + "evidence": "compiler-feature:worker_threads.BroadcastChannel extends EventTarget", + "tests": [ + "tests/corpus/5800-broadcast-channel.ts", + "tests/corpus/5801-broadcast-channel.cjs", + "tests/corpus/5802-broadcast-channel-workers.ts" + ] }, "anchorSource": "exact" }, @@ -112978,8 +113261,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:worker_threads" + "status": "partial", + "evidence": "compiler-feature:worker_threads.broadcastChannel.close", + "tests": [ + "tests/corpus/5800-broadcast-channel.ts", + "tests/corpus/5801-broadcast-channel.cjs", + "tests/corpus/5802-broadcast-channel-workers.ts" + ] }, "anchorSource": "exact" }, @@ -113001,8 +113289,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:worker_threads" + "status": "partial", + "evidence": "compiler-feature:worker_threads.broadcastChannel.postMessage", + "tests": [ + "tests/corpus/5800-broadcast-channel.ts", + "tests/corpus/5801-broadcast-channel.cjs", + "tests/corpus/5802-broadcast-channel-workers.ts" + ] }, "anchorSource": "exact" }, @@ -113024,8 +113317,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:worker_threads" + "status": "partial", + "evidence": "compiler-feature:worker_threads.broadcastChannel.ref", + "tests": [ + "tests/corpus/5800-broadcast-channel.ts", + "tests/corpus/5801-broadcast-channel.cjs", + "tests/corpus/5802-broadcast-channel-workers.ts" + ] }, "anchorSource": "exact" }, @@ -113047,8 +113345,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:worker_threads" + "status": "partial", + "evidence": "compiler-feature:worker_threads.broadcastChannel.unref", + "tests": [ + "tests/corpus/5800-broadcast-channel.ts", + "tests/corpus/5801-broadcast-channel.cjs", + "tests/corpus/5802-broadcast-channel-workers.ts" + ] }, "anchorSource": "exact" }, @@ -113070,8 +113373,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:worker_threads" + "status": "partial", + "evidence": "compiler-feature:worker_threads.onmessage", + "tests": [ + "tests/corpus/5800-broadcast-channel.ts", + "tests/corpus/5801-broadcast-channel.cjs", + "tests/corpus/5802-broadcast-channel-workers.ts" + ] }, "anchorSource": "ancestor" }, @@ -113121,7 +113429,8 @@ "tests": [ "tests/corpus/5793-worker-message-channel.ts", "tests/corpus/5796-worker-message-channel.cjs", - "tests/corpus/5797-worker-message-port-transfer.ts" + "tests/corpus/5797-worker-message-port-transfer.ts", + "tests/corpus/5805-worker-receive-message-parent-port.ts" ] }, "anchorSource": "exact" @@ -113708,8 +114017,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:worker_threads" + "status": "partial", + "evidence": "compiler-feature:worker_threads.worker", + "tests": [ + "tests/corpus/5991-worker-async-dispose.mjs" + ] }, "anchorSource": "exact" }, @@ -113777,8 +114089,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:worker_threads" + "status": "partial", + "evidence": "compiler-feature:worker_threads.resourceLimits", + "tests": [ + "tests/corpus/5806-worker-resource-limits.ts" + ] }, "anchorSource": "ancestor" }, @@ -113800,8 +114115,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:worker_threads" + "status": "partial", + "evidence": "compiler-feature:worker_threads.stderr", + "tests": [ + "tests/corpus/5874-worker-stdio-streams.ts", + "tests/corpus/5875-worker-stdout-pipe.cjs", + "tests/corpus/5879-worker-stdin.ts" + ] }, "anchorSource": "ancestor" }, @@ -113823,8 +114143,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:worker_threads" + "status": "partial", + "evidence": "compiler-feature:worker_threads.stdin", + "tests": [ + "tests/corpus/5874-worker-stdio-streams.ts", + "tests/corpus/5875-worker-stdout-pipe.cjs", + "tests/corpus/5879-worker-stdin.ts" + ] }, "anchorSource": "ancestor" }, @@ -113846,8 +114171,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:worker_threads" + "status": "partial", + "evidence": "compiler-feature:worker_threads.stdout", + "tests": [ + "tests/corpus/5874-worker-stdio-streams.ts", + "tests/corpus/5875-worker-stdout-pipe.cjs", + "tests/corpus/5879-worker-stdin.ts" + ] }, "anchorSource": "ancestor" }, @@ -113897,8 +114227,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:worker_threads" + "status": "partial", + "evidence": "compiler-feature:worker_threads.threadName", + "tests": [ + "tests/corpus/5870-worker-thread-names.ts", + "tests/corpus/5871-worker-untransferable.cjs" + ] }, "anchorSource": "ancestor" }, @@ -113925,7 +114259,9 @@ "tests": [ "tests/corpus/native-worker-references.ts", "tests/corpus/native-worker-error.ts", - "tests/corpus/native-worker-exit-listeners.ts" + "tests/corpus/native-worker-exit-listeners.ts", + "tests/corpus/5807-worker-error-values.ts", + "tests/corpus/5808-worker-unhandled-rejections.ts" ] }, "anchorSource": "exact" @@ -113953,7 +114289,9 @@ "tests": [ "tests/corpus/native-worker-references.ts", "tests/corpus/native-worker-error.ts", - "tests/corpus/native-worker-exit-listeners.ts" + "tests/corpus/native-worker-exit-listeners.ts", + "tests/corpus/5807-worker-error-values.ts", + "tests/corpus/5808-worker-unhandled-rejections.ts" ] }, "anchorSource": "exact" @@ -113981,7 +114319,9 @@ "tests": [ "tests/corpus/native-worker-references.ts", "tests/corpus/native-worker-error.ts", - "tests/corpus/native-worker-exit-listeners.ts" + "tests/corpus/native-worker-exit-listeners.ts", + "tests/corpus/5807-worker-error-values.ts", + "tests/corpus/5808-worker-unhandled-rejections.ts" ] }, "anchorSource": "exact" @@ -114032,7 +114372,9 @@ "tests": [ "tests/corpus/native-worker-references.ts", "tests/corpus/native-worker-error.ts", - "tests/corpus/native-worker-exit-listeners.ts" + "tests/corpus/native-worker-exit-listeners.ts", + "tests/corpus/5807-worker-error-values.ts", + "tests/corpus/5808-worker-unhandled-rejections.ts" ] }, "anchorSource": "exact" @@ -114055,8 +114397,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:worker_threads" + "status": "partial", + "evidence": "compiler-feature:worker_threads.worker_threads.getEnvironmentData", + "tests": [ + "tests/corpus/5803-worker-environment-data.ts", + "tests/corpus/5804-worker-environment-data.cjs" + ] }, "anchorSource": "exact" }, @@ -114078,8 +114424,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:worker_threads" + "status": "partial", + "evidence": "compiler-feature:worker_threads.worker_threads.markAsUntransferable", + "tests": [ + "tests/corpus/5870-worker-thread-names.ts", + "tests/corpus/5871-worker-untransferable.cjs" + ] }, "anchorSource": "exact" }, @@ -114101,8 +114451,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:worker_threads" + "status": "partial", + "evidence": "compiler-feature:worker_threads.worker_threads.isMarkedAsUntransferable", + "tests": [ + "tests/corpus/5870-worker-thread-names.ts", + "tests/corpus/5871-worker-untransferable.cjs" + ] }, "anchorSource": "exact" }, @@ -114124,8 +114478,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:worker_threads" + "status": "partial", + "evidence": "compiler-feature:worker_threads.worker_threads.markAsUncloneable", + "tests": [ + "tests/corpus/5990-worker-uncloneable.mjs" + ] }, "anchorSource": "exact" }, @@ -114198,7 +114555,8 @@ "tests": [ "tests/corpus/5793-worker-message-channel.ts", "tests/corpus/5796-worker-message-channel.cjs", - "tests/corpus/5797-worker-message-port-transfer.ts" + "tests/corpus/5797-worker-message-port-transfer.ts", + "tests/corpus/5805-worker-receive-message-parent-port.ts" ] }, "anchorSource": "exact" @@ -114221,8 +114579,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:worker_threads" + "status": "partial", + "evidence": "compiler-feature:worker_threads.worker_threads.setEnvironmentData", + "tests": [ + "tests/corpus/5803-worker-environment-data.ts", + "tests/corpus/5804-worker-environment-data.cjs" + ] }, "anchorSource": "exact" }, @@ -114244,8 +114606,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:worker_threads" + "status": "partial", + "evidence": "compiler-feature:worker_threads.isInternalThread", + "tests": [ + "tests/corpus/5990-worker-uncloneable.mjs" + ] }, "anchorSource": "chapter" }, @@ -114323,8 +114688,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:worker_threads" + "status": "partial", + "evidence": "compiler-feature:worker_threads.resourceLimits", + "tests": [ + "tests/corpus/5806-worker-resource-limits.ts" + ] }, "anchorSource": "chapter" }, @@ -114397,8 +114765,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:worker_threads" + "status": "partial", + "evidence": "compiler-feature:worker_threads.threadName", + "tests": [ + "tests/corpus/5870-worker-thread-names.ts", + "tests/corpus/5871-worker-untransferable.cjs" + ] }, "anchorSource": "chapter" }, diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index d680113934..fded9de97b 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -104,6 +104,7 @@ "node-builtin.os.endianness": ["tests/corpus/5140-os-host-facts.ts"], "node-builtin.os.freemem": ["tests/corpus/5140-os-host-facts.ts"], "node-builtin.os.getPriority": ["tests/corpus/5140-os-host-facts.ts"], + "node-builtin.os.setPriority": ["tests/corpus/5883-os-set-priority.ts"], "node-builtin.os.hostname": ["tests/corpus/5140-os-host-facts.ts"], "node-builtin.os.loadavg": ["tests/corpus/5140-os-host-facts.ts"], "node-builtin.os.machine": ["tests/corpus/5140-os-host-facts.ts"], @@ -145,7 +146,7 @@ "node-builtin.process.versions": ["tests/corpus/process-versions-storage.ts"], "node-builtin.process.rows": ["tests/corpus/2992-stream-rows.ts", "tests/harness/terminal-geometry.test.ts"], "node-builtin.process.columns": ["tests/corpus/1448-stream-columns.ts", "tests/harness/terminal-geometry.test.ts"], - "node-builtin.process.exitCode": ["tests/corpus/2941-process-exit-code.ts", "tests/corpus/2942-process-exit-code-override.ts", "tests/corpus/2943-process-exit-code-invalid.ts", "tests/corpus/2944-process-exit-code-wrap.ts"], + "node-builtin.process.exitCode": ["tests/corpus/5809-process-exit-and-rejections.cjs", "tests/corpus/2941-process-exit-code.ts", "tests/corpus/2942-process-exit-code-override.ts", "tests/corpus/2943-process-exit-code-invalid.ts", "tests/corpus/2944-process-exit-code-wrap.ts"], "node-builtin.querystring": ["tests/corpus/957-builtins-namespace.ts", "tests/corpus/2917-builtin-callable-widths.ts"], "node-builtin.readline": ["tests/corpus/1475-readline-closed-stdin.ts"], "node-builtin.stream": ["tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1746-stream-for-await.ts", "tests/corpus/1814-stream-pipeline.cjs", "tests/corpus/2629-stream-consumers.ts", "tests/corpus/2861-readable-iterator-close.ts"], @@ -204,10 +205,20 @@ {"chapter": "worker_threads", "symbols": ["worker.terminate"], "status": "partial", "evidence": ["tests/corpus/native-worker-terminate-loop.ts", "tests/corpus/native-worker-terminate-wait.ts", "tests/corpus/native-worker-terminate-suspended.ts", "tests/corpus/native-worker-loop-cancellation.ts"]}, {"chapter": "worker_threads", "symbols": ["worker.ref", "worker.unref"], "status": "partial", "evidence": ["tests/corpus/native-worker-references.ts"]}, {"chapter": "worker_threads", "symbols": ["port.close", "port.start", "port.ref", "port.unref", "port.hasRef"], "status": "partial", "evidence": ["tests/corpus/native-worker-port-lifetime.ts"]}, - {"chapter": "worker_threads", "parent": "Class: `Worker`", "signatures": ["Event: `'online'`", "Event: `'message'`", "Event: `'error'`", "Event: `'exit'`"], "status": "partial", "evidence": ["tests/corpus/native-worker-references.ts", "tests/corpus/native-worker-error.ts", "tests/corpus/native-worker-exit-listeners.ts"]}, + {"chapter": "worker_threads", "parent": "Class: `Worker`", "signatures": ["Event: `'online'`", "Event: `'message'`", "Event: `'error'`", "Event: `'exit'`"], "status": "partial", "evidence": ["tests/corpus/native-worker-references.ts", "tests/corpus/native-worker-error.ts", "tests/corpus/native-worker-exit-listeners.ts", "tests/corpus/5807-worker-error-values.ts", "tests/corpus/5808-worker-unhandled-rejections.ts"]}, {"chapter": "worker_threads", "parent": "Class: `MessagePort`", "signatures": ["Event: `'message'`", "Event: `'close'`"], "status": "partial", "evidence": ["tests/corpus/native-worker-port-lifetime.ts", "tests/corpus/native-worker-messages.ts"]}, - {"chapter": "worker_threads", "symbols": ["MessageChannel", "worker_threads.receiveMessageOnPort"], "status": "partial", "evidence": ["tests/corpus/5793-worker-message-channel.ts", "tests/corpus/5796-worker-message-channel.cjs", "tests/corpus/5797-worker-message-port-transfer.ts"]}, + {"chapter": "worker_threads", "symbols": ["MessageChannel", "worker_threads.receiveMessageOnPort"], "status": "partial", "evidence": ["tests/corpus/5793-worker-message-channel.ts", "tests/corpus/5796-worker-message-channel.cjs", "tests/corpus/5797-worker-message-port-transfer.ts", "tests/corpus/5805-worker-receive-message-parent-port.ts"]}, + {"chapter": "worker_threads", "symbols": ["worker_threads.getEnvironmentData", "worker_threads.setEnvironmentData"], "status": "partial", "evidence": ["tests/corpus/5803-worker-environment-data.ts", "tests/corpus/5804-worker-environment-data.cjs"]}, + {"chapter": "worker_threads", "symbols": ["stdout", "stderr", "stdin"], "status": "partial", "evidence": ["tests/corpus/5874-worker-stdio-streams.ts", "tests/corpus/5875-worker-stdout-pipe.cjs", "tests/corpus/5879-worker-stdin.ts"]}, + {"chapter": "worker_threads", "symbols": ["threadName", "worker_threads.markAsUntransferable", "worker_threads.isMarkedAsUntransferable"], "status": "partial", "evidence": ["tests/corpus/5870-worker-thread-names.ts", "tests/corpus/5871-worker-untransferable.cjs"]}, + {"chapter": "worker_threads", "symbols": ["worker_threads.markAsUncloneable", "isInternalThread"], "status": "partial", "evidence": ["tests/corpus/5990-worker-uncloneable.mjs"]}, + {"chapter": "worker_threads", "signatures": ["`worker[Symbol.asyncDispose]()`"], "status": "partial", "evidence": ["tests/corpus/5991-worker-async-dispose.mjs"]}, + {"chapter": "worker_threads", "symbols": ["resourceLimits"], "status": "partial", "evidence": ["tests/corpus/5806-worker-resource-limits.ts"]}, + {"chapter": "worker_threads", "symbols": ["BroadcastChannel extends EventTarget", "broadcastChannel.close", "broadcastChannel.postMessage", "broadcastChannel.ref", "broadcastChannel.unref", "onmessage"], "status": "partial", "evidence": ["tests/corpus/5800-broadcast-channel.ts", "tests/corpus/5801-broadcast-channel.cjs", "tests/corpus/5802-broadcast-channel-workers.ts"]}, {"chapter": "util", "symbols": ["util.types.isAnyArrayBuffer", "util.types.isArrayBuffer", "util.types.isArrayBufferView", "util.types.isDataView", "util.types.isTypedArray", "util.types.isUint8Array", "util.types.isUint8ClampedArray", "util.types.isUint16Array", "util.types.isUint32Array", "util.types.isInt8Array", "util.types.isInt16Array", "util.types.isInt32Array", "util.types.isFloat32Array", "util.types.isFloat64Array", "util.types.isMap", "util.types.isSet", "util.types.isWeakMap", "util.types.isWeakSet", "util.types.isRegExp", "util.types.isNativeError", "util.types.isPromise", "util.types.isProxy", "util.types.isAsyncFunction", "util.types.isGeneratorFunction", "util.types.isGeneratorObject"], "status": "partial", "evidence": ["tests/corpus/node24-util-types.ts", "tests/corpus/node24-util-types.js", "tests/corpus/node24-util-types.cjs", "tests/fixtures/node-types/util-types.ts"]}, + {"chapter": "util", "symbols": ["util.format", "util.formatWithOptions"], "status": "partial", "evidence": ["tests/corpus/1636-util-format.ts", "tests/corpus/2440-console-inspect-args.ts", "tests/corpus/5835-util-format-integer-specifiers.ts"]}, + {"chapter": "util", "symbols": ["util.types.isAnyArrayBuffer", "util.types.isArrayBuffer", "util.types.isArrayBufferView", "util.types.isDataView", "util.types.isTypedArray", "util.types.isUint8Array", "util.types.isUint8ClampedArray", "util.types.isUint16Array", "util.types.isUint32Array", "util.types.isInt8Array", "util.types.isInt16Array", "util.types.isInt32Array", "util.types.isFloat32Array", "util.types.isFloat64Array", "util.types.isMap", "util.types.isSet", "util.types.isWeakMap", "util.types.isWeakSet", "util.types.isRegExp", "util.types.isNativeError", "util.types.isPromise", "util.types.isProxy", "util.types.isAsyncFunction", "util.types.isGeneratorFunction", "util.types.isGeneratorObject", "util.types.isDate", "util.types.isSharedArrayBuffer", "util.types.isMapIterator", "util.types.isSetIterator", "util.types.isKeyObject", "util.types.isNumberObject", "util.types.isStringObject", "util.types.isBooleanObject", "util.types.isBigIntObject", "util.types.isSymbolObject", "util.types.isBoxedPrimitive", "util.types.isBigInt64Array", "util.types.isBigUint64Array", "util.types.isFloat16Array", "util.types.isCryptoKey", "util.types.isExternal"], "status": "partial", "evidence": ["tests/corpus/5831-util-types-brands.ts", "tests/corpus/5832-util-types-brands-dynamic.js", "tests/corpus/node24-util-types.ts", "tests/corpus/node24-util-types.js", "tests/corpus/node24-util-types.cjs", "tests/fixtures/node-types/util-types.ts"]}, + { "chapter": "util", "signatures": ["`util.inspect(object[, options])`"], "status": "partial", "evidence": ["tests/corpus/1630-inspect-scalars.ts", "tests/corpus/1632-inspect-records.ts", "tests/corpus/1637-inspect-dyn.ts", "tests/corpus/2485-inspect-circular-refs.ts", "tests/corpus/5884-inspect-layout-options.ts"] }, { "chapter": "util", "symbols": ["util.styleText"], "status": "partial", "evidence": ["tests/corpus/node24-style-text.ts", "tests/corpus/node24-style-text-validation.js", "tests/corpus/node24-style-text-environment.js", "tests/corpus/node24-style-text.cjs", "tests/fixtures/node-types/style-text.ts"] }, { "chapter": "util", "symbols": ["util.isDeepStrictEqual"], "status": "partial", "evidence": ["tests/corpus/node24-deep-equality.ts", "tests/corpus/node24-deep-equality.cjs", "tests/corpus/node24-deep-equality-properties.js", "tests/fixtures/node-types/deep-equality.ts", "packages/compiler/test/deep-equality.test.ts"] }, { "chapter": "deprecations", "symbols": ["util.toUSVString"], "status": "not-applicable" }, @@ -300,7 +311,7 @@ { "chapter": "modules", "symbols": ["cache", "children", "id", "isPreloading", "loaded", "parent", "path", "paths", "Module"], "status": "partial", "evidence": ["tests/corpus/2880-cjs-module-graph/main.cjs"] }, { "chapter": "modules", "symbols": ["extensions"], "status": "not-implemented" }, { "chapter": "module", "symbols": ["module.createRequire"], "status": "partial", "evidence": ["tests/corpus/2631-create-require/main.ts", "tests/corpus/2637-create-require-bare/main.ts", "tests/corpus/2675-create-require-nested-esm.js", "tests/corpus/2932-create-require-program/main.ts", "tests/harness/npm-static.test.ts"] }, - { "chapter": "module", "symbols": ["module.isBuiltin", "module.syncBuiltinESMExports"], "status": "partial", "evidence": ["tests/corpus/2932-create-require-program/main.ts"] }, + { "chapter": "module", "symbols": ["module.isBuiltin", "module.syncBuiltinESMExports"], "status": "partial", "evidence": ["tests/corpus/2932-create-require-program/main.ts", "tests/corpus/5846-events-esm-default-max.ts"] }, { "chapter": "module", "symbols": ["builtinModules"], "status": "partial", "evidence": ["tests/corpus/2631-create-require/main.ts", "tests/corpus/2637-create-require-bare/main.ts", "tests/corpus/2932-create-require-program/main.ts"] }, { "chapter": "modules", "symbols": ["node:", "node_modules"], "status": "partial", "evidence": ["tests/corpus/1598-cjs-builtin-require/main.js", "tests/corpus/1596-cjs-modules/main.js"] }, { "chapter": "esm", "symbols": ["import", "file:", "node:", "await"], "status": "partial", "evidence": ["tests/corpus/950-modules-basic/main.ts", "tests/corpus/2646-top-level-await.ts", "tests/corpus/2933-static-dynamic-import/main.ts", "tests/corpus/2934-static-import-stored.ts", "tests/corpus/2935-module-namespace-values/main.ts"] }, @@ -311,6 +322,7 @@ { "chapter": "packages", "symbols": ["--input-type"], "status": "not-applicable" }, { "chapter": "child_process", "signatures": ["`subprocess[Symbol.dispose]()`"], "status": "partial", "evidence": ["tests/corpus/2844-node-disposable-resources.ts"] }, { "chapter": "child_process", "signatures": ["`child_process.execFile(file[, args][, options][, callback])`"], "status": "partial", "evidence": ["tests/corpus/2911-execfile-callback.ts"] }, + { "chapter": "child_process", "signatures": ["`child_process.exec(command[, options][, callback])`"], "status": "partial", "evidence": ["tests/corpus/5627-exec-callback.ts", "tests/fixtures/node-types/child-exec.ts", "tests/harness/project-config.test.ts"] }, { "chapter": "child_process", "signatures": ["`child_process.fork(modulePath[, args][, options])`", "`subprocess.send(message[, sendHandle[, options]][, callback])`", "`subprocess.disconnect()`", "`connected` Type: {boolean} Set to `false` after `subprocess.disconnect()` is called.", "Event: `'message'`", "Event: `'disconnect'`"], "status": "partial", "evidence": ["tests/corpus/2946-child-fork-ipc/main.ts"] }, { "chapter": "child_process", "signatures": ["Event: `'close'`"], "status": "partial", "evidence": ["tests/corpus/2939-child-shell-text.ts", "tests/corpus/3131-child-rest-listeners.js"] }, { "chapter": "child_process", "signatures": ["`stdout` Type: {stream.Readable|null|undefined}", "`stderr` Type: {stream.Readable|null|undefined}"], "status": "partial", "evidence": ["tests/corpus/1565-spawn-pipe-streams.ts", "tests/corpus/2939-child-shell-text.ts"] }, @@ -337,14 +349,18 @@ { "chapter": "webstreams", "signatures": ["`streamConsumers.arrayBuffer(stream)`"], "status": "partial", "evidence": ["tests/corpus/4875-stream-consumers-array-buffer.ts"] }, { "chapter": "url", "symbols": ["url.fileURLToPath", "url.fileURLToPathBuffer", "url.pathToFileURL"], "status": "partial", "evidence": ["tests/corpus/node24-file-url-buffer.ts", "tests/corpus/node24-file-url-options.ts", "tests/corpus/node24-file-url-errors.js", "tests/fixtures/node-types/file-url-conversion.ts"] }, { "chapter": "url", "symbols": ["URL.canParse", "URL.parse", "url.toString", "url.toJSON"], "status": "partial", "evidence": ["tests/corpus/node24-url-factories.ts", "tests/corpus/node24-url-factory-errors.ts", "tests/fixtures/node-types/url-factories.ts", "tests/harness/project-config.test.ts"] }, + { "chapter": "url", "symbols": ["urlSearchParams.entries", "urlSearchParams.keys", "urlSearchParams.values"], "status": "partial", "evidence": ["tests/corpus/5833-url-search-params-iterators.ts", "tests/corpus/5834-url-search-params-iterators-dynamic.mjs"] }, { "chapter": "url", "signatures": ["Class: `URL`"], "status": "partial", "evidence": ["tests/corpus/url-runtime-base.ts", "tests/corpus/node24-url-mutation.ts", "tests/corpus/node24-url-mutation-coercion.js", "tests/fixtures/node-types/url-mutation.ts"] }, { "chapter": "url", "parent": "Class: `URL`", "signatures": ["`protocol` Type: {string}", "`username` Type: {string}", "`password` Type: {string}", "`host` Type: {string}", "`hostname` Type: {string}", "`port` Type: {string}"], "status": "partial", "evidence": ["tests/corpus/node24-url-authority.ts", "tests/corpus/node24-url-authority-coercion.js", "tests/fixtures/node-types/url-mutation.ts"] }, { "chapter": "url", "parent": "Class: `URL`", "signatures": ["`href` Type: {string}", "`pathname` Type: {string}", "`search` Type: {string}", "`hash` Type: {string}"], "status": "partial", "evidence": ["tests/corpus/node24-url-mutation.ts", "tests/corpus/node24-url-mutation-coercion.js", "tests/fixtures/node-types/url-mutation.ts"] }, { "chapter": "url", "signatures": ["`port` Type: {string}"], "status": "partial", "evidence": ["tests/corpus/2946-url-port-hash.ts"] }, { "chapter": "url", "signatures": ["Legacy `urlObject`", "`urlObject.auth`", "`urlObject.hash`", "`urlObject.host`", "`urlObject.hostname`", "`urlObject.href`", "`urlObject.path`", "`urlObject.pathname`", "`urlObject.port`", "`urlObject.protocol`", "`urlObject.query`", "`urlObject.search`", "`urlObject.slashes`"], "status": "partial", "evidence": ["tests/corpus/5221-url-parse-shapes.ts", "tests/corpus/5222-url-parse-hosts.ts", "tests/corpus/5223-url-parse-query.ts", "tests/corpus/5224-url-format-objects.ts", "tests/fixtures/node-types/url-legacy.ts"] }, + { "chapter": "test", "parent": "`context.assert`", "symbols": ["context.assert.snapshot", "context.assert.fileSnapshot"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/snapshot.ts", "tests/harness/node-test.test.ts"] }, { "chapter": "test", "parent": "Class: `TestContext`", "symbols": ["context.diagnostic", "context.name"], "status": "partial", "evidence": ["tests/harness/node-test.test.ts"] }, { "chapter": "test", "parent": "Class: `MockTracker`", "symbols": ["mock.fn"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-fn.ts", "tests/harness/node-test.test.ts"] }, { "chapter": "test", "parent": "Class: `MockTracker`", "symbols": ["mock.method", "mock.reset", "mock.restoreAll"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-method.ts", "tests/corpus/5810-mock-method-records.ts", "tests/corpus/5811-mock-tracker-restore.ts", "tests/harness/node-test.test.ts"] }, + { "chapter": "test", "parent": "Class: `MockTimers`", "symbols": ["timers.enable", "timers.reset", "timers.tick", "timers.runAll", "timers.setTime"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-timers.ts", "tests/fixtures/node-test/cases/mock-timers-date.ts", "tests/corpus/5813-mock-timers-module.ts", "tests/corpus/5814-mock-timers-date.ts", "tests/harness/node-test.test.ts"] }, + { "chapter": "test", "parent": "Class: `MockTimers`", "signatures": ["`timers[Symbol.dispose]()`"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-timers-date.ts", "tests/harness/node-test.test.ts"] }, { "chapter": "test", "parent": "Class: `MockFunctionContext`", "symbols": ["calls", "ctx.callCount", "ctx.mockImplementation", "ctx.mockImplementationOnce", "ctx.resetCalls", "ctx.restore"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-fn.ts", "tests/fixtures/node-test/cases/mock-method.ts", "tests/harness/node-test.test.ts"] }, { "chapter": "test", "parent": "Class: `TestContext`", "symbols": ["context.before", "context.after", "context.beforeEach", "context.afterEach", "context.plan", "context.fullName"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/ctx-hooks.ts", "tests/fixtures/node-test/cases/ctx-plan.ts", "tests/harness/node-test.test.ts"] }, { "chapter": "test", "symbols": ["describe.skip", "describe.todo", "describe.only", "suite.skip", "suite.todo", "suite.only", "it.skip", "it.todo", "it.only"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/suite-variants.ts", "tests/fixtures/node-test/cases/only-nested.ts", "tests/fixtures/node-test/cases/todo-suites.ts", "tests/harness/node-test.test.ts"] }, @@ -359,7 +375,7 @@ { "symbols": ["console.log", "console.info", "console.debug", "console.error", "console.warn"], "status": "partial", "evidence": ["tests/corpus/1460-console-error-warn.ts", "tests/corpus/2440-console-inspect-args.ts", "tests/corpus/2631-create-require/main.ts", "tests/corpus/console-native-value.js", "tests/harness/npm-static.test.ts"] }, { "symbols": ["console.count", "console.countReset", "console.group", "console.groupCollapsed", "console.groupEnd"], "status": "partial", "evidence": ["tests/corpus/5095-console-count-group.ts", "tests/corpus/5098-console-time-warnings.ts", "tests/corpus/5099-console-methods-dynamic.cjs"] }, { "symbols": ["console.time", "console.timeEnd", "console.timeLog"], "status": "partial", "evidence": ["tests/corpus/5098-console-time-warnings.ts"] }, - { "symbols": ["console.assert", "console.dir", "console.dirxml"], "status": "partial", "evidence": ["tests/corpus/5096-console-assert-dir.ts", "tests/corpus/5099-console-methods-dynamic.cjs"] }, + { "symbols": ["console.assert", "console.dir", "console.dirxml"], "status": "partial", "evidence": ["tests/corpus/5096-console-assert-dir.ts", "tests/corpus/5099-console-methods-dynamic.cjs", "tests/corpus/5884-inspect-layout-options.ts"] }, { "symbols": ["console.table"], "status": "partial", "evidence": ["tests/corpus/5097-console-table.ts", "tests/corpus/5099-console-methods-dynamic.cjs"] }, { "symbols": ["console.clear", "console.profile", "console.profileEnd", "console.timeStamp"], "status": "partial", "evidence": ["tests/corpus/5099-console-methods-dynamic.cjs"] }, { "symbols": ["assert.AssertionError"], "status": "partial", "evidence": ["tests/corpus/5510-assert-error-properties.js", "tests/corpus/5511-assert-error-class.ts", "tests/corpus/5512-assert-error-options.js"] }, @@ -375,9 +391,9 @@ { "symbols": ["crypto.randomInt"], "status": "partial", "evidence": ["tests/corpus/5442-crypto-random-values.ts", "tests/corpus/2890-crypto-static-utility.ts"] }, { "symbols": ["crypto.hkdf", "crypto.hkdfSync", "crypto.scrypt", "crypto.scryptSync"], "status": "partial", "evidence": ["tests/corpus/node24-crypto-kdf.ts", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/5438-crypto-kdf-digests.ts"] }, { "symbols": ["EventEmitter", "emitter.on", "emitter.addListener", "emitter.once", "emitter.prependListener", "emitter.prependOnceListener", "emitter.off", "emitter.removeListener", "emitter.removeAllListeners", "emitter.emit", "emitter.listenerCount", "emitter.listeners", "emitter.rawListeners", "emitter.eventNames", "emitter.setMaxListeners", "emitter.getMaxListeners", "events.once"], "status": "partial", "evidence": ["tests/corpus/1677-emitter-listeners.ts", "tests/corpus/2620-ee-override-once-order.ts"] }, - { "symbols": ["events.getEventListeners", "events.getMaxListeners", "events.listenerCount", "events.setMaxListeners", "events.defaultMaxListeners"], "status": "partial", "evidence": ["tests/corpus/5885-events-max-listener-helpers.ts"] }, + { "symbols": ["events.getEventListeners", "events.getMaxListeners", "events.listenerCount", "events.setMaxListeners", "events.defaultMaxListeners"], "status": "partial", "evidence": ["tests/corpus/5885-events-max-listener-helpers.ts", "tests/corpus/5846-events-esm-default-max.ts", "tests/corpus/5847-events-cjs-default-max.cjs"] }, { "symbols": ["AsyncLocalStorage", "asyncLocalStorage.run", "asyncLocalStorage.exit", "asyncLocalStorage.getStore", "asyncLocalStorage.enterWith", "asyncLocalStorage.disable"], "status": "partial", "evidence": ["tests/corpus/2213-async-local-storage.cjs"] }, - { "symbols": ["StringDecoder", "stringDecoder.write", "stringDecoder.end"], "status": "partial", "evidence": ["tests/corpus/1662-string-decoder-encodings.ts", "tests/corpus/node24-string-decoder.ts"] }, + { "symbols": ["StringDecoder", "stringDecoder.write", "stringDecoder.end"], "status": "partial", "evidence": ["tests/corpus/1662-string-decoder-encodings.ts", "tests/corpus/node24-string-decoder.ts", "tests/corpus/5882-string-decoder-runtime-encoding.ts"] }, { "symbols": ["setImmediate", "setInterval", "setTimeout", "clearImmediate", "clearInterval", "clearTimeout", "timersPromises.setTimeout", "timersPromises.setImmediate", "timersPromises.setInterval", "immediate.hasRef", "immediate.ref", "immediate.unref", "timeout.hasRef", "timeout.ref", "timeout.unref", "timeout.refresh"], "status": "partial", "evidence": ["tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", "tests/corpus/2093-timers-promises.ts", "tests/corpus/2852-timers-promises-interval.ts", "tests/corpus/5830-timers-promises-signal.ts"] }, { "symbols": ["timersPromises.scheduler.wait", "timersPromises.scheduler.yield"], "status": "partial", "evidence": ["tests/corpus/5839-timers-promises-scheduler.ts", "tests/corpus/5889-timers-scheduler-signal.ts"] }, { "symbols": ["stream.finished", "stream.pipeline", "stream.getDefaultHighWaterMark", "readable.push", "readable.unshift", "readable.read", "readable.pause", "readable.resume", "readable.isPaused", "readable.setEncoding", "readable.pipe", "readable.unpipe", "readable.destroy", "writable.write", "writable.end", "writable.cork", "writable.uncork", "writable.setDefaultEncoding"], "status": "partial", "evidence": ["tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", "tests/corpus/1814-stream-pipeline.cjs", "tests/corpus/5302-stream-promises-options.ts", "tests/corpus/5303-stream-finished-callback-opts.cjs", "tests/corpus/5425-pipeline-generator-stage.ts", "tests/corpus/5428-pipeline-generator-stages.cjs", "tests/corpus/5617-pipeline-function-ends.ts", "tests/corpus/5619-pipeline-function-ends.cjs"] }, diff --git a/native/llvm-codegen/README.md b/native/llvm-codegen/README.md index 5b8f38f2ac..8debaeeaf2 100644 --- a/native/llvm-codegen/README.md +++ b/native/llvm-codegen/README.md @@ -35,6 +35,8 @@ requested output. Repeating `--output` divides the program into that many partitions, which are compiled concurrently and linked together. The helper splits the emitter's LLVM assembly text directly, so partitions also parse concurrently; input outside the emitter's narrow grammar is parsed whole and split with LLVM instead. Internal definitions become hidden so partitions can reference each other. At opt-level 0 each partition is compiled independently. Optimized partitions follow the ThinLTO model: each is simplified and summarized on its own thread, a thin link decides which small functions every partition imports for inlining and which hidden definitions become internal again, and each partition is then optimized and compiled on its own thread. Partition placement depends only on the module and the partition count, so output never depends on the host. +WebAssembly code generation differs from LLVM's default pipeline in two ways that do not change its output. The pass that gives multiple-entry loops a single entry runs only on functions whose control flow is irreducible; its reachability analysis grows with the square of a function's block count even when it has nothing to change. Object emission also leaves out the module pass that collects external symbols for assembly declarations, so each function's machine code is freed once it is printed instead of when the whole module is compiled. + `--cache-dir ` names a private directory the caller owns for one helper build. Simplified partitions are stored under the hash of their exact source and objects under the hash of the exact module after importing, each with a trailing digest and published by rename, so a rebuild recompiles only the partitions an edit can affect. Unsanitized `speed` program builds, and unsanitized `release` builds of programs small enough for one partition, pass `--import-bitcode ` once per runtime-pack unit the program links that ships bitcode. Small runtime functions reachable from the program's runtime calls are imported as `available_externally` so LLVM can inline them; the runtime objects remain the only definitions. The helper never runs a sanitizer pass, so with import it also drops the emitter's inert `sanitize_address` attributes, which would otherwise block that inlining. Without `--import-bitcode` the module is optimized exactly as emitted. diff --git a/native/llvm-codegen/src/emit.cpp b/native/llvm-codegen/src/emit.cpp index b39d2c1bf0..a5f87e4b64 100644 --- a/native/llvm-codegen/src/emit.cpp +++ b/native/llvm-codegen/src/emit.cpp @@ -10,9 +10,15 @@ #include "llvm/ADT/StringExtras.h" #include "llvm/ADT/StringRef.h" #include "llvm/ADT/StringSet.h" +#include "llvm/Analysis/RuntimeLibcallInfo.h" +#include "llvm/Analysis/TargetLibraryInfo.h" #include "llvm/Bitcode/BitcodeReader.h" #include "llvm/Bitcode/BitcodeWriter.h" #include "llvm/Bitcode/BitcodeWriterPass.h" +#include "llvm/CodeGen/MachineDominators.h" +#include "llvm/CodeGen/MachineFunction.h" +#include "llvm/CodeGen/MachineFunctionPass.h" +#include "llvm/CodeGen/MachineModuleInfo.h" #include "llvm/IR/DiagnosticInfo.h" #include "llvm/IR/DiagnosticPrinter.h" #include "llvm/IR/LegacyPassManager.h" @@ -20,6 +26,9 @@ #include "llvm/IR/Verifier.h" #include "llvm/IRReader/IRReader.h" #include "llvm/LTO/LTO.h" +#include "llvm/Pass.h" +#include "llvm/PassInfo.h" +#include "llvm/PassRegistry.h" #include "llvm/Passes/PassBuilder.h" #include "llvm/Support/Caching.h" #include "llvm/Support/FileSystem.h" @@ -140,6 +149,138 @@ std::optional verify(Module &M, StringRef Code) { return std::nullopt; } +// Runs WebAssembly's pass that gives multiple-entry loops a single entry only +// on functions that have such a loop. The pass first records, for every +// block, every block it can reach, so its time and memory grow with the +// square of the block count even when it changes nothing: a module +// initializer of 40,000 blocks took four minutes and 22 GB in it. It changes +// a function only when two loop entries can reach each other, which a +// reducible control flow graph never has, so skipping reducible functions +// leaves the output unchanged. +class IrreducibleControlFlowOnly final : public MachineFunctionPass { +public: + static char ID; + + explicit IrreducibleControlFlowOnly(FunctionPass *Fix) + : MachineFunctionPass(ID), Fix(Fix) {} + + StringRef getPassName() const override { return Fix->getPassName(); } + + bool doInitialization(Module &M) override { + MachineFunctionPass::doInitialization(M); + return Fix->doInitialization(M); + } + + bool doFinalization(Module &M) override { return Fix->doFinalization(M); } + + void getAnalysisUsage(AnalysisUsage &Usage) const override { + Fix->getAnalysisUsage(Usage); + } + + bool runOnMachineFunction(MachineFunction &MF) override { + if (isReducible(MF)) + return false; + // The wrapped pass reads the machine function through the module's + // machine code analysis, which this pass requires on its behalf. + if (Fix->getResolver() == nullptr) { + auto *Resolver = new AnalysisResolver(getResolver()->getPMDataManager()); + Resolver->addAnalysisImplsPair( + &MachineModuleInfoWrapperPass::ID, + &getAnalysis()); + Fix->setResolver(Resolver); + } + return Fix->runOnFunction(MF.getFunction()); + } + +private: + // A graph is reducible when every edge that a depth-first search finds + // going back to a block on its stack targets a block that dominates the + // edge's source. Blocks the entry cannot reach count as irreducible, since + // the wrapped pass also examines them. + static bool isReducible(MachineFunction &MF) { + MachineDominatorTree Dominators(MF); + enum class State : unsigned char { New, Active, Done }; + std::vector States(MF.getNumBlockIDs(), State::New); + SmallVector, + 32> + Stack; + size_t Visited = 1; + MachineBasicBlock *Entry = &MF.front(); + States[Entry->getNumber()] = State::Active; + Stack.push_back({Entry, Entry->succ_begin()}); + while (!Stack.empty()) { + auto &[Block, Next] = Stack.back(); + if (Next == Block->succ_end()) { + States[Block->getNumber()] = State::Done; + Stack.pop_back(); + continue; + } + MachineBasicBlock *Successor = *Next++; + State &Seen = States[Successor->getNumber()]; + if (Seen == State::Active) { + if (!Dominators.dominates(Successor, Block)) + return false; + } else if (Seen == State::New) { + Seen = State::Active; + ++Visited; + Stack.push_back({Successor, Successor->succ_begin()}); + } + } + return Visited == MF.size(); + } + + std::unique_ptr Fix; +}; + +char IrreducibleControlFlowOnly::ID = 0; + +// The code generation pipeline for one module, with two changes for +// WebAssembly. The irreducible control flow pass runs only where it can +// change the function (see IrreducibleControlFlowOnly). Object emission also +// leaves out the last machine pass, a module pass that records the external +// symbols every function uses so that assembly output can declare them +// before the first function. A module pass in the middle of the pipeline +// keeps the machine code of every function alive until all of them are +// compiled. Object files do not contain those declarations, and the printer +// creates each symbol when it lowers the first use, so without the pass each +// function's machine code is freed once it is printed. +class CodeGenPasses final : public legacy::PassManagerBase { +public: + CodeGenPasses(const TargetMachine &Machine, CodeGenFileType Type) + : Wasm(Machine.getTargetTriple().isWasm()), + Object(Type == CodeGenFileType::ObjectFile) {} + + void add(Pass *P) override { + if (Wasm) { + StringRef Argument = passArgument(*P); + if (Argument == "wasm-fix-irreducible-control-flow") { + Passes.add( + new IrreducibleControlFlowOnly(static_cast(P))); + return; + } + if (Object && Argument == "wasm-mclower-prepass") { + delete P; + return; + } + } + Passes.add(P); + } + + void run(Module &M) { Passes.run(M); } + +private: + static StringRef passArgument(const Pass &P) { + const PassInfo *Info = + PassRegistry::getPassRegistry()->getPassInfo(P.getPassID()); + return Info == nullptr ? StringRef() : Info->getPassArgument(); + } + + bool Wasm; + bool Object; + legacy::PassManager Passes; +}; + // Writes code for an optimized module to a unique sibling of OutputPath, // which is appended to Temporaries once it exists. std::optional @@ -153,7 +294,7 @@ generateCode(Module &M, TargetMachine &Machine, CodeGenFileType Type, return EmitFailure{"output_open_failed", EC.message()}; Temporaries.push_back(TemporaryPath); raw_fd_ostream Output(TemporaryFd, true); - legacy::PassManager CodeGeneration; + CodeGenPasses CodeGeneration(Machine, Type); if (Machine.addPassesToEmitFile(CodeGeneration, Output, nullptr, Type)) return EmitFailure{"emission_not_supported", "target does not support the requested file type"}; @@ -325,7 +466,7 @@ compileUnoptimizedPartitions(const EmitOptions &Options, P.PB.buildPerModuleDefaultPipeline(OptimizationLevel::O0) .run(**Mod, P.MAM); raw_svector_ostream Output(Objects[I]); - legacy::PassManager CodeGeneration; + CodeGenPasses CodeGeneration(*Machine, CodeGenFileType::ObjectFile); if (Machine->addPassesToEmitFile(CodeGeneration, Output, nullptr, CodeGenFileType::ObjectFile)) return Failures.set({"emission_not_supported", @@ -437,6 +578,44 @@ compileOptimizedPartitions(const EmitOptions &Options, return true; }; + // The backend's own code generation cannot use CodeGenPasses, so + // WebAssembly partitions are compiled here with the passes it would add, + // and returning false skips it. + if (TargetTriple.isWasm()) + Config.PreCodeGenModuleHook = [&](unsigned Task, const Module &Optimized) { + size_t I = Task - 1; + if (I >= Count) + return true; + std::string LookupError; + std::unique_ptr Machine = + createTargetMachine(Options.Target, Options.OptLevel, LookupError); + if (!Machine) { + Failures.set({"target_machine_failed", LookupError}); + return false; + } + // The backend passes the module it owns; only the hook's signature is + // const. + Module &Mod = const_cast(Optimized); + Objects[I].clear(); + raw_svector_ostream Output(Objects[I]); + CodeGenPasses CodeGeneration(*Machine, CodeGenFileType::ObjectFile); + TargetLibraryInfoImpl LibraryInfo(Mod.getTargetTriple(), + Machine->Options.VecLib); + CodeGeneration.add(new TargetLibraryInfoWrapperPass(LibraryInfo)); + CodeGeneration.add(new RuntimeLibraryInfoWrapper( + Mod.getTargetTriple(), Machine->Options.ExceptionModel, + Machine->Options.FloatABIType, Machine->Options.EABIVersion, + Machine->Options.MCOptions.ABIName, Machine->Options.VecLib)); + if (Machine->addPassesToEmitFile(CodeGeneration, Output, nullptr, + CodeGenFileType::ObjectFile)) { + Failures.set({"emission_not_supported", + "target does not support object emission"}); + return false; + } + CodeGeneration.run(Mod); + return false; + }; + // The link refers to module identifiers for its whole lifetime. std::vector Names(Count); for (size_t I = 0; I < Count; ++I) diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index d87172f610..69093a6007 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -708,6 +708,7 @@ interface MessageEvent extends Event { readonly data: T; readonly origin: string; readonly lastEventId: string; + readonly source: unknown; readonly ports: readonly MessagePort[]; } declare var MessageEvent: { @@ -2109,6 +2110,8 @@ declare module "os" { export function version(): string; export function endianness(): "BE" | "LE"; export function getPriority(pid?: number): number; + export function setPriority(priority: number): void; + export function setPriority(pid: number, priority: number): void; /* The TARGET platform's null device ("\\\\.\\nul" under a win32 triple). */ export const devNull: string; /* The platform's constants: null-prototype objects with read-only @@ -3030,10 +3033,10 @@ declare module "child_process" { args?: string[] | null, options?: ExecSyncOptions, ): unknown; - /* The async shell form — declared surface without full lowering (the - * type-level tolerance story): harness code on paths tests don't reach - * must typecheck; a reached call fences at its site. util.promisify - * recognizes it statically, as it does execFile. */ + /* The async shell form. The callback forms lower like execFile's, with + * the command line run by /bin/sh -c, and util.promisify recognizes it + * statically, as it does execFile; reached calls without a callback + * fence at their site. */ export function exec( command: string, options?: ExecSyncOptions | ((error: Error | null, stdout: string, stderr: string) => void), @@ -3250,6 +3253,7 @@ declare module "util/types" { export function isInt8Array(value: unknown): value is Int8Array; export function isInt16Array(value: unknown): value is Int16Array; export function isInt32Array(value: unknown): value is Int32Array; + export function isFloat16Array(value: unknown): value is Float16Array; export function isFloat32Array(value: unknown): value is Float32Array; export function isFloat64Array(value: unknown): value is Float64Array; export function isMap(value: unknown): value is Map; @@ -3264,6 +3268,21 @@ declare module "util/types" { export function isGeneratorFunction(value: unknown): boolean; export function isGeneratorObject(value: unknown): boolean; export function isModuleNamespaceObject(value: unknown): boolean; + export function isDate(value: unknown): value is Date; + export function isSharedArrayBuffer(value: unknown): value is SharedArrayBuffer; + export function isMapIterator(value: unknown): boolean; + export function isSetIterator(value: unknown): boolean; + export function isKeyObject(value: unknown): boolean; + export function isNumberObject(value: unknown): boolean; + export function isStringObject(value: unknown): boolean; + export function isBooleanObject(value: unknown): boolean; + export function isBigIntObject(value: unknown): boolean; + export function isSymbolObject(value: unknown): boolean; + export function isBoxedPrimitive(value: unknown): boolean; + export function isBigInt64Array(value: unknown): value is BigInt64Array; + export function isBigUint64Array(value: unknown): value is BigUint64Array; + export function isCryptoKey(value: unknown): boolean; + export function isExternal(value: unknown): boolean; } declare module "node:util/types" { export * from "util/types"; @@ -3319,7 +3338,12 @@ declare module "node:test" { beforeEach(fn: HookFn): void; afterEach(fn: HookFn): void; } + interface AssertSnapshotOptions { + readonly serializers?: ReadonlyArray<(value: any) => any> | undefined; + } interface TestContextAssert { + snapshot(value: any, options?: AssertSnapshotOptions): void; + fileSnapshot(value: any, path: string, options?: AssertSnapshotOptions): void; ok(value: unknown, message?: string): void; strictEqual(actual: unknown, expected: unknown, message?: string): void; notStrictEqual(actual: unknown, expected: unknown, message?: string): void; @@ -3410,6 +3434,19 @@ declare module "node:test" { : never; reset(): void; restoreAll(): void; + readonly timers: MockTimers; + } + interface MockTimersOptions { + apis: ReadonlyArray<"setInterval" | "setTimeout" | "setImmediate" | "Date">; + now?: number | Date | undefined; + } + interface MockTimers { + enable(options?: MockTimersOptions): void; + setTime(milliseconds?: number): void; + reset(): void; + tick(milliseconds?: number): void; + runAll(): void; + [Symbol.dispose](): void; } const mock: MockTracker; /* Declared for typechecking only — every use fences (no lowering). @@ -3437,6 +3474,8 @@ declare module "node:test" { MockFunctionContext, MockFunctionCall, MockTracker, + MockTimers, + MockTimersOptions, }; export default test; } @@ -3618,7 +3657,8 @@ declare module "querystring" { | string | number | boolean - | ReadonlyArray + | bigint + | ReadonlyArray | null | undefined; } @@ -5806,19 +5846,42 @@ declare module "worker_threads" { export const isMainThread: boolean; export const isInternalThread: boolean; export const threadId: number; + export interface ResourceLimits { + maxYoungGenerationSizeMb?: number; + maxOldGenerationSizeMb?: number; + codeRangeSizeMb?: number; + stackSizeMb?: number; + } + /* {} in the main thread */ + export const resourceLimits: ResourceLimits; + /* the Worker's name option, trimmed ("WorkerThread" by default); "" in the main thread */ + export const threadName: string | null; export interface WorkerOptions { workerData?: unknown; transferList?: readonly unknown[]; argv?: readonly unknown[]; name?: string; + resourceLimits?: ResourceLimits; + stdout?: boolean; + stderr?: boolean; + stdin?: boolean; [option: string]: unknown; } export class Worker { constructor(filename: string | URL, options?: WorkerOptions); readonly threadId: number; + readonly resourceLimits?: ResourceLimits; + readonly threadName: string | null; + /* with the stdout/stderr options: the worker's writes to those streams */ + readonly stdout: import("stream").Readable; + readonly stderr: import("stream").Readable; + /* with the stdin option: what the worker reads from process.stdin */ + readonly stdin: import("stream").Writable | null; + /* terminate(), resolving with undefined once the worker exited */ + [Symbol.asyncDispose](): Promise; on(event: "online", listener: () => void): this; on(event: "message", listener: (value: any) => void): this; - on(event: "error", listener: (error: Error) => void): this; + on(event: "error", listener: (error: any) => void): this; on(event: "exit", listener: (code: number) => void): this; on(event: string, listener: (...args: any[]) => void): this; once(event: string, listener: (...args: any[]) => void): this; @@ -5860,6 +5923,27 @@ declare module "worker_threads" { readonly port2: MessagePort; } export function receiveMessageOnPort(port: MessagePort): { message: any } | undefined; + /* This thread's environment data; each new Worker starts from a clone. */ + export function setEnvironmentData(key: any, value?: any): void; + export function getEnvironmentData(key: any): any; + /* A marked object cannot be listed in a transfer list. */ + export function markAsUntransferable(object: any): void; + /* structuredClone and postMessage throw DataCloneError for a marked + * object, error or class instance */ + export function markAsUncloneable(object: any): void; + export function isMarkedAsUntransferable(object: any): boolean; + /* Delivers each message to every other open channel with the name, in + * any thread, as a 'message' MessageEvent. */ + export class BroadcastChannel extends EventTarget { + constructor(name: string); + readonly name: string; + onmessage: ((event: MessageEvent) => void) | null; + onmessageerror: ((event: MessageEvent) => void) | null; + postMessage(message: unknown): void; + close(): void; + ref(): this; + unref(): this; + } export const parentPort: MessagePort | null; export const workerData: any; } diff --git a/packages/compiler/src/backend/cycle-analysis.test.ts b/packages/compiler/src/backend/cycle-analysis.test.ts index 5fb4084037..8972b9e708 100644 --- a/packages/compiler/src/backend/cycle-analysis.test.ts +++ b/packages/compiler/src/backend/cycle-analysis.test.ts @@ -513,6 +513,19 @@ test("a subclass constructor runs after its base constructor's escape", () => { checkExact(mod, ["object:Base", "object:Sub"], ["link"]); }); +test("a base without a constructor function lets nothing escape", () => { + // Instantiations of a generic class name the generic class as their base, + // which has no constructor function of its own. + const mod = module(); + mod.classes = [ + { name: "Chain", fields: [{ name: "next", type: link }], loc }, + { name: "Chain%0", base: "Chain", fields: [{ name: "next", type: link }], loc }, + ]; + mod.unions = [{ id: "link", arms: [obj("Chain%0"), { kind: "nullT" }] }]; + mod.functions.push(ctor("Chain%0", [setThis("Chain%0", "next", link)])); + checkExact(mod, []); +}); + test("field reads of this inside the constructor do not escape", () => { const read: IrStmt = { kind: "fieldSet", diff --git a/packages/compiler/src/backend/cycle-mutability.ts b/packages/compiler/src/backend/cycle-mutability.ts index 24d3c470f4..c7d4765279 100644 --- a/packages/compiler/src/backend/cycle-mutability.ts +++ b/packages/compiler/src/backend/cycle-mutability.ts @@ -216,8 +216,13 @@ export function computeCycleMutability( escapePoints.set(name, point); return point; }; - const escapes = (name: string): boolean => - escapePoint(name) < (fnByName.get(`%${name}.constructor`)?.body.length ?? 1); + const escapes = (name: string): boolean => { + const fn = fnByName.get(`%${name}.constructor`); + // A program class without a constructor function runs no code at construction. Generic + // classes are such bases: their instantiations name them as their base. + if (fn === undefined && classByName.get(name)?.runtime !== true) return false; + return escapePoint(name) < (fn?.body.length ?? 1); + }; const ancestorEscaped = (name: string): boolean => { for (let c = classByName.get(name)?.base; c !== undefined; c = classByName.get(c)?.base) if (escapes(c)) return true; diff --git a/packages/compiler/src/backend/llvm/blocks.test.ts b/packages/compiler/src/backend/llvm/blocks.test.ts index c2ee90b5f8..b96fc851ee 100644 --- a/packages/compiler/src/backend/llvm/blocks.test.ts +++ b/packages/compiler/src/backend/llvm/blocks.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "vitest"; -import { BlockBuilder } from "./blocks.js"; +import { BlockBuilder, volatileSlotAccess } from "./blocks.js"; import { f64Lit } from "./common.js"; test("entry allocations precede instructions and temporaries stay unique", () => { @@ -106,3 +106,34 @@ test("inclusive loops and nested loops keep labels separate", () => { expect(text).toContain("fcmp ole double"); expect(text.match(/alloca double/g)).toHaveLength(2); }); + +test("volatile slots render their loads and stores volatile and nothing else", () => { + const slots = new Set(["%sc_l_a_0"]); + expect(volatileSlotAccess(" %t1 = load ptr, ptr %sc_l_a_0 ; a", slots)).toBe( + " %t1 = load volatile ptr, ptr %sc_l_a_0 ; a", + ); + expect(volatileSlotAccess(" store ptr %t1, ptr %sc_l_a_0, !dbg !7", slots)).toBe( + " store volatile ptr %t1, ptr %sc_l_a_0, !dbg !7", + ); + expect(volatileSlotAccess(" store ptr null, ptr %sc_l_a_0", slots)).toBe( + " store volatile ptr null, ptr %sc_l_a_0", + ); + // Other addresses, a slot stored as a value, prefixes and other opcodes. + for (const line of [ + " %t2 = load ptr, ptr %sc_l_a_01", + " store ptr %sc_l_a_0, ptr %sc_l_b_0", + " %t3 = getelementptr inbounds ptr, ptr %sc_l_a_0, i64 1", + " call void @scr_box_release(ptr %sc_l_a_0)", + " %t4 = load volatile ptr, ptr %sc_l_a_0", + ]) + expect(volatileSlotAccess(line, slots)).toBe(line); + + const blocks = new BlockBuilder(); + blocks.volatileSlots = slots; + blocks.entryAllocas.push("%sc_l_a_0 = alloca ptr"); + blocks.line("store ptr null, ptr %sc_l_a_0"); + blocks.terminate("ret void"); + expect(blocks.render()).toBe( + "entry:\n %sc_l_a_0 = alloca ptr\n store volatile ptr null, ptr %sc_l_a_0\n ret void", + ); +}); diff --git a/packages/compiler/src/backend/llvm/blocks.ts b/packages/compiler/src/backend/llvm/blocks.ts index 5be4ef8801..6d22bf3bf6 100644 --- a/packages/compiler/src/backend/llvm/blocks.ts +++ b/packages/compiler/src/backend/llvm/blocks.ts @@ -31,6 +31,9 @@ export class BlockBuilder { /** Temps known to hold immortal values (temp names are only unique * within one builder). */ readonly immortalTemps = new Set(); + /** Entry slots whose loads and stores render volatile, so no LLVM pass + * promotes them to SSA values (see volatileSlotAccess). */ + volatileSlots: ReadonlySet | null = null; constructor() { this.cur = { label: "entry", lines: [], term: null }; @@ -130,9 +133,14 @@ export class BlockBuilder { } render(): string { + const slots = this.volatileSlots; return this.blocks .map((b, i) => { - const lines = i === 0 ? [...this.entryAllocas.map((l) => ` ${l}`), ...b.lines] : b.lines; + const body = + slots === null || slots.size === 0 + ? b.lines + : b.lines.map((line) => volatileSlotAccess(line, slots)); + const lines = i === 0 ? [...this.entryAllocas.map((l) => ` ${l}`), ...body] : body; // A block left unterminated is a structurally unreachable join // (every predecessor jumped elsewhere); the verifier still wants a // terminator. @@ -142,3 +150,36 @@ export class BlockBuilder { .join("\n"); } } + +/** `line` with its load or store made volatile when the address operand is + * one of `slots`. Lines are this builder's own instruction text, where the + * address is the first `ptr %name` operand after the opcode (a stored value + * precedes it without a comma). */ +export function volatileSlotAccess(line: string, slots: ReadonlySet): string { + let op = 0; + while (line.charCodeAt(op) === 32) op++; + if (line.charCodeAt(op) === 37) { + const assign = line.indexOf(" = ", op); + if (assign < 0) return line; + op = assign + 3; + } + const kind = line.startsWith("load ", op) ? 5 : line.startsWith("store ", op) ? 6 : 0; + if (kind === 0 || line.startsWith("volatile ", op + kind)) return line; + const address = line.indexOf(", ptr %", op + kind); + if (address < 0) return line; + const start = address + 6; + let end = start + 1; + while (end < line.length && isNameCode(line.charCodeAt(end))) end++; + if (!slots.has(line.slice(start, end))) return line; + return `${line.slice(0, op + kind)}volatile ${line.slice(op + kind)}`; +} + +function isNameCode(code: number): boolean { + return ( + (code >= 48 && code <= 57) || + (code >= 65 && code <= 90) || + (code >= 97 && code <= 122) || + code === 95 || + code === 46 + ); +} diff --git a/packages/compiler/src/backend/llvm/byte-numbers.ts b/packages/compiler/src/backend/llvm/byte-numbers.ts index d12ef08b9e..c51d03744a 100644 --- a/packages/compiler/src/backend/llvm/byte-numbers.ts +++ b/packages/compiler/src/backend/llvm/byte-numbers.ts @@ -182,7 +182,11 @@ export function emitByteNumber( const integerType = bits <= 32 ? "i32" : "i64"; if (spec.write) { let raw: string; - if (spec.floating) { + if (spec.floating && bits === 16) { + host.declare(`declare zeroext i16 @scr_f16_from_f64(double)`); + raw = B.tmp(); + B.line(`${raw} = call zeroext i16 @scr_f16_from_f64(double ${value!.name})`); + } else if (spec.floating) { let stored = value!.name; if (bits === 32) { stored = B.tmp(); @@ -228,7 +232,10 @@ export function emitByteNumber( const raw = endianBits(host, loaded, spec.width, little); fastResult = B.tmp(); if (spec.floating) { - if (bits === 32) { + if (bits === 16) { + host.declare(`declare double @scr_f16_to_f64(i16 zeroext)`); + B.line(`${fastResult} = call double @scr_f16_to_f64(i16 zeroext ${raw})`); + } else if (bits === 32) { const float = B.tmp(); B.line(`${float} = bitcast i32 ${raw} to float`); B.line(`${fastResult} = fpext float ${float} to double`); diff --git a/packages/compiler/src/backend/llvm/call-lifetimes.ts b/packages/compiler/src/backend/llvm/call-lifetimes.ts index 65dd76de7e..db5bdb5741 100644 --- a/packages/compiler/src/backend/llvm/call-lifetimes.ts +++ b/packages/compiler/src/backend/llvm/call-lifetimes.ts @@ -178,6 +178,12 @@ function collectUses( ) return true; break; + case "recordClone": + // A same-shape spread copies the source's fields (retaining the + // references) and keeps no reference to the source itself. + if (node.source.kind !== "varRef") break; + for (const field of node.overrides) expr(field.value); + return true; case "recordLit": if (node.type.kind !== "record") break; for (const field of node.fields) diff --git a/packages/compiler/src/backend/llvm/common.ts b/packages/compiler/src/backend/llvm/common.ts index 0a473e65b2..8a87789190 100644 --- a/packages/compiler/src/backend/llvm/common.ts +++ b/packages/compiler/src/backend/llvm/common.ts @@ -159,6 +159,7 @@ export const BYTES_ELEM_NUM: Record = { u16: 6, i16: 7, u8c: 8, + f16: 9, }; /** The payload marker of an ABSENT record field slot (IR fieldAbsent): the diff --git a/packages/compiler/src/backend/llvm/dense-array-access.test.ts b/packages/compiler/src/backend/llvm/dense-array-access.test.ts index dc23ceae21..31680a5183 100644 --- a/packages/compiler/src/backend/llvm/dense-array-access.test.ts +++ b/packages/compiler/src/backend/llvm/dense-array-access.test.ts @@ -3,6 +3,7 @@ import { BOOL, F64, STRING, + UNDEFINED_T, VOID, arrayOf, type IrExpr, @@ -23,11 +24,17 @@ const ref = (localId: string, type: IrExpr["type"]): IrExpr => ({ }); const index = ref("i", F64); -function module(name: string, returnType: IrExpr["type"], body: IrStmt[]): IrModule { +function module( + name: string, + returnType: IrExpr["type"], + body: IrStmt[], + unions: IrModule["unions"] = [], +): IrModule { return { irVersion: 16, sourceFile: loc.file, entry: "__main", + unions, functions: [ { name: "__main", params: [], returnType: VOID, locals: [], body: [], loc }, { @@ -174,6 +181,76 @@ test.each([32, 64] as const)( }, ); +test.each([32, 64] as const)( + "pop removes the last element of a packed array inline and keeps a cold runtime pop (%i-bit)", + (bits) => { + const maybe = { kind: "union", unionId: "maybe" } as const; + const pop: IrExpr = { + kind: "arrIntrinsic", + method: "pop", + receiver: ref("a", nums), + args: [], + type: maybe, + loc, + }; + const llvm = emitLlvmModule( + module( + "take", + maybe, + [{ kind: "return", value: pop, loc }], + [{ id: "maybe", arms: [F64, UNDEFINED_T] }], + ), + { pointerBits: bits }, + ); + const fn = body(llvm, "take"); + const size = bits === 32 ? "i32" : "i64"; + // An empty array answers undefined without a call. + expect(fn).toMatch(new RegExp(`icmp eq ${size} %t\\d+, 0\\n br i1 %t\\d+, label %remove\\.a`)); + expect(fn).toMatch(new RegExp(`sub nuw ${size} %t\\d+, 1`)); + expect(fn).toMatch(/store i64 0, ptr /); + expect(fn).toMatch(/store i8 0, ptr /); + expect(fn).toMatch(/phi i64 /); + expect(fn).toMatch(/call zeroext i8 @scr_arr_pop_state\(.*\) cold$/m); + }, +); + +test("a discarded pop releases the removed element inline", () => { + const words = ref("s", arrayOf(STRING)); + const length: IrExpr = { + kind: "arrIntrinsic", + method: "length", + receiver: words, + args: [], + type: F64, + loc, + }; + const llvm = emitLlvmModule( + module("drop", VOID, [ + { + kind: "arraySetLength", + arr: words, + length: { + kind: "bin", + op: "-", + left: length, + right: { kind: "numLit", value: 1, type: F64, loc }, + type: F64, + loc, + }, + loc, + pop: true, + }, + ]), + ); + const fn = body(llvm, "drop"); + expect(fn).toMatch(/fsub double %t\d+, 1\.0/); + expect(fn).toMatch(/load i64, ptr .*\n(?:.*\n)*? store i64 0, ptr /); + expect(fn).toMatch(/inttoptr i64 %t\d+ to ptr/); + expect(fn).toContain("call void @scr_str_release("); + // Holes, sparse storage, and every other length keep the runtime call. + expect(fn).toContain("call void @scr_arr_set_len("); +}); + test.each([32, 64] as const)( "indices below the hole-free prefix bound skip the state bytes (%i-bit)", (bits) => { diff --git a/packages/compiler/src/backend/llvm/dense-array-access.ts b/packages/compiler/src/backend/llvm/dense-array-access.ts index aeaf3e70f8..183bb8239a 100644 --- a/packages/compiler/src/backend/llvm/dense-array-access.ts +++ b/packages/compiler/src/backend/llvm/dense-array-access.ts @@ -508,6 +508,75 @@ export function emitDenseArraySet( B.startBlock(join); } +/** The dense half of `pop`. An empty array branches to `empty` (the + * runtime's pop returns a hole state there and changes nothing). When the + * hole-free prefix bound equals the length, the last element is a present + * value in dense storage, and the runtime's pop is exactly: clear its slot + * and state, lower the bound and the length to its index, and move the + * slot out. That block is left open and its slot bits are returned as an + * i64 (a boolean's 0 or 1, zero-extended); every other array branches to + * `slow`, which keeps the runtime call. */ +export function emitDenseArrayPop( + host: LlvmEmitterContext, + array: string, + acc: ArrayAccess, + empty: string, + slow: string, +): string { + const B = host.B; + const size = host.sizeType; + const header = (field: number): string => { + const ptr = B.tmp(); + B.line(`${ptr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 ${field}`); + host.markMemoryPointer(ptr, "array:header"); + return ptr; + }; + const lenPtr = header(SCR_ARR_FIELD.len), + len = B.tmp(), + none = B.tmp(); + B.line(`${len} = load ${size}, ptr ${lenPtr}${host.fieldAliasAttachment(lenPtr)}`); + B.line(`${none} = icmp eq ${size} ${len}, 0`); + const check = B.newLabel("arr.pop.check"), + dense = B.newLabel("arr.pop.dense"); + B.condBr(none, empty, check); + B.startBlock(check); + const filledPtr = header(SCR_ARR_FIELD.filled), + filled = B.tmp(), + packed = B.tmp(); + B.line(`${filled} = load i32, ptr ${filledPtr}${host.fieldAliasAttachment(filledPtr)}`); + B.line(`${packed} = icmp eq ${size} ${sizedFilled(host, filled)}, ${len}`); + B.condBr(packed, dense, slow); + B.startBlock(dense); + const last = B.tmp(); + B.line(`${last} = sub nuw ${size} ${len}, 1`); + const dataPtr = header(SCR_ARR_FIELD.data), + data = B.tmp(), + statesPtr = header(SCR_ARR_FIELD.present), + states = B.tmp(); + B.line(`${data} = load ptr, ptr ${dataPtr}${host.fieldAliasAttachment(dataPtr)}`); + B.line(`${states} = load ptr, ptr ${statesPtr}${host.fieldAliasAttachment(statesPtr)}`); + const valuePtr = valuePointer(host, { offset: last, data }, acc); + const statePtr = statePointer(host, { offset: last, states }); + const slotTy = slotType(acc); + let raw = B.tmp(); + B.line(`${raw} = load ${slotTy}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); + if (slotTy === "i8") { + const wide = B.tmp(); + B.line(`${wide} = zext i8 ${raw} to i64`); + raw = wide; + } + B.line(`store ${slotTy} 0, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); + B.line(`store i8 0, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`); + let last32 = last; + if (size === "i64") { + last32 = B.tmp(); + B.line(`${last32} = trunc i64 ${last} to i32`); + } + B.line(`store i32 ${last32}, ptr ${filledPtr}${host.fieldAliasAttachment(filledPtr)}`); + B.line(`store ${size} ${last}, ptr ${lenPtr}${host.fieldAliasAttachment(lenPtr)}`); + return raw; +} + /** `scr_arr_push_*` (the array takes the value's reference) returning the * new length as a double. When `len < cap` the runtime's append is exactly * a dense store at `len` plus `len + 1`: no sparse entry can exist past the diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index 86a8a945e5..bb35e70c2e 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -2418,6 +2418,15 @@ export class LlDyn { const B = new BlockBuilder(); let sourceAccessor: { name: string; release: string } | null = null; switch (t.kind) { + case "date": { + // A Date handle holding the same time value; native Dates are + // immutable values, and so is the handle. + host.declare(`declare ptr @scr_dyn_native_date_from_ms(double)`); + const r = B.tmp(); + B.line(`${r} = call ptr @scr_dyn_native_date_from_ms(double %v)`); + B.terminate(`ret ptr ${r}`); + break; + } case "url": case "searchParams": { const native = t.kind === "url" ? "url" : "search_params"; @@ -3209,82 +3218,19 @@ export class LlDyn { this.puts(B, "%b", "[object Object]"); B.br(done); B.startBlock(lErr); - const en = this.objGetLit(B, "%d", "name"); - const em = this.objGetLit(B, "%d", "message"); - // ens/ems: the member's ScrStr when it is a string, else null. - const strOf = (m: string, hint: string): string => { - const slot = B.slot(); - B.entryAllocas.push(`${slot} = alloca ptr`); - B.line(`store ptr null, ptr ${slot}`); - const nn = B.tmp(); - B.line(`${nn} = icmp ne ptr ${m}, null`); - const lt = B.newLabel(`${hint}.t`); - const lj = B.newLabel(`${hint}.j`); - B.condBr(nn, lt, lj); - B.startBlock(lt); - const k = this.kindOf(B, m); - const isStr = B.tmp(); - B.line(`${isStr} = icmp eq i32 ${k}, ${DYN_KIND.STR}`); - const ls = B.newLabel(`${hint}.s`); - B.condBr(isStr, ls, lj); - B.startBlock(ls); - const sv = this.payloadOf(B, m, "ptr"); - B.line(`store ptr ${sv}, ptr ${slot}`); - B.br(lj); - B.startBlock(lj); - const out = B.tmp(); - B.line(`${out} = load ptr, ptr ${slot}`); - return out; - }; - const ens = strOf(en, "ds.en"); - const ems = strOf(em, "ds.em"); - const putIf = (s: string, hint: string): void => { - const nn = B.tmp(); - B.line(`${nn} = icmp ne ptr ${s}, null`); - const lt = B.newLabel(`${hint}.t`); - const lj = B.newLabel(`${hint}.j`); - B.condBr(nn, lt, lj); - B.startBlock(lt); - this.putScrStr(B, "%b", s); - B.br(lj); - B.startBlock(lj); - }; - putIf(ens, "ds.pn"); - { - // if (ens && ens->len && ems && ems->len) ": " - const lenNz = (s: string, hint: string): string => { - const slot = B.slot(); - B.entryAllocas.push(`${slot} = alloca i1`); - B.line(`store i1 false, ptr ${slot}`); - const nn = B.tmp(); - B.line(`${nn} = icmp ne ptr ${s}, null`); - const lt = B.newLabel(`${hint}.t`); - const lj = B.newLabel(`${hint}.j`); - B.condBr(nn, lt, lj); - B.startBlock(lt); - const { len } = this.strParts(B, s); - const nz = B.tmp(); - B.line(`${nz} = icmp ne ${this.S} ${len}, 0`); - B.line(`store i1 ${nz}, ptr ${slot}`); - B.br(lj); - B.startBlock(lj); - const out = B.tmp(); - B.line(`${out} = load i1, ptr ${slot}`); - return out; - }; - const a = lenNz(ens, "ds.ln"); - const b2 = lenNz(ems, "ds.lm"); - const both = B.tmp(); - B.line(`${both} = and i1 ${a}, ${b2}`); - const lt = B.newLabel("ds.cl"); - const lj = B.newLabel("ds.cj"); - B.condBr(both, lt, lj); - B.startBlock(lt); - this.puts(B, "%b", ": "); - B.br(lj); - B.startBlock(lj); - } - putIf(ems, "ds.pm"); + // Error.prototype.toString, reading name and message through the + // prototype. Null leaves the exception its conversion threw pending. + host.declare(`declare ptr @scr_dyn_error_string(ptr)`); + host.declare(`declare void @scr_str_release(ptr)`); + const text = B.tmp(); + B.line(`${text} = call ptr @scr_dyn_error_string(ptr %d)`); + const written = B.tmp(); + B.line(`${written} = icmp ne ptr ${text}, null`); + const lPut = B.newLabel("ds.ep"); + B.condBr(written, lPut, done); + B.startBlock(lPut); + this.putScrStr(B, "%b", text); + B.line(`call void @scr_str_release(ptr ${text})`); B.br(done); } B.startBlock(labels.get(DYN_KIND.BYTES)!); diff --git a/packages/compiler/src/backend/llvm/emitter.test.ts b/packages/compiler/src/backend/llvm/emitter.test.ts index 4fda0f7e0c..74dc8f8598 100644 --- a/packages/compiler/src/backend/llvm/emitter.test.ts +++ b/packages/compiler/src/backend/llvm/emitter.test.ts @@ -126,6 +126,61 @@ test("the shared indexed-read helpers carry an inline hint", () => { expect(define("plain")).not.toContain("inlinehint"); }); +test("bodies holding thousands of capture boxes compile unoptimized with boxes in memory", () => { + const number = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc }); + const read = (localId: string): IrExpr => ({ kind: "varRef", localId, type: F64, loc }); + // A declaring body with `count` boxed bindings, and a closure body that + // receives `count` of them through its environment. + const declaring = (name: string, count: number): IrFunction => { + const ids = Array.from({ length: count }, (_, i) => `${name}${i}`); + return { + name, + params: [], + returnType: F64, + locals: ids.map((id) => ({ id, name: id, type: F64, mutable: true, boxed: true as const })), + body: [ + ...ids.map((id, i): IrStmt => ({ kind: "varDecl", localId: id, init: number(i), loc })), + { kind: "return", value: read(ids[count - 1]!), loc }, + ], + loc, + }; + }; + const receiving = (name: string, count: number): IrFunction => { + const ids = Array.from({ length: count }, (_, i) => `${name}${i}`); + return { + name, + params: [], + returnType: F64, + captures: ids.map((id) => ({ localId: id, name: id, type: F64 })), + locals: ids.map((id) => ({ id, name: id, type: F64, mutable: true, boxed: true as const })), + body: [{ kind: "return", value: read(ids[0]!), loc }], + loc, + }; + }; + const module = moduleFor([]); + const llvm = emitLlvmModule({ + ...module, + functions: [ + ...module.functions, + declaring("wide", 512), + declaring("narrow", 511), + receiving("env", 512), + ], + }); + const body = (name: string): string => { + const start = llvm.split("\n").findIndex((line) => line.endsWith(`{ ; ${name}`)); + const lines = llvm.split("\n").slice(start); + return lines.slice(0, lines.indexOf("}")).join("\n"); + }; + expect(body("wide")).toMatch(/\) noinline optnone #0 \{/); + expect(body("wide")).toMatch(/store volatile ptr %[st]\d+, ptr %sc_l_wide0\b/); + expect(body("wide")).toMatch(/= load volatile ptr, ptr %sc_l_wide511\b/); + expect(body("env")).toMatch(/\) noinline optnone #0 \{/); + expect(body("env")).toMatch(/store volatile ptr %t\d+, ptr %sc_l_env0 ; captured env0/); + expect(body("narrow")).not.toContain("optnone"); + expect(body("narrow")).not.toContain("volatile"); +}); + test("throwing-call count does not multiply identical scope cleanup", () => { for (const boxed of [false, true]) { const small = emitLlvmModule(moduleFor(Array.from({ length: 4 }, call), { boxed })); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 2c3a714574..2cccda19ce 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -49,7 +49,7 @@ import { STRING, VOID, } from "../../ir/ir.js"; -import { moduleRuntimeFeatures } from "../../ir/runtime-features.js"; +import { moduleRuntimeFeatures, moduleUsesMockTimers } from "../../ir/runtime-features.js"; import { matchIntegerArrayForLoop, matchIntegerBytesForLoop, @@ -88,7 +88,7 @@ import { type StringSplit, } from "./split-loops.js"; import { scalarizeNumericRecords } from "../../ir/scalar-records.js"; -import { RESULT_ENTRY_PREFIX, addRecordResultEntries } from "./record-results.js"; +import { RESULT_DST_ID, RESULT_ENTRY_PREFIX, addRecordResultEntries } from "./record-results.js"; import { specializeCallbackCalls } from "../../ir/callback-specialization.js"; import { specializeNumericCalls } from "../../ir/numeric-call-specialization.js"; import { everyExpr, everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse.js"; @@ -166,7 +166,9 @@ import { emitStackObject, findLocalStackObjects, findReassignedStackObjects, + stackObjectFields, stackObjectStruct, + type StackObjectField, type StackObjectSource, } from "./stack-objects.js"; import { @@ -199,7 +201,12 @@ import { type LocalArrayRead, } from "./local-array-reads.js"; import { CheckedNarrows, emitCheckedNarrowReceiver } from "./checked-narrows.js"; -import { emitDenseArrayGet, emitDenseArraySet, emitDenseSlotUnlink } from "./dense-array-access.js"; +import { + emitDenseArrayGet, + emitDenseArrayPop, + emitDenseArraySet, + emitDenseSlotUnlink, +} from "./dense-array-access.js"; import { emitStackMapRead, findMapReadLifetimes, @@ -548,10 +555,35 @@ const AWAIT_SUSPENSIONS = /@(?:scr_await_(?:void|f64|bool|str|ref|hop)\b|sc_a[wd * own fiber and promise. */ const COROUTINE_SUSPENDS = /@(?:scr_(?:await_|module_await\b|promise_await_settled\b|test_sub\b|gen_yield_|async_gen_|fiber_(?!self\b|promise\b))|sc_a[wd]_)/; +/** Captured bindings (boxes a body declares or receives) from which a + * function compiles without LLVM optimization (see boxSlotCount). */ +const UNOPTIMIZED_BOX_SLOTS = 512; /** ScrExcKind tags of a generator slot holding a number or a boolean. */ const SCR_EXC_F64 = 1; const SCR_EXC_BOOL = 2; +/** The capture boxes `fn` holds: its boxed bindings and the bindings its + * environment brings in. Each box pointer is a value from its binding (or + * function entry) to its last use, and every one of them crosses the calls + * in between. With thousands of them in one body, as in a factory whose + * returned methods share a large environment, LLVM's inliner, instruction + * selection and register allocation grow quadratically and take minutes. + * Past UNOPTIMIZED_BOX_SLOTS, which ordinary functions stay far below, the + * body is compiled `optnone` (nothing inlines into it and code generation + * takes its fast paths) and its box slots are accessed volatile, so no + * pass promotes them to values even where a pipeline runs IR passes over + * optnone bodies, as the native helper's does. Each use reloads its box + * pointer and code generation stays linear. Semantics, boxes and + * environments are unchanged; only that body's machine code is slower. */ +function boxSlotCount(fn: IrFunction): number { + const captured = new Set(); + for (const capture of fn.captures ?? []) captured.add(capture.localId); + for (const capture of fn.classCaptures ?? []) captured.add(capture.localId); + let count = 0; + for (const local of fn.locals) if (local.boxed === true || captured.has(local.id)) count++; + return count; +} + /** Whether any node of `e` names the local (reads, writes, declarations, * captures). */ function mentionsLocal(e: IrExpr, localId: string): boolean { @@ -595,6 +627,9 @@ export class LlEmitter { private reassignedStackObjects = new Set(); /** The binding slot of each reassigned stack-object local. */ private stackObjectSlots = new Map(); + /** Entry-block pointers to the reference fields of each frame object + * slot (stack-objects.ts stackObjectFields). */ + stackObjectFieldPtrs = new Map(); private localUnionStorageProofs = new Map(); private localUnionStorage = new Map(); integerArrayBindings = new Set(); @@ -882,6 +917,9 @@ export class LlEmitter { /** setTimeout and friends appeared somewhere: main must run the event * loop even in programs with no async functions. */ usesTimers = false; + /** The program calls mock.timers members (moduleUsesMockTimers): timer + * calls emit scr_test.c's mock-aware entry points. */ + readonly mockTimers: boolean; /** Emitted ref-kind resolve thunks for new Promise, interned per inner * typeKey → thunk symbol. */ readonly resolveThunks = new Map(); @@ -1108,6 +1146,7 @@ export class LlEmitter { }), ); this.nullableUnions = new NullableUnions(mod); + this.mockTimers = moduleUsesMockTimers(mod); this.debug = options.debugSources === undefined ? null @@ -2028,12 +2067,28 @@ export class LlEmitter { this.dyn.toDynHelper({ kind: "object", className: "%Writable" }), ] : null; - if (childStreamBoxes) { + // worker.stdout and worker.stderr box the stream unit's Readable the + // same way, and worker.stdin its Writable when the program has one + // (scr_worker_stdio_install, scr_worker_events.c). + const workerStdioBox = + this.mod.workers === true && usesStream && this.classMeta.has("%Readable") + ? (childStreamBoxes?.[0] ?? + this.dyn.toDynHelper({ kind: "object", className: "%Readable" })) + : null; + const workerStdinBox = + this.mod.workers === true && usesStream && this.classMeta.has("%Writable") + ? (childStreamBoxes?.[1] ?? + this.dyn.toDynHelper({ kind: "object", className: "%Writable" })) + : null; + if (childStreamBoxes || workerStdioBox || workerStdinBox) { this.declare( `@scr_stream_child_ops = external constant { ptr, ptr, ptr, ptr, ptr, ptr, ptr, ptr, ptr }`, ); - this.declare(`declare void @scr_child_dyn_streams_install(ptr, ptr, ptr)`); } + if (childStreamBoxes) + this.declare(`declare void @scr_child_dyn_streams_install(ptr, ptr, ptr)`); + if (workerStdioBox || workerStdinBox) + this.declare(`declare void @scr_worker_stdio_install(ptr, ptr, ptr)`); if (usesFsWatch) this.declare(`declare void @scr_watch_install()`); if (this.ffiHasForeignCallback) this.declare(`declare void @scr_ffi_install()`); if (usesStream) this.declare(`declare void @scr_stream_install()`); @@ -2046,7 +2101,10 @@ export class LlEmitter { if (usesHttp) this.declare(`declare void @scr_http_dyn_install()`); if (usesFetch) this.declare(`declare void @scr_fetch_install()`); - if (usesNodeTest) this.declare(`declare i32 @scr_test_exit_code()`); + if (usesNodeTest) { + this.declare(`declare i32 @scr_test_exit_code()`); + this.declare(`declare void @scr_test_init()`); + } if (snapshotsTlsCa) { this.declare(`declare void @scr_tls_ca_install()`); } @@ -2190,6 +2248,12 @@ export class LlEmitter { if (this.mod.lib.exports.some((e) => e.returns === "bytes")) { this.declare(`declare void @scr_library_bytes_out(ptr, ptr, ptr)`); } + if (this.mod.lib.exports.some((e) => e.params.includes("cstring"))) { + this.declare(`declare ptr @scr_library_cstr_in(ptr, ptr)`); + } + if (this.mod.lib.exports.some((e) => e.returns === "cstring")) { + this.declare(`declare ptr @scr_library_cstr_out(ptr)`); + } } // Helpers assemble BEFORE the declaration table flushes (they add // write/abort declarations). @@ -2623,6 +2687,11 @@ export class LlEmitter { ` call void @scr_child_dyn_streams_install(ptr @scr_stream_child_ops, ptr @${childStreamBoxes[0]}, ptr @${childStreamBoxes[1]})`, ] : []), + ...(workerStdioBox || workerStdinBox + ? [ + ` call void @scr_worker_stdio_install(ptr @scr_stream_child_ops, ptr ${workerStdioBox ? `@${workerStdioBox}` : "null"}, ptr ${workerStdinBox ? `@${workerStdinBox}` : "null"})`, + ] + : []), // fs.watch programs fill the loop's watch hooks the same way — // scr_watch.c links only when this line is emitted. ...(usesFsWatch ? [` call void @scr_watch_install()`] : []), @@ -2635,6 +2704,8 @@ export class LlEmitter { ...(usesHttp2 ? [` call void @scr_http2_dyn_install()`] : []), ...(usesStream ? [` call void @scr_stream_install()`] : []), ` call void @scr_lib_init(i32 %argc, ptr %argv)`, + // node:test takes Node's --test-update-snapshots out of argv first. + ...(usesNodeTest ? [` call void @scr_test_init()`] : []), ...(asyncEntry ? [` %top = call ptr @${mangleAsyncSpawn(this.mod.entry)}()`] : [` call void @${mangleFunction(this.mod.entry)}()`]), @@ -2966,6 +3037,15 @@ export class LlEmitter { ``, ); } + if (e.inboundCstringTrap !== undefined) { + // The cstring-in helper's host-contract trap message (a NULL + // `const char *`): same assembled structured form and SC4012 code. + const trapBytes = Buffer.byteLength(e.inboundCstringTrap, "utf8"); + out.push( + `@sc_lib_cstr_trap_${e.symbol} = internal constant [${trapBytes + 1} x i8] c"${llStrBytes(e.inboundCstringTrap)}"`, + ``, + ); + } e.params.forEach((cls, i) => { switch (cls) { case "f64": @@ -3023,6 +3103,13 @@ export class LlEmitter { ); args.push(`ptr %c${i}`); break; + case "cstring": + params.push(`ptr %a${i}`); + body.push( + ` %c${i} = call ptr @scr_library_cstr_in(ptr %a${i}, ptr @sc_lib_cstr_trap_${e.symbol})`, + ); + args.push(`ptr %c${i}`); + break; } }); if (e.returns === "string" || e.returns === "bytes") { @@ -3087,6 +3174,16 @@ export class LlEmitter { ` ret void`, ); break; + case "cstring": + // The result's arena copy, returned as `const char *`. + retType = "ptr"; + body.push( + ` %r = call ptr ${target}(${callArgs})`, + ` call void @scr_library_check_exc()`, + ` %z = call ptr @scr_library_cstr_out(ptr %r)`, + ` ret ptr %z`, + ); + break; } out.push( `define ${retType} @${e.symbol}(${params.join(", ")}) ${FN_ATTRS} { ; library export ${e.fnName}`, @@ -4699,6 +4796,30 @@ export class LlEmitter { if (isRefCounted(type)) this.currentFrame().push({ name: slot, type, slot: true }); } + /** The innermost lexical scope releases a frame object's reference + * fields when it exits, as it releases its bindings. */ + ownScopeFields(fields: readonly StackObjectField[]): void { + const scope = this.scopes[this.scopes.length - 1]!; + for (const field of fields) scope.push({ slot: field.ptr, type: field.type }); + } + + /** Strike frame-owned slots (ownSlot) from their statement frames: their + * contents are moving to another owner. */ + disownSlots(slots: readonly { ptr: string }[]): void { + for (const { ptr } of slots) { + let found = false; + for (let i = this.frames.length - 1; i >= 0 && !found; i--) { + const idx = this.frames[i]!.findIndex((e) => e.slot === true && e.name === ptr); + if (idx >= 0) { + this.frames[i]!.splice(idx, 1); + found = true; + } + } + if (!found) + throw new InternalCompilerError(`llvm emitter bug: disowned slot ${ptr} not in a frame`); + } + } + /** Strike a refcounted temp from its frame: ownership is being moved. */ moveTemp(v: LlValue): void { if (!isRefCounted(v.type) || this.isImmortal(v.name)) return; @@ -6639,6 +6760,7 @@ export class LlEmitter { ? findReassignedStackObjects(this, fn, this.callLifetimes) : new Set(); this.stackObjectSlots = new Map(); + this.stackObjectFieldPtrs = new Map(); this.localUnionStorageProofs = findLocalUnionStorage(fn, this.callLifetimes, this.unionSources); this.localUnionStorage = new Map(); this.walkBorrows = this.walkBorrowsByFunction.get(fn.name) ?? new Set(); @@ -6817,6 +6939,15 @@ export class LlEmitter { B.line(`store ptr null, ptr %${mangleLocal(local.id)}`); } } + // A body holding thousands of capture boxes compiles unoptimized, its + // box pointers kept in their slots (see boxSlotCount). + const unoptimized = this.currentCoro === null && boxSlotCount(fn) >= UNOPTIMIZED_BOX_SLOTS; + if (unoptimized) { + const slots = new Set(); + for (const local of this.currentLocals.values()) + if (local.boxed || this.captureIds.has(local.id)) slots.add(`%${mangleLocal(local.id)}`); + B.volatileSlots = slots; + } // Captured bindings come in through the environment — borrowed for the // whole call (the closure owns them): bound here, never released here. (fn.captures ?? []).forEach((c, i) => { @@ -7020,6 +7151,7 @@ export class LlEmitter { if (fn.name.startsWith("%dyn.class.protocol.fallback.")) attrs = `noinline ${attrs}`; else if (fn.name.startsWith("%dyn.class.protocol.")) attrs = `inlinehint ${attrs}`; if (fn.name.startsWith(RESULT_ENTRY_PREFIX)) attrs = `inlinehint ${attrs}`; + if (unoptimized && attrs === FN_ATTRS) attrs = `noinline optnone ${attrs}`; const debug = this.debugScope === null ? "" : ` !dbg ${this.debugScope}`; const symbol = borrowed ? mangleBorrowedFunction(fn.name) : mangleFunction(fn.name); this.privateGlobalSlots.clear(); @@ -7764,12 +7896,18 @@ export class LlEmitter { } const stackObject = this.localStackObjects.get(s.localId); if (stackObject) { - B.line(`store ptr ${emitStackObject(this, stackObject).name}, ptr ${b.slot}`); + const value = emitStackObject(this, stackObject, (fields) => this.ownScopeFields(fields)); + B.line(`store ptr ${value.name}, ptr ${b.slot}`); break; } if (s.init && this.reassignedStackObjects.has(s.localId)) { const slot = this.stackObjectSlot(s.localId, s.init.type); - emitStackObject(this, s.init as StackObjectSource, slot); + emitStackObject( + this, + s.init as StackObjectSource, + (fields) => this.ownScopeFields(fields), + slot, + ); B.line(`store ptr ${slot}, ptr ${b.slot}`); break; } @@ -7942,12 +8080,28 @@ export class LlEmitter { break; } if (this.reassignedStackObjects.has(s.localId)) { - // The new value may read the old one: build it apart, then copy. + // The new value may read the old one: build it apart (its + // reference fields owned by this statement until it completes), + // then copy it over the binding's slot. The binding takes over the + // new fields; the old ones are released after the slot no longer + // holds them. const struct = stackObjectStruct(s.value.type); - const value = emitStackObject(this, s.value as StackObjectSource); + let owned: readonly StackObjectField[] = []; + const value = emitStackObject(this, s.value as StackObjectSource, (fields) => { + owned = fields; + for (const field of fields) this.ownSlot(field.ptr, field.type); + }); + const target = this.stackObjectSlot(s.localId, s.value.type); + const old = stackObjectFields(this, s.value.type, target).map((field) => { + const t = B.tmp(); + B.line(`${t} = load ptr, ptr ${field.ptr}`); + return { name: t, type: field.type }; + }); const bits = B.tmp(); B.line(`${bits} = load ${struct}, ptr ${value.name}`); - B.line(`store ${struct} ${bits}, ptr ${this.stackObjectSlot(s.localId, s.value.type)}`); + B.line(`store ${struct} ${bits}, ptr ${target}`); + this.disownSlots(owned); + for (const field of old) this.releaseValue(field.name, field.type); break; } const localStorage = this.localUnionStorage.get(s.localId); @@ -8068,6 +8222,28 @@ export class LlEmitter { B.line(`${same} = fcmp oeq double ${length.name}, ${lenNumber}`); B.condBr(same, done, resize); B.startBlock(resize); + if (s.pop === true) { + // A discarded pop's `length - 1` on a packed array removes the + // last element inline, then releases it. + const call = B.newLabel("arr.len.call"), + pop = B.newLabel("arr.len.pop"); + const lastNumber = B.tmp(), + dropsLast = B.tmp(); + B.line(`${lastNumber} = fsub double ${lenNumber}, 1.0`); + B.line(`${dropsLast} = fcmp oeq double ${length.name}, ${lastNumber}`); + B.condBr(dropsLast, pop, call); + B.startBlock(pop); + const elem = s.arr.type.elem; + const acc = elemAccess(elem); + const raw = emitDenseArrayPop(this, arr.name, acc, call, call); + if (acc === "ref" && isRefCounted(elem)) { + const old = B.tmp(); + B.line(`${old} = inttoptr i64 ${raw} to ptr`); + this.releaseValue(old, elem); + } + B.br(done); + B.startBlock(call); + } B.line(`call void @scr_arr_set_len(ptr ${arr.name}, double ${length.name})`); this.emitPendingCheck(); B.br(done); @@ -8157,7 +8333,15 @@ export class LlEmitter { s.kind === "fieldSet" ? this.classFieldPtr(obj.name, s.className, s.field) : this.recordFieldPtr(obj.name, s.shapeId, s.field); - this.replaceField(ptr, type, inline, v); + // A result-slot entry fills zeroed caller storage, each field once. + if ( + s.kind === "recordSet" && + s.obj.kind === "varRef" && + s.obj.localId === RESULT_DST_ID && + this.currentFunctionName.startsWith(RESULT_ENTRY_PREFIX) + ) + this.initField(ptr, type, inline, v); + else this.replaceField(ptr, type, inline, v); if ( s.kind === "fieldSet" && s.field === "name" && @@ -8919,6 +9103,12 @@ export class LlEmitter { // own turn is the await (nothing runs in between). const genSettles = this.currentAsyncGenerator && (s.awaited === true || this.finallyStack.length > 0); + // A completion turn that awaits x replaces a return() request's + // already-taken await. + if (this.currentAsyncGenerator && !genSettles) { + this.declare(`declare void @scr_async_gen_return_awaits()`); + B.line(`call void @scr_async_gen_return_awaits()`); + } if (this.currentAsyncGenerator && s.awaited !== true && this.finallyStack.length > 0) { if (this.wasi) this.emitWasiSuspend(null); else { diff --git a/packages/compiler/src/backend/llvm/expr-async.ts b/packages/compiler/src/backend/llvm/expr-async.ts index 7b1d870df3..6731e13c9f 100644 --- a/packages/compiler/src/backend/llvm/expr-async.ts +++ b/packages/compiler/src/backend/llvm/expr-async.ts @@ -338,8 +338,17 @@ export function emitSerializationExpr( // checked source can throw from the dyn walker: finish still runs // (frees the buffer, the partial string joins the frame and // releases on unwind), then the pending check unwinds — the - // runtime ABI’s contract exactly. - if (host.walkers.jsonWriteMayThrow(e.value.type)) host.emitPendingCheck(); + // runtime ABI’s contract exactly. Under %j the runtime first turns + // a circular-structure TypeError into the text "[Circular]". + if (host.walkers.jsonWriteMayThrow(e.value.type)) { + if (e.circular) { + host.declare(`declare ptr @scr_util_format_j_text(ptr)`); + const shown = B.tmp(); + B.line(`${shown} = call ptr @scr_util_format_j_text(ptr ${t})`); + compact = host.own({ name: shown, type: e.type }); + } + host.emitPendingCheck(); + } } // A pretty-print form (`stringify(v, null, 2)`): the frontend // resolved the space to a compile-time indent string (Node's diff --git a/packages/compiler/src/backend/llvm/expr-bytes.ts b/packages/compiler/src/backend/llvm/expr-bytes.ts index 89a482b525..ac025df099 100644 --- a/packages/compiler/src/backend/llvm/expr-bytes.ts +++ b/packages/compiler/src/backend/llvm/expr-bytes.ts @@ -44,6 +44,7 @@ const DV_GET_KIND: Record = { dvGetFloat64: 7, dvGetBigUint64Number: 8, dvGetBigInt64Number: 9, + dvGetFloat16: 10, }; const DV_SET_KIND: Record = { @@ -55,6 +56,7 @@ const DV_SET_KIND: Record = { dvSetInt32: 5, dvSetFloat32: 6, dvSetFloat64: 7, + dvSetFloat16: 10, }; export function emitIntegerLoopIndex(host: LlvmEmitterContext, expr: IrExpr): string | null { @@ -273,7 +275,7 @@ function emitSharedOrLocalLoad( idx: string, ): LlValue { const B = host.B; - const floating = elem === "f32" || elem === "f64"; + const floating = elem === "f16" || elem === "f32" || elem === "f64"; const fn = floating ? "scr_bytes_shared_get_f64" : "scr_bytes_shared_get_int"; const ret = floating ? "double" : "i32"; host.declare(`declare ${ret} @${fn}(ptr, ${host.sizeType})`); @@ -314,6 +316,16 @@ function emitBytesLoadAt( const B = host.B; const data = host.emitBytesData(receiver); const p = B.tmp(); + if (elem === "f16") { + // Half-precision bits widen exactly; the runtime converts them. + host.declare(`declare double @scr_f16_to_f64(i16 zeroext)`); + const raw = B.tmp(); + const out = B.tmp(); + B.line(`${p} = getelementptr inbounds i16, ptr ${data}, ${host.sizeType} ${idx}`); + B.line(`${raw} = load i16, ptr ${p}, align 1`); + B.line(`${out} = call double @scr_f16_to_f64(i16 zeroext ${raw})`); + return { name: out, type: F64 }; + } if (BYTES_ELEMENT_SIZE[elem] < 4) { const bits = BYTES_ELEMENT_SIZE[elem] * 8; const signed = elem === "i8" || elem === "i16"; @@ -472,10 +484,19 @@ export function emitBytesSet( B.line(`${stored} = fptoui double ${clamped} to i32`); } else stored = - elem === "f32" || elem === "f64" ? null : host.emitToUint32(value.name, expr, value.uint32); + elem === "f16" || elem === "f32" || elem === "f64" + ? null + : host.emitToUint32(value.name, expr, value.uint32); const data = host.emitBytesData(receiver); const p = B.tmp(); - if (BYTES_ELEMENT_SIZE[elem] < 4) { + if (elem === "f16") { + // One round-to-nearest-even step from the double, in the runtime. + host.declare(`declare zeroext i16 @scr_f16_from_f64(double)`); + const narrowed = B.tmp(); + B.line(`${narrowed} = call zeroext i16 @scr_f16_from_f64(double ${value.name})`); + B.line(`${p} = getelementptr inbounds i16, ptr ${data}, ${host.sizeType} ${idx}`); + B.line(`store i16 ${narrowed}, ptr ${p}, align 1`); + } else if (BYTES_ELEMENT_SIZE[elem] < 4) { const bits = BYTES_ELEMENT_SIZE[elem] * 8; const byte = B.tmp(); B.line(`${byte} = trunc i32 ${stored!} to i${bits}`); @@ -882,6 +903,7 @@ export function emitBytesIntrinsic( case "dvGetInt16": case "dvGetUint32": case "dvGetInt32": + case "dvGetFloat16": case "dvGetFloat32": case "dvGetFloat64": case "dvGetBigUint64Number": @@ -912,6 +934,7 @@ export function emitBytesIntrinsic( case "dvSetInt16": case "dvSetUint32": case "dvSetInt32": + case "dvSetFloat16": case "dvSetFloat32": case "dvSetFloat64": { // DataView setters: [offset, value, littleEndian?] — void; throw diff --git a/packages/compiler/src/backend/llvm/expr-calls.ts b/packages/compiler/src/backend/llvm/expr-calls.ts index 1a34f5e81f..059af307b7 100644 --- a/packages/compiler/src/backend/llvm/expr-calls.ts +++ b/packages/compiler/src/backend/llvm/expr-calls.ts @@ -147,7 +147,10 @@ export function emitCallExpr( return emitScalarOptional(host, a); if (a.kind === "call" && host.scalarResultCall(a)) return emitCallExpr(host, a, false, true); - if (host.stackObjects && canStackObject(host, a)) return emitStackObject(host, a); + if (host.stackObjects && canStackObject(host, a)) + return emitStackObject(host, a, (fields) => { + for (const field of fields) host.ownSlot(field.ptr, field.type); + }); if (host.nullableFieldGet(a)) return { name: host.emitOwnedNullableStack(a).box, type: a.type }; // An inline union field read copies its tag and bits. @@ -333,6 +336,16 @@ export function emitCallExpr( natArgs.push(`ptr ${data}`, `${host.sizeType} ${len}`); break; } + case "cstring": { + // The string's own NUL-terminated data, after the check that + // it holds no U+0000 (a C string cannot carry one). + host.declare(`declare ptr @scr_library_cstr_arg(ptr)`); + const data = B.tmp(); + B.line(`${data} = call ptr @scr_library_cstr_arg(ptr ${arg.name})`); + natTypes.push("ptr"); + natArgs.push(`ptr ${data}`); + break; + } case "bytes": { const lenPtr = B.tmp(); const len = B.tmp(); diff --git a/packages/compiler/src/backend/llvm/expr-containers.ts b/packages/compiler/src/backend/llvm/expr-containers.ts index a2d5fd35aa..9c77690f8b 100644 --- a/packages/compiler/src/backend/llvm/expr-containers.ts +++ b/packages/compiler/src/backend/llvm/expr-containers.ts @@ -49,7 +49,11 @@ import { STRING_UTF16_LEN_DECL, } from "./string-reads.js"; import { exactInteger, widenInteger } from "./integer-values.js"; -import { emitDenseArrayNumber, emitDenseArrayPush } from "./dense-array-access.js"; +import { + emitDenseArrayNumber, + emitDenseArrayPop, + emitDenseArrayPush, +} from "./dense-array-access.js"; import { emitUnionWiden } from "./expr-records.js"; export function resolveThunkFor(host: LlvmEmitterContext, inner: IrType): string { @@ -667,19 +671,38 @@ export function emitArrIntrinsic( const rawSlot = B.slot(); const resultSlot = B.slot(); B.entryAllocas.push(`${rawSlot} = alloca i64`, `${resultSlot} = alloca ptr`); + const lp = B.newLabel("remove.p"); + const la = B.newLabel("remove.a"); + const lj = B.newLabel("remove.j"); + // pop answers an empty or packed array inline; shift and every other + // pop take the runtime call. + let denseRaw: string | null = null; + let denseLabel = ""; + let slowLabel = ""; + if (method === "pop") { + slowLabel = B.newLabel("remove.slow"); + denseRaw = emitDenseArrayPop(host, r.name, acc, la, slowLabel); + denseLabel = B.currentLabel(); + B.br(lp); + B.startBlock(slowLabel); + } B.line(`store i64 0, ptr ${rawSlot}`); const state = B.tmp(); const has = B.tmp(); - B.line(`${state} = call zeroext i8 @scr_arr_${method}_state(ptr ${r.name}, ptr ${rawSlot})`); + const slowRaw = B.tmp(); + B.line( + `${state} = call zeroext i8 @scr_arr_${method}_state(ptr ${r.name}, ptr ${rawSlot})${denseRaw === null ? "" : " cold"}`, + ); + B.line(`${slowRaw} = load i64, ptr ${rawSlot}`); B.line(`${has} = icmp eq i8 ${state}, 1`); - const lp = B.newLabel("remove.p"); - const la = B.newLabel("remove.a"); - const lj = B.newLabel("remove.j"); B.condBr(has, lp, la); B.startBlock(lp); - const raw = B.tmp(); + let raw = slowRaw; const value = B.tmp(); - B.line(`${raw} = load i64, ptr ${rawSlot}`); + if (denseRaw !== null) { + raw = B.tmp(); + B.line(`${raw} = phi i64 [ ${denseRaw}, %${denseLabel} ], [ ${slowRaw}, %${slowLabel} ]`); + } if (into) { // Slot bits for f64; a canonical 0/1 for bool, like scr_union_new_bool. let bits = raw; diff --git a/packages/compiler/src/backend/llvm/lazy-captures.test.ts b/packages/compiler/src/backend/llvm/lazy-captures.test.ts index b0a47d0814..b0e5c55c55 100644 --- a/packages/compiler/src/backend/llvm/lazy-captures.test.ts +++ b/packages/compiler/src/backend/llvm/lazy-captures.test.ts @@ -47,7 +47,7 @@ function lifted(name: string, body: IrStmt[], captured = "options"): IrFunction loc, }; } -function lazy(...functions: IrFunction[]): Set { +function lazy(...functions: IrFunction[]): ReadonlySet { const map = new Map(functions.map((fn) => [fn.name, fn])); return new LazyCaptures(map).parameters(functions[0]!); } diff --git a/packages/compiler/src/backend/llvm/lazy-captures.ts b/packages/compiler/src/backend/llvm/lazy-captures.ts index 12fae0bf46..2014869af3 100644 --- a/packages/compiler/src/backend/llvm/lazy-captures.ts +++ b/packages/compiler/src/backend/llvm/lazy-captures.ts @@ -26,9 +26,15 @@ function rebound(node: IrStmt | IrExpr): string | null { * closure then allocate no box. The proof follows each capture into nested * function bodies; class environments keep the eager box. */ export class LazyCaptures { - private readonly unchanged = new Map(); private readonly stable = new Map>(); private readonly once = new Map>(); + private readonly unchangedParameters = new Map>(); + /** Per function, every local rebound there or through nested captures; + * the second map counts declarations separately (see rebindings). Each + * body is walked once per map, so the queries stay linear in the IR. */ + private readonly rebound = new Map>(); + private readonly reboundAfterDeclaration = new Map>(); + private readonly pending = new Set(); private sites: Map | null = null; constructor(private readonly functions: ReadonlyMap) {} @@ -112,8 +118,11 @@ export class LazyCaptures { return sites; } - parameters(fn: IrFunction): Set { + parameters(fn: IrFunction): ReadonlySet { + const known = this.unchangedParameters.get(fn); + if (known !== undefined) return known; const result = new Set(); + this.unchangedParameters.set(fn, result); if (fn.async || fn.generator) return result; const locals = new Map(fn.locals.map((local) => [local.id, local])); for (const param of fn.params) { @@ -132,18 +141,33 @@ export class LazyCaptures { ids: ReadonlySet, declarationsBind = false, ): Set { + const all = this.reboundIn(fn, declarationsBind); const changed = new Set(); + for (const id of ids) if (all.has(id)) changed.add(id); + return changed; + } + + /** Every local rebound in `fn` or, through the closures it creates, in + * any function that captures it, directly or through further nesting. + * With `declarationsBind`, declarations are counted separately. */ + private reboundIn(fn: IrFunction, declarationsBind: boolean): ReadonlySet { + const memo = declarationsBind ? this.reboundAfterDeclaration : this.rebound; + const known = memo.get(fn); + if (known !== undefined) return known; + const changed = new Set(); + // Capture chains follow lexical nesting and cannot cycle; on malformed + // input a function reached again while its walk is pending reports + // every capture as rebound (see isUnchanged). + if (!declarationsBind) this.pending.add(fn); const nested: { target: string; local: string; source: string }[] = []; const visit = (node: IrStmt | IrExpr): boolean => { - // With `declarationsBind`, declarations are counted separately. const target = declarationsBind && node.kind === "varDecl" ? null : rebound(node); - if (target !== null && ids.has(target)) changed.add(target); + if (target !== null) changed.add(target); if (node.kind === "classRef") { - for (const id of node.captures ?? []) if (ids.has(id)) changed.add(id); + for (const id of node.captures ?? []) changed.add(id); } else if (node.kind === "closure") { const callee = this.functions.get(node.fnName); node.captures.forEach((id, index) => { - if (!ids.has(id)) return; const capture = callee?.captures?.[index]; if (!capture) changed.add(id); else nested.push({ target: node.fnName, local: capture.localId, source: id }); @@ -156,19 +180,14 @@ export class LazyCaptures { if (!changed.has(edge.source) && !this.isUnchanged(edge.target, edge.local)) changed.add(edge.source); } + if (!declarationsBind) this.pending.delete(fn); + memo.set(fn, changed); return changed; } private isUnchanged(fnName: string, localId: string): boolean { - const key = `${fnName}\0${localId}`; - const known = this.unchanged.get(key); - if (known !== undefined) return known; - // Capture chains follow lexical nesting and cannot cycle; the - // provisional entry only bounds work on malformed input. - this.unchanged.set(key, false); const fn = this.functions.get(fnName); - const result = fn !== undefined && this.rebindings(fn, new Set([localId])).size === 0; - this.unchanged.set(key, result); - return result; + if (fn === undefined || this.pending.has(fn)) return false; + return !this.reboundIn(fn, false).has(localId); } } diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index 50804bf5a7..4a9f4017a1 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -11,7 +11,12 @@ import { InternalCompilerError } from "../../errors.js"; import { MAY_THROW_LIB_FNS } from "../../ir/builtin-effects.js"; import { LlvmUnsupportedError } from "./unsupported.js"; import type { LlvmEmitterContext, LibCallExpr, LibCallPrefix, LlValue } from "./expr-context.js"; -import { ASSERT_VERDICT_LIB_FNS, LIB_FN_SYMS, USES_TIMERS_LIB_FNS } from "./lib-shared.js"; +import { + ASSERT_VERDICT_LIB_FNS, + LIB_FN_SYMS, + mockTimerSym, + USES_TIMERS_LIB_FNS, +} from "./lib-shared.js"; import { isDynUndefined } from "../../ir/runtime-features.js"; import { DYN_KIND } from "./dyn.js"; import { rawBytes } from "./lib-abi.js"; @@ -431,7 +436,7 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll if (MAY_THROW_LIB_FNS.has(e.fn) && LIB_FN_SYMS[e.fn] === undefined) { throw new LlvmUnsupportedError(`libCall:${e.fn}`, e.loc); } - const sym = LIB_FN_SYMS[e.fn]; + const sym = mockTimerSym(host, e) ?? LIB_FN_SYMS[e.fn]; if (sym === undefined) throw new LlvmUnsupportedError(`libCall:${e.fn}`, e.loc); const args = READ_ONLY_STRING_LIB_FNS.has(e.fn) ? emitBorrowedInputs(host, e.args) @@ -680,6 +685,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { case "events": case "blob": case "record": + case "disposable": return host.emitGenericLibCall(e); case "test": return emitTestLibCall(host, e); diff --git a/packages/compiler/src/backend/llvm/lib-filesystem.ts b/packages/compiler/src/backend/llvm/lib-filesystem.ts index 1083c69d54..0e7d02a15a 100644 --- a/packages/compiler/src/backend/llvm/lib-filesystem.ts +++ b/packages/compiler/src/backend/llvm/lib-filesystem.ts @@ -746,6 +746,8 @@ export function emitPathUrlLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll const dict = B.tmp(); B.line(`${dict} = call ptr @${mangleRecordNew(e.type.shapeId)}()`); const out = host.own({ name: dict, type: e.type }); + // Node builds the result with a null prototype. + B.line(`store i8 1, ptr ${host.recordMapFlagsPtr(dict, e.type.shapeId)}`); const ovf = host.recordOvfPtr(dict, e.type.shapeId); B.line( `call void @scr_qs_parse_into(ptr ${ovf}, ptr ${args[0]!.name}, ptr ${args[1]!.name}, ptr ${args[2]!.name}, double ${args[3]!.name}, i32 ${strTag}, i32 ${arrTag})`, @@ -1037,6 +1039,16 @@ export function emitPrimitiveLibCall(host: LlvmEmitterContext, e: LibCallExpr): B.line(`${t} = sitofp i32 ${product} to double`); return { name: t, type: e.type, uint32: product }; } + if (e.fn === "math.f16round") { + const v = host.emitExpr(e.args[0]!); + host.declare(`declare zeroext i16 @scr_f16_from_f64(double)`); + host.declare(`declare double @scr_f16_to_f64(i16 zeroext)`); + const narrowed = B.tmp(); + const t = B.tmp(); + B.line(`${narrowed} = call zeroext i16 @scr_f16_from_f64(double ${v.name})`); + B.line(`${t} = call double @scr_f16_to_f64(i16 zeroext ${narrowed})`); + return { name: t, type: e.type }; + } if (e.fn === "math.fround") { const v = host.emitExpr(e.args[0]!); const narrowed = B.tmp(); diff --git a/packages/compiler/src/backend/llvm/lib-process.ts b/packages/compiler/src/backend/llvm/lib-process.ts index 46d08a58f4..546c9a209a 100644 --- a/packages/compiler/src/backend/llvm/lib-process.ts +++ b/packages/compiler/src/backend/llvm/lib-process.ts @@ -4,6 +4,7 @@ import { undefinedArmTag } from "../../ir/analysis.js"; import { F64, RUNTIME_ERROR_CLASSES, STRING } from "../../ir/ir.js"; import type { LlvmEmitterContext, LibCallExpr, LlValue } from "./expr-context.js"; import { f64Lit } from "./common.js"; +import { mockTimerSym } from "./lib-shared.js"; export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { const B = host.B; @@ -18,12 +19,13 @@ export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr const argv = host.emitExpr(e.args[1]!); const cb = host.emitExpr(callbackArg); host.moveTemp(cb); + const shell = host.emitExpr(e.args[3]!); const adapter = host.execFileThunkFor(cbT); host.usesTimers = true; - host.declare(`declare ptr @scr_exec_file(ptr, ptr, ptr, ptr)`); + host.declare(`declare ptr @scr_exec_file(ptr, ptr, ptr, ptr, i1)`); const out = B.tmp(); B.line( - `${out} = call ptr @scr_exec_file(ptr ${cmd.name}, ptr ${argv.name}, ptr ${cb.name}, ptr @${adapter})`, + `${out} = call ptr @scr_exec_file(ptr ${cmd.name}, ptr ${argv.name}, ptr ${cb.name}, ptr @${adapter}, i1 ${shell.name})`, ); return host.own({ name: out, type: e.type }); } @@ -372,14 +374,16 @@ export function emitAsyncContextLibCall(host: LlvmEmitterContext, e: LibCallExpr host.usesTimers = true; const cb = host.emitExpr(e.args[0]!); host.moveTemp(cb); - const sym = { - "timers.setTimeout": "scr_set_timeout", - "timers.setInterval": "scr_set_interval", - "timers.setTimeoutHandle": "scr_set_timeout_handle", - "timers.setImmediate": "scr_set_immediate", - "process.nextTick": "scr_next_tick", - "timers.queueMicrotask": "scr_queue_microtask", - }[e.fn]; + const sym = + mockTimerSym(host, e) ?? + { + "timers.setTimeout": "scr_set_timeout", + "timers.setInterval": "scr_set_interval", + "timers.setTimeoutHandle": "scr_set_timeout_handle", + "timers.setImmediate": "scr_set_immediate", + "process.nextTick": "scr_next_tick", + "timers.queueMicrotask": "scr_queue_microtask", + }[e.fn]; const rest = e.args.slice(1).map((a) => host.emitExpr(a)); const argList = [`ptr ${cb.name}`, ...rest.map((a) => `${host.llType(a.type)} ${a.name}`)].join( ", ", @@ -405,21 +409,24 @@ export function emitAsyncContextLibCall(host: LlvmEmitterContext, e: LibCallExpr // The chaining forms: bookkeep, then yield the handle itself (the // C comma expressions). const h = host.emitExpr(e.args[0]!); - const sym = { - "timers.unref": "scr_timer_unref", - "timers.ref": "scr_timer_ref", - "timers.refresh": "scr_timer_refresh", - "timers.immediateUnref": "scr_immediate_unref", - "timers.immediateRef": "scr_immediate_ref", - }[e.fn]; + const sym = + mockTimerSym(host, e) ?? + { + "timers.unref": "scr_timer_unref", + "timers.ref": "scr_timer_ref", + "timers.refresh": "scr_timer_refresh", + "timers.immediateUnref": "scr_immediate_unref", + "timers.immediateRef": "scr_immediate_ref", + }[e.fn]; host.declare(`declare void @${sym}(double)`); B.line(`call void @${sym}(double ${h.name})`); return { name: h.name, type: e.type }; } if (e.fn === "timers.clearTimeout" || e.fn === "timers.clearInterval") { const h = host.emitExpr(e.args[0]!); - host.declare(`declare void @scr_clear_interval(double)`); - B.line(`call void @scr_clear_interval(double ${h.name})`); + const sym = mockTimerSym(host, e) ?? "scr_clear_interval"; + host.declare(`declare void @${sym}(double)`); + B.line(`call void @${sym}(double ${h.name})`); return { name: "", type: e.type }; } if (e.fn === "timers.clearNoop") { diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 9232cbd033..f69c96e6ed 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -37,7 +37,7 @@ export const ASSERT_VERDICT_LIB_FNS: ReadonlyMap = new Ma export const LIB_FN_SYMS: Record = { "assert.expectsErrDyn": "scr_assert_expects_err_dyn", - "insp.jsonDyn": "scr_dyn_format_j", + "insp.jsonDyn": "scr_util_format_j", "http.reqH2StreamOrThrow": "scr_http_req_h2_stream_or_throw", "intl.segmenterNew": "scr_intl_segmenter_new", "weakMap.is": "scr_weak_map_is", @@ -65,6 +65,7 @@ export const LIB_FN_SYMS: Record = { "arrayBuffer.viewU8": "scr_array_buffer_view_u8", "arrayBuffer.viewU32": "scr_array_buffer_view_u32", "arrayBuffer.viewI32": "scr_array_buffer_view_i32", + "arrayBuffer.viewF16": "scr_array_buffer_view_f16", "arrayBuffer.viewF32": "scr_array_buffer_view_f32", "arrayBuffer.viewF64": "scr_array_buffer_view_f64", "arrayBuffer.viewDV": "scr_array_buffer_view_dv", @@ -188,6 +189,7 @@ export const LIB_FN_SYMS: Record = { "os.uptime": "scr_os_uptime", "os.loadavg": "scr_os_loadavg", "os.getPriority": "scr_os_get_priority", + "os.setPriority": "scr_os_set_priority", "os.cpusTake": "scr_os_cpus_take", "os.cpusModel": "scr_os_cpus_model", "os.cpusField": "scr_os_cpus_field", @@ -198,6 +200,7 @@ export const LIB_FN_SYMS: Record = { "os.userHomedir": "scr_os_user_homedir", "os.tmpdir": "scr_os_tmpdir", "process.argv": "scr_process_argv", + "process.execArgv": "scr_process_exec_argv", "process.platform": "scr_process_platform", "process.cwd": "scr_process_cwd", "process.pid": "scr_process_pid", @@ -477,6 +480,7 @@ export const LIB_FN_SYMS: Record = { "strdec.write": "scr_strdec_write", "strdec.next": "scr_strdec_next", "strdec.end": "scr_strdec_end", + "strdec.encoding": "scr_strdec_encoding", "stats.isFile": "scr_stats_is_file", "stats.isDirectory": "scr_stats_is_dir", "stats.isSymbolicLink": "scr_stats_is_symlink", @@ -575,14 +579,17 @@ export const LIB_FN_SYMS: Record = { "process.stdoutWriteBytes": "scr_process_stdout_write_bytes", "process.stderrWriteBytes": "scr_process_stderr_write_bytes", "insp.buffer": "scr_insp_buffer", + "insp.typedArray": "scr_insp_typed_array", // util.inspect (scr_inspect.c): all args borrowed; string results +1; // the begin/entry/key/moreItems/end accumulator drives the emitted // container walks. None of these throw. "insp.f64": "scr_insp_f64", "insp.str": "scr_insp_str", "insp.regex": "scr_insp_regex", + "insp.date": "scr_insp_date", "insp.error": "scr_insp_error", "insp.begin": "scr_insp_begin", + "insp.layout": "scr_insp_layout", "insp.entry": "scr_insp_entry", "insp.key": "scr_insp_key", "insp.moreItems": "scr_insp_more_items", @@ -931,8 +938,18 @@ export const LIB_FN_SYMS: Record = { "worker.root": "scr_worker_root", "worker.data": "scr_worker_data", "worker.parentPort": "scr_worker_parent_port", + "worker.resourceLimits": "scr_worker_resource_limits", + "worker.threadName": "scr_worker_thread_name_current", "worker.channel": "scr_worker_message_channel", "worker.receiveMessageOnPort": "scr_worker_receive_message_on_port", + "worker.setEnvironmentData": "scr_worker_set_environment_data", + "worker.getEnvironmentData": "scr_worker_get_environment_data", + "worker.markUntransferable": "scr_worker_mark_untransferable", + "worker.isMarkedUntransferable": "scr_worker_is_marked_untransferable", + "dyn.markUncloneable": "scr_mark_uncloneable", + "stream.workerStdio": "scr_stream_worker_stdio", + "worker.broadcastChannel": "scr_worker_broadcast_channel", + "event.broadcastInstall": "scr_et_broadcast_install", "process.stdinDestroy": "scr_process_stdin_destroy", "process.stdinSetRawMode": "scr_process_stdin_set_raw_mode", // node:events EventEmitter (scr_events_emitter.c): receivers borrowed, @@ -961,6 +978,8 @@ export const LIB_FN_SYMS: Record = { "emitter.getMax": "scr_emitter_get_max", "emitter.setDefaultMax": "scr_emitter_set_default_max", "emitter.getDefaultMax": "scr_emitter_get_default_max", + "emitter.esmDefaultMax": "scr_emitter_esm_default_max_get", + "emitter.esmSync": "scr_emitter_esm_sync", "emitter.errorMonitor": "scr_emitter_error_monitor", // node:stream (scr_stream.c): receivers borrowed, chunks borrowed, // chaining forms answer the receiver +1. The throwing members @@ -1223,10 +1242,13 @@ export const LIB_FN_SYMS: Record = { // loop gate already runs) and diagnostics_channel (scr_dc.c): all // generic shapes; the throwing members ride the pending check. "tp.setTimeout": "scr_tp_set_timeout", + "tp.schedulerWait": "scr_tp_set_timeout", "tp.setImmediate": "scr_tp_set_immediate", "tp.setTimeoutValue": "scr_tp_set_timeout_value", "tp.setImmediateValue": "scr_tp_set_immediate_value", "tp.setTimeoutSignal": "scr_tp_set_timeout_signal", + "tp.schedulerWaitValue": "scr_tp_set_timeout_value", + "tp.schedulerWaitSignal": "scr_tp_set_timeout_signal", "tp.setImmediateSignal": "scr_tp_set_immediate_signal", // stream/promises' finished form (+1 promise; may-throw pending check; // the pipeline form needs the stream-array compound and emits below). @@ -1324,6 +1346,9 @@ export const LIB_FN_SYMS: Record = { "dyn.reflectGet": "scr_dyn_reflect_get", "dyn.usingMethod": "scr_dyn_using_method", "dyn.usingDispose": "scr_dyn_using_dispose", + "disposable.stackNew": "scr_disposable_stack_new", + "disposable.stackIs": "scr_disposable_stack_is", + "disposable.stackUseCallback": "scr_disposable_stack_use_callback", "dyn.reflectSet": "scr_dyn_reflect_set", "dyn.reflectDefine": "scr_dyn_reflect_define", "dyn.abstractEq": "scr_dyn_abstract_eq", @@ -1376,6 +1401,40 @@ export const LIB_FN_SYMS: Record = { "timers.immediatePromise": "scr_immediate_promise", }; +/** The mock-aware entry points (scr_test.c) that the timer calls of a + * program using mock.timers emit in place of the loop's functions; each + * falls back to the loop's function while its API is not mocked. */ +export const MOCK_TIMER_SYMS: Readonly> = { + "timers.setTimeout": "scr_test_mt_set_timeout", + "timers.setTimeoutHandle": "scr_test_mt_set_timeout_handle", + "timers.setInterval": "scr_test_mt_set_interval", + "timers.clearTimeout": "scr_test_mt_clear_timeout", + "timers.clearInterval": "scr_test_mt_clear_interval", + "timers.unref": "scr_test_mt_unref", + "timers.ref": "scr_test_mt_ref", + "timers.refresh": "scr_test_mt_refresh", + "timers.hasRef": "scr_test_mt_has_ref", + "timers.setImmediate": "scr_test_mt_set_immediate", + "timers.clearImmediate": "scr_test_mt_clear_immediate", + "timers.immediateUnref": "scr_test_mt_immediate_unref", + "timers.immediateRef": "scr_test_mt_immediate_ref", + "timers.immediateHasRef": "scr_test_mt_immediate_has_ref", + "date.now": "scr_test_mt_date_now", + "date.newNow": "scr_test_mt_date_now", + "tp.schedulerWait": "scr_test_mt_scheduler_wait", + "tp.schedulerWaitValue": "scr_test_mt_scheduler_wait_value", + "tp.schedulerWaitSignal": "scr_tp_scheduler_wait_signal", +}; + +/** The mock-aware symbol for a timer call of a program that uses + * mock.timers, undefined for other calls and for `realTimer` calls. */ +export function mockTimerSym( + host: { readonly mockTimers: boolean }, + e: { fn: string; realTimer?: true }, +): string | undefined { + return host.mockTimers && !e.realTimer ? MOCK_TIMER_SYMS[e.fn] : undefined; +} + export const USES_TIMERS_LIB_FNS = new Set([ "process.emitWarning", // Decipheriv.setAuthTag reports DEP0182 on the next tick. @@ -1393,6 +1452,7 @@ export const USES_TIMERS_LIB_FNS = new Set([ "worker.parentPort", "worker.channel", "worker.receiveMessageOnPort", + "worker.broadcastChannel", "process.stdio", "fetch.start", "fetch.input", diff --git a/packages/compiler/src/backend/llvm/lib-test.ts b/packages/compiler/src/backend/llvm/lib-test.ts index 79590cd9de..d0d9a9bfc2 100644 --- a/packages/compiler/src/backend/llvm/lib-test.ts +++ b/packages/compiler/src/backend/llvm/lib-test.ts @@ -66,6 +66,34 @@ export function emitTestLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlVal return finishRuntimeCall(host, e, "scr_test_ctx_mock_restore_all", a); case "test.ctxMockReset": return finishRuntimeCall(host, e, "scr_test_ctx_mock_reset", a); + case "test.snapshotValue": + return finishRuntimeCall(host, e, "scr_test_snapshot_value", a); + case "test.ctxSnapshot": + return finishRuntimeCall(host, e, "scr_test_ctx_snapshot", a); + case "test.fileSnapshot": + return finishRuntimeCall(host, e, "scr_test_file_snapshot", a); + case "test.snapshotSerialize": + return finishRuntimeCall(host, e, "scr_test_snapshot_serialize", a); + case "test.mockTimersEnable": + return finishRuntimeCall(host, e, "scr_test_mock_timers_enable", a); + case "test.mockTimersTick": + return finishRuntimeCall(host, e, "scr_test_mock_timers_tick", a); + case "test.mockTimersRunAll": + return finishRuntimeCall(host, e, "scr_test_mock_timers_run_all", a); + case "test.mockTimersReset": + return finishRuntimeCall(host, e, "scr_test_mock_timers_reset", a); + case "test.mockTimersSetTime": + return finishRuntimeCall(host, e, "scr_test_mock_timers_set_time", a); + case "test.ctxMockTimersEnable": + return finishRuntimeCall(host, e, "scr_test_ctx_mock_timers_enable", a); + case "test.ctxMockTimersTick": + return finishRuntimeCall(host, e, "scr_test_ctx_mock_timers_tick", a); + case "test.ctxMockTimersRunAll": + return finishRuntimeCall(host, e, "scr_test_ctx_mock_timers_run_all", a); + case "test.ctxMockTimersReset": + return finishRuntimeCall(host, e, "scr_test_ctx_mock_timers_reset", a); + case "test.ctxMockTimersSetTime": + return finishRuntimeCall(host, e, "scr_test_ctx_mock_timers_set_time", a); default: throw new InternalCompilerError(`unhandled test call ${e.fn}`); } diff --git a/packages/compiler/src/backend/llvm/record-results.ts b/packages/compiler/src/backend/llvm/record-results.ts index 41d76564e3..47a7983d2d 100644 --- a/packages/compiler/src/backend/llvm/record-results.ts +++ b/packages/compiler/src/backend/llvm/record-results.ts @@ -21,6 +21,16 @@ export function resultEntryName(name: string): string { return `${RESULT_ENTRY_PREFIX}${name}`; } +/** The local id of a result-slot entry's destination parameter. The + * caller passes zeroed frame storage, and on every path the entry stores + * each of its fields at most once, so a store into it is an + * initialization: there is no old value to release. */ +export const RESULT_DST_ID = "%into.dst"; + +function distinctNames(names: readonly string[]): boolean { + return new Set(names).size === names.length; +} + /** A plain record shape whose fields are numbers, booleans and inline * scalar unions (inline-unions.ts): its fields own nothing. */ export function scalarRecordShape( @@ -38,6 +48,15 @@ export function scalarRecordShape( ); } +/** A closed record or tuple shape: declared fields only. Its instance can + * live in caller storage; fields that own references are released by the + * frame that holds the storage (stack-objects.ts). Tuples are element + * records: destructuring and element reads project them, and every + * array-like use of one is a whole-value use that keeps it on the heap. */ +export function frameRecordShape(shape: IrRecordShape | undefined): boolean { + return shape !== undefined && !shape.indexValue; +} + /** A literal that stores every field of its shape, none of them through * the overflow map, and that sets no overflow-map flag. */ export function completeRecordLiteral( @@ -110,7 +129,7 @@ export function constructorInitializesFields(cls: IrClassDef, ctor: IrFunction): } /** Add a result-slot entry for every function whose returns all build a - * fresh scalar record or standalone scalar object, return another such + * fresh closed record or standalone scalar object, return another such * producer's result (forwarding the slot), or return a parameter of a * record shape no store can change (copied into the slot). The original * function stays as it is; the backend calls the entry only where the @@ -155,8 +174,12 @@ export function addRecordResultEntries(mod: IrModule): IrModule { }, }); const dynamicStores = dynamicFieldStores(mod); + // Copies are limited to scalar shapes: a copy of a reference-owning + // record would not carry the undeclared source keys a checked conversion + // may keep beside the original (record-sources.ts). const copyable = (type: IrType): boolean => type.kind === "record" && + scalarRecordShape(shapes.get(type.shapeId), inline) && mod.lib === undefined && !written.has(type.shapeId) && !dynamicStores.all && @@ -165,9 +188,11 @@ export function addRecordResultEntries(mod: IrModule): IrModule { const fillable = (value: IrExpr, params: ReadonlySet): boolean => { switch (value.kind) { case "recordLit": - return completeRecordLiteral( - value, - shapes.get((value.type as IrType & { kind: "record" }).shapeId), + return ( + completeRecordLiteral( + value, + shapes.get((value.type as IrType & { kind: "record" }).shapeId), + ) && distinctNames(value.fields.map((field) => field.name)) ); case "new": return stackClass(value.className); @@ -175,6 +200,15 @@ export function addRecordResultEntries(mod: IrModule): IrModule { return fillable(value.then, params) && fillable(value.else_, params); case "varRef": return params.has(value.localId) && copyable(value.type); + case "recordClone": + // A spread of a binding: the copies read it before any override + // evaluates, as the heap clone does. + return ( + value.source.kind === "varRef" && + value.type.kind === "record" && + frameRecordShape(shapes.get(value.type.shapeId)) && + distinctNames(value.overrides.map((override) => override.name)) + ); case "call": return producers.has(value.callee); default: @@ -210,6 +244,39 @@ export function addRecordResultEntries(mod: IrModule): IrModule { loc: value.loc, })); } + case "recordClone": { + const shapeId = (value.type as IrType & { kind: "record" }).shapeId; + const overridden = new Set(value.overrides.map((override) => override.name)); + const copies: IrStmt[] = shapes + .get(shapeId)! + .fields.filter((field) => !overridden.has(field.name)) + .map((field): IrStmt => ({ + kind: "recordSet", + obj: dst, + shapeId, + field: field.name, + value: { + kind: "recordGet", + obj: value.source, + shapeId, + field: field.name, + type: field.type, + loc: value.loc, + }, + loc: value.loc, + })); + return [ + ...copies, + ...value.overrides.map((override): IrStmt => ({ + kind: "recordSet", + obj: dst, + shapeId, + field: override.name, + value: override.value, + loc: override.value.loc, + })), + ]; + } case "call": return [ { @@ -327,7 +394,7 @@ export function addRecordResultEntries(mod: IrModule): IrModule { return false; const ret = fn.returnType; return ( - (ret.kind === "record" && scalarRecordShape(shapes.get(ret.shapeId), inline)) || + (ret.kind === "record" && frameRecordShape(shapes.get(ret.shapeId))) || (ret.kind === "object" && stackClass(ret.className)) ); }); @@ -360,7 +427,7 @@ export function addRecordResultEntries(mod: IrModule): IrModule { for (const fn of candidates) { if (!producers.has(fn.name)) continue; const ret = fn.returnType; - const dstId = "%into.dst"; + const dstId = RESULT_DST_ID; const dst: IrExpr = { kind: "varRef", localId: dstId, type: ret, loc: fn.loc }; const body = transformStmtList(fn.body, { expr: (e) => e, diff --git a/packages/compiler/src/backend/llvm/reference-writes.ts b/packages/compiler/src/backend/llvm/reference-writes.ts index 33110eaa4d..2371147a9b 100644 --- a/packages/compiler/src/backend/llvm/reference-writes.ts +++ b/packages/compiler/src/backend/llvm/reference-writes.ts @@ -106,6 +106,7 @@ const INERT_LIB_CALLS = new Set([ "regex.groupParseInt", "process.cwd", "process.argv", + "process.execArgv", "process.platform", "process.execPath", "process.isTTY", diff --git a/packages/compiler/src/backend/llvm/stack-objects.test.ts b/packages/compiler/src/backend/llvm/stack-objects.test.ts index 845d6ae5ea..a413db333d 100644 --- a/packages/compiler/src/backend/llvm/stack-objects.test.ts +++ b/packages/compiler/src/backend/llvm/stack-objects.test.ts @@ -275,3 +275,64 @@ console.log(pick({ x: 1, y: 2 }, { x: 3, y: 4 }, true).y, forward({ x: 1, y: 1 } .sort(); expect(entries).toEqual([`${RESULT_ENTRY_PREFIX}forward`, `${RESULT_ENTRY_PREFIX}pick`]); }); + +const TOKENS = ` +interface Token { kind: string; text: string; pos: number } +function make(kind: string, text: string, pos: number): Token { return { kind, text, pos }; } +function width(t: Token): number { return t.text.length + t.kind.length; } +function divmod(a: number, b: number): [number, number] { return [Math.floor(a / b), a % b]; } +interface State { name: string; count: number } +function bump(s: State): State { return { ...s, count: s.count + 1 }; } +export function tokens(n: number, w: string): number { + const t = { kind: "id", text: w, pos: n }; + const u = make("num", w, n); + const [q, r] = divmod(n, 7); + return t.pos + width(u) + width({ kind: w, text: w, pos: 0 }) + q + r; +} +export function states(n: number, w: string): number { + let s: State = { name: w, count: 0 }; + for (let i = 0; i < n; i++) { + s = { ...s, count: s.count + 1 }; + s = bump(s); + } + return s.count + s.name.length; +} +console.log(tokens(3, "ab"), states(4, "cd")); +`; + +test("records and tuples that own references live in frame storage", async () => { + const ir = emitLlvmModule(await moduleFor(TOKENS)); + const tokens = body(ir, "tokens"); + expect(tokens).not.toMatch(/@sc_rnew_/); + expect(tokens).toMatch(/call void @sc_\w*into_make\(/); + expect(tokens).toMatch(/call void @sc_\w*into_divmod\(/); + // Reference fields start empty in entry-addressed slots, and the frame + // releases them; an interned literal field (`kind: "id"`) needs neither. + const fields = [...tokens.matchAll(/(%s\d+) = getelementptr inbounds %sc_rs_\w+, ptr %s\d+/g)]; + const owned = fields.filter(([, ptr]) => tokens.includes(`store ptr null, ptr ${ptr}\n`)); + expect(owned.length).toBe(5); + expect(fields.length).toBe(6); + for (const [, ptr] of owned) + expect(tokens).toMatch(new RegExp(`load ptr, ptr ${ptr}\\n\\s+call void @\\w+\\(`)); +}); + +test("spread updates copy into the binding's slot and release the replaced fields", async () => { + const mod = await moduleFor(TOKENS); + const entries = addRecordResultEntries(mod) + .functions.map((fn) => fn.name) + .filter((name) => name.startsWith(RESULT_ENTRY_PREFIX)); + expect(entries).toEqual( + expect.arrayContaining([ + `${RESULT_ENTRY_PREFIX}make`, + `${RESULT_ENTRY_PREFIX}divmod`, + `${RESULT_ENTRY_PREFIX}bump`, + ]), + ); + const states = body(emitLlvmModule(mod), "states"); + expect(states).not.toMatch(/@sc_rnew_|@sc_rclone_/); + expect(states).toMatch(/call void @sc_\w*into_bump\(/); + // The old name is loaded before the copy and released after it. + expect(states).toMatch( + /(%t\d+) = load ptr, ptr %s\d+\n\s+(%t\d+) = load %sc_rs_\w+, ptr %s\d+\n\s+store %sc_rs_\w+ \2, ptr %s\d+\n(?:.*\n)*?\s+call void @\w+\(ptr \1\)/, + ); +}); diff --git a/packages/compiler/src/backend/llvm/stack-objects.ts b/packages/compiler/src/backend/llvm/stack-objects.ts index c03c7fb442..e9f437f149 100644 --- a/packages/compiler/src/backend/llvm/stack-objects.ts +++ b/packages/compiler/src/backend/llvm/stack-objects.ts @@ -1,9 +1,16 @@ -import { VOID, typeEquals, type IrExpr, type IrFunction, type IrType } from "../../ir/ir.js"; +import { + VOID, + isRefCounted, + typeEquals, + type IrExpr, + type IrFunction, + type IrType, +} from "../../ir/ir.js"; import { completeRecordLiteral, constructorInitializesFields, + frameRecordShape, resultEntryName, - scalarRecordShape, scalarStandaloneClass, } from "./record-results.js"; import { everyStmtList } from "../../ir/traverse.js"; @@ -14,23 +21,45 @@ import type { CallLifetimes } from "./call-lifetimes.js"; import { emitCallExpr, emitConstruction } from "./expr-calls.js"; import type { LlValue, LlvmEmitterContext } from "./expr-context.js"; import { emitRecordLitFields } from "./expr-primitives.js"; +import { llFieldType } from "./shapes.js"; + +export type StackObjectSource = IrExpr & { kind: "recordLit" | "recordClone" | "new" | "call" }; + +/** A field of a frame object that owns a reference: an entry-block pointer + * to its slot, and the field's type for the release. */ +export interface StackObjectField { + name: string; + ptr: string; + type: IrType; +} -export type StackObjectSource = IrExpr & { kind: "recordLit" | "new" | "call" }; +/** Records the owners of a frame object's reference fields before anything + * stores them: a lexical scope for bindings, a statement frame for + * temporaries. */ +export type StackObjectFieldOwner = (fields: readonly StackObjectField[]) => void; -/** A record literal, `new` or result-slot producer call whose object can - * live in the frame's own storage: a plain scalar record shape, or a +/** A record literal, spread copy, `new` or result-slot producer call whose + * object can live in the frame's own storage: a closed record shape, or a * standalone scalar class whose constructor never lets `this` escape * (record-results.ts). The caller proves that every use of the value only - * projects it. */ + * projects it. Record fields may own references; the frame releases them + * (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 { + 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 // caller's storage. if (e.kind === "call") return host.fnByName.has(resultEntryName(e.callee)); if (e.kind === "recordLit") { if (e.type.kind !== "record") return false; const shape = host.recordsById.get(e.type.shapeId); - return scalarRecordShape(shape, host.inlineUnions) && completeRecordLiteral(e, shape); + return frameRecordShape(shape) && completeRecordLiteral(e, shape); } + // A same-shape spread (`{ ...source, field: value }`) copies the source's + // fields; the frontend builds it only for plain declared-field records. + if (e.kind === "recordClone") + return e.type.kind === "record" && frameRecordShape(host.recordsById.get(e.type.shapeId)); if (e.kind !== "new") return false; const meta = host.classMeta.get(e.className); return ( @@ -45,6 +74,33 @@ export function canStackObject(host: LlvmEmitterContext, e: IrExpr): e is StackO ); } +/** Whether a frame object of this type has fields that own references. */ +function ownsReferences(host: LlvmEmitterContext, type: IrType): boolean { + return ( + type.kind === "record" && + host + .recordShape(type.shapeId) + .fields.some((field) => isRefCounted(field.type) && !host.inlineUnions.record(field.type)) + ); +} + +/** Locals declared directly in a switch case. The cases share one scope + * whose exit runs whichever case was entered, so a scope-owned field would + * be released without its declaration having run. */ +function caseScopedLocals(fn: IrFunction): Set { + const ids = new Set(); + everyStmtList(fn.body, { + expr: () => true, + stmt: (stmt) => { + if (stmt.kind === "switch") + for (const c of stmt.cases) + for (const s of c.body) if (s.kind === "varDecl") ids.add(s.localId); + return true; + }, + }); + return ids; +} + /** Locals initialized once by a stack-eligible object and only projected * afterwards (CallLifetimes.locals): field reads, and arguments to * parameters that are themselves only projected. */ @@ -56,6 +112,7 @@ export function findLocalStackObjects( const result = new Map(); const safe = lifetimes.locals.get(fn.name); if (!safe?.size || fn.async || fn.generator) return result; + const caseScoped = caseScopedLocals(fn); everyStmtList(fn.body, { expr: () => true, stmt: (stmt) => { @@ -63,7 +120,8 @@ export function findLocalStackObjects( stmt.kind === "varDecl" && safe.has(stmt.localId) && stmt.init && - canStackObject(host, stmt.init) + canStackObject(host, stmt.init) && + !(caseScoped.has(stmt.localId) && ownsReferences(host, stmt.init.type)) ) result.set(stmt.localId, stmt.init); return true; @@ -112,7 +170,10 @@ export function findReassignedStackObjects( return true; }, }); - for (const id of result) if (!initialized.has(id)) result.delete(id); + const caseScoped = caseScopedLocals(fn); + for (const id of result) + if (!initialized.has(id) || (caseScoped.has(id) && ownsReferences(host, types.get(id)!))) + result.delete(id); return result; } @@ -122,13 +183,77 @@ export function stackObjectStruct(type: IrType): string { : `%${mangleClassStruct((type as IrType & { kind: "object" }).className)}`; } +/** The reference fields of the frame object in `slot`, addressed from the + * entry block so that every cleanup path can reach them. Inline union + * fields copy bits and own nothing; class objects in frame storage are + * scalar. */ +export function stackObjectFields( + host: LlvmEmitterContext, + type: IrType, + slot: string, +): readonly StackObjectField[] { + if (type.kind !== "record") return []; + const cached = host.stackObjectFieldPtrs.get(slot); + if (cached) return cached; + const struct = stackObjectStruct(type); + const fields: StackObjectField[] = []; + host.recordShape(type.shapeId).fields.forEach((field, index) => { + if (!isRefCounted(field.type) || host.inlineUnions.record(field.type)) return; + const ptr = host.B.slot(); + host.B.entryAllocas.push( + `${ptr} = getelementptr inbounds ${struct}, ptr ${slot}, i64 0, i32 ${index + 1}`, + ); + fields.push({ name: field.name, ptr, type: field.type }); + }); + host.stackObjectFieldPtrs.set(slot, fields); + return fields; +} + +/** Copy a same-shape spread's source fields into `slot`, retaining the + * references, then store the overrides in source order. The source is + * read once, before any override evaluates, as the heap clone helper + * reads it; an overridden field is never copied, so its slot stays empty + * until the override initializes it. */ +function emitRecordCloneFields( + host: LlvmEmitterContext, + e: IrExpr & { kind: "recordClone" }, + slot: string, +): void { + const B = host.B; + const shapeId = (e.type as IrType & { kind: "record" }).shapeId; + const overridden = new Set(e.overrides.map((override) => override.name)); + const source = host.emitReadReceiver(e.source); + for (const field of host.recordShape(shapeId).fields) { + if (overridden.has(field.name)) continue; + const from = host.recordFieldPtr(source.name, shapeId, field.name); + const to = host.recordFieldPtr(slot, shapeId, field.name); + const fieldTy = llFieldType(from.type, from.inline); + const v = B.tmp(); + B.line(`${v} = load ${fieldTy}, ptr ${from.ptr}`); + const stored = isRefCounted(from.type) && !from.inline ? host.retainValue(v, from.type) : v; + B.line(`store ${fieldTy} ${stored}, ptr ${to.ptr}`); + } + const initialized = new Set(); + for (const override of e.overrides) { + const v = host.emitFieldValue(override.value, host.recordFieldInline(shapeId, override.name)); + const { ptr, type, inline } = host.recordFieldPtr(slot, shapeId, override.name); + // The first store fills an empty slot; a repeated name replaces it. + if (initialized.has(override.name)) host.replaceField(ptr, type, inline, v); + else host.initField(ptr, type, inline, v); + initialized.add(override.name); + } +} + /** Build the object in an entry-block slot (or `into`, an existing one). * The immortal reference count makes every retain and release a no-op; * the proof that the value is only projected keeps the pointer from - * outliving the frame. */ + * outliving the frame. Reference fields start empty and are handed to + * `owner` before anything is stored, so an exception while the object is + * being built releases exactly the fields stored so far. */ export function emitStackObject( host: LlvmEmitterContext, e: StackObjectSource, + owner: StackObjectFieldOwner, into?: string, ): LlValue { const B = host.B; @@ -139,6 +264,18 @@ export function emitStackObject( slot = B.slot(); B.entryAllocas.push(`${slot} = alloca ${struct}, align 8`); } + // A literal's interned string fields are immortal and need no owner. A + // binding's own slot (`into`) keeps an owner for every field: later + // assignments copy arbitrary values into it. + const literals = + into === undefined && e.kind === "recordLit" + ? new Set(e.fields.filter((f) => f.value.kind === "strLit").map((f) => f.name)) + : new Set(); + const fields = stackObjectFields(host, type, slot).filter((f) => !literals.has(f.name)); + if (fields.length > 0) { + for (const field of fields) B.line(`store ptr null, ptr ${field.ptr}`); + owner(fields); + } // Literals store every field; constructors that store every field // before reading one need no zeroed storage either. if (type.kind === "object") { @@ -169,6 +306,7 @@ export function emitStackObject( host.markImmortal({ name: slot, type }), ); } else if (e.kind === "recordLit") emitRecordLitFields(host, e, slot); + else if (e.kind === "recordClone") emitRecordCloneFields(host, e, slot); else emitConstruction(host, e, slot, host.fnByName.get(`%${e.className}.constructor`)!); return { name: slot, type: e.type }; } diff --git a/packages/compiler/src/backend/llvm/walkers.ts b/packages/compiler/src/backend/llvm/walkers.ts index d3c4f29909..9b8d80b874 100644 --- a/packages/compiler/src/backend/llvm/walkers.ts +++ b/packages/compiler/src/backend/llvm/walkers.ts @@ -64,9 +64,6 @@ export class LlWalkers { private get S(): "i32" | "i64" { return this.host.sizeType; } - private abiOffset(native64: number, wasm32: number): number { - return this.S === "i32" ? wasm32 : native64; - } /** The value-parameter LLVM type of a writer for `t`. */ private valTy(t: IrType): string { @@ -220,10 +217,10 @@ export class LlWalkers { B.line(`call void @scr_jb_put_json_str(ptr %b, ptr %v)`); break; case "dyn": - // Overflow values under an `unknown` index signature: the checked-dynamic tree - // serializes itself (runtime walker). Bare stringify of dyn stays - // frontend-fenced; this writer is reachable only through overflow - // entries. + // A checked-dynamic value serializes itself (runtime walker). + // Bare stringify of dyn stays frontend-fenced, and overflow + // entries under an `unknown` index signature go through + // scr_jb_put_dyn_entry, which also runs the value's toJSON. this.host.declare(`declare void @scr_jb_put_dyn(ptr, ptr)`); B.line(`call void @scr_jb_put_dyn(ptr %b, ptr %v)`); break; @@ -494,10 +491,13 @@ export class LlWalkers { } /** Whether the walker for `t` can leave an exception pending: cyclic - * values, and records serialized through a kept checked source. */ + * values, records serialized through a kept checked source, and + * checked-dynamic values (toJSON methods, getters, BigInts). */ jsonWriteMayThrow(t: IrType, seen = new Set()): boolean { if (traceAdapter(this.host, t) !== null) return true; switch (t.kind) { + case "dyn": + return true; case "array": return this.jsonWriteMayThrow(t.elem, seen); case "union": @@ -537,6 +537,15 @@ export class LlWalkers { `${ovfp} = getelementptr inbounds %${mangleRecordStruct(shape.id)}, ptr %v, i64 0, i32 ${shape.fields.length + 1}`, ); B.line(`${ovf} = load ptr, ptr ${ovfp} ; overflow map`); + if (iv.kind === "dyn") { + // The runtime writes every entry with JSON.stringify's member rules: + // each value's toJSON runs with its key before the entry is dropped + // or written, and containers below it join the circular-structure + // stack. It may leave an exception pending. + host.declare(`declare void @scr_jb_put_dyn_entries(ptr, ptr, ptr, ptr)`); + B.line(`call void @scr_jb_put_dyn_entries(ptr %b, ptr %v, ptr ${ovf}, ptr ${first})`); + return; + } host.declare(`declare ptr @scr_map_keys_js_order(ptr)`); host.declare(`declare double @scr_arr_len(ptr)`); host.declare(`declare ptr @scr_arr_get_ref(ptr, double)`); @@ -576,23 +585,9 @@ export class LlWalkers { B.line(`${val} = call ptr @scr_map_get_str_ref(ptr ${ovf}, ptr ${k}) ; value (+1)`); } // Undefined-valued entries drop, exactly the optional-field rule: a - // dyn value whose dyn kind follows its size_t RC word (enum - // member 6 — scr_runtime.h's ScrDynKind), or a union holding its - // undefined arm. + // union holding its undefined arm. let skipUndef: string | null = null; - if (iv.kind === "dyn") { - const kp = B.tmp(); - const kd = B.tmp(); - const isu = B.tmp(); - B.line( - `${kp} = getelementptr inbounds i8, ptr ${val}, i64 ${this.abiOffset(8, 4)} ; ->kind`, - ); - B.line(`${kd} = load i32, ptr ${kp}`); - B.line( - `${isu} = icmp eq i32 ${kd}, 6 ; SCR_DYN_UNDEF (NULL is 0 — null members DO serialize)`, - ); - skipUndef = isu; - } else if (iv.kind === "union" && undefinedArmTag(iv, this.host.unionsById) >= 0) { + if (iv.kind === "union" && undefinedArmTag(iv, this.host.unionsById) >= 0) { const tag = this.unionTag(B, val, iv.unionId); const isu = B.tmp(); B.line(`${isu} = icmp eq i32 ${tag}, ${undefinedArmTag(iv, this.host.unionsById)}`); diff --git a/packages/compiler/src/backend/target-diagnostics.ts b/packages/compiler/src/backend/target-diagnostics.ts index 65d954fb5d..365d5e913c 100644 --- a/packages/compiler/src/backend/target-diagnostics.ts +++ b/packages/compiler/src/backend/target-diagnostics.ts @@ -129,6 +129,7 @@ const WASI_UNAVAILABLE_HOST_FACTS: ReadonlyMap = new Map([ ["os.loadavg", "system load averages (WASI Preview 1 has no load API)"], ["os.uptime", "system uptime (WASI Preview 1 has no boot-time API)"], ["os.getPriority", "process scheduling priority (WASI Preview 1 has no priority API)"], + ["os.setPriority", "process scheduling priority (WASI Preview 1 has no priority API)"], ["process.memoryUsageTake", "process memory statistics (WASI Preview 1 has no memory API)"], ["process.rss", "process memory statistics (WASI Preview 1 has no memory API)"], ["process.ppid", "the parent process id (WASI Preview 1 has no process API)"], diff --git a/packages/compiler/src/diagnostics/diagnostic.ts b/packages/compiler/src/diagnostics/diagnostic.ts index 9a7b4a37a7..eaae1b5313 100644 --- a/packages/compiler/src/diagnostics/diagnostic.ts +++ b/packages/compiler/src/diagnostics/diagnostic.ts @@ -411,7 +411,7 @@ export const UNSUPPORTED: Record = { SC1120: { feature: "this regex feature", milestone: "later", - hint: "supported: literal regexes with the g/i/m/s/u/y flags — .test(), .exec()/.match()/.matchAll(), named capture groups (.groups, $ templates, \\k), .source/.flags, and string replace/replaceAll/split with string replacement templates", + hint: "supported: literal regexes with the g/i/m/s/u/v/y flags — .test(), .exec()/.match()/.matchAll(), named capture groups (.groups, $ templates, \\k), .source/.flags, and string replace/replaceAll/split with string replacement templates", }, // SC1121 (stateful g/y regex tests) shipped — code retired, do not reuse. }; diff --git a/packages/compiler/src/diagnostics/render.test.ts b/packages/compiler/src/diagnostics/render.test.ts new file mode 100644 index 0000000000..c5078dd0e9 --- /dev/null +++ b/packages/compiler/src/diagnostics/render.test.ts @@ -0,0 +1,56 @@ +import { expect, test } from "vitest"; +import type { ScrDiagnostic } from "./diagnostic.js"; +import { renderDiagnostic, renderDiagnostics } from "./render.js"; + +function diag(file: string, start: number, end: number): ScrDiagnostic { + return { code: "SC2020", message: "not supported", loc: { file, start, end } }; +} + +test("a short line renders whole", () => { + const text = "const a = 1;\nconst b = a.c;\nconst d = 2;"; + const start = text.indexOf("a.c"); + expect(renderDiagnostic(diag("m.ts", start, start + 3), { text })).toBe( + [ + "m.ts:2:11 - error SC2020: not supported", + "", + " 1 | const a = 1;", + " 2 | const b = a.c;", + " | ^~~", + " 3 | const d = 2;", + ].join("\n"), + ); +}); + +test("a long line renders a window around the column", () => { + const line = "x".repeat(1000) + "bad" + "y".repeat(1000); + const text = "first\n" + line + "\nlast"; + const start = text.indexOf("bad"); + const out = renderDiagnostic(diag("min.js", start, start + 3), { text }); + const lines = out.split("\n"); + expect(lines[0]).toBe("min.js:2:1001 - error SC2020: not supported"); + // the window starts 120 characters before the column + expect(lines[3]).toBe(` 2 | …${"x".repeat(120)}bad${"y".repeat(277)}…`); + expect(lines[4]).toBe(` | ${" ".repeat(121)}^~~`); + // context lines share the window: "first" and "last" start before it + expect(lines[2]).toBe(" 1 | …"); + expect(lines[5]).toBe(" 3 | …"); +}); + +test("a window at the line's end keeps the last characters", () => { + const line = "a".repeat(600) + "end"; + const start = line.indexOf("end"); + const out = renderDiagnostic(diag("e.js", start, start + 3), { text: line }); + const lines = out.split("\n"); + expect(lines[2]).toBe(` 1 | …${"a".repeat(397)}end`); + expect(lines[3]).toBe(` | ${" ".repeat(398)}^~~`); +}); + +test("thousands of diagnostics on a minified line render without exhausting string length", () => { + // one 2 MB line with 20,000 diagnostics: rendering every frame line whole + // would need tens of gigabytes + const text = "a;".repeat(1_000_000); + const diags = Array.from({ length: 20_000 }, (_, i) => diag("bundle.js", i * 100, i * 100 + 1)); + const out = renderDiagnostics(diags, new Map([["bundle.js", text]])); + expect(out.split(" - error SC2020: ").length - 1).toBe(20_000); + expect(out.length).toBeLessThan(20_000 * 1_000); +}); diff --git a/packages/compiler/src/diagnostics/render.ts b/packages/compiler/src/diagnostics/render.ts index 7fd68910fe..e5a75c6bcf 100644 --- a/packages/compiler/src/diagnostics/render.ts +++ b/packages/compiler/src/diagnostics/render.ts @@ -1,6 +1,8 @@ /* Terminal rendering: file:line:col, a hand-rolled code frame with ±1 line * of context and a ^~~~ underline sized to the span, optional hint line, - * and the optional attributed profile-note line after it. + * and the optional attributed profile-note line after it. A source line + * longer than FRAME_MAX_WIDTH (minified sources) shows a window of that + * width around the diagnostic's column, with "…" where text is cut. */ import type { ScrDiagnostic } from "./diagnostic.js"; @@ -11,6 +13,27 @@ export interface RenderOptions { interface SourceLookup { /** Full text of the file the diagnostic points into. */ text: string; + /** The text's lines and their start offsets; computed from `text` when + * absent. renderDiagnostics computes them once per file. */ + index?: LineIndex; +} + +interface LineIndex { + lines: string[]; + lineStarts: number[]; +} + +/** Frame lines longer than this many characters show a window of it. */ +const FRAME_MAX_WIDTH = 400; +/** Characters a windowed frame keeps before the diagnostic's column. */ +const FRAME_LEAD = 120; + +function lineIndex(text: string): LineIndex { + const lineStarts = [0]; + for (let i = 0; i < text.length; i++) { + if (text[i] === "\n") lineStarts.push(i + 1); + } + return { lines: text.split("\n"), lineStarts }; } const RED = "\x1b[31m"; @@ -33,32 +56,52 @@ export function renderDiagnostic( const frame: string[] = []; if (source) { - const lineStarts = [0]; - for (let i = 0; i < source.text.length; i++) { - if (source.text[i] === "\n") lineStarts.push(i + 1); - } + const { lines, lineStarts } = source.index ?? lineIndex(source.text); + // the last line starting at or before the diagnostic let lo = 0; - while (lo + 1 < lineStarts.length && lineStarts[lo + 1]! <= diag.loc.start) lo++; + let hi = lineStarts.length - 1; + while (lo < hi) { + const mid = (lo + hi + 1) >> 1; + if (lineStarts[mid]! <= diag.loc.start) lo = mid; + else hi = mid - 1; + } lineNum = lo + 1; colNum = diag.loc.start - lineStarts[lo]! + 1; - const lines = source.text.split("\n"); + const lineText = lines[lineNum - 1] ?? ""; + // The window every frame line shows: the whole line, or for a long + // line FRAME_MAX_WIDTH characters starting FRAME_LEAD before the column. + const windowStart = + lineText.length > FRAME_MAX_WIDTH + ? Math.max(0, Math.min(colNum - 1 - FRAME_LEAD, lineText.length - FRAME_MAX_WIDTH)) + : 0; + const shown = (text: string): string => { + if (windowStart === 0 && text.length <= FRAME_MAX_WIDTH) return text; + const end = windowStart + FRAME_MAX_WIDTH; + return ( + (windowStart > 0 ? "…" : "") + text.slice(windowStart, end) + (text.length > end ? "…" : "") + ); + }; const gutterWidth = String(Math.min(lineNum + 1, lines.length)).length; const emitLine = (n: number) => { const text = lines[n - 1]; if (text === undefined) return; - frame.push(c(DIM, ` ${String(n).padStart(gutterWidth)} | `) + text); + frame.push(c(DIM, ` ${String(n).padStart(gutterWidth)} | `) + shown(text)); }; emitLine(lineNum - 1); emitLine(lineNum); - const lineText = lines[lineNum - 1] ?? ""; + const caretCol = colNum - 1 - windowStart + (windowStart > 0 ? 1 : 0); + const visibleEnd = + windowStart === 0 && lineText.length <= FRAME_MAX_WIDTH + ? lineText.length + : Math.min(lineText.length, windowStart + FRAME_MAX_WIDTH); const spanOnLine = Math.max( 1, - Math.min(diag.loc.end - diag.loc.start, lineText.length - (colNum - 1)), + Math.min(diag.loc.end - diag.loc.start, visibleEnd - (colNum - 1)), ); frame.push( c(DIM, ` ${" ".repeat(gutterWidth)} | `) + - " ".repeat(colNum - 1) + + " ".repeat(caretCol) + c(/^SC6\d{3}$/.test(diag.code) ? YELLOW : RED, "^" + "~".repeat(spanOnLine - 1)), ); emitLine(lineNum + 1); @@ -94,10 +137,17 @@ export function renderDiagnostics( const sorted = [...diags].sort( (a, b) => a.loc.file.localeCompare(b.loc.file) || a.loc.start - b.loc.start, ); + const indexes = new Map(); return sorted .map((d) => { const text = sourceTextByFile.get(d.loc.file); - return renderDiagnostic(d, text === undefined ? undefined : { text }, opts); + if (text === undefined) return renderDiagnostic(d, undefined, opts); + let index = indexes.get(d.loc.file); + if (index === undefined) { + index = lineIndex(text); + indexes.set(d.loc.file, index); + } + return renderDiagnostic(d, { text, index }, opts); }) .join("\n\n"); } diff --git a/packages/compiler/src/frontend/lowering/builtin-calls.ts b/packages/compiler/src/frontend/lowering/builtin-calls.ts index a0aa0ea252..7bb5f3e624 100644 --- a/packages/compiler/src/frontend/lowering/builtin-calls.ts +++ b/packages/compiler/src/frontend/lowering/builtin-calls.ts @@ -1,5 +1,10 @@ import { lowerReadlineCursorCall } from "./builtins/readline.js"; -import { lowerReceiveMessageOnPort } from "./builtins/workers.js"; +import { + lowerEnvironmentData, + lowerReceiveMessageOnPort, + lowerUncloneable, + lowerUntransferable, +} from "./builtins/workers.js"; import * as ts from "../ts7/adapter.js"; import { dynUndefinedExpr } from "../../ir/build.js"; import { DYN, type IrExpr, type SrcLoc } from "../../ir/ir.js"; @@ -57,8 +62,8 @@ export function lowerBuiltinCall( loc: SrcLoc, namespaceMember?: ts.MemberName, ): IrExpr { - if (bi.module === "child_process" && bi.member === "execFile") { - return lowerer.lowerExecFileCall(expr, loc); + if (bi.module === "child_process" && (bi.member === "execFile" || bi.member === "exec")) { + return lowerer.lowerExecFileCall(expr, loc, bi.member === "exec"); } if (bi.module === "events" && bi.member === "addAbortListener") { return lowerAddAbortListener(lowerer, expr, loc); @@ -101,6 +106,21 @@ export function lowerBuiltinCall( if (bi.module === "worker_threads" && bi.member === "receiveMessageOnPort") { return lowerReceiveMessageOnPort(lowerer, expr); } + if ( + bi.module === "worker_threads" && + (bi.member === "setEnvironmentData" || bi.member === "getEnvironmentData") + ) { + return lowerEnvironmentData(lowerer, expr, bi.member); + } + if ( + bi.module === "worker_threads" && + (bi.member === "markAsUntransferable" || bi.member === "isMarkedAsUntransferable") + ) { + return lowerUntransferable(lowerer, expr, bi.member); + } + if (bi.module === "worker_threads" && bi.member === "markAsUncloneable") { + return lowerUncloneable(lowerer, expr); + } const builtinFn = builtinModuleFnOf(lowerer, bi.module, bi.member); if (!builtinFn) { lowerer.noLowering( diff --git a/packages/compiler/src/frontend/lowering/builtins/child-process.ts b/packages/compiler/src/frontend/lowering/builtins/child-process.ts index 8c06a28df6..ecec14fc5b 100644 --- a/packages/compiler/src/frontend/lowering/builtins/child-process.ts +++ b/packages/compiler/src/frontend/lowering/builtins/child-process.ts @@ -725,35 +725,57 @@ export function lowerForkCall(lowerer: Lowerer, expr: ts.CallExpression, loc: Sr } /** `execFile(file[, args][, options], callback)` — the asynchronous callback - * slice. The child starts immediately with stdin/stdout/stderr piped; the - * runtime captures both outputs and invokes the error-first callback after - * settlement. The callback may ignore a suffix of `(error, stdout, + * slice. With `shell`, `exec(command[, options], callback)`: the command + * line runs with /bin/sh -c, as execSync runs it, and failure messages show + * the command line as Node's do. The child starts immediately with + * stdin/stdout/stderr piped; the runtime captures both outputs and invokes + * the error-first callback after settlement. The callback may ignore a suffix of `(error, stdout, * stderr)`, but every declared parameter must have the Node shape. Inline * `encoding: "utf8"` and a pure numeric `maxBuffer` are accepted; the latter * is not enforced by the growing native capture, as * with the synchronous and promisified capture forms. Other options remain * fenced until their lifecycle can share this asynchronous core. */ -export function lowerExecFileCall(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { +export function lowerExecFileCall( + lowerer: Lowerer, + expr: ts.CallExpression, + loc: SrcLoc, + shell = false, +): IrExpr { + const what = shell ? "exec" : "execFile"; if ( expr.arguments.some(ts.isSpreadElement) || expr.arguments.length < 2 || - expr.arguments.length > 4 + expr.arguments.length > (shell ? 3 : 4) ) { lowerer.noLowering( - `execFile with ${expr.arguments.length} arguments`, + `${what} with ${expr.arguments.length} arguments`, expr, - "the supported callback forms are execFile(file, callback), execFile(file, args, callback), and execFile(file, args, { encoding: 'utf8', maxBuffer: N }, callback)", + shell + ? "the supported callback forms are exec(command, callback) and exec(command, { encoding: 'utf8', maxBuffer: N }, callback)" + : "the supported callback forms are execFile(file, callback), execFile(file, args, callback), and execFile(file, args, { encoding: 'utf8', maxBuffer: N }, callback)", ); } - const cmd = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); - const argsNode = expr.arguments.length >= 3 ? expr.arguments[1] : undefined; + const first = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); const callbackNode = expr.arguments[expr.arguments.length - 1]!; - const argv = lowerChildArgsArg(lowerer, argsNode, loc); - if (expr.arguments.length === 4) { - const options = expr.arguments[2]!; + // exec: /bin/sh -c , execSync's lowering (the runtime reads the + // command line back from argv[1] for failure messages); execFile: the + // file and its args list. + const cmd: IrExpr = shell ? { kind: "strLit", value: "/bin/sh", type: STRING, loc } : first; + const argv: IrExpr = shell + ? { + kind: "arrayLit", + elems: [{ kind: "strLit", value: "-c", type: STRING, loc }, first], + type: arrayOf(STRING), + loc, + } + : lowerChildArgsArg(lowerer, expr.arguments.length >= 3 ? expr.arguments[1] : undefined, loc); + const optionsNode = + expr.arguments.length === (shell ? 3 : 4) ? expr.arguments[shell ? 1 : 2] : undefined; + if (optionsNode) { + const options = optionsNode; if (!ts.isObjectLiteralExpression(options)) { lowerer.noLowering( - "execFile with a non-literal options argument", + `${what} with a non-literal options argument`, options, "pass encoding and maxBuffer in an inline object literal", ); @@ -774,7 +796,7 @@ export function lowerExecFileCall(lowerer: Lowerer, expr: ts.CallExpression, loc const member = optionMember(property); if (!member) lowerer.noLowering( - "execFile with this options shape", + `${what} with this options shape`, property, "use plain encoding and maxBuffer properties without spreads or computed keys", ); @@ -784,7 +806,7 @@ export function lowerExecFileCall(lowerer: Lowerer, expr: ts.CallExpression, loc (member.value.text !== "utf8" && member.value.text !== "utf-8") ) { lowerer.noLowering( - "execFile with a non-literal utf8 encoding", + `${what} with a non-literal utf8 encoding`, member.value, "pass the literal 'utf8' or 'utf-8' (other expressions might have side effects)", ); @@ -792,14 +814,14 @@ export function lowerExecFileCall(lowerer: Lowerer, expr: ts.CallExpression, loc } else if (member.name === "maxBuffer") { if (!pureNumber(member.value)) { lowerer.noLowering( - "execFile with a non-literal maxBuffer", + `${what} with a non-literal maxBuffer`, member.value, "pass a numeric literal or literal arithmetic (the native capture grows without enforcing this limit)", ); } } else { lowerer.noLowering( - `execFile option '${member.name}'`, + `${what} option '${member.name}'`, property, "only encoding: 'utf8' and a pure numeric maxBuffer are supported in the callback form", ); @@ -815,14 +837,14 @@ export function lowerExecFileCall(lowerer: Lowerer, expr: ts.CallExpression, loc lowerer.unsupported( "SC1090", callbackNode, - "execFile callbacks take (error), (error, stdout), (error, stdout, stderr), or no parameters", + `${what} callbacks take (error), (error, stdout), (error, stdout, stderr), or no parameters`, ); } if (callback.type.ret.kind !== "void") { lowerer.unsupported( "SC1090", callbackNode, - "execFile callbacks returning a value (make the callback body a block, or return nothing)", + `${what} callbacks returning a value (make the callback body a block, or return nothing)`, ); } const errorParam = callback.type.params[0]; @@ -837,7 +859,7 @@ export function lowerExecFileCall(lowerer: Lowerer, expr: ts.CallExpression, loc lowerer.unsupported( "SC1090", callbackNode, - `execFile callbacks whose first parameter is not 'Error | null' (got '${lowerer.fmt(errorParam)}')`, + `${what} callbacks whose first parameter is not 'Error | null' (got '${lowerer.fmt(errorParam)}')`, ); } } @@ -846,9 +868,15 @@ export function lowerExecFileCall(lowerer: Lowerer, expr: ts.CallExpression, loc lowerer.unsupported( "SC1090", callbackNode, - `execFile callbacks whose ${i === 1 ? "stdout" : "stderr"} parameter is not 'string'`, + `${what} callbacks whose ${i === 1 ? "stdout" : "stderr"} parameter is not 'string'`, ); } } - return { kind: "libCall", fn: "cp.execFile", args: [cmd, argv, callback], type: CHILD_T, loc }; + return { + kind: "libCall", + fn: "cp.execFile", + args: [cmd, argv, callback, boolLit(shell, loc)], + type: CHILD_T, + loc, + }; } diff --git a/packages/compiler/src/frontend/lowering/builtins/dates.ts b/packages/compiler/src/frontend/lowering/builtins/dates.ts index 023d9abf20..fa880bdc95 100644 --- a/packages/compiler/src/frontend/lowering/builtins/dates.ts +++ b/packages/compiler/src/frontend/lowering/builtins/dates.ts @@ -1,7 +1,17 @@ import * as ts from "../../ts7/adapter.js"; import { type Lowerer, own } from "../lowerer.js"; import { locOf } from "../../program.js"; -import { F64, type IrExpr, type IrLibFn, STRING } from "../../../ir/ir.js"; +import { + BOOL, + F64, + type IrExpr, + type IrLibFn, + type IrStmt, + type IrType, + NULL_T, + STRING, + typeKey, +} from "../../../ir/ir.js"; const DATE_GETTER_FNS: Readonly> = { getFullYear: "date.getFullYear", @@ -24,7 +34,7 @@ const DATE_GETTER_FNS: Readonly> = { }; const DATE_METHOD_HINT = - "getTime(), valueOf(), toISOString(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering"; + "getTime(), valueOf(), toISOString(), 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; @@ -80,6 +90,7 @@ export function lowerDateCall( if (lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind !== "date") return null; if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; + if (name === "toJSON") return lowerDateToJson(lowerer, call, access); if (call.arguments.length !== 0) { lowerer.noLowering(`Date.prototype.${name} with arguments`, call, DATE_METHOD_HINT); } @@ -113,3 +124,77 @@ export function lowerDateCall( lowerer.checker.getSymbolAtLocation(access.name), ); } + +/** `d.toJSON(key?)`: Node answers null for an invalid date and the + * `toISOString()` string otherwise, as `string | null` (the lib types say + * `string`). The argument is evaluated and ignored, as the built-in does. */ +function lowerDateToJson( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, +): IrExpr { + const loc = locOf(call); + if (call.arguments.length > 1 || call.arguments.some((a) => ts.isSpreadElement(a))) + lowerer.noLowering(`Date.prototype.toJSON with ${call.arguments.length} arguments`, call); + const raw = lowerer.lowerExpr(access.expression); + const receiver: IrExpr = + raw.type.kind === "dyn" + ? { kind: "libCall", fn: "date.checkedValue", args: [raw], type: { kind: "date" }, loc } + : raw; + if (receiver.type.kind !== "date") + lowerer.badType(access.expression, lowerer.typeOf(access.expression)); + const date = lowerer.declareHiddenLocal("%date", receiver.type); + const stmts: IrStmt[] = [{ kind: "varDecl", localId: date.id, init: receiver, loc }]; + const key = call.arguments[0]; + if (key) stmts.push({ kind: "exprStmt", expr: lowerer.lowerExpr(key), loc }); + const value: IrExpr = { kind: "varRef", localId: date.id, type: receiver.type, loc }; + const time: IrExpr = { kind: "libCall", fn: "date.getTime", args: [value], type: F64, loc }; + const type = dateJsonType(lowerer); + return { + kind: "seqExpr", + stmts, + result: { + kind: "ternary", + // An invalid date's time value is NaN, the one value unequal to itself. + cond: { kind: "bin", op: "!==", left: time, right: time, type: BOOL, loc }, + then: lowerer.coerceToExpected({ kind: "unitLit", unit: "null", type: NULL_T, loc }, type), + else_: lowerer.coerceToExpected( + { kind: "libCall", fn: "date.toISOStringValue", args: [value], type: STRING, loc }, + type, + ), + type, + loc, + }, + type, + loc, + }; +} + +/** `string | null`, the type of a Date's `toJSON()` result. */ +export function dateJsonType(lowerer: Lowerer): IrType { + const arms = [STRING, NULL_T].sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1)); + return { kind: "union", unionId: lowerer.unions.intern(arms) }; +} + +/** A Date `toJSON()` call, or a binding initialized by one + * (Lowerer.dateJsonBindings): its value can be null where the lib types + * say string. */ +export function isDateJsonSource(lowerer: Lowerer, node: ts.Node): boolean { + while (ts.isParenthesizedExpression(node)) node = node.expression; + if (ts.isIdentifier(node)) { + const symbol = + node.parent && ts.isShorthandPropertyAssignment(node.parent) && node.parent.name === node + ? lowerer.checker.getShorthandAssignmentValueSymbol(node.parent) + : lowerer.checker.getSymbolAtLocation(node); + return symbol !== undefined && lowerer.dateJsonBindings.has(symbol); + } + if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression)) return false; + const access = node.expression; + return ( + access.name.text === "toJSON" && + !node.questionDotToken && + !access.questionDotToken && + lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind === "date" && + lowerer.isStdlibMember(access) + ); +} diff --git a/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts b/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts new file mode 100644 index 0000000000..469a66acee --- /dev/null +++ b/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts @@ -0,0 +1,136 @@ +import * as ts from "../../ts7/adapter.js"; +import { type Lowerer } from "../lowerer.js"; +import { locOf } from "../../program.js"; +import { newFnCtx } from "../function-context.js"; +import { BOOL, DYN, VOID, type IrExpr, type IrType } from "../../../ir/ir.js"; +import { varRef } from "../../../ir/build.js"; + +/** Whether `node`'s static type is the standard library's DisposableStack. */ +function isStdlibDisposableStack(lowerer: Lowerer, node: ts.Expression): boolean { + const type = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(node)); + for (const symbol of [type.getSymbol(), type.getAliasSymbol()]) { + if (symbol?.name === "DisposableStack" && lowerer.isStdlibSymbol(symbol)) return true; + } + return false; +} + +/** `new DisposableStack()`: a native stack handle (scr_symbol.c). Its + * members are function values and getters of the handle, reached through + * the checked-dynamic method and property paths. Null for other + * constructors. */ +export function lowerDisposableStackNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null { + if (!ts.isIdentifier(expr.expression) || expr.expression.text !== "DisposableStack") return null; + const symbol = lowerer.resolveValueSymbol(expr.expression); + if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null; + if ((expr.arguments ?? []).length !== 0) return null; + return { kind: "libCall", fn: "disposable.stackNew", args: [], type: DYN, loc: locOf(expr) }; +} + +/** `x instanceof DisposableStack`, or null when the right side is not the + * standard library's DisposableStack. */ +export function lowerDisposableStackInstanceOf( + lowerer: Lowerer, + expr: ts.BinaryExpression, +): IrExpr | null { + if (!lowerer.isStdlibGlobal(expr.right, "DisposableStack")) return null; + const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN); + return { kind: "libCall", fn: "disposable.stackIs", args: [value], type: BOOL, loc: locOf(expr) }; +} + +/** `stack.use(value)` where `value` is a class instance. The dyn view of a + * class instance has no methods, so the stack receives a function that + * calls the class's [Symbol.dispose] method on the value, and the call + * answers the value at its static type. Other values take the + * checked-dynamic method path, which reads the value's [Symbol.dispose]. + * Null when the call is not that form. */ +export function lowerDisposableStackUseCall( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, +): IrExpr | null { + if (call.questionDotToken || access.questionDotToken) return null; + if (access.name.text !== "use" || call.arguments.length !== 1) return null; + const argNode = call.arguments[0]!; + if (ts.isSpreadElement(argNode)) return null; + if (!isStdlibDisposableStack(lowerer, access.expression)) return null; + const valueT = lowerer.mapTypeOf(lowerer.typeOf(argNode)); + if (valueT?.kind === "union") { + const arms = lowerer.unions.get(valueT.unionId)?.arms ?? []; + if (arms.some((arm) => arm.kind === "object")) { + lowerer.unsupported( + "SC1090", + argNode, + "DisposableStack.use on a union with class instances (narrow the value to the class first)", + ); + } + } + if (valueT?.kind !== "object") return null; + const loc = locOf(call); + const className = valueT.className; + const info = lowerer.classes.get(className); + if (!info) lowerer.flushDeferredClass(className); + const found = info ? lowerer.findMethodOn(info, "sym:dispose") : null; + if (!found) return null; + if (found.sig.params.length !== 0) { + lowerer.unsupported("SC1090", argNode, "[Symbol.dispose] methods with parameters"); + } + const stack = lowerer.lowerExpr(access.expression); + const value = lowerer.lowerExpr(argNode); + const key = `disposableStack.use:${className}`; + let helper = lowerer.valueHelpers.get(key); + if (!helper) { + helper = `%disposableStack.use.${lowerer.valueHelpers.size}`; + lowerer.valueHelpers.set(key, helper); + const context = newFnCtx(false, null, null, VOID); + lowerer.fnStack.push(context); + try { + const param = lowerer.declareHiddenLocal("value", valueT); + const self = varRef(param.id, valueT, loc); + lowerer.liftedFns.push({ + name: helper, + params: [{ localId: param.id, name: "value", type: valueT }], + returnType: VOID, + locals: context.locals, + body: [ + { + kind: "exprStmt", + expr: lowerer.accessorCall(className, "sym:dispose", self, [], found.sig.ret, loc), + loc, + }, + ], + loc, + }); + } finally { + lowerer.fnStack.pop(); + } + } + const stackLocal = lowerer.declareHiddenLocal("%disposableStack", DYN); + const valueLocal = lowerer.declareHiddenLocal("%disposableValue", valueT); + const fnT: IrType = { kind: "func", params: [valueT], ret: VOID }; + const dispose: IrExpr = { kind: "closure", fnName: helper, captures: [], type: fnT, loc }; + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: stackLocal.id, init: stack, loc }, + { kind: "varDecl", localId: valueLocal.id, init: value, loc }, + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "disposable.stackUseCallback", + args: [ + varRef(stackLocal.id, DYN, loc), + lowerer.coerceToExpected(varRef(valueLocal.id, valueT, loc), DYN), + lowerer.coerceToExpected(dispose, DYN), + ], + type: BOOL, + loc, + }, + loc, + }, + ], + result: varRef(valueLocal.id, valueT, loc), + type: valueT, + loc, + }; +} diff --git a/packages/compiler/src/frontend/lowering/builtins/errors.ts b/packages/compiler/src/frontend/lowering/builtins/errors.ts index 5947f37148..50a97cead7 100644 --- a/packages/compiler/src/frontend/lowering/builtins/errors.ts +++ b/packages/compiler/src/frontend/lowering/builtins/errors.ts @@ -11,6 +11,28 @@ const ASSERTION_ERROR_PROPERTIES: ReadonlySet = new Set([ "diff", ]); +/** True for child_process.ExecException, exec's callback error. Its + * `code` is the child's numeric exit status (null after a signal), which + * the runtime error's string code slot does not hold, so reads, `in` tests + * and typeof tests of `code` refuse instead of answering from the slot. */ +export function isExecExceptionType(lowerer: Lowerer, t: ts.Type): boolean { + const sym = t.getSymbol(); + return ( + sym?.name === "ExecException" && + lowerer.checker + .declarationsOf(sym) + .some((d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile())) + ); +} + +export function refuseExecExceptionCode(lowerer: Lowerer, node: ts.Node): never { + return lowerer.noLowering( + "ExecException.code", + node, + "spawn reports the exit status through its 'close' event", + ); +} + /** `.code` and `.cause` on an error-hierarchy receiver. The latter reads * the hidden dyn slot installed by constructor options. The former is NodeJS.ErrnoException's * member (the fallback declares the same shape): the runtime Error's @@ -122,6 +144,9 @@ export function lowerErrorCodeProperty( !(recvT.className === "%Error" && lowerer.typeOf(expr.expression).isIntersectionType()) ) return null; + if (expr.name.text === "code" && isExecExceptionType(lowerer, lowerer.typeOf(expr.expression))) { + refuseExecExceptionCode(lowerer, expr); + } const rawReceiver = lowerer.lowerExpr(expr.expression); const receiver = rawReceiver.type.kind === "dyn" diff --git a/packages/compiler/src/frontend/lowering/builtins/json.ts b/packages/compiler/src/frontend/lowering/builtins/json.ts index 475cfd51c1..a73c9d5e74 100644 --- a/packages/compiler/src/frontend/lowering/builtins/json.ts +++ b/packages/compiler/src/frontend/lowering/builtins/json.ts @@ -1,3 +1,4 @@ +import { isDateJsonSource } from "./dates.js"; import { dynUndefinedExpr, varRef } from "../../../ir/build.js"; import { InternalCompilerError } from "../../../errors.js"; import * as ts from "../../ts7/adapter.js"; @@ -82,7 +83,12 @@ export function lowerJsonMethodCall( // conversion so a stale capture cannot read an impossible payload. // Optional strings have a serializer that preserves a missing root; // an unchecked array read can still be absent despite its checker type. - if (value.type.kind === "union" && optionalStringTags(lowerer, value.type) === null) { + // A Date's toJSON() result keeps its null: the lib types say string. + if ( + value.type.kind === "union" && + optionalStringTags(lowerer, value.type) === null && + !isDateJsonSource(lowerer, argNode) + ) { const narrowed = lowerer.mapTypeOf(lowerer.typeOf(argNode)); if (narrowed && !isUnitType(narrowed) && narrowed.kind !== "void") { const helper = diff --git a/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts b/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts index 3fa6eb0c9d..9c3a8c0cfc 100644 --- a/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts +++ b/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts @@ -34,21 +34,48 @@ export function builtinImportOf( // binding (`const { readFileSync } = require("fs")`, renames via // `{ readFileSync: rf }`) keys the same tables. if (ts.isBindingElement(decl) && decl.name !== undefined && ts.isIdentifier(decl.name)) { - const varDecl = decl.parent?.parent; + // A NESTED pattern names a member of a sub-module (`const { types: { + // isPromise } } = require("util")`): util.types IS the util/types + // module object, so each level composes the module name. + const path: string[] = []; + let pattern: ts.Node | undefined = decl.parent; + let varDecl = pattern?.parent; + while ( + pattern !== undefined && + ts.isObjectBindingPattern(pattern) && + varDecl !== undefined && + ts.isBindingElement(varDecl) + ) { + const key = varDecl.propertyName; + if (varDecl.initializer !== undefined || key === undefined || !ts.isIdentifier(key)) { + return null; + } + path.unshift(key.text); + pattern = varDecl.parent; + varDecl = pattern?.parent; + } if ( - ts.isObjectBindingPattern(decl.parent) && + pattern !== undefined && + ts.isObjectBindingPattern(pattern) && + varDecl !== undefined && ts.isVariableDeclaration(varDecl) && varDecl.initializer !== undefined ) { const spec = requireSpecOf(varDecl.initializer); - const module = + let module = spec !== null ? canonicalBuiltinModule(spec) - : // The one-hop alias twin: `const crypto = require('crypto'); - // const { createSign } = crypto;` — test/common's idiom. The - // destructure re-binds module members under local names, so the - // bindings key the same tables as the direct-require form. - builtinNamespaceDestructureModuleOf(lowerer, varDecl); + : path.length > 0 + ? null + : // The one-hop alias twin: `const crypto = require('crypto'); + // const { createSign } = crypto;` — test/common's idiom. The + // destructure re-binds module members under local names, so the + // bindings key the same tables as the direct-require form. + builtinNamespaceDestructureModuleOf(lowerer, varDecl); + for (const key of path) { + if (module === null) break; + module = canonicalBuiltinModule(`${module}/${key}`); + } if (module === null) return null; const member = decl.propertyName && ts.isIdentifier(decl.propertyName) @@ -114,6 +141,24 @@ export function builtinImportOf( return null; } +/** The ES module binding kind of `ident`: "namespace" for a namespace + * import (`import * as events from "node:events"`), "named" for a named + * import (`import { defaultMaxListeners } from "node:events"`, also through + * a re-exporting module), null otherwise. Namespace members and named + * imports of a builtin read its ES module namespace, which holds copies of + * the module's exports (module.syncBuiltinESMExports refreshes them), where + * default imports and CommonJS bindings read the module object itself. */ +export function esmImportBindingKind( + lowerer: Lowerer, + ident: ts.Identifier, +): "namespace" | "named" | null { + const symbol = lowerer.checker.getSymbolAtLocation(ident); + const decl = symbol ? lowerer.checker.declarationsOf(symbol)[0] : undefined; + if (decl === undefined) return null; + if (ts.isNamespaceImport(decl)) return "namespace"; + return ts.isImportSpecifier(decl) ? "named" : null; +} + /** True for `const = require("").` — the * member-binding require import builtinImportOf resolves. Both * declaration walks (lowerVarDecl, collectGlobals) skip these: like a diff --git a/packages/compiler/src/frontend/lowering/builtins/module-calls.ts b/packages/compiler/src/frontend/lowering/builtins/module-calls.ts index dc141045f7..58ac85abab 100644 --- a/packages/compiler/src/frontend/lowering/builtins/module-calls.ts +++ b/packages/compiler/src/frontend/lowering/builtins/module-calls.ts @@ -40,6 +40,7 @@ import { lowerForkCall } from "./child-process.js"; import { lowerOsCpusCall, lowerOsGetPriorityCall, + lowerOsSetPriorityCall, lowerOsNetworkInterfacesCall, lowerOsUserInfoCall, lowerPathFormatCall, @@ -163,6 +164,9 @@ export function lowerBuiltinModuleCall( if (bi.module === "os" && bi.member === "cpus") { return lowerOsCpusCall(lowerer, expr, loc); } + if (bi.module === "os" && bi.member === "setPriority") { + return lowerOsSetPriorityCall(lowerer, expr, loc); + } if (bi.module === "os" && bi.member === "getPriority") { return lowerOsGetPriorityCall(lowerer, expr, loc); } diff --git a/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts b/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts index f1b36e3c63..5fcf355418 100644 --- a/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts +++ b/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts @@ -42,9 +42,10 @@ import { stripTypeCasts } from "./arguments.js"; /** node:module's two compiler-only calls. isBuiltin compares one evaluated * string against the pinned Node 24 list (bare builtins also accept their * node: spelling; prefix-only entries do not gain a bare alias). - * syncBuiltinESMExports is observably a no-op inside the static surface: - * builtin exports cannot be mutated, so its only supported behavior is - * evaluating no arguments and returning undefined. */ + * syncBuiltinESMExports takes no arguments and returns undefined; as a + * statement it refreshes the events module's ES module namespace copy of + * defaultMaxListeners (lowerExprStatement), the one builtin export a program + * can change. */ export function lowerNodeModuleCall( lowerer: Lowerer, expr: ts.CallExpression, @@ -64,7 +65,7 @@ export function lowerNodeModuleCall( lowerer.noLowering( "module.syncBuiltinESMExports used as a value", expr, - "call syncBuiltinESMExports() as its own statement; the supported static effect is a no-op", + "call syncBuiltinESMExports() as its own statement", ); } return { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }; diff --git a/packages/compiler/src/frontend/lowering/builtins/operating-system.ts b/packages/compiler/src/frontend/lowering/builtins/operating-system.ts index 051981f092..b88b97eb69 100644 --- a/packages/compiler/src/frontend/lowering/builtins/operating-system.ts +++ b/packages/compiler/src/frontend/lowering/builtins/operating-system.ts @@ -10,6 +10,7 @@ import { STRING, type SrcLoc, UNDEFINED_T, + VOID, arrayOf, typeKey, } from "../../../ir/ir.js"; @@ -511,6 +512,29 @@ export function lowerPathFormatCall( return { kind: "call", callee: helper, args: [value], type: STRING, loc }; } +/** `os.setPriority([pid, ]priority)` — the one-argument form is pid 0 + * (Node's `if (priority === undefined) { priority = pid; pid = 0; }`); + * the runtime validates both. */ +export function lowerOsSetPriorityCall( + lowerer: Lowerer, + call: ts.CallExpression, + loc: SrcLoc, +): IrExpr { + const args = call.arguments; + if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement)) { + lowerer.noLowering( + "setPriority with these arguments", + call, + "setPriority(priority) or setPriority(pid, priority) takes numbers", + ); + } + const [pid, priority] = + args.length === 1 + ? [numLit(0, loc), lowerer.lowerExprExpecting(args[0]!, F64)] + : [lowerer.lowerExprExpecting(args[0]!, F64), lowerer.lowerExprExpecting(args[1]!, F64)]; + return { kind: "libCall", fn: "os.setPriority", args: [pid, priority], type: VOID, loc }; +} + /** `os.getPriority(pid?)` — an omitted or undefined pid is 0 (Node's * `if (pid === undefined) pid = 0`); the runtime validates the rest. */ export function lowerOsGetPriorityCall( diff --git a/packages/compiler/src/frontend/lowering/builtins/process.ts b/packages/compiler/src/frontend/lowering/builtins/process.ts index 2aa1a47ce1..97bf1e9637 100644 --- a/packages/compiler/src/frontend/lowering/builtins/process.ts +++ b/packages/compiler/src/frontend/lowering/builtins/process.ts @@ -362,10 +362,10 @@ export function lowerProcessProperty( if (member === "connected") { return { kind: "libCall", fn: "process.connected", args: [], type: BOOL, loc }; } - // process.execArgv: the extra CLI arguments Node itself consumed — a - // compiled binary consumed none, so the honest answer is a fresh []. + // process.execArgv: the Node runtime flags kept out of process.argv — + // a leading --test-update-snapshots in node:test programs, else []. if (member === "execArgv") { - return { kind: "arrayLit", elems: [], type: arrayOf(STRING), loc }; + return { kind: "libCall", fn: "process.execArgv", args: [], type: arrayOf(STRING), loc }; } // process._exiting — the runtime's exit-sequence flag (true while // 'exit' listeners run), Node's own undocumented member. diff --git a/packages/compiler/src/frontend/lowering/builtins/timers.ts b/packages/compiler/src/frontend/lowering/builtins/timers.ts index 33a907abdc..98624dc4a4 100644 --- a/packages/compiler/src/frontend/lowering/builtins/timers.ts +++ b/packages/compiler/src/frontend/lowering/builtins/timers.ts @@ -308,12 +308,15 @@ function lowerSchedulerCall( const promiseVoid: IrType = { kind: "promise", inner: VOID }; if (member === "scheduler.yield") return { kind: "libCall", fn: "tp.setImmediate", args: [], type: promiseVoid, loc }; + // The scheduler.wait libCalls emit timers/promises setTimeout's runtime + // calls; mock.timers mocks scheduler.wait and leaves setTimeout's named + // and namespace imports real. An omitted delay is NaN, Node's undefined. const [delayNode, optionsNode] = expr.arguments; const ms: IrExpr = delayNode ? lowerer.lowerExprExpecting(delayNode, F64) - : { kind: "numLit", value: 1, type: F64, loc }; + : { kind: "numLit", value: NaN, type: F64, loc }; if (optionsNode === undefined) - return { kind: "libCall", fn: "tp.setTimeout", args: [ms], type: promiseVoid, loc }; + return { kind: "libCall", fn: "tp.schedulerWait", args: [ms], type: promiseVoid, loc }; const { ref, signal } = timersPromisesOptions(lowerer, member, optionsNode, loc); const value: IrExpr = { kind: "intrinsic", @@ -324,7 +327,7 @@ function lowerSchedulerCall( }; return { kind: "libCall", - fn: signal === null ? "tp.setTimeoutValue" : "tp.setTimeoutSignal", + fn: signal === null ? "tp.schedulerWaitValue" : "tp.schedulerWaitSignal", args: signal === null ? [ms, value, ref] : [ms, value, ref, signal], type: promiseVoid, loc, diff --git a/packages/compiler/src/frontend/lowering/builtins/url.ts b/packages/compiler/src/frontend/lowering/builtins/url.ts index 6e3beb9c2d..daf857650f 100644 --- a/packages/compiler/src/frontend/lowering/builtins/url.ts +++ b/packages/compiler/src/frontend/lowering/builtins/url.ts @@ -26,6 +26,7 @@ import { } from "../../../ir/ir.js"; import { optionalStringTags } from "./arguments.js"; import { type BuiltinModuleFn } from "../surfaces.js"; +import { lowerDynDispatchMethodCall } from "../lower-calls.js"; /** URL component assignment captures its receiver and RHS once, and yields * the original RHS after the setter performs string conversion. */ @@ -255,10 +256,12 @@ export function lowerSearchParamsNew( return { kind: "libCall", fn: "sp.fromPairs", args: [init], type: SEARCH_PARAMS_T, loc }; } if (init.type.kind === "array" && init.type.elem.kind === "record") { + const pairs = searchParamsFromTuples(lowerer, init, init.type.elem, loc); + if (pairs) return pairs; lowerer.unsupported( "SC1090", arg, - "URLSearchParams from tuple-typed pairs (type the pairs as string[][] — the tuple rows have a different layout)", + "URLSearchParams from tuple rows other than [string, string] (type the pairs as [string, string][] or string[][])", ); } lowerer.unsupported( @@ -268,6 +271,97 @@ export function lowerSearchParamsNew( ); } +/** `new URLSearchParams(pairs)` over a `[string, string][]` (an + * Object.entries result, for example): a helper per tuple shape appends + * each pair's name and value in array order. Null for other row types. */ +function searchParamsFromTuples( + lowerer: Lowerer, + init: IrExpr, + row: IrType & { kind: "record" }, + loc: SrcLoc, +): IrExpr | null { + const shape = lowerer.shapes.get(row.shapeId); + if ( + !shape?.tuple || + shape.fields.length !== 2 || + !shape.fields.every( + (field) => (field.name === "0" || field.name === "1") && field.type.kind === "string", + ) + ) + return null; + const key = `sp.fromTuples:${row.shapeId}`; + let helper = lowerer.valueHelpers.get(key); + if (!helper) { + helper = `%sp.fromTuples.${lowerer.valueHelpers.size}`; + lowerer.valueHelpers.set(key, helper); + const pairsT = init.type; + const sp = (): IrExpr => varRef("sp.0", SEARCH_PARAMS_T, loc); + const pair = (): IrExpr => varRef("pair.0", row, loc); + const field = (name: "0" | "1"): IrExpr => ({ + kind: "recordGet", + obj: pair(), + shapeId: row.shapeId, + field: name, + type: STRING, + loc, + }); + const body: IrStmt[] = [ + { + kind: "varDecl", + localId: "sp.0", + init: { kind: "libCall", fn: "sp.new", args: [], type: SEARCH_PARAMS_T, loc }, + loc, + }, + countedFor( + loc, + { + kind: "arrIntrinsic", + method: "length", + receiver: varRef("pairs.0", pairsT, loc), + args: [], + type: F64, + loc, + }, + (index) => [ + { + kind: "varDecl", + localId: "pair.0", + init: { kind: "arrayGet", arr: varRef("pairs.0", pairsT, loc), index, type: row, loc }, + loc, + }, + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "sp.append", + args: [sp(), field("0"), field("1")], + type: VOID, + loc, + }, + loc, + }, + ], + ), + { kind: "return", value: sp(), loc }, + ]; + const fn: IrFunction = { + name: helper, + params: [{ localId: "pairs.0", name: "pairs", type: pairsT }], + returnType: SEARCH_PARAMS_T, + locals: [ + { id: "pairs.0", name: "pairs", type: pairsT, mutable: false }, + { id: "sp.0", name: "sp", type: SEARCH_PARAMS_T, mutable: false }, + { id: "i.0", name: "i", type: F64, mutable: true }, + { id: "pair.0", name: "pair", type: row, mutable: false }, + ], + body, + loc, + }; + lowerer.liftedFns.push(fn); + } + return { kind: "call", callee: helper, args: [init], type: SEARCH_PARAMS_T, loc }; +} + function lowerOptionalStringSearchParams( lowerer: Lowerer, init: IrExpr, @@ -534,12 +628,14 @@ export function lowerSearchParamsMethodCall( if (name === "forEach" && args.length === 1) { return lowerSpForEachCall(lowerer, call, access); } - if (name === "keys" || name === "values" || name === "entries") { - lowerer.unsupported( - "SC1090", - call, - `URLSearchParams iterator objects outside a for-of head (write \`for (const x of sp.${name}())\` directly)`, - ); + if ((name === "keys" || name === "values" || name === "entries") && args.length === 0) { + // Outside a for-of head the iterator is a live object: the checked- + // dynamic view's iterator walks the pair list by index + // (scr_native_sp_step), so entries appended after it was created are + // visited, as in Node. + const boxed = lowerer.coerceInto(access.expression, lowerer.lowerExpr(access.expression), DYN); + const iterator = lowerDynDispatchMethodCall(lowerer, call, access, boxed, false); + if (iterator) return iterator; } lowerer.noLowering( `URLSearchParams.${name}`, @@ -751,3 +847,21 @@ export function lowerLegacyUrlCall( } return { kind: "libCall", fn: fn.fn, args, type: fn.result, loc }; } + +/** A URLSearchParams value, or a keys()/values()/entries() call on one: an + * iterable whose checked-dynamic view walks the live pair list. */ +export function isSearchParamsIterable(lowerer: Lowerer, node: ts.Expression): boolean { + while (ts.isParenthesizedExpression(node)) node = node.expression; + if (lowerer.mapTypeOf(lowerer.typeOf(node))?.kind === "searchParams") return true; + if ( + !ts.isCallExpression(node) || + node.arguments.length !== 0 || + !ts.isPropertyAccessExpression(node.expression) + ) + return false; + const name = node.expression.name.text; + return ( + (name === "keys" || name === "values" || name === "entries") && + lowerer.mapTypeOf(lowerer.typeOf(node.expression.expression))?.kind === "searchParams" + ); +} diff --git a/packages/compiler/src/frontend/lowering/builtins/util-types.ts b/packages/compiler/src/frontend/lowering/builtins/util-types.ts index 979a996d8e..4399c730e7 100644 --- a/packages/compiler/src/frontend/lowering/builtins/util-types.ts +++ b/packages/compiler/src/frontend/lowering/builtins/util-types.ts @@ -12,6 +12,22 @@ export function lowerUtilTypeCall( ): IrExpr { if (expr.arguments.some(ts.isSpreadElement)) lowerer.noLowering(`${name} with spread arguments`, expr); + const first = expr.arguments[0]; + if (first !== undefined && lowerer.mapTypeOf(lowerer.typeOf(first))?.kind === "date") { + // A Date value has no checked-dynamic form; its static type decides + // every brand. The arguments still evaluate in order. + return { + kind: "seqExpr", + stmts: expr.arguments.map((arg): IrStmt => ({ + kind: "exprStmt", + expr: lowerer.lowerExpr(arg), + loc, + })), + result: { kind: "boolLit", value: bi.member === "isDate", type: BOOL, loc }, + type: BOOL, + loc, + }; + } const input = expr.arguments[0] ? lowerer.lowerExprExpecting(expr.arguments[0], DYN) : dynUndefinedExpr(loc); diff --git a/packages/compiler/src/frontend/lowering/builtins/workers.ts b/packages/compiler/src/frontend/lowering/builtins/workers.ts index 3144f208f1..2311e0f965 100644 --- a/packages/compiler/src/frontend/lowering/builtins/workers.ts +++ b/packages/compiler/src/frontend/lowering/builtins/workers.ts @@ -9,6 +9,7 @@ import { F64, STRING, UNDEFINED_T, + VOID, isFfiCallbackParam, type IrExpr, type SrcLoc, @@ -27,6 +28,10 @@ export function lowerWorkerMetadata(member: string, loc: SrcLoc): IrExpr | null return { kind: "libCall", fn: "worker.parentPort", args: [], type: DYN, loc }; case "isInternalThread": return { kind: "boolLit", value: false, type: BOOL, loc }; + case "resourceLimits": + return { kind: "libCall", fn: "worker.resourceLimits", args: [], type: DYN, loc }; + case "threadName": + return { kind: "libCall", fn: "worker.threadName", args: [], type: DYN, loc }; default: return null; } @@ -76,6 +81,7 @@ export function lowerWorkerNew(lowerer: Lowerer, expression: ts.NewExpression): "native callbacks in worker executables must run on their owning script thread", ); } + refuseHeapLimits(lowerer, args[1]); lowerer.usesWorkers = true; const loc = locOf(expression); const options: IrExpr = args[1] @@ -86,7 +92,7 @@ export function lowerWorkerNew(lowerer: Lowerer, expression: ts.NewExpression): type: DYN, loc, }; - return { + const create: IrExpr = { kind: "libCall", fn: "worker.new", args: [ @@ -97,6 +103,70 @@ export function lowerWorkerNew(lowerer: Lowerer, expression: ts.NewExpression): type: DYN, loc, }; + if (!capturesStdio(args[1])) return create; + return { + kind: "seqExpr", + stmts: [ + { + kind: "exprStmt", + expr: { kind: "libCall", fn: "stream.workerStdio", args: [], type: VOID, loc }, + loc, + }, + ], + result: create, + type: DYN, + loc, + }; +} + +/** True when the options are not a literal, or a literal whose stdout, + * stderr or stdin member is not the literal false: worker.stdout, + * worker.stderr and worker.stdin then need the stream unit. */ +function capturesStdio(options: ts.Expression | undefined): boolean { + if (!options) return false; + if (!ts.isObjectLiteralExpression(options)) return true; + return options.properties.some((option) => { + if (ts.isSpreadAssignment(option)) return true; + if (!option.name) return false; + const name = propertyNameText(option.name); + if (name !== "stdout" && name !== "stderr" && name !== "stdin") return name === undefined; + return !( + ts.isPropertyAssignment(option) && option.initializer.kind === ts.SyntaxKind.FalseKeyword + ); + }); +} + +const HEAP_LIMITS = new Set([ + "maxOldGenerationSizeMb", + "maxYoungGenerationSizeMb", + "codeRangeSizeMb", +]); + +/** resourceLimits' V8 heap fields, when an options literal spells them + * with numbers (Node ignores other values; the runtime refuses the rest). */ +function refuseHeapLimits(lowerer: Lowerer, options: ts.Expression | undefined): void { + if (!options || !ts.isObjectLiteralExpression(options)) return; + for (const option of options.properties) { + if (!ts.isPropertyAssignment(option) || propertyNameText(option.name) !== "resourceLimits") + continue; + if (!ts.isObjectLiteralExpression(option.initializer)) continue; + for (const limit of option.initializer.properties) { + if (!ts.isPropertyAssignment(limit)) continue; + const name = propertyNameText(limit.name); + if (name === undefined || !HEAP_LIMITS.has(name)) continue; + const type = lowerer.checker.getTypeAtLocation(limit.initializer); + if (!(type.flags & ts.TypeFlags.NumberLike)) continue; + lowerer.noLowering( + `Worker resourceLimits.${name}`, + limit, + "worker threads have no separate V8 heap; resourceLimits supports stackSizeMb", + ); + } + } +} + +function propertyNameText(name: ts.PropertyName): string | undefined { + return ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : undefined; } /** True when `node`'s type is worker_threads' `name` class (also reached @@ -166,3 +236,142 @@ export function lowerReceiveMessageOnPort(lowerer: Lowerer, call: ts.CallExpress loc, }; } + +/** new BroadcastChannel(name): the EventTarget unit's half installs + * first, then the channel opens (the name converts at runtime). */ +export function lowerBroadcastChannelNew( + lowerer: Lowerer, + expression: ts.NewExpression, +): IrExpr | null { + if (!isWorkerThreadsClass(lowerer, expression, "BroadcastChannel")) return null; + const args = expression.arguments ?? []; + if (args.length > 1 || args.some(ts.isSpreadElement)) { + lowerer.noLowering( + `BroadcastChannel constructor with ${args.length} arguments`, + expression, + "use new BroadcastChannel(name)", + ); + } + if (lowerer.targetPlatform === "wasi") { + lowerer.noLowering( + "BroadcastChannel in this target", + expression, + "use a native static executable", + ); + } + lowerer.usesWorkers = true; + const loc = locOf(expression); + return { + kind: "seqExpr", + stmts: [ + { + kind: "exprStmt", + expr: { kind: "libCall", fn: "event.broadcastInstall", args: [], type: VOID, loc }, + loc, + }, + ], + result: { + kind: "libCall", + fn: "worker.broadcastChannel", + args: [ + { + kind: "dynArrLit", + elems: args.map((a) => lowerer.lowerExprExpecting(a, DYN)), + type: DYN, + loc, + }, + ], + type: DYN, + loc, + }, + type: DYN, + loc, + }; +} + +/** setEnvironmentData(key, value?) and getEnvironmentData(key): this + * thread's map, cloned into each Worker it constructs. */ +export function lowerEnvironmentData( + lowerer: Lowerer, + call: ts.CallExpression, + member: "setEnvironmentData" | "getEnvironmentData", +): IrExpr { + const args = call.arguments; + const arity = member === "setEnvironmentData" ? 2 : 1; + if (args.length > arity || args.some(ts.isSpreadElement)) { + lowerer.noLowering(`${member} with ${args.length} arguments`, call); + } + if (lowerer.targetPlatform === "wasi") { + lowerer.noLowering(`${member} in this target`, call, "use a native static executable"); + } + lowerer.usesWorkers = true; + const loc = locOf(call); + const lowered: IrExpr[] = []; + for (let i = 0; i < arity; i++) { + lowered.push( + args[i] + ? lowerer.lowerExprExpecting(args[i]!, DYN) + : { + kind: "dynFrom", + value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }, + type: DYN, + loc, + }, + ); + } + return { + kind: "libCall", + fn: member === "setEnvironmentData" ? "worker.setEnvironmentData" : "worker.getEnvironmentData", + args: lowered, + type: member === "setEnvironmentData" ? VOID : DYN, + loc, + }; +} + +/** markAsUncloneable(value): structuredClone and postMessage then throw + * DataCloneError for the value (scr_json.c). It needs no worker runtime. */ +export function lowerUncloneable(lowerer: Lowerer, call: ts.CallExpression): IrExpr { + const args = call.arguments; + if (args.length > 1 || args.some(ts.isSpreadElement)) { + lowerer.noLowering(`markAsUncloneable with ${args.length} arguments`, call); + } + const loc = locOf(call); + const value: IrExpr = args[0] + ? lowerer.lowerExprExpecting(args[0], DYN) + : { + kind: "dynFrom", + value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }, + type: DYN, + loc, + }; + return { kind: "libCall", fn: "dyn.markUncloneable", args: [value], type: VOID, loc }; +} + +/** markAsUntransferable(value) and isMarkedAsUntransferable(value): marks + * kept by this thread (an ArrayBuffer's on its storage). */ +export function lowerUntransferable( + lowerer: Lowerer, + call: ts.CallExpression, + member: "markAsUntransferable" | "isMarkedAsUntransferable", +): IrExpr { + const args = call.arguments; + if (args.length > 1 || args.some(ts.isSpreadElement)) { + lowerer.noLowering(`${member} with ${args.length} arguments`, call); + } + if (lowerer.targetPlatform === "wasi") { + lowerer.noLowering(`${member} in this target`, call, "use a native static executable"); + } + lowerer.usesWorkers = true; + const loc = locOf(call); + const value: IrExpr = args[0] + ? lowerer.lowerExprExpecting(args[0], DYN) + : { + kind: "dynFrom", + value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }, + type: DYN, + loc, + }; + return member === "markAsUntransferable" + ? { kind: "libCall", fn: "worker.markUntransferable", args: [value], type: VOID, loc } + : { kind: "libCall", fn: "worker.isMarkedUntransferable", args: [value], type: BOOL, loc }; +} diff --git a/packages/compiler/src/frontend/lowering/class-callbacks.ts b/packages/compiler/src/frontend/lowering/class-callbacks.ts index e276796b43..8599388456 100644 --- a/packages/compiler/src/frontend/lowering/class-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/class-callbacks.ts @@ -14,7 +14,7 @@ import { isJsSourceFile, locOf } from "../program.js"; import type { Lowerer } from "./lowerer.js"; import { findMethodOn, type ClassInfo } from "./lower-classes.js"; import { classPropertiesHelper } from "./class-dynamic-dispatch.js"; -import { classPrototypeData } from "./class-prototypes.js"; +import { observedPrototypeMethod } from "./class-prototypes.js"; import { checkedIterableSpread } from "./checked-iterable-spread.js"; import { tryLowerExpression } from "./expressions/try-lower-expression.js"; @@ -46,6 +46,16 @@ export function isClassCallback(lowerer: Lowerer, info: ClassInfo, name: string) ); } +/** The census marked `name` and the receiver's class keeps the method in + * its prototype data, so the property bag answers every read. */ +function observedSlot(lowerer: Lowerer, receiver: IrExpr, name: string, loc: SrcLoc): boolean { + const info = + receiver.type.kind === "object" ? lowerer.classes.get(receiver.type.className) : undefined; + return info + ? observedPrototypeMethod(lowerer, info, name, loc, receiver) + : lowerer.prototypeMethodAccesses.has(name); +} + function callbackBag( lowerer: Lowerer, receiver: IrExpr, @@ -53,9 +63,8 @@ function callbackBag( name: string, readOnly = false, ): IrExpr { - const info = - receiver.type.kind === "object" ? lowerer.classes.get(receiver.type.className) : null; - if (info && lowerer.prototypeMethodAccesses.has(name)) classPrototypeData(lowerer, info, loc); + // Builds the prototype data that an observed read falls back to. + observedSlot(lowerer, receiver, name, loc); return { kind: "call", callee: classPropertiesHelper(lowerer, loc, readOnly).name, @@ -119,7 +128,7 @@ export function classCallbackValue( const bag = lowerer.declareHiddenLocal("%callbackBag", DYN); const value = varRef(bag.id, DYN, loc); const key: IrExpr = { kind: "strLit", value: name, type: STRING, loc }; - const result: IrExpr = lowerer.prototypeMethodAccesses.has(name) + const result: IrExpr = observedSlot(lowerer, receiver, name, loc) ? { kind: "dynKeyGet", value, key, type: DYN, loc } : { kind: "ternary", @@ -176,7 +185,8 @@ export function classCallbackCall( type: DYN, loc, }; - const result: IrExpr = lowerer.prototypeMethodAccesses.has(name) + const observed = observedSlot(lowerer, receiver, name, loc); + const result: IrExpr = observed ? call : { kind: "ternary", @@ -209,7 +219,7 @@ export function classCallbackCall( { kind: "varDecl", localId: present.id, - init: lowerer.prototypeMethodAccesses.has(name) + init: observed ? { kind: "boolLit", value: true, type: BOOL, loc } : callbackPresent(value, key, loc), loc, diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index fe95e5b80c..444a9cf237 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -40,6 +40,7 @@ import { import { classPrototypeData, hasClassPrototypeData, + observedPrototypeMethod, reflectClassPrototype, } from "./class-prototypes.js"; import { @@ -697,6 +698,8 @@ export class ClassDynamicDispatch { private readonly bagClasses = new Set(); private readonly bagInitializers = new Map[]>(); private readonly typedPropertyBags = new Map(); + /** Per-class prototype selection for new property bags (bagPrototypeHelper). */ + private readonly bagPrototypeHelpers = new Map(); private readonly readPropertyBags: { helper: IrFunction; initialize: Extract; @@ -755,6 +758,7 @@ export class ClassDynamicDispatch { const seenTypes = new Set(); const rewrite = new Set(); const toPrimitiveSites = new Set(); + const hasKeySites = new Set(); const discover = (type: IrType): void => { const key = typeKey(type); if (seenTypes.has(key)) return; @@ -817,6 +821,9 @@ export class ClassDynamicDispatch { ) rewrite.add(fn); break; + case "dynHasKey": + hasKeySites.add(fn); + break; case "dynKeyGet": case "dynInvoke": rewrite.add(fn); @@ -853,6 +860,8 @@ export class ClassDynamicDispatch { const converting = this.convertingHints(lowerer); const calling = this.calledConversions(lowerer); if (converting.size || calling.size) for (const fn of toPrimitiveSites) rewrite.add(fn); + const prototypeMembers = this.prototypeMemberNames(lowerer); + if (prototypeMembers.size) for (const fn of hasKeySites) rewrite.add(fn); for (const fn of functions) { if ( !fn.name.startsWith("%dyn.class.instanceof:") && @@ -1120,38 +1129,8 @@ export class ClassDynamicDispatch { current.subclasses.some( (child) => lowerer.classCanBeConstructed(child) && needsPrototype(child), ); - const prototypeFor = (current: ClassInfo, receiver: IrExpr, loc: SrcLoc): IrExpr | null => { - const currentReceiver: IrExpr = - receiver.type.kind === "object" && receiver.type.className !== current.def.name - ? { - kind: "downcast", - value: receiver, - type: { kind: "object", className: current.def.name }, - loc, - } - : receiver; - let result = classPrototypeData(lowerer, current, loc, currentReceiver); - for (const child of current.subclasses) { - if (!lowerer.classCanBeConstructed(child)) continue; - const selected = prototypeFor(child, receiver, loc); - if (selected && result) - result = { - kind: "ternary", - cond: { - kind: "instanceOf", - value: receiver, - className: child.def.name, - type: BOOL, - loc, - }, - then: selected, - else_: result, - type: DYN, - loc, - }; - } - return result; - }; + const prototypeFor = (current: ClassInfo, receiver: IrExpr, loc: SrcLoc): IrExpr | null => + this.bagPrototype(lowerer, current, receiver, loc); for (const existingInit of this.bagInitializers.get(className) ?? []) { if (existingInit.value.kind === "dynObjLit" && needsPrototype(info)) { const prototype = prototypeFor(info, existingInit.obj, existingInit.loc); @@ -1293,6 +1272,38 @@ export class ClassDynamicDispatch { fn.body = transformStmtList(fn.body, { stmt: (stmt) => stmt, expr: (expr) => { + if (expr.kind === "dynHasKey" && prototypeMembers.has(expr.key)) { + // A class instance's snapshot can carry its class's prototype + // data, which holds only the methods the program reads through + // the prototype. The computed-key form answers methods and + // accessors from the class itself. + const loc = expr.loc; + const test: IrExpr = { + kind: "libCall", + fn: "dyn.hasKeyComputed", + args: [ + expr.value, + { + kind: "dynFrom", + value: { kind: "strLit", value: expr.key, type: STRING, loc }, + type: DYN, + loc, + }, + ], + type: BOOL, + loc, + }; + changed = true; + return { + kind: "seqExpr", + stmts: [], + result: expr.negated + ? { kind: "unary", op: "!", operand: test, type: BOOL, loc } + : test, + type: BOOL, + loc, + }; + } const called = calling.size ? this.convertMethodCall(lowerer, fn, expr, calling) : null; if (called) { changed = true; @@ -2438,8 +2449,13 @@ export class ClassDynamicDispatch { const value = varRef("p.0", DYN, loc); const { receiver, condition } = this.propertyReceiver(plan, loc); if (isClassCallback(lowerer, info, dispatch.source.method)) { - const prototypeMethod = lowerer.prototypeMethodAccesses.has(dispatch.source.method); - if (prototypeMethod) classPrototypeData(lowerer, info, loc); + const prototypeMethod = observedPrototypeMethod( + lowerer, + info, + dispatch.source.method, + loc, + receiver, + ); const bag = this.readPropertyBagOrEmpty(lowerer, value, loc); dispatch.callback.body.unshift({ kind: "if", @@ -2786,7 +2802,8 @@ export class ClassDynamicDispatch { return dispatch.reflect ? this.reflectPropertyBody(lowerer, { ...dispatch, name }, info, receiver) : this.propertyBody(lowerer, { ...dispatch, name }, info, receiver); - const prototypeMethod = lowerer.prototypeMethodAccesses.has(name); + const prototypeMethod = + !info.fields.has(name) && observedPrototypeMethod(lowerer, info, name, loc, receiver); if (info.fields.has(name) || (dispatch.probe === "in" && !prototypeMethod)) { if (info.fields.has(name) && info.def.tracksOwnFields) { const bag = this.readPropertyBagOrEmpty( @@ -2819,7 +2836,6 @@ export class ClassDynamicDispatch { { kind: "return", value: { kind: "boolLit", value: true, type: BOOL, loc }, loc }, ]; } - if (prototypeMethod) classPrototypeData(lowerer, info, loc, receiver); const bag = this.readPropertyBagOrEmpty( lowerer, lowerer.coerceToExpected(receiver, DYN), @@ -3429,6 +3445,19 @@ export class ClassDynamicDispatch { }; } + /** The method and accessor names of boxed classes whose instances can + * carry their class's prototype data. */ + private prototypeMemberNames(lowerer: Lowerer): Set { + const names = new Set(); + for (const { info } of this.propertyReceivers(lowerer)) { + if (!hasClassPrototypeData(info)) continue; + for (let c: ClassInfo | null = info; c; c = c.base) + for (const name of c.methods.keys()) + names.add(name.startsWith("get:") || name.startsWith("set:") ? name.slice(4) : name); + } + return names; + } + /** The methods some boxed class declares among valueOf and toString. */ private calledConversions(lowerer: Lowerer): Set<"valueOf" | "toString"> { const methods = new Set<"valueOf" | "toString">(); @@ -3865,10 +3894,20 @@ export class ClassDynamicDispatch { obj: receiver, value: transformExpr(initializers[0]!.value, { stmt: (stmt) => stmt, - expr: (expr) => - expr.kind === "dynCheck" && expr.value.kind === "varRef" && expr.value.localId === "p.0" - ? receiver - : expr, + expr: (expr) => { + const checked = (e: IrExpr): boolean => + e.kind === "dynCheck" && e.value.kind === "varRef" && e.value.localId === "p.0"; + // The checked receiver, possibly cast to this class (the prototype + // helper's argument), is the typed parameter itself. + if ( + expr.kind === "downcast" && + checked(expr.value) && + expr.type.kind === "object" && + expr.type.className === type.className + ) + return receiver; + return checked(expr) ? receiver : expr; + }, }), loc, }; @@ -3897,6 +3936,79 @@ export class ClassDynamicDispatch { return helper; } + /** The prototype a new property bag of `current` (or of the subclass the + * receiver is) starts from, or null when `current` has no prototype data. + * A class with constructible subclasses that have prototype data selects + * it through its `%class.bagPrototype` helper; any other class reads its + * own prototype data. */ + private bagPrototype( + lowerer: Lowerer, + current: ClassInfo, + receiver: IrExpr, + loc: SrcLoc, + ): IrExpr | null { + const arg: IrExpr = + receiver.type.kind === "object" && receiver.type.className !== current.def.name + ? { + kind: "downcast", + value: receiver, + type: { kind: "object", className: current.def.name }, + loc, + } + : receiver; + const helper = this.bagPrototypeHelper(lowerer, current, loc); + return helper + ? { kind: "call", callee: helper.name, args: [arg], type: DYN, loc } + : classPrototypeData(lowerer, current, loc, arg); + } + + /** `%class.bagPrototype:(this)`: the prototype data of the most + * derived constructible class `this` is an instance of, tested from the + * last subclass to the first and then the class itself. One helper per + * class, calling its subclasses' helpers, keeps every property-bag branch + * a single call: a whole hierarchy inlined into each branch grows with the + * square of its depth. Null when the class has no prototype data, in which + * case its subclasses are not tested either, or while no constructible + * subclass has any. The body is rebuilt on each use, as classes become + * constructible. */ + private bagPrototypeHelper(lowerer: Lowerer, current: ClassInfo, loc: SrcLoc): IrFunction | null { + const type: IrType = { kind: "object", className: current.def.name }; + const self = varRef("this", type, loc); + const own = classPrototypeData(lowerer, current, loc, self); + if (!own) return null; + const body: IrStmt[] = []; + const children = current.subclasses.filter((child) => lowerer.classCanBeConstructed(child)); + for (const child of children.reverse()) { + const selected = this.bagPrototype(lowerer, child, self, loc); + if (!selected) continue; + body.push({ + kind: "if", + cond: { kind: "instanceOf", value: self, className: child.def.name, type: BOOL, loc }, + then: [{ kind: "return", value: selected, loc }], + else_: null, + loc, + }); + } + let helper = this.bagPrototypeHelpers.get(current.def.name); + if (!helper && body.length === 0) return null; + body.push({ kind: "return", value: own, loc }); + if (!helper) { + helper = { + name: `%class.bagPrototype:${current.def.name}`, + params: [{ localId: "this", name: "this", type }], + returnType: DYN, + locals: [{ id: "this", name: "this", type, mutable: false }], + body, + loc, + }; + this.bagPrototypeHelpers.set(current.def.name, helper); + this.generated.add(helper); + lowerer.liftedFns.push(helper); + } + helper.body = body; + return helper; + } + /** Prototype state may appear in a later reachable body, so read-only bags * settle only after the dispatch worklist closes. A read never stores: an * absent empty bag stays undefined (callers test it), and an absent @@ -4496,8 +4608,7 @@ export class ClassDynamicDispatch { const body: IrStmt[] = []; const symbol = symbolMemberKey(lowerer, info, name, loc); if (symbol || (isClassOwnEnumerableFieldName(name) && !name.startsWith("sym:"))) { - const prototypeMethod = lowerer.prototypeMethodAccesses.has(name); - if (prototypeMethod) classPrototypeData(lowerer, info, loc, receiver); + const prototypeMethod = observedPrototypeMethod(lowerer, info, name, loc, receiver); // Selecting the method must not give the object own storage. const bag = this.readPropertyBag(lowerer, receiver, loc); const key: IrExpr = symbol ?? { kind: "strLit", value: name, type: STRING, loc }; diff --git a/packages/compiler/src/frontend/lowering/class-method-mutations.ts b/packages/compiler/src/frontend/lowering/class-method-mutations.ts index 8e7b1f1525..75abeaf57f 100644 --- a/packages/compiler/src/frontend/lowering/class-method-mutations.ts +++ b/packages/compiler/src/frontend/lowering/class-method-mutations.ts @@ -288,7 +288,12 @@ export function collectClassMethodMutations( ts.isMethodDeclaration(node) || ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node)) && - ts.isComputedPropertyName(node.name) + ts.isComputedPropertyName(node.name) && + // A computed key in an object literal defines a property of the new + // object, which is not a class instance. Literals passed to + // Object.assign or Object.defineProperties are judged with their + // receiver below. + !ts.isObjectLiteralExpression(node.parent) ) { markKey(node.name.expression, node); } diff --git a/packages/compiler/src/frontend/lowering/class-prototypes.ts b/packages/compiler/src/frontend/lowering/class-prototypes.ts index 77f7cb5416..1db709e915 100644 --- a/packages/compiler/src/frontend/lowering/class-prototypes.ts +++ b/packages/compiler/src/frontend/lowering/class-prototypes.ts @@ -229,6 +229,27 @@ export function classPrototypeData( return { kind: "call", callee: info.def.prototypeDataHelper, args: [], type: DYN, loc }; } +/** Whether reads of method `name` on instances of `info` use the observed + * prototype slot: the census marked the name and the class has prototype + * data that holds the method. Classes without prototype data (generic + * instantiations, mixins and their subclasses) and function-local classes + * check for the property and otherwise use the declared method. */ +export function observedPrototypeMethod( + lowerer: Lowerer, + info: ClassInfo, + name: string, + loc: SrcLoc, + receiver?: IrExpr, +): boolean { + if (!lowerer.prototypeMethodAccesses.has(name)) return false; + // Building the data also marks a function-local class whose instances + // need it. That class value's prototype holds only methods the program + // replaced there, so its reads check for the key (own or inherited) and + // otherwise use the declared method. + const data = classPrototypeData(lowerer, info, loc, receiver); + return data !== null && info.localClass === undefined; +} + export function hasClassPrototypeData(info: ClassInfo): boolean { return ( info.callableBase !== undefined || diff --git a/packages/compiler/src/frontend/lowering/construction-escapes.ts b/packages/compiler/src/frontend/lowering/construction-escapes.ts index 484920a1c2..a1626f12d5 100644 --- a/packages/compiler/src/frontend/lowering/construction-escapes.ts +++ b/packages/compiler/src/frontend/lowering/construction-escapes.ts @@ -124,6 +124,15 @@ export function constructionEscape(inputs: ConstructionInputs): ConstructionEsca continue; } if (ts.isExpressionStatement(stmt) && !exposes(stmt.expression, depth)) continue; + // A check that only reads assigned fields and throws or does nothing + // with the receiver (`if (this.size < 0) throw new RangeError()`) + // does not expose the instance; assignments in its branches are not + // definite and still count as exposures. + if ( + (ts.isIfStatement(stmt) || ts.isThrowStatement(stmt) || ts.isBlock(stmt)) && + !exposes(stmt, depth) + ) + continue; return false; } return true; diff --git a/packages/compiler/src/frontend/lowering/containers/array-construction.ts b/packages/compiler/src/frontend/lowering/containers/array-construction.ts index 88c542ec0b..015cb2123f 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-construction.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-construction.ts @@ -1004,6 +1004,10 @@ export function lowerArrayFromCall( if (src.type.kind === "dyn" && args.length === 1) { return lowerCheckedArrayFrom(lowerer, src, loc); } + // Array.from(sp): the checked-dynamic view's live pair iteration. + if (src.type.kind === "searchParams" && args.length === 1) { + return lowerCheckedArrayFrom(lowerer, lowerer.coerceInto(args[0]!, src, DYN), loc); + } // A generator (or an iterator adapted into one) drains in order. if (src.type.kind === "generator" && args.length === 1) { const arrT = lowerer.mapTypeOf(lowerer.typeOf(call)); diff --git a/packages/compiler/src/frontend/lowering/containers/array-from-async.ts b/packages/compiler/src/frontend/lowering/containers/array-from-async.ts index 43e8b49595..db60cdf1ba 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-from-async.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-from-async.ts @@ -17,9 +17,10 @@ import { locOf } from "../../program.js"; import { arrayValueRead, arrayValueStore, arrayValueType } from "../array-values.js"; import { newFnCtx } from "../function-context.js"; import { lowerArrayCallback } from "./callback-arguments.js"; +import { extractIteratorValue, resultRecordOf, type GenType } from "../lower-generators.js"; -/** `Array.fromAsync(source, mapFn?)` over an array, as an async helper - * with the spec's loop: +/** `Array.fromAsync(source, mapFn?)` over an array or an async generator, + * as an async helper with the spec's loop: * * const out = []; let k = 0; * for await (const v of source) out.push(mapFn ? await mapFn(v, k++) : v); @@ -29,9 +30,10 @@ import { lowerArrayCallback } from "./callback-arguments.js"; * awaited (a promise element through its promise, any other value as one * resolved promise), and the iterator result is awaited again, which the * helper spells as the same microtask hops the `for await` array loop - * takes. A throwing or rejecting mapFn, and a rejected element, close the - * iterator before the returned promise rejects. Null when this is not an - * `Array.fromAsync` call. */ + * takes. An async generator is driven directly: each next() result is + * awaited once and its value is used as is. A throwing or rejecting mapFn, + * and a rejected array element, close the iterator before the returned + * promise rejects. Null when this is not an `Array.fromAsync` call. */ export function lowerArrayFromAsyncCall( lowerer: Lowerer, call: ts.CallExpression, @@ -54,23 +56,18 @@ export function lowerArrayFromAsyncCall( lowerer.badType(call, lowerer.typeOf(call)); const outT = resultT.inner; const source = lowerer.lowerExpr(args[0]!); - if (source.type.kind !== "array") { - if (source.type.kind === "generator" && source.type.async) { - // An async generator's completion settles one microtask later here - // than in Node, which would reorder the returned promise. - lowerer.noLowering( - "Array.fromAsync over an async generator", - args[0]!, - "collect its values with for await", - ); - } + let step: SourceStep; + if (source.type.kind === "array") { + step = arraySourceStep(lowerer, args[0]!, source.type.elem); + } else if (source.type.kind === "generator" && source.type.async) { + step = generatorSourceStep(lowerer, args[0]!, source.type); + } else { lowerer.noLowering( `Array.fromAsync over a value of type '${lowerer.checker.typeToString(lowerer.typeOf(args[0]!))}'`, args[0]!, - "an array is the supported source", + "an array or an async generator is the supported source", ); } - const step = arraySourceStep(lowerer, args[0]!, source.type.elem); let mapper: (IrExpr & { type: IrType & { kind: "func" } }) | null = null; if (args[1]) { const callback = lowerArrayCallback(lowerer, args[1], [step.valueT], outT); @@ -207,19 +204,173 @@ function nonThenable(lowerer: Lowerer, type: IrType): boolean { ); } +interface SourceStep { + /** The value each step hands to the mapping function. */ + valueT: IrType; + loop(source: IrExpr, append: (value: IrExpr) => IrStmt[], loc: SrcLoc): IrStmt[]; +} + +/** The async generator loop: each next() result is awaited, `done` ends + * the loop, and the value goes to the mapping function as is (no further + * await, unlike the async-from-sync array form). A mapping function that + * throws or rejects closes the generator by awaiting return(); a failure + * of that close is dropped, so the mapping error rejects the result. A + * rejected next() does not close the generator and rejects the result on + * the turn Node rejects it. */ +function generatorSourceStep(lowerer: Lowerer, node: ts.Expression, genT: GenType): SourceStep { + if (genT.yieldT.kind === "void") { + lowerer.noLowering("Array.fromAsync over an async generator that never yields", node); + } + if (genT.nextT.kind !== "undefinedT" && genT.nextT.kind !== "dyn") { + lowerer.noLowering( + `Array.fromAsync over an async generator whose yields expect .next(value) ('${lowerer.fmt(genT.nextT)}')`, + node, + ); + } + const recT = resultRecordOf(lowerer, genT); + const promiseT: IrType = { kind: "promise", inner: recT }; + const fieldT = lowerer.shapes.get(recT.shapeId)!.fields.find((f) => f.name === "value")!.type; + return { + valueT: genT.yieldT, + loop(source, append, loc) { + const g = lowerer.declareHiddenLocal("%fromAsyncGenerator", genT); + const needsClose = lowerer.declareHiddenLocal("%fromAsyncNeedsClose", BOOL); + needsClose.mutable = true; + const p = lowerer.declareHiddenLocal("%fromAsyncNext", promiseT); + const r = lowerer.declareHiddenLocal("%fromAsyncResult", recT); + r.mutable = true; + const nextError = lowerer.declareHiddenLocal("%fromAsyncNextError", CAUGHT); + const closeError = lowerer.declareHiddenLocal("%fromAsyncCloseError", CAUGHT); + const gRef = varRef(g.id, genT, loc); + const rRef = varRef(r.id, recT, loc); + const read: IrExpr = { + kind: "recordGet", + obj: rRef, + shapeId: recT.shapeId, + field: "value", + type: fieldT, + loc, + }; + const value = extractIteratorValue(lowerer, genT.yieldT, fieldT, read, loc); + if (!value) { + lowerer.noLowering( + `Array.fromAsync over an async generator yielding '${lowerer.fmt(genT.yieldT)}'`, + node, + ); + } + const setClose = (on: boolean): IrStmt => ({ + kind: "assign", + localId: needsClose.id, + value: boolLit(on, loc), + loc, + }); + const loop: IrStmt = { + kind: "while", + cond: boolLit(true, loc), + body: [ + { + kind: "varDecl", + localId: p.id, + init: { kind: "genResume", mode: "next", gen: gRef, arg: null, type: promiseT, loc }, + loc, + }, + { kind: "varDecl", localId: r.id, init: null, loc }, + // A rejected next() rejects the result two reactions later, as + // the rejected array element does. + { + kind: "tryCatch", + tryBody: [ + { + kind: "assign", + localId: r.id, + value: { kind: "awaitExpr", value: varRef(p.id, promiseT, loc), type: recT, loc }, + loc, + }, + ], + catchBody: [hop(loc), hop(loc), { kind: "rethrow", localId: nextError.id, loc }], + catchLocalId: nextError.id, + finallyBody: null, + loc, + }, + { + kind: "if", + cond: { + kind: "recordGet", + obj: rRef, + shapeId: recT.shapeId, + field: "done", + type: BOOL, + loc, + }, + then: [{ kind: "break", loc }], + else_: null, + loc, + }, + setClose(true), + ...append(value!), + setClose(false), + ], + loc, + }; + const stmts: IrStmt[] = [ + { kind: "varDecl", localId: g.id, init: source, loc }, + { kind: "varDecl", localId: needsClose.id, init: boolLit(false, loc), loc }, + { + kind: "tryCatch", + tryBody: [loop], + catchBody: null, + catchLocalId: null, + finallyBody: [ + { + kind: "if", + cond: varRef(needsClose.id, BOOL, loc), + then: [closeGenerator(gRef, promiseT, recT, closeError.id, loc)], + else_: null, + loc, + }, + ], + loc, + }, + ]; + return stmts; + }, + }; +} + +/** Await `gen.return()`, dropping a failure of the close. */ +function closeGenerator( + gen: IrExpr, + promiseT: IrType, + recT: IrType, + errorLocalId: string, + loc: SrcLoc, +): IrStmt { + return { + kind: "tryCatch", + tryBody: [ + { + kind: "exprStmt", + expr: { + kind: "awaitExpr", + value: { kind: "genResume", mode: "return", gen, arg: null, type: promiseT, loc }, + type: recT, + loc, + }, + loc, + }, + ], + catchBody: [], + catchLocalId: errorLocalId, + finallyBody: null, + loc, + }; +} + /** The `for await` array loop (lowerForOf's async-from-sync form): two * hops per element that is not a promise, the element's own await plus * one hop for a promise element, two hops before the final done result, * and one hop to close the iterator when the body completes abruptly. */ -function arraySourceStep( - lowerer: Lowerer, - node: ts.Expression, - elem: IrType, -): { - /** The awaited value each element hands to the mapping function. */ - valueT: IrType; - loop(source: IrExpr, append: (value: IrExpr) => IrStmt[], loc: SrcLoc): IrStmt[]; -} { +function arraySourceStep(lowerer: Lowerer, node: ts.Expression, elem: IrType): SourceStep { const yieldedT = arrayValueType(lowerer, elem); let promiseTag: number | null = null; let valueT = yieldedT; diff --git a/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts b/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts index 0ceb4d6777..cf45a75751 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts @@ -108,6 +108,7 @@ const PURE_MATH = new Set([ "math.exp", "math.expm1", "math.floor", + "math.f16round", "math.fround", "math.imul", "math.log", diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts index b9400cbbd3..7a19927de2 100644 --- a/packages/compiler/src/frontend/lowering/containers/bytes.ts +++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts @@ -151,7 +151,7 @@ function lowerBytesSortCall( /* ── typed arrays / Buffer ─────────────────────────────────────────────── */ /** The typed-array constructors with a runtime representation, by lib - * interface name. Other flavors (Float16Array, BigInt64Array, ...) fall through + * interface name. Other flavors (BigInt64Array, BigUint64Array) fall through * to the generic stdlib-constructor fence. DataView is handled separately. */ const BYTES_CTORS: Record = { Uint8Array: "u8", @@ -161,6 +161,7 @@ const BYTES_CTORS: Record = { Int16Array: "i16", Uint32Array: "u32", Int32Array: "i32", + Float16Array: "f16", Float32Array: "f32", Float64Array: "f64", }; @@ -397,6 +398,7 @@ function lowerArrayBufferView( u8: "arrayBuffer.viewU8", u32: "arrayBuffer.viewU32", i32: "arrayBuffer.viewI32", + f16: "arrayBuffer.viewF16", f32: "arrayBuffer.viewF32", f64: "arrayBuffer.viewF64", dv: "arrayBuffer.viewDV", @@ -1220,6 +1222,7 @@ const DV_GETTERS: Record { + if (ts.isCallExpression(node) && requireSpecOf(node) !== null) calls.push(node); + return undefined; + }); + const created = new Set(); + for (const call of calls) { + const jsonSf = jsonRequireTarget(lowerer, call); + if (jsonSf === null) continue; + try { + let mod = lowerer.jsonRequireModules.get(jsonSf.fileName); + if (mod === undefined) { + const tsType = lowerer.typeOf(call); + const mapped = lowerer.mapTypeOf(tsType); + if (!mapped || !lowerer.comptimeBakeable(mapped)) lowerer.badType(call, tsType); + let parsed: unknown; + try { + parsed = JSON.parse(jsonSf.text); + } catch (e) { + lowerer.pushDiag( + invalidJsonModuleDiag( + jsonSf.fileName, + e instanceof Error ? e.message : String(e), + locOf(call), + ), + ); + throw new PoisonError(); + } + const id = `%g.jsonRequire.${lowerer.jsonRequireModules.size}`; + const global: IrGlobal = { id, name: "%json", type: mapped, mutable: true }; + const loadedId = `${id}%loaded`; + lowerer.globalsList.push(global, { + id: loadedId, + name: "%loaded", + type: BOOL, + mutable: true, + }); + mod = { global, loadedId, parsed, type: mapped }; + lowerer.jsonRequireModules.set(jsonSf.fileName, mod); + } + lowerer.jsonRequireCalls.set(call, mod); + if (created.has(mod)) continue; + created.add(mod); + const loc = locOf(call); + const value = lowerer.comptimeValueToIr(mod.parsed, mod.type, "$", call); + const actions = lowerer.jsonInitActions.get(sf) ?? []; + lowerer.jsonInitActions.set(sf, actions); + actions.push({ + kind: "if", + cond: { + kind: "unary", + op: "!", + operand: { kind: "varRef", localId: mod.loadedId, type: BOOL, loc }, + type: BOOL, + loc, + }, + then: [ + { + kind: "assign", + localId: mod.loadedId, + value: { kind: "boolLit", value: true, type: BOOL, loc }, + loc, + }, + { kind: "assign", localId: mod.global.id, value, loc }, + ], + else_: null, + loc, + }); + } catch (e) { + if (!(e instanceof PoisonError)) throw e; + // diagnostic already recorded + } + } + } +} + +/** A require() call of a JSON document: the shared object. */ +export function lowerJsonRequireCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null { + const mod = lowerer.jsonRequireCalls.get(call); + if (mod === undefined) return null; + return { kind: "varRef", localId: mod.global.id, type: mod.global.type, loc: locOf(call) }; +} diff --git a/packages/compiler/src/frontend/lowering/lower-assert.ts b/packages/compiler/src/frontend/lowering/lower-assert.ts index 0bccde3fd7..9d434eae1f 100644 --- a/packages/compiler/src/frontend/lowering/lower-assert.ts +++ b/packages/compiler/src/frontend/lowering/lower-assert.ts @@ -2364,6 +2364,7 @@ const BYTES_BRANDS: ReadonlySet = new Set([ "Int8Array", "Int16Array", "Int32Array", + "Float16Array", "Float32Array", "Float64Array", "BigInt64Array", diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 2ba83a3d58..b3094d5c7c 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -13,6 +13,7 @@ import { lowerTimersMemberCall, TIMER_MODULE_MEMBERS, } from "./lower-timers.js"; +import { lowerMockTimersElementCall } from "./lower-test.js"; import { objectEnumerationReceiver } from "./object-enumeration-receiver.js"; import { InternalCompilerError } from "../../errors.js"; import { isFsStreamMember, lowerFsStreamMethodCall } from "./builtins/fs-streams.js"; @@ -58,6 +59,7 @@ import { } from "../../ir/ir.js"; import { lowerProtocolMethodCall } from "./class-protocol-dispatch.js"; import { isJsSourceFile, locOf, requireSpecOf } from "../program.js"; +import { lowerJsonRequireCall } from "./json-require.js"; import { lowerAssertInstanceCall } from "./assert-instances.js"; import { isGenericCallableMemberType, typeKey } from "../type-mapper.js"; import { dynFallbackType, jsFuncNameOf, newFnCtx, staticImportNamespaceType } from "./lowerer.js"; @@ -106,6 +108,7 @@ import { } from "./builtins/module-resolution.js"; import { lowerCipherMethodCall, lowerCryptoHashMethodCall } from "./builtins/crypto.js"; import { lowerErrorIsErrorCall } from "./builtins/errors.js"; +import { lowerDisposableStackUseCall } from "./builtins/disposable-stack.js"; import { lowerDirentMethodCall, lowerFileHandleMethodCall, @@ -221,6 +224,10 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { if (object) return object; } + // require() of a JSON document: the shared parsed object. + const json = lowerJsonRequireCall(lowerer, expr); + if (json) return json; + // A call whose chain ROOTS at an ambient-undefined name (`declare // const value: Y | undefined; value?.foo("a")`, `declare function // chain...; chain(o).mapValues(f).value()`, a trap binding's read): @@ -1379,6 +1386,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { lowerer.fenceStaticResponseMember(expr.expression, "call") ?? lowerer.fenceStaticHeadersMember(expr.expression, "call") ?? lowerer.fenceStaticReadableStreamMember(expr.expression, "call") ?? + lowerDisposableStackUseCall(lowerer, expr, expr.expression) ?? lowerer.lowerStdlibMethodCall(expr, expr.expression) ?? // Dyn receivers (JSON.parse-derived `unknown`/`any` values) — @@ -1519,6 +1527,8 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // 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)) { + const timersDispose = lowerMockTimersElementCall(lowerer, expr, expr.expression); + if (timersDispose) return timersDispose; const symbolMethod = lowerClassSymbolMethodCall(lowerer, expr); if (symbolMethod) return symbolMethod; lowerer.fenceUnsupportedFetchConstructorMember(expr.expression); diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 17a4833cb4..8edc78ea07 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -79,11 +79,15 @@ import { isAssertClassNew } from "./assert-instances.js"; import { lowerArrayConstructor } from "./containers/array-construction.js"; import { lowerMapSeedNew, lowerSetSeedNew } from "./containers/collection-methods.js"; import { lowerObjectConstructor } from "./containers/indexed-objects.js"; -import { bufEncoding } from "./containers/bytes.js"; +import { knownBufEncoding } from "./containers/bytes.js"; import { isSafeToRepeat } from "./expressions/evaluation-safety.js"; import { lowerSearchParamsNew } from "./builtins/url.js"; import { lowerTextCodecNew } from "./builtins/text-codecs.js"; -import { lowerMessageChannelNew, lowerWorkerNew } from "./builtins/workers.js"; +import { + lowerBroadcastChannelNew, + lowerMessageChannelNew, + lowerWorkerNew, +} from "./builtins/workers.js"; import { unsupportedPackageValueDiag, unsupportedDiag, @@ -139,7 +143,8 @@ import { lowerObjectFactoryNew } from "./object-factory-new.js"; import { tryLowerExpression } from "./expressions/try-lower-expression.js"; import { lowerInstanceConstructorNew } from "./class-instance-constructor.js"; import { classPrototypeData } from "./class-prototypes.js"; -import { constructionEscape } from "./construction-escapes.js"; +import { constructionEscape, mentionsReceiver } from "./construction-escapes.js"; +import { sourceOverlayAssignsClassFields } from "../source-overlays.js"; import { checkedClassConstruction, checkedClassConstructionPacked } from "./class-construction.js"; import { initializeRuntimeStatics, hasRuntimeStatics } from "./class-runtime-statics.js"; import { reflectedClassStaticMethodValue } from "./class-method-values.js"; @@ -464,6 +469,11 @@ export interface ClassInfo { * unassigned read throws the catchable TypeError instead of yielding * an undefined the declared type cannot hold (SEMANTICS.md). */ deferredInitFields?: Set; + /** Fields (inherited included) that code construction exposes the + * instance to can read before their assignment: the slot is the + * undefined-armed union, allocation writes undefined, and reads yield the + * union value, so an early read sees undefined as in Node. */ + earlyReadFields?: Set; /** True when construction may hand the instance to other code (a method * call, an argument, a callback, or a base constructor that does) — a * subclass's own fields can then be read before their assignment. */ @@ -514,6 +524,13 @@ export interface ClassInfo { * body local AFTER the field initializers run (Node's order: super() → * field initializers → parameter-property assignments → ctor body). */ paramProps?: { name: string; type: IrType; param: ts.ParameterDeclaration }[]; + /** --sources: the class belongs to a package built with + * `useDefineForClassFields: false`, so its field initializers are + * assignments that run AFTER the parameter-property assignments (the + * package's emitted order), and initializer-less fields are no own + * properties until assigned (collection admits only the shapes where + * that is unobservable — checkAssignSemanticsFields). */ + assignFields?: true; /** EFFECTIVE constructor params: the own constructor's, or (constructor * omitted) the base's — `new Derived(...)` is typed by tsc against the * inherited signature, and the synthesized constructor forwards to it @@ -3469,55 +3486,61 @@ export function collectClassShapeInner( const derived = decl.heritageClauses?.some((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword) ?? false; - // Generic instances and mixins share layouts across declarations, so - // they only report whether construction exposes the instance. - const escape: { escapes: boolean; assigned: Set | null } = - familyMode || mixin - ? { escapes: derived, assigned: null } - : constructionEscape({ - ctor, - fields: fieldOrder.flatMap((f) => - paramProps.some((pp) => pp.name === f.name) - ? [] - : [{ name: f.name, initializer: f.initializer }], - ), - paramProps: paramProps.map((pp) => pp.name), - derived, - baseEscapes: - base === null || - callableBase !== undefined || - factoryBaseExpression !== undefined || - base.constructionEscapes === true, - isField: (name) => - fields.has(name) && - !methods.has(`set:${name}`) && - !lowerer.findMethodOn(base, `set:${name}`), - privateMethodBody: (call) => { - const callee = call.expression; - if (!ts.isPropertyAccessExpression(callee)) return null; - const name = callee.name.text; - const declaration = decl.members.find( - (member): member is ts.MethodDeclaration => - ts.isMethodDeclaration(member) && - member.body !== undefined && - (ts.isIdentifier(member.name) || ts.isPrivateIdentifier(member.name)) && - member.name.text === name, - ); - if (!declaration?.body || declaration.asteriskToken) return null; - const modifiers = ts.getModifiers(declaration) ?? []; - if ( - modifiers.some( - (m) => - m.kind === ts.SyntaxKind.StaticKeyword || m.kind === ts.SyntaxKind.AsyncKeyword, - ) + // A generic family is not emitted; its instances and each mixin + // application analyze their own construction with their actual base. + const escape: { escapes: boolean; assigned: Set | null } = familyMode + ? { escapes: derived, assigned: null } + : constructionEscape({ + ctor, + fields: fieldOrder.flatMap((f) => + paramProps.some((pp) => pp.name === f.name) + ? [] + : [{ name: f.name, initializer: f.initializer }], + ), + paramProps: paramProps.map((pp) => pp.name), + derived, + baseEscapes: + base === null || + callableBase !== undefined || + factoryBaseExpression !== undefined || + base.constructionEscapes === true, + isField: (name) => + fields.has(name) && + !methods.has(`set:${name}`) && + !lowerer.findMethodOn(base, `set:${name}`), + privateMethodBody: (call) => { + const callee = call.expression; + if (!ts.isPropertyAccessExpression(callee)) return null; + const name = callee.name.text; + const declaration = decl.members.find( + (member): member is ts.MethodDeclaration => + ts.isMethodDeclaration(member) && + member.body !== undefined && + (ts.isIdentifier(member.name) || ts.isPrivateIdentifier(member.name)) && + member.name.text === name, + ); + if (!declaration?.body || declaration.asteriskToken) return null; + const modifiers = ts.getModifiers(declaration) ?? []; + if ( + modifiers.some( + (m) => + m.kind === ts.SyntaxKind.StaticKeyword || m.kind === ts.SyntaxKind.AsyncKeyword, ) - return null; - const isPrivate = - ts.isPrivateIdentifier(declaration.name) || - modifiers.some((m) => m.kind === ts.SyntaxKind.PrivateKeyword); - return isPrivate ? declaration.body : null; - }, - }); + ) + return null; + const isPrivate = + ts.isPrivateIdentifier(declaration.name) || + modifiers.some((m) => m.kind === ts.SyntaxKind.PrivateKeyword); + return isPrivate ? declaration.body : null; + }, + }); + // --sources: assign-semantics class fields (useDefineForClassFields: + // false in the package's build). A generic family defers to its + // instances, which analyze their own construction. + const assignFields = sourceOverlayAssignsClassFields(decl.getSourceFile().fileName); + if (assignFields && escape.assigned !== null) { + checkAssignSemanticsFields(lowerer, decl, ctor, escape.assigned); + } // The deferred definite-assignment check: a field on the unguarded // list passes only with an unconditional `this.x = ...` at the // constructor's TOP LEVEL — the same standard the JS-class path @@ -3573,18 +3596,30 @@ export function collectClassShapeInner( } } - if (escape.assigned && !inst) { + // Fields assigned after construction can expose the instance (a method + // call, a base constructor that calls an override) can be read while + // Node still holds undefined. Their slots admit undefined, allocation + // writes it, and reads yield it, as in Node. + const earlyReadFields = new Set(base?.earlyReadFields ?? []); + if (escape.assigned && escape.escapes) { 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) + if (declared === undefined || !earlyReadable(lowerer, declared)) continue; + // A declared `T | undefined` field already starts undefined. + if (declared.kind === "union" && lowerer.armTag(declared.unionId, UNDEFINED_T) >= 0) continue; - const armed = lowerer.withUndefinedArm(declared); - if (armed === null || armed.kind !== "union") continue; + const armed = lowerer.runtimeOptionalType(declared); + if (armed.kind !== "union") continue; fields.set(f.name, armed); f.type = armed; - deferredInitFields.add(f.name); + // Primitive and union slots yield the undefined value itself. + // Reference slots keep the deferred-init reads: consumers that + // observe undefined (printing, equality, typeof, nullish defaults) + // see it, and member access on it throws a TypeError, as in Node. + if (declared.kind === "union" || PRIMITIVE_EARLY_READS.has(declared.kind)) + earlyReadFields.add(f.name); + else deferredInitFields.add(f.name); } } @@ -3689,6 +3724,43 @@ export function collectClassShapeInner( (name) => !hasAccessor(name) && !methods.has(name) && !lowerer.findMethodOn(base, name), ) : null; + // Constructor statements that run after the instance may have been + // exposed (a method call, `this` passed along, a base constructor + // that exposes it): a field they create can be read before its + // assignment, while Node holds undefined. + const exposedBefore = new Set(); + { + let exposed = false; + for (const stmt of ctor?.body?.statements ?? []) { + const expression = ts.isExpressionStatement(stmt) ? stmt.expression : null; + if ( + expression && + ts.isCallExpression(expression) && + expression.expression.kind === ts.SyntaxKind.SuperKeyword + ) { + if (base?.constructionEscapes === true || expression.arguments.some(mentionsReceiver)) + exposed = true; + continue; + } + const assigned = + expression && + ts.isBinaryExpression(expression) && + expression.operatorToken.kind === ts.SyntaxKind.EqualsToken && + ts.isPropertyAccessExpression(expression.left) && + expression.left.expression.kind === ts.SyntaxKind.ThisKeyword + ? expression + : null; + if (exposed || (assigned && mentionsReceiver(assigned.right))) exposedBefore.add(stmt); + const plainAssignment = + assigned !== null && + ts.isPropertyAccessExpression(assigned.left) && + !mentionsReceiver(assigned.right) && + !hasAccessor(assigned.left.name.text) && + !methods.has(assigned.left.name.text) && + !lowerer.findMethodOn(base, assigned.left.name.text); + if (!plainAssignment && mentionsReceiver(stmt)) exposed = true; + } + } if (ctor?.body) { for (const stmt of ctor.body.statements) { const lhs = @@ -3759,8 +3831,15 @@ export function collectClassShapeInner( type = inheritedJsFieldType(lowerer, decl, name, type); // super() can invoke a subclass setter or method before its // constructor assignments run. These slots start undefined. - if (base && lowerer.dynConvertible(type) && !sealedPrefix?.created.has(name)) - type = DYN; + const sealed = sealedPrefix?.created.has(name) === true; + if (base && lowerer.dynConvertible(type) && !sealed) type = DYN; + if (exposedBefore.has(stmt) && !sealed && earlyReadable(lowerer, type)) { + const armed = lowerer.runtimeOptionalType(type); + if (armed.kind === "union") { + type = armed; + earlyReadFields.add(name); + } + } fields.set(name, type); fieldOrder.push({ name, type, initializer: undefined }); continue; @@ -4021,7 +4100,9 @@ export function collectClassShapeInner( ? { classDecorators: { nodes: classDecoratorNodes } } : {}), ...(deferredInitFields.size > 0 ? { deferredInitFields } : {}), + ...(earlyReadFields.size > 0 ? { earlyReadFields } : {}), ...(escape.escapes ? { constructionEscapes: true as const } : {}), + ...(assignFields ? { assignFields: true as const } : {}), }; // GENERIC members get their declaring-class backlink now that the // info exists (instance lowering reads it for `this` typing and the @@ -6195,6 +6276,33 @@ function overrideReturnAbi( return declared; } +/** Field kinds whose early reads yield the undefined value (earlyReadFields). */ +const PRIMITIVE_EARLY_READS: ReadonlySet = new Set([ + "f64", + "bool", + "string", + "bigint", +]); + +/** Field types whose slot can admit undefined for reads before the field's + * assignment. Date values are scalars, so their unassigned slot is memory + * safe; checked-dynamic slots already hold undefined. */ +function earlyReadable(lowerer: Lowerer, type: IrType): boolean { + if (isUnitType(type)) return false; + switch (type.kind) { + case "dyn": + case "void": + case "date": + case "generator": + case "caught": + return false; + case "object": + return lowerer.classes.get(type.className)?.def.runtime !== true; + default: + return true; + } +} + /** The nearest declaration of `name` at or above `info` — the method a * receiver of that static class runs when nothing below overrides it. */ export function findMethodOn( @@ -6877,9 +6985,8 @@ function lowerClassCtorInner(lowerer: Lowerer, info: ClassInfo): IrFunction { // Node's base-class order: field initializers run at the start // of construction, the parameter-property assignments open the // constructor body (probed — a field initializer reading a - // parameter property sees undefined). - body.push(...lowerer.fieldInitStmts(info, thisLocal)); - body.push(...paramPropInitStmts(lowerer, info, thisLocal)); + // parameter property sees undefined). Assign semantics swaps them. + body.push(...ownFieldInitStmts(lowerer, info, thisLocal)); if (info.ctor.body) body.push(...lowerer.lowerStmts(info.ctor.body.statements)); } else if (info.ctor.body) { body.push(...lowerer.lowerDerivedCtorBody(info, thisLocal)); @@ -7599,6 +7706,94 @@ export function fieldInitStmts(lowerer: Lowerer, info: ClassInfo, thisLocal: IrL return out; } +/** The class's own construction prologue: field initializers, then the + * parameter-property assignments (define semantics, Node's order for + * TypeScript it runs); a package built with `useDefineForClassFields: + * false` emits the parameter-property assignments first, then its field + * initializers as assignments (`assignFields`). */ +function ownFieldInitStmts(lowerer: Lowerer, info: ClassInfo, thisLocal: IrLocal): IrStmt[] { + return info.assignFields + ? [...paramPropInitStmts(lowerer, info, thisLocal), ...lowerer.fieldInitStmts(info, thisLocal)] + : [...lowerer.fieldInitStmts(info, thisLocal), ...paramPropInitStmts(lowerer, info, thisLocal)]; +} + +/** --sources, assign-semantics classes: an initializer-less instance field + * is no own property until the constructor assigns it, where define + * semantics (this layout) defines it as undefined up front and orders it + * by declaration. The two agree exactly when every such field is assigned + * before construction can expose the instance (`assigned`, the + * construction-escape analysis, which already runs parameter properties + * before initializers), the assignments run in declaration order, and no + * field with an initializer is declared after one without. Other shapes + * keep a named fence (the overlay falls back to the published JS). */ +function checkAssignSemanticsFields( + lowerer: Lowerer, + decl: ts.ClassLikeDeclaration, + ctor: ts.ConstructorDeclaration | null, + assigned: ReadonlySet, +): void { + const why = "(useDefineForClassFields: false, the package's class-field semantics)"; + const pending: string[] = []; + let firstPending: ts.Identifier | null = null; + for (const member of decl.members) { + if (!ts.isPropertyDeclaration(member) || !ts.isIdentifier(member.name)) continue; + const modifiers = ts.getModifiers(member) ?? []; + if ( + modifiers.some( + (m) => + m.kind === ts.SyntaxKind.StaticKeyword || + m.kind === ts.SyntaxKind.DeclareKeyword || + m.kind === ts.SyntaxKind.AbstractKeyword, + ) + ) + continue; + if (member.initializer) { + if (firstPending !== null) { + lowerer.unsupported( + "SC1090", + member.name, + `field initializers declared after an initializer-less field ${why}`, + `'${member.name.text}' is assigned before the constructor assigns '${firstPending.text}', so the instance's property order differs from the declaration order`, + ); + } + continue; + } + if (!assigned.has(member.name.text)) { + lowerer.unsupported( + "SC1090", + member.name, + `initializer-less fields the constructor does not assign before the instance can be observed ${why}`, + `'${member.name.text}' is no own property until it is assigned`, + ); + } + firstPending ??= member.name; + pending.push(member.name.text); + } + if (pending.length < 2) return; + const order: string[] = []; + for (const stmt of ctor?.body?.statements ?? []) { + if ( + ts.isExpressionStatement(stmt) && + ts.isBinaryExpression(stmt.expression) && + stmt.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken && + ts.isPropertyAccessExpression(stmt.expression.left) && + stmt.expression.left.expression.kind === ts.SyntaxKind.ThisKeyword + ) { + const name = stmt.expression.left.name.text; + if (pending.includes(name) && !order.includes(name)) order.push(name); + } + } + const mismatch = pending.findIndex((name, i) => order[i] !== name); + if (mismatch >= 0) { + lowerer.unsupported( + "SC1090", + firstPending!, + `initializer-less fields assigned out of declaration order ${why}`, + "the instance's property order follows the constructor's assignments", + ); + } +} + /** PARAMETER-PROPERTY assignments (`this.x = x`, synthesized): run AFTER * the field initializers — Node's transform defines the fields at the * top of the class body (undefined until assigned) and injects the @@ -7624,7 +7819,11 @@ function paramPropInitStmts(lowerer: Lowerer, info: ClassInfo, thisLocal: IrLoca obj: { kind: "varRef", localId: thisLocal.id, type: thisType, loc }, className: info.def.name, field: pp.name, - value: { kind: "varRef", localId: local!.id, type: local!.type, loc }, + // An early-readable field's slot admits undefined (earlyReadFields). + value: lowerer.coerceToExpected( + { kind: "varRef", localId: local!.id, type: local!.type, loc }, + info.fields.get(pp.name) ?? local!.type, + ), loc, }); } @@ -7771,10 +7970,7 @@ export function lowerDerivedCtorBody( }); delete context.superInitialized; try { - statements.push( - ...lowerer.fieldInitStmts(info, thisLocal), - ...paramPropInitStmts(lowerer, info, thisLocal), - ); + statements.push(...ownFieldInitStmts(lowerer, info, thisLocal)); } finally { context.superInitialized = initialized; } @@ -7857,7 +8053,6 @@ export function superCallStmt( args: IrExpr[], loc: SrcLoc, ): IrStmt { - const base = superBaseOf(info)!; const thisRef: IrExpr = { kind: "varRef", localId: thisLocal.id, @@ -7902,6 +8097,8 @@ export function superCallStmt( }, loc, }; + // A callable base has no class info, so the base is read after that case. + const base = superBaseOf(info)!; if (base.builtinError) { // super(message) into the runtime-provided Error constructor: stamps // name/message on the (already-allocated) object. Receiver + message @@ -8398,7 +8595,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { if (worker) return worker; const readlineUi = lowerReadlineUiNew(lowerer, expr); if (readlineUi) return readlineUi; - const channel = lowerMessageChannelNew(lowerer, expr); + const channel = lowerMessageChannelNew(lowerer, expr) ?? lowerBroadcastChannelNew(lowerer, expr); if (channel) return channel; let intlConstructor = expr.expression; if (ts.isIdentifier(intlConstructor)) { @@ -8890,20 +9087,10 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { }; const pattern = strArg(args[0], "pattern"); const flags = strArg(args[1], "flags argument"); - // Node accepts the 'd' and 'v' flags the engine does not have; a - // literal flags string refuses here like the literal regex form. - if (flags.kind === "strLit" && args[1] !== undefined) { - for (const f of ["d", "v"]) { - if (flags.value.includes(f)) { - lowerer.unsupported( - "SC1120", - args[1], - f === "d" - ? "the regex 'd' flag (match indices)" - : "the regex 'v' flag (unicode sets)", - ); - } - } + // Node accepts the 'd' flag the engine does not have; a literal + // flags string refuses here like the literal regex form. + if (flags.kind === "strLit" && args[1] !== undefined && flags.value.includes("d")) { + lowerer.unsupported("SC1120", args[1], "the regex 'd' flag (match indices)"); } return { kind: "libCall", @@ -8972,24 +9159,41 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { ); } // `new StringDecoder(encoding?)` (node:string_decoder): the decoder - // is a two-field record — the CANONICAL encoding name (aliases fold - // at compile time, exactly what `.encoding` answers in Node) and - // the packed-f64 pending state starting at 0 (nothing buffered). - // The encoding must be a literal (Node's alias set); omitted means - // utf8, Node's default. + // is a two-field record — the CANONICAL encoding name (exactly what + // `.encoding` answers in Node) and the packed-f64 pending state + // starting at 0 (nothing buffered). A literal Buffer encoding folds + // its alias at compile time; any other encoding argument is + // normalized at run time (Node's normalizeEncoding, which throws + // ERR_UNKNOWN_ENCODING for an unknown name). Omitted means utf8, + // Node's default. if (symbol && symbol.name === "StringDecoder" && lowerer.isStdlibSymbol(symbol)) { const args = expr.arguments ?? []; if (args.length > 1) { lowerer.noLowering("new StringDecoder with 2 arguments", expr, undefined, symbol); } - const encName = - args.length === 1 ? bufEncoding(lowerer, "new StringDecoder", args[0]!) : "utf8"; + const encNode = args[0]; + const encT = encNode ? lowerer.typeOf(encNode) : undefined; + const encName = !encNode + ? "utf8" + : encT!.isStringLiteralType() + ? knownBufEncoding(encT!.value) + : undefined; + const encoding: IrExpr = + encName !== undefined + ? { kind: "strLit", value: encName, type: STRING, loc } + : { + kind: "libCall", + fn: "strdec.encoding", + args: [lowerer.lowerExprExpecting(encNode!, DYN)], + type: STRING, + loc, + }; const decT = lowerer.mapTypeOf(lowerer.typeOf(expr)); if (decT?.kind !== "record") lowerer.badType(expr, lowerer.typeOf(expr)); return { kind: "recordLit", fields: [ - { name: "%enc", value: { kind: "strLit", value: encName, type: STRING, loc } }, + { name: "%enc", value: encoding }, { name: "%pending", value: { kind: "numLit", value: 0, type: F64, loc } }, ], type: decT, diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index 08e12518bb..c8874fe34f 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -1,15 +1,19 @@ +import { dateJsonType, isDateJsonSource } from "./builtins/dates.js"; import { guardNullProto, nullProtoRecordShape, recordNullProtoTest } from "./null-prototype.js"; import { lowerProtocolPropertyRead } from "./class-protocol-dispatch.js"; import { generatorDrain } from "./iterator-adapters.js"; import { lowerEventObjectProperty, lowerEventObjectPropertyWrite } from "./lower-event-target.js"; +import { lowerDisposableStackInstanceOf } from "./builtins/disposable-stack.js"; import { lowerEventStaticProperty } from "./builtins/numbers.js"; import { lowerUnionEquality, tagEqualityMayMissAlias } from "./strict-equality.js"; import { isOptionalProcessStreamProperty } from "./builtins/process.js"; import { lowerWorkerMetadata } from "./builtins/workers.js"; +import { esmImportBindingKind } from "./builtins/module-bindings.js"; import { lowerReadlineProperty } from "./builtins/readline.js"; import { lowerBlobInstanceof } from "./builtins/blob.js"; import { lowerKeyObjectInstanceof } from "./builtins/key-object.js"; import { lowerNetAddressInstanceof, lowerNetAddressStaticRead } from "./builtins/net-address.js"; +import { isExecExceptionType, refuseExecExceptionCode } from "./builtins/errors.js"; import { STATS_TYPE_TESTS } from "./builtins/filesystem.js"; import { threadsImportOf } from "../threads-import.js"; import { experimentalImportOf, lifetimeRegionMemberOf } from "../experimental-import.js"; @@ -225,7 +229,7 @@ import { } from "./lower-builtin-values.js"; import { lowerArrayFromValue } from "./containers/array-construction.js"; import { lowerPerfHooksTypeof } from "./builtins/performance.js"; -import { lowerUrlAssignment } from "./builtins/url.js"; +import { isSearchParamsIterable, lowerUrlAssignment } from "./builtins/url.js"; import { jsBindingHasOpenWrites } from "./lower-stmts.js"; import { checkedClassInstanceOf } from "./class-construction.js"; import { lowerModuleNamespaceElement } from "./module-namespace-elements.js"; @@ -646,6 +650,30 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { // have non-stdlib symbols and keep the ordinary path. let tested = expr.expression; while (ts.isParenthesizedExpression(tested)) tested = tested.expression; + // `typeof zlib.Inflate` / `typeof zlib.createGzip`: an export of a + // Node builtin module read through a plain binding (an import or + // require result). The read has no side effects, and Node's answer for + // every exported function and class is "function". + if ( + ts.isPropertyAccessExpression(tested) && + ts.isIdentifier(tested.expression) && + lowerer.isStdlibMember(tested) + ) { + const sym = lowerer.checker.getSymbolAtLocation(tested.name); + const decls = sym ? lowerer.checker.declarationsOf(sym) : []; + if ( + decls.some(isBuiltinModuleValueDecl) && + decls.every((decl) => builtinModuleExportParent(decl) !== undefined) + ) { + const t = lowerer.typeOf(tested); + if ( + lowerer.checker.getCallSignatures(t).length > 0 || + lowerer.checker.getConstructSignatures(t).length > 0 + ) { + return { kind: "strLit", value: "function", type: STRING, loc }; + } + } + } if (ts.isIdentifier(tested)) { const sym = lowerer.checker.getSymbolAtLocation(tested); if (!sym) { @@ -1148,6 +1176,20 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { const value = lowerWorkerMetadata(bi.member, loc); if (value) return value; } + // A named import of defaultMaxListeners reads the ES module + // namespace's copy. A CommonJS destructured or member binding holds + // the value read when its declaration ran, which these bindings do + // not store. + if (bi.module === "events" && bi.member === "defaultMaxListeners") { + if (esmImportBindingKind(lowerer, expr) === "named") { + return { kind: "libCall", fn: "emitter.esmDefaultMax", args: [], type: F64, loc }; + } + lowerer.noLowering( + "events.defaultMaxListeners through a CommonJS destructured or member binding", + expr, + "read require('events').defaultMaxListeners or EventEmitter.defaultMaxListeners where the value is needed", + ); + } const c = builtinModuleConstOf(lowerer, bi.module, bi.member); if (c !== undefined) return builtinConstLit(c, loc); // module.builtinModules — the baked Node v24 list, a fresh @@ -3092,6 +3134,51 @@ export function lowerTernary( * the same union (unnarrowed use), to a SUB-union (partial narrowing — * unrepresentable without a re-tag), or to nothing (`never` in an * exhaustive default) leaves the expression union-typed. */ +/** Uses that read an early-read field's value as Node does when it is + * undefined: printing, templates, operators, tests, member access, calls of + * program functions (whose parameters the optional-value analysis widens), + * and stores into bindings, fields and returns. Other uses, such as library + * arguments, extract the declared type with a check. */ +function observesEarlyRead(node: ts.Node): boolean { + let child = node; + let parent = node.parent; + while (parent && ts.isParenthesizedExpression(parent)) { + child = parent; + parent = parent.parent; + } + if (!parent) return false; + if ( + ts.isTemplateSpan(parent) || + ts.isBinaryExpression(parent) || + ts.isPrefixUnaryExpression(parent) || + ts.isPostfixUnaryExpression(parent) || + ts.isTypeOfExpression(parent) || + ts.isConditionalExpression(parent) || + ts.isIfStatement(parent) || + ts.isWhileStatement(parent) || + ts.isReturnStatement(parent) || + ts.isVariableDeclaration(parent) || + ts.isPropertyAssignment(parent) || + ts.isArrayLiteralExpression(parent) || + (ts.isPropertyAccessExpression(parent) && parent.expression === child) || + (ts.isElementAccessExpression(parent) && parent.expression === child) + ) + return true; + if (ts.isCallExpression(parent) && parent.arguments.includes(child as ts.Expression)) { + const callee = parent.expression; + if ( + ts.isPropertyAccessExpression(callee) && + ts.isIdentifier(callee.expression) && + callee.expression.text === "console" + ) + return true; + return ( + !ts.isPropertyAccessExpression(callee) || callee.expression.kind === ts.SyntaxKind.ThisKeyword + ); + } + return false; +} + export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrExpr { if ( (expr.type.kind === "union" || expr.type.kind === "dyn") && @@ -3302,6 +3389,25 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx if (expr.type.kind !== "union") return expr; if (ts.isPropertyAccessExpression(node) && isOptionalProcessStreamProperty(lowerer, node)) return expr; + // A field read before its assignment holds undefined although its + // declared type omits it; the declared type is no narrowing proof. A flow + // narrowing past the declared type (`if (this.x !== null)`) extracts the + // narrowed arm with a check, since the slot may still hold undefined. + if ( + expr.kind === "fieldGet" && + lowerer.classes.get(expr.className)?.earlyReadFields?.has(expr.field) === true + ) { + const symbol = ts.isPropertyAccessExpression(node) + ? lowerer.checker.getSymbolAtLocation(node.name) + : undefined; + const declared = symbol ? lowerer.mapTypeOf(lowerer.checker.getTypeOfSymbol(symbol)) : null; + const narrowedArm = lowerer.mapTypeOf(lowerer.typeOf(node)); + if (narrowedArm === null || narrowedArm.kind === "union") return expr; + if (declared !== null && typeEquals(declared, narrowedArm) && observesEarlyRead(node)) + return expr; + const helper = lowerer.narrowedArmHelper(expr.type.unionId, narrowedArm, expr.loc); + return helper ? lowerer.checkedNarrowCall(helper, expr, narrowedArm, expr.loc) : expr; + } // Represented fields and index values can include undefined even when // checker inference or lib.d.ts declares strings (RegExp captures). @@ -3311,7 +3417,11 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx (expr.kind === "recordGet" || expr.kind === "recordKeyGet") && (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) ) { - const receiver = lowerer.mapTypeOf(lowerer.typeOf(node.expression)); + // The read itself rules out a null or undefined receiver, which a + // JavaScript receiver such as `m.groups` still declares. + const receiver = lowerer.mapTypeOf( + lowerer.checker.getNonNullableType(lowerer.typeOf(node.expression)), + ); const shape = receiver?.kind === "record" ? lowerer.shapes.get(receiver.shapeId) : undefined; const declared = expr.kind === "recordGet" @@ -3323,10 +3433,23 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx !(declared.kind === "union" && lowerer.armTag(declared.unionId, UNDEFINED_T) >= 0) ) return expr; + // The same for a field holding a Date's toJSON() result, whose null + // the declared string does not represent. + if ( + expr.kind === "recordGet" && + lowerer.isRuntimeOptionalField(expr.shapeId, expr.field) && + lowerer.armTag(expr.type.unionId, NULL_T) >= 0 && + declared && + !(declared.kind === "union" && lowerer.armTag(declared.unionId, NULL_T) >= 0) + ) + return expr; } if (ts.isIdentifier(node)) { const symbol = lowerer.resolveValueSymbol(node); if (symbol && lowerer.unstableSwitchBindings.has(symbol)) return expr; + // A binding initialized by a Date's toJSON() can hold null where the + // checker says string; consumers check it. + if (symbol && lowerer.dateJsonBindings.has(symbol)) return expr; } if ( ts.isPropertyAccessExpression(node) && @@ -5313,6 +5436,27 @@ export function lowerIntrinsicProperty( loc: locOf(expr), }; } + // The flag getters answer whether `flags` holds the flag's letter. + const flag = REGEX_FLAG_GETTERS.get(name); + if (flag !== undefined) { + const loc = locOf(expr); + const flags: IrExpr = { + kind: "regexIntrinsic", + method: "flags", + receiver: strictReceiver({ kind: "regex" }), + args: [], + type: STRING, + loc, + }; + return { + kind: "strIntrinsic", + method: "includes", + receiver: flags, + args: [{ kind: "strLit", value: flag, type: STRING, loc }], + type: BOOL, + loc, + }; + } if (name === "test") { lowerer.unsupported("SC1090", expr, "regex methods as values (call 'test' directly)"); } @@ -5649,6 +5793,15 @@ export function lowerArrayLiteral( } } } + // A Date's toJSON() element keeps its null in a literal with no declared + // destination: the lib types say string. + if ( + !expected && + mapped?.kind === "array" && + mapped.elem.kind === "string" && + expr.elements.some((element) => isDateJsonSource(lowerer, element)) + ) + mapped = { kind: "array", elem: dateJsonType(lowerer) }; // An array literal inside an npm-static implicit-any instance can // inherit the checker's poisoned any[] context even after the // active binding gives every element one concrete lowering. Recover a @@ -5889,6 +6042,11 @@ export function lowerArrayLiteral( const drained = generatorDrain(lowerer, src, type.elem, locOf(el)); if (drained) src = drained; } + // `[...sp]` and `[...sp.entries()]` over a URLSearchParams: its + // checked-dynamic view iterates the live pair list, and the drained + // [name, value] pairs convert to the literal's tuple elements. + const searchParamsSource = isSearchParamsIterable(lowerer, el.expression); + if (src.type.kind === "searchParams") src = lowerer.coerceInto(el.expression, src, DYN); // A native checked iterable can supply scalar elements even when // the checker inferred a typed array from its producer. Drain once // and validate the elements before the ordinary spread copy. @@ -5897,7 +6055,8 @@ export function lowerArrayLiteral( (type.elem.kind === "dyn" || type.elem.kind === "f64" || type.elem.kind === "string" || - type.elem.kind === "bool") + type.elem.kind === "bool" || + searchParamsSource) ) { src = lowerer.coerceInto(el.expression, checkedIteratorPack(lowerer, src, locOf(el)), type); } @@ -10521,7 +10680,14 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr loc, }; if (bothStr) return { kind: "strCmp", op: cmpOp, left, right, type: BOOL, loc }; - if (lowerer.dynConvertible(left.type) && lowerer.dynConvertible(right.type)) { + // A Date converts to a new checked-dynamic Date on each crossing, so + // comparisons keep their Date fence instead of comparing copies. + if ( + left.type.kind !== "date" && + right.type.kind !== "date" && + lowerer.dynConvertible(left.type) && + lowerer.dynConvertible(right.type) + ) { return { kind: "libCall", fn: "dyn.compare", @@ -10771,7 +10937,14 @@ export function lowerStrictEquality( loc, }; } - if (lowerer.dynConvertible(idLeft.type) && lowerer.dynConvertible(idRight.type)) { + // A Date converts to a new checked-dynamic Date on each crossing; its + // identity has no representation, so equality keeps the fence. + if ( + idLeft.type.kind !== "date" && + idRight.type.kind !== "date" && + lowerer.dynConvertible(idLeft.type) && + lowerer.dynConvertible(idRight.type) + ) { return { kind: "dynScalarEq", left: lowerer.coerceToExpected(idLeft, DYN), @@ -11218,6 +11391,7 @@ function lowerErrorCodeTypeofTest( recv = recv.expression; const errT = errorRootedObjectOf(lowerer, lowerer.typeOf(recv)); if (!errT) continue; + if (isExecExceptionType(lowerer, lowerer.typeOf(recv))) refuseExecExceptionCode(lowerer, prop); const caught = lowerer.caughtLocalOf(recv); let receiver: IrExpr; if (caught) { @@ -11972,6 +12146,10 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN); return { kind: "libCall", fn: "dyn.nativeUrlIs", args: [value], type: BOOL, loc }; } + { + const stack = lowerDisposableStackInstanceOf(lowerer, expr); + if (stack) return stack; + } if (lowerer.isStdlibGlobal(expr.right, "DataView")) { const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN); return { kind: "libCall", fn: "dyn.dataViewIs", args: [value], type: BOOL, loc }; @@ -12816,6 +12994,9 @@ function lowerInExpression(lowerer: Lowerer, expr: ts.BinaryExpression, loc: Src loc, }; } + if (key === "code" && isExecExceptionType(lowerer, lowerer.typeOf(expr.right))) { + refuseExecExceptionCode(lowerer, expr); + } let recv = lowerer.lowerExpr(expr.right); const classInfo = recv.type.kind === "classval" ? lowerer.classes.get(recv.type.className) : undefined; @@ -13213,10 +13394,22 @@ function lowerRuntimeKeyIn( return { kind: "call", callee: helper, args: [keyIr, recv], type: BOOL, loc }; } +/** RegExp.prototype's flag getters and the flag each one reads. */ +const REGEX_FLAG_GETTERS: ReadonlyMap = new Map([ + ["hasIndices", "d"], + ["global", "g"], + ["ignoreCase", "i"], + ["multiline", "m"], + ["dotAll", "s"], + ["unicode", "u"], + ["unicodeSets", "v"], + ["sticky", "y"], +]); + /** A regex literal `/ab+c/gi` → regexLit (fresh state per evaluation, * shared bytecode per (pattern, flags) pair). The TS parser has already syntax-checked the literal; - * what remains here is the flag-alphabet fence (d and v are - * declared-valid TS flags outside this slice). Named capture groups + * what remains here is the flag-alphabet fence (d is a + * declared-valid TS flag outside this slice). Named capture groups * `(?...)` and `\k` backreferences compile — libregexp * executes them natively, replace templates resolve `$` at * runtime, and `.groups` reads desugar at their access sites @@ -13227,20 +13420,19 @@ export function lowerRegexLiteral(lowerer: Lowerer, expr: ts.RegularExpressionLi const text = expr.text; const lastSlash = text.lastIndexOf("/"); const pattern = text.slice(1, lastSlash); - const flags = text.slice(lastSlash + 1); - for (const f of flags) { - if (!"gimsuy".includes(f)) { + const written = text.slice(lastSlash + 1); + for (const f of written) { + if (!"gimsuvy".includes(f)) { lowerer.unsupported( "SC1120", expr, - f === "d" - ? "the regex 'd' flag (match indices)" - : f === "v" - ? "the regex 'v' flag (unicode sets)" - : `the regex '${f}' flag`, + f === "d" ? "the regex 'd' flag (match indices)" : `the regex '${f}' flag`, ); } } + // `flags`, `toString()` and inspection list the flags in getter order, + // whatever order the literal spells them in. + const flags = [..."gimsuvy"].filter((f) => written.includes(f)).join(""); return { kind: "regexLit", pattern, flags, type: REGEX, loc: locOf(expr) }; } @@ -13447,6 +13639,15 @@ function matchProducerRegexOf(lowerer: Lowerer, e: ts.Expression): ts.Expression if (ts.isElementAccessExpression(expr)) { return matchAllRegexOf(lowerer, expr.expression); } + // A row picked by `rows.find(cb)`, `rows.findLast(cb)` or `rows.at(i)`. + if ( + ts.isCallExpression(expr) && + ts.isPropertyAccessExpression(expr.expression) && + MATCH_ROW_PICK_METHODS.has(expr.expression.name.text) && + lowerer.isStdlibMember(expr.expression) + ) { + return matchAllRegexOf(lowerer, expr.expression.expression); + } if (ts.isIdentifier(expr)) { // The for-of binding over a matchAll drain: `for (const m of // s.matchAll(re))` (or over a stored const rows). @@ -13471,6 +13672,12 @@ function matchProducerRegexOf(lowerer: Lowerer, e: ts.Expression): ts.Expression return null; } +/** Array methods that return one row of the receiver. */ +const MATCH_ROW_PICK_METHODS = new Set(["find", "findLast", "at"]); + +/** Array methods whose result holds rows of the receiver. */ +const MATCH_ROW_SUBSET_METHODS = new Set(["filter", "slice", "toReversed", "toSorted"]); + const MATCH_ROW_CALLBACK_METHODS = new Set([ "map", "forEach", @@ -13491,6 +13698,11 @@ function matchAllCallbackRegexOf( const fn = param.parent; if (!(ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) || fn.parameters[0] !== param) return null; + // A reassigned parameter can hold a row of another regex. + if (ts.isIdentifier(param.name)) { + const sym = lowerer.checker.getSymbolAtLocation(param.name); + if (sym === undefined || !bindingNeverReassigned(lowerer, sym, param)) return null; + } let site: ts.Node = fn; while (ts.isParenthesizedExpression(site.parent)) site = site.parent; const call = site.parent; @@ -13512,6 +13724,17 @@ function matchAllCallbackRegexOf( return null; } +/** The named groups of the rows `param` receives when it is the first + * parameter of an inline matchAll callback (matchAllCallbackRegexOf). */ +export function matchCallbackParamGroupsOf( + lowerer: Lowerer, + param: ts.ParameterDeclaration, +): { name: string; index: number }[] | null { + const reExpr = matchAllCallbackRegexOf(lowerer, param); + const pattern = reExpr === null ? null : staticRegexPatternOf(lowerer, reExpr); + return pattern === null ? null : namedCaptureGroupsOfPattern(pattern); +} + /** The regex argument of a (possibly const-stored) `s.matchAll(re)`. */ function matchAllRegexOf(lowerer: Lowerer, e: ts.Expression): ts.Expression | null { let expr = e; @@ -13537,6 +13760,14 @@ function matchAllRegexOf(lowerer: Lowerer, e: ts.Expression): ts.Expression | nu const only = expr.elements[0]!; if (ts.isSpreadElement(only)) return matchAllRegexOf(lowerer, only.expression); } + if ( + ts.isCallExpression(expr) && + ts.isPropertyAccessExpression(expr.expression) && + MATCH_ROW_SUBSET_METHODS.has(expr.expression.name.text) && + lowerer.isStdlibMember(expr.expression) + ) { + return matchAllRegexOf(lowerer, expr.expression.expression); + } if (ts.isIdentifier(expr)) { const init = constInitializerOf(lowerer, expr); if (init !== null) return matchAllRegexOf(lowerer, init); @@ -13560,11 +13791,11 @@ function matchAllRegexOf(lowerer: Lowerer, e: ts.Expression): ts.Expression | nu * - ES2025 duplicate names (distinct alternatives) project the first * participating occurrence, including a participating empty string; * - key order is group declaration order (declaredOrder), Node's own. - * The record has no prototype — Node's groups object is null-prototype, - * observable only through util.inspect's "[Object: null prototype]" - * prefix (the Object.groupBy stance, ledgered). Null when the receiver - * isn't a match slice or the regex isn't traceable — the caller's - * member fence (with the groups hint) names the gap. */ + * The record has a null prototype, like Node's groups object. A + * JavaScript callback parameter holds the row as a dynamic array; the + * projection reads its elements through dynamic reads. Null when the + * receiver isn't a match slice or the regex isn't traceable — the + * caller's member fence (with the groups hint) names the gap. */ function lowerMatchGroupsRead(lowerer: Lowerer, expr: ts.PropertyAccessExpression): IrExpr | null { if (expr.name.text !== "groups" || expr.questionDotToken !== undefined) return null; const loc = locOf(expr); @@ -13590,11 +13821,9 @@ function lowerMatchGroupsRead(lowerer: Lowerer, expr: ts.PropertyAccessExpressio return null; } const recv = lowerer.lowerExpr(expr.expression); - // A JavaScript callback parameter holds the row as a checked-dynamic - // value without its groups: refuse rather than read undefined. - if (recv.type.kind === "dyn" && isJsSourceFile(expr.getSourceFile())) - lowerer.stdlibMemberFence(expr); - if (!isMatchSliceType(lowerer, recv.type)) return null; + // A JavaScript callback parameter holds the row as a dynamic array. + const dynRow = recv.type.kind === "dyn" && isJsSourceFile(expr.getSourceFile()); + if (!dynRow && !isMatchSliceType(lowerer, recv.type)) return null; return lowerGroupsProjection(lowerer, recv, groups, loc); } @@ -13640,6 +13869,21 @@ function matchSliceUnionArms( return { arrTag, units }; } +/** Node's TypeError for `.groups` read off null or undefined. */ +function groupsReadTypeError(unit: "null" | "undefined", loc: SrcLoc): IrStmt { + return { + kind: "throw", + value: { + kind: "libCall", + fn: "error.new", + args: [strLit(`Cannot read properties of ${unit} (reading 'groups')`, loc)], + type: { kind: "object", className: "%TypeError" }, + loc, + }, + loc, + }; +} + /** The groups-record projection holds string | undefined values, including * an own overflow entry for every nonparticipating capture. One lifted helper per * (index list, receiver representation) builds it; see @@ -13666,7 +13910,8 @@ export function lowerGroupsProjection( const valueType = arrayValueType(lowerer, STRING); const shapeId = lowerer.shapes.intern([], false, valueType); const recordT: IrType = { kind: "record", shapeId }; - const recvKey = recv.type.kind === "union" ? recv.type.unionId : "arr"; + const recvKey = + recv.type.kind === "union" ? recv.type.unionId : recv.type.kind === "dyn" ? "dyn" : "arr"; const key = `regexgroups:${shapeId}:${groups.map((g) => `${g.name}=${g.index}`).join(",")}:${recvKey}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { @@ -13690,26 +13935,7 @@ export function lowerGroupsProjection( type: BOOL, loc, }, - then: [ - { - kind: "throw", - value: { - kind: "libCall", - fn: "error.new", - args: [ - { - kind: "strLit", - value: `Cannot read properties of ${u.unit} (reading 'groups')`, - type: STRING, - loc, - }, - ], - type: { kind: "object", className: "%TypeError" }, - loc, - }, - loc, - }, - ], + then: [groupsReadTypeError(u.unit, loc)], else_: null, loc, }); @@ -13725,10 +13951,33 @@ export function lowerGroupsProjection( locals.push({ id: "arr.0", name: "arr", type: arrayOf(STRING), mutable: false }); body.push({ kind: "varDecl", localId: "arr.0", init: narrowed, loc }); mRef = { kind: "varRef", localId: "arr.0", type: arrayOf(STRING), loc }; + } else if (recv.type.kind === "dyn") { + // A JavaScript callback parameter holds the row as a dynamic array: + // Node's TypeError for null and undefined, then dynamic element reads. + for (const unit of ["null", "undefined"] as const) { + body.push({ + kind: "if", + cond: { kind: "dynTest", test: unit, value: mRef, type: BOOL, loc }, + then: [groupsReadTypeError(unit, loc)], + else_: null, + loc, + }); + } } const elem = (index: number): IrExpr => - arrayValueRead(lowerer, mRef, numLit(index, loc), STRING, loc); + mRef.type.kind === "dyn" + ? lowerer.coerceToExpected( + { + kind: "dynKeyGet", + value: mRef, + key: strLit(String(index), loc), + type: DYN, + loc, + }, + valueType, + ) + : arrayValueRead(lowerer, mRef, numLit(index, loc), STRING, loc); const values = new Map(); order.forEach((name, i) => { const idxs = indicesByName.get(name)!; @@ -13766,6 +14015,7 @@ export function lowerGroupsProjection( value: { kind: "recordLit", fields: order.map((name) => ({ name, value: values.get(name)!, overflow: true as const })), + nullProto: true, type: recordT, loc, }, @@ -13995,6 +14245,28 @@ export function lowerUnionProperty( } return null; } + // A class instance the checker sees through a wider union (`(concrete + // as A | B).value`): the value is that class's instance, so read its + // field directly. + if (value.type.kind === "object") { + const className = value.type.className; + const info = lowerer.classes.get(className) ?? null; + const field = expr.name.text; + const fieldType = info?.fields.get(field); + if (fieldType !== undefined) + return lowerer.fieldGetExpr( + { container: "class", obj: value, className, field, fieldType }, + locOf(expr), + expr, + ); + const getter = lowerer.findMethodOn(info, `get:${field}`); + if (getter) + return lowerer.fieldGetExpr( + { container: "accessor", obj: value, className, field, fieldType: getter.sig.ret }, + locOf(expr), + expr, + ); + } if (value.type.kind !== "union") { lowerer.unsupported( "SC1090", @@ -14900,6 +15172,20 @@ export function fieldGetExpr( const helper = lowerer.deferredReadHelper(target.fieldType.unionId, inner, loc); if (helper) return { kind: "call", callee: helper, args: [read], type: inner, loc }; } + // An early-read field (ClassInfo.earlyReadFields) yields its undefined + // where the use reads it as Node does; elsewhere the declared type is + // extracted with a check. + if ( + target.fieldType.kind === "union" && + lowerer.classes.get(target.className)?.earlyReadFields?.has(target.field) === true && + !observesEarlyRead(blame) + ) { + const declared = lowerer.mapTypeOf(lowerer.typeOf(blame)); + if (declared && declared.kind !== "union") { + const helper = lowerer.narrowedArmHelper(target.fieldType.unionId, declared, loc); + if (helper) return lowerer.checkedNarrowCall(helper, read, declared, loc); + } + } return read; } return { @@ -15440,6 +15726,22 @@ function lowerFieldCompoundValue( if (op === "+" && read.type.kind === "string") { appended = lowerer.ensureString(rhs, rhsNode ?? access); value = { kind: "strConcat", left: read, right: appended, type: STRING, loc }; + } else if ( + op === "+" && + read.type.kind === "union" && + lowerer.stripUndefinedArm(read.type).kind === "string" && + target.container === "class" && + lowerer.classes.get(target.className)?.earlyReadFields?.has(target.field) === true + ) { + // A string field read before its assignment holds undefined, which + // concatenates as "undefined". + value = { + kind: "strConcat", + left: lowerer.ensureString(read, access), + right: lowerer.ensureString(rhs, rhsNode ?? access), + type: STRING, + loc, + }; } else if (numericRead.type.kind === "f64" && numericRhs.type.kind === "f64") { value = { kind: "bin", op, left: numericRead, right: numericRhs, type: F64, loc }; } else if (bigintValue) { @@ -15661,3 +15963,20 @@ function checkedIteratorPack(lowerer: Lowerer, source: IrExpr, loc: SrcLoc): IrE } return { kind: "call", callee: name, args: [source], type: DYN, loc }; } + +/** The `declare module "..."` body (or nested namespace body) a module + * export's declaration sits directly in, or undefined. */ +function builtinModuleExportParent(decl: ts.Node): ts.ModuleBlock | undefined { + const parent = ts.isVariableDeclaration(decl) ? decl.parent?.parent?.parent : decl.parent; + return parent !== undefined && ts.isModuleBlock(parent) ? parent : undefined; +} + +/** A function, class or variable exported by a builtin module's body. */ +function isBuiltinModuleValueDecl(decl: ts.Node): boolean { + return ( + builtinModuleExportParent(decl) !== undefined && + (ts.isFunctionDeclaration(decl) || + ts.isClassDeclaration(decl) || + ts.isVariableDeclaration(decl)) + ); +} diff --git a/packages/compiler/src/frontend/lowering/lower-generators.ts b/packages/compiler/src/frontend/lowering/lower-generators.ts index c61920c608..2eda4c4cec 100644 --- a/packages/compiler/src/frontend/lowering/lower-generators.ts +++ b/packages/compiler/src/frontend/lowering/lower-generators.ts @@ -32,7 +32,7 @@ export type GenType = IrType & { kind: "generator" }; /** The interned IteratorResult record of a generator type (never null for * a MAPPED generator — mapType required it to intern). */ -function resultRecordOf(lowerer: Lowerer, genT: GenType): IrType & { kind: "record" } { +export function resultRecordOf(lowerer: Lowerer, genT: GenType): IrType & { kind: "record" } { const rec = genResultRecord(genT.yieldT, genT.retT, lowerer.shapes, lowerer.unions); if (!rec) throw new InternalCompilerError("lowerer bug: mapped generator without a result record"); diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index c8be163962..1d67cef928 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -135,12 +135,17 @@ function inspectSupport( case "regex": case "symbol": case "bigint": + case "date": case "dyn": return null; case "array": return inspectSupport(lowerer, t.elem, visiting, out); case "bytes": - return "typed arrays inside composites have no inspect lowering yet (Buffer's form vs Uint8Array's is not recorded in the static type here)"; + // A Uint8Array and a Buffer share the u8 representation and print + // differently; the other element kinds are never Buffers. + return t.elem === "u8" + ? "Uint8Array and Buffer values inside composites have no inspect lowering yet (Buffer's form vs Uint8Array's is not recorded in the static type here)" + : null; case "record": { if (visiting.has(t.shapeId)) { out.recursive = true; @@ -321,7 +326,20 @@ export function inspectExpr( return { kind: "libCall", fn: "sym.toString", args: [value], type: STRING, loc }; case "bigint": return { kind: "libCall", fn: "bigint.inspect", args: [value], type: STRING, loc }; + case "date": + // Node prints a Date's toISOString() at every depth, and its + // toString() ("Invalid Date") when the time value is NaN. + return { kind: "libCall", fn: "insp.date", args: [value], type: STRING, loc }; case "bytes": + if (t.elem !== "u8") { + return { + kind: "libCall", + fn: "insp.typedArray", + args: [value, recurse, depth], + type: STRING, + loc, + }; + } return { kind: "libCall", fn: "insp.buffer", args: [value], type: STRING, loc }; case "dyn": return { kind: "libCall", fn: "insp.dyn", args: [value, recurse, depth], type: STRING, loc }; @@ -1436,23 +1454,27 @@ export function lowerConsoleInspectArg( } if (t.kind === "bytes") { // One runtime representation serves Buffer AND Uint8Array; their - // renderings differ, so the argument's checker type picks — and only - // Buffer lowers (lowerInspectCall's stance). + // renderings differ, so the argument's checker type picks + // (lowerInspectCall's stance). const tname = lowerer.checker.typeToString( lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(node)), ); - const isBuffer = - tname === "Buffer" || - tname === "NonSharedBuffer" || - tname.startsWith("Buffer<") || - tname.startsWith("NonSharedBuffer<"); - if (t.elem !== "u8" || !isBuffer) { + if (isDataViewTypeName(tname)) { lowerer.unsupported( "SC1090", node, - `${surface} of '${tname}' values (Buffer's form is the lowered typed-array rendering; other typed arrays fence)`, + `${surface} of '${tname}' values (Buffer and the numeric typed arrays are the lowered byte renderings; DataView has none yet)`, ); } + if (!isBufferTypeName(tname)) { + return { + kind: "libCall", + fn: "insp.typedArray", + args: [value, numLit(0, loc), numLit(2, loc)], + type: STRING, + loc, + }; + } return inspectExpr(lowerer, t, value, numLit(0, loc), numLit(2, loc), loc); } if (t.kind === "void") { @@ -1476,18 +1498,41 @@ export function lowerConsoleInspectArg( return formatValueExpr(lowerer, t, value, 2, loc); } +/** The checker's spelling of a Buffer, whose bytes print as . */ +function isBufferTypeName(tname: string): boolean { + return ( + tname === "Buffer" || + tname === "NonSharedBuffer" || + tname.startsWith("Buffer<") || + tname.startsWith("NonSharedBuffer<") + ); +} + +function isDataViewTypeName(tname: string): boolean { + return tname === "DataView" || tname.startsWith("DataView<"); +} + /* ── the callsite: options, direct function/class names ──────────────── */ /** The parsed options literal: the resolved depth (numeric; Infinity for - * `null`), with every non-default knob fenced by name. */ + * `null`), breakLength (numeric; default 80) and compact (numeric, 0 for + * false; default 3), with every other non-default knob fenced by name. */ +interface InspectOptions { + depth: number; + breakLength: number; + compact: number; + sorted: boolean; +} + function parseInspectOptions( lowerer: Lowerer, node: ts.Expression | undefined, recursive: boolean, dir = false, -): number { +): InspectOptions { const surface = dir ? "console.dir" : "util.inspect"; - if (!node || (ts.isIdentifier(node) && node.text === "undefined")) return 2; + const options: InspectOptions = { depth: 2, breakLength: 80, compact: 3, sorted: false }; + if (!node || (ts.isIdentifier(node) && node.text === "undefined")) return options; if (!ts.isObjectLiteralExpression(node)) { lowerer.noLowering( `${surface} with a non-literal options argument`, @@ -1495,7 +1540,6 @@ function parseInspectOptions( "the options must be an object literal ({ depth, colors: false, ... }) so the knobs resolve at compile time", ); } - let depth = 2; for (const prop of node.properties) { if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) { lowerer.noLowering( @@ -1526,7 +1570,7 @@ function parseInspectOptions( // machinery ([Circular *N]) cuts every cycle, so the walk is // bounded by the (finite) object graph — exactly Node. void recursive; - depth = Infinity; + options.depth = Infinity; } else if (!(ts.isIdentifier(value) && value.text === "undefined")) { const n = numeric(); if (n === null) { @@ -1536,7 +1580,7 @@ function parseInspectOptions( "depth must be a numeric literal (or null)", ); } - depth = n; + options.depth = n; } } else if (key === "colors") { if (value.kind !== ts.SyntaxKind.FalseKeyword) { @@ -1547,21 +1591,37 @@ function parseInspectOptions( ); } } else if (key === "compact") { - if (numeric() !== 3) { + // Node tries the single-line form and groups arrays only for a + // compact of at least 1; false and smaller numbers break every + // composite onto its own lines. compact: true is the older layout. + const n = value.kind === ts.SyntaxKind.FalseKeyword ? 0 : numeric(); + if (n === null || Number.isNaN(n)) { + lowerer.noLowering( + `${surface} with this compact option`, + value, + "compact must be false or a numeric literal (compact: true has no lowering)", + ); + } + options.compact = Math.max(n, 0); + } else if (key === "sorted") { + if (value.kind !== ts.SyntaxKind.TrueKeyword && value.kind !== ts.SyntaxKind.FalseKeyword) { lowerer.noLowering( - `${surface} with a non-default compact option`, + `${surface} with this sorted option`, value, - "only the default compact: 3 is lowered", + "sorted must be true or false (a comparator function has no lowering)", ); } + options.sorted = value.kind === ts.SyntaxKind.TrueKeyword; } else if (key === "breakLength") { - if (numeric() !== 80) { + const n = ts.isIdentifier(value) && value.text === "Infinity" ? Infinity : numeric(); + if (n === null || Number.isNaN(n)) { lowerer.noLowering( - `${surface} with a non-default breakLength option`, + `${surface} with a non-literal breakLength option`, value, - "only the default breakLength: 80 is lowered", + "breakLength must be a numeric literal or Infinity", ); } + options.breakLength = n; } else if (dir && key === "customInspect") { // console.dir's own default; only that value is lowered. if (value.kind !== ts.SyntaxKind.FalseKeyword) { @@ -1575,11 +1635,49 @@ function parseInspectOptions( lowerer.noLowering( `${surface} with the '${key}' option`, prop, - `the lowered options are depth (numeric literal or null), colors: false, compact: 3, ${dir ? "breakLength: 80, and customInspect: false" : "and breakLength: 80"}`, + `the lowered options are depth (numeric literal or null), colors: false, compact (false or a number), breakLength, ${dir ? "sorted (true or false), and customInspect: false" : "and sorted (true or false)"}`, ); } } - return depth; + return options; +} + +/** A render under non-default breakLength/compact/sorted: the value is evaluated + * first, the layout applies to this render only, and the defaults return + * afterwards. */ +function withInspectLayout( + lowerer: Lowerer, + options: InspectOptions, + value: IrExpr, + render: (value: IrExpr) => IrExpr, + loc: SrcLoc, +): IrExpr { + if (options.breakLength === 80 && options.compact === 3 && !options.sorted) return render(value); + const input = lowerer.declareHiddenLocal("%inspectInput", value.type); + const text = lowerer.declareHiddenLocal("%inspectText", STRING); + const layout = (breakLength: number, compact: number, sorted: boolean): IrStmt => ({ + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "insp.layout", + args: [numLit(breakLength, loc), numLit(compact, loc), boolLit(sorted, loc)], + type: VOID, + loc, + }, + loc, + }); + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: input.id, init: value, loc }, + layout(options.breakLength, options.compact, options.sorted), + { kind: "varDecl", localId: text.id, init: render(varRef(input.id, value.type, loc)), loc }, + layout(80, 3, false), + ], + result: varRef(text.id, STRING, loc), + type: STRING, + loc, + }; } /** `util.inspect(fn)` / `util.inspect(Cls)` over a DIRECT identifier @@ -1668,33 +1766,45 @@ export function lowerInspectValue( if (value.type.kind === "bytes") { // One runtime representation serves Buffer AND Uint8Array; their // renderings differ ( vs Uint8Array(1) [ 170 ]), so the - // TOP-LEVEL argument's checker type picks — and only Buffer lowers. - // Nested bytes fence in inspectSupport (no checker type survives - // into shapes). + // TOP-LEVEL argument's checker type picks. Nested u8 bytes fence in + // inspectSupport (no checker type survives into shapes). const tname = lowerer.checker.typeToString( lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(valueNode)), ); - const isBuffer = - tname === "Buffer" || - tname === "NonSharedBuffer" || - tname.startsWith("Buffer<") || - tname.startsWith("NonSharedBuffer<"); - if (dir) { + const isBuffer = isBufferTypeName(tname); + if (dir && isBuffer) { lowerer.noLowering( `console.dir of '${tname}' values`, valueNode, "console.dir disables custom inspection, and the typed-array rendering it falls back to has no lowering yet — use console.log", ); } - if (value.type.elem !== "u8" || !isBuffer) { + if (isDataViewTypeName(tname)) { lowerer.noLowering( `util.inspect of '${tname}' values`, valueNode, - "Buffer's form is the lowered typed-array rendering; other typed arrays fence", + "Buffer and the numeric typed arrays are the lowered byte renderings; DataView has none yet", + ); + } + const options = parseInspectOptions(lowerer, optionsNode, false, dir); + if (!isBuffer) { + // A numeric typed array: Uint8Array(n) [ ... ] under the depth and + // layout options. + return withInspectLayout( + lowerer, + options, + value, + (input) => ({ + kind: "libCall", + fn: "insp.typedArray", + args: [input, numLit(0, loc), numLit(options.depth, loc)], + type: STRING, + loc, + }), + loc, ); } - const bufDepth = parseInspectOptions(lowerer, optionsNode, false, dir); - void bufDepth; // Buffers render fully at any depth (custom inspect) + // Buffers render fully at any depth and on one line (custom inspect). return inspectExpr(lowerer, value.type, value, numLit(0, loc), numLit(2, loc), loc); } const walk = { recursive: false }; @@ -1702,17 +1812,30 @@ export function lowerInspectValue( if (reason !== null) { lowerer.noLowering(`${surface} of '${lowerer.fmt(value.type)}' values`, valueNode, reason); } - const depth = parseInspectOptions(lowerer, optionsNode, walk.recursive, dir); + const options = parseInspectOptions(lowerer, optionsNode, walk.recursive, dir); + const depth = options.depth; if (dir && value.type.kind === "dyn") { - return { - kind: "libCall", - fn: "console.dirDyn", - args: [value, numLit(depth, loc)], - type: STRING, + return withInspectLayout( + lowerer, + options, + value, + (input) => ({ + kind: "libCall", + fn: "console.dirDyn", + args: [input, numLit(depth, loc)], + type: STRING, + loc, + }), loc, - }; + ); } - return inspectExpr(lowerer, value.type, value, numLit(0, loc), numLit(depth, loc), loc); + return withInspectLayout( + lowerer, + options, + value, + (input) => inspectExpr(lowerer, value.type, input, numLit(0, loc), numLit(depth, loc), loc), + loc, + ); } /* ── util.format / util.formatWithOptions ────────────────────────────── */ @@ -1815,6 +1938,34 @@ function formatOArg(lowerer: Lowerer, node: ts.Expression, depth: number, loc: S return inspectExpr(lowerer, value.type, value, numLit(0, loc), numLit(depth, loc), loc); } +/** %d and %i over the argument types Node formats without a number + * conversion: a bigint prints as `${n}n` (formatBigInt) and a symbol as NaN; + * null is Number(null) (0) under %d and parseInt("null") (NaN) under %i, and + * undefined (including a void call) is NaN under both. Other arguments + * still evaluate. Null for other types. */ +function formatIntegerFixed(value: IrExpr, spec: "%d" | "%i", loc: SrcLoc): IrExpr | null { + if (value.type.kind === "bigint") { + return { kind: "libCall", fn: "bigint.inspect", args: [value], type: STRING, loc }; + } + let text: string; + if ( + value.type.kind === "symbol" || + value.type.kind === "undefinedT" || + value.type.kind === "void" + ) + text = "NaN"; + else if (value.type.kind === "nullT") text = spec === "%d" ? "0" : "NaN"; + else return null; + if (value.kind === "unitLit") return strLit(text, loc); + return { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: value, loc }], + result: strLit(text, loc), + type: STRING, + loc, + }; +} + /** util.format(...) / util.formatWithOptions({}, ...): the compile-time * %-substitution over a literal format string (formatWithOptionsInternal * ported exactly — the arg cursor, the args-exhausted guard, %c's @@ -1884,6 +2035,8 @@ export function lowerFormatCall( // strings through the runtime's ECMA-exact StringToNumber // (num.fromString — the same lowering Number(aString) takes). const value = lowerer.lowerExpr(node); + const fixed = formatIntegerFixed(value, "%d", loc); + if (fixed) return fixed; if (value.type.kind === "f64") return { kind: "libCall", fn: "insp.f64", args: [value], type: STRING, loc }; if (value.type.kind === "string") { @@ -1939,6 +2092,8 @@ export function lowerFormatCall( case 105: { // %i — parseInt(ToString(arg)): the spec-exact composition. const value = lowerer.lowerExpr(node); + const fixed = formatIntegerFixed(value, "%i", loc); + if (fixed) return fixed; if ( value.type.kind === "f64" || value.type.kind === "bool" || @@ -1979,10 +2134,10 @@ export function lowerFormatCall( return strLit("undefined", loc); } // A checked-dynamic argument (`console.error('headers: %j', - // headers)` — the suite's http logging idiom): the runtime dyn - // walk stringifies JS-exactly (root undefined/function prints - // "undefined"; a handle in the tree throws the "not supported yet" - // Error). + // headers)` — the suite's http logging idiom): the runtime runs + // JSON.stringify's walk (toJSON hooks included; a root it drops + // prints "undefined") and prints "[Circular]" for a circular + // structure. A typed argument prints "[Circular]" the same way. if (value.type.kind === "dyn") { return { kind: "libCall", fn: "insp.jsonDyn", args: [value], type: STRING, loc }; } @@ -1993,7 +2148,7 @@ export function lowerFormatCall( "only JSON-safe static types lower", ); } - return { kind: "jsonStringify", value, type: STRING, loc }; + return { kind: "jsonStringify", value, circular: true, type: STRING, loc }; } case 102: { // %f — parseFloat(String(arg)) formatted: a number keeps its value diff --git a/packages/compiler/src/frontend/lowering/lower-mock.ts b/packages/compiler/src/frontend/lowering/lower-mock.ts index b49cd045b7..2022d8232d 100644 --- a/packages/compiler/src/frontend/lowering/lower-mock.ts +++ b/packages/compiler/src/frontend/lowering/lower-mock.ts @@ -32,7 +32,8 @@ import { type IrType, type SrcLoc, } from "../../ir/ir.js"; -import { countedFor, dynUndefinedExpr, numLit, varRef } from "../../ir/build.js"; +import { countedFor, dynUndefinedExpr, numLit, strLit, varRef } from "../../ir/build.js"; +import type { IrLibFn } from "../../ir/builtin-signatures.js"; import { mockType as mockHybridType } from "../mock-shapes.js"; import { locOf } from "../program.js"; import { resultIsDiscarded } from "./call-position.js"; @@ -862,6 +863,157 @@ export function lowerMockTrackerCall( }; } +const MOCK_TIMERS_HINT = + "enable, tick, runAll, reset and setTime are the supported mock.timers members"; +const MOCK_TIMERS_APIS_HINT = + "list the mocked APIs as string literals: enable({ apis: ['setTimeout', 'Date'] })"; +/** The codes scr_test.c reads for the APIs enable() mocks, in Node's + * SUPPORTED_APIS order. */ +const MOCK_TIMERS_API_CODES: Readonly> = { + setTimeout: "T", + setInterval: "I", + setImmediate: "M", + Date: "D", + "scheduler.wait": "W", + "AbortSignal.timeout": "A", +}; +/** enable() and enable({ now }) without `apis` mock every API. */ +const MOCK_TIMERS_ALL_APIS = "TIMDWA"; + +/** `mock.timers.enable(...)` and the other MockTimers members, on the + * module's `mock` (tracker null) and t.mock. enable() takes no options or + * an object literal whose `apis`, when given, is an array literal of API + * names. */ +export function lowerMockTimersCall( + lowerer: Lowerer, + call: ts.CallExpression, + member: string, + what: string, + tracker: () => MockTracker, + sym: ts.Symbol | null, +): IrExpr { + const loc = locOf(call); + const args = call.arguments; + const optionalNumber = (fallback: number): IrExpr[] => { + if (args.length > 1) + lowerer.noLowering(`${what}.${member} with ${args.length} arguments`, call); + return [args.length === 1 ? lowerer.lowerExprExpecting(args[0]!, F64) : numLit(fallback, loc)]; + }; + if (!["enable", "tick", "setTime", "runAll", "reset"].includes(member)) { + lowerer.noLowering(`${what}.${member}`, call, MOCK_TIMERS_HINT, sym); + } + // The receiver evaluates before the arguments. + const t = tracker(); + let rest: IrExpr[]; + switch (member) { + case "enable": + rest = lowerMockTimersOptions(lowerer, call, what); + break; + case "tick": + rest = optionalNumber(1); + break; + case "setTime": + rest = optionalNumber(0); + break; + default: + if (args.length > 0) { + lowerer.noLowering(`${what}.${member} with ${args.length} arguments`, call); + } + rest = []; + } + const name = member[0]!.toUpperCase() + member.slice(1); + return { + kind: "libCall", + fn: (t ? `test.ctxMockTimers${name}` : `test.mockTimers${name}`) as IrLibFn, + args: t ? [t, ...rest] : rest, + type: VOID, + loc, + }; +} + +/** enable()'s { apis, now } as the libCall's API codes, `now` and flags + * (1 = `now` given, 2 = it is a Date). */ +function lowerMockTimersOptions(lowerer: Lowerer, call: ts.CallExpression, what: string): IrExpr[] { + const loc = locOf(call); + const options = call.arguments[0]; + if (call.arguments.length > 1) { + lowerer.noLowering(`${what}.enable with ${call.arguments.length} arguments`, call); + } + if (options === undefined) { + return [strLit(MOCK_TIMERS_ALL_APIS, loc), numLit(0, loc), numLit(0, loc)]; + } + if (!ts.isObjectLiteralExpression(options)) { + lowerer.noLowering( + `${what}.enable with a non-literal options object`, + options, + MOCK_TIMERS_APIS_HINT, + ); + } + let codes = ""; + let listed = false; + let now: IrExpr = numLit(0, loc); + let flags = 0; + for (const prop of options.properties) { + // `{ apis: [...] }`, `{ "now": n }` and the `{ now }` shorthand. + const init = ts.isPropertyAssignment(prop) + ? prop.initializer + : ts.isShorthandPropertyAssignment(prop) && ts.isIdentifier(prop.name) + ? prop.name + : null; + const key = + init && prop.name && (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) + ? prop.name.text + : null; + if (key === "apis") { + const list = init!; + if (!ts.isArrayLiteralExpression(list)) { + lowerer.noLowering( + `${what}.enable with a non-literal apis list`, + list, + MOCK_TIMERS_APIS_HINT, + ); + } + listed = true; + for (const el of list.elements) { + if (!ts.isStringLiteralLike(el)) { + lowerer.noLowering( + `${what}.enable with a non-literal API name`, + el, + MOCK_TIMERS_APIS_HINT, + ); + } + const code = MOCK_TIMERS_API_CODES[el.text] ?? ""; + if (code === "") { + lowerer.noLowering(`${what}.enable with the ${el.text} API`, el, MOCK_TIMERS_APIS_HINT); + } + codes += code; + } + } else if (key === "now") { + const value = lowerer.lowerExpr(init!); + if (value.type.kind === "date") { + now = { kind: "libCall", fn: "date.getTime", args: [value], type: F64, loc }; + flags = 3; + } else if (value.type.kind === "f64") { + now = value; + flags = 1; + } else { + lowerer.noLowering( + `${what}.enable with a now value of this type`, + prop, + "now takes a number or a Date", + ); + } + } else { + lowerer.noLowering( + `${what}.enable with this option`, + prop, + "apis and now are the supported enable options", + ); + } + } + return [strLit(listed ? codes : MOCK_TIMERS_ALL_APIS, loc), now, numLit(flags, loc)]; +} + /** `fn.mock.calls`: a copy of the log, as Node's getter returns. Null for * other reads. */ export function lowerMockContextProperty( diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index d18c4f547b..81ded6a585 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -35,6 +35,8 @@ import { withClassInitFlag, withCycleInitFlags, } from "./cycle-initialization.js"; +import { markRequireEarlyBindings } from "./require-initialization.js"; +import { collectJsonRequires, isJsonRequireDecl } from "./json-require.js"; import { invalidJsonModuleDiag, unsupportedPackageImportDiag, @@ -409,6 +411,7 @@ export function collectProgram(lowerer: Lowerer, parts: FileParts[]): void { for (const fp of parts) lowerer.collectGlobals(fp.sf, fp.topStmts); lowerer.collectNpmImports(parts); lowerer.collectJsonImports(parts); + collectJsonRequires(lowerer, parts); } finally { lowerer.diagSink = null; } @@ -416,6 +419,7 @@ export function collectProgram(lowerer: Lowerer, parts: FileParts[]): void { for (const fp of parts) lowerer.collectGlobals(fp.sf, fp.topStmts); lowerer.collectNpmImports(parts); lowerer.collectJsonImports(parts); + collectJsonRequires(lowerer, parts); } for (const decl of computed) { const symbol = declSymbolOf(lowerer, decl); @@ -493,6 +497,7 @@ export function collectProgram(lowerer: Lowerer, parts: FileParts[]): void { lowerer.collecting = wasCollecting; } markCycleEarlyBindings(lowerer); + markRequireEarlyBindings(lowerer); } export function collectNpmImports(lowerer: Lowerer, parts: FileParts[]): void { @@ -1532,8 +1537,9 @@ export function collectGlobals( isRequireStatement(stmt) && !( ts.isVariableStatement(stmt) && - stmt.declarationList.declarations.some((decl) => - isNativeFfiRequire(lowerer, decl.initializer), + stmt.declarationList.declarations.some( + (decl) => + isNativeFfiRequire(lowerer, decl.initializer) || isJsonRequireDecl(lowerer, decl), ) ) ) { diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index bd381f434e..f74e1d5b07 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -10,6 +10,8 @@ import { InternalCompilerError } from "../../errors.js"; * flow (if/while/for/for-of/do, switch, try/catch, abrupt completions), * explicit resource management, and blocked-binding poisoning. */ import * as ts from "../ts7/adapter.js"; +import { requireInitFlagStmt } from "./require-initialization.js"; +import { isJsonRequireDecl } from "./json-require.js"; import { bindingSource } from "../binding-source.js"; import type { Lowerer } from "./lowerer.js"; import { @@ -1512,81 +1514,96 @@ export function lowerVarStatement(lowerer: Lowerer, stmt: ts.VariableStatement): const isLet = (list.flags & ts.NodeFlags.Let) !== 0 || (list.flags & ts.NodeFlags.BlockScoped) === 0; return list.declarations.flatMap((decl) => { - // The canonical ESM bridge to a compiled program module. Like a - // top-level CommonJS require declaration, the binding is alias - // plumbing while the dependency evaluates at this statement's - // exact position through its run-once initializer. - if (isConst && createRequireProgramModuleDecl(lowerer, decl.name, decl.initializer)) { - if (!ts.isSourceFile(stmt.parent)) { - lowerer.unsupported( - "SC1090", - decl, - "createRequire program-module bindings outside the module's top level (move the binding to the top of the file; side-effect-only requires may remain nested)", - ); - } - const target = createRequireProgramModuleOf(lowerer, decl.initializer)!; - const init = lowerer.requireInitStmt(target.spec, decl); - return init ? [init] : []; - } - // CommonJS require declarations (JS files): at the module's top - // level the BINDINGS are alias plumbing (no storage; - // resolveValueSymbol routes the reads), but the require itself is - // Node's inline module evaluation — a relative require lowers to the - // required module's guarded %init call at exactly this position - // (first require runs the body, later ones are cache hits; builtins - // load nothing). Binding requires anywhere else would need lazily- - // initialized alias storage this model doesn't represent; named - // fence. - if ( - decl.initializer !== undefined && - requireSpecOf(decl.initializer) !== null && - !isNativeFfiRequire(lowerer, decl.initializer) && - isJsSourceFile(decl.getSourceFile()) - ) { - const spec = requireSpecOf(decl.initializer)!; - if (lowerer.externalTypes.has(spec)) lowerer.externalHostFence(spec, decl, false); - if (ts.isSourceFile(stmt.parent)) { - const init = lowerer.requireInitStmt(spec, decl); - return init ? [init] : []; - } - // A nested BUILTIN require (`get inFreeBSDJail() { const { execSync } - // = require('child_process'); ... }` — test/common's lazy-getter - // idiom): builtins load nothing, so the statement is PURE alias - // plumbing wherever it sits — the bindings resolve through the - // same routing top-level requires use, and no statement remains. - // Relative requires keep the fence: their module-init-at-position - // semantics needs storage this model doesn't represent. - if (canonicalBuiltinModule(spec) !== null) { - return []; - } + const lowered = lowerVarDeclarator(lowerer, stmt, decl, isConst, isLet); + // A require binding earlier code can read is initialized from here on + // (require-initialization.ts). + const initialized = requireInitFlagStmt(lowerer, decl); + return initialized ? [...lowered, initialized] : lowered; + }); +} + +function lowerVarDeclarator( + lowerer: Lowerer, + stmt: ts.VariableStatement, + decl: ts.VariableDeclaration, + isConst: boolean, + isLet: boolean, +): IrStmt[] { + // The canonical ESM bridge to a compiled program module. Like a + // top-level CommonJS require declaration, the binding is alias + // plumbing while the dependency evaluates at this statement's + // exact position through its run-once initializer. + if (isConst && createRequireProgramModuleDecl(lowerer, decl.name, decl.initializer)) { + if (!ts.isSourceFile(stmt.parent)) { lowerer.unsupported( "SC1090", decl, - "require() with bindings outside the module's top level (move it to the top of the file)", + "createRequire program-module bindings outside the module's top level (move the binding to the top of the file; side-effect-only requires may remain nested)", ); } - // `const { NGHTTP2_CANCEL } = http2.constants` — a destructure over - // the baked constants table: alias plumbing, no storage (uses read - // their literals via builtinConstantBindingOf; collectGlobals skipped - // the globals by the same test). - if (lowerer.builtinConstantsDestructureDecl(decl.name, decl.initializer)) return []; - if (ts.isArrayBindingPattern(decl.name) || ts.isObjectBindingPattern(decl.name)) { - // `const { createSign } = crypto` over a builtin NAMESPACE binding: - // alias plumbing (builtinImportOf routes the reads) — no statement. - if (builtinNamespaceDestructureModuleOf(lowerer, decl) !== null) return []; - return lowerer.lowerDestructuringDecl(decl, isLet); - } - // STORED protocol values with statically closed representations: - // numeric value iterators retain source/cursor/done hidden locals; a - // matchAll drain retains its companion index array so a later - // for-of can serve `m.index`. Both are const-only interceptions. - const numericIterator = lowerNumericIteratorDecl(lowerer, decl, isConst); - if (numericIterator) return numericIterator; - const drained = lowerMatchAllDrainDecl(lowerer, decl, isConst); - if (drained) return drained; - const lowered = lowerer.lowerVarDecl(decl, isLet); - return lowered ? [lowered] : []; - }); + const target = createRequireProgramModuleOf(lowerer, decl.initializer)!; + const init = lowerer.requireInitStmt(target.spec, decl); + return init ? [init] : []; + } + // CommonJS require declarations (JS files): at the module's top + // level the BINDINGS are alias plumbing (no storage; + // resolveValueSymbol routes the reads), but the require itself is + // Node's inline module evaluation — a relative require lowers to the + // required module's guarded %init call at exactly this position + // (first require runs the body, later ones are cache hits; builtins + // load nothing). Binding requires anywhere else would need lazily- + // initialized alias storage this model doesn't represent; named + // fence. + if ( + decl.initializer !== undefined && + requireSpecOf(decl.initializer) !== null && + !isNativeFfiRequire(lowerer, decl.initializer) && + !isJsonRequireDecl(lowerer, decl) && + isJsSourceFile(decl.getSourceFile()) + ) { + const spec = requireSpecOf(decl.initializer)!; + if (lowerer.externalTypes.has(spec)) lowerer.externalHostFence(spec, decl, false); + if (ts.isSourceFile(stmt.parent)) { + const init = lowerer.requireInitStmt(spec, decl); + return init ? [init] : []; + } + // A nested BUILTIN require (`get inFreeBSDJail() { const { execSync } + // = require('child_process'); ... }` — test/common's lazy-getter + // idiom): builtins load nothing, so the statement is PURE alias + // plumbing wherever it sits — the bindings resolve through the + // same routing top-level requires use, and no statement remains. + // Relative requires keep the fence: their module-init-at-position + // semantics needs storage this model doesn't represent. + if (canonicalBuiltinModule(spec) !== null) { + return []; + } + lowerer.unsupported( + "SC1090", + decl, + "require() with bindings outside the module's top level (move it to the top of the file)", + ); + } + // `const { NGHTTP2_CANCEL } = http2.constants` — a destructure over + // the baked constants table: alias plumbing, no storage (uses read + // their literals via builtinConstantBindingOf; collectGlobals skipped + // the globals by the same test). + if (lowerer.builtinConstantsDestructureDecl(decl.name, decl.initializer)) return []; + if (ts.isArrayBindingPattern(decl.name) || ts.isObjectBindingPattern(decl.name)) { + // `const { createSign } = crypto` over a builtin NAMESPACE binding: + // alias plumbing (builtinImportOf routes the reads) — no statement. + if (builtinNamespaceDestructureModuleOf(lowerer, decl) !== null) return []; + return lowerer.lowerDestructuringDecl(decl, isLet); + } + // STORED protocol values with statically closed representations: + // numeric value iterators retain source/cursor/done hidden locals; a + // matchAll drain retains its companion index array so a later + // for-of can serve `m.index`. Both are const-only interceptions. + const numericIterator = lowerNumericIteratorDecl(lowerer, decl, isConst); + if (numericIterator) return numericIterator; + const drained = lowerMatchAllDrainDecl(lowerer, decl, isConst); + if (drained) return drained; + const lowered = lowerer.lowerVarDecl(decl, isLet); + return lowered ? [lowered] : []; } /** The numeric indexed source of a built-in value-iterator expression: @@ -1914,44 +1931,14 @@ export function lowerDestructuringDecl( init = { kind: "unionNarrow", unionId: init.type.unionId, tag, value: uref, type: arm, loc }; } } - // `const { groups } = re.exec(s)!` / `const { groups: { year } } = m` - // — every pattern element binds `groups` off a MATCH SLICE whose - // regex is statically known: the slice has no record shape of its - // own, so the groups projection (lowerGroupsProjection — the same - // record `m.groups` reads produce) wraps one level in a synthesized - // `{ groups: ... }` record and the ordinary record destructure below - // serves the pattern — aliases and nested patterns included. Only - // named-group regexes take the wrap: a group-less regex destructures - // `groups: undefined` in Node (a nested pattern then throws), which - // the fence reports honestly instead. - if ( - ts.isObjectBindingPattern(decl.name) && - decl.name.elements.length > 0 && - isMatchSliceType(lowerer, init.type) && - decl.name.elements.every((el) => { - if (el.dotDotDotToken !== undefined || el.name === undefined) return false; - const prop = el.propertyName; - if (prop === undefined) return ts.isIdentifier(el.name) && el.name.text === "groups"; - return (ts.isIdentifier(prop) || ts.isStringLiteral(prop)) && prop.text === "groups"; - }) - ) { - const groups = matchResultNamedGroupsOf(lowerer, decl.initializer!); - if (groups !== null && groups.length > 0) { - const proj = lowerGroupsProjection(lowerer, init, groups, loc); - const outerShape = lowerer.shapes.intern( - [{ name: "groups", type: proj.type }], - false, - undefined, - ["groups"], - ); - init = { - kind: "recordLit", - fields: [{ name: "groups", value: proj }], - type: { kind: "record", shapeId: outerShape }, - loc, - }; - } - } + const groupsSource = matchGroupsPatternSource( + lowerer, + decl.name, + init, + () => matchResultNamedGroupsOf(lowerer, decl.initializer!), + loc, + ); + if (groupsSource !== null) init = groupsSource; const tmp = lowerer.declareHiddenLocal("%destr", init.type); out.push({ kind: "varDecl", localId: tmp.id, init, loc }); // V8's destructuring TypeError spells STATIC sources: an identifier @@ -1978,6 +1965,55 @@ export function lowerDestructuringDecl( return out; } +/** `const { groups } = re.exec(s)!` / `const { groups: { year } } = m` + * — every pattern element binds `groups` off a match row whose regex is + * statically known. The slice (or a JavaScript callback's dynamic row) + * has no record shape of its own, so the groups projection + * (lowerGroupsProjection — the same record `m.groups` reads produce) + * wraps one level in a synthesized `{ groups: ... }` record and the + * ordinary record destructure serves the pattern — aliases and nested + * patterns included. Only named-group regexes take the wrap: a + * group-less regex destructures `groups: undefined` in Node (a nested + * pattern then throws), which the fence reports honestly instead. Null + * when the pattern or the row does not qualify. */ +export function matchGroupsPatternSource( + lowerer: Lowerer, + pattern: ts.BindingName, + row: IrExpr, + groupsOf: () => { name: string; index: number }[] | null, + loc: SrcLoc, +): IrExpr | null { + if ( + !ts.isObjectBindingPattern(pattern) || + pattern.elements.length === 0 || + !(row.type.kind === "dyn" + ? isJsSourceFile(pattern.getSourceFile()) + : isMatchSliceType(lowerer, row.type)) || + !pattern.elements.every((el) => { + if (el.dotDotDotToken !== undefined || el.name === undefined) return false; + const prop = el.propertyName; + if (prop === undefined) return ts.isIdentifier(el.name) && el.name.text === "groups"; + return (ts.isIdentifier(prop) || ts.isStringLiteral(prop)) && prop.text === "groups"; + }) + ) + return null; + const groups = groupsOf(); + if (groups === null || groups.length === 0) return null; + const proj = lowerGroupsProjection(lowerer, row, groups, loc); + const outerShape = lowerer.shapes.intern( + [{ name: "groups", type: proj.type }], + false, + undefined, + ["groups"], + ); + return { + kind: "recordLit", + fields: [{ name: "groups", value: proj }], + type: { kind: "record", shapeId: outerShape }, + loc, + }; +} + /** One pattern level: emits a declaration (or global assignment) per * bound name, recursing through hidden temps for nested patterns. */ export function lowerBindingPattern( @@ -5037,6 +5073,23 @@ function jsObjectBindingExpands( ) { const name = property(node.left); if (name === null || (name !== undefined && !names.has(name))) expands = true; + // `state = { _tag: "Done", right }` stores an object with properties + // the initializer's shape lacks; later reads of them must see them. + const target = unwrap(node.left); + const value = unwrap(node.right); + const targetSymbol = ts.isIdentifier(target) ? lowerer.resolveValueSymbol(target) : undefined; + if ( + targetSymbol && + aliases.has(targetSymbol) && + ts.isObjectLiteralExpression(value) && + value.properties.some( + (field) => + !(ts.isPropertyAssignment(field) || ts.isShorthandPropertyAssignment(field)) || + !(ts.isIdentifier(field.name) || ts.isStringLiteral(field.name)) || + !names.has(field.name.text), + ) + ) + expands = true; } if (ts.isDeleteExpression(node) && property(node.expression) !== undefined) expands = true; if ( @@ -5266,7 +5319,7 @@ function lowerDynSwitch( loc: locOf(expression), }; } - if (!lowerer.dynConvertible(test.type)) { + if (test.type.kind === "date" || !lowerer.dynConvertible(test.type)) { lowerer.unsupported( "SC1100", expression, @@ -6019,16 +6072,22 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm ); } // syncBuiltinESMExports() is the other compiler-only statement: the - // call validates its zero-argument shape, then emits no code because - // the static builtin surface is immutable. + // call validates its zero-argument shape, then refreshes the events + // module's ES module namespace copy of defaultMaxListeners, the one + // builtin export a program can change. const bi = ts.isIdentifier(expr.expression) ? lowerer.builtinImportOf(expr.expression) : ts.isPropertyAccessExpression(expr.expression) ? lowerer.builtinMemberOf(expr.expression) : null; if (bi?.module === "module" && bi.member === "syncBuiltinESMExports") { - lowerNodeModuleCall(lowerer, expr, bi, locOf(expr)); - return { kind: "block", body: [], loc: locOf(expr) }; + const loc = locOf(expr); + lowerNodeModuleCall(lowerer, expr, bi, loc); + return { + kind: "exprStmt", + expr: { kind: "libCall", fn: "emitter.esmSync", args: [], type: VOID, loc }, + loc, + }; } } if ( diff --git a/packages/compiler/src/frontend/lowering/lower-test.ts b/packages/compiler/src/frontend/lowering/lower-test.ts index 6b25634148..d18aec3055 100644 --- a/packages/compiler/src/frontend/lowering/lower-test.ts +++ b/packages/compiler/src/frontend/lowering/lower-test.ts @@ -21,23 +21,26 @@ import { type IrStmt, type IrType, type SrcLoc, + BOOL, + DYN, F64, STRING, VOID, isUnitType, } from "../../ir/ir.js"; -import { numLit, strLit } from "../../ir/build.js"; +import { dynUndefinedExpr, numLit, strLit, varRef } from "../../ir/build.js"; import { lowerMockContextCall, lowerMockContextProperty, lowerMockFn, lowerMockMethod, + lowerMockTimersCall, lowerMockTrackerCall, type MockTracker, } from "./lower-mock.js"; const MOCK_HINT = - "mock.fn, mock.method, mock.restoreAll and mock.reset are the supported mock members"; + "mock.fn, mock.method, mock.restoreAll, mock.reset and mock.timers are the supported mock members"; /** The mock tracker members that lower, for the module's `mock` (tracker * null) and t.mock. Null for other members. */ @@ -517,7 +520,11 @@ export function lowerNodeTestModuleCall( ); break; case "snapshot": - lowerer.noLowering("test.snapshot", expr, "snapshot testing has no lowering"); + lowerer.noLowering( + "test.snapshot", + expr, + "t.assert.snapshot and t.assert.fileSnapshot are the supported snapshot surface", + ); break; default: lowerer.noLowering( @@ -556,6 +563,195 @@ export function lowerTestDirectCall( return lowerRegistration(lowerer, expr, "test(...)", loc, 0, null); } +/** t.assert.snapshot(value[, options]) and t.assert.fileSnapshot(value, + * path[, options]), Node's SnapshotManager assertions: the value runs + * through options.serializers, by default Node's JSON.stringify(value, + * null, 2), and String() of the result is compared with + * assert.strictEqual against the snapshot that scr_test.c reads from + * ".snapshot" or `path`, or records in update mode. A throw + * while serializing becomes Node's ERR_INVALID_STATE. */ +function lowerSnapshotAssert( + lowerer: Lowerer, + call: ts.CallExpression, + member: "snapshot" | "fileSnapshot", + testCtx: ts.Expression, +): IrExpr { + const loc = locOf(call); + const what = `t.assert.${member}`; + const t = lowerer.lowerExpr(testCtx); + const stmts: IrStmt[] = []; + const slot = (name: string, type: IrType, init: IrExpr): IrExpr => { + const local = lowerer.declareHiddenLocal(name, type); + stmts.push({ kind: "varDecl", localId: local.id, init, loc }); + return varRef(local.id, type, loc); + }; + // Arguments evaluate in order before the assertion serializes, as in Node. + const valueNode = call.arguments[0]; + const input = slot( + "%snapshotInput", + DYN, + valueNode ? lowerer.lowerExprExpecting(valueNode, DYN) : dynUndefinedExpr(loc), + ); + let path: IrExpr | null = null; + if (member === "fileSnapshot") { + const pathNode = call.arguments[1]; + path = slot( + "%snapshotPath", + STRING, + pathNode ? lowerer.lowerExprExpecting(pathNode, STRING) : strLit("", loc), + ); + } + const serializers = + snapshotSerializers(lowerer, call.arguments[member === "snapshot" ? 1 : 2], what)?.map((fn) => + slot("%snapshotSerializer", DYN, fn), + ) ?? null; + if (call.arguments.length > (member === "snapshot" ? 2 : 3)) { + lowerer.noLowering(`${what} with ${call.arguments.length} arguments`, call); + } + const text: IrExpr = { + kind: "toString", + operand: { + kind: "libCall", + fn: "test.snapshotSerialize", + args: [ + input, + serializers === null + ? dynUndefinedExpr(loc) + : { kind: "dynArrLit", elems: serializers, type: DYN, loc }, + ], + type: DYN, + loc, + }, + type: STRING, + loc, + }; + const actual = slot( + "%snapshotActual", + STRING, + member === "snapshot" + ? { kind: "libCall", fn: "test.snapshotValue", args: [text], type: STRING, loc } + : text, + ); + const expected: IrExpr = + member === "snapshot" + ? { + kind: "libCall", + fn: "test.ctxSnapshot", + args: [t, actual, strLit(`${lowerer.entry.fileName}.snapshot`, loc)], + type: STRING, + loc, + } + : { kind: "libCall", fn: "test.fileSnapshot", args: [actual, path!], type: STRING, loc }; + const no: IrExpr = { kind: "boolLit", value: false, type: BOOL, loc }; + return { + kind: "seqExpr", + stmts, + result: { + kind: "libCall", + fn: "assert.eqStr", + args: [actual, expected, no, no, strLit("", loc), no], + type: VOID, + loc, + }, + type: VOID, + loc, + }; +} + +/** The serializer functions of a snapshot options object literal, as + * checked-dynamic values in order; null without a serializers option. + * Options that are not an object literal, a serializers list that is not + * an array literal, and other options fence. */ +function snapshotSerializers( + lowerer: Lowerer, + options: ts.Expression | undefined, + what: string, +): IrExpr[] | null { + if (options === undefined) return null; + const hint = "pass the options as an object literal: { serializers: [(value) => ...] }"; + if (!ts.isObjectLiteralExpression(options)) { + lowerer.noLowering(`${what} options that are not an object literal`, options, hint); + } + let fns: IrExpr[] | null = null; + for (const prop of options.properties) { + const name = prop.name && ts.isIdentifier(prop.name) ? prop.name.text : null; + if ( + name !== "serializers" || + !(ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) + ) { + lowerer.noLowering(`${what} options other than serializers`, prop, hint); + } + const list = ts.isPropertyAssignment(prop) ? prop.initializer : prop.name; + if (!ts.isArrayLiteralExpression(list)) { + lowerer.noLowering(`${what} with a non-literal serializers list`, list, hint); + } + fns = list.elements.map((el) => { + if (ts.isSpreadElement(el)) lowerer.noLowering(`${what} with a spread serializer`, el, hint); + return lowerer.lowerExprExpecting(el, DYN); + }); + } + return fns; +} + +/** `mock.timers` (tracker null) or `t.mock.timers` on a TestContext; + * null for other expressions. */ +function mockTimersReceiver( + lowerer: Lowerer, + expr: ts.Expression, +): { what: string; tracker: () => MockTracker } | null { + if ( + !ts.isPropertyAccessExpression(expr) || + expr.questionDotToken || + expr.name.text !== "timers" || + !lowerer.isStdlibMember(expr) + ) { + return null; + } + const owner = expr.expression; + if (ts.isIdentifier(owner)) { + const bi = lowerer.builtinImportOf(owner); + return bi && bi.module === "test" && bi.member === "mock" + ? { what: "mock.timers", tracker: () => null } + : null; + } + if ( + ts.isPropertyAccessExpression(owner) && + !owner.questionDotToken && + owner.name.text === "mock" && + lowerer.mapTypeOf(lowerer.typeOf(owner.expression))?.kind === "testCtx" && + lowerer.isStdlibMember(owner) + ) { + const testCtx = owner.expression; + return { what: "t.mock.timers", tracker: () => lowerer.lowerExpr(testCtx) }; + } + return null; +} + +/** `mock.timers[Symbol.dispose]()` and its t.mock twin: an alias of + * reset(), as in Node. Null for other element-access calls. */ +export function lowerMockTimersElementCall( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.ElementAccessExpression, +): IrExpr | null { + if (call.questionDotToken || access.questionDotToken) return null; + const timers = mockTimersReceiver(lowerer, access.expression); + if (!timers) return null; + const key = access.argumentExpression; + const isDispose = + ts.isPropertyAccessExpression(key) && + key.name.text === "dispose" && + lowerer.stdlibGlobalMember(key, "Symbol") === "dispose"; + if (!isDispose) { + lowerer.noLowering( + `${timers.what} computed member calls`, + call, + "call timers[Symbol.dispose]()", + ); + } + return lowerMockTimersCall(lowerer, call, "reset", timers.what, timers.tracker, null); +} + /** Method-position node:test calls — one entry in lower-calls.ts's * intrinsic chain: skip/todo/only twins on NAMED import bindings * (`test.skip(...)` where `test` came from `{ test }`), the TestContext @@ -634,6 +830,13 @@ export function lowerTestMethodCall( ); if (served) return served; } + // mock.timers.enable(...) and the other MockTimers members, on the + // module's `mock` and on t.mock. + const timers = mockTimersReceiver(lowerer, access.expression); + if (timers) { + const sym = lowerer.checker.getSymbolAtLocation(access.name) ?? null; + return lowerMockTimersCall(lowerer, call, member, timers.what, timers.tracker, sym); + } // t.assert.strictEqual(...) — the callee's receiver is `t.assert` on a // TestContext: Node's t.assert methods ARE the assert functions (bound // to the test for its counters — not modeled; the throw is identical), @@ -645,7 +848,10 @@ export function lowerTestMethodCall( lowerer.mapTypeOf(lowerer.typeOf(access.expression.expression))?.kind === "testCtx" && lowerer.isStdlibMember(access.expression) ) { - const served = lowerer.lowerAssertModuleCall(call, { module: "assert", member }, loc); + const served = + member === "snapshot" || member === "fileSnapshot" + ? lowerSnapshotAssert(lowerer, call, member, access.expression.expression) + : lowerer.lowerAssertModuleCall(call, { module: "assert", member }, loc); // Each t.assert call counts against the test's plan (t.plan). if (served) return { diff --git a/packages/compiler/src/frontend/lowering/lower-timers.ts b/packages/compiler/src/frontend/lowering/lower-timers.ts index 47e929b497..44b2b6a1d8 100644 --- a/packages/compiler/src/frontend/lowering/lower-timers.ts +++ b/packages/compiler/src/frontend/lowering/lower-timers.ts @@ -299,6 +299,40 @@ function tolerantClearNoop(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLo return null; } +const MOCKABLE_TIMER_FNS: ReadonlySet = new Set([ + "timers.setTimeout", + "timers.setTimeoutHandle", + "timers.setInterval", + "timers.clearTimeout", + "timers.clearInterval", + "timers.setImmediate", + "timers.clearImmediate", +]); + +/** Whether a timer call's callee is an ESM named or namespace import of + * node:timers (destructured bindings included): Node's mock.timers leaves + * those bindings alone and replaces the globals. */ +function isRealTimerCallee(lowerer: Lowerer, callee: ts.Expression): boolean { + if (ts.isIdentifier(callee)) return lowerer.builtinImportOf(callee)?.module === "timers"; + if (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.expression)) { + const sym = lowerer.checker.getSymbolAtLocation(callee.expression); + const decl = sym ? lowerer.checker.declarationsOf(sym)[0] : undefined; + return decl !== undefined && ts.isNamespaceImport(decl); + } + return false; +} + +/** Marks a lowered timer call `realTimer` when its callee is a binding + * mock.timers does not replace. */ +function markRealTimerCall( + lowerer: Lowerer, + expr: ts.CallExpression, + lowered: IrExpr | null, +): IrExpr | null { + if (lowered?.kind !== "libCall" || !MOCKABLE_TIMER_FNS.has(lowered.fn)) return lowered; + return isRealTimerCallee(lowerer, expr.expression) ? { ...lowered, realTimer: true } : lowered; +} + /** One timer call by MEMBER NAME — the shared lowering behind the ambient * globals, the node:timers named/destructured imports, and the namespace * form (`timers.setTimeout(...)`). Null when the member isn't a lowered @@ -308,6 +342,15 @@ export function lowerTimersMemberCall( expr: ts.CallExpression, member: string, loc: SrcLoc, +): IrExpr | null { + return markRealTimerCall(lowerer, expr, lowerTimerCall(lowerer, expr, member, loc)); +} + +function lowerTimerCall( + lowerer: Lowerer, + expr: ts.CallExpression, + member: string, + loc: SrcLoc, ): IrExpr | null { // Delayed timers share callback adaptation and the 1ms default. Trailing // arguments are captured now and delivered when the callback fires. @@ -328,10 +371,12 @@ export function lowerTimersMemberCall( expr.arguments[0]!, loc, ); + // An omitted delay is NaN, Node's undefined: the loop clamps it to + // 1ms, and mock.timers queues it to run at the next tick. const ms: IrExpr = expr.arguments.length >= 2 ? lowerer.lowerExpr(expr.arguments[1]!) - : { kind: "numLit", value: 1, type: F64, loc }; + : { kind: "numLit", value: NaN, type: F64, loc }; if (member === "setInterval") { return { kind: "libCall", fn: "timers.setInterval", args: [cb, ms], type: F64, loc }; } diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 074a77a6c5..2c99de1a87 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -10,6 +10,12 @@ import { refuseUncheckedCycleReads, type CycleInitFlag, } from "./cycle-initialization.js"; +import { isJsonRequireDecl, type JsonRequireModule } from "./json-require.js"; +import { + checkRequireBindingAccesses, + type EarlyReference, + type RequireInitFlag, +} from "./require-initialization.js"; import { identityPreservingWidening } from "./coercions/identity.js"; import { scanDivergences } from "./divergences.js"; import { checkedClassAssertion } from "./class-assertions.js"; @@ -388,6 +394,7 @@ import { createRequireProgramModuleOf, createRequireSpecOf, fencedBuiltinImportOf, + esmImportBindingKind, } from "./builtins/module-bindings.js"; import { stripTypeCasts } from "./builtins/arguments.js"; import { lowerBuiltinModuleCall } from "./builtins/module-calls.js"; @@ -417,6 +424,7 @@ import { lowerTracingChannelMethodCall, lowerTracingChannelProperty, } from "./builtins/async-context.js"; +import { lowerDisposableStackNew } from "./builtins/disposable-stack.js"; import { lowerErrorCodeProperty } from "./builtins/errors.js"; import { lowerProcessProperty, @@ -433,7 +441,7 @@ import { } from "./builtins/console.js"; import { lowerConsoleNew } from "./lower-console.js"; import { lowerNumberStaticCall, lowerNumberStaticProperty } from "./builtins/numbers.js"; -import { lowerDateCall } from "./builtins/dates.js"; +import { dateJsonType, isDateJsonSource, lowerDateCall } from "./builtins/dates.js"; import { lowerFsConstantsProperty, lowerBuiltinConstantsProperty, @@ -523,6 +531,7 @@ import { lowerExprStatement, lowerForOf, lowerForStatement, + matchGroupsPatternSource, } from "./lower-stmts.js"; import { type FieldTarget, @@ -557,6 +566,7 @@ import { fieldSetStmt, lowerFieldCompound, uniqueSymbolKeyOf, + matchCallbackParamGroupsOf, } from "./lower-exprs.js"; import { finishOptionalChain, @@ -1876,6 +1886,8 @@ export class Lowerer { /** Record fields promoted to an undefined-armed union by an indexed-read * value. The key is the concrete emitted shape and field name. */ readonly runtimeOptionalFields = new Set(); + /** Bindings initialized by a Date's `toJSON()` (analyzeDateJsonResults). */ + readonly dateJsonBindings = new Set(); /** Record bindings whose flow class reaches storage outside the class: * they keep the declared layout, so an initializer from unchecked reads * converts once instead of the binding adopting the widened variant. */ @@ -2437,6 +2449,10 @@ export class Lowerer { * prepended by lowerFileInit so the bindings are live before any * top-level statement runs. */ readonly jsonInitActions = new Map(); + /** CommonJS require() of JSON documents: the shared object per document + * and the calls reading it (json-require.ts). */ + readonly jsonRequireModules = new Map(); + readonly jsonRequireCalls = new Map(); /** Dynamic `import("literal")` resolutions, keyed * `fileName\u0000specifier` (collectDynamicImports fills it during npm * collection; lowerDynamicImportCall reads it per site). */ @@ -2604,6 +2620,10 @@ export class Lowerer { * by class name (cycle-initialization.ts). */ readonly classInitFlags = new Map(); readonly cycleInitDeclarations = new Map(); + /** CommonJS require declarations whose bindings code running before the + * require can read, and those references (require-initialization.ts). */ + readonly requireInitFlags = new Map(); + readonly requireEarlyReferences: EarlyReference[] = []; get ctx(): FnCtx { const top = this.fnStack[this.fnStack.length - 1]; @@ -3444,6 +3464,7 @@ export class Lowerer { return null; const directSpec = requireSpecOf(decl.initializer); if (directSpec !== null) { + if (isJsonRequireDecl(this, decl)) return null; return ( resolveImport(this.program, decl.getSourceFile(), directSpec, "require") ?? npmStaticDepSf7(this.program, decl.getSourceFile(), directSpec, "require") @@ -3847,6 +3868,62 @@ export class Lowerer { * TypeScript's default indexed-access type is a useful source annotation, * but it is not a runtime proof; promoting only the affected slots keeps * the rest of the dense ABI unchanged. */ + /** A Date's `toJSON()` answers null for an invalid date while the lib + * types say `string`: unannotated bindings initialized by such a call, + * and object-literal fields holding one, keep `string | null`. */ + analyzeDateJsonResults(parts: FileParts[]): void { + const nullable = dateJsonType(this); + for (const { sf } of parts) { + if (!sf.text.includes("toJSON")) continue; + ts.walkPreorder(sf, (node) => { + // `x = d.toJSON()`: the checker narrows x to string after the + // assignment, so its reads must stay checked. + if ( + ts.isBinaryExpression(node) && + node.operatorToken.kind === ts.SyntaxKind.EqualsToken && + ts.isIdentifier(node.left) && + isDateJsonSource(this, node.right) + ) { + const symbol = this.checker.getSymbolAtLocation(node.left); + if (symbol) this.dateJsonBindings.add(symbol); + return undefined; + } + if (!ts.isVariableDeclaration(node) || !node.initializer) return undefined; + if (!ts.isIdentifier(node.name)) return undefined; + const symbol = this.checker.getSymbolAtLocation(node.name); + if (!symbol) return undefined; + let init: ts.Expression = node.initializer; + while (ts.isParenthesizedExpression(init)) init = init.expression; + if (isDateJsonSource(this, init)) { + this.dateJsonBindings.add(symbol); + // An annotation keeps its declared storage. + if (!node.type) this.runtimeOptionalBindingTypes.set(symbol, nullable); + return undefined; + } + if (node.type) return undefined; + if (!ts.isObjectLiteralExpression(init)) return undefined; + const declared = this.mapTypeOf(this.typeOf(node.name)); + if (declared?.kind !== "record") return undefined; + let widened: IrType = declared; + for (const property of init.properties) { + const value = ts.isPropertyAssignment(property) + ? property.initializer + : ts.isShorthandPropertyAssignment(property) + ? property.name + : null; + const name = + property.name && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) + ? property.name.text + : null; + if (value && name !== null && isDateJsonSource(this, value)) + widened = this.runtimeOptionalRecordField(widened, name, nullable); + } + if (widened !== declared) this.runtimeOptionalBindingTypes.set(symbol, widened); + return undefined; + }); + } + } + analyzeRuntimeOptionalArrayReads(parts: FileParts[]): void { const provenance = RuntimeOptionalProvenance.fromEnvironment(); // The site whose value the current promotion follows (debug only). @@ -3871,6 +3948,18 @@ export class Lowerer { if (!s) return null; return s.flags & ts.SymbolFlags.Alias ? this.checker.getAliasedSymbol(s) : s; }; + // Fields that can be read before their assignment hold undefined then; + // their reads are runtime-optional values like missing array reads. + for (const info of this.classes.values()) { + if (!info.earlyReadFields || !info.decl?.name) continue; + const classSymbol = symbolOf(info.decl.name); + if (!classSymbol) continue; + const instance = this.checker.getDeclaredTypeOfSymbol(classSymbol); + for (const name of info.earlyReadFields) { + const property = this.checker.getPropertyOfType(instance, name); + if (property) optionalSymbols.add(property); + } + } const signatureOf = (node: ts.Identifier): FnSig | null => { const symbol = symbolOf(node); if (!symbol) return null; @@ -5996,6 +6085,7 @@ export class Lowerer { } this.collectProgram(parts); this.analyzeRuntimeOptionalArrayReads(parts); + this.analyzeDateJsonResults(parts); // Decorated classes analyze AFTER the whole collection pass: a // decorator's return type may name a subclass declared below the // class, and reference lowering needs each class's rebindability @@ -6226,6 +6316,7 @@ export class Lowerer { // slots), uniformly across params/locals/globals/fields/body types so // every producer and consumer agrees. Programs with no unregistered // reference are untouched — byte-stability holds. + checkRequireBindingAccesses(this, functions); const module: IrModule | null = this.diags.length > 0 ? null @@ -6380,6 +6471,7 @@ export class Lowerer { }; this.collectProgram(parts); this.analyzeRuntimeOptionalArrayReads(parts); + this.analyzeDateJsonResults(parts); // Decorated classes analyze post-collection here too: the %init seeds // lower the decoration calls, whose edges (decorator bodies, construct // thunks) reachable emit must see. @@ -9983,6 +10075,22 @@ export class Lowerer { const pickedT = shape.bodyType; srcType = pickedT; srcRef = () => ({ kind: "varRef", localId: src.id, type: pickedT, loc }); + } else { + // `({ groups }) => ...` over matchAll rows. + const groupsSource = matchGroupsPatternSource( + this, + decl.name, + srcRef(), + () => matchCallbackParamGroupsOf(this, decl), + loc, + ); + if (groupsSource !== null) { + const src = this.declareHiddenLocal("%psrc", groupsSource.type); + prologue.push({ kind: "varDecl", localId: src.id, init: groupsSource, loc }); + const groupsT = groupsSource.type; + srcType = groupsT; + srcRef = () => ({ kind: "varRef", localId: src.id, type: groupsT, loc }); + } } this.lowerBindingPattern(decl.name, srcRef, srcType, true, prologue); return; @@ -11063,6 +11171,7 @@ export class Lowerer { lowerLifetimeRegionNew(this, expr) ?? lowerInternTableNew(this, expr) ?? lowerAbortControllerNew(this, expr) ?? + lowerDisposableStackNew(this, expr) ?? lowerEventTargetNew(this, expr) ?? lowerFetchWebNew(this, expr) ?? lowerResponseNew(this, expr) ?? @@ -11382,9 +11491,19 @@ export class Lowerer { return pathModuleValue(this, `path/${bi.member}`, loc); } // events.defaultMaxListeners READS the process-wide default (its - // write twin routes through emitter.setDefaultMaxChk). + // write twin routes through emitter.setDefaultMaxChk). Through an ES + // module namespace it reads the namespace's copy instead. if (bi.module === "events" && bi.member === "defaultMaxListeners") { - return { kind: "libCall", fn: "emitter.getDefaultMax", args: [], type: F64, loc }; + const esm = + ts.isIdentifier(expr.expression) && + esmImportBindingKind(this, expr.expression) === "namespace"; + return { + kind: "libCall", + fn: esm ? "emitter.esmDefaultMax" : "emitter.getDefaultMax", + args: [], + type: F64, + loc, + }; } if (bi.module === "worker_threads") { const value = lowerWorkerMetadata(bi.member, loc); @@ -11629,8 +11748,8 @@ export class Lowerer { return lowerSpawnCall(this, expr, loc); } - lowerExecFileCall(expr: ts.CallExpression, loc: SrcLoc): IrExpr { - return lowerExecFileCall(this, expr, loc); + lowerExecFileCall(expr: ts.CallExpression, loc: SrcLoc, shell = false): IrExpr { + return lowerExecFileCall(this, expr, loc, shell); } lowerExecSyncCall(expr: ts.CallExpression, shell: boolean, loc: SrcLoc): IrExpr { diff --git a/packages/compiler/src/frontend/lowering/nan-coded-reads.ts b/packages/compiler/src/frontend/lowering/nan-coded-reads.ts index 2a537d6de2..f7023cc52d 100644 --- a/packages/compiler/src/frontend/lowering/nan-coded-reads.ts +++ b/packages/compiler/src/frontend/lowering/nan-coded-reads.ts @@ -191,6 +191,7 @@ const TYPED_ARRAYS = new Set([ "Uint16Array", "Int32Array", "Uint32Array", + "Float16Array", "Float32Array", "Float64Array", ]); diff --git a/packages/compiler/src/frontend/lowering/narrowing-stability.ts b/packages/compiler/src/frontend/lowering/narrowing-stability.ts index 9c7850ceb4..4ae8eec675 100644 --- a/packages/compiler/src/frontend/lowering/narrowing-stability.ts +++ b/packages/compiler/src/frontend/lowering/narrowing-stability.ts @@ -834,6 +834,7 @@ const INERT_CONSTRUCTORS = new Set([ "Int16Array", "Uint32Array", "Int32Array", + "Float16Array", "Float32Array", "Float64Array", "BigInt64Array", diff --git a/packages/compiler/src/frontend/lowering/require-initialization.ts b/packages/compiler/src/frontend/lowering/require-initialization.ts new file mode 100644 index 0000000000..5375f3792e --- /dev/null +++ b/packages/compiler/src/frontend/lowering/require-initialization.ts @@ -0,0 +1,262 @@ +/* CommonJS require bindings read before their require ran. A top-level + * `const x = require("./dep")` binds x AT the require: Node throws "Cannot + * access 'x' before initialization" when code that runs earlier (a hoisted + * function called above the require, say) reads it. The lowering keeps + * require bindings as module aliases, so the reads themselves cannot + * observe the order. When preflight finds code that can reach a binding + * before its require (requireEarlyReference), the declaration gets a BOOL + * initialization flag, set right after the require evaluated the module, + * and every reference that can run earlier checks it first — the same + * module.tdzCheck the import-cycle bindings use. A reference whose lowering + * leaves no expression to check keeps the SC1013 fence. */ +import * as ts from "../ts7/adapter.js"; +import type { Lowerer } from "./lowerer.js"; +import { + inDeferredPosition, + isCjsJsFile, + locOf, + requireEarlyReference, + requireSpecOf, +} from "../program.js"; +import { unsupportedDiag } from "../../diagnostics/diagnostic.js"; +import { + BOOL, + STRING, + VOID, + type IrExpr, + type IrFunction, + type IrStmt, + type SrcLoc, +} from "../../ir/ir.js"; +import { mapExprChildren, mapStmtChildren } from "../../ir/traverse.js"; + +/** One flagged require declaration. */ +export interface RequireInitFlag { + flagId: string; + /** The binding names, by symbol. */ + names: Map; +} + +/** A reference that can run before its require: the identifier, the + * outermost expression whose evaluation starts with reading it, and the + * body of the innermost function holding it (null at the top level). */ +export interface EarlyReference { + flag: RequireInitFlag; + name: string; + ref: SrcLoc; + chain: SrcLoc; + body: SrcLoc | null; + node: ts.Identifier; +} + +function boundNames(name: ts.BindingName): ts.Identifier[] { + if (ts.isIdentifier(name)) return [name]; + const out: ts.Identifier[] = []; + for (const el of name.elements) { + if (ts.isOmittedExpression(el) || el.name === undefined) continue; + out.push(...boundNames(el.name)); + } + return out; +} + +/** The outermost node whose evaluation begins by evaluating `ref`: member + * reads, calls and constructions through it, and the transparent wrappers + * around them. */ +function chainOf(ref: ts.Identifier): ts.Node { + let node: ts.Node = ref; + for (;;) { + const parent = node.parent; + if (parent === undefined) return node; + if ( + ((ts.isPropertyAccessExpression(parent) || + ts.isElementAccessExpression(parent) || + ts.isCallExpression(parent) || + ts.isNewExpression(parent)) && + parent.expression === node) || + (ts.isTaggedTemplateExpression(parent) && parent.tag === node) || + ts.isParenthesizedExpression(parent) || + ts.isNonNullExpression(parent) || + (ts.isAsExpression(parent) && parent.expression === node) || + (ts.isSatisfiesExpression(parent) && parent.expression === node) + ) { + node = parent; + continue; + } + return node; + } +} + +function enclosingBody(node: ts.Node): ts.Node | null { + for (let p = node.parent; p !== undefined && !ts.isSourceFile(p); p = p.parent) { + if (ts.isFunctionLike(p)) { + const body = (p as { body?: ts.Node }).body; + if (body !== undefined) return body; + } + if (ts.isPropertyDeclaration(p) && p.initializer !== undefined) return p.initializer; + } + return null; +} + +/** Registers a flag for every top-level const/let require declaration of a + * CommonJS module whose bindings earlier code can reach, and collects the + * references that can run before the require. Runs once per lowering pass, + * after global collection. */ +export function markRequireEarlyBindings(lowerer: Lowerer): void { + for (const sf of lowerer.moduleOrder) { + if (sf.isDeclarationFile || !isCjsJsFile(sf, lowerer.program)) continue; + const declared: { decl: ts.VariableDeclaration; stmt: ts.VariableStatement }[] = []; + for (const stmt of sf.statements) { + if (!ts.isVariableStatement(stmt)) continue; + if ((stmt.declarationList.flags & (ts.NodeFlags.Const | ts.NodeFlags.Let)) === 0) continue; + for (const decl of stmt.declarationList.declarations) { + if (decl.initializer === undefined || requireSpecOf(decl.initializer) === null) continue; + if (requireEarlyReference(lowerer.program, decl) === null) continue; + declared.push({ decl, stmt }); + } + } + if (declared.length === 0) continue; + const rawTag = lowerer.fileTag.get(sf) ?? ""; + const tag = rawTag === "" ? "e." : rawTag.replace(/^%/, ""); + const bySymbol = new Map(); + for (const { decl } of declared) { + const flagId = `%g.${tag}%required.${decl.getStart(sf)}`; + lowerer.globalsList.push({ id: flagId, name: "%initialized", type: BOOL, mutable: true }); + const flag: RequireInitFlag = { flagId, names: new Map() }; + for (const id of boundNames(decl.name)) { + const symbol = lowerer.checker.getSymbolAtLocation(id); + if (symbol === undefined) continue; + flag.names.set(symbol, id.text); + bySymbol.set(symbol, { flag, decl }); + } + lowerer.requireInitFlags.set(decl, flag); + } + const names = new Set(); + for (const { flag } of bySymbol.values()) for (const n of flag.names.values()) names.add(n); + const idents: ts.Identifier[] = []; + ts.walkPreorder(sf, (node) => { + if (ts.isIdentifier(node) && names.has(node.text)) idents.push(node); + return undefined; + }); + lowerer.checker.prefetchSymbolNodesExact(idents); + for (const id of idents) { + const parent = id.parent; + const symbol = + parent !== undefined && ts.isShorthandPropertyAssignment(parent) && parent.name === id + ? lowerer.checker.getShorthandAssignmentValueSymbol(parent) + : lowerer.checker.getSymbolAtLocation(id); + const owner = symbol === undefined ? undefined : bySymbol.get(symbol); + if (owner === undefined) continue; + if (boundNames(owner.decl.name).includes(id)) continue; + // Code after the require runs after it, except inside functions. + if (id.getStart(sf) >= owner.decl.getEnd() && !inDeferredPosition(id)) continue; + const body = enclosingBody(id); + lowerer.requireEarlyReferences.push({ + flag: owner.flag, + name: owner.flag.names.get(symbol!)!, + ref: locOf(id), + chain: locOf(chainOf(id)), + body: body === null ? null : locOf(body), + node: id, + }); + } + } +} + +/** The statement marking a flagged require declaration initialized, placed + * right after its require evaluated the module. */ +export function requireInitFlagStmt(lowerer: Lowerer, decl: ts.VariableDeclaration): IrStmt | null { + const flag = lowerer.requireInitFlags.get(decl); + if (flag === undefined) return null; + const loc = locOf(decl); + return { + kind: "assign", + localId: flag.flagId, + value: { kind: "boolLit", value: true, type: BOOL, loc }, + loc, + }; +} + +const within = (inner: SrcLoc, outer: SrcLoc): boolean => + inner.file === outer.file && outer.start <= inner.start && inner.end <= outer.end; + +/** Inserts the initialization check before the outermost expression built + * for each early reference, and refuses a reference whose lowered code + * holds no expression for it. */ +export function checkRequireBindingAccesses(lowerer: Lowerer, functions: IrFunction[]): void { + const refs = lowerer.requireEarlyReferences; + if (refs.length === 0) return; + const byFile = new Map(); + for (const r of refs) { + const list = byFile.get(r.ref.file); + if (list) list.push(r); + else byFile.set(r.ref.file, [r]); + } + const covered = new Set(); + const loweredBodies = new Set(); + const claimed = new Set(); + const check = (r: EarlyReference, loc: SrcLoc): IrStmt => ({ + kind: "exprStmt", + expr: { + kind: "intrinsic", + name: "module.tdzCheck", + args: [ + { kind: "varRef", localId: r.flag.flagId, type: BOOL, loc }, + { kind: "strLit", value: r.name, type: STRING, loc }, + ], + type: VOID, + loc, + }, + loc, + }); + const note = (loc: SrcLoc | undefined): EarlyReference[] => { + if (loc === undefined) return []; + const list = byFile.get(loc.file); + if (list === undefined) return []; + const mine: EarlyReference[] = []; + for (const r of list) { + if (r.body !== null && !loweredBodies.has(r) && within(loc, r.body)) loweredBodies.add(r); + if (!claimed.has(r) && within(r.ref, loc) && within(loc, r.chain)) mine.push(r); + } + return mine; + }; + const rewriteExpr = (e: IrExpr): IrExpr => { + const mine = note(e.loc); + for (const r of mine) claimed.add(r); + const out = mapExprChildren(e, rewriteExpr, rewriteStmt); + for (const r of mine) { + claimed.delete(r); + covered.add(r); + } + if (mine.length === 0) return out; + const flags = new Set(); + const stmts: IrStmt[] = []; + for (const r of mine) { + if (flags.has(r.flag.flagId)) continue; + flags.add(r.flag.flagId); + stmts.push(check(r, e.loc)); + } + return { kind: "seqExpr", stmts, result: out, type: out.type, loc: out.loc }; + }; + const rewriteStmt = (s: IrStmt): IrStmt => { + note(s.loc); + return mapStmtChildren(s, rewriteExpr, rewriteStmt); + }; + for (const fn of functions) fn.body = fn.body.map(rewriteStmt); + if (lowerer.remainder) return; + const reported = new Set(); + for (const r of refs) { + if (covered.has(r)) continue; + // A function body that never lowered never runs. + if (r.body !== null && !loweredBodies.has(r)) continue; + const key = `${r.ref.file}:${r.ref.start}`; + if (reported.has(key)) continue; + reported.add(key); + lowerer.pushDiag( + unsupportedDiag( + "SC1013", + r.ref, + `reading require() binding '${r.name}' where it can run before the require (this use has no checked form for Node's ReferenceError — move the require above the code that can run first)`, + ), + ); + } +} diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 4c74e8b6a0..d50fc8be3b 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -664,6 +664,7 @@ export const STDLIB_SURFACE = { cosh: ISL_N1, exp: ISL_N1, expm1: ISL_N1, + f16round: ISL_N1, fround: ISL_N1, hypot: ISL_N2, imul: ISL_N2, @@ -751,6 +752,7 @@ export const STATIC_MATH_FNS: Record, sites?: Map, + overlayDeps?: Map>, ): string[] { // package → the resolved types file AND the file whose import found it: // the runtime-JS probe below must resolve from the SAME importing file, @@ -174,6 +179,15 @@ function detectAutoPackages( continue; } if (judged?.has(npm.packageName)) continue; + // --sources: an overlay's untyped dependency compiles from its + // published JS (overlayUntypedImport). + const overlayKey = overlayUntypedImport(sf.fileName, npm.typesFile); + if (overlayKey !== null) { + const keys = overlayDeps?.get(npm.packageName) ?? new Set(); + keys.add(overlayKey); + overlayDeps?.set(npm.packageName, keys); + continue; + } if (!seen.has(npm.packageName)) { seen.set(npm.packageName, { typesFile: npm.typesFile, fromFile: sf.fileName, spec }); sites?.set(npm.packageName, loc); @@ -181,6 +195,13 @@ function detectAutoPackages( } } const chosen: string[] = []; + if (overlayDeps !== undefined) { + for (const pkg of overlayDeps.keys()) { + if (judged?.has(pkg)) continue; + judged?.add(pkg); + chosen.push(pkg); + } + } for (const [pkg, { typesFile, fromFile, spec }] of seen) { judged?.add(pkg); // A package without a root entry (no "main", "exports" or index file) @@ -221,6 +242,74 @@ function packagesNamedByDiag(message: string, optedIn: ReadonlySet): Set return hits; } +/** --sources: the overlay key when `fromFile` is an active overlay file + * whose bare import resolves to a package's JavaScript (no declarations + * anywhere: upstream's @types package is a devDependency, never + * installed). The package compiles from its published JS, typed by + * inference, exactly as it does under the overlay package's own published + * JS — the declaration-surface eligibility of auto selection does not + * apply, the compilation checks do. Null otherwise and without --sources. */ +function overlayUntypedImport(fromFile: string, typesFile: string): string | null { + if (!isJsSourceFileName(typesFile) || !isSourceOverlayFile(fromFile)) return null; + return sourceOverlayOfFile(fromFile)!.key; +} + +/** --sources: the JavaScript packages an overlay's untyped dependencies + * import in turn (import declarations and top-level requires in their + * files the checker loaded — the module order may stop short when the + * program has preflight errors), not yet requested. They compile from + * their published JS with the dependency, as they do under the overlay + * package's published JS; each is attributed to the same overlays. */ +function overlayDependencyClosure( + load: LoadResult, + overlayDeps: Map>, + requested: readonly string[], + judged: Set, +): string[] { + const grown: string[] = []; + for (const sf of load.program.getImplementationSourceFiles()) { + const owner = npmPackageNameOf(sf.fileName); + const keys = owner === null ? undefined : overlayDeps.get(owner); + if (keys === undefined) continue; + const specs: string[] = []; + for (const stmt of sf.statements) { + if ( + (isImportDeclaration(stmt) || isExportDeclaration(stmt)) && + stmt.moduleSpecifier && + isStringLiteral(stmt.moduleSpecifier) + ) { + specs.push(stmt.moduleSpecifier.text); + } + for (const req of requiresOf(stmt)) specs.push(req.spec); + } + for (const spec of specs) { + if (isRelativeSpecifier(spec) || spec.startsWith("node:") || spec.startsWith("#")) continue; + if (canonicalBuiltinModule(spec) !== null) continue; + const js = resolveBareModule(sf.fileName, spec, "js-only"); + if (js === null || !isJsSourceFileName(js.typesFile)) continue; + const pkg = js.packageName; + if (pkg === owner || requested.includes(pkg) || grown.includes(pkg)) continue; + const deps = overlayDeps.get(pkg) ?? new Set(); + for (const key of keys) deps.add(key); + overlayDeps.set(pkg, deps); + judged.add(pkg); + grown.push(pkg); + } + } + return grown; +} + +/** True for the checker's implicit-any diagnostic (TS7016) at an overlay's + * untyped import: the scout program runs before the dependency's JS joins + * the checked program. */ +function untypedOverlayImportDiag(d: ScrDiagnostic, text: string | undefined): boolean { + if (text === undefined || !d.message.startsWith("Could not find a declaration file for module")) + return false; + const spec = text.slice(d.loc.start, d.loc.end).replace(/^['"]|['"]$/g, ""); + const npm = resolveNpmImport(d.loc.file, spec); + return npm !== null && overlayUntypedImport(d.loc.file, npm.typesFile) !== null; +} + /** Where a diagnostic sits, relative to its overlay ("src/x.ts:12"). */ function overlaySite( file: string, @@ -245,6 +334,7 @@ function overlaySite( function sourceOverlayCulprits( load: LoadResult, preflight: readonly ScrDiagnostic[], + scout = false, ): Map { const culprits = takeSourceOverlayProblems(); if (!sourceOverlaysActive()) return culprits; @@ -257,6 +347,7 @@ function sourceOverlayCulprits( for (const d of preflight) { const owner = sourceOverlayOfFile(d.loc.file); if (owner !== null && isSourceOverlayFile(d.loc.file)) { + if (scout && untypedOverlayImportDiag(d, texts.get(d.loc.file))) continue; const site = overlaySite(d.loc.file, owner.overlayDir, texts.get(d.loc.file), d.loc.start); add(owner.key, `${d.code} at ${site}: ${d.message}`); continue; @@ -492,6 +583,8 @@ function loadFrontend( // Explicit library attempts bypass only the auto-selection heuristics. // The shared preflight/lowering refusal and dependency-closure checks remain. const judged = new Set(libraryNpmStatic); + // --sources: overlays' untyped dependencies → the overlays importing them. + const overlayDeps = new Map>(); let requested: string[] = []; let reusableScout: ReturnType | null = null; let reusablePreflight: ScrDiagnostic[] | null = null; @@ -503,12 +596,15 @@ function loadFrontend( const scoutPreflight = checkPreflight(scout); // --sources: overlays that already fail here fall back before any // package selection depends on them. - overlayCulprits = sourceOverlayCulprits(scout, scoutPreflight); + overlayCulprits = sourceOverlayCulprits(scout, scoutPreflight, true); if (overlayCulprits.size === 0) { requested = npmStatic === "lib" - ? [...libraryNpmStatic, ...detectAutoPackages(scout, statuses, "lib", judged, npmSites)] - : detectAutoPackages(scout, statuses); + ? [ + ...libraryNpmStatic, + ...detectAutoPackages(scout, statuses, "lib", judged, npmSites, overlayDeps), + ] + : detectAutoPackages(scout, statuses, "auto", undefined, undefined, overlayDeps); // With no package to opt in, the scout already IS the final frontend: // same roots, resolution posture, preflight, and module order. Retain it // instead of spawning a second tsgo server and checking the whole graph @@ -561,6 +657,18 @@ function loadFrontend( // and library builds with a package-specific diagnostic. let load = reusableScout ?? loadProgram(entryPath, { npmStatic: requested, externalTypes }); let preflight = reusablePreflight ?? checkPreflight(load); + // --sources: an overlay's untyped dependencies bring their own JS + // dependencies along (bounded: every round adds at least one package). + if (overlayDeps.size > 0) { + for (;;) { + const grown = overlayDependencyClosure(load, overlayDeps, requested, judged); + if (grown.length === 0) break; + requested = [...requested, ...grown]; + load.dispose(); + load = loadProgram(entryPath, { npmStatic: requested, externalTypes }); + preflight = checkPreflight(load); + } + } // Library mode's fixpoint: the opted-in packages' files joined the // program just now, and THEIR bare edges (import statements and // top-level requires) name packages the scout could not see. Judge each @@ -570,7 +678,7 @@ function loadFrontend( // at least one new package for good). if (npmStatic === "lib") { for (;;) { - const grown = detectAutoPackages(load, statuses, "lib", judged, npmSites); + const grown = detectAutoPackages(load, statuses, "lib", judged, npmSites, overlayDeps); if (grown.length === 0) break; requested = [...requested, ...grown]; load.dispose(); @@ -680,7 +788,25 @@ function loadFrontend( // failures (a published CommonJS dependency's require() of an augmented // package, a preflight error only the full graph reaches). Drop those // instances and restart the whole selection without them. - const lateCulprits = sourceOverlayCulprits(load, preflight); + // An overlay's untyped dependency that does not compile from its + // published JS would fail the build the published package passes. That + // reason comes first: the checker's TS7016 at the import is its symptom. + const lateCulprits = new Map(); + for (const status of statuses) { + const keys = status.status === "refused" ? overlayDeps.get(status.package) : undefined; + if (keys === undefined) continue; + for (const key of keys) { + if (!lateCulprits.has(key)) { + lateCulprits.set( + key, + `its untyped dependency ${status.package} does not compile from its published JS (${status.detail ?? "refused"})`, + ); + } + } + } + for (const [key, reason] of sourceOverlayCulprits(load, preflight)) { + if (!lateCulprits.has(key)) lateCulprits.set(key, reason); + } if (lateCulprits.size > 0) { dropSourceOverlays(lateCulprits); load.dispose(); diff --git a/packages/compiler/src/frontend/program-analysis.ts b/packages/compiler/src/frontend/program-analysis.ts index 0ecf09e6a7..e49b9307e4 100644 --- a/packages/compiler/src/frontend/program-analysis.ts +++ b/packages/compiler/src/frontend/program-analysis.ts @@ -1,4 +1,9 @@ -import type { ExportDeclaration, SourceFile, VariableDeclaration } from "./ts7/ast-types.js"; +import type { + ExportDeclaration, + Identifier, + SourceFile, + VariableDeclaration, +} from "./ts7/ast-types.js"; import type { ModuleSourceCandidates } from "./module-source-candidates.js"; import type { CycleEarlyBinding } from "./program.js"; @@ -20,6 +25,11 @@ export class ProgramAnalysis { readonly exactCycleMembers = new Set(); cycleEarlyBindings: EarlyBindingsMemo | null = null; moduleEarlyBindings: EarlyBindingsMemo | null = null; + /** Top-level require declarations: the first reference to their bindings + * that code running before the require can reach (requireEarlyReference). */ + readonly requireEarlyReferences = new Map(); + /** Per file: the first top-level statement that can run user code. */ + readonly requireFirstRunnable = new Map(); clear(): void { this.createRequireReasons.clear(); @@ -29,5 +39,7 @@ export class ProgramAnalysis { this.exactCycleMembers.clear(); this.cycleEarlyBindings = null; this.moduleEarlyBindings = null; + this.requireEarlyReferences.clear(); + this.requireFirstRunnable.clear(); } } diff --git a/packages/compiler/src/frontend/program.test.ts b/packages/compiler/src/frontend/program.test.ts index 33abf6d679..8fec2f486f 100644 --- a/packages/compiler/src/frontend/program.test.ts +++ b/packages/compiler/src/frontend/program.test.ts @@ -126,10 +126,12 @@ test.for([ ["replacement export property", "module.exports = read; module.exports.name = read;"], ["non-export assignment chain", "const box = {}; exports.alias = box.read = read;"], ])("require still refuses an early read through %s", ([, early]) => { + // A var binding keeps the fence (Node reads undefined there); a const + // compiles with initialization checks instead, from the same analysis. const diagnostics = requireOrderDiagnostics(` exports.read = read; ${early} -const dep = require('./dep.cjs'); +var dep = require('./dep.cjs'); function read() { return dep.value; } `); expect( @@ -140,13 +142,43 @@ function read() { return dep.value; } test("publishing a require binding itself still reads it before initialization", () => { const diagnostics = requireOrderDiagnostics(` exports.read = read; -const { read } = require('./dep.cjs'); +var { read } = require('./dep.cjs'); `); expect( diagnostics.some((diag) => diag.code === "SC1013" && diag.message.includes("binding 'read'")), ).toBe(true); }); +test("an early-reachable const require binding compiles with initialization checks", () => { + // Preflight admits it; the lowering checks every reference that can run + // before the require (require-initialization.ts). + expect( + requireOrderDiagnostics(` +exports.read = read; +read(); +const dep = require('./dep.cjs'); +function read() { return dep.value; } +`), + ).toEqual([]); +}); + +test.for([ + ["a class whose methods read it", "class Reader { read() { return dep.value; } }"], + ["a stored class expression", "var Reader = class { read() { return dep.value; } };"], + ["a later-job callback", "setTimeout(() => dep.value, 0);"], + ["a published getter", "Object.defineProperty(exports, 'value', { get: () => dep.value });"], + ["a property stored on a published function", "module.exports = read; read.again = again;"], +])("code that cannot run before the require does not reach it: %s", ([, early]) => { + expect( + requireOrderDiagnostics(` +${early} +var dep = require('./dep.cjs'); +function read() { return dep.value; } +function again() { return dep.value; } +`), + ).toEqual([]); +}); + test.for([ "var Object = null; Object.defineProperty(exports, '__esModule', { value: true }); exports.read = read;", "var exports = null; exports.read = read;", @@ -154,7 +186,7 @@ test.for([ ])("source bindings cannot masquerade as a CommonJS prologue: %s", (prefix) => { const diagnostics = requireOrderDiagnostics(` ${prefix} -const dep = require('./dep.cjs'); +var dep = require('./dep.cjs'); function read() { return dep.value; } `); expect( @@ -165,7 +197,7 @@ function read() { return dep.value; } test("an earlier declarator can call an export before the require initializes", () => { const diagnostics = requireOrderDiagnostics(` exports.read = read; -const before = read(), dep = require('./dep.cjs'); +var before = read(), dep = require('./dep.cjs'); function read() { return dep.value; } `); expect( @@ -183,6 +215,63 @@ function read() { return dep.value; } ).toEqual([]); }); +test("a member added to a replaced export does not run the replacement (on-finished)", () => { + expect( + requireOrderDiagnostics(` +module.exports = read; +module.exports.check = check; +var hooks = probe(); +var dep = require('./dep.cjs'); +function read() { return dep.value; } +function check() { return 1; } +function probe() { return check(); } +`), + ).toEqual([]); +}); + +test("a require cycle whose modules export first and use each other only later is admitted", () => { + expect( + requireOrderDiagnostics( + ` +const ANY = Symbol('ANY'); +class Main { dep() { return new Dep(); } } +module.exports = Main; +const Dep = require('./dep.cjs'); +`, + "class Dep { main() { return new Main(); } }\nmodule.exports = Dep;\nconst Main = require('./main.cjs');\n", + ), + ).toEqual([]); +}); + +test.for([ + [ + "an export after the require", + "const Main = require('./main.cjs');\nmodule.exports = { Dep: 1 };\n", + ], + [ + "a top-level use of the cycle binding", + "module.exports = 1;\nconst Main = require('./main.cjs');\nnew Main();\n", + ], + [ + "an escaping export object", + "module.exports = 1;\nconst Main = require('./main.cjs');\nObject.assign(exports, {});\n", + ], + [ + "a call before the export", + "setup();\nmodule.exports = 1;\nconst Main = require('./main.cjs');\nfunction setup() {}\n", + ], +])("a require cycle keeps SC1016 for %s", ([, dependency]) => { + const diagnostics = requireOrderDiagnostics( + ` +class Main {} +module.exports = Main; +const dep = require('./dep.cjs'); +`, + dependency, + ); + expect(diagnostics.some((diag) => diag.code === "SC1016")).toBe(true); +}); + test("function publication does not admit CommonJS cycles", () => { const diagnostics = requireOrderDiagnostics( ` diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index 991ce23ee7..d13b5f914a 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -90,6 +90,10 @@ import type { NpmStaticOverloadSignature, } from "./npm-static-declaration-syntax.js"; import { cjsLexedExportsOfFile, cjsVisibleNames } from "./cjs-syntax.js"; +import { + activeSourceOverlayAmbientRoots, + sourceOverlayAssignsClassFields, +} from "./source-overlays.js"; import { ambientDtsPath, fallbackDtsPath, @@ -709,7 +713,15 @@ function loadProgram7( } options = { ...options, paths }; } - const coreRoots = [entryPath, ambientDtsPath(), nodeTypes ?? fallbackDtsPath()]; + // --sources: an active overlay's own ambient declarations (module + // augmentations its build includes without importing them) type its + // files as the package's build did. Empty without --sources. + const coreRoots = [ + entryPath, + ambientDtsPath(), + nodeTypes ?? fallbackDtsPath(), + ...activeSourceOverlayAmbientRoots(), + ]; const programRoots = [...coreRoots]; let program = host.createProgram([...programRoots, overridesDtsPath()], options); try { @@ -1315,6 +1327,510 @@ export function bindingEarlyUse7( return hit; } +/** The first reference to a top-level require declaration's bindings that + * code running before the require completes can reach, or null when no + * earlier code can run or reach one. Memoized per program: preflight + * decides the fence and the lowering places the initialization checks + * from the same answer. */ +function requireEarlyReference7( + program: ts.Program, + decl: ts.VariableDeclaration, +): ts.Identifier | null { + const memo = program.analysis.requireEarlyReferences; + const cached = memo.get(decl); + if (cached !== undefined) return cached; + let found: ts.Identifier | null = null; + const stmt = decl.parent?.parent; + const sf = decl.getSourceFile(); + if (stmt !== undefined && ts.isVariableStatement(stmt) && ts.isSourceFile(stmt.parent)) { + const stmts = sf.statements; + const k = stmts.indexOf(stmt); + const declarations = stmt.declarationList.declarations; + // Earlier declarators run too: `const x = read(), dep = require('./dep')`. + const precedingDecls = declarations.slice(0, declarations.indexOf(decl)); + let firstRunnable = program.analysis.requireFirstRunnable.get(sf); + if (firstRunnable === undefined) { + firstRunnable = stmts.findIndex((s) => !purePrefixStmt7(program, s)); + program.analysis.requireFirstRunnable.set(sf, firstRunnable); + } + const prefixCanRun = + (firstRunnable >= 0 && firstRunnable < k) || precedingDecls.some((d) => !purePrefixDecl7(d)); + if (prefixCanRun) found = requireBindingEarlyUse7(program, sf, k, decl, precedingDecls); + } + memo.set(decl, found); + return found; +} + +/** The CommonJS twin of bindingEarlyUse7 for a top-level require binding: + * the first reference to one of `decl`'s bindings that code running before + * the require completes can reach, or null. It follows Node's evaluation + * order instead of the conservative "every earlier statement" scan: + * - hoisted functions and classes, and the function and class values a + * var stored before the require, run only when reachable code names + * them; a class statement runs only its heritage, computed keys, + * decorators and static initializers; + * - callbacks handed to timers, microtasks and promise reactions run after + * the module body, hence after the require; + * - publishing a value (`module.exports = f`, `exports.n = f`, + * `Object.defineProperty(exports, "n", { get })`, `F.prototype.m = f`) + * runs nothing. The published value becomes reachable only when + * reachable code reads it back: an `exports.n` read reaches the values + * published as `n`, any other read of the export object reaches all of + * them, and naming `F` reaches what was stored on `F`. No other module + * can read this module's exports before its body completes: require + * cycles keep their SC1016 fence and a compiled module graph has no + * reflective exports object. + * Every other function-like met in reachable code counts as reachable + * because it may be handed to a callee. */ +export function requireBindingEarlyUse7( + program: ts.Program, + sf: ts.SourceFile, + k: number, + decl: ts.VariableDeclaration, + precedingDecls: readonly ts.VariableDeclaration[], +): ts.Identifier | null { + const checker = program.getTypeChecker(); + const bindings = new Set(); + for (const id of boundNames7(decl.name)) { + const sym = checker.getSymbolAtLocation(id); + if (sym) bindings.add(sym); + } + if (bindings.size === 0) return null; + const stmts = sf.statements; + const cjs = isCjsJsFile7(sf, program); + const symbolOf = (id: ts.Identifier): ts.Symbol | undefined => { + const parent = id.parent; + if (parent !== undefined && ts.isShorthandPropertyAssignment(parent) && parent.name === id) { + return checker.getShorthandAssignmentValueSymbol(parent); + } + return checker.getSymbolAtLocation(id); + }; + // Stored callables and the scans that skip them. + const units = new Map(); + const stored = new Set(); + const addUnit = (name: ts.Identifier, node: ts.Node): void => { + const sym = checker.getSymbolAtLocation(name); + if (sym === undefined) return; + stored.add(node); + const list = units.get(sym); + if (list) list.push(node); + else units.set(sym, [node]); + }; + const storedCallables = (e: ts.Expression, out: ts.Node[]): void => { + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (ts.isArrowFunction(e) || ts.isFunctionExpression(e) || ts.isClassExpression(e)) { + out.push(e); + } else if (ts.isObjectLiteralExpression(e)) { + for (const p of e.properties) { + if ( + (ts.isMethodDeclaration(p) || + ts.isGetAccessorDeclaration(p) || + ts.isSetAccessorDeclaration(p)) && + !ts.isComputedPropertyName(p.name) + ) { + out.push(p); + } else if (ts.isPropertyAssignment(p) && !ts.isComputedPropertyName(p.name)) { + storedCallables(p.initializer, out); + } + } + } + }; + stmts.forEach((s, i) => { + if ((ts.isFunctionDeclaration(s) || ts.isClassDeclaration(s)) && s.name !== undefined) { + addUnit(s.name, s); + } else if (i < k && ts.isVariableStatement(s)) { + for (const d of s.declarationList.declarations) { + if (!ts.isIdentifier(d.name) || d.initializer === undefined) continue; + const out: ts.Node[] = []; + storedCallables(d.initializer, out); + for (const node of out) addUnit(d.name, node); + } + } + }); + // Callbacks that only run as later jobs never run before the require: + // the timers, microtasks and promise reactions. (Event listeners can run + // synchronously from an emit, so they keep the conservative rule.) + const laterJob = (callee: ts.Expression): boolean => { + while (ts.isParenthesizedExpression(callee)) callee = callee.expression; + const name = ts.isIdentifier(callee) + ? callee.text + : ts.isPropertyAccessExpression(callee) + ? callee.name.text + : null; + return ( + name !== null && + (LATER_JOB_BUILTINS7.has(name) || name === "nextTick") && + laterJobCallee7(checker, callee) + ); + }; + if (!hasTopLevelAwait7(sf)) { + const callbacks = (e: ts.Expression): void => { + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (ts.isArrowFunction(e) || ts.isFunctionExpression(e)) stored.add(e); + }; + for (const s of stmts.slice(0, k)) { + ts.walkPreorder(s, (node) => { + if ((ts.isCallExpression(node) || ts.isNewExpression(node)) && laterJob(node.expression)) { + for (const arg of node.arguments ?? []) callbacks(arg); + } + return undefined; + }); + } + } + const isWrapperName = (e: ts.Node, name: string): e is ts.Identifier => + ts.isIdentifier(e) && e.text === name && !sourceBinding7(program, e); + /** `this` at the module's top level: the original export object. */ + const isModuleThis = (e: ts.Node): boolean => { + if (e.kind !== ts.SyntaxKind.ThisKeyword) return false; + for (let p = e.parent; p !== undefined && !ts.isSourceFile(p); p = p.parent) { + if (ts.isFunctionLike(p) && !ts.isArrowFunction(p)) return false; + if (ts.isClassExpression(p) || ts.isClassDeclaration(p)) return false; + } + return true; + }; + const isExportObject = (e: ts.Expression): boolean => + isWrapperName(e, "exports") || + isModuleThis(e) || + (isModuleExportsAccess(e) && isWrapperName(e.expression, "module")); + const replacesExports = stmts.some((st) => cjsExportAssignmentOf(st)?.kind === "table"); + // Published values: by export name ("*" for a whole replacement), and by + // the stored callable whose properties hold them. + const exportLinks = new Map(); + const ownerLinks = new Map(); + const reachedKeys = new Set(); + const writtenKeys = new Set(); + const reachedUnits = new Set(); + const work: ts.Node[] = []; + let hit: ts.Identifier | null = null; + const reachUnit = (node: ts.Node): void => { + if (reachedUnits.has(node)) return; + reachedUnits.add(node); + work.push(node); + }; + const reachSymbol = (sym: ts.Symbol): void => { + for (const node of units.get(sym) ?? []) reachUnit(node); + for (const node of ownerLinks.get(sym) ?? []) reachUnit(node); + }; + let allReached = false; + const reachKey = (key: string | null): void => { + if (key === null) { + allReached = true; + for (const list of exportLinks.values()) for (const node of list) reachUnit(node); + return; + } + reachedKeys.add(key); + for (const name of [key, "*"]) for (const node of exportLinks.get(name) ?? []) reachUnit(node); + }; + const keyReached = (key: string): boolean => + allReached || reachedKeys.has(key) || (key === "*" && reachedKeys.size > 0); + /** A read of the export object: which published names it can observe. */ + const exportRead = (node: ts.Expression): void => { + let use: ts.Node = node; + while (ts.isParenthesizedExpression(use.parent)) use = use.parent; + const parent = use.parent; + // A truthiness test reads no member (tsc's `this && this.__helper`). + if ( + ts.isBinaryExpression(parent) && + parent.left === use && + parent.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken + ) { + return; + } + if (ts.isTypeOfExpression(parent)) return; + if (ts.isPropertyAccessExpression(parent) && parent.expression === use) { + let target: ts.Node = parent; + while (ts.isParenthesizedExpression(target.parent)) target = target.parent; + const assignment = target.parent; + if ( + ts.isBinaryExpression(assignment) && + assignment.left === target && + assignment.operatorToken.kind === ts.SyntaxKind.EqualsToken + ) { + // A plain write reads no member. It can still run a setter: one + // published for that name (now or later), or a static one of a + // replacing class. + writtenKeys.add(parent.name.text); + for (const node of exportLinks.get(parent.name.text) ?? []) reachUnit(node); + for (const node of exportLinks.get("*") ?? []) { + if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) reachUnit(node); + } + return; + } + reachKey(parent.name.text); + return; + } + if ( + ts.isElementAccessExpression(parent) && + parent.expression === use && + ts.isStringLiteralLike(parent.argumentExpression) + ) { + reachKey(parent.argumentExpression.text); + return; + } + reachKey(null); + }; + const scan = (root: ts.Node): void => { + ts.walkPreorder(root, (n) => { + if (hit !== null) return "stop"; + if (n !== root && stored.has(n)) { + if (ts.isClassExpression(n) || ts.isClassDeclaration(n)) { + for (const part of classSyncParts7(n)) scan(part); + } + return "skip"; + } + // A self-require answers this module's partially built exports. + if (cjs && ts.isCallExpression(n)) { + const spec = requireSpecOf7(n); + if (spec !== null && resolveImport7(program, sf, spec, "require") === sf) reachKey(null); + } + if (cjs && n.kind === ts.SyntaxKind.ThisKeyword && isModuleThis(n)) { + exportRead(n as ts.Expression); + return undefined; + } + if (!ts.isIdentifier(n)) return undefined; + if (inTypePosition7(n)) return undefined; + const named = n.parent; + if ( + (ts.isPropertyAccessExpression(named) || + ts.isPropertyAssignment(named) || + ts.isMethodDeclaration(named) || + ts.isGetAccessorDeclaration(named) || + ts.isSetAccessorDeclaration(named) || + ts.isPropertyDeclaration(named)) && + named.name === n + ) { + return undefined; + } + // A declaration's own name reads nothing. + if (named !== undefined && declaredName7(named) === n) return undefined; + if (cjs && isWrapperName(n, "exports")) { + exportRead(n); + return undefined; + } + if (cjs && isWrapperName(n, "module")) { + const parent = n.parent; + if (ts.isPropertyAccessExpression(parent) && parent.expression === n) { + if (parent.name.text === "exports") exportRead(parent); + return "skip"; + } + reachKey(null); + return undefined; + } + const sym = symbolOf(n); + if (sym === undefined) return undefined; + if (bindings.has(sym)) { + hit = n; + return "stop"; + } + reachSymbol(sym); + return undefined; + }); + }; + /** Links a published value: evaluating it reads identifiers, but the + * callables it names run only once they are read back. */ + const publish = (value: ts.Expression, link: (node: ts.Node) => void): void => { + while (ts.isParenthesizedExpression(value)) value = value.expression; + if (ts.isIdentifier(value)) { + const sym = symbolOf(value); + if (sym !== undefined && bindings.has(sym)) { + hit = value; + return; + } + if (sym !== undefined) { + // What was stored on the value stays reachable through it. + for (const node of [...(units.get(sym) ?? []), ...(ownerLinks.get(sym) ?? [])]) link(node); + } + return; + } + if (ts.isFunctionExpression(value) || ts.isArrowFunction(value)) { + link(value); + return; + } + if (ts.isClassExpression(value)) { + for (const part of classSyncParts7(value)) scan(part); + link(value); + return; + } + if (ts.isObjectLiteralExpression(value)) { + for (const p of value.properties) { + if (ts.isShorthandPropertyAssignment(p)) { + const sym = checker.getShorthandAssignmentValueSymbol(p); + if (sym !== undefined && bindings.has(sym)) hit = p.name as ts.Identifier; + else if (sym !== undefined) { + for (const node of [...(units.get(sym) ?? []), ...(ownerLinks.get(sym) ?? [])]) { + link(node); + } + } + } else if (ts.isPropertyAssignment(p) && !ts.isComputedPropertyName(p.name)) { + publish(p.initializer, link); + } else if ( + (ts.isMethodDeclaration(p) || + ts.isGetAccessorDeclaration(p) || + ts.isSetAccessorDeclaration(p)) && + !ts.isComputedPropertyName(p.name) + ) { + link(p); + } else scan(p); + } + return; + } + if ( + ts.isStringLiteralLike(value) || + ts.isNumericLiteral(value) || + value.kind === ts.SyntaxKind.TrueKeyword || + value.kind === ts.SyntaxKind.FalseKeyword || + value.kind === ts.SyntaxKind.NullKeyword || + (ts.isVoidExpression(value) && ts.isNumericLiteral(value.expression)) + ) { + return; + } + scan(value); + // Whatever an arbitrary expression evaluates to is opaque: treat it as + // reachable through the target too (scan already reached its parts). + }; + /** Where a publication stores: an export name, or a stored callable. */ + type Target = { kind: "key"; key: string } | { kind: "owner"; owner: ts.Symbol }; + const targetOf = (left: ts.Expression): Target | null => { + if (isModuleExportsAccess(left) && isWrapperName(left.expression, "module")) { + return { kind: "key", key: "*" }; + } + const names: string[] = []; + let e: ts.Expression = left; + for (;;) { + if (ts.isPropertyAccessExpression(e) && !e.questionDotToken) { + if (isExportObject(e)) break; + names.unshift(e.name.text); + e = e.expression; + } else if ( + ts.isElementAccessExpression(e) && + !e.questionDotToken && + ts.isStringLiteralLike(e.argumentExpression) + ) { + names.unshift(e.argumentExpression.text); + e = e.expression; + } else break; + } + if (names.length === 0) return null; + if (isExportObject(e)) { + // __proto__ and the interop marker are not plain data slots, and a + // member of a replaced export may be a read-only function property: + // those writes keep the conservative scan. + if (names[0] === "__proto__" || names[0] === "__esModule" || replacesExports) return null; + return { kind: "key", key: names[0]! }; + } + if (ts.isIdentifier(e)) { + const sym = symbolOf(e); + if (sym !== undefined && units.has(sym) && !bindings.has(sym)) + return { kind: "owner", owner: sym }; + } + return null; + }; + const linkTo = (target: Target, node: ts.Node): void => { + if (target.kind === "key") { + const replacingClass = + target.key === "*" && + writtenKeys.size > 0 && + (ts.isClassDeclaration(node) || ts.isClassExpression(node)); + if (keyReached(target.key) || writtenKeys.has(target.key) || replacingClass) { + reachUnit(node); + } + const list = exportLinks.get(target.key); + if (list) list.push(node); + else exportLinks.set(target.key, [node]); + } else { + if ((units.get(target.owner) ?? []).some((u) => reachedUnits.has(u))) reachUnit(node); + if ((units.get(target.owner) ?? []).includes(node)) return; + const list = ownerLinks.get(target.owner); + if (list === undefined) ownerLinks.set(target.owner, [node]); + else if (!list.includes(node)) list.push(node); + } + }; + /** A top-level publication statement, handled in place; false for any + * other statement. */ + const publication = (s: ts.Statement): boolean => { + if (!cjs || !ts.isExpressionStatement(s)) return false; + const e = s.expression; + if (ts.isBinaryExpression(e) && e.operatorToken.kind === ts.SyntaxKind.EqualsToken) { + const targets: Target[] = []; + let value: ts.Expression = e; + while ( + ts.isBinaryExpression(value) && + value.operatorToken.kind === ts.SyntaxKind.EqualsToken + ) { + const target = targetOf(value.left); + if (target === null) return false; + targets.push(target); + value = value.right; + } + publish(value, (node) => { + for (const t of targets) linkTo(t, node); + }); + return true; + } + // Object.defineProperty(exports, "name", { get() {...}, value: f, ... }) + if ( + ts.isCallExpression(e) && + ts.isPropertyAccessExpression(e.expression) && + isWrapperName(e.expression.expression, "Object") && + e.expression.name.text === "defineProperty" && + e.arguments.length === 3 + ) { + const [recv, key, desc] = e.arguments as unknown as [ + ts.Expression, + ts.Expression, + ts.Expression, + ]; + if (!isExportObject(recv) || !ts.isStringLiteralLike(key)) return false; + if (!ts.isObjectLiteralExpression(desc)) return false; + const keyTarget: Target = { kind: "key", key: key.text }; + const link = (node: ts.Node): void => linkTo(keyTarget, node); + for (const p of desc.properties) { + if ( + (ts.isMethodDeclaration(p) || ts.isPropertyAssignment(p)) && + ts.isIdentifier(p.name) && + (p.name.text === "get" || p.name.text === "set") + ) { + if (ts.isMethodDeclaration(p)) link(p); + else publish(p.initializer, link); + } else if ( + ts.isPropertyAssignment(p) && + ts.isIdentifier(p.name) && + p.name.text === "value" + ) { + publish(p.initializer, link); + } else scan(p); + } + return true; + } + return false; + }; + const drain = (): void => { + while (hit === null && work.length > 0) { + const unit = work.pop()!; + checker.prefetchSymbolRoots([unit]); + scan(unit); + } + }; + const roots = stmts.slice(0, k); + checker.prefetchSymbolRoots([...roots, ...precedingDecls]); + for (const s of roots) { + if (hit !== null) break; + if (ts.isFunctionDeclaration(s)) continue; + if (ts.isClassDeclaration(s)) { + for (const part of classSyncParts7(s)) scan(part); + } else if (!publication(s)) { + scan(s); + } + drain(); + } + for (const d of precedingDecls) { + if (hit !== null) break; + scan(d); + drain(); + } + return hit; +} + /** Bare `require("...")` EXPRESSION STATEMENTS anywhere below the top * level of a file — inside top-level ifs/blocks/loops and inside function * bodies (the lazy-require idiom). No bindings, pure side-effect loads. */ @@ -2310,8 +2826,19 @@ export function makeCycleAdmission( bindingUses.set(sf, uses); return uses; }; + // The CommonJS cluster verdict, same keying (cjsCycleRefusal7). + const cjsVerdict = new Map(); return (importer: ts.SourceFile, e: CycleEdge): string | null => { - if (e.stmt === undefined) return "the cycle closes through a require() edge"; + if (e.stmt === undefined) { + if (!index.has(importer)) strongconnect(importer); + const comp = sccOf.get(e.dep); + if (comp === undefined || comp.length < 2 || !comp.includes(importer)) { + return "the cycle closes through a require() edge"; + } + if (!cjsVerdict.has(comp)) cjsVerdict.set(comp, cjsCycleRefusal7(program, comp, lineOf)); + const reason = cjsVerdict.get(comp) ?? null; + return reason === null ? null : `the cycle closes through a require() edge: ${reason}`; + } // Cheap per-edge admission: nothing readable binds through the edge. if (ts.isImportDeclaration(e.stmt)) { const clause = e.stmt.importClause; @@ -2357,6 +2884,211 @@ export function makeCycleAdmission( }; } +/** The reason a strongly-connected cluster of CommonJS modules cannot be + * initialized exactly through its require() cycle, or null when it can. + * Node answers a require of a module that is still evaluating with that + * module's exports object as it stands, while the compiled bindings alias + * the exporter's final declarations. The two agree when: + * - every member settles its exports before anything can enter the + * cycle: all of its export statements come before its first top-level + * statement that can run another module's code (a require, or anything + * but literals, plain reads, function and class values, and + * standard-library globals called with literal arguments), and every + * other use of its export object is a plain member read; and + * - no member's top-level code can reach its binding of another member, + * so no member runs another member's code while the cluster is still + * initializing. Each module then initializes exactly as it would + * outside the cycle, and its own require bindings keep the acyclic + * analysis (requireEarlyReference). + * Other cycles keep the SC1016 fence. */ +function cjsCycleRefusal7( + program: ts.Program, + comp: readonly ts.SourceFile[], + lineOf: (node: ts.Node) => string, +): string | null { + const members = new Set(comp); + const builtinRoot = (e: ts.Expression): boolean => { + while (ts.isPropertyAccessExpression(e)) e = e.expression; + return ts.isIdentifier(e) && !sourceBinding7(program, e); + }; + const literal = (e: ts.Node): boolean => + ts.isStringLiteralLike(e) || + ts.isNumericLiteral(e) || + ts.isRegularExpressionLiteral(e) || + e.kind === ts.SyntaxKind.TrueKeyword || + e.kind === ts.SyntaxKind.FalseKeyword || + e.kind === ts.SyntaxKind.NullKeyword; + /** An expression whose evaluation runs no user code and requires + * nothing: literals, plain reads, function and class values, literal + * containers of those, and standard-library globals called with + * literal arguments. */ + const inertExpr = (e: ts.Node): boolean => { + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (literal(e) || ts.isIdentifier(e)) return true; + if (ts.isFunctionExpression(e) || ts.isArrowFunction(e)) return true; + if (ts.isClassExpression(e)) return classSyncParts7(e).every(inertPart); + if (ts.isObjectLiteralExpression(e)) { + return e.properties.every((p) => + ts.isShorthandPropertyAssignment(p) + ? true + : ts.isPropertyAssignment(p) + ? !ts.isComputedPropertyName(p.name) && inertExpr(p.initializer) + : (ts.isMethodDeclaration(p) || + ts.isGetAccessorDeclaration(p) || + ts.isSetAccessorDeclaration(p)) && + !ts.isComputedPropertyName(p.name), + ); + } + if (ts.isArrayLiteralExpression(e)) { + return e.elements.every((el) => !ts.isSpreadElement(el) && inertExpr(el)); + } + if (ts.isPropertyAccessExpression(e)) return builtinRoot(e); + if (ts.isVoidExpression(e)) return literal(e.expression); + if (ts.isCallExpression(e) || ts.isNewExpression(e)) { + return ( + requireSpecOf7(e) === null && + builtinRoot(e.expression) && + (e.arguments ?? []).every(literal) + ); + } + return false; + }; + const inertPart = (part: ts.Node): boolean => { + if (ts.isHeritageClause(part)) return part.types.every((t) => inertExpr(t.expression)); + if (ts.isComputedPropertyName(part)) return inertExpr(part.expression); + return ts.isExpression(part) && inertExpr(part); + }; + const exportObject = (e: ts.Expression): boolean => + (ts.isIdentifier(e) && e.text === "exports" && !sourceBinding7(program, e)) || + (isModuleExportsAccess(e) && + ts.isIdentifier(e.expression) && + !sourceBinding7(program, e.expression)); + /** A top-level statement that only publishes inert values. */ + const exportStatement = (stmt: ts.Statement): boolean => { + if (!ts.isExpressionStatement(stmt)) return false; + if (isEsModuleStamp(stmt.expression)) return true; + const cjs = cjsExportAssignmentOf(stmt); + if (cjs !== null) { + let value: ts.Expression = cjs.expr; + while ( + ts.isBinaryExpression(value) && + value.operatorToken.kind === ts.SyntaxKind.EqualsToken + ) { + const left = value.left; + if ( + !isModuleExportsAccess(left) && + !(ts.isPropertyAccessExpression(left) && exportObject(left.expression)) + ) + return false; + value = value.right; + } + return inertExpr(value); + } + const call = stmt.expression; + return ( + ts.isCallExpression(call) && + ts.isPropertyAccessExpression(call.expression) && + ts.isIdentifier(call.expression.expression) && + call.expression.expression.text === "Object" && + !sourceBinding7(program, call.expression.expression) && + call.expression.name.text === "defineProperty" && + call.arguments.length === 3 && + exportObject(call.arguments[0]!) && + ts.isStringLiteralLike(call.arguments[1]!) && + inertExpr(call.arguments[2]!) + ); + }; + for (const m of comp) { + if (!isCjsJsFile7(m, program)) { + return `${m.fileName} is not a CommonJS module — require cycles are admitted between CommonJS modules only`; + } + const stmts = m.statements; + // The first statement that can run another module's code. + let firstEntry = stmts.length; + for (let i = 0; i < stmts.length; i++) { + const st = stmts[i]!; + const inert = + ts.isFunctionDeclaration(st) || + ts.isEmptyStatement(st) || + (ts.isExpressionStatement(st) && ts.isStringLiteral(st.expression)) || + (ts.isClassDeclaration(st) && classSyncParts7(st).every(inertPart)) || + (ts.isVariableStatement(st) && + st.declarationList.declarations.every( + (d) => + ts.isIdentifier(d.name) && (d.initializer === undefined || inertExpr(d.initializer)), + )) || + exportStatement(st); + if (!inert) { + firstEntry = i; + break; + } + } + // Every other use of the export object must be a plain member read: + // anything else can change what a module of the cycle observes. + let stray: ts.Node | null = null; + const settled = new Set(stmts.slice(0, firstEntry).filter(exportStatement)); + ts.walkPreorder(m, (n) => { + if (stray !== null) return "stop"; + if (ts.isStatement(n) && settled.has(n)) return "skip"; + let use: ts.Expression | null = null; + if (n.kind === ts.SyntaxKind.ThisKeyword) { + let moduleThis = true; + for (let p = n.parent; p !== undefined && !ts.isSourceFile(p); p = p.parent) { + if ( + (ts.isFunctionLike(p) && !ts.isArrowFunction(p)) || + ts.isClassDeclaration(p) || + ts.isClassExpression(p) + ) { + moduleThis = false; + break; + } + } + if (moduleThis) use = n as ts.Expression; + } else if ( + ts.isIdentifier(n) && + (n.text === "exports" || n.text === "module") && + !(ts.isPropertyAccessExpression(n.parent) && n.parent.name === n) && + !sourceBinding7(program, n) + ) { + if (n.text === "exports") use = n; + else if (!ts.isPropertyAccessExpression(n.parent) || n.parent.expression !== n) stray = n; + else if (n.parent.name.text === "exports") use = n.parent; + } + if (use === null) return undefined; + const parent = use.parent; + const read = + ts.isPropertyAccessExpression(parent) && + parent.expression === use && + !( + ts.isBinaryExpression(parent.parent) && + parent.parent.left === parent && + parent.parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && + parent.parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment + ) && + !(ts.isCallExpression(parent.parent) && parent.parent.expression === parent) && + !ts.isDeleteExpression(parent.parent); + if (!read) stray = use; + return undefined; + }); + if (stray !== null) { + return `${m.fileName} uses its exports at ${lineOf(stray)} outside the export statements that precede its first require, so a module of the cycle can observe them before they settle`; + } + // Bindings of other members must stay unreachable from top-level code. + for (const st of stmts) { + for (const req of requiresOf7(st)) { + if (req.decl === null) continue; + const dep = resolveImport7(program, m, req.spec, "require"); + if (dep === null || !members.has(dep)) continue; + const reached = requireBindingEarlyUse7(program, m, stmts.length, req.decl, []); + if (reached !== null) { + return `the top-level code of ${m.fileName} can use its binding '${reached.text}' of a module of the cycle (${lineOf(reached)}) while the cycle initializes`; + } + } + } + } + return null; +} + /* ── exact cyclic initialization ───────────────────────────────────────── * Cycles that fail the declaration-only rule still evaluate exactly like * Node when their shape is representable: the guarded %init calls already @@ -3509,6 +4241,53 @@ function preflight7(load: LoadResult): { : spec.split("/")[0]!; return isWorkspacePackageName(prefix); }; + /* --sources: a package built with `useDefineForClassFields: false` runs + * its parameter-property assignments BEFORE its field initializers (the + * class-field lowering follows the package's semantics), so a field + * initializer that reads a parameter property is not a use before + * initialization there — the package's own build accepts it. The + * checker runs with the program's define semantics, so TS2729 at exactly + * those reads (`this.p` where `p` is a parameter property of the + * enclosing class's constructor) is the define-semantics verdict and is + * suppressed. Every other TS2729 keeps tsc's voice. */ + const assignFieldsSuppressed = (p: ts.Program, d: ts.Diagnostic): boolean => { + if (d.code !== 2729 || d.fileName === undefined || d.pos === undefined) return false; + if (!sourceOverlayAssignsClassFields(d.fileName)) return false; + const sf = p.getSourceFile(d.fileName); + if (!sf) return false; + const name = sf.text.slice(d.pos, d.end); + const start = d.pos; + const end = d.end ?? d.pos; + let found = false; + const visit = (node: ts.Node): void => { + if (node.pos > start || node.end < end) return; + if (ts.isClassLikeDeclaration(node)) { + const ctor = node.members.find( + (m): m is ts.ConstructorDeclaration => ts.isConstructorDeclaration(m) && !!m.body, + ); + const paramProp = ctor?.parameters.some( + (param) => + ts.isIdentifier(param.name) && + param.name.text === name && + ts + .getModifiers(param) + ?.some( + (m) => + m.kind === ts.SyntaxKind.PublicKeyword || + m.kind === ts.SyntaxKind.PrivateKeyword || + m.kind === ts.SyntaxKind.ProtectedKeyword || + m.kind === ts.SyntaxKind.ReadonlyKeyword, + ), + ); + // The innermost class decides; a read inside a nested class + // refers to that class's members. + found = paramProp === true; + } + node.forEachChild(visit); + }; + sf.forEachChild(visit); + return found; + }; // A JavaScript file that TypeScript checks keeps the type errors that // protect its JSDoc types: a value of another type reaching a typed slot, // a call with the wrong arity, and a subclass member that does not @@ -3542,6 +4321,7 @@ function preflight7(load: LoadResult): { !inferredJsDiagnosticSuppressed(p, d) && !isNpmStaticSubclassArgument(p, d) && !workspaceImplicitAnySuppressed(p, d) && + !assignFieldsSuppressed(p, d) && !jsdocTypeSuppressed(p, d, commentDup), ); }; @@ -4052,10 +4832,37 @@ function preflight7(load: LoadResult): { { const jsRequireFile = isJsSourceFileName(sf.fileName); const stmts = sf.statements; - let firstRunnable = -1; - stmts.forEach((s, i) => { - if (firstRunnable < 0 && !purePrefixStmt7(program, s)) firstRunnable = i; - }); + // A binding that code running before the require can reach. Node + // initializes it AT the require: a const/let read earlier throws + // ReferenceError, which the lowering reproduces with an + // initialization check at every reference that can run early + // (require-initialization.ts). A var reads undefined there, which + // module aliases cannot represent; createRequire bindings have no + // checked form either. + const earlyRequireFence = ( + req: { decl: ts.VariableDeclaration | null }, + bindingKind: RequireCallBindingKind7, + loc: { file: string; start: number; end: number }, + ): boolean => { + const early = req.decl !== null ? requireEarlyReference7(program, req.decl) : null; + if ( + early === null || + (bindingKind !== "createRequire" && + ((req.decl!.parent?.flags ?? 0) & (ts.NodeFlags.Const | ts.NodeFlags.Let)) !== 0) + ) { + return false; + } + diags.push( + unsupportedDiag( + "SC1013", + loc, + bindingKind === "createRequire" + ? `createRequire() binding '${early.text}' below code that can reach it (Node throws ReferenceError on any earlier access, which is not modeled — move the require above the code that can run first)` + : `var require() binding '${early.text}' below code that can reach it (Node reads undefined before the require runs, which is not modeled — declare it with const or let, or move the require above the code that can run first)`, + ), + ); + return true; + }; for (let k = 0; k < stmts.length; k++) { const stmt = stmts[k]!; for (const req of requiresOf7(stmt)) { @@ -4075,24 +4882,62 @@ function preflight7(load: LoadResult): { if (load.externalTypes.has(req.spec)) { continue; } + // A JSON document: the require answers its cached parsed object + // (json-require.ts), an ordinary value the binding stores, so + // every destructuring form lowers as usual. No module edge. + const jsonDep = resolveImport7(program, sf, req.spec, "require"); + if (jsonDep !== null && jsonDep.fileName.endsWith(".json")) { + if (bindingKind === "createRequire") continue; + if (ts.isVariableStatement(stmt) && requiresOf7(stmt).length > 1) { + const others = requiresOf7(stmt).some((r) => { + const d = resolveImport7(program, sf, r.spec, "require"); + return d === null || !d.fileName.endsWith(".json"); + }); + if (others) { + diags.push( + unsupportedDiag( + "SC1012", + loc, + "require() of a JSON module declared together with other require() bindings", + ), + ); + continue; + } + } + earlyRequireFence(req, bindingKind, loc); + continue; + } if (req.decl && ts.isArrayBindingPattern(req.decl.name)) { diags.push(unsupportedDiag("SC1012", loc, "array-destructuring require() bindings")); continue; } if (req.decl && ts.isObjectBindingPattern(req.decl.name)) { - const bad = req.decl.name.elements.find( - (e) => - e.name === undefined || - !ts.isIdentifier(e.name) || - e.initializer !== undefined || - e.dotDotDotToken !== undefined, - ); - if (bad) { + // A nested pattern of a builtin binds members of the sub-module + // its key names (`const { types: { isPromise } } = + // require("util")` — util.types IS util/types). + const builtin = canonicalBuiltinModule(req.spec); + const plainElements = ( + pattern: ts.ObjectBindingPattern, + module: string | null, + ): boolean => + pattern.elements.every((e) => { + if (e.initializer !== undefined || e.dotDotDotToken !== undefined) return false; + if (e.name !== undefined && ts.isIdentifier(e.name)) return true; + if (module === null || e.name === undefined || !ts.isObjectBindingPattern(e.name)) + return false; + const key = e.propertyName; + if (key === undefined || !ts.isIdentifier(key)) return false; + const sub = canonicalBuiltinModule(`${module}/${key.text}`); + return sub !== null && plainElements(e.name, sub); + }); + if (!plainElements(req.decl.name, builtin)) { diags.push( unsupportedDiag( "SC1012", loc, - "require() destructuring with defaults, rest, or nested patterns", + builtin !== null + ? "require() destructuring with defaults, rest, or nested patterns other than builtin sub-modules" + : "require() destructuring with defaults, rest, or nested patterns of program modules (JSON documents and builtin sub-modules destructure fully)", ), ); continue; @@ -4135,38 +4980,7 @@ function preflight7(load: LoadResult): { continue; } } - if (dep && dep.fileName.endsWith(".json")) { - if (bindingKind === "createRequire") continue; - diags.push(unsupportedDiag("SC1012", loc, "require() of JSON modules")); - continue; - } - // Earlier declarators run too: `const x = read(), dep = - // require('./dep')` must not lose the guard merely because both - // initializers share one VariableStatement. - const precedingDecls = - req.decl && ts.isVariableStatement(stmt) - ? stmt.declarationList.declarations.slice( - 0, - stmt.declarationList.declarations.indexOf(req.decl), - ) - : []; - const prefixCanRun = - (firstRunnable >= 0 && firstRunnable < k) || - precedingDecls.some((decl) => !purePrefixDecl7(decl)); - const tdzName = - prefixCanRun && req.decl - ? bindingEarlyUse7(program, sf, k, req.decl, precedingDecls) - : null; - if (tdzName !== null) { - diags.push( - unsupportedDiag( - "SC1013", - loc, - `require() below code that can reach its binding '${tdzName}' (Node initializes the binding AT the require and throws ReferenceError on any earlier access, which is not modeled — move the require above the code that can run first)`, - ), - ); - continue; - } + if (earlyRequireFence(req, bindingKind, loc)) continue; if (dep) deps.push({ dep }); } } @@ -4194,11 +5008,9 @@ function preflight7(load: LoadResult): { } const projectDep = resolveImport7(program, sf, spec, "require"); if (projectDep !== null) { - if (projectDep.fileName.endsWith(".json")) { - if (bindingKind === "createRequire") continue; - diags.push(unsupportedDiag("SC1012", loc, "require() of JSON modules")); - continue; - } + // A bare require of a JSON document only parses it: nothing to + // evaluate (json-require.ts creates the cached object). + if (projectDep.fileName.endsWith(".json")) continue; deps.push({ dep: projectDep }); continue; } @@ -4933,6 +5745,8 @@ export { resolveImport7 as resolveImport, resolveNpmImport7 as resolveNpmImport, locOf7 as locOf, + requireEarlyReference7 as requireEarlyReference, + inDeferredPosition7 as inDeferredPosition, }; /** True for JavaScript source files (.js/.mjs/.cjs) — the files whose diff --git a/packages/compiler/src/frontend/source-overlays.test.ts b/packages/compiler/src/frontend/source-overlays.test.ts index 5dd2924679..1416392c8a 100644 --- a/packages/compiler/src/frontend/source-overlays.test.ts +++ b/packages/compiler/src/frontend/source-overlays.test.ts @@ -5,12 +5,14 @@ import { afterEach, beforeEach, expect, test } from "vitest"; import { normalizeCliArgs } from "../cli/usage.js"; import { clearResolveCaches, resolveSourceOverlayModule, sourceOverlayPaths } from "./resolve.js"; import { + activeSourceOverlayAmbientRoots, activeSourceOverlays, beginSourceOverlayCompile, dropSourceOverlay, findSourceManifest, installSourceOverlays, loadSourceManifest, + sourceOverlayAssignsClassFields, sourceOverlayFallbacks, takeSourceOverlayProblems, } from "./source-overlays.js"; @@ -114,6 +116,44 @@ test("names with several augmented instances start dropped", () => { ]); }); +test("reads the additive semantic fields: ambient declarations and assign-semantics class fields", () => { + const a = instance("a", "1.0.0"); + const overlay = a["overlayDir"] as string; + mkdirSync(join(overlay, "typings")); + writeFileSync(join(overlay, "typings/extra.d.ts"), "type Extra = string;\n"); + const b = instance("b", "1.0.0"); + const path = writeManifest([ + { + ...a, + ambientDeclarations: ["typings/extra.d.ts"], + semanticOptions: { useDefineForClassFields: false }, + }, + { ...b, semanticOptions: { useDefineForClassFields: true } }, + ]); + installSourceOverlays(loadSourceManifest(path)); + beginSourceOverlayCompile(); + expect(activeSourceOverlayAmbientRoots()).toEqual([join(overlay, "typings/extra.d.ts")]); + expect(sourceOverlayAssignsClassFields(join(overlay, "src/index.ts"))).toBe(true); + expect(sourceOverlayAssignsClassFields(join(b["overlayDir"] as string, "src/index.ts"))).toBe( + false, + ); + dropSourceOverlay(a["installedDir"] as string, "test"); + expect(activeSourceOverlayAmbientRoots()).toEqual([]); + expect(sourceOverlayAssignsClassFields(join(overlay, "src/index.ts"))).toBe(false); + + // Paths must be relative declaration files inside the overlay; a missing + // file skips the instance with a note. + const bad = (ambientDeclarations: unknown): string => + writeManifest([{ ...a, ambientDeclarations }]); + expect(() => loadSourceManifest(bad(["/abs.d.ts"]))).toThrow(/relative paths/); + expect(() => loadSourceManifest(bad(["src/index.ts"]))).toThrow(/not a declaration file/); + writeFileSync(join(root, "outside.d.ts"), ""); + expect(() => loadSourceManifest(bad(["../../../../outside.d.ts"]))).toThrow(/leaves the overlay/); + const missing = loadSourceManifest(bad(["typings/gone.d.ts"])); + expect(missing.instances).toEqual([]); + expect(missing.notes[0]).toMatch(/ambient declaration typings\/gone\.d\.ts does not exist/); +}); + test("--sources takes an optional inline value", () => { expect(normalizeCliArgs(["build", "--sources", "m.ts"])).toEqual(["build", "--sources=", "m.ts"]); expect(normalizeCliArgs(["--sources=x.json", "--", "--sources"])).toEqual([ diff --git a/packages/compiler/src/frontend/source-overlays.ts b/packages/compiler/src/frontend/source-overlays.ts index f40c2971c1..4ca1c142e1 100644 --- a/packages/compiler/src/frontend/source-overlays.ts +++ b/packages/compiler/src/frontend/source-overlays.ts @@ -35,6 +35,15 @@ export interface SourceOverlayInstance { /** Human-readable source origin for reports ("github.com/o/r @ abc123"), * or null when the producer did not say. */ origin: string | null; + /** The package's own ambient declaration files (realpaths inside + * overlayDir) that its build includes without importing them, e.g. + * module augmentations under `typings/`. They join the checker's roots + * while the instance is active. */ + ambientDeclarations?: string[]; + /** "assign" when the package was built with `useDefineForClassFields: + * false`: its class field initializers are assignments that run after + * the parameter properties (class-field lowering, preflight). */ + classFields?: "assign"; } /** A complete overlay set, ready to install for a compile. */ @@ -171,6 +180,26 @@ export function isSourceOverlayFile(file: string): boolean { return instance !== null && !state!.drops.has(instance.key); } +/** True when `file` belongs to an active overlay built with + * `useDefineForClassFields: false` — the gate for assign-semantics class + * fields. False without --sources. */ +export function sourceOverlayAssignsClassFields(file: string): boolean { + if (state === null) return false; + const instance = sourceOverlayOfFile(file); + return instance?.classFields === "assign" && !state.drops.has(instance.key); +} + +/** The active instances' ambient declaration files, for the checker's + * roots; empty without --sources. */ +export function activeSourceOverlayAmbientRoots(): string[] { + if (state === null) return []; + const roots: string[] = []; + for (const instance of activeSourceOverlays()) { + for (const file of instance.ambientDeclarations ?? []) roots.push(file); + } + return roots; +} + export function dropSourceOverlay(key: string, reason: string): void { if (state === null || !state.byKey.has(key) || state.drops.has(key)) return; state.drops.set(key, reason); @@ -279,6 +308,34 @@ function originOf(source: unknown): string | null { return `${s.repo}${commit}${subdir}`; } +/** An instance's `ambientDeclarations`: declaration files relative to its + * overlay directory, which must stay inside it. Returns realpaths, or the + * first entry that does not exist. */ +function ambientDeclarationsOf( + raw: unknown, + overlayDir: string, + bad: (what: string) => Error, +): string[] | { missing: string } { + if (raw === undefined) return []; + if (!Array.isArray(raw)) throw bad("must be an array of relative paths"); + const out: string[] = []; + for (const entry of raw) { + if (typeof entry !== "string" || entry === "" || isAbsolute(entry)) { + throw bad("must be an array of relative paths"); + } + if (!/\.d\.(?:ts|mts|cts)$/.test(entry)) throw bad(`entry ${entry} is not a declaration file`); + let real: string; + try { + real = realpathSync(join(overlayDir, entry)).split("\\").join("/"); + } catch { + return { missing: entry }; + } + if (!real.startsWith(`${overlayDir}/`)) throw bad(`entry ${entry} leaves the overlay`); + if (!out.includes(real)) out.push(real); + } + return out; +} + /** Reads and validates a schema-1 manifest. Throws an Error naming the * first problem for an unreadable or malformed manifest; instances whose * directories are missing are skipped with a note (they compile from @@ -353,6 +410,26 @@ export function loadSourceManifest(manifestPath: string): SourceOverlaySet { } if (seen.has(key)) throw bad(`instances[${index}] repeats installed directory ${key}`); seen.add(key); + // Additive schema-1 fields: absent means the historical behavior. + const semantic = e["semanticOptions"]; + if (semantic !== undefined && (semantic === null || typeof semantic !== "object")) { + throw bad(`instances[${index}].semanticOptions must be an object`); + } + const useDefine = (semantic as Record | undefined)?.[ + "useDefineForClassFields" + ]; + if (useDefine !== undefined && typeof useDefine !== "boolean") { + throw bad(`instances[${index}].semanticOptions.useDefineForClassFields must be a boolean`); + } + const ambient = ambientDeclarationsOf(e["ambientDeclarations"], realOverlay, (what) => + bad(`instances[${index}].ambientDeclarations ${what}`), + ); + if (!Array.isArray(ambient)) { + notes.push( + `${name}@${version}: ambient declaration ${ambient.missing} does not exist in the overlay; skipped`, + ); + continue; + } instances.push({ key, name, @@ -360,6 +437,8 @@ export function loadSourceManifest(manifestPath: string): SourceOverlaySet { overlayDir: realOverlay, exports: exportsField, origin: originOf(e["source"]), + ...(ambient.length > 0 ? { ambientDeclarations: ambient } : {}), + ...(useDefine === false ? { classFields: "assign" as const } : {}), }); } const fallbacks = doc["fallbacks"]; diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 54fb8f70e5..8157d8c04f 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -130,6 +130,7 @@ export const FETCH_AMBIENT_TYPES = [ "RequestInit", "Event", "CustomEvent", + "MessageEvent", "EventTarget", "AddEventListenerOptions", "EventListenerOptions", @@ -1243,7 +1244,8 @@ function isWorkerHandleSymbol( return ( (symbol?.name === "Worker" || symbol?.name === "MessagePort" || - symbol?.name === "MessageChannel") && + symbol?.name === "MessageChannel" || + symbol?.name === "BroadcastChannel") && ctx.checker .declarationsOf(symbol) .some( @@ -1384,6 +1386,23 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { ) { return { kind: "object", className: "%Error" }; } + // child_process.ExecException, exec's callback error, is an interface + // extending Error with optional metadata members; the value is one + // ordinary Error as well. + const execException = type.getSymbol(); + if ( + execException?.name === "ExecException" && + checker + .declarationsOf(execException) + .some( + (decl) => + ts.isInterfaceDeclaration(decl) && + ctx.isStdlibFile(decl.getSourceFile()) && + isDeclaredInAmbientModule(decl, "child_process"), + ) + ) { + return { kind: "object", className: "%Error" }; + } if (isOsConstantsType(type, ctx) || isLegacyUrlType(type, ctx)) return DYN; // Immutable (@scriptc/types) is a deep-readonly view: the value is // still the T it maps. @@ -1442,6 +1461,18 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { contextResolutions++; return viaIndexed; } + // A conditional type over a bound parameter (`Infer`, a + // mapped type's member): evaluate it against the bindings. + if (type.isConditionalType()) { + const evaluated = evalMappedTemplate(type, new Map(), new Map(), ctx); + if (evaluated && evaluated !== type) { + const viaConditional = mapType(evaluated, ctx); + if (viaConditional !== null) { + contextResolutions++; + return viaConditional; + } + } + } } const widened = checker.getBaseTypeOfLiteralType(type); const flags = widened.flags; @@ -2377,9 +2408,13 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // representation and consumed by the console method lowerings. The // stdlib Disposable (events.addAbortListener's result) and // AsyncDisposable are checked objects carrying their Symbol.dispose or - // Symbol.asyncDispose method. + // Symbol.asyncDispose method. A DisposableStack is a native handle whose + // methods are function values of its checked-dynamic view. if ( - (psym?.name === "Console" || psym?.name === "Disposable" || psym?.name === "AsyncDisposable") && + (psym?.name === "Console" || + psym?.name === "Disposable" || + psym?.name === "AsyncDisposable" || + psym?.name === "DisposableStack") && checker .declarationsOf(psym) .some((d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())) @@ -2505,6 +2540,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // ToInt32-wrap) — the Atomics.wait sleep idiom constructs one over a // SharedArrayBuffer, and the i32 semantics hold for every other use. if (isStdlibInterface("Int32Array")) return bytesOf("i32"); + if (isStdlibInterface("Float16Array")) return bytesOf("f16"); if (isStdlibInterface("Float32Array")) return bytesOf("f32"); if (isStdlibInterface("Float64Array")) return bytesOf("f64"); // DataView: the ONE view kind — a u8 bytes value whose runtime @@ -4086,6 +4122,8 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { if (childShaped && core >= 2) return CHILD_T; } if (flags & (ts.TypeFlags.Object | ts.TypeFlags.Intersection) && callSigs.length === 0) { + const viaMapped = mapBoundMappedType(widened, ctx); + if (viaMapped) return viaMapped; const viaAlias = mapGenericUtilityAlias(widened, ctx); if (viaAlias) return viaAlias; const record = mapRecordType(widened, ctx); @@ -4725,6 +4763,187 @@ function mapGenericIndexedAccess(type: ts.Type, ctx: TypeMapperCtx): IrType | nu return covered.every((t) => typeEquals(t, first)) ? first : null; } +/** A mapped type over a type parameter of the instance being lowered (`{ + * [K in keyof S]: S[K] }`, `{ [K in keyof S]: Infer }`): the checker + * keeps it symbolic inside the generic body, but the instance binds `S`, + * so each property of the bound type maps through the template evaluated + * with `K` set to that property's key. Optionality follows the `?` + * modifier, or the bound property's own for a homomorphic mapping. Null + * when the shape is anything else or a template form does not evaluate. */ +function mapBoundMappedType(widened: ts.Type, ctx: TypeMapperCtx): IrType | null { + const { checker, resolveTypeParamTs, shapes, unions } = ctx; + if (!resolveTypeParamTs || !isMappedShape(widened)) return null; + const symbol = widened.getSymbol(); + const node = symbol + ? checker.declarationsOf(symbol).find((d): d is ts.MappedTypeNode => ts.isMappedTypeNode(d)) + : undefined; + const constraint = node?.typeParameter.constraint; + if ( + !node?.type || + !constraint || + !ts.isTypeOperatorNode(constraint) || + constraint.operator !== ts.SyntaxKind.KeyOfKeyword + ) + return null; + const source = checker.getTypeFromTypeNode(constraint.type); + if (!(source.flags & ts.TypeFlags.TypeParameter)) return null; + const bound = resolveTypeParamTs(source); + const key = checker.getSymbolAtLocation(node.typeParameter.name); + if (!bound || !key) return null; + const template = checker.getTypeFromTypeNode(node.type); + const modifier = node.questionToken?.kind; + const fields: { name: string; type: IrType }[] = []; + const order: string[] = []; + for (const property of checker.getPropertiesOfType(bound)) { + const name = property.name; + const evaluated = evalMappedTemplate(template, new Map([[key, name]]), new Map(), ctx); + let type = evaluated ? mapType(evaluated, ctx) : null; + if (!type) return null; + const optional = + modifier === ts.SyntaxKind.MinusToken + ? false + : modifier !== undefined || (property.flags & ts.SymbolFlags.Optional) !== 0; + if (optional) { + const armed = withUndefinedArm(type, unions); + if (!armed) return null; + type = armed; + } + fields.push({ name, type }); + order.push(name); + } + fields.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)); + return { kind: "record", shapeId: shapes.intern(fields, false, undefined, order) }; +} + +/** The checker type a mapped type's template stands for at one key: + * `keys` binds the mapped type parameter to a property name, `aliases` + * binds type-alias and `infer` parameters met on the way. Handles type + * parameters (through the instance's bindings), indexed accesses + * (`S[K]`, `S[K]["_output"]`) and conditional types whose `extends` side + * is a reference to the same generic type as the checked type + * (`S extends Schema ? T : never`); answers the type itself + * when it mentions none of the bound parameters, null otherwise. */ +function evalMappedTemplate( + type: ts.Type, + keys: Map, + aliases: Map, + ctx: TypeMapperCtx, +): ts.Type | null { + const { checker, resolveTypeParamTs } = ctx; + if (type.flags & ts.TypeFlags.TypeParameter) { + const symbol = type.getSymbol(); + if (symbol && keys.has(symbol)) return null; + if (symbol && aliases.has(symbol)) return aliases.get(symbol)!; + return resolveTypeParamTs?.(type) ?? null; + } + if (type.isIndexedAccessType()) { + const object = type.getObjectType(); + const index = type.getIndexType(); + if (!object || !index) return null; + const receiver = evalMappedTemplate(object, keys, aliases, ctx); + const indexSymbol = index.flags & ts.TypeFlags.TypeParameter ? index.getSymbol() : undefined; + const name = + indexSymbol && keys.has(indexSymbol) + ? keys.get(indexSymbol)! + : index.isStringLiteralType() + ? index.value + : index.isNumberLiteralType() + ? String(index.value) + : null; + if (!receiver || name === null) return null; + const property = checker.getPropertyOfType(receiver, name); + return property ? checker.getTypeOfSymbol(property) : null; + } + if (type.isConditionalType()) { + // A conditional reached through an alias (`Infer`) checks the + // alias's own parameters: bind them to the evaluated arguments. + let scope = aliases; + const alias = type.getAliasSymbol(); + const aliasArgs = type.getAliasTypeArguments(); + const aliasDecl = alias + ? checker + .declarationsOf(alias) + .find((d): d is ts.TypeAliasDeclaration => ts.isTypeAliasDeclaration(d)) + : undefined; + if (aliasDecl?.typeParameters && aliasArgs.length === aliasDecl.typeParameters.length) { + scope = new Map(aliases); + for (let i = 0; i < aliasArgs.length; i++) { + const param = checker.getSymbolAtLocation(aliasDecl.typeParameters[i]!.name); + const arg = evalMappedTemplate(aliasArgs[i]!, keys, aliases, ctx); + if (!param || !arg) return null; + scope.set(param, arg); + } + } + const checkType = type.getCheckType(); + const extendsType = type.getExtendsType(); + if (!checkType || !extendsType) return null; + const checked = evalMappedTemplate(checkType, keys, scope, ctx); + if (!checked) return null; + const inferred = matchInferReference(checked, extendsType, ctx); + if (inferred === undefined) return null; + const branch = inferred ? type.getTrueType() : type.getFalseType(); + const branchScope = inferred ? new Map([...scope, ...inferred]) : scope; + return evalMappedTemplate(branch, keys, branchScope, ctx); + } + return mentionsMappedParams(type, keys, aliases) ? null : type; +} + +/** `checked extends G<..., infer T, ...>`: the infer bindings when + * `checked` is a reference to the same generic `G` (its arguments bind the + * infer positions; other positions must be the same types), false when it + * is a reference to another type or a primitive, undefined when the match + * cannot be decided this way. */ +function matchInferReference( + checked: ts.Type, + extendsType: ts.Type, + ctx: TypeMapperCtx, +): Map | false | undefined { + const { checker } = ctx; + const target = (t: ts.Type): ts.Type | undefined => + t.isTypeReference?.() ? (t as ts.TypeReference).getTarget?.() : undefined; + const wantTarget = target(extendsType) ?? extendsType.getAliasSymbol(); + if (!wantTarget) return undefined; + const gotTarget = target(checked) ?? checked.getAliasSymbol(); + if (!gotTarget) return checked.flags & ts.TypeFlags.Object ? undefined : false; + if (gotTarget !== wantTarget) return undefined; + const wantArgs = target(extendsType) + ? checker.getTypeArguments(extendsType as ts.TypeReference) + : extendsType.getAliasTypeArguments(); + const gotArgs = target(checked) + ? checker.getTypeArguments(checked as ts.TypeReference) + : checked.getAliasTypeArguments(); + if (wantArgs.length !== gotArgs.length) return undefined; + const inferred = new Map(); + for (let i = 0; i < wantArgs.length; i++) { + const want = wantArgs[i]!; + const symbol = want.flags & ts.TypeFlags.TypeParameter ? want.getSymbol() : undefined; + const isInfer = symbol + ? checker + .declarationsOf(symbol) + .some((d) => d.parent !== undefined && ts.isInferTypeNode(d.parent)) + : false; + if (isInfer && symbol) inferred.set(symbol, gotArgs[i]!); + else if (want !== gotArgs[i]) return undefined; + } + return inferred; +} + +/** The type mentions a mapped key or bound alias parameter (a template + * shape evalMappedTemplate does not reduce). */ +function mentionsMappedParams( + type: ts.Type, + keys: Map, + aliases: Map, +): boolean { + if (type.flags & ts.TypeFlags.TypeParameter) { + const symbol = type.getSymbol(); + return symbol !== undefined && (keys.has(symbol) || aliases.has(symbol)); + } + if (type.isUnionType() || type.isIntersectionType()) + return ts.constituentTypes(type).some((part) => mentionsMappedParams(part, keys, aliases)); + return false; +} + function mapGenericUtilityAlias(widened: ts.Type, ctx: TypeMapperCtx): IrType | null { const { resolveTypeParam, shapes, unions } = ctx; if (!resolveTypeParam) return null; diff --git a/packages/compiler/src/ir/analysis.ts b/packages/compiler/src/ir/analysis.ts index 850783d4e0..88f6f394f1 100644 --- a/packages/compiler/src/ir/analysis.ts +++ b/packages/compiler/src/ir/analysis.ts @@ -219,6 +219,7 @@ const BORROW_SAFE_MATH = new Set([ "math.sign", "math.exp", "math.expm1", + "math.f16round", "math.fround", "math.log", "math.log1p", diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 2381413f86..1bb9cbd14f 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -16,6 +16,22 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "test.sub", "test.subEmpty", "test.suite", + // mock.timers: Node's ERR_INVALID_STATE and ERR_INVALID_ARG_VALUE, and + // the throws of the timer callbacks tick and runAll run. + "test.ctxSnapshot", + "test.fileSnapshot", + "test.snapshotSerialize", + "test.mockTimersEnable", + "test.mockTimersTick", + "test.mockTimersRunAll", + "test.mockTimersSetTime", + "test.ctxMockTimersEnable", + "test.ctxMockTimersTick", + "test.ctxMockTimersRunAll", + "test.ctxMockTimersSetTime", + "tp.schedulerWait", + "tp.schedulerWaitValue", + "tp.schedulerWaitSignal", "console.write", "console.label", "console.countReset", @@ -24,8 +40,12 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "worker.new", "worker.data", "worker.parentPort", + "worker.resourceLimits", "worker.channel", "worker.receiveMessageOnPort", + "worker.setEnvironmentData", + "worker.getEnvironmentData", + "worker.broadcastChannel", "process.exit", "text.encodeInto", "buffer.isAscii", @@ -34,11 +54,13 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "cp.spawnDynamic", "child.onDyn", "text.decoderEncoding", + "strdec.encoding", "dyn.construct", "dyn.reflectApply", "dyn.reflectGet", "dyn.usingMethod", "dyn.usingDispose", + "disposable.stackUseCallback", "dyn.reflectSet", "dyn.reflectDefine", "dyn.abstractEq", @@ -73,6 +95,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "arrayBuffer.viewU8", "arrayBuffer.viewU32", "arrayBuffer.viewI32", + "arrayBuffer.viewF16", "arrayBuffer.viewF32", "arrayBuffer.viewF64", "arrayBuffer.viewDV", @@ -556,6 +579,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "dyn.arrayProtoCall", "process.chdir", "os.getPriority", + "os.setPriority", // Pattern length TypeError, brace-sequence RangeError, and the // SyntaxError of a segment regex invalid under the u flag. "path.matchesGlob", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 5e219ecf62..ca27495918 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -550,6 +550,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ /** os.getPriority(pid): validateInt32 (RangeError ERR_OUT_OF_RANGE), * then the priority; a failure throws Node's SystemError. */ "os.getPriority": { argTypes: [F64], result: F64 }, + /** os.setPriority(pid, priority): validateInt32 for both (priority in + * -20..19, RangeError ERR_OUT_OF_RANGE), then the priority; a failure + * throws Node's SystemError. */ + "os.setPriority": { argTypes: [F64, F64], result: VOID }, /** os.cpus(): cpusTake snapshots every CPU and answers the count; * cpusModel(i) and cpusField(i, k) (k: 0 speed, 1 user, 2 nice, 3 sys, * 4 idle, 5 irq) read the snapshot. Never throw. */ @@ -644,6 +648,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "math.sign": { argTypes: [F64], result: F64 }, "math.exp": { argTypes: [F64], result: F64 }, "math.expm1": { argTypes: [F64], result: F64 }, + "math.f16round": { argTypes: [F64], result: F64 }, "math.fround": { argTypes: [F64], result: F64 }, "math.sqrt": { argTypes: [F64], result: F64 }, "math.log": { argTypes: [F64], result: F64 }, @@ -1109,10 +1114,33 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "worker.root": { argTypes: [], result: F64 }, "worker.data": { argTypes: [], result: DYN }, "worker.parentPort": { argTypes: [], result: DYN }, + /** worker_threads.resourceLimits: {} in the main thread; a refusal in a + * worker (scr_worker_events.c). */ + "worker.resourceLimits": { argTypes: [], result: DYN }, + /** worker_threads.threadName: the Worker's name in a worker, "" in the + * main thread (scr_worker.c). */ + "worker.threadName": { argTypes: [], result: DYN }, /** new MessageChannel() ({ port1, port2 }) and receiveMessageOnPort(port) * (scr_worker_events.c; the latter may throw). */ "worker.channel": { argTypes: [], result: DYN }, "worker.receiveMessageOnPort": { argTypes: [DYN], result: DYN }, + /** setEnvironmentData(key, value) and getEnvironmentData(key) + * (scr_worker_events.c; arguments borrowed). */ + "worker.setEnvironmentData": { argTypes: [DYN, DYN], result: VOID }, + "worker.getEnvironmentData": { argTypes: [DYN], result: DYN }, + /** markAsUntransferable(value) and isMarkedAsUntransferable(value) + * (scr_worker_events.c; the value borrowed). */ + "worker.markUntransferable": { argTypes: [DYN], result: VOID }, + "worker.isMarkedUntransferable": { argTypes: [DYN], result: BOOL }, + /** markAsUncloneable(value) (scr_json.c; the value borrowed). */ + "dyn.markUncloneable": { argTypes: [DYN], result: VOID }, + /** before new Worker() with the stdout or stderr option: links the stream + * unit that worker.stdout and worker.stderr read through (scr_stream.c). */ + "stream.workerStdio": { argTypes: [], result: VOID }, + /** new BroadcastChannel(...args) (scr_worker_events.c; may throw), after + * event.broadcastInstall, which installs its EventTarget side. */ + "worker.broadcastChannel": { argTypes: [DYN], result: DYN }, + "event.broadcastInstall": { argTypes: [], result: VOID }, /** child_process.spawnSync (scr_child.c): posix_spawn + waitpid + piped * utf8 capture — cmd borrowed, args one borrowed string[] (the frontend * completes an omitted list to an empty literal), result an owned (+1) @@ -1443,6 +1471,32 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ }, "test.ctxMockRestoreAll": { argTypes: [TESTCTX_T], result: VOID }, "test.ctxMockReset": { argTypes: [TESTCTX_T], result: VOID }, + // mock.timers on the module's `mock` and on t.mock. enable takes one + // code per listed API ('T' setTimeout, 'I' setInterval), the `now` + // value and its flags (1 given, 2 from a Date). enable, tick, runAll and + // setTime throw Node's errors; tick and runAll run timer callbacks. + // t.assert.snapshot and fileSnapshot: the escaped, wrapped serialized + // value, and the stored value it is compared with (snapshot: the test, + // the value, the snapshot file; fileSnapshot: the value, the path). + // Both read or, in update mode, record; missing and unreadable + // snapshots throw Node's ERR_INVALID_STATE. + "test.snapshotValue": { argTypes: [STRING], result: STRING }, + "test.ctxSnapshot": { argTypes: [TESTCTX_T, STRING, STRING], result: STRING }, + "test.fileSnapshot": { argTypes: [STRING, STRING], result: STRING }, + // Node's serializeValue over a dyn array of serializers (undefined for + // the default JSON.stringify(value, null, 2)); a throw becomes Node's + // "The provided serializers did not generate a string." ERR_INVALID_STATE. + "test.snapshotSerialize": { argTypes: [DYN, DYN], result: DYN }, + "test.mockTimersEnable": { argTypes: [STRING, F64, F64], result: VOID }, + "test.mockTimersTick": { argTypes: [F64], result: VOID }, + "test.mockTimersRunAll": { argTypes: [], result: VOID }, + "test.mockTimersReset": { argTypes: [], result: VOID }, + "test.mockTimersSetTime": { argTypes: [F64], result: VOID }, + "test.ctxMockTimersEnable": { argTypes: [TESTCTX_T, STRING, F64, F64], result: VOID }, + "test.ctxMockTimersTick": { argTypes: [TESTCTX_T, F64], result: VOID }, + "test.ctxMockTimersRunAll": { argTypes: [TESTCTX_T], result: VOID }, + "test.ctxMockTimersReset": { argTypes: [TESTCTX_T], result: VOID }, + "test.ctxMockTimersSetTime": { argTypes: [TESTCTX_T, F64], result: VOID }, // node:http (scr_http.c over scr_net.c). The handler and the data // listeners are program-dependent closures (null slots, checked in the // libCall case); reqHeader's result is the interned string|undefined @@ -2289,8 +2343,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ /** child_process.execFile's callback slice: starts an all-piped child, * captures stdout/stderr, and moves the error-first callback into the * child registry. The callback shape is program-dependent and checked - * by the validator/backends. */ - "cp.execFile": { argTypes: [STRING, arrayOf(STRING), null], result: CHILD_T }, + * by the validator/backends. The last argument is exec's shell flag: + * the argv is ["-c", command] and failure messages show the command. */ + "cp.execFile": { argTypes: [STRING, arrayOf(STRING), null, BOOL], result: CHILD_T }, // spawn's options form: per-slot stdio modes (0 ignore / 1 inherit / // 2 fd) with the out/err fds for mode 2, detached, env replacement // pairs, cwd ("" = inherit). @@ -2843,6 +2898,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ }, "fileHandle.stat": { argTypes: [FILEHANDLE_T], result: { kind: "promise", inner: STATS_T } }, "process.argv": { argTypes: [], result: arrayOf(STRING) }, + // The Node runtime flag a node:test binary took out of process.argv + // (--test-update-snapshots), or [] — a fresh array per read. + "process.execArgv": { argTypes: [], result: arrayOf(STRING) }, "process.platform": { argTypes: [], result: STRING }, // The one libCall whose result type is program-dependent (union ids are // per-module): the `result` here is a placeholder — the libCall case @@ -3159,6 +3217,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "arrayBuffer.viewU8": { argTypes: [DYN, DYN, DYN], result: BYTES_U8 }, "arrayBuffer.viewU32": { argTypes: [DYN, DYN, DYN], result: bytesOf("u32") }, "arrayBuffer.viewI32": { argTypes: [DYN, DYN, DYN], result: bytesOf("i32") }, + "arrayBuffer.viewF16": { argTypes: [DYN, DYN, DYN], result: bytesOf("f16") }, "arrayBuffer.viewF32": { argTypes: [DYN, DYN, DYN], result: bytesOf("f32") }, "arrayBuffer.viewF64": { argTypes: [DYN, DYN, DYN], result: bytesOf("f64") }, "arrayBuffer.viewDV": { argTypes: [DYN, DYN, DYN], result: BYTES_U8 }, @@ -3210,6 +3269,13 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * `await using`. Both throw Node's TypeErrors or the method's error. */ "dyn.usingMethod": { argTypes: [DYN, BOOL], result: DYN }, "dyn.usingDispose": { argTypes: [DYN, DYN], result: DYN }, + /** DisposableStack (scr_symbol.c): a new stack handle; whether a value is + * one; and `stack.use(value)` for a class instance, registering the + * compiler's disposal function for it. The last throws Node's + * ReferenceError on a disposed stack. */ + "disposable.stackNew": { argTypes: [], result: DYN }, + "disposable.stackIs": { argTypes: [DYN], result: BOOL }, + "disposable.stackUseCallback": { argTypes: [DYN, DYN, DYN], result: BOOL }, "dyn.reflectSet": { argTypes: [DYN, DYN, DYN, DYN], result: BOOL }, "dyn.reflectDefine": { argTypes: [DYN, DYN, DYN], result: BOOL }, "dyn.abstractEq": { argTypes: [DYN, DYN], result: BOOL }, @@ -3346,6 +3412,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "emitter.getMax": { argTypes: [null], result: F64 }, "emitter.setDefaultMax": { argTypes: [F64], result: VOID }, "emitter.getDefaultMax": { argTypes: [], result: F64 }, + // The ES module namespace's copy of defaultMaxListeners, and + // module.syncBuiltinESMExports() refreshing it. + "emitter.esmDefaultMax": { argTypes: [], result: F64 }, + "emitter.esmSync": { argTypes: [], result: VOID }, /** The events.errorMonitor event key: an immortal string the runtime * registry matches by identity, so no string event name reaches it. */ "emitter.errorMonitor": { argTypes: [], result: STRING }, @@ -3742,11 +3812,18 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "insp.jsonDyn": { argTypes: [DYN], result: STRING }, "insp.str": { argTypes: [STRING], result: STRING }, "insp.regex": { argTypes: [REGEX], result: STRING }, + /** A Date's inspect text: toISOString(), or "Invalid Date". */ + "insp.date": { argTypes: [DATE_T], result: STRING }, "insp.buffer": { argTypes: [BYTES_U8], result: STRING }, + /** A numeric typed array that is not a Buffer (any element kind). */ + "insp.typedArray": { argTypes: [null, F64, F64], result: STRING }, "insp.error": { argTypes: [null, F64, F64], result: STRING }, "insp.dyn": { argTypes: [DYN, F64, F64], result: STRING }, "insp.dynS": { argTypes: [DYN, F64], result: STRING }, "insp.begin": { argTypes: [F64], result: VOID }, + /** util.inspect's breakLength, compact (0 for false) and sorted for the + * renders that follow; the call site restores 80, 3 and false afterwards. */ + "insp.layout": { argTypes: [F64, F64, BOOL], result: VOID }, "insp.entry": { argTypes: [STRING, BOOL], result: VOID }, // Circular references: the receiver slot is program-dependent (any // cycle-capable record/array/map/class type — the insp.error precedent). @@ -3785,6 +3862,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "strdec.write": { argTypes: [STRING, F64, BYTES_U8], result: STRING }, "strdec.next": { argTypes: [STRING, F64, BYTES_U8], result: F64 }, "strdec.end": { argTypes: [STRING, F64], result: STRING }, + /** A runtime encoding argument's canonical name (Node's normalizeEncoding: + * nullish and "" are utf8); an unknown name throws ERR_UNKNOWN_ENCODING. */ + "strdec.encoding": { argTypes: [DYN], result: STRING }, // node:readline: the interface handle is f64; the callbacks' func types // are program-dependent (the emitter picks the adapter by parameter // count), so those slots are null like child.onExit's. @@ -3889,6 +3969,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * through the immediate queue (fires before due timers of later loop * turns, Node's check phase). Neither throws. */ "tp.setTimeout": { argTypes: [F64], result: { kind: "promise", inner: VOID } }, + // scheduler.wait(delay): tp.setTimeout's promise. It throws Node's + // ERR_INVALID_THIS once mock.timers has restored the real scheduler.wait. + "tp.schedulerWait": { argTypes: [F64], result: { kind: "promise", inner: VOID } }, "tp.setImmediate": { argTypes: [], result: { kind: "promise", inner: VOID } }, /** events.once (scr_events_emitter.c): [emitter, name, boxer, * errorBoxer, listenForError] → a pending promise of any[]; the boxers @@ -3905,6 +3988,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * AbortError (scr_event_target.c). */ "tp.setTimeoutSignal": { argTypes: [F64, null, BOOL, DYN], result: VOID }, "tp.setImmediateSignal": { argTypes: [null, BOOL, DYN], result: VOID }, + /** scheduler.wait(delay, options): tp.setTimeoutValue's and + * tp.setTimeoutSignal's shapes, with tp.schedulerWait's throws. */ + "tp.schedulerWaitValue": { argTypes: [F64, null, BOOL], result: VOID }, + "tp.schedulerWaitSignal": { argTypes: [F64, null, BOOL, DYN], result: VOID }, /** node:diagnostics_channel (scr_dc.c, linked when any dc.* appears — * the zlib gating precedent): a process-global name→channel registry; * channel values are f64 handles (type-mapper.ts maps Channel to F64, the diff --git a/packages/compiler/src/ir/byte-numbers.ts b/packages/compiler/src/ir/byte-numbers.ts index a6a079af67..89ffaaa986 100644 --- a/packages/compiler/src/ir/byte-numbers.ts +++ b/packages/compiler/src/ir/byte-numbers.ts @@ -25,6 +25,7 @@ const FORMATS: Readonly> = { i16: { width: 2, signed: true, floating: false }, u32: { width: 4, signed: false, floating: false }, i32: { width: 4, signed: true, floating: false }, + f16: { width: 2, signed: false, floating: true }, f32: { width: 4, signed: false, floating: true }, f64: { width: 8, signed: false, floating: true }, u64: { width: 8, signed: false, floating: false }, @@ -38,6 +39,7 @@ const VIEW_FORMATS: Readonly> = { dvGetInt16: "i16", dvGetUint32: "u32", dvGetInt32: "i32", + dvGetFloat16: "f16", dvGetFloat32: "f32", dvGetFloat64: "f64", dvGetBigUint64Number: "u64", @@ -48,6 +50,7 @@ const VIEW_FORMATS: Readonly> = { dvSetInt16: "i16", dvSetUint32: "u32", dvSetInt32: "i32", + dvSetFloat16: "f16", dvSetFloat32: "f32", dvSetFloat64: "f64", }; diff --git a/packages/compiler/src/ir/capture-values.ts b/packages/compiler/src/ir/capture-values.ts index 94cff053bb..d7a1516bb1 100644 --- a/packages/compiler/src/ir/capture-values.ts +++ b/packages/compiler/src/ir/capture-values.ts @@ -244,8 +244,13 @@ interface FunctionFacts { classCaptured: Set; reads: Map; /** Closure capture edges: the creator's binding `source` flows into - * capture `local` of `target`. */ - edges: { target: string; index: number; source: string }[]; + * capture `local` of `target`, grouped by `source`. */ + edges: Map; + /** The function's own captures (first index of each id), class + * captures, and parameters, for constant-time membership tests. */ + captureIndex: Map; + classCaptures: Set; + params: Set; } interface CreationSite { @@ -317,7 +322,7 @@ export class CaptureFlow { ? value.fnName : null; } - const index = fn.captures?.findIndex((c) => c.localId === id) ?? -1; + const index = this.factsOf(fn).captureIndex.get(id) ?? -1; if (index < 0 || !this.stableCapture(fnName, id)) return null; let result: string | null = null; for (const site of this.sites.get(fnName) ?? []) { @@ -399,7 +404,7 @@ export class CaptureFlow { return true; }; everyStmtList(body.slice(declared + 1), { stmt: () => true, expr: count }); - const edges = facts.edges.filter((edge) => edge.source === id).length; + const edges = facts.edges.get(id)?.length ?? 0; return nodes === edges && !facts.classCaptured.has(id); } @@ -410,7 +415,7 @@ export class CaptureFlow { const local = facts.locals.get(id); const declaration = facts.declarations.get(id); if (local === undefined || local.boxed === true || local.tdz === true) return false; - if (fn.params.some((p) => p.localId === id) || declaration?.count !== 1) return false; + if (facts.params.has(id) || declaration?.count !== 1) return false; const writes = facts.writes.get(id) ?? 0; return declaration.initialized ? writes === 0 : facts.hoisted.has(id) && writes === 1; } @@ -450,8 +455,14 @@ export class CaptureFlow { tdzInits: new Map(), classCaptured: new Set(), reads: new Map(), - edges: [], + edges: new Map(), + captureIndex: new Map(), + classCaptures: new Set((fn.classCaptures ?? []).map((c) => c.localId)), + params: new Set(fn.params.map((p) => p.localId)), }; + (fn.captures ?? []).forEach((c, index) => { + if (!facts.captureIndex.has(c.localId)) facts.captureIndex.set(c.localId, index); + }); this.facts.set(fn.name, facts); const capturesIn = (e: IrExpr): Set => { const ids = new Set(); @@ -464,19 +475,17 @@ export class CaptureFlow { visitExpr(e); return ids; }; + // One pass over runs of empty declarations and closure assignments: + // each empty declaration pairs with the first later assignment of its + // binding in the same run. const findHoisted = (body: readonly IrStmt[]): void => { - body.forEach((s, i) => { - if (s.kind !== "varDecl" || s.init !== null) return; - for (let j = i + 1; j < body.length; j++) { - const next = body[j]!; - if (next.kind === "assign" && next.value.kind === "closure") { - if (next.localId === s.localId) { - facts.hoisted.set(s.localId, next); - return; - } - } else if (next.kind !== "varDecl" || next.init !== null) return; - } - }); + const pending = new Set(); + for (const s of body) { + if (s.kind === "assign" && s.value.kind === "closure") { + if (pending.delete(s.localId)) facts.hoisted.set(s.localId, s); + } else if (s.kind === "varDecl" && s.init === null) pending.add(s.localId); + else pending.clear(); + } }; const declare = (s: IrStmt & { kind: "varDecl" }, splittable: boolean): void => { const entry = facts.declarations.get(s.localId); @@ -514,9 +523,11 @@ export class CaptureFlow { let list = this.sites.get(e.fnName); if (list === undefined) this.sites.set(e.fnName, (list = [])); list.push({ owner: fn.name, captures: e.captures, adapts: e.adapts === true }); - e.captures.forEach((source, index) => - facts.edges.push({ target: e.fnName, index, source }), - ); + e.captures.forEach((source, index) => { + let edges = facts.edges.get(source); + if (edges === undefined) facts.edges.set(source, (edges = [])); + edges.push({ target: e.fnName, index, source }); + }); break; } case "classRef": @@ -535,12 +546,15 @@ export class CaptureFlow { // iteration, and statement lists inside expressions release their // locals with the enclosing frame; declarations there stay as they are. const findTdzInits = (body: readonly IrStmt[]): void => { + // The first empty declaration of each binding in this list. + const empty = new Map(); body.forEach((s, j) => { + if (s.kind === "varDecl" && s.init === null && !empty.has(s.localId)) + empty.set(s.localId, j); if (s.kind !== "assign" || s.initializes !== true) return; - const declared = body.findIndex( - (d, i) => i < j && d.kind === "varDecl" && d.localId === s.localId && d.init === null, - ); - if (declared >= 0) facts.tdzInits.set(s.localId, { body, declared, initialized: j }); + const declared = empty.get(s.localId); + if (declared !== undefined && declared < j) + facts.tdzInits.set(s.localId, { body, declared, initialized: j }); }); }; const list = (body: readonly IrStmt[], split: boolean): boolean => { @@ -591,10 +605,6 @@ export class CaptureFlow { return facts; } - private local(fn: IrFunction, id: string): IrLocal | undefined { - return this.factsOf(fn).locals.get(id); - } - /** Nothing rebinds `id` in `fnName` (declarations aside, and `ownWrites` * writes of its own body) or in any body that shares its box through * closure captures. */ @@ -610,8 +620,7 @@ export class CaptureFlow { result = (facts.writes.get(id) ?? 0) === ownWrites && !facts.classCaptured.has(id) && - facts.edges.every((edge) => { - if (edge.source !== id) return true; + (facts.edges.get(id) ?? []).every((edge) => { const capture = this.functions.get(edge.target)?.captures?.[edge.index]; return capture !== undefined && this.unchanged(edge.target, capture.localId); }); @@ -623,19 +632,18 @@ export class CaptureFlow { /** A boxed binding of `fn` itself (not a capture) whose box is filled * once, before any environment observes it, and never written again. */ stableOrigin(fn: IrFunction, id: string): boolean { - const local = this.local(fn, id); + const facts = this.factsOf(fn); + const local = facts.locals.get(id); if (local?.boxed !== true) return false; if (local.tdz === true) { - if (fn.captures?.some((c) => c.localId === id)) return false; - if (this.factsOf(fn).declarations.get(id)?.count !== 1) return false; + if (facts.captureIndex.has(id)) return false; + if (facts.declarations.get(id)?.count !== 1) return false; return this.orderedTdz(fn, id) && this.unchanged(fn.name, id, 1); } - if (fn.captures?.some((c) => c.localId === id)) return false; - if (fn.classCaptures?.some((c) => c.localId === id)) return false; - const facts = this.factsOf(fn); + if (facts.captureIndex.has(id)) return false; + if (facts.classCaptures.has(id)) return false; const declaration = facts.declarations.get(id); - if (fn.params.some((p) => p.localId === id)) - return declaration === undefined && this.unchanged(fn.name, id); + if (facts.params.has(id)) return declaration === undefined && this.unchanged(fn.name, id); if (declaration?.count !== 1) return false; if (declaration.initialized) return this.unchanged(fn.name, id); return facts.hoisted.has(id) && this.unchanged(fn.name, id, 1); @@ -654,8 +662,8 @@ export class CaptureFlow { private computeStableCapture(fnName: string, id: string): boolean { const fn = this.functions.get(fnName); - const index = fn?.captures?.findIndex((c) => c.localId === id) ?? -1; - if (fn === undefined || index < 0) return false; + const index = fn === undefined ? undefined : this.factsOf(fn).captureIndex.get(id); + if (fn === undefined || index === undefined) return false; const sites = this.sites.get(fnName); if (sites === undefined || sites.length === 0) return false; for (const site of sites) { @@ -679,7 +687,7 @@ export class CaptureFlow { const assign = init?.body[init.initialized]; return assign?.kind === "assign" ? plainLiteral(constantNumber(assign.value)) : own.literal; } - const index = fn.captures?.findIndex((c) => c.localId === id) ?? -1; + const index = this.factsOf(fn).captureIndex.get(id) ?? -1; if (index < 0 || !this.stableCapture(fnName, id)) return null; let value: number | null = null; for (const site of this.sites.get(fnName) ?? []) { @@ -715,8 +723,8 @@ export class CaptureFlow { const immutable = new Set(); for (const local of fn.locals) { if (local.boxed !== true) continue; - const capture = fn.captures?.some((c) => c.localId === local.id) === true; - const param = fn.params.some((p) => p.localId === local.id); + const capture = facts.captureIndex.has(local.id); + const param = facts.params.has(local.id); if (capture && local.mutable && this.stableCapture(fn.name, local.id)) immutable.add(local.id); if (!facts.reads.has(local.id)) continue; @@ -871,10 +879,8 @@ export class CaptureFlow { ); const pruned = dead.size === 0 ? body : prune(body, dead); const liveIds = new Set(live.map((s) => (s.kind === "varDecl" ? s.localId : ""))); - const added = locals.filter( - (local) => - liveIds.has(local.id) || !entry.some((s) => s.kind === "varDecl" && s.localId === local.id), - ); + const entryIds = new Set(entry.map((s) => (s.kind === "varDecl" ? s.localId : ""))); + const added = locals.filter((local) => liveIds.has(local.id) || !entryIds.has(local.id)); return { ...fn, locals: [...kept, ...added], body: [...live, ...pruned] }; } } diff --git a/packages/compiler/src/ir/function-references.test.ts b/packages/compiler/src/ir/function-references.test.ts index 32284c8209..0b1f9fb497 100644 --- a/packages/compiler/src/ir/function-references.test.ts +++ b/packages/compiler/src/ir/function-references.test.ts @@ -193,6 +193,7 @@ const MODULE_FIELDS: Record>, Role> = { "lib.exports[].fnName": "reference", "lib.exports[].inboundBytesTrap": "other", "lib.exports[].inboundIntTrap": "other", + "lib.exports[].inboundCstringTrap": "other", "lib.trapOverlays[].code": "other", "lib.trapOverlays[].teaching": "other", "lib.trapOverlays[].remediation": "other", diff --git a/packages/compiler/src/ir/int32-slots.ts b/packages/compiler/src/ir/int32-slots.ts index 5e868793c6..d7d06db7d1 100644 --- a/packages/compiler/src/ir/int32-slots.ts +++ b/packages/compiler/src/ir/int32-slots.ts @@ -336,6 +336,7 @@ const BOUNDARY_KINDS = new Set([ * property, except literal-keyed `dyn.keySet`/`dyn.keyDelete`. */ const DYN_READ_ONLY = new Set([ "arrayBuffer.new", + "arrayBuffer.viewF16", "arrayBuffer.viewF32", "arrayBuffer.viewF64", "arrayBuffer.viewI16", @@ -889,27 +890,44 @@ export function analyzeInt32Slots(mod: IrModule): Int32Slots { else groups.set(root, [key]); } const boxOrigin = new Map(); + // Per function, computed on first use: its capture ids and the locals an + // initialized declaration binds (one body walk each, not one per box). + const captureIds = new Map>(); + const capturedIn = (fnName: string, localId: string): boolean => { + let ids = captureIds.get(fnName); + if (ids === undefined) { + ids = new Set((functions.get(fnName)?.captures ?? []).map((c) => c.localId)); + captureIds.set(fnName, ids); + } + return ids.has(localId); + }; + const initializedDeclarations = new Map>(); + const initializedIn = (fn: IrFunction, localId: string): boolean => { + let ids = initializedDeclarations.get(fn.name); + if (ids === undefined) { + const found = new Set(); + walkFunction(fn, { + stmt: (s) => { + if (s.kind === "varDecl" && s.init) found.add(s.localId); + }, + }); + initializedDeclarations.set(fn.name, (ids = found)); + } + return ids.has(localId); + }; for (const [group, members] of groups) { if (members.some((key) => unstableBoxes.has(key))) continue; // The origin is the one member that is not a capture of its function. const origins = members.filter((key) => { const [fnName, localId] = key.split("\0") as [string, string]; - return !functions.get(fnName)?.captures?.some((c) => c.localId === localId); + return !capturedIn(fnName, localId); }); if (origins.length !== 1) continue; const [fnName, localId] = origins[0]!.split("\0") as [string, string]; const origin = functions.get(fnName)!; if (origin.params.some((p) => p.localId === localId)) { if (!slots.hasParam(fnName, localId)) continue; - } else { - let initialized = false; - walkFunction(origin, { - stmt: (s) => { - if (s.kind === "varDecl" && s.localId === localId && s.init) initialized = true; - }, - }); - if (!initialized) continue; - } + } else if (!initializedIn(origin, localId)) continue; boxOrigin.set(`param:${fnName}\0${localId}`, group); slots.addBox(group, members); // A box starts from its origin parameter's value: it can only be diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index bfaa13c0e4..9591c684a3 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -26,9 +26,19 @@ export interface SrcLoc { /* ── types ─────────────────────────────────────────────────────────────── */ -/** Numeric typed-array element kinds with a native representation. Float16 - * and BigInt typed arrays remain frontend-fenced. */ -export type IrBytesElem = "u8" | "u8c" | "i8" | "u16" | "i16" | "u32" | "i32" | "f32" | "f64"; +/** Numeric typed-array element kinds with a native representation. BigInt + * typed arrays remain frontend-fenced. */ +export type IrBytesElem = + | "u8" + | "u8c" + | "i8" + | "u16" + | "i16" + | "u32" + | "i32" + | "f16" + | "f32" + | "f64"; export const BYTES_ELEMENT_SIZE: Record = { u8: 1, @@ -38,13 +48,14 @@ export const BYTES_ELEMENT_SIZE: Record = { i16: 2, u32: 4, i32: 4, + f16: 2, f32: 4, f64: 8, }; /** Integer element kinds: a valid element read is never NaN. */ export function isIntegerBytesElem(elem: IrBytesElem): boolean { - return elem !== "f32" && elem !== "f64"; + return elem !== "f16" && elem !== "f32" && elem !== "f64"; } export const BYTES_ELEMENT_NAME: Record = { @@ -55,6 +66,7 @@ export const BYTES_ELEMENT_NAME: Record = { i16: "Int16Array", u32: "Uint32Array", i32: "Int32Array", + f16: "Float16Array", f32: "Float32Array", f64: "Float64Array", }; @@ -1176,8 +1188,19 @@ export interface IrLibExport { /** i64/u64 (ask 4): int64_t/uint64_t at the C edge — inbound values * range-check in the wrapper (`inboundIntTrap`), internal call sites * and returns are compile-time proven before this lands on the IR. */ - params: ("f64" | "bool" | "string" | "bytes" | "u8" | "u32" | "i32" | "i64" | "u64")[]; - returns: "f64" | "bool" | "string" | "bytes" | "void" | "i64" | "u64"; + params: ( + | "f64" + | "bool" + | "string" + | "cstring" + | "bytes" + | "u8" + | "u32" + | "i32" + | "i64" + | "u64" + )[]; + returns: "f64" | "bool" | "string" | "cstring" | "bytes" | "void" | "i64" | "u64"; /** The exact sink-message bytes this wrapper passes to the inbound-bytes * marshalling helper's trap — present exactly when a parameter is * bytes-classed. Already the assembled structured trap-teaching form @@ -1192,6 +1215,10 @@ export interface IrLibExport { * value cannot ride f64 exactly (|v| past 2^53−1). Same assembled * structured form and the same SC4012 code — one host-contract story. */ inboundIntTrap?: string; + /** The sibling message for a NULL cstring parameter: present exactly + * when a parameter is cstring-classed, passed to scr_library_cstr_in. + * Same assembled structured form and the same SC4012 code. */ + inboundCstringTrap?: string; } /** One runtime-trap overlay row: the profile's teaching (replaces the @@ -1218,7 +1245,7 @@ export interface IrLibCallback { name: string; /** The runtime channel slot (profile declaration order). */ slot: number; - params: ("f64" | "bool" | "string" | "bytes" | "u8" | "u32" | "i32")[]; + params: ("f64" | "bool" | "string" | "cstring" | "bytes" | "u8" | "u32" | "i32")[]; returns: "f64" | "bool" | "u8" | "u32" | "i32" | "i64" | "u64" | "pointer" | "void"; /** The unregistered-call trap text (a DETECTED trap: plain bytes the * library funnel classifies SC4025 and assembles with the current @@ -2227,6 +2254,7 @@ export type IrBytesIntrinsicMethod = | "dvGetInt16" | "dvGetUint32" | "dvGetInt32" + | "dvGetFloat16" | "dvGetFloat32" | "dvGetFloat64" | "dvGetBigUint64Number" @@ -2237,6 +2265,7 @@ export type IrBytesIntrinsicMethod = | "dvSetInt16" | "dvSetUint32" | "dvSetInt32" + | "dvSetFloat16" | "dvSetFloat32" | "dvSetFloat64"; @@ -2268,6 +2297,7 @@ export const MAY_THROW_BYTES_METHODS: ReadonlySet = new "dvGetInt16", "dvGetUint32", "dvGetInt32", + "dvGetFloat16", "dvGetFloat32", "dvGetFloat64", "dvGetBigUint64Number", @@ -2278,6 +2308,7 @@ export const MAY_THROW_BYTES_METHODS: ReadonlySet = new "dvSetInt16", "dvSetUint32", "dvSetInt32", + "dvSetFloat16", "dvSetFloat32", "dvSetFloat64", ]); @@ -2533,7 +2564,7 @@ export type IrExpr = } /** A regex literal `/ab+c/gi`. `pattern` is the text between the slashes * exactly as written (escapes UNprocessed — the regex engine parses them), - * `flags` the trailing flags in source order (alphabet fenced to gimsuy by + * `flags` the trailing flags in getter order (alphabet fenced to gimsuvy by * the frontend). Backends intern ONE immortal static per (pattern, flags) * pair — like string literals, so repeated evaluation is free and * `re === re` would hold — and compile the pattern lazily at first use @@ -3556,6 +3587,11 @@ export type IrExpr = type: IrType; loc: SrcLoc; prototypeIdentityOnly?: true; + /** A timer call through an ESM named or namespace import of + * node:timers. Node's mock.timers replaces the globals and the + * module object's properties, which these bindings do not follow, + * so the call keeps the loop's timer. */ + realTimer?: true; /** An assertion call made through a node:assert Assert instance: the * instance options the runtime applies to this call. Backends set * them right before the runtime call, after its arguments are @@ -3573,8 +3609,17 @@ export type IrExpr = * `null` and -0 as `0`, exactly like JS; a record field holding the * undefined arm of its union (an optional field) is DROPPED from the * output — Node's rule for undefined-valued properties. The value is - * BORROWED; the result string is owned (+1). Never throws. */ - | { kind: "jsonStringify"; value: IrExpr; indent?: string; type: IrType; loc: SrcLoc } + * BORROWED; the result string is owned (+1). Never throws. `circular` + * is util.format's %j (Node's tryStringify): a circular structure gives + * the text "[Circular]" instead of the TypeError. */ + | { + kind: "jsonStringify"; + value: IrExpr; + indent?: string; + circular?: true; + type: IrType; + loc: SrcLoc; + } /** The dynamic-boundary check — a CHECKED cast `dynValue as T`: validate * the dyn value's JSON dyn against `type` (a non-dyn, JSON-representable * IR type) and BUILD the typed value (+1), or THROW a catchable @@ -4010,6 +4055,7 @@ function canBoxDynCompositeAt( case "regex": case "url": case "searchParams": + case "date": return true; case "func": return canBoxFuncIntoDyn(t, getRecord, getUnion, visiting); diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 80a9c9b570..d16edfabd0 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -34,6 +34,9 @@ export interface RuntimeFeatures { parseArgs: boolean; fsWatch: boolean; nodeTest: boolean; + /** The program calls mock.timers members: its timer calls go through + * scr_test.c's mock-aware entry points. */ + mockTimers: boolean; dgram: boolean; http: boolean; http2: boolean; @@ -205,6 +208,7 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run parseArgs: false, fsWatch: false, nodeTest: false, + mockTimers: false, dgram: false, http: false, http2: false, @@ -252,6 +256,7 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run fn === "rl.questionAsyncSignal" || fn === "http.clientSignal" || fn === "tp.setTimeoutSignal" || + fn === "tp.schedulerWaitSignal" || fn === "tp.setImmediateSignal" || fn === "stream.addAbortSignal" || fn === "stream.addAbortSignalFn" || @@ -288,6 +293,7 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run fn === "stream.destroy" || fn === "stream.destroyErr" || fn === "stream.prop" || + fn === "stream.workerStdio" || fn === "stream.errored" || fn === "sp.finished" || fn === "sp.pipeline" || @@ -377,7 +383,8 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run fn.startsWith("sym.") || fn === "util.isDeepStrictEqual" || fn === "dyn.usingMethod" || - fn === "dyn.usingDispose" + fn === "dyn.usingDispose" || + fn.startsWith("disposable.") ) features.symbol = true; if (fn === "process.hrtimeValue") features.bigint = true; @@ -409,6 +416,9 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run if (fn.startsWith("test.")) { features.nodeTest = true; features.inspect = true; + if (fn.startsWith("test.mockTimers") || fn.startsWith("test.ctxMockTimers")) { + features.mockTimers = true; + } } if (fn.startsWith("dgram.") || fn.startsWith("dns.")) features.dgram = true; if (fn.startsWith("tlsca.")) features.tlsCa = true; @@ -793,6 +803,14 @@ export function moduleUsesNodeTest(mod: IrModule): boolean { return scanRuntimeFeatures(mod, "nodeTest").nodeTest; } +/** True when the module calls a mock.timers member (a test.mockTimers* or + * test.ctxMockTimers* libCall). The emitter then routes the timer calls + * through scr_test.c's mock-aware entry points; other programs keep the + * loop's timer functions. */ +export function moduleUsesMockTimers(mod: IrModule): boolean { + return scanRuntimeFeatures(mod, "mockTimers").mockTimers; +} + /** True when the module contains any dgram.* or dns.* libCall — the * link switch that pulls scr_dgram.c into the binary and has the emitted * main call scr_dgram_install (native-toolchain.ts + emitter; the scr_net gating diff --git a/packages/compiler/src/ir/validate.test.ts b/packages/compiler/src/ir/validate.test.ts index d44a187f44..b7caf8c41b 100644 --- a/packages/compiler/src/ir/validate.test.ts +++ b/packages/compiler/src/ir/validate.test.ts @@ -951,6 +951,7 @@ test("library callbacks retain child, specialized, and generic result diagnostic { kind: "strLit", value: "tool", type: STRING, loc }, { kind: "arrayLit", elems: [], type: arrayOf(STRING), loc }, { kind: "boolLit", value: true, type: F64, loc }, + { kind: "boolLit", value: false, type: BOOL, loc }, ], }; expect(validateModule(expressionModule(expr, [])).map((error) => error.message)).toEqual([ diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index d526e7cb8d..f7d30a1bba 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -357,7 +357,7 @@ export function validateModule(mod: IrModule): IrValidationError[] { const fits = (cls: string, t: IrType): boolean => cls === "bool" ? t.kind === "bool" - : cls === "string" + : cls === "string" || cls === "cstring" ? t.kind === "string" : cls === "bytes" ? t.kind === "bytes" && t.elem === "u8" @@ -1932,8 +1932,8 @@ function validateFunction( } case "regexLit": { if (e.type.kind !== "regex") err("regexLit must be regex-typed", e.loc); - if (!/^[gimsuy]*$/.test(e.flags)) { - err(`regexLit flags "${e.flags}" outside the supported alphabet (gimsuy)`, e.loc); + if (!/^[gimsuvy]*$/.test(e.flags)) { + err(`regexLit flags "${e.flags}" outside the supported alphabet (gimsuvy)`, e.loc); } if (new Set(e.flags).size !== e.flags.length) { err(`regexLit flags "${e.flags}" contain a duplicate`, e.loc); @@ -4116,9 +4116,12 @@ function validateFunction( e.fn === "tp.setTimeoutValue" || e.fn === "tp.setImmediateValue" || e.fn === "tp.setTimeoutSignal" || - e.fn === "tp.setImmediateSignal" + e.fn === "tp.setImmediateSignal" || + e.fn === "tp.schedulerWaitValue" || + e.fn === "tp.schedulerWaitSignal" ) { - const value = e.args[e.fn === "tp.setTimeoutValue" || e.fn === "tp.setTimeoutSignal" ? 1 : 0]; + const value = + e.args[e.fn === "tp.setImmediateValue" || e.fn === "tp.setImmediateSignal" ? 0 : 1]; if (value?.type.kind !== "promise" || !typeEquals(e.type, value.type)) { err(`libCall ${e.fn} must be the value's promise type`, e.loc); } @@ -5692,6 +5695,8 @@ function validateFunction( e.fn !== "emitter.setDefaultMax" && e.fn !== "emitter.setDefaultMaxChk" && e.fn !== "emitter.getDefaultMax" && + e.fn !== "emitter.esmDefaultMax" && + e.fn !== "emitter.esmSync" && e.fn !== "emitter.errorMonitor" && e.fn !== "emitter.checkListener" ) { diff --git a/packages/compiler/src/library/ir-policy.ts b/packages/compiler/src/library/ir-policy.ts index 51a5c3b9cc..3c3855e5ca 100644 --- a/packages/compiler/src/library/ir-policy.ts +++ b/packages/compiler/src/library/ir-policy.ts @@ -66,6 +66,11 @@ const LIB_MODE_REFUSED_PREFIXES: readonly [string, string][] = [ ["worker.new", "native worker threads"], ["worker.channel", "native worker threads"], ["worker.receiveMessageOnPort", "native worker threads"], + ["worker.setEnvironmentData", "native worker threads"], + ["worker.getEnvironmentData", "native worker threads"], + ["worker.markUntransferable", "native worker threads"], + ["worker.isMarkedUntransferable", "native worker threads"], + ["worker.broadcastChannel", "native worker threads"], ]; /** Value/type kinds whose mere presence means an excluded unit's code (or @@ -191,6 +196,7 @@ export const LIB_NONDETERMINISTIC_PREFIXES: readonly [string, string][] = [ ["blob.fileNew", "the live clock (File lastModified)"], ["process.env", "environment reads"], ["process.argv", "process.argv"], + ["process.execArgv", "process.execArgv"], ["process.cwd", "process.cwd"], ["process.chdir", "process.chdir"], ["process.pid", "process identity"], diff --git a/packages/compiler/src/library/library-profile.ts b/packages/compiler/src/library/library-profile.ts index f0126c25f2..019dbb7e1a 100644 --- a/packages/compiler/src/library/library-profile.ts +++ b/packages/compiler/src/library/library-profile.ts @@ -10,7 +10,7 @@ * * Ratified shape (the ratified library-mode design §5.1 + the ABI ratification session): * { - * "profile_format": 1, + * "profile_format": 1 | 2, // 2 adds the cstring class * "name": "", * "entry": "src/lib.ts", // ONE module, profile-relative * "emission": "llvm", // pins the emission; no fallback @@ -268,6 +268,7 @@ export const LIB_PARAM_CLASSES = [ "f64", "bool", "string", + "cstring", "bytes", "u8", "u32", @@ -282,7 +283,16 @@ export const LIB_PARAM_CLASSES = [ * u8/u32/i32 plumbing classes stay param-only: they were ratified as * inbound plumbing, and outbound integers are the proven classes' * business. */ -export const LIB_RETURN_CLASSES = ["f64", "bool", "string", "bytes", "void", "i64", "u64"] as const; +export const LIB_RETURN_CLASSES = [ + "f64", + "bool", + "string", + "cstring", + "bytes", + "void", + "i64", + "u64", +] as const; /** Ask 4's declared integer classes (the prove-or-refuse pair). */ export type LibIntClass = "i64" | "u64"; @@ -297,7 +307,16 @@ export type LibReturnClass = (typeof LIB_RETURN_CLASSES)[number]; * declared integer classes stay export-map surface: an outbound i64/u64 * demands the prove-or-refuse machinery, which is scoped to export * returns today. */ -const LIB_CALLBACK_PARAM_CLASSES = ["f64", "bool", "string", "bytes", "u8", "u32", "i32"] as const; +const LIB_CALLBACK_PARAM_CLASSES = [ + "f64", + "bool", + "string", + "cstring", + "bytes", + "u8", + "u32", + "i32", +] as const; /** Marshalling classes legal in a CALLBACK RETURN position (inbound: host * → compiled code). Scalars only — every conversion to f64 is exact by * construction; a buffer return would need an ownership contract the mode @@ -377,7 +396,7 @@ export interface LibrarySidecarConfig { } export interface LibraryProfile { - profileFormat: 1; + profileFormat: 1 | 2; name: string; /** The profile file that produced this configuration. Semantic * designation errors anchor here, where the offending name is written. */ @@ -516,8 +535,16 @@ export function loadLibraryProfile( } const p = raw as Record; const format = req(p["profile_format"], "profile_format", "number"); - if (format !== 1) - throw new ProfileError(`unsupported profile_format ${format} (this scriptc reads format 1)`); + if (format !== 1 && format !== 2) + throw new ProfileError( + `unsupported profile_format ${format} (this scriptc reads formats 1 and 2)`, + ); + // The cstring class (a NUL-terminated `const char *`) is format 2. + const cstringClass = (cls: string, path: string): void => { + if (cls === "cstring" && format < 2) { + throw new ProfileError(`'${path}' class 'cstring' requires profile_format 2`); + } + }; // Root keys are strict (checked AFTER the format gate, so a future // format's profile refuses as the wrong format, not as a typo). The // ask-5 key names get the pointed message: a `fences` array pasted at @@ -656,6 +683,7 @@ export function loadLibraryProfile( `'${path}.params[${j}]' must be one of ${LIB_PARAM_CLASSES.join("/")}, got ${JSON.stringify(c)}`, ); } + cstringClass(c, `${path}.params[${j}]`); return c as LibParamClass; }); const returns = req(ee["returns"], `${path}.returns`, "string"); @@ -666,6 +694,7 @@ export function loadLibraryProfile( : `'${path}.returns' must be one of ${LIB_RETURN_CLASSES.join("/")}, got '${returns}'`; throw new ProfileError(detail); } + cstringClass(returns, `${path}.returns`); entries.push({ export: exportName, symbol, params, returns: returns as LibReturnClass }); }); @@ -709,12 +738,13 @@ export function loadLibraryProfile( `'${path}.params[${j}]' must be one of ${LIB_CALLBACK_PARAM_CLASSES.join("/")}, got ${JSON.stringify(cls)}`, ); } + cstringClass(cls, `${path}.params[${j}]`); return cls as LibCallbackParamClass; }); const cbReturns = req(cc["returns"], `${path}.returns`, "string"); if (!(LIB_CALLBACK_RETURN_CLASSES as readonly string[]).includes(cbReturns)) { const detail = - cbReturns === "string" || cbReturns === "bytes" + cbReturns === "string" || cbReturns === "cstring" || cbReturns === "bytes" ? `'${path}.returns': a callback return stays scalar — a buffer flowing host → library needs an ownership contract the mode does not define; pass buffers as callback parameters (library → host, borrowed for the call) or entry parameters instead` : `'${path}.returns' must be one of ${LIB_CALLBACK_RETURN_CLASSES.join("/")}, got '${cbReturns}'`; throw new ProfileError(detail); @@ -1010,7 +1040,7 @@ export function loadLibraryProfile( return { ok: true, profile: { - profileFormat: 1, + profileFormat: format as 1 | 2, name, profilePath, entry, diff --git a/packages/compiler/src/library/prepare.ts b/packages/compiler/src/library/prepare.ts index 9c6fe03fc6..94f20198ad 100644 --- a/packages/compiler/src/library/prepare.ts +++ b/packages/compiler/src/library/prepare.ts @@ -65,6 +65,7 @@ function libClassFits(cls: string, t: IrType): boolean { case "bool": return t.kind === "bool"; case "string": + case "cstring": return t.kind === "string"; case "bytes": return t.kind === "bytes" && t.elem === "u8"; @@ -178,6 +179,18 @@ function resolveLibrarySection( profileRemediation(profile, LIB_INBOUND_BYTES_TRAP_CODE), ); } + if (e.params.includes("cstring")) { + // The host-contract trap for a NULL cstring parameter: a C string + // argument is a pointer to NUL-terminated text, never NULL. Same + // code and assembly-once discipline as the bytes and integer traps. + resolvedExport.inboundCstringTrap = assembleTrapTeaching( + profileTeaching(profile, LIB_INBOUND_BYTES_TRAP_CODE) ?? + "scriptc: library inbound cstring parameter is NULL\n", + LIB_INBOUND_BYTES_TRAP_CODE, + e.symbol, + profileRemediation(profile, LIB_INBOUND_BYTES_TRAP_CODE), + ); + } if (e.params.includes("i64") || e.params.includes("u64")) { // The sibling host-contract trap for inbound declared-integer // parameters (ask 4): a value past ±(2^53−1) cannot ride f64 @@ -524,10 +537,12 @@ export function prepareLibrary( // SC4024 diagnostics, unused channels legal, undeclared references // refused with the callback teaching. The library lane never loads a // native-FFI manifest, so the channel set owns the surface outright. + // A cstring channel parameter is a string on the TypeScript side; the + // channel's own classes (lib.callbacks) drive the marshalling. const cbImports: IrFfiImport[] = profile.callbacks.map((cb) => ({ name: cb.name, symbol: cb.name, - params: [...cb.params], + params: cb.params.map((cls) => (cls === "cstring" ? "string" : cls)), returns: cb.returns, })); const integerSlotRoots: string[] = []; diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index 63efde3782..c01101f6f3 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -192,7 +192,7 @@ "name": "this regex feature", "status": "unsupported", "code": "SC1120", - "note": "supported: literal regexes with the g/i/m/s/u/y flags — .test(), .exec()/.match()/.matchAll(), named capture groups (.groups, $ templates, \\k), .source/.flags, and string replace/replaceAll/split with string replacement templates" + "note": "supported: literal regexes with the g/i/m/s/u/v/y flags — .test(), .exec()/.match()/.matchAll(), named capture groups (.groups, $ templates, \\k), .source/.flags, and string replace/replaceAll/split with string replacement templates" }, { "id": "diagnostic.sc2001", @@ -1367,7 +1367,7 @@ "name": "module.syncBuiltinESMExports", "status": "unsupported", "code": "SC2020", - "note": "the direct zero-argument call is a no-op because compiled builtin exports are immutable and cannot become stale" + "note": "the direct zero-argument call statement refreshes the ES module namespace copy of events.defaultMaxListeners, the one builtin export a program can change" }, { "id": "node-builtin.net", @@ -1512,6 +1512,12 @@ "name": "os.release", "status": "static" }, + { + "id": "node-builtin.os.setPriority", + "kind": "node-builtin", + "name": "os.setPriority", + "status": "static" + }, { "id": "node-builtin.os.tmpdir", "kind": "node-builtin", @@ -1938,6 +1944,12 @@ "status": "static", "note": "reads, writes, deletes, and enumeration of the process environment (the process global)" }, + { + "id": "node-builtin.process.execArgv", + "kind": "node-builtin", + "name": "process.execArgv", + "status": "static" + }, { "id": "node-builtin.process.execPath", "kind": "node-builtin", @@ -2501,12 +2513,66 @@ "name": "util/types.isAsyncFunction", "status": "static" }, + { + "id": "node-builtin.util.types.isBigInt64Array", + "kind": "node-builtin", + "name": "util/types.isBigInt64Array", + "status": "static" + }, + { + "id": "node-builtin.util.types.isBigIntObject", + "kind": "node-builtin", + "name": "util/types.isBigIntObject", + "status": "static" + }, + { + "id": "node-builtin.util.types.isBigUint64Array", + "kind": "node-builtin", + "name": "util/types.isBigUint64Array", + "status": "static" + }, + { + "id": "node-builtin.util.types.isBooleanObject", + "kind": "node-builtin", + "name": "util/types.isBooleanObject", + "status": "static" + }, + { + "id": "node-builtin.util.types.isBoxedPrimitive", + "kind": "node-builtin", + "name": "util/types.isBoxedPrimitive", + "status": "static" + }, + { + "id": "node-builtin.util.types.isCryptoKey", + "kind": "node-builtin", + "name": "util/types.isCryptoKey", + "status": "static" + }, { "id": "node-builtin.util.types.isDataView", "kind": "node-builtin", "name": "util/types.isDataView", "status": "static" }, + { + "id": "node-builtin.util.types.isDate", + "kind": "node-builtin", + "name": "util/types.isDate", + "status": "static" + }, + { + "id": "node-builtin.util.types.isExternal", + "kind": "node-builtin", + "name": "util/types.isExternal", + "status": "static" + }, + { + "id": "node-builtin.util.types.isFloat16Array", + "kind": "node-builtin", + "name": "util/types.isFloat16Array", + "status": "static" + }, { "id": "node-builtin.util.types.isFloat32Array", "kind": "node-builtin", @@ -2549,18 +2615,36 @@ "name": "util/types.isInt8Array", "status": "static" }, + { + "id": "node-builtin.util.types.isKeyObject", + "kind": "node-builtin", + "name": "util/types.isKeyObject", + "status": "static" + }, { "id": "node-builtin.util.types.isMap", "kind": "node-builtin", "name": "util/types.isMap", "status": "static" }, + { + "id": "node-builtin.util.types.isMapIterator", + "kind": "node-builtin", + "name": "util/types.isMapIterator", + "status": "static" + }, { "id": "node-builtin.util.types.isNativeError", "kind": "node-builtin", "name": "util/types.isNativeError", "status": "static" }, + { + "id": "node-builtin.util.types.isNumberObject", + "kind": "node-builtin", + "name": "util/types.isNumberObject", + "status": "static" + }, { "id": "node-builtin.util.types.isPromise", "kind": "node-builtin", @@ -2585,6 +2669,30 @@ "name": "util/types.isSet", "status": "static" }, + { + "id": "node-builtin.util.types.isSetIterator", + "kind": "node-builtin", + "name": "util/types.isSetIterator", + "status": "static" + }, + { + "id": "node-builtin.util.types.isSharedArrayBuffer", + "kind": "node-builtin", + "name": "util/types.isSharedArrayBuffer", + "status": "static" + }, + { + "id": "node-builtin.util.types.isStringObject", + "kind": "node-builtin", + "name": "util/types.isStringObject", + "status": "static" + }, + { + "id": "node-builtin.util.types.isSymbolObject", + "kind": "node-builtin", + "name": "util/types.isSymbolObject", + "status": "static" + }, { "id": "node-builtin.util.types.isTypedArray", "kind": "node-builtin", @@ -4500,6 +4608,13 @@ "status": "static", "note": "compiles statically at arity 1; other declared call shapes are refused with SC2012" }, + { + "id": "stdlib.math.f16round", + "kind": "stdlib", + "name": "Math.f16round", + "status": "static", + "note": "compiles statically at arity 1; other declared call shapes are refused with SC2012" + }, { "id": "stdlib.math.floor", "kind": "stdlib", diff --git a/packages/compiler/test/class-early-field-reads.test.ts b/packages/compiler/test/class-early-field-reads.test.ts new file mode 100644 index 0000000000..c602a2f42a --- /dev/null +++ b/packages/compiler/test/class-early-field-reads.test.ts @@ -0,0 +1,93 @@ +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, validateModule } from "../src/index.js"; +import type { IrModule } from "../src/ir/ir.js"; + +async function lower(source: string, file = "main.ts"): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-early-field-reads-")); + try { + const entry = join(dir, file), + output = join(dir, "main.ir.json"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath: output, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((item) => `${item.code}: ${item.message}`).join("\n")); + const module = deserializeModule(await readFile(output, "utf8")); + expect(validateModule(module)).toEqual([]); + return module; + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +/** Field name → IR kind for a class. */ +function fields(module: IrModule, name: string): string { + const cls = module.classes.find((candidate) => candidate.name === name); + expect(cls, name).toBeDefined(); + return cls!.fields + .filter((field) => !field.name.startsWith("%")) + .map((field) => `${field.name}:${field.type.kind}`) + .join(" "); +} + +test("fields assigned after construction can call an override admit undefined", async () => { + const module = await lower(` +abstract class Base { + constructor() { + this.describe(); + } + abstract describe(): void; +} +class Panel extends Base { + label: string; + width: number; + count = 0; + constructor() { + super(); + this.label = "panel"; + this.width = 2; + } + describe(): void { + console.log(this.label, this.width, this.count); + } +} +class Plain { + label: string; + width: number; + constructor() { + this.label = "plain"; + this.width = 1; + this.describe(); + } + describe(): void { + console.log(this.label, this.width); + } +} +new Panel(); +new Plain(); +`); + expect(fields(module, "Panel")).toBe("label:union width:union count:union"); + expect(fields(module, "Plain")).toBe("label:string width:f64"); +}); + +test("JavaScript fields created after the constructor calls a method admit undefined", async () => { + const module = await lower( + ` +class Widget { + constructor() { + this.ready = true; + this.render(); + this.title = "widget"; + } + render() { + console.log(this.ready, this.title); + } +} +new Widget(); +`, + "main.js", + ); + expect(fields(module, "Widget")).toBe("ready:bool title:union"); +}); diff --git a/packages/compiler/test/class-method-mutations.test.ts b/packages/compiler/test/class-method-mutations.test.ts index 50c8a2ae2e..2258bd54a5 100644 --- a/packages/compiler/test/class-method-mutations.test.ts +++ b/packages/compiler/test/class-method-mutations.test.ts @@ -87,3 +87,30 @@ test("an asserted string key keeps checked dispatch", async () => { ); expect(await callersOf(source, "%dyn.class.properties")).not.toEqual([]); }); + +const tally = ` +class Tally { + n = 0; + add(): void { this.n++; } +} +function entry(target: object, key: string, value: number): TYPE { + return LITERAL; +} +const tally = new Tally(); +tally.add(); +console.log(tally.n, Object.keys(entry(tally, String(tally.n), 2))); +`; + +test("a computed key in an object literal leaves method calls direct", async () => { + const source = tally + .replace("TYPE", "Record") + .replace("LITERAL", "{ [key]: value }"); + expect(await callersOf(source, "%dyn.class.properties")).toEqual([]); +}); + +test("a computed key in an Object.assign source keeps checked dispatch", async () => { + const source = tally + .replace("TYPE", "object") + .replace("LITERAL", "Object.assign(target, { [key]: value })"); + expect(await callersOf(source, "%dyn.class.properties")).not.toEqual([]); +}); diff --git a/packages/compiler/test/native-codegen-integration.test.ts b/packages/compiler/test/native-codegen-integration.test.ts index fb00116cce..1c5f9e3e77 100644 --- a/packages/compiler/test/native-codegen-integration.test.ts +++ b/packages/compiler/test/native-codegen-integration.test.ts @@ -205,6 +205,72 @@ ${[0, 1, 2, 3, 4, 5].map((index) => ` call void @add${index}(i64 ${index + 2})` }); }); + test("WebAssembly emission gives irreducible loops a single entry", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-helper-irreducible-")); + dirs.push(dir); + const input = join(dir, "loops.ll"); + // @irreducible enters its loop at %a or %b; @reducible has one loop entry. + await writeFile( + input, + `declare void @step(i32) + +define i32 @irreducible(i32 %n, i1 %c) { +entry: + br i1 %c, label %a, label %b +a: + %ia = phi i32 [ 0, %entry ], [ %ib1, %b ] + call void @step(i32 %ia) + %ia1 = add i32 %ia, 1 + %da = icmp slt i32 %ia1, %n + br i1 %da, label %b, label %done +b: + %ib = phi i32 [ 0, %entry ], [ %ia1, %a ] + call void @step(i32 %ib) + %ib1 = add i32 %ib, 2 + %db = icmp slt i32 %ib1, %n + br i1 %db, label %a, label %done +done: + %r = phi i32 [ %ia1, %a ], [ %ib1, %b ] + ret i32 %r +} + +define i32 @reducible(i32 %n) { +entry: + br label %loop +loop: + %i = phi i32 [ 0, %entry ], [ %i1, %loop ] + call void @step(i32 %i) + %i1 = add i32 %i, 1 + %d = icmp slt i32 %i1, %n + br i1 %d, label %loop, label %done +done: + ret i32 %i1 +} +`, + ); + const wasm = "wasm32-unknown-wasi"; + const assemblyPath = join(dir, "loops.s"); + const assemblyArgs = helperArgs(input, assemblyPath, wasm); + assemblyArgs[assemblyArgs.indexOf("--filetype") + 1] = "asm"; + expect((await run(helper, assemblyArgs)).exitCode).toBe(0); + const assembly = await readFile(assemblyPath, "utf8"); + const body = (name: string) => + assembly.slice(assembly.indexOf(`\n${name}:\n`)).split("end_function")[0]!; + // The dispatch block that replaces the second loop entry. + expect(body("irreducible")).toMatch(/\bbr_table\b/); + expect(body("reducible")).not.toMatch(/\bbr_table\b/); + + const unoptimized = helperArgs(input, join(dir, "dev.o"), wasm); + unoptimized[unoptimized.indexOf("--opt-level") + 1] = "0"; + expect((await run(helper, unoptimized)).exitCode).toBe(0); + const partitioned = [ + ...helperArgs(input, join(dir, "a.o"), wasm), + "--output", + join(dir, "b.o"), + ]; + expect((await run(helper, partitioned)).exitCode).toBe(0); + }); + test("LLVM fatal diagnostics remain process-isolated JSON", async () => { const result = await execFileAsync(helper, ["version", "--format=json"], { encoding: "utf8", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5023-lifetime-region-await.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5023-lifetime-region-await.ts.json new file mode 100644 index 0000000000..9e7d481300 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5023-lifetime-region-await.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5023-lifetime-region-await.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5024-lifetime-region-await-forms.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5024-lifetime-region-await-forms.ts.json new file mode 100644 index 0000000000..87cb98c435 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5024-lifetime-region-await-forms.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5024-lifetime-region-await-forms.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5627-exec-callback.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5627-exec-callback.ts.json new file mode 100644 index 0000000000..cbefe8acca --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5627-exec-callback.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5627-exec-callback.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5681-array-pop-and-length.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5681-array-pop-and-length.ts.json new file mode 100644 index 0000000000..d20701cae3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5681-array-pop-and-length.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5681-array-pop-and-length.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5800-broadcast-channel.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5800-broadcast-channel.ts.json new file mode 100644 index 0000000000..9858f853a7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5800-broadcast-channel.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5800-broadcast-channel.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5801-broadcast-channel.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5801-broadcast-channel.cjs.json new file mode 100644 index 0000000000..56a80c6eae --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5801-broadcast-channel.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5801-broadcast-channel.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5802-broadcast-channel-workers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5802-broadcast-channel-workers.ts.json new file mode 100644 index 0000000000..5e3522da6d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5802-broadcast-channel-workers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5802-broadcast-channel-workers.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5803-worker-environment-data.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5803-worker-environment-data.ts.json new file mode 100644 index 0000000000..397c5afe29 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5803-worker-environment-data.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5803-worker-environment-data.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5804-worker-environment-data.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5804-worker-environment-data.cjs.json new file mode 100644 index 0000000000..da385edab0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5804-worker-environment-data.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5804-worker-environment-data.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5805-worker-receive-message-parent-port.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5805-worker-receive-message-parent-port.ts.json new file mode 100644 index 0000000000..0976b291e1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5805-worker-receive-message-parent-port.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5805-worker-receive-message-parent-port.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5806-worker-resource-limits.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5806-worker-resource-limits.ts.json new file mode 100644 index 0000000000..2340ef776b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5806-worker-resource-limits.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5806-worker-resource-limits.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5807-worker-error-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5807-worker-error-values.ts.json new file mode 100644 index 0000000000..a116df7f1a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5807-worker-error-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5807-worker-error-values.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5808-worker-unhandled-rejections.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5808-worker-unhandled-rejections.ts.json new file mode 100644 index 0000000000..ea84f13087 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5808-worker-unhandled-rejections.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5808-worker-unhandled-rejections.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5809-process-exit-and-rejections.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5809-process-exit-and-rejections.cjs.json new file mode 100644 index 0000000000..21816332e1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5809-process-exit-and-rejections.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5809-process-exit-and-rejections.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5813-mock-timers-module.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5813-mock-timers-module.ts.json new file mode 100644 index 0000000000..bc8a46ae13 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5813-mock-timers-module.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5813-mock-timers-module.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5814-mock-timers-date.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5814-mock-timers-date.ts.json new file mode 100644 index 0000000000..edb0dba8cb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5814-mock-timers-date.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5814-mock-timers-date.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5831-util-types-brands.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5831-util-types-brands.ts.json new file mode 100644 index 0000000000..64ba3cdd44 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5831-util-types-brands.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5831-util-types-brands.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5832-util-types-brands-dynamic.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5832-util-types-brands-dynamic.js.json new file mode 100644 index 0000000000..9ed42a8cec --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5832-util-types-brands-dynamic.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5832-util-types-brands-dynamic.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5833-url-search-params-iterators.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5833-url-search-params-iterators.ts.json new file mode 100644 index 0000000000..b1626d547c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5833-url-search-params-iterators.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5833-url-search-params-iterators.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5834-url-search-params-iterators-dynamic.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5834-url-search-params-iterators-dynamic.mjs.json new file mode 100644 index 0000000000..3a60dda608 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5834-url-search-params-iterators-dynamic.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5834-url-search-params-iterators-dynamic.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5835-util-format-integer-specifiers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5835-util-format-integer-specifiers.ts.json new file mode 100644 index 0000000000..07c8e6c793 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5835-util-format-integer-specifiers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5835-util-format-integer-specifiers.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5836-url-search-params-tuple-pairs.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5836-url-search-params-tuple-pairs.ts.json new file mode 100644 index 0000000000..20a6b62bf7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5836-url-search-params-tuple-pairs.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5836-url-search-params-tuple-pairs.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5837-util-format-json-circular.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5837-util-format-json-circular.ts.json new file mode 100644 index 0000000000..55daf6bd83 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5837-util-format-json-circular.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5837-util-format-json-circular.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5838-qs-stringify-bigint.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5838-qs-stringify-bigint.ts.json new file mode 100644 index 0000000000..0c1b09788e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5838-qs-stringify-bigint.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5838-qs-stringify-bigint.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5841-json-typed-dyn-members.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5841-json-typed-dyn-members.ts.json new file mode 100644 index 0000000000..08d5b62db4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5841-json-typed-dyn-members.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5841-json-typed-dyn-members.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5842-qs-parse-null-prototype.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5842-qs-parse-null-prototype.ts.json new file mode 100644 index 0000000000..a5b4a3e218 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5842-qs-parse-null-prototype.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5842-qs-parse-null-prototype.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5843-qs-parse-null-prototype-js.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5843-qs-parse-null-prototype-js.mjs.json new file mode 100644 index 0000000000..228f0a4c0b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5843-qs-parse-null-prototype-js.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5843-qs-parse-null-prototype-js.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5844-date-unknown-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5844-date-unknown-values.ts.json new file mode 100644 index 0000000000..7331cf7f0e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5844-date-unknown-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5844-date-unknown-values.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5846-events-esm-default-max.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5846-events-esm-default-max.ts.json new file mode 100644 index 0000000000..f1867c914b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5846-events-esm-default-max.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5846-events-esm-default-max.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5847-events-cjs-default-max.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5847-events-cjs-default-max.cjs.json new file mode 100644 index 0000000000..1464ab2cc4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5847-events-cjs-default-max.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5847-events-cjs-default-max.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5857-regexp-unicode-sets.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5857-regexp-unicode-sets.ts.json new file mode 100644 index 0000000000..20742aa5ae --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5857-regexp-unicode-sets.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5857-regexp-unicode-sets.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5858-disposable-stack.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5858-disposable-stack.ts.json new file mode 100644 index 0000000000..316261387c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5858-disposable-stack.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5858-disposable-stack.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5859-disposable-stack-js.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5859-disposable-stack-js.js.json new file mode 100644 index 0000000000..f35375f637 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5859-disposable-stack-js.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5859-disposable-stack-js.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5870-worker-thread-names.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5870-worker-thread-names.ts.json new file mode 100644 index 0000000000..2c16d66dbf --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5870-worker-thread-names.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5870-worker-thread-names.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5871-worker-untransferable.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5871-worker-untransferable.cjs.json new file mode 100644 index 0000000000..df294877c8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5871-worker-untransferable.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5871-worker-untransferable.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5874-worker-stdio-streams.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5874-worker-stdio-streams.ts.json new file mode 100644 index 0000000000..12434a7573 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5874-worker-stdio-streams.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5874-worker-stdio-streams.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5875-worker-stdout-pipe.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5875-worker-stdout-pipe.cjs.json new file mode 100644 index 0000000000..64115b7461 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5875-worker-stdout-pipe.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5875-worker-stdout-pipe.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5879-worker-stdin.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5879-worker-stdin.ts.json new file mode 100644 index 0000000000..9b7dbdcd7b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5879-worker-stdin.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5879-worker-stdin.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5881-inspect-weak-collections.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5881-inspect-weak-collections.ts.json new file mode 100644 index 0000000000..342179cde8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5881-inspect-weak-collections.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5881-inspect-weak-collections.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5882-string-decoder-runtime-encoding.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5882-string-decoder-runtime-encoding.ts.json new file mode 100644 index 0000000000..29b8b27592 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5882-string-decoder-runtime-encoding.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5882-string-decoder-runtime-encoding.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5883-os-set-priority.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5883-os-set-priority.ts.json new file mode 100644 index 0000000000..5be1d2b3cf --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5883-os-set-priority.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5883-os-set-priority.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5884-inspect-layout-options.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5884-inspect-layout-options.ts.json new file mode 100644 index 0000000000..17935d5a47 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5884-inspect-layout-options.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5884-inspect-layout-options.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5886-inspect-dates.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5886-inspect-dates.ts.json new file mode 100644 index 0000000000..65525fb9bf --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5886-inspect-dates.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5886-inspect-dates.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5887-inspect-typed-arrays.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5887-inspect-typed-arrays.ts.json new file mode 100644 index 0000000000..7c49ebfaa9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5887-inspect-typed-arrays.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5887-inspect-typed-arrays.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5888-inspect-sorted.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5888-inspect-sorted.ts.json new file mode 100644 index 0000000000..5e0eedde23 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5888-inspect-sorted.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5888-inspect-sorted.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5900-float16-array.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5900-float16-array.ts.json new file mode 100644 index 0000000000..47cf8723f6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5900-float16-array.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5900-float16-array.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5901-float16-array-js.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5901-float16-array-js.js.json new file mode 100644 index 0000000000..148849dca9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5901-float16-array-js.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5901-float16-array-js.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5904-async-generator-completion.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5904-async-generator-completion.ts.json new file mode 100644 index 0000000000..dd2338a5ba --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5904-async-generator-completion.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5904-async-generator-completion.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5905-array-from-async-generator.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5905-array-from-async-generator.ts.json new file mode 100644 index 0000000000..dd92769264 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5905-array-from-async-generator.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5905-array-from-async-generator.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5906-array-from-async-generator-js.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5906-array-from-async-generator-js.js.json new file mode 100644 index 0000000000..fc7977463d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5906-array-from-async-generator-js.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5906-array-from-async-generator-js.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5913-prototype-type-error-messages.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5913-prototype-type-error-messages.ts.json new file mode 100644 index 0000000000..f7e3d10b93 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5913-prototype-type-error-messages.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5913-prototype-type-error-messages.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5914-dyn-in-class-prototype-members.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5914-dyn-in-class-prototype-members.ts.json new file mode 100644 index 0000000000..4df0c23de2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5914-dyn-in-class-prototype-members.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5914-dyn-in-class-prototype-members.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5915-dyn-error-string-conversions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5915-dyn-error-string-conversions.ts.json new file mode 100644 index 0000000000..032b5c5ba3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5915-dyn-error-string-conversions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5915-dyn-error-string-conversions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5923-matchall-groups-js.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5923-matchall-groups-js.js.json new file mode 100644 index 0000000000..591c14d125 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5923-matchall-groups-js.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5923-matchall-groups-js.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5932-generic-class-observed-methods.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5932-generic-class-observed-methods.ts.json new file mode 100644 index 0000000000..e3a78d9d8e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5932-generic-class-observed-methods.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5932-generic-class-observed-methods.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5938-object-literal-computed-keys-keep-methods.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5938-object-literal-computed-keys-keep-methods.ts.json new file mode 100644 index 0000000000..356382374f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5938-object-literal-computed-keys-keep-methods.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5938-object-literal-computed-keys-keep-methods.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5939-js-let-object-reassigned-wider.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5939-js-let-object-reassigned-wider.js.json new file mode 100644 index 0000000000..b783afe3f4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5939-js-let-object-reassigned-wider.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5939-js-let-object-reassigned-wider.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5942-local-class-observed-methods.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5942-local-class-observed-methods.ts.json new file mode 100644 index 0000000000..89502dd27b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5942-local-class-observed-methods.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5942-local-class-observed-methods.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5943-fields-read-before-assignment.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5943-fields-read-before-assignment.ts.json new file mode 100644 index 0000000000..d2dcdcc242 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5943-fields-read-before-assignment.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5943-fields-read-before-assignment.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5944-js-fields-read-before-assignment.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5944-js-fields-read-before-assignment.js.json new file mode 100644 index 0000000000..e9800b71de --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5944-js-fields-read-before-assignment.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5944-js-fields-read-before-assignment.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5958-asserted-union-receiver.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5958-asserted-union-receiver.ts.json new file mode 100644 index 0000000000..d294caa65f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5958-asserted-union-receiver.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5958-asserted-union-receiver.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5959-date-to-json.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5959-date-to-json.ts.json new file mode 100644 index 0000000000..935958e1dd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5959-date-to-json.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5959-date-to-json.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5960-frame-records-own-references.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5960-frame-records-own-references.ts.json new file mode 100644 index 0000000000..afe039025d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5960-frame-records-own-references.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5960-frame-records-own-references.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5960-generic-mapped-types.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5960-generic-mapped-types.ts.json new file mode 100644 index 0000000000..a95837f802 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5960-generic-mapped-types.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5960-generic-mapped-types.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5961-frame-spread-updates.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5961-frame-spread-updates.ts.json new file mode 100644 index 0000000000..255b1238e3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5961-frame-spread-updates.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5961-frame-spread-updates.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5962-frame-tuples.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5962-frame-tuples.ts.json new file mode 100644 index 0000000000..ab8d80fafb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5962-frame-tuples.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5962-frame-tuples.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5973-large-closure-environment.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5973-large-closure-environment.ts.json new file mode 100644 index 0000000000..5455362850 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5973-large-closure-environment.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5973-large-closure-environment.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5974-typeof-module-exports.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5974-typeof-module-exports.cjs.json new file mode 100644 index 0000000000..469ee32729 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5974-typeof-module-exports.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5974-typeof-module-exports.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5990-worker-uncloneable.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5990-worker-uncloneable.mjs.json new file mode 100644 index 0000000000..93337e4a0d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5990-worker-uncloneable.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5990-worker-uncloneable.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5991-worker-async-dispose.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5991-worker-async-dispose.mjs.json new file mode 100644 index 0000000000..bed1b18a9d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5991-worker-async-dispose.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5991-worker-async-dispose.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5992-worker-thread-name-lifetime.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5992-worker-thread-name-lifetime.mjs.json new file mode 100644 index 0000000000..20ef551423 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5992-worker-thread-name-lifetime.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5992-worker-thread-name-lifetime.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6020-class-extends-function-constructor.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6020-class-extends-function-constructor.js.json new file mode 100644 index 0000000000..b2fa4c986f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6020-class-extends-function-constructor.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6020-class-extends-function-constructor.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6140-cjs-require-tdz-throws/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6140-cjs-require-tdz-throws/main.js.json new file mode 100644 index 0000000000..db768b31be --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6140-cjs-require-tdz-throws/main.js.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/6140-cjs-require-tdz-throws/lib.js", + "/tests/corpus/6140-cjs-require-tdz-throws/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6141-cjs-require-tdz-checked/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6141-cjs-require-tdz-checked/main.js.json new file mode 100644 index 0000000000..b074bf18d6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6141-cjs-require-tdz-checked/main.js.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/6141-cjs-require-tdz-checked/lib.js", + "/tests/corpus/6141-cjs-require-tdz-checked/counter.js", + "/tests/corpus/6141-cjs-require-tdz-checked/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6142-cjs-require-published-early/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6142-cjs-require-published-early/main.js.json new file mode 100644 index 0000000000..55720ef1a4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6142-cjs-require-published-early/main.js.json @@ -0,0 +1,13 @@ +{ + "order": [ + "/tests/corpus/6142-cjs-require-published-early/parse-options.js", + "/tests/corpus/6142-cjs-require-published-early/debug.js", + "/tests/corpus/6142-cjs-require-published-early/comparator.js", + "/tests/corpus/6142-cjs-require-published-early/helpers.js", + "/tests/corpus/6142-cjs-require-published-early/glob.js", + "/tests/corpus/6142-cjs-require-published-early/core.js", + "/tests/corpus/6142-cjs-require-published-early/bundle.js", + "/tests/corpus/6142-cjs-require-published-early/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6143-cjs-require-json/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6143-cjs-require-json/main.js.json new file mode 100644 index 0000000000..92fe169c31 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6143-cjs-require-json/main.js.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/6143-cjs-require-json/counter.js", + "/tests/corpus/6143-cjs-require-json/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6144-cjs-require-builtin-nested/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6144-cjs-require-builtin-nested/main.js.json new file mode 100644 index 0000000000..df78ca9ed9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6144-cjs-require-builtin-nested/main.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6144-cjs-require-builtin-nested/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6145-cjs-require-cycle/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6145-cjs-require-cycle/main.js.json new file mode 100644 index 0000000000..3e8122d1f2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6145-cjs-require-cycle/main.js.json @@ -0,0 +1,9 @@ +{ + "order": [ + "/tests/corpus/6145-cjs-require-cycle/range.js", + "/tests/corpus/6145-cjs-require-cycle/util.js", + "/tests/corpus/6145-cjs-require-cycle/comparator.js", + "/tests/corpus/6145-cjs-require-cycle/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/native-worker-discarded-heaps.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/native-worker-discarded-heaps.ts.json new file mode 100644 index 0000000000..b7dd8fd174 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/native-worker-discarded-heaps.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/native-worker-discarded-heaps.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/cjs-require-cycle/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/cjs-require-cycle/main.js.json index a54923bcfe..67d01f47e2 100644 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/cjs-require-cycle/main.js.json +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/cjs-require-cycle/main.js.json @@ -7,7 +7,7 @@ "diags": [ { "code": "SC1016", - "message": "circular imports (/tests/diagnostics/cjs-require-cycle/a.js → /tests/diagnostics/cjs-require-cycle/b.js → /tests/diagnostics/cjs-require-cycle/a.js; the cycle closes through a require() edge) are not supported yet", + "message": "circular imports (/tests/diagnostics/cjs-require-cycle/a.js → /tests/diagnostics/cjs-require-cycle/b.js → /tests/diagnostics/cjs-require-cycle/a.js; the cycle closes through a require() edge: /tests/diagnostics/cjs-require-cycle/a.js uses its exports at /tests/diagnostics/cjs-require-cycle/a.js:6 outside the export statements that precede its first require, so a module of the cycle can observe them before they settle) are not supported yet", "loc": { "file": "/tests/diagnostics/cjs-require-cycle/a.js", "start": 0, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/cjs-require-tdz-unchecked/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/cjs-require-tdz-unchecked/main.js.json new file mode 100644 index 0000000000..aaeea4cf27 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/cjs-require-tdz-unchecked/main.js.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/diagnostics/cjs-require-tdz-unchecked/lib.js", + "/tests/diagnostics/cjs-require-tdz-unchecked/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/cjs-require-tdz/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/cjs-require-tdz/main.js.json index d15d404ae9..205a8cd6ee 100644 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/cjs-require-tdz/main.js.json +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/cjs-require-tdz/main.js.json @@ -5,11 +5,11 @@ "diags": [ { "code": "SC1013", - "message": "require() below code that can reach its binding 'greet' (Node initializes the binding AT the require and throws ReferenceError on any earlier access, which is not modeled — move the require above the code that can run first) is not supported yet", + "message": "var require() binding 'lib' below code that can reach it (Node reads undefined before the require runs, which is not modeled — declare it with const or let, or move the require above the code that can run first) is not supported yet", "loc": { "file": "/tests/diagnostics/cjs-require-tdz/main.js", - "start": 336, - "end": 367 + "start": 402, + "end": 427 }, "milestone": "later" } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/mock-timers-fences.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/mock-timers-fences.ts.json new file mode 100644 index 0000000000..f8800b52c7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/mock-timers-fences.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/mock-timers-fences.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/snapshot-fences.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/snapshot-fences.ts.json new file mode 100644 index 0000000000..7449cca82e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/snapshot-fences.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/snapshot-fences.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/child-exec-code-fences.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/child-exec-code-fences.ts.json new file mode 100644 index 0000000000..8f8a2a4b89 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/child-exec-code-fences.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/child-exec-code-fences.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/child-exec.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/child-exec.ts.json new file mode 100644 index 0000000000..73db22acae --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/child-exec.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/child-exec.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/program-adapter.test.ts b/packages/compiler/test/ts7/program-adapter.test.ts index 660394a6fd..e7479c7498 100644 --- a/packages/compiler/test/ts7/program-adapter.test.ts +++ b/packages/compiler/test/ts7/program-adapter.test.ts @@ -119,7 +119,7 @@ ${uses} return required.value + value; } before(); -const required = require("./dep.cjs"); +var required = require("./dep.cjs"); console.log(required.value); `, ); @@ -144,6 +144,8 @@ console.log(required.value); }) as Checker; program.checkerFacade = new CheckerFacade(proxy, { project: program.project.checker.project }); + // A var binding keeps the SC1013 fence (Node reads undefined there); a + // const would compile with initialization checks from the same scan. expect(checkPreflight(load).map((diag) => diag.code)).toContain("SC1013"); expect(calls.some(([arg]) => Array.isArray(arg) && arg.length > 24)).toBe(true); expect(calls.some(([arg]) => !Array.isArray(arg))).toBe(false); diff --git a/packages/compiler/test/tuple-forof.test.ts b/packages/compiler/test/tuple-forof.test.ts index 3a66d9c099..78cc9376de 100644 --- a/packages/compiler/test/tuple-forof.test.ts +++ b/packages/compiler/test/tuple-forof.test.ts @@ -25,7 +25,12 @@ console.log(total, calls); const mod = deserializeModule(await readFile(outPath, "utf8")); expect(validateModule(mod)).toEqual([]); const llvm = emitLlvmModule(mod); - expect(llvm.match(/call ptr @sc_f_pair\(/g)).toHaveLength(1); + // The receiver is computed once: an ordinary call, or (when the loop + // only reads elements) the result-slot entry filling frame storage. + const calls = + (llvm.match(/call ptr @sc_f_pair\(/g) ?? []).length + + (llvm.match(/call void @sc_\w*into_pair\(ptr %s\d+\)/g) ?? []).length; + expect(calls).toBe(1); expect(llvm).not.toContain("@scr_arr_new"); } finally { await rm(dir, { recursive: true, force: true }); diff --git a/packages/experimental/README.md b/packages/experimental/README.md index 413deeff24..8a44053b0b 100644 --- a/packages/experimental/README.md +++ b/packages/experimental/README.md @@ -21,7 +21,7 @@ function rebuild(): Program { - `run(fn)` calls `fn` with the region as the current region and returns its result. `ProgramLifetime` instances created during the call belong to the region; calls nest. - Instances created outside every `run` call belong to the default region, which is never freed. -- Work that resumes after an `await` inside `fn` runs outside the region. +- Async work started inside `fn` keeps its region: what it creates after an `await` belongs to the region too, and the region is not freed before that work has finished. - `release()` requests that the region be freed. scriptc frees it once nothing outside the region refers to any of its instances; until then the region stays allocated. Later `run` calls on a released region allocate in the default region. - A region that holds a published object (`@scriptc/threads`) is never freed. diff --git a/packages/experimental/index.d.ts b/packages/experimental/index.d.ts index d831cbff96..057dcde09f 100644 --- a/packages/experimental/index.d.ts +++ b/packages/experimental/index.d.ts @@ -7,13 +7,14 @@ export declare class LifetimeRegion { constructor(); /** Calls `fn` with this region as the current region and returns its * result. ProgramLifetime instances created during the call, outside - * nested `run` calls of other regions, belong to this region. Work that - * resumes after an `await` inside `fn` runs outside the region. */ + * nested `run` calls of other regions, belong to this region, and so do + * those that async functions called during it create after an `await`. */ run(fn: () => T): T; /** Requests that the region be freed. Its instances are freed once - * nothing outside the region refers to any of them; until then the - * region stays allocated. Later `run` calls allocate outside the region. - * Never observable except through memory use. */ + * nothing outside the region refers to any of them and the async work + * started inside it has finished; until then the region stays allocated. + * Later `run` calls allocate outside the region. Never observable except + * through memory use. */ release(): void; } diff --git a/packages/runtime/src/scr_alloc.c b/packages/runtime/src/scr_alloc.c index eaf391a9e1..2212a1a364 100644 --- a/packages/runtime/src/scr_alloc.c +++ b/packages/runtime/src/scr_alloc.c @@ -630,6 +630,14 @@ static bool scr_sa_reserve_region(void) { * scr_sa_trim). */ static size_t scr_sa_claim_carved[SCR_SA_ARENAS]; static size_t scr_sa_carved(void); +/* A worker thread's memory, discarded when it parks (see "Discarding a + * worker's memory" below). */ +typedef struct ScrSaKeep ScrSaKeep; +static _Thread_local bool scr_sa_worker; /* set by scr_sa_thread_starting */ +static _Thread_local ScrSaKeep *scr_sa_keep; /* while the worker's memory is kept apart */ +static void scr_sa_keep_begin(unsigned i); +static void scr_sa_keep_drained(ScrSaKeep *k, ScrSaBlock *b, unsigned c); +static bool scr_sa_drain(void); #endif /* Moves every block other threads freed into this arena's free lists. */ @@ -638,6 +646,13 @@ static void scr_sa_drain_remote(void) { while (b) { ScrSaBlock *next = b->next; unsigned c = (unsigned)(((uintptr_t)b - scr_sa.base) >> scr_sa.shift); +#ifdef SCR_SA_TRIM + if (scr_sa_keep) { + scr_sa_keep_drained(scr_sa_keep, b, c); + b = next; + continue; + } +#endif b->next = scr_sa.free[c]; scr_sa.free[c] = b; b = next; @@ -655,6 +670,15 @@ static bool scr_sa_reserve(void) { uint32_t claimed = atomic_load_explicit(&scr_sa_claimed, memory_order_relaxed); for (;;) { if (claimed == UINT32_MAX) { +#ifdef SCR_SA_TRIM + /* Arenas held by discards that wait (see scr_sa_thread_park): this + * thread discards them and claims one. */ + if (scr_sa_drain()) { + scr_sa_refused = false; + claimed = atomic_load_explicit(&scr_sa_claimed, memory_order_relaxed); + continue; + } +#endif scr_sa_refused = true; return false; } @@ -672,6 +696,7 @@ static bool scr_sa_reserve(void) { scr_sa_drain_remote(); #ifdef SCR_SA_TRIM scr_sa_claim_carved[i] = scr_sa_carved(); + if (scr_sa_worker) scr_sa_keep_begin(i); #endif return true; } @@ -884,6 +909,312 @@ static void scr_sa_trim(void) { scr_sa_arena, (int)trim, scr_sa_trim_stats[0] >> 20, scr_sa_trim_stats[1] >> 20, scr_sa_trim_stats[2] >> 20); } + +/* Discarding a worker's memory (Linux). Script objects stay on the thread + * that allocated them: other threads reach one only through a message or an + * intern table that shares a published (immortal) graph by pointer, or a + * process-wide immortal cache, and each of those calls + * scr_sa_thread_shares. Runtime state that outlives a thread (messages, + * mailboxes, shared bytes, intern table entries) comes from the system + * allocator directly. So when a worker thread parks without having shared, + * nothing it allocated through scr_mem_* can be reachable from another + * thread, and what it never freed (ProgramLifetime instances, published + * module constants) is garbage. Such a thread's memory goes back whole: + * - when a worker claims an arena, the arena's free lists and returned + * regions are set aside, and the worker carves only past each slice's + * frontier, so its blocks of class c are [start[c], bump[c]), plus the + * blocks it takes from the free lists set aside once a slice has no bump + * room left, which it records; + * - the system blocks it allocates (large blocks, an exhausted slice) are + * recorded until it frees them; + * - a remote free into its blocks means another thread held one: the same + * as sharing; + * - when it parks without having shared, its arena stays claimed until no + * other worker thread is running (scr_sa_thread_park): returning pages + * interrupts every running thread of the process to flush its TLB, and + * glibc's trim locks the arenas other threads allocate from, so discards + * wait for the last worker of a group, which hands them to one helper; + * - the helper then returns the pages + * it carved (MADV_DONTNEED; the start of the first page, which it may + * share with older blocks, is cleared), frees the recorded system blocks, + * and parks the arena as the worker found it, with the blocks it took + * free again. A worker that shared parks + * its arena with the blocks set aside added to its free lists, and the + * ordinary trim applies. + * SCR_SA_DISCARD=0 keeps every worker's memory. SCR_SA_DISCARD=poison makes + * the carved pages inaccessible instead, and keeps the system blocks, so + * that a reference to discarded memory faults (a check for tests). */ +#define SCR_SA_SYS_GONE ((void *)1) /* a removed entry of ScrSaKeep.sys */ + +struct ScrSaKeep { + /* The arena as the worker claimed it: free lists (to which remote frees + * of older blocks are added), carve cursors and returned regions. */ + ScrSaBlock *free[SCR_SA_NCLASS]; + char *bump[SCR_SA_NCLASS], *lim[SCR_SA_NCLASS]; + ScrSaHoles holes[SCR_SA_NCLASS]; + /* Each slice's frontier then (bump and limit, NULL limits for a slice not + * opened yet), and the first address the worker carves in it. */ + char *front_bump[SCR_SA_NCLASS], *front_lim[SCR_SA_NCLASS]; + char *start[SCR_SA_NCLASS]; + /* The worker's system blocks: an open-addressing set of `cap` slots (a + * power of two, or 0), NULL for an empty slot and SCR_SA_SYS_GONE for a + * removed entry; `used` counts both kinds of filled slots. */ + void **sys; + size_t cap, used, live; + /* The blocks of the free lists set aside that the worker took once a slice + * had no bump room left, so that older free blocks are used before the + * slice grows; they go back to those lists with the worker's memory. */ + ScrSaBlock **taken; + size_t ntaken, cap_taken; + bool shared; + bool poison; +}; + +static _Thread_local bool scr_sa_discarded; /* this thread's memory went back */ +static ScrSaKeep *scr_sa_parked_keep[SCR_SA_ARENAS]; /* of discards that wait */ + +/* SCR_SA_DISCARD: 0 off, 1 on (the default), 2 poison. */ +static int scr_sa_discard_mode(void) { + const char *mode = getenv("SCR_SA_DISCARD"); + if (!mode) return 1; + if (strcmp(mode, "0") == 0) return 0; + return strcmp(mode, "poison") == 0 ? 2 : 1; +} + +static void scr_sa_keep_begin(unsigned i) { + int mode = scr_sa_discard_mode(); + if (!mode) return; + ScrSaKeep *k = calloc(1, sizeof *k); + if (!k) return; + k->poison = mode == 2; + for (unsigned c = 0; c < SCR_SA_NCLASS; c++) { + ScrSaHoles *h = &scr_sa_holes[i][c]; + k->free[c] = scr_sa.free[c]; + k->bump[c] = scr_sa.bump[c]; + k->lim[c] = scr_sa.lim[c]; + k->holes[c] = *h; + memset(h, 0, sizeof *h); + scr_sa.free[c] = NULL; + /* While the slice carves a returned region, its frontier is kept apart. */ + if (k->holes[c].lim) { + scr_sa.bump[c] = k->holes[c].bump; + scr_sa.lim[c] = k->holes[c].lim; + } + k->front_bump[c] = scr_sa.bump[c]; + k->front_lim[c] = scr_sa.lim[c]; + k->start[c] = scr_sa.lim[c] ? scr_sa.bump[c] : (char *)scr_sa.base + ((size_t)c << scr_sa.shift); + } + scr_sa_keep = k; +} + +/* A remote free of class c, drained while the worker keeps its memory + * apart: one of its own blocks makes it share; an older block goes to the + * free lists set aside. Once the worker has taken blocks from those lists, + * any older block might be one of them, so it shares as well. */ +static void scr_sa_keep_drained(ScrSaKeep *k, ScrSaBlock *b, unsigned c) { + if ((char *)b >= k->start[c] && (char *)b < scr_sa.bump[c]) { + k->shared = true; + b->next = scr_sa.free[c]; + scr_sa.free[c] = b; + } else { + if (k->ntaken) k->shared = true; + b->next = k->free[c]; + k->free[c] = b; + } +} + +/* A block of class c from the free lists set aside, for a worker whose + * slice has no bump room left; NULL when there is none or it cannot be + * recorded. */ +static ScrSaBlock *scr_sa_keep_take(ScrSaKeep *k, unsigned c) { + ScrSaBlock *b = k->free[c]; + if (!b) return NULL; + if (k->ntaken == k->cap_taken) { + size_t cap = k->cap_taken ? 2 * k->cap_taken : 256; + ScrSaBlock **taken = realloc(k->taken, cap * sizeof *taken); + if (!taken) return NULL; + k->taken = taken; + k->cap_taken = cap; + } + k->free[c] = b->next; + k->taken[k->ntaken++] = b; + return b; +} + +static size_t scr_sa_sys_slot(const void *p, size_t cap) { + return (size_t)((((uint64_t)(uintptr_t)p >> 4) * UINT64_C(0x9E3779B97F4A7C15)) >> 32) & (cap - 1); +} + +/* Records system block p. A worker whose set cannot grow keeps its memory. */ +static void scr_sa_keep_add(ScrSaKeep *k, void *p) { + if ((k->used + 1) * 2 > k->cap) { + size_t cap = 64; + while (cap < (k->live + 1) * 4) cap *= 2; + void **sys = calloc(cap, sizeof *sys); + if (!sys) { + k->shared = true; + return; + } + for (size_t s = 0; s < k->cap; s++) { + void *q = k->sys[s]; + if (!q || q == SCR_SA_SYS_GONE) continue; + size_t j = scr_sa_sys_slot(q, cap); + while (sys[j]) j = (j + 1) & (cap - 1); + sys[j] = q; + } + free(k->sys); + k->sys = sys; + k->cap = cap; + k->used = k->live; + } + size_t j = scr_sa_sys_slot(p, k->cap); + while (k->sys[j] && k->sys[j] != SCR_SA_SYS_GONE) j = (j + 1) & (k->cap - 1); + if (!k->sys[j]) k->used++; + k->sys[j] = p; + k->live++; +} + +/* Forgets system block p; whether it was recorded. */ +static bool scr_sa_keep_remove(ScrSaKeep *k, const void *p) { + if (!k->live) return false; + for (size_t j = scr_sa_sys_slot(p, k->cap); k->sys[j]; j = (j + 1) & (k->cap - 1)) { + if (k->sys[j] != p) continue; + k->sys[j] = SCR_SA_SYS_GONE; + k->live--; + return true; + } + return false; +} + +/* The calling thread's arena keeps the worker's memory (it shared): the + * free lists set aside go back, followed by the worker's own free blocks, + * and so do the returned regions, past which the worker carved. */ +static void scr_sa_keep_merge(ScrSaKeep *k) { + for (unsigned c = 0; c < SCR_SA_NCLASS; c++) { + ScrSaBlock *kept = k->free[c]; + if (kept) { + ScrSaBlock *tail = kept; + while (tail->next) tail = tail->next; + tail->next = scr_sa.free[c]; + scr_sa.free[c] = kept; + } + ScrSaHoles *h = &scr_sa_holes[scr_sa_arena][c]; + *h = k->holes[c]; + if (h->lim) { + h->bump = scr_sa.bump[c]; + h->lim = scr_sa.lim[c]; + scr_sa.bump[c] = k->bump[c]; + scr_sa.lim[c] = k->lim[c]; + } + } + free(k->sys); + free(k->taken); + free(k); +} + +/* Returns the memory of the worker whose arena the calling thread (the + * trim helper) holds, and restores the arena to its state at the worker's + * claim; with poison, makes the pages inaccessible and moves the frontier + * past them instead. */ +static void scr_sa_discard(ScrSaKeep *k) { + /* Remote frees since the worker's last drain. */ + scr_sa_keep = k; + scr_sa_drain_remote(); + scr_sa_keep = NULL; + if (k->shared) { + scr_sa_keep_merge(k); + scr_sa_trim(); + return; + } + uintptr_t page = (uintptr_t)sysconf(_SC_PAGESIZE); + if (page == 0 || page == (uintptr_t)-1) page = 4096; + size_t returned = 0, sys_bytes = 0, sys_blocks = 0; + for (unsigned c = 0; c < SCR_SA_NCLASS; c++) { + char *lo = k->start[c], *top = scr_sa.bump[c], *lim = scr_sa.lim[c]; + char *bump = k->front_bump[c], *limit = k->front_lim[c]; + if (lim && top > lo) { + char *first = (char *)(((uintptr_t)lo + page - 1) & ~(page - 1)); + char *used = (char *)(((uintptr_t)top + page - 1) & ~(page - 1)); + /* The part of a page that may also hold older blocks. */ + if (first > lo) memset(lo, 0, (size_t)((first < top ? first : top) - lo)); + if (k->poison) { + if (used > first) (void)mprotect(first, (size_t)(used - first), PROT_NONE); + bump = used > first ? used : top; + limit = lim > bump ? lim : bump; + } else { + char *end = (char *)(((uintptr_t)lim + page - 1) & ~(page - 1)); + if (end > first) { + (void)madvise(first, (size_t)(end - first), MADV_DONTNEED); + returned += (size_t)(end - first); + } + } + } else { + bump = top; + limit = lim; + } + scr_sa.free[c] = k->free[c]; + ScrSaHoles *h = &scr_sa_holes[scr_sa_arena][c]; + *h = k->holes[c]; + if (h->lim) { + h->bump = bump; + h->lim = limit; + scr_sa.bump[c] = k->bump[c]; + scr_sa.lim[c] = k->lim[c]; + } else { + scr_sa.bump[c] = bump; + scr_sa.lim[c] = limit; + } + } + /* The older free blocks the worker took are free again. */ + if (!k->poison) + for (size_t t = 0; t < k->ntaken; t++) { + ScrSaBlock *b = k->taken[t]; + unsigned c = (unsigned)(((uintptr_t)b - scr_sa.base) >> scr_sa.shift); + b->next = scr_sa.free[c]; + scr_sa.free[c] = b; + } + if (!k->poison) + for (size_t s = 0; s < k->cap; s++) { + void *p = k->sys[s]; + if (!p || p == SCR_SA_SYS_GONE) continue; +#if defined(__GLIBC__) + sys_bytes += malloc_usable_size(p); +#endif + sys_blocks++; + free(p); + } +#if defined(__GLIBC__) + if (sys_blocks) (void)malloc_trim(0); +#endif + const char *stats = getenv("SCR_SA_STATS"); + if (stats && stats[0] == '1') + fprintf(stderr, "scr_sa: discard arena %d returned %zu MB, %zu system blocks (%zu MB)\n", scr_sa_arena, + returned >> 20, sys_blocks, sys_bytes >> 20); + free(k->sys); + free(k->taken); + free(k); +} + +/* Hooks of the allocator's system paths and of sharing (scr_rc.h). */ +static void scr_sa_keep_note_system(void *p) { + if (p && scr_sa_keep) scr_sa_keep_add(scr_sa_keep, p); +} + +void scr_sa_thread_shares(void) { + if (scr_sa_keep) scr_sa_keep->shared = true; +} + +/* Worker threads that started and have not parked. */ +static _Atomic(unsigned) scr_sa_workers_live; + +/* Library archives run on their embedder's threads and keep their memory. */ +static void scr_sa_worker_starting(void) { +#ifndef SCR_LIB + scr_sa_worker = true; + atomic_fetch_add_explicit(&scr_sa_workers_live, 1, memory_order_relaxed); +#endif +} +#define SCR_SA_WORKER_STARTING() scr_sa_worker_starting() #endif #ifdef SCR_SA_TRIM @@ -912,6 +1243,85 @@ static void *scr_sa_trim_main(void *arg) { return NULL; } +/* Hands the calling thread's arena to a trim helper; false if no helper + * could start. */ +static bool scr_sa_start_helper(unsigned i) { + scr_sa_parked[i] = scr_sa; + atomic_fetch_add_explicit(&scr_sa_trimming, 1, memory_order_relaxed); + pthread_attr_t attr; + pthread_t helper; + bool started = pthread_attr_init(&attr) == 0; + if (started) { + started = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED) == 0 && + pthread_create(&helper, &attr, scr_sa_trim_main, (void *)(uintptr_t)i) == 0; + pthread_attr_destroy(&attr); + } + if (!started) atomic_fetch_sub_explicit(&scr_sa_trimming, 1, memory_order_relaxed); + return started; +} + +/* Discards that wait for the last running worker: one bit per arena, whose + * state and ScrSaKeep are in scr_sa_parked and scr_sa_parked_keep. */ +static pthread_mutex_t scr_sa_pending_lock = PTHREAD_MUTEX_INITIALIZER; +static uint32_t scr_sa_pending; + +/* Queues the discard of the calling worker's arena i; the arena stays + * claimed until it is done. */ +static void scr_sa_defer_discard(unsigned i, ScrSaKeep *k) { + atomic_fetch_add_explicit(&scr_sa_trimming, 1, memory_order_relaxed); + pthread_mutex_lock(&scr_sa_pending_lock); + scr_sa_parked[i] = scr_sa; + scr_sa_parked_keep[i] = k; + scr_sa_pending |= 1u << i; + pthread_mutex_unlock(&scr_sa_pending_lock); +} + +/* Runs the waiting discards on the calling thread, which ends with no arena; + * returns whether there were any. */ +static bool scr_sa_drain(void) { + bool any = false; + for (;;) { + pthread_mutex_lock(&scr_sa_pending_lock); + uint32_t pending = scr_sa_pending; + unsigned i = pending ? (unsigned)__builtin_ctz(pending) : 0; + scr_sa_pending &= ~(1u << i); + pthread_mutex_unlock(&scr_sa_pending_lock); + if (!pending) return any; + any = true; + scr_sa_arena = (int)i; + scr_sa = scr_sa_parked[i]; + ScrSaKeep *k = scr_sa_parked_keep[i]; + scr_sa_parked_keep[i] = NULL; + scr_sa_discard(k); + scr_sa_release(i); + atomic_fetch_sub_explicit(&scr_sa_trimming, 1, memory_order_release); + } +} + +static void *scr_sa_drain_main(void *arg) { + (void)arg; + scr_sa_drain(); + return NULL; +} + +/* A worker parked: the last one running starts the waiting discards. */ +static void scr_sa_worker_parked(void) { + if (atomic_fetch_sub_explicit(&scr_sa_workers_live, 1, memory_order_acq_rel) != 1) return; + pthread_mutex_lock(&scr_sa_pending_lock); + bool pending = scr_sa_pending != 0; + pthread_mutex_unlock(&scr_sa_pending_lock); + if (!pending) return; + pthread_attr_t attr; + pthread_t helper; + bool started = pthread_attr_init(&attr) == 0; + if (started) { + started = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED) == 0 && + pthread_create(&helper, &attr, scr_sa_drain_main, NULL) == 0; + pthread_attr_destroy(&attr); + } + if (!started) scr_sa_drain(); +} + void scr_sa_wait_trims(void) { while (atomic_load_explicit(&scr_sa_trimming, memory_order_acquire)) { struct timespec pause = {0, 1000000}; @@ -920,27 +1330,45 @@ void scr_sa_wait_trims(void) { } #else void scr_sa_wait_trims(void) {} +void scr_sa_thread_shares(void) {} #endif +static void scr_sa_park_arena(void); + void scr_sa_thread_park(void) { +#ifdef SCR_SA_TRIM + bool worker = scr_sa_worker; + scr_sa_worker = false; + scr_sa_park_arena(); + if (worker) scr_sa_worker_parked(); +#else + scr_sa_park_arena(); +#endif +} + +static void scr_sa_park_arena(void) { if (scr_sa_arena < 0) return; unsigned i = (unsigned)scr_sa_arena; #if defined(SCR_SA_PREFAULT) && defined(SCR_WORKERS) scr_sa_pf_park(); #endif #ifdef SCR_SA_TRIM + ScrSaKeep *k = scr_sa_keep; + scr_sa_keep = NULL; + if (k && !k->shared) { + /* Nothing this thread allocated is reachable from another one: its + * memory goes back once no other worker runs (scr_sa_worker_parked), + * and later frees on this thread are dropped. */ + scr_sa_discarded = true; + scr_sa_defer_discard(i, k); + memset(&scr_sa, 0, sizeof scr_sa); + scr_sa_arena = -1; + scr_sa_refused = true; + return; + } + if (k) scr_sa_keep_merge(k); if (scr_sa_trim_worth()) { - scr_sa_parked[i] = scr_sa; - atomic_fetch_add_explicit(&scr_sa_trimming, 1, memory_order_relaxed); - pthread_attr_t attr; - pthread_t helper; - bool started = pthread_attr_init(&attr) == 0; - if (started) { - started = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED) == 0 && - pthread_create(&helper, &attr, scr_sa_trim_main, (void *)(uintptr_t)i) == 0; - pthread_attr_destroy(&attr); - } - if (started) { + if (scr_sa_start_helper(i)) { /* Later frees on this thread take the foreign path into the arena's * remote list; the helper releases the claim. */ memset(&scr_sa, 0, sizeof scr_sa); @@ -948,7 +1376,6 @@ void scr_sa_thread_park(void) { scr_sa_refused = true; return; } - atomic_fetch_sub_explicit(&scr_sa_trimming, 1, memory_order_relaxed); scr_sa_trim(); } #endif @@ -971,6 +1398,9 @@ static size_t scr_sa_foreign_size(const void *p) { } void scr_sa_free_foreign(void *p) { +#ifdef SCR_SA_TRIM + if (scr_sa_discarded) return; /* the block went back with the thread's memory */ +#endif uintptr_t region = atomic_load_explicit(&scr_sa_region, memory_order_acquire); uintptr_t off = (uintptr_t)p - region; if (region && off < scr_sa_arena_span * SCR_SA_ARENAS) { @@ -982,6 +1412,9 @@ void scr_sa_free_foreign(void *p) { memory_order_relaxed)); return; } +#ifdef SCR_SA_TRIM + if (scr_sa_keep) (void)scr_sa_keep_remove(scr_sa_keep, p); +#endif free(p); } #else @@ -1020,6 +1453,15 @@ __attribute__((cold, noinline)) void *scr_sa_slow(size_t n, bool zero) { if (zero) memset(b, 0, n); return b; } +#ifdef SCR_SA_TRIM + /* A worker that keeps its memory apart uses the arena's older free + * blocks before its slice grows. */ + if (scr_sa_keep && (size_t)(scr_sa.lim[c] - scr_sa.bump[c]) < scr_sa_class_size(c) && + (b = scr_sa_keep_take(scr_sa_keep, c)) != NULL) { + if (zero) memset(b, 0, n); + return b; + } +#endif #endif /* Carve from the slice, opening it or raising its limit as needed; * fresh pages are zero. */ @@ -1051,6 +1493,9 @@ __attribute__((cold, noinline)) void *scr_sa_slow(size_t n, bool zero) { SCR_SA_LARGE_NOTE(q, n); SCR_SA_THREAD_LARGE_NOTE(); } +#ifdef SCR_SA_TRIM + scr_sa_keep_note_system(q); +#endif return q; } @@ -1065,8 +1510,14 @@ void *scr_mem_realloc(void *p, size_t n) { } #endif else { +#ifdef SCR_SA_TRIM + bool kept = scr_sa_keep && scr_sa_keep_remove(scr_sa_keep, p); +#endif void *q = realloc(p, n); if (n > SCR_SA_MAX) SCR_SA_LARGE_NOTE(q, n); +#ifdef SCR_SA_TRIM + if (kept) scr_sa_keep_note_system(q ? q : p); /* a failed realloc leaves p */ +#endif return q; } /* Still fits its block, which is aligned for n (see the contract). */ @@ -1082,10 +1533,15 @@ void *scr_mem_realloc(void *p, size_t n) { return realloc(p, n); } #ifdef SCR_WORKERS void scr_sa_thread_park(void) {} void scr_sa_wait_trims(void) {} +void scr_sa_thread_shares(void) {} void scr_sa_free_foreign(void *p) { free(p); } #endif #endif +#ifndef SCR_SA_WORKER_STARTING +#define SCR_SA_WORKER_STARTING() ((void)0) +#endif + #ifdef SCR_WORKERS /* Blocks above SCR_SA_MAX come from glibc, which gives each thread that * allocates its own arena: a heap reserved up front and made usable with @@ -1116,7 +1572,10 @@ void scr_sa_free_foreign(void *p) { free(p); } * that are not workers (they never reserve). */ static _Thread_local unsigned scr_sa_thread_large; -void scr_sa_thread_starting(void) { scr_sa_thread_large = 1; } +void scr_sa_thread_starting(void) { + scr_sa_thread_large = 1; + SCR_SA_WORKER_STARTING(); +} static void scr_sa_thread_reserve(void) { void *blocks[SCR_SA_THREAD_HEAP_RESERVE / SCR_SA_THREAD_HEAP_STEP + 1]; @@ -1136,7 +1595,7 @@ static void scr_sa_thread_large_note(void) { if (count == SCR_SA_THREAD_RESERVE_AFTER) scr_sa_thread_reserve(); } #else -void scr_sa_thread_starting(void) {} +void scr_sa_thread_starting(void) { SCR_SA_WORKER_STARTING(); } #endif #endif @@ -1152,6 +1611,92 @@ void scr_mem_prefault(void *lo, void *hi) { #endif } +/* Free order. Free lists are LIFO, so blocks freed in descending address + * order come back in ascending order, the order the bump pointers carve + * fresh memory in, and the order the program then touches them in. The + * order within a page matters as much as across pages: blocks of a page + * handed out in no particular order defeat the hardware prefetcher. + * + * The pointers are marked in a bitmap of block indices per class, over the + * range of blocks they span, and read back from the highest block down. + * That takes one bit per block in that range, at most one per block in + * use, and reading the bitmaps back costs a step per word; a set spread so + * thinly that they would take more than SCR_SA_ORDER_SPARSE words per + * pointer is left as it is. Pointers outside the reservation, not at + * `offset` in a block, or repeated, come first, in the order they had. */ +#define SCR_SA_ORDER_SPARSE 8 + +#if SCR_SMALL_ALLOC +/* The class and block index of a pointer `off` bytes past the reservation + * base plus the block offset, if it is at that offset in a block. Slices + * span at most 1 << 30 bytes, so an offset within one fits in 32 bits. */ +static inline bool scr_sa_order_block(uintptr_t off, uintptr_t span, unsigned shift, + const uint32_t *size, unsigned *c, uint32_t *idx) { + if (off >= span) return false; + *c = (unsigned)(off >> shift); + uint32_t in = (uint32_t)(off & (((uintptr_t)1 << shift) - 1)); + *idx = in / size[*c]; + return in == *idx * size[*c]; +} +#endif + +void scr_mem_order_for_free(void **ptrs, size_t n, size_t offset) { +#if SCR_SMALL_ALLOC + if (n < 2 || !scr_sa.span) return; + uintptr_t base = scr_sa.base + offset, span = scr_sa.span; + unsigned shift = scr_sa.shift, c; + uint32_t idx; + uint32_t size[SCR_SA_NCLASS], lo[SCR_SA_NCLASS], hi[SCR_SA_NCLASS]; + size_t at[SCR_SA_NCLASS], words = 0; + for (c = 0; c < SCR_SA_NCLASS; c++) { + size[c] = (uint32_t)scr_sa_class_size(c); + lo[c] = UINT32_MAX; + hi[c] = 0; + } + for (size_t i = 0; i < n; i++) { + if (!scr_sa_order_block((uintptr_t)ptrs[i] - base, span, shift, size, &c, &idx)) continue; + if (idx < lo[c]) lo[c] = idx; + if (idx > hi[c]) hi[c] = idx; + } + for (c = 0; c < SCR_SA_NCLASS; c++) { + at[c] = words; + if (lo[c] <= hi[c]) words += hi[c] / 64 - lo[c] / 64 + 1; + } + if (words == 0 || words / SCR_SA_ORDER_SPARSE > n) return; + uint64_t *bits = calloc(words, sizeof *bits); + if (!bits) return; /* the order only affects performance */ + size_t k = 0; + for (size_t i = 0; i < n; i++) { + void *p = ptrs[i]; + if (scr_sa_order_block((uintptr_t)p - base, span, shift, size, &c, &idx)) { + uint64_t *word = &bits[at[c] + idx / 64 - lo[c] / 64], bit = (uint64_t)1 << (idx % 64); + if (!(*word & bit)) { + *word |= bit; + continue; + } + } + ptrs[k++] = p; + } + for (c = 0; c < SCR_SA_NCLASS; c++) { + if (lo[c] > hi[c]) continue; + char *first = (char *)base + ((uintptr_t)c << shift) + (size_t)(lo[c] / 64) * 64 * size[c]; + for (size_t w = hi[c] / 64 - lo[c] / 64 + 1; w-- > 0;) { + uint64_t word = bits[at[c] + w]; + while (word) { + unsigned b = 63 - (unsigned)__builtin_clzll(word); + word &= ~((uint64_t)1 << b); + ptrs[k++] = first + (w * 64 + b) * size[c]; + } + } + } + free(bits); +#else + (void)ptrs; + (void)n; + (void)offset; +#endif +} + #if defined(__SANITIZE_ADDRESS__) #define SCR_HEAP_SANITIZER 1 #elif defined(__has_feature) diff --git a/packages/runtime/src/scr_alloc.test.c b/packages/runtime/src/scr_alloc.test.c index 26300296ed..f0efdc9355 100644 --- a/packages/runtime/src/scr_alloc.test.c +++ b/packages/runtime/src/scr_alloc.test.c @@ -119,6 +119,62 @@ static void check_churn(void) { } } +/* scr_mem_order_for_free keeps every pointer, puts system blocks first and + * each class's blocks in descending address order, and freeing in that + * order hands each class's blocks back in ascending address order. Builds + * without the size-class allocator leave the array as it is. */ +static int compare_pointers(const void *a, const void *b) { + uintptr_t x = (uintptr_t)*(void *const *)a, y = (uintptr_t)*(void *const *)b; + return x < y ? -1 : x > y; +} + +static void check_free_order(void) { + enum { BLOCKS = 12000, SYSTEM = 64, N = BLOCKS + SYSTEM }; + static void *ptrs[N], *before[N], *a[N], *b[N]; + const size_t sizes[2] = {32, 96}; + for (size_t i = 0; i < BLOCKS; i++) ptrs[i] = scr_mem_alloc(sizes[i % 2]); + for (size_t i = BLOCKS; i < N; i++) ptrs[i] = malloc(1024); + uint32_t x = 0x2545f491u; + for (size_t i = N - 1; i > 0; i--) { + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + size_t j = x % (i + 1); + void *t = ptrs[i]; + ptrs[i] = ptrs[j]; + ptrs[j] = t; + } + memcpy(before, ptrs, sizeof ptrs); + scr_mem_order_for_free(ptrs, N, 0); + memcpy(a, ptrs, sizeof ptrs); + memcpy(b, before, sizeof before); + qsort(a, N, sizeof *a, compare_pointers); + qsort(b, N, sizeof *b, compare_pointers); + assert(memcmp(a, b, sizeof a) == 0); +#if SCR_SMALL_ALLOC + size_t i = 0; + uintptr_t last[SCR_SA_NCLASS] = {0}; + for (; i < SYSTEM; i++) assert(!owned(ptrs[i])); + for (; i < N; i++) { + assert(owned(ptrs[i])); + unsigned c = (unsigned)(((uintptr_t)ptrs[i] - scr_sa.base) >> scr_sa.shift); + assert(!last[c] || (uintptr_t)ptrs[i] < last[c]); + last[c] = (uintptr_t)ptrs[i]; + } + for (i = 0; i < N; i++) scr_mem_free(ptrs[i]); + for (size_t c = 0; c < 2; c++) { + for (size_t k = 0; k < BLOCKS / 2; k++) { + a[k] = scr_mem_alloc(sizes[c]); + if (k) assert(a[k] > a[k - 1]); + } + for (size_t k = 0; k < BLOCKS / 2; k++) scr_mem_free(a[k]); + } +#else + assert(memcmp(ptrs, before, sizeof ptrs) == 0); + for (size_t i = 0; i < N; i++) scr_mem_free(ptrs[i]); +#endif +} + #if SCR_SMALL_ALLOC #include #ifdef __linux__ @@ -373,6 +429,332 @@ static void check_trim(void) { } #endif +#if defined(SCR_WORKERS) && SCR_SMALL_ALLOC && defined(__linux__) +#include +#include +#include +#include +#include + +/* A worker thread (scr_sa_thread_starting) that exits without sharing + * gives its memory back: the pages it carved, and the system blocks it + * still holds. Older blocks of the arena, and the memory of a worker that + * shared (or got a remote free of its own blocks), stay. */ +/* DISCARD_SIZE and POISON_SIZE are classes the earlier checks leave unused + * in the workers' arena, so their state there is known. */ +enum { DISCARD_BLOCKS = 1 << 20, DISCARD_SIZE = 40, POISON_SIZE = 56, DISCARD_OLD = 64, DISCARD_LARGE = 64 }; +enum { DISCARD_LARGE_SIZE = 64 << 10 }; +static unsigned char *old_blocks[DISCARD_OLD]; +static unsigned char **discard_blocks; +static unsigned char *discard_large[DISCARD_LARGE]; +static uintptr_t discard_base, discard_lo, discard_hi; +static size_t block_size = DISCARD_SIZE; /* of the workers' and reusing threads' blocks */ +/* Whether discard workers claim the arena of discard_old_owner (no other worker holds it). */ +static bool discard_in_old_arena = true; +/* The end of the highest block of class DISCARD_SIZE so far, and how many blocks of a run lay past the mark + * at its start: those the slice grew for. */ +static uintptr_t class_hi; +static size_t discard_beyond, reuse_beyond; +static bool discard_shares; +static atomic_int discard_step; + +static void wait_step(int step) { + while (atomic_load(&discard_step) < step) { + struct timespec pause = {0, 100000}; + nanosleep(&pause, NULL); + } +} + +/* A plain script thread whose blocks stay live in its parked arena. */ +static void *discard_old_owner(void *arg) { + (void)arg; + for (unsigned i = 0; i < DISCARD_OLD; i++) { + old_blocks[i] = scr_mem_alloc(DISCARD_SIZE); + assert(owned(old_blocks[i])); + fill(old_blocks[i], DISCARD_SIZE, (unsigned char)i); + } + discard_base = scr_sa.base; + scr_sa_thread_park(); + return NULL; +} + +static void allocate_discardable(void) { + discard_lo = UINTPTR_MAX; + discard_hi = 0; + discard_beyond = 0; + for (size_t i = 0; i < DISCARD_BLOCKS; i++) { + unsigned char *p = scr_mem_alloc(block_size); + assert(p && owned(p)); + fill(p, block_size, (unsigned char)i); + discard_blocks[i] = p; + if ((uintptr_t)p >= class_hi) discard_beyond++; + if ((uintptr_t)p < discard_lo) discard_lo = (uintptr_t)p; + if ((uintptr_t)p + block_size > discard_hi) discard_hi = (uintptr_t)p + block_size; + } + if (block_size == DISCARD_SIZE && discard_hi > class_hi) class_hi = discard_hi; + /* System blocks: some freed, some moved by realloc, the rest kept. */ + for (unsigned i = 0; i < DISCARD_LARGE; i++) { + discard_large[i] = scr_mem_alloc(DISCARD_LARGE_SIZE); + assert(discard_large[i] && !owned(discard_large[i])); + fill(discard_large[i], 64, (unsigned char)i); + } + for (unsigned i = 0; i < DISCARD_LARGE; i += 4) { + scr_mem_free(discard_large[i]); + discard_large[i] = NULL; + } + for (unsigned i = 1; i < DISCARD_LARGE; i += 4) discard_large[i] = scr_mem_realloc(discard_large[i], 2 * DISCARD_LARGE_SIZE); +} + +static void *discard_worker(void *arg) { + (void)arg; + scr_sa_thread_starting(); + allocate_discardable(); + /* None of the arena's older blocks or free blocks is reused. */ + if (scr_sa.base == discard_base) + for (unsigned i = 0; i < DISCARD_OLD; i++) assert(discard_lo > (uintptr_t)old_blocks[i]); + else assert(!discard_in_old_arena); + if (discard_shares) scr_sa_thread_shares(); + scr_sa_thread_park(); + return NULL; +} + +/* A worker one of whose blocks the main thread frees while it runs. */ +static void *remote_freed_worker(void *arg) { + (void)arg; + scr_sa_thread_starting(); + allocate_discardable(); + atomic_store(&discard_step, 1); + wait_step(2); + /* A class this thread has not used: its first block takes the slow path, + * which drains the remote free. */ + void *p = scr_mem_alloc(200); + assert(p); + scr_sa_thread_park(); + return NULL; +} + +/* A plain thread that takes as many blocks again from the arena: carved + * ones must be zero (the fast path does not clear them). */ +static size_t reuse_outside; +static uintptr_t reuse_hi; +static void *discard_reuse(void *arg) { + (void)arg; + reuse_outside = 0; + reuse_hi = 0; + reuse_beyond = 0; + for (size_t i = 0; i < DISCARD_BLOCKS; i++) { + unsigned char *p = scr_mem_calloc(block_size); + assert(p && owned(p)); + assert(scr_sa.base == discard_base); + for (size_t k = 0; k < block_size; k++) assert(p[k] == 0); + for (unsigned j = 0; j < DISCARD_OLD; j++) assert(p != old_blocks[j]); + if ((uintptr_t)p < discard_lo || (uintptr_t)p >= discard_hi) reuse_outside++; + if ((uintptr_t)p + block_size > reuse_hi) reuse_hi = (uintptr_t)p + block_size; + if ((uintptr_t)p >= class_hi) reuse_beyond++; + discard_blocks[i] = p; + } + if (block_size == DISCARD_SIZE && reuse_hi > class_hi) class_hi = reuse_hi; + for (size_t i = 0; i < DISCARD_BLOCKS; i++) scr_mem_free(discard_blocks[i]); + scr_sa_thread_park(); + return NULL; +} + +static void run(void *(*fn)(void *)) { + pthread_t t; + assert(pthread_create(&t, NULL, fn, NULL) == 0); + assert(pthread_join(t, NULL) == 0); + scr_sa_wait_trims(); +} + +static void verify_old_blocks(void) { + for (unsigned i = 0; i < DISCARD_OLD; i++) verify(old_blocks[i], DISCARD_SIZE, (unsigned char)i); +} + +/* The worker's blocks stayed as it left them, and the next thread to claim + * the arena carves past them; then they are freed (remote frees). */ +static void verify_kept(void) { + for (size_t i = 0; i < DISCARD_BLOCKS; i++) verify(discard_blocks[i], block_size, (unsigned char)i); + for (unsigned i = 0; i < DISCARD_LARGE; i++) { + if (!discard_large[i]) continue; + verify(discard_large[i], 64, (unsigned char)i); + free(discard_large[i]); + } + for (size_t i = 0; i < DISCARD_BLOCKS; i++) scr_mem_free(discard_blocks[i]); +} + +/* The protection of the mapping that holds address a, from /proc/self/maps. */ +static bool readable(uintptr_t a) { + FILE *maps = fopen("/proc/self/maps", "r"); + assert(maps); + char line[512]; + bool found = false, result = false; + while (!found && fgets(line, sizeof line, maps)) { + unsigned long lo, hi; + char perms[8]; + if (sscanf(line, "%lx-%lx %7s", &lo, &hi, perms) != 3) continue; + if (a >= lo && a < hi) { + found = true; + result = perms[0] == 'r'; + } + } + fclose(maps); + assert(found); + return result; +} + +/* A worker that runs until discard_step reaches 3. */ +static void *holding_worker(void *arg) { + (void)arg; + scr_sa_thread_starting(); + void *p = scr_mem_alloc(DISCARD_SIZE); + assert(p && owned(p)); + atomic_store(&discard_step, 1); + wait_step(3); + scr_sa_thread_park(); + return NULL; +} + +/* A worker that allocates one block of a class the other checks leave + * alone and parks without sharing. */ +static void *small_worker(void *arg) { + (void)arg; + scr_sa_thread_starting(); + void *p = scr_mem_alloc(200); + assert(p && owned(p)); + scr_sa_thread_park(); + return NULL; +} + +/* Discards wait while another worker runs, and the last worker to park + * starts them. When every arena is held by a discard that waits, a worker + * that needs one runs them itself. */ +static void check_deferred_discard(void) { + size_t page = (size_t)sysconf(_SC_PAGESIZE); + atomic_store(&discard_step, 0); + pthread_t holder; + assert(pthread_create(&holder, NULL, holding_worker, NULL) == 0); + wait_step(1); + discard_shares = false; + discard_in_old_arena = false; + pthread_t t; + assert(pthread_create(&t, NULL, discard_worker, NULL) == 0); + assert(pthread_join(t, NULL) == 0); + uintptr_t first = (discard_lo + page - 1) & ~(uintptr_t)(page - 1); + size_t total = (discard_hi - first) / page; + /* Still resident (the arena is not reused meanwhile, so the blocks are intact). */ + assert(resident_pages(first, discard_hi) > total / 2); + for (size_t i = 0; i < DISCARD_BLOCKS; i += 4099) verify(discard_blocks[i], block_size, (unsigned char)i); + /* More arenas held by waiting discards than there are free ones. */ + for (unsigned n = 0; n < 40; n++) { + assert(pthread_create(&t, NULL, small_worker, NULL) == 0); + assert(pthread_join(t, NULL) == 0); + } + atomic_store(&discard_step, 3); + assert(pthread_join(holder, NULL) == 0); + scr_sa_wait_trims(); + assert(resident_pages(first, discard_hi) < total / 2); + discard_in_old_arena = true; +} + +static void check_discard(void) { + discard_blocks = malloc(DISCARD_BLOCKS * sizeof *discard_blocks); + assert(discard_blocks); + run(discard_old_owner); + size_t page = (size_t)sysconf(_SC_PAGESIZE); + + /* Discarded: its pages are returned, and carved again (zeroed) by the next + * thread, which does not grow the slice. The arena's older blocks stay. */ + discard_shares = false; + run(discard_worker); + verify_old_blocks(); + uintptr_t first = (discard_lo + page - 1) & ~(uintptr_t)(page - 1); + /* Pre-fault requests still in flight may fault in a few megabytes again. */ + size_t total = (discard_hi - first) / page; + assert(resident_pages(first, discard_hi) < total / 2); + run(discard_reuse); + assert(reuse_outside < 256); + verify_old_blocks(); + + /* SCR_SA_DISCARD=poison makes the pages inaccessible; the next thread + * carves past them. */ + block_size = POISON_SIZE; + setenv("SCR_SA_DISCARD", "poison", 1); + run(discard_worker); + unsetenv("SCR_SA_DISCARD"); + verify_old_blocks(); + first = (discard_lo + page - 1) & ~(uintptr_t)(page - 1); + assert(!readable(first)); + assert(!readable((discard_hi - 1) & ~(uintptr_t)(page - 1))); + run(discard_reuse); + assert(reuse_outside == DISCARD_BLOCKS); + verify_old_blocks(); + block_size = DISCARD_SIZE; + + /* A worker that shared keeps its memory. */ + discard_shares = true; + run(discard_worker); + verify_old_blocks(); + verify_kept(); + + /* So does one that got a remote free of one of its blocks. */ + atomic_store(&discard_step, 0); + pthread_t t; + assert(pthread_create(&t, NULL, remote_freed_worker, NULL) == 0); + wait_step(1); + scr_mem_free(discard_blocks[0]); + discard_blocks[0] = scr_mem_alloc(DISCARD_SIZE); /* main's own block, for verify_kept */ + fill(discard_blocks[0], DISCARD_SIZE, 0); + atomic_store(&discard_step, 2); + assert(pthread_join(t, NULL) == 0); + scr_sa_wait_trims(); + verify_old_blocks(); + verify_kept(); + + /* SCR_SA_DISCARD=0 keeps every worker's memory. */ + discard_shares = false; + setenv("SCR_SA_DISCARD", "0", 1); + run(discard_worker); + unsetenv("SCR_SA_DISCARD"); + verify_old_blocks(); + verify_kept(); + + /* The kept workers' blocks are free now (remote frees): a worker takes + * them once its slice has no bump room left, and they are free again + * after its memory goes back, so the next thread does not grow the slice + * either. Past the highest block so far, each carves at most the bump room + * the slice had left (a few megabytes). */ + size_t room = ((size_t)4 << 20) / DISCARD_SIZE; + run(discard_worker); + verify_old_blocks(); + assert(discard_beyond < room); + run(discard_reuse); + assert(reuse_beyond < room); + verify_old_blocks(); + + /* Returned regions (holes) of an earlier trim are kept apart while a + * worker runs, and carved again afterwards. */ + trim_blocks = malloc(TRIM_BLOCKS * sizeof *trim_blocks); /* check_trim freed its own */ + assert(trim_blocks); + run(trim_allocate); + uintptr_t hole_lo = trim_lo, hole_hi = trim_hi; + discard_shares = false; + run(discard_worker); + verify_old_blocks(); + for (size_t k = 0; k < TRIM_BLOCKS / TRIM_KEEP; k++) + verify(trim_kept[k], TRIM_SIZE, (unsigned char)(k * TRIM_KEEP)); + trim_lo = hole_lo; + trim_hi = hole_hi; + run(trim_reuse); + assert(trim_outside < 4 * TRIM_BLOCKS / TRIM_KEEP + 256); + for (size_t k = 0; k < TRIM_BLOCKS / TRIM_KEEP; k++) scr_mem_free(trim_kept[k]); + free(trim_blocks); + + check_deferred_discard(); + for (unsigned i = 0; i < DISCARD_OLD; i++) scr_mem_free(old_blocks[i]); + free(discard_blocks); +} +#endif + int main(void) { #if SCR_SMALL_ALLOC check_slice_growth(); /* first: its class must be untouched */ @@ -382,6 +764,7 @@ int main(void) { check_realloc(); check_system_pointers(); check_churn(); + check_free_order(); #if SCR_SMALL_ALLOC && defined(__linux__) && defined(__GLIBC__) check_heap_huge_pages(); #endif @@ -389,6 +772,7 @@ int main(void) { check_threads(); #ifdef __linux__ check_trim(); + check_discard(); #endif printf("allocator checks passed: size-class workers\n"); #else diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index eba0367f76..ef64be56d0 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -3111,6 +3111,11 @@ static double scr_bytes_sort_load(const ScrBytes *b, size_t i) { memcpy(&v, at, sizeof v); return v; } + case SCR_BYTES_F16: { + uint16_t v; + memcpy(&v, at, sizeof v); + return scr_f16_to_f64(v); + } case SCR_BYTES_F32: { float v; memcpy(&v, at, sizeof v); @@ -3151,6 +3156,11 @@ static void scr_bytes_sort_store(ScrBytes *b, size_t i, double value) { memcpy(at, &v, sizeof v); break; } + case SCR_BYTES_F16: { + uint16_t v = scr_f16_from_f64(value); + memcpy(at, &v, sizeof v); + break; + } case SCR_BYTES_F32: { float v = (float)value; memcpy(at, &v, sizeof v); @@ -3263,6 +3273,18 @@ static int scr_bytes_f32_key(const uint8_t *at, int32_t order, uint32_t *key) { return 0; } +/* Float16 keys use the low 16 bits of a 32-bit key; a descending order + * complements all 32, which keeps the order of keys. */ +static int scr_bytes_f16_key(const uint8_t *at, int32_t order, uint32_t *key) { + uint16_t bits; + memcpy(&bits, at, sizeof bits); + if ((bits & 0x7fff) > 0x7c00) return order ? 2 : 1; + if (order && bits == 0x8000) return 2; + uint32_t k = (bits & 0x8000) ? (uint16_t)~bits : (uint32_t)bits | 0x8000; + *key = k ^ (order < 0 ? ~UINT32_C(0) : 0); + return 0; +} + static int scr_bytes_f64_key(const uint8_t *at, int32_t order, uint64_t *key) { double v; uint64_t bits; @@ -3323,6 +3345,9 @@ static bool scr_bytes_sort_refused(const ScrBytes *b, int32_t order) { if (b->elem == SCR_BYTES_F32) { uint32_t key; if (scr_bytes_f32_key(data + i * 4, order, &key)) return true; + } else if (b->elem == SCR_BYTES_F16) { + uint32_t key; + if (scr_bytes_f16_key(data + i * 2, order, &key)) return true; } else if (b->elem == SCR_BYTES_F64) { uint64_t key; if (scr_bytes_f64_key(data + i * 8, order, &key)) return true; @@ -3353,7 +3378,7 @@ bool scr_bytes_sort_numeric(ScrBytes *b, int32_t order) { size_t width = scr_bytes_elem_size(b->elem); uint8_t *data = b->data; /* Integer elements have no NaN or -0: every order is a value order. */ - if (width == 1 || (width == 2 && n >= SCR_BYTES_COUNT_SORT)) { + if (width == 1 || (width == 2 && b->elem != SCR_BYTES_F16 && n >= SCR_BYTES_COUNT_SORT)) { scr_bytes_count_sort(b, order < 0); return true; } @@ -3392,13 +3417,23 @@ bool scr_bytes_sort_numeric(ScrBytes *b, int32_t order) { uint32_t bits_ = (k_ & SCR_SORT_SIGN32) ? k_ & ~SCR_SORT_SIGN32 : ~k_; \ memcpy(data + (i) * 4, &bits_, 4); \ } while (0) +#define SCR_BYTES_READ32_F16(i, key) scr_bytes_f16_key(data + (i) * 2, order, (key)) +#define SCR_BYTES_WRITE32_F16(i, key) \ + do { \ + uint16_t k_ = (uint16_t)((key) ^ flip32); \ + uint16_t bits_ = (k_ & 0x8000) ? (uint16_t)(k_ & 0x7fff) : (uint16_t)~k_; \ + memcpy(data + (i) * 2, &bits_, 2); \ + } while (0) if (b->elem == SCR_BYTES_F32) SCR_BYTES_KEYS32(F32); + else if (b->elem == SCR_BYTES_F16) SCR_BYTES_KEYS32(F16); else SCR_BYTES_KEYS32(INT); #undef SCR_BYTES_KEYS32 #undef SCR_BYTES_READ32_INT #undef SCR_BYTES_WRITE32_INT #undef SCR_BYTES_READ32_F32 #undef SCR_BYTES_WRITE32_F32 +#undef SCR_BYTES_READ32_F16 +#undef SCR_BYTES_WRITE32_F16 free(keys); return true; } diff --git a/packages/runtime/src/scr_assert.c b/packages/runtime/src/scr_assert.c index 9e2e79418e..5a47690382 100644 --- a/packages/runtime/src/scr_assert.c +++ b/packages/runtime/src/scr_assert.c @@ -791,7 +791,7 @@ static bool scr_assert_loose_error(const ScrDyn *a, const ScrDyn *b) { } static bool scr_assert_loose_float_elem(ScrBytesElem elem) { - return elem == SCR_BYTES_F32 || elem == SCR_BYTES_F64; + return elem == SCR_BYTES_F16 || elem == SCR_BYTES_F32 || elem == SCR_BYTES_F64; } /* Typed arrays and Buffers: the same tag (a Buffer is a Uint8Array here), @@ -1305,6 +1305,7 @@ static const char *scr_assert_cf_bytes_class(ScrBytesElem elem) { switch (elem) { case SCR_BYTES_U8: return "Uint8Array"; case SCR_BYTES_U32: return "Uint32Array"; + case SCR_BYTES_F16: return "Float16Array"; case SCR_BYTES_F32: return "Float32Array"; case SCR_BYTES_I32: return "Int32Array"; case SCR_BYTES_F64: return "Float64Array"; diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index 46175cf82e..308c9198d6 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -29,6 +29,7 @@ #include #include +#include #include #include #include @@ -594,6 +595,11 @@ struct ScrFiber { * SPAWNER at spawn (Node's init-time capture), swapped active with the * fiber (the exc-cell pattern) so run()'s window rides awaits. */ ScrAlsCtx *als; + /* The lifetime region the body was spawned in (scr_region_fiber_hooks; + * NULL = the default region): the loop makes it current again whenever + * it resumes the fiber, stackful or stackless. Async bodies only; + * generators run in their resumer's region. */ + void *region; bool done; /* A native synchronous generator compiled as an LLVM coroutine (see coro * above): no stack or saved context; resumes call into the frame on the @@ -682,6 +688,12 @@ void scr_als_ctx_release(ScrAlsCtx *c) { free(c); } +/* The lifetime region a new async fiber carries (ScrFiber.region). */ +static inline void *scr_fiber_region_capture(void) { + const ScrRgnFiberHooks *regions = atomic_load_explicit(&scr_region_fiber_hooks, memory_order_relaxed); + return regions != NULL ? regions->capture() : NULL; +} + static SCR_TL ScrCtx scr_loop_ctx; /* the main stack (scheduler home) */ static SCR_TL ScrFiber *scr_current = NULL; /* A finished fiber whose last switch went to the loop (scr_resume_fiber). */ @@ -994,6 +1006,9 @@ double scr_set_interval(ScrClosure *cb, double ms) { return (double)id; } +SCR_TL void (*scr_mt_abort_timeout_arm)(ScrClosure *cb, double ms) = NULL; +SCR_TL int scr_mt_wait_state = 0; + double scr_set_timeout_handle(ScrClosure *cb, double ms) { ms = scr_timer_coerce_ms(ms); unsigned long id = scr_interval_next_id++; @@ -1900,6 +1915,7 @@ static void scr_fiber_destroy(ScrFiber *f) { #endif #endif scr_als_ctx_release(f->als); + if (f->region != NULL) atomic_load_explicit(&scr_region_fiber_hooks, memory_order_relaxed)->drop(f->region); #if !defined(_WIN32) && !defined(__wasi__) scr_fiber_stack_free(f->stack); #else @@ -1931,6 +1947,7 @@ static ScrPromise *scr_async_spawn_impl(void (*entry)(ScrFiber *, void *), void /* AsyncLocalStorage: the child runs in the SPAWNER's context (Node's * init-time capture); snapshots are immutable, so a retain suffices. */ f->als = scr_als_ctx_retain(*SCR_ALS_SLOT()); + f->region = scr_fiber_region_capture(); scr_fibers_live++; #ifdef _WIN32 @@ -2072,6 +2089,9 @@ ScrPromise *scr_async_spawn_stackless(void (*entry)(ScrFiber *, void *), void *a f->promise = scr_promise_new(); f->stackless = true; f->als = scr_als_ctx_retain(*SCR_ALS_SLOT()); + /* The lifetime region the eager prefix runs in, which every later step + * resumes in (scr_resume_fiber), as a fiber's body does. */ + f->region = scr_fiber_region_capture(); scr_fibers_live++; #ifdef SCR_ASAN_FIBERS f->stackless_next = scr_stackless_fibers; @@ -2245,6 +2265,7 @@ ScrPromise *scr_async_spawn_after(ScrPromise *dependency, f->entry = entry; f->argpack = argpack; f->als = scr_als_ctx_retain(*SCR_ALS_SLOT()); + f->region = scr_fiber_region_capture(); scr_fibers_live++; if (dependency->state == SCR_PROM_PENDING) { @@ -3793,6 +3814,9 @@ static void scr_resume_fiber(ScrFiber *f) { (void)scr_win_self(); #endif f->return_to = &scr_loop_ctx; + /* The body continues in the lifetime region it was spawned in. */ + const ScrRgnFiberHooks *regions = atomic_load_explicit(&scr_region_fiber_hooks, memory_order_relaxed); + void *region_before = regions != NULL ? regions->resume(f->region) : NULL; #ifdef __wasi__ scr_current = f; scr_exc_swap_cell(&f->exc); @@ -3809,6 +3833,7 @@ static void scr_resume_fiber(ScrFiber *f) { scr_current = NULL; scr_exc_swap_cell(NULL); scr_als_active = &scr_als_main_slot; + if (regions != NULL) regions->suspend(region_before); #else if (f->stackless) { /* A stackless async body continues on the loop's stack. */ @@ -3819,8 +3844,10 @@ static void scr_resume_fiber(ScrFiber *f) { scr_current = NULL; scr_exc_swap_cell(NULL); scr_als_active = &scr_als_main_slot; + if (regions != NULL) regions->suspend(region_before); } else { scr_switch(&scr_loop_ctx, &f->ctx, f); + if (regions != NULL) regions->suspend(region_before); f = scr_loop_reap; if (f == NULL) return; scr_loop_reap = NULL; @@ -4580,7 +4607,9 @@ bool scr_report_unhandled_rejections(void) { #ifdef SCR_WORKERS if (scr_context_report_error) { scr_promise_rethrow(p); + scr_context_report_rejection = true; scr_exc_print_uncaught(); + scr_context_report_rejection = false; scr_promise_release(p); continue; } @@ -4860,6 +4889,13 @@ struct ScrGen { ScrExcCell async_error; bool settle_queued; bool success_hop_done; + /* A return() request at a yield: the queued job resumes the body after + * the return value's await (return_queued), and the body's completion, + * on that request or a later one, settles without another turn + * (return_awaited). An explicit `return x` during the unwind clears it, + * since that return takes its own turn to await x. */ + bool return_queued; + bool return_awaited; #ifdef SCR_WORKERS bool context_abandoned; #endif @@ -5261,6 +5297,8 @@ void scr_coro_yield_ref(ScrFiber *self, void *v, void (*release)(void *)) { scr_gen_slot_ref(&self->gen->out, v, release); } +void scr_async_gen_return_awaits(void) { scr_gen_self()->return_awaited = false; } + void scr_async_gen_hop_done(void) { ScrGen *g = scr_gen_self(); if (!g->is_async) { @@ -5540,6 +5578,21 @@ static void scr_async_gen_capture_caller_error(ScrGen *g) { if (cell->kind != SCR_EXC_NONE) scr_gen_exc_move(&g->async_error, cell); } +/* Whether the body's latest stop settles its request without a further + * turn: a yield whose await already supplied the value, or a completion + * after a return() request's await. */ +static bool scr_async_gen_settles_now(ScrGen *g) { + if (g->success_hop_done) { + g->success_hop_done = false; + return true; + } + if (g->return_awaited && g->state == SCR_GEN_DONE) { + g->return_awaited = false; + return true; + } + return false; +} + static void scr_async_gen_run_active(ScrGen *g, ScrAsyncGenRequest *req) { const bool was_done = g->state == SCR_GEN_DONE; if (req->mode == SCR_AG_NEXT) { @@ -5551,6 +5604,13 @@ static void scr_async_gen_run_active(ScrGen *g, ScrAsyncGenRequest *req) { scr_gen_slot_reset(&g->ret); g->ret = req->arg; memset(&req->arg, 0, sizeof req->arg); + if (g->state == SCR_GEN_SUSPENDED) { + /* At a yield, AsyncGeneratorYield awaits the return value before + * the body resumes, so finally blocks run one turn later. */ + g->return_queued = true; + scr_async_gen_schedule_settle(g); + return; + } scr_gen_resume_return(g); } else { scr_gen_exc_move(scr_exc_current_cell(), &req->thrown); @@ -5565,18 +5625,39 @@ static void scr_async_gen_run_active(ScrGen *g, ScrAsyncGenRequest *req) { if (g->state == SCR_GEN_AWAITING) return; /* next() on an already-completed generator produces its undefined done - * result immediately. Yield, body return, and return(value) all pass - * through AsyncGeneratorYield's successful Await step. */ + * result immediately. A yield and return(value) on an unstarted or + * completed generator pass through an Await step; a body that falls off + * its end, or whose `return x` already awaited x, marks its completion + * as settled. */ if (was_done && req->mode == SCR_AG_NEXT) { scr_async_gen_finish_active(g, false); - } else if (g->success_hop_done) { - g->success_hop_done = false; + } else if (scr_async_gen_settles_now(g)) { scr_async_gen_finish_active(g, false); } else { scr_async_gen_schedule_settle(g); } } +/* The return() request's await has taken its turn: resume the body with + * the return completion. */ +static void scr_async_gen_resume_returning(ScrGen *g) { + g->return_queued = false; + g->return_awaited = true; + scr_gen_resume_return(g); + scr_async_gen_capture_caller_error(g); + if (g->async_error.kind != SCR_EXC_NONE) { + scr_async_gen_finish_active(g, true); + } else if (g->state == SCR_GEN_AWAITING) { + return; + } else if (scr_async_gen_settles_now(g)) { + scr_async_gen_finish_active(g, false); + } else { + scr_async_gen_schedule_settle(g); + return; + } + scr_async_gen_start_queued(g); +} + static void scr_async_gen_start_queued(ScrGen *g) { /* Keep the object alive while an immediately-settled request releases * its own retain and the loop inspects the next queue node. */ @@ -5687,6 +5768,12 @@ static void scr_async_gen_job(ScrGen *g) { fputs("scriptc: internal error: invalid async generator settlement job\n", stderr); abort(); } + if (g->return_queued) { + scr_gen_retain(g); + scr_async_gen_resume_returning(g); + scr_gen_release(g); + return; + } scr_async_gen_finish_active(g, false); scr_async_gen_start_queued(g); } @@ -5704,8 +5791,7 @@ static void scr_async_gen_resume_ready(ScrGen *g) { scr_async_gen_finish_active(g, true); scr_async_gen_start_queued(g); } else if (g->state != SCR_GEN_AWAITING) { - if (g->success_hop_done) { - g->success_hop_done = false; + if (scr_async_gen_settles_now(g)) { scr_async_gen_finish_active(g, false); scr_async_gen_start_queued(g); } else { diff --git a/packages/runtime/src/scr_async.h b/packages/runtime/src/scr_async.h index a391d2a26f..7f294ca29f 100644 --- a/packages/runtime/src/scr_async.h +++ b/packages/runtime/src/scr_async.h @@ -367,6 +367,14 @@ ScrPromise *scr_immediate_promise(void); void scr_clear_interval(double handle); double scr_set_timeout_handle(ScrClosure *cb, double ms); /* cb ownership moves in */ +/* Queues `cb` (moved) on the MockTimers that mocks AbortSignal.timeout + * (scr_test.c), NULL while it is not mocked. AbortSignal.timeout then + * aborts its signal from that queue instead of arming a loop timer. */ +extern SCR_TL void (*scr_mt_abort_timeout_arm)(ScrClosure *cb, double ms); +/* scheduler.wait's mock.timers state (scr_test.c): 0 real, 1 mocked, 2 the + * real one restored after a reset, which throws ERR_INVALID_THIS in Node. + * scr_tp_scheduler_wait_signal reads it. */ +extern SCR_TL int scr_mt_wait_state; void scr_timers_teardown(void); /* Timeout.unref()/ref()/hasRef(): loop-liveness bookkeeping over the timer * handle (setTimeout/setInterval's id). unref drops the timer from the diff --git a/packages/runtime/src/scr_async_dyn.c b/packages/runtime/src/scr_async_dyn.c index a14ae6e6a4..b7387c13e6 100644 --- a/packages/runtime/src/scr_async_dyn.c +++ b/packages/runtime/src/scr_async_dyn.c @@ -341,9 +341,100 @@ void scr_process_off_uncaught_exception(ScrDyn *fn, bool monitor) { scr_uncaught_sync_hook(); } +/* V8's side-effect-free string of a value, which Node's + * UnhandledPromiseRejection message quotes: primitives as String() gives + * them, a symbol as Symbol(description), an array as [object Array], an + * object with its own toString as [object Object], and other objects as + * #. */ +static bool scr_rejection_custom_to_string(const ScrDyn *value) { + if (value->kind == SCR_DYN_TYPED_REF) { + ScrStr *key = scr_str_new("toString", 8); + bool found = scr_dyn_typed_ref_class_has(value, key); + scr_str_release(key); + return found; + } + for (const ScrDyn *p = value; p; p = p->prototype) + if (p->kind == SCR_DYN_OBJ && scr_dyn_obj_get(p, "toString", 8)) return true; + return false; +} + +static ScrStr *scr_rejection_reason_text(const ScrDyn *reason, const ScrDyn *view) { + switch (view->kind) { + case SCR_DYN_UNDEF: return scr_str_new("undefined", 9); + case SCR_DYN_NULL: return scr_str_new("null", 4); + case SCR_DYN_SYMBOL: return reason->v.symbol.render(reason->v.symbol.value); + case SCR_DYN_ARR: return scr_str_new("[object Array]", 14); + case SCR_DYN_PROMISE: return scr_str_new("#", 10); + case SCR_DYN_OBJ: { + if (scr_rejection_custom_to_string(reason) || scr_rejection_custom_to_string(view)) + return scr_str_new("[object Object]", 15); + const char *name = reason->kind == SCR_DYN_OBJ ? scr_dyn_class_name(reason) : NULL; + const ScrStr *typed = reason->kind == SCR_DYN_TYPED_REF ? scr_dyn_typed_ref_class_name(reason) : NULL; + ScrStr *parts[3] = { + scr_str_new("#<", 2), + typed ? scr_str_new(typed->data, typed->len) : scr_str_new(name ? name : "Object", name ? strlen(name) : 6), + scr_str_new(">", 1), + }; + ScrStr *text = scr_str_concat_parts(parts, 3); + for (size_t i = 0; i < 3; i++) scr_str_release(parts[i]); + return text; + } + default: return scr_dyn_to_string(view, NULL); + } +} + +ScrDyn *scr_unhandled_rejection_error(ScrDyn *reason) { + ScrError *error = scr_errdyn_err_of(reason); + ScrDyn *view = reason->kind == SCR_DYN_TYPED_REF ? scr_dyn_typed_ref_materialize(reason) : scr_dyn_retain(reason); + if (!view || scr_exc_pending()) { + scr_error_release(error); + scr_dyn_release(view); + return NULL; + } + ScrStr *stack = scr_str_new("stack", 5); + bool error_like = error || (view->kind == SCR_DYN_OBJ && scr_dyn_has_own(view, stack)); + scr_str_release(stack); + if (error_like) { + scr_error_release(error); + scr_dyn_release(view); + return scr_dyn_retain(reason); + } + ScrStr *text = scr_rejection_reason_text(reason, view); + scr_dyn_release(view); + if (!text || scr_exc_pending()) { + scr_str_release(text); + return NULL; + } + static const char head[] = "This error originated either by throwing inside of an async function without a " + "catch block, or by rejecting a promise which was not handled with .catch(). The " + "promise rejected with the reason \""; + ScrStr *parts[3] = {scr_str_new(head, sizeof head - 1), text, scr_str_new("\".", 2)}; + ScrStr *message = scr_str_concat_parts(parts, 3); + for (size_t i = 0; i < 3; i++) scr_str_release(parts[i]); + error = scr_error_new(SCR_ERR_ERROR, message); + scr_str_release(message); + scr_str_release(error->name); + error->name = scr_str_new("UnhandledPromiseRejection", 25); + error->code = scr_str_new("ERR_UNHANDLED_REJECTION", 23); + ScrDyn *value = scr_dyn_from_error(error); + /* Node's class fields: code, then name, both own and enumerable */ + scr_dyn_obj_set(value, "name", 4, scr_dyn_new_str(error->name)); + scr_error_release(error); + return value; +} + static int scr_uncaught_dispatch(bool from_promise) { ScrCaught *caught = scr_exc_take(); ScrDyn *error = scr_caught_to_dyn(caught); + if (from_promise) { + ScrDyn *wrapped = scr_unhandled_rejection_error(error); + scr_dyn_release(error); + if (!wrapped) { + scr_caught_release(caught); + return -1; + } + error = wrapped; + } const char *origin = from_promise ? "unhandledRejection" : "uncaughtException"; ScrStr *text = scr_str_new(origin, strlen(origin)); ScrDyn *args[2] = {error, scr_dyn_new_str(text)}; diff --git a/packages/runtime/src/scr_bytes.c b/packages/runtime/src/scr_bytes.c index df5b8a3c07..28258a29cf 100644 --- a/packages/runtime/src/scr_bytes.c +++ b/packages/runtime/src/scr_bytes.c @@ -87,10 +87,60 @@ static void scr_bytes_oom(void) { scr_trap("scriptc: out of memory\n"); } +uint16_t scr_f16_from_f64(double value) { + uint64_t bits; + memcpy(&bits, &value, sizeof bits); + uint16_t sign = (uint16_t)((bits >> 48) & 0x8000); + uint64_t magnitude = bits & UINT64_C(0x7fffffffffffffff); + if (magnitude >= UINT64_C(0x7ff0000000000000)) { + if (magnitude == UINT64_C(0x7ff0000000000000)) return sign | 0x7c00; + return sign | 0x7e00 | (uint16_t)((magnitude >> 42) & 0x3ff); + } + int exponent = (int)(magnitude >> 52) - 1023; + if (exponent > 15) return sign | 0x7c00; + uint64_t fraction = magnitude & ((UINT64_C(1) << 52) - 1); + uint64_t significand = exponent == -1023 ? fraction : fraction | (UINT64_C(1) << 52); + if (exponent == -1023) exponent = -1022; + int biased = exponent + 15; + /* Normal results keep 10 of the 52 fraction bits; subnormal results + * shift further, by how far the exponent is below the normal range. */ + int shift = biased >= 1 ? 42 : 42 + (1 - biased); + if (shift >= 54) return sign; + uint64_t q = significand >> shift; + uint64_t rest = significand & ((UINT64_C(1) << shift) - 1); + uint64_t half = UINT64_C(1) << (shift - 1); + if (rest > half || (rest == half && (q & 1))) q++; + if (biased < 1) return sign | (uint16_t)q; /* a carry into 0x400 is the smallest normal */ + if (q == 2048) { + q = 1024; + biased++; + } + if (biased >= 31) return sign | 0x7c00; + return sign | (uint16_t)(biased << 10) | (uint16_t)(q & 0x3ff); +} + +double scr_f16_to_f64(uint16_t h) { + uint64_t sign = (uint64_t)(h >> 15) << 63; + unsigned exponent = (h >> 10) & 0x1f, fraction = h & 0x3ff; + uint64_t bits; + if (exponent == 0) { + double v = (double)fraction * 5.9604644775390625e-8; /* fraction * 2^-24, exact */ + memcpy(&bits, &v, sizeof bits); + bits |= sign; + } else if (exponent == 31) { + bits = sign | UINT64_C(0x7ff0000000000000) | ((uint64_t)fraction << 42); + } else { + bits = sign | ((uint64_t)(exponent - 15 + 1023) << 52) | ((uint64_t)fraction << 42); + } + double out; + memcpy(&out, &bits, sizeof out); + return out; +} + size_t scr_bytes_elem_size(ScrBytesElem elem) { switch (elem) { case SCR_BYTES_U8: case SCR_BYTES_U8C: case SCR_BYTES_I8: return 1; - case SCR_BYTES_U16: case SCR_BYTES_I16: return 2; + case SCR_BYTES_U16: case SCR_BYTES_I16: case SCR_BYTES_F16: return 2; case SCR_BYTES_U32: case SCR_BYTES_I32: case SCR_BYTES_F32: return 4; case SCR_BYTES_F64: return 8; } @@ -106,6 +156,7 @@ const char *scr_bytes_elem_name(ScrBytesElem elem) { case SCR_BYTES_I16: return "Int16Array"; case SCR_BYTES_U32: return "Uint32Array"; case SCR_BYTES_I32: return "Int32Array"; + case SCR_BYTES_F16: return "Float16Array"; case SCR_BYTES_F32: return "Float32Array"; case SCR_BYTES_F64: return "Float64Array"; } @@ -329,6 +380,11 @@ static double scr_bytes_decode(ScrBytesElem elem, const uint8_t *p) { memcpy(&v, p, 4); return (double)v; } + case SCR_BYTES_F16: { + uint16_t v; + memcpy(&v, p, 2); + return scr_f16_to_f64(v); + } case SCR_BYTES_F32: { float v; memcpy(&v, p, 4); @@ -368,6 +424,11 @@ static void scr_bytes_encode(ScrBytesElem elem, uint8_t *p, double v) { memcpy(p, &u, 4); break; } + case SCR_BYTES_F16: { + uint16_t h = scr_f16_from_f64(v); + memcpy(p, &h, 2); + break; + } case SCR_BYTES_F32: { float f = (float)v; /* round-to-nearest-even, exactly Float32Array */ memcpy(p, &f, 4); @@ -511,6 +572,13 @@ static void scr_bytes_read_numbers(double *out, const uint8_t *src, ScrBytesElem SCR_READ_NUMBERS(SCR_BYTES_I32, int32_t); SCR_READ_NUMBERS(SCR_BYTES_F32, float); SCR_READ_NUMBERS(SCR_BYTES_F64, double); + case SCR_BYTES_F16: + for (size_t i = 0; i < count; i++) { + uint16_t value; + memcpy(&value, src + i * sizeof value, sizeof value); + out[i] = scr_f16_to_f64(value); + } + return; } #undef SCR_READ_NUMBERS } @@ -535,6 +603,9 @@ static void scr_bytes_write_numbers(uint8_t *dst, ScrBytesElem elem, const doubl case SCR_BYTES_U32: case SCR_BYTES_I32: { SCR_WRITE_NUMBERS(uint32_t, scr_numeric_to_u32(src[i])); } + case SCR_BYTES_F16: { + SCR_WRITE_NUMBERS(uint16_t, scr_f16_from_f64(src[i])); + } case SCR_BYTES_F32: { SCR_WRITE_NUMBERS(float, (float)src[i]); } @@ -936,6 +1007,7 @@ static size_t scr_dataview_get_size(ScrDataViewGet kind) { return 1; case SCR_DV_U16: case SCR_DV_I16: + case SCR_DV_F16: return 2; case SCR_DV_U32: case SCR_DV_I32: @@ -974,6 +1046,8 @@ double scr_dataview_get(const ScrBytes *b, double byte_off, ScrDataViewGet kind, return (double)(int16_t)u; case SCR_DV_I32: return (double)(int32_t)u; + case SCR_DV_F16: + return scr_f16_to_f64((uint16_t)u); case SCR_DV_F32: { uint32_t bits = (uint32_t)u; float f; @@ -1018,6 +1092,9 @@ void scr_dataview_set(ScrBytes *b, double byte_off, double value, ScrDataViewGet case SCR_DV_I32: u = (uint64_t)scr_bytes_to_u32(value); break; + case SCR_DV_F16: + u = scr_f16_from_f64(value); + break; case SCR_DV_F32: { float f = (float)value; uint32_t bits; @@ -2462,6 +2539,22 @@ static ScrStr *scr_bytes_normalize_encoding(const ScrStr *raw) { return NULL; } +/* new StringDecoder(encoding) with an encoding known only at run time: + * Node's normalizeEncoding. undefined, null and "" are utf8; a Buffer + * encoding name in any ASCII case answers its canonical name (+1); any + * other string throws ERR_UNKNOWN_ENCODING (NULL). Encodings that are not + * strings have no lowering. */ +ScrStr *scr_strdec_encoding(const ScrDyn *enc) { + if (enc->kind == SCR_DYN_UNDEF || enc->kind == SCR_DYN_NULL) return scr_str_new("utf8", 4); + if (enc->kind != SCR_DYN_STR) { + static const char message[] = "new StringDecoder with an encoding that is not a string has no lowering"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + return NULL; + } + if (enc->v.str->len == 0) return scr_str_new("utf8", 4); + return scr_bytes_normalize_encoding(enc->v.str); +} + ScrStr *scr_bytes_to_str_checked(const ScrBytes *b, const ScrStr *enc) { /* Node returns before resolving the encoding when there are no bytes * to decode, so even an unknown runtime name answers the empty string. */ diff --git a/packages/runtime/src/scr_bytes.h b/packages/runtime/src/scr_bytes.h index b56f9477d8..4309154c9f 100644 --- a/packages/runtime/src/scr_bytes.h +++ b/packages/runtime/src/scr_bytes.h @@ -20,7 +20,8 @@ * Elements are scalars only and the backing edge is acyclic by * construction (owners point at nothing): never part of a cycle, no * trace. Element reads widen to double; writes coerce JS-exactly (ToUint8 - * / ToUint32 modular truncation, double→float rounding for f32). + * / ToUint32 modular truncation, double→float rounding for f32, one + * double→binary16 rounding for f16). * Out-of-bounds typed ELEMENT reads trap like arrays; invalid writes are * ignored as in JavaScript. The Node-shaped operations * (construction lengths, set(), the read/write numeric families, the @@ -37,7 +38,16 @@ typedef enum ScrBytesElem { SCR_BYTES_U16, /* Uint16Array */ SCR_BYTES_I16, /* Int16Array */ SCR_BYTES_U8C, /* Uint8ClampedArray */ + SCR_BYTES_F16, /* Float16Array (IEEE 754 binary16 storage) */ } ScrBytesElem; +#define SCR_BYTES_ELEM_COUNT 10 + +/* Float16Array's element conversions: one round-to-nearest-even step from + * the double (Float16Array stores, Math.f16round and DataView.setFloat16 + * all round this way), and the exact widening back. NaN keeps its sign and + * top payload bits with the quiet bit set. */ +uint16_t scr_f16_from_f64(double value); +double scr_f16_to_f64(uint16_t bits); typedef struct ScrSharedBytes ScrSharedBytes; @@ -94,6 +104,26 @@ ScrDyn *scr_worker_parent_port(void); * (scr_worker_events.c). */ ScrDyn *scr_worker_message_channel(void); ScrDyn *scr_worker_receive_message_on_port(ScrDyn *port); +/* new BroadcastChannel(...args) (scr_worker_events.c): args is the argument + * vector as one array (borrowed); may throw. */ +ScrDyn *scr_worker_broadcast_channel(ScrDyn *args); +/* setEnvironmentData(key, value) and getEnvironmentData(key) (+1); both + * borrow their arguments. */ +void scr_worker_set_environment_data(ScrDyn *key, ScrDyn *value); +ScrDyn *scr_worker_get_environment_data(ScrDyn *key); +ScrDyn *scr_worker_resource_limits(void); +ScrDyn *scr_worker_thread_name_current(void); +/* markAsUntransferable(value) and isMarkedAsUntransferable(value) + * (scr_worker_events.c); both borrow the value. */ +void scr_worker_mark_untransferable(ScrDyn *value); +bool scr_worker_is_marked_untransferable(ScrDyn *value); +/* worker.stdout, worker.stderr and worker.stdin streams: main installs the + * stream unit's operations and the Readable and Writable boxes (the + * Writable one may be NULL) when the program reads them. */ +struct ScrChildStreamOps; +void scr_worker_stdio_install(const struct ScrChildStreamOps *ops, ScrDyn *(*readable_box)(void *), + ScrDyn *(*writable_box)(void *)); +void scr_worker_stdio_default(const struct ScrChildStreamOps *ops, ScrDyn *(*view)(void *)); ScrDyn *scr_worker_new(double root, ScrStr *filename, ScrDyn *options); bool scr_shared_array_buffer_is(const ScrDyn *); bool scr_buffer_storage_is(const ScrDyn *); @@ -119,6 +149,7 @@ double scr_bytes_to_u8_clamp(double value); ScrStr *scr_strdec_write(const ScrStr *enc, double pending, const ScrBytes *chunk); double scr_strdec_next(const ScrStr *enc, double pending, const ScrBytes *chunk); ScrStr *scr_strdec_end(const ScrStr *enc, double pending); +ScrStr *scr_strdec_encoding(const ScrDyn *enc); /* +1, or NULL with ERR_UNKNOWN_ENCODING pending */ /* `new Uint8Array(n)` / Buffer.alloc(n): zero-filled. n goes through * ToIndex like JS (NaN → 0, truncate toward zero; negative or > 2^53-1 @@ -148,6 +179,7 @@ ScrBytes *scr_array_buffer_view_i16(ScrDyn *, ScrDyn *, ScrDyn *); ScrBytes *scr_array_buffer_view_u8(ScrDyn *, ScrDyn *, ScrDyn *); ScrBytes *scr_array_buffer_view_u32(ScrDyn *, ScrDyn *, ScrDyn *); ScrBytes *scr_array_buffer_view_i32(ScrDyn *, ScrDyn *, ScrDyn *); +ScrBytes *scr_array_buffer_view_f16(ScrDyn *, ScrDyn *, ScrDyn *); ScrBytes *scr_array_buffer_view_f32(ScrDyn *, ScrDyn *, ScrDyn *); ScrBytes *scr_array_buffer_view_f64(ScrDyn *, ScrDyn *, ScrDyn *); ScrBytes *scr_array_buffer_view_dv(ScrDyn *, ScrDyn *, ScrDyn *); @@ -205,7 +237,7 @@ ScrBytes *scr_dataview_new(ScrBytes *src, double byte_off, bool has_len, double * with the exception pending). */ typedef enum ScrDataViewGet { SCR_DV_U8, SCR_DV_I8, SCR_DV_U16, SCR_DV_I16, SCR_DV_U32, SCR_DV_I32, - SCR_DV_F32, SCR_DV_F64, SCR_DV_BIGU64, SCR_DV_BIGI64, + SCR_DV_F32, SCR_DV_F64, SCR_DV_BIGU64, SCR_DV_BIGI64, SCR_DV_F16, } ScrDataViewGet; double scr_dataview_get(const ScrBytes *b, double byte_off, ScrDataViewGet kind, bool le); diff --git a/packages/runtime/src/scr_child.c b/packages/runtime/src/scr_child.c index 2d72c21d2a..1235001ba7 100644 --- a/packages/runtime/src/scr_child.c +++ b/packages/runtime/src/scr_child.c @@ -2266,14 +2266,14 @@ static void scr_exec_file_fire(ScrChild *c) { } ScrChild *scr_exec_file(ScrStr *cmd, ScrArr *args, ScrClosure *cb, - ScrExecFileFn fn) { + ScrExecFileFn fn, bool shell) { ScrStr *empty = scr_str_new("", 0); ScrChild *c = scr_spawn_opts(cmd, args, 3, 3, 3, 0, 0, false, false, false, NULL, empty); scr_str_release(empty); c->exec_cb = cb; c->exec_fn = fn; - char *display = scr_exec_display(cmd, args, false); + char *display = scr_exec_display(cmd, args, shell); c->exec_display = scr_str_new(display, strlen(display)); free(display); scr_child_stream_capture_begin(c->out_stream); @@ -4911,14 +4911,14 @@ static void scr_exec_file_fire(ScrChild *c) { } ScrChild *scr_exec_file(ScrStr *cmd, ScrArr *args, ScrClosure *cb, - ScrExecFileFn fn) { + ScrExecFileFn fn, bool shell) { ScrStr *empty = scr_str_new("", 0); ScrChild *c = scr_spawn_opts(cmd, args, 3, 3, 3, 0, 0, false, false, false, NULL, empty); scr_str_release(empty); c->exec_cb = cb; c->exec_fn = fn; - char *display = scr_exec_display(cmd, args, false); + char *display = scr_exec_display(cmd, args, shell); c->exec_display = scr_str_new(display, strlen(display)); free(display); scr_child_stream_capture_begin(c->out_stream); diff --git a/packages/runtime/src/scr_child.h b/packages/runtime/src/scr_child.h index 350995e315..1819b9c538 100644 --- a/packages/runtime/src/scr_child.h +++ b/packages/runtime/src/scr_child.h @@ -155,9 +155,11 @@ ScrChild *scr_spawn_dynamic(ScrStr *cmd, ScrArr *args, const ScrDyn *options); void scr_child_on_spawn(ScrChild *child, ScrClosure *cb); /* moves zero-argument callback */ /* The callback slice: file + args, default options. The returned child has * all three stdio slots piped; stdout/stderr are captured internally and - * the callback moves into the child registry until settlement. */ + * the callback moves into the child registry until settlement. With shell + * (exec), args is ["-c", command] for /bin/sh and failure messages show + * the command, as for scr_exec_sync. */ ScrChild *scr_exec_file(ScrStr *cmd, ScrArr *args, ScrClosure *cb /*moves*/, - ScrExecFileFn fn); + ScrExecFileFn fn, bool shell); ScrChild *scr_child_retain(ScrChild *c); void scr_child_release(ScrChild *c); void *scr_child_retain_v(void *p); diff --git a/packages/runtime/src/scr_console.c b/packages/runtime/src/scr_console.c index d6e4e93fb6..1e031a2d04 100644 --- a/packages/runtime/src/scr_console.c +++ b/packages/runtime/src/scr_console.c @@ -204,6 +204,61 @@ void scr_init(void) { /* A real definition lets relocatable library links localize this hook; * a tentative common symbol stays externally visible on Mach-O. */ SCR_TL bool (*scr_stdio_write_hook)(int fd, const void *data, size_t len, bool wide) = NULL; +SCR_TL bool (*scr_stdio_capture)(int fd, const void *data, size_t len) = NULL; + +SCR_TL bool scr_stdin_fed = false; +SCR_TL void (*scr_stdin_fed_attach)(void) = NULL; +SCR_TL bool (*scr_stdin_fed_waiting)(void) = NULL; +static SCR_TL ScrBytes **scr_stdin_fed_q; +static SCR_TL size_t scr_stdin_fed_head, scr_stdin_fed_len, scr_stdin_fed_cap; +static SCR_TL bool scr_stdin_fed_end; + +void scr_stdin_fed_push(ScrBytes *chunk) { + if (scr_stdin_fed_end) { + if (chunk) scr_bytes_release(chunk); + return; + } + if (!chunk) { + scr_stdin_fed_end = true; + return; + } + if (scr_stdin_fed_head + scr_stdin_fed_len == scr_stdin_fed_cap) { + if (scr_stdin_fed_head > 0) { + memmove(scr_stdin_fed_q, scr_stdin_fed_q + scr_stdin_fed_head, + scr_stdin_fed_len * sizeof *scr_stdin_fed_q); + scr_stdin_fed_head = 0; + } + if (scr_stdin_fed_len == scr_stdin_fed_cap) { + scr_stdin_fed_cap = scr_stdin_fed_cap ? scr_stdin_fed_cap * 2 : 8; + scr_stdin_fed_q = realloc(scr_stdin_fed_q, scr_stdin_fed_cap * sizeof *scr_stdin_fed_q); + if (!scr_stdin_fed_q) scr_trap("scriptc: out of memory\n"); + } + } + scr_stdin_fed_q[scr_stdin_fed_head + scr_stdin_fed_len++] = chunk; +} + +ScrBytes *scr_stdin_fed_take(bool *ended) { + if (scr_stdin_fed_len == 0) { + *ended = scr_stdin_fed_end; + return NULL; + } + *ended = false; + ScrBytes *chunk = scr_stdin_fed_q[scr_stdin_fed_head++]; + if (--scr_stdin_fed_len == 0) scr_stdin_fed_head = 0; + return chunk; +} + +void scr_stdin_fed_clear(void) { + for (size_t i = 0; i < scr_stdin_fed_len; i++) + scr_bytes_release(scr_stdin_fed_q[scr_stdin_fed_head + i]); + free(scr_stdin_fed_q); + scr_stdin_fed_q = NULL; + scr_stdin_fed_head = scr_stdin_fed_len = scr_stdin_fed_cap = 0; + scr_stdin_fed_end = false; + scr_stdin_fed = false; + scr_stdin_fed_attach = NULL; + scr_stdin_fed_waiting = NULL; +} /* Lines up to this size render on the C stack (fiber stacks are 256 KiB). */ #define SCR_CONSOLE_STACK_LINE 2048 @@ -338,7 +393,7 @@ static void scr_console_write(int fd, size_t n, const ScrLogArg *args, bool glob #if SCR_STDIO_DIRECT /* A large single string goes out without a copy: string + newline in one * vectored write. */ - if (!scr_stdio_write_hook && !(global && scr_console_indenter) && n == 1 && + if (!scr_stdio_write_hook && !scr_stdio_capture && !(global && scr_console_indenter) && n == 1 && (args[0].tag & SCR_ARG_KIND) == SCR_ARG_STR && args[0].v.s->len >= SCR_CONSOLE_STACK_LINE) { char *owned; @@ -409,6 +464,7 @@ void scr_console_write_fd(int fd, size_t n, const ScrLogArg *args) { * Buffered runtime-internal stdout bytes go out first (for both streams) to * preserve stdout order and the merged-fd ordering convention. */ int scr_stdio_write_raw(int fd, const void *data, size_t len) { + if (__builtin_expect(scr_stdio_capture != NULL, 0) && scr_stdio_capture(fd == 2 ? 2 : 1, data, len)) return 0; #if SCR_STDIO_DIRECT FILE *locked = scr_fd_stream_lock(fd == 2 ? 2 : 1); if (scr_stdout_pending()) fflush(stdout); diff --git a/packages/runtime/src/scr_context.c b/packages/runtime/src/scr_context.c index fc26863747..d5bb78957d 100644 --- a/packages/runtime/src/scr_context.c +++ b/packages/runtime/src/scr_context.c @@ -23,6 +23,7 @@ static const atomic_bool scr_context_raised = true; static SCR_TL const atomic_bool *scr_context_signal = &scr_context_unsignaled; static SCR_TL bool scr_context_stopped; SCR_TL void (*scr_context_report_error)(void); +SCR_TL bool scr_context_report_rejection; typedef struct { char *name; char *value; } ScrContextEnvEntry; struct ScrContextEnv { size_t count; ScrContextEnvEntry *entries; }; @@ -196,6 +197,13 @@ double scr_context_thread_number(void) { return 0; } double scr_worker_root(void) { return -1; } ScrDyn *scr_worker_data(void) { return scr_dyn_new_null(); } ScrDyn *scr_worker_parent_port(void) { return scr_dyn_new_null(); } +ScrDyn *scr_worker_resource_limits(void) { return scr_dyn_new_obj(); } +ScrDyn *scr_worker_thread_name_current(void) { + ScrStr *empty = scr_str_new("", 0); + ScrDyn *name = scr_dyn_new_str(empty); + scr_str_release(empty); + return name; +} #endif /* ── native stack guard ───────────────────────────────────────────────── diff --git a/packages/runtime/src/scr_core.h b/packages/runtime/src/scr_core.h index a640169fa7..6d251e8ced 100644 --- a/packages/runtime/src/scr_core.h +++ b/packages/runtime/src/scr_core.h @@ -294,6 +294,15 @@ double scr_library_i64_in(int64_t v, const char *trap_msg); double scr_library_u64_in(uint64_t v, const char *trap_msg); void scr_library_str_out(ScrStr *s, const uint8_t **out, size_t *out_len); void scr_library_bytes_out(ScrBytes *b, const uint8_t **out, size_t *out_len); +/* The cstring class (a NUL-terminated `const char *`). Inbound: a copy + * decoded as UTF-8 with U+FFFD replacement; NULL delivers trap_msg (the + * assembled SC4012 message). Outbound results move into the arena and + * return their data; callback arguments return the string's own data, + * borrowed for the call. A string containing U+0000 cannot be a C string: + * both outbound forms trap instead of truncating. */ +ScrStr *scr_library_cstr_in(const char *p, const char *trap_msg); /* +1 */ +const char *scr_library_cstr_out(ScrStr *s); +const char *scr_library_cstr_arg(const ScrStr *s); #define scr_atexit(fn) scr_library_register_reset(fn) #elif defined(SCR_WORKERS) @@ -332,6 +341,8 @@ void scr_exc_alert_add(int delta); /* worker.terminate() references */ void scr_exc_alert_thread_exit(void); void scr_loop_context_shutdown(void); extern SCR_TL void (*scr_context_report_error)(void); +/* Set while scr_context_report_error reports an unhandled rejection. */ +extern SCR_TL bool scr_context_report_rejection; typedef struct ScrContextEnv ScrContextEnv; ScrContextEnv *scr_context_env_capture(void); void scr_context_env_free(ScrContextEnv *environment); diff --git a/packages/runtime/src/scr_cycle.c b/packages/runtime/src/scr_cycle.c index 229122e717..69485dd6ca 100644 --- a/packages/runtime/src/scr_cycle.c +++ b/packages/runtime/src/scr_cycle.c @@ -94,6 +94,25 @@ * objects that are not garbage. A pass that frees almost * nothing therefore doubles this fraction, and a productive * one restores it. + * full, death live objects have died outside passes since the last + * full pass (the live count fell between passes, by at + * least 1/32 of the heap that pass left) and have stopped + * dying, and the heap has grown the base fraction since. + * Growth alone cannot tell a heap that keeps live data + * from one that keeps garbage, and once full passes stop + * finding anything the fraction widens to the cap. A + * program that drops a large structure and builds another + * (a compiler rebuilding in watch mode) would then hold + * the dead structures' cyclic parts until the heap had + * grown to the cap. The acyclic parts of such a structure + * go by reference counting, which shows as the fall. One + * that finds fewer older objects than had died doubles the + * fall the next one needs, and a productive one restores + * it. Like every full pass the growth rule did not choose + * (backlog, scheduled age), it can raise the levels' + * growth baselines but not lower them: the heap regrowing + * to where it was before such a pass reclaimed a dropped + * structure is a rebuild, not growth. * mature the same two rules one level down: a growth fraction * measured from the last mature-or-full pass and scheduled * by everything mature passes free, and a backlog rule over @@ -196,6 +215,12 @@ typedef struct { unsigned char growth_shift[SCR_CYC_NGENS]; unsigned char growth_streak[SCR_CYC_NGENS]; unsigned backlog_shift[SCR_CYC_NGENS]; + size_t fallen; + size_t live_seen; + size_t full_after; + unsigned char death_shift; + bool falling; + bool keep_base; size_t nbuffered; size_t trigger; size_t alloc_trigger; @@ -234,6 +259,12 @@ static _Thread_local ScrCycThread scr_cyc_thread = {.gen_limit = SCR_CYC_OLD}; #define scr_cyc_scheduled_mature_age (scr_cyc_thread.scheduled_mature_age) #define scr_xg_freed (scr_cyc_thread.xg_freed) #define scr_cyc_mature_dead (scr_cyc_thread.mature_dead) +#define scr_cyc_fallen (scr_cyc_thread.fallen) +#define scr_cyc_live_seen (scr_cyc_thread.live_seen) +#define scr_cyc_full_after (scr_cyc_thread.full_after) +#define scr_cyc_death_shift (scr_cyc_thread.death_shift) +#define scr_cyc_falling (scr_cyc_thread.falling) +#define scr_cyc_keep_base (scr_cyc_thread.keep_base) #define scr_cyc_kinds (scr_cyc_thread.kinds) #define scr_cyc_kinds_cap (scr_cyc_thread.kinds_cap) #endif @@ -475,6 +506,22 @@ static SCR_TL unsigned scr_cyc_backlog_shift[SCR_CYC_NGENS]; static SCR_TL size_t scr_cyc_mature_dead = 0; #endif +/* The death rule (see the scheduling notes above). `fallen`: how far the + * live count fell between passes since the last full pass; `live_seen`: the + * live count at the end of the last pass or check; `full_after`: the live + * count after the last full pass; `falling`: whether it fell since the + * previous check; `death_shift`: doublings of the fall a pass needs, earned + * by passes that found less than had died; `keep_base`: the pass running + * is a full pass the growth rule did not choose, which may raise the + * levels' growth baselines but not lower them. */ +#define SCR_CYC_DEATH_DIV 32 +#define SCR_CYC_DEATH_MAX_SHIFT 8 +#ifndef SCR_WORKERS +static SCR_TL size_t scr_cyc_fallen = 0, scr_cyc_live_seen = 0, scr_cyc_full_after = 0; +static SCR_TL unsigned char scr_cyc_death_shift = 0; +static SCR_TL bool scr_cyc_falling = false, scr_cyc_keep_base = false; +#endif + static size_t scr_cyc_nursery_threshold(void) { static SCR_TL size_t cached = 0; if (cached == 0) { @@ -621,6 +668,27 @@ static void scr_cyc_adapt_growth(unsigned gen, size_t growth, } } +/* How far the live count fell since the last check: what died outside + * passes, less what was allocated meanwhile. Every pass ends with a fresh + * reading, so what passes free does not count. */ +static void scr_cyc_note_fall(void) { + scr_cyc_falling = scr_cyc_live < scr_cyc_live_seen; + if (scr_cyc_falling) scr_cyc_fallen += scr_cyc_live_seen - scr_cyc_live; + scr_cyc_live_seen = scr_cyc_live; +} + +/* The death rule: enough has died since the last full pass, it has stopped + * dying (a structure torn down by reference counting falls over several + * checks, and a pass in the middle would find its rest still referenced), + * and the heap has grown the base fraction since. */ +static SCR_CYC_SCHEDULING bool scr_cyc_died(void) { + if (scr_cyc_falling) return false; + size_t base = scr_cyc_growth_base(scr_cyc_full_after); + size_t need = base / SCR_CYC_DEATH_DIV; + need = need > (SIZE_MAX >> scr_cyc_death_shift) ? SIZE_MAX : need << scr_cyc_death_shift; + return scr_cyc_fallen >= need && scr_cyc_live > base + base / SCR_CYC_GROWTH_DIV; +} + /* One scheduled pass at `gen`. A mature or old pass adapts that level's * backlog threshold to its yield; the explicit sweep (scr_collect_cycles) is * not a scheduling decision and leaves the thresholds alone. Productive means @@ -630,11 +698,14 @@ static void scr_cyc_adapt_growth(unsigned gen, size_t growth, * * Every level shares this one body, so a program that links the collector * carries a single copy of the scheduling logic. */ -static SCR_CYC_SCHEDULING void scr_cyc_scheduled_pass(unsigned gen, bool by_growth) { +static SCR_CYC_SCHEDULING void scr_cyc_scheduled_pass(unsigned gen, bool by_growth, bool by_death) { size_t base = scr_cyc_growth_base(scr_cyc_live_after[gen]); size_t growth = scr_cyc_live > base ? scr_cyc_live - base : 0; + size_t fallen = scr_cyc_fallen; size_t old_freed_before = scr_cyc_old_freed; + scr_cyc_keep_base = gen == SCR_CYC_OLD && !by_growth; size_t freed = scr_cyc_pass(gen); + scr_cyc_keep_base = false; size_t old_reclaimed = scr_cyc_old_freed - old_freed_before + scr_cyc_mature_dead; if (gen == SCR_CYC_NURSERY) return; if (scr_cands.n != 0) { @@ -643,7 +714,10 @@ static SCR_CYC_SCHEDULING void scr_cyc_scheduled_pass(unsigned gen, bool by_grow else if (scr_cyc_backlog_shift[gen] < SCR_CYC_BACKLOG_MAX_SHIFT) scr_cyc_backlog_shift[gen]++; } - if (by_growth) + if (by_death) { + if (old_reclaimed >= fallen) scr_cyc_death_shift = 0; + else if (scr_cyc_death_shift < SCR_CYC_DEATH_MAX_SHIFT) scr_cyc_death_shift++; + } else if (by_growth) scr_cyc_adapt_growth(gen, growth, gen == SCR_CYC_OLD ? old_reclaimed : freed); } @@ -652,15 +726,17 @@ static SCR_CYC_SCHEDULING void scr_cyc_scheduled_pass(unsigned gen, bool by_grow * per pass, so it is kept out of line and built for size, which leaves the * hot buffering path in scr_cyc_on_release small. */ static SCR_CYC_SCHEDULING void scr_cyc_collect_due(void) { + scr_cyc_note_fall(); unsigned gen = SCR_CYC_OLD; bool by_growth = scr_cyc_grown(SCR_CYC_OLD); - if (!by_growth && scr_roots[SCR_CYC_OLD].n < scr_cyc_backlog_threshold(SCR_CYC_OLD)) { + bool by_death = !by_growth && scr_cyc_died(); + if (!by_growth && !by_death && scr_roots[SCR_CYC_OLD].n < scr_cyc_backlog_threshold(SCR_CYC_OLD)) { gen = SCR_CYC_MATURE; by_growth = scr_cyc_grown(SCR_CYC_MATURE); if (!by_growth && scr_roots[SCR_CYC_MATURE].n < scr_cyc_backlog_threshold(SCR_CYC_MATURE)) gen = SCR_CYC_NURSERY; } - scr_cyc_scheduled_pass(gen, by_growth); + scr_cyc_scheduled_pass(gen, by_growth, by_death); } /* One scheduled pass, for callers that reach a natural collection point @@ -669,6 +745,7 @@ static SCR_CYC_SCHEDULING void scr_cyc_collect_due(void) { * per turn would walk the whole live heap every turn — which is exactly the * cost the generations exist to avoid. */ void scr_cyc_collect_scheduled(void) { + scr_cyc_note_fall(); if (scr_cyc_buffered() == 0) { scr_cyc_scheduled_mature_age = 0; return; @@ -677,7 +754,7 @@ void scr_cyc_collect_scheduled(void) { scr_cyc_scheduled_mature_age = 0; } else if (++scr_cyc_scheduled_mature_age >= scr_cyc_nursery_threshold()) { - scr_cyc_scheduled_pass(SCR_CYC_OLD, false); + scr_cyc_scheduled_pass(SCR_CYC_OLD, false, false); return; } scr_cyc_collect_due(); @@ -926,6 +1003,9 @@ static void scr_mark_gray(void *obj, uintptr_t depth) { ScrCycHdr *h = scr_cyc_hdr(obj); if (scr_cyc_color(h) == SCR_CYC_GRAY) return; scr_cyc_set_color(h, SCR_CYC_GRAY); + /* A full pass takes every buffer, so each buffered object it reaches + * leaves its buffer here, in the header write the walk makes anyway. */ + if (scr_gen_limit == SCR_CYC_OLD && !scr_cyc_take) scr_cyc_clear_buffered(h); if (scr_white.n < SCR_CYC_GRAY_LIST_MAX) scr_cyc_push_work(&scr_white, obj); if (SCR_RC(obj) > 0) scr_gray_referenced++; if (depth == SCR_CYC_WALK_DEPTH) { @@ -1047,6 +1127,10 @@ static SCR_CYC_COMPACT void scr_xg_release(void *obj) { scr_rc_destroy(obj, scr_xg_destroy); } +/* Dead sets at least this large are torn down in address order. Smaller + * ones mostly stay in cache, where ordering them costs more than it saves. */ +#define SCR_CYC_ORDER_MIN 16384 + /* One pass over every candidate at or below `gen_limit`; returns how many * objects it freed. */ /* The second half of a pass: settle the white set, promote, re-buffer, @@ -1125,8 +1209,18 @@ static SCR_CYC_COMPACT size_t scr_cyc_settle(unsigned gen_limit) { /* Teardowns run after the full walk. They may release untraced children * (plain RC) — which can re-buffer survivors for the NEXT pass — but - * never touch traced (white, already-accounted) edges. */ + * never touch traced (white, already-accounted) edges. + * + * A large dead set is torn down in address order, highest address first + * (scr_mem_order_for_free). The walk leaves it in graph order, in which + * the free lists would hand its blocks to the next allocations scattered + * over every page the dead structure used, and the structures built from + * them, and every later walk over those, would miss the cache on most + * objects. In address order the blocks come back in the order the bump + * pointers hand out fresh memory. Teardown order is not observable. */ size_t freed = scr_white.n; + if (scr_white.n >= SCR_CYC_ORDER_MIN) + scr_mem_order_for_free(scr_white.v, scr_white.n, sizeof(ScrCycHdr)); for (size_t i = 0; i < scr_white.n; i++) { void *obj = scr_white.v[i]; scr_cyc_type(scr_cyc_hdr(obj))->free_fn(obj); @@ -1150,11 +1244,20 @@ static SCR_CYC_COMPACT size_t scr_cyc_settle(unsigned gen_limit) { /* A limited pass walked only some of the candidates: the schedule's * baselines stay those of the last pass over them all. */ + scr_cyc_live_seen = scr_cyc_live; if (scr_cyc_take == NULL) { - if (gen_limit >= SCR_CYC_MATURE) scr_cyc_live_after[SCR_CYC_MATURE] = scr_cyc_live; + /* A full pass the growth rule did not choose (death, backlog, scheduled + * age) may raise the growth baselines but not lower them. After it + * reclaims a dropped structure, the heap regrowing to where it was is + * the program rebuilding what it had, not growth: from the low point, + * the next growth passes would walk the rebuilt live heap for nothing, + * at both levels. */ + for (unsigned g = SCR_CYC_MATURE; g <= gen_limit; g++) + if (!scr_cyc_keep_base || scr_cyc_live > scr_cyc_live_after[g]) scr_cyc_live_after[g] = scr_cyc_live; if (gen_limit >= SCR_CYC_OLD) { - scr_cyc_live_after[SCR_CYC_OLD] = scr_cyc_live; scr_cyc_scheduled_mature_age = 0; + scr_cyc_fallen = 0; + scr_cyc_full_after = scr_cyc_live; } } /* markRoots drained buffers in bulk and teardowns moved the count around; @@ -1242,18 +1345,23 @@ static size_t scr_cyc_pass(unsigned gen_limit) { scr_cands = scr_roots[largest]; scr_roots[largest] = previous; scr_roots[largest].n = 0; - for (size_t i = 0; i < scr_cands.n; i++) { - ScrCycHdr *h = scr_cyc_hdr(scr_cands.v[i]); - scr_cyc_clear_buffered(h); - if (gen_limit < SCR_CYC_OLD) scr_cyc_walked(scr_cyc_type(h), gen_limit); - } + /* A full pass clears the buffered marks as markGray reaches the + * candidates, so only a limited level's candidates are touched here. */ + if (gen_limit < SCR_CYC_OLD) + for (size_t i = 0; i < scr_cands.n; i++) { + ScrCycHdr *h = scr_cyc_hdr(scr_cands.v[i]); + scr_cyc_clear_buffered(h); + scr_cyc_walked(scr_cyc_type(h), gen_limit); + } } for (unsigned g = 0; g <= gen_limit && !scr_cyc_take; g++) { for (size_t i = 0; i < scr_roots[g].n; i++) { void *obj = scr_roots[g].v[i]; - ScrCycHdr *h = scr_cyc_hdr(obj); - scr_cyc_clear_buffered(h); - if (gen_limit < SCR_CYC_OLD) scr_cyc_walked(scr_cyc_type(h), gen_limit); + if (gen_limit < SCR_CYC_OLD) { + ScrCycHdr *h = scr_cyc_hdr(obj); + scr_cyc_clear_buffered(h); + scr_cyc_walked(scr_cyc_type(h), gen_limit); + } scr_cyc_push(&scr_cands, obj); } scr_roots[g].n = 0; diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 375ced5737..667ceced0e 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -168,6 +168,9 @@ typedef enum { SCR_DYNH_DECIPHER, /* node:crypto Decipheriv (scr_cipher.c) */ SCR_DYNH_NET_SOCKET_ADDRESS, /* net.SocketAddress (scr_net.c) */ SCR_DYNH_NET_BLOCK_LIST, /* net.BlockList (scr_net.c) */ + SCR_DYNH_DISPOSABLE_STACK, /* DisposableStack (scr_symbol.c) */ + SCR_DYNH_MESSAGE_EVENT, /* ScrEvent with data — BroadcastChannel's 'message' */ + SCR_DYNH_BROADCAST_CHANNEL, /* worker_threads BroadcastChannel (scr_worker_events.c) */ SCR_DYNH_COUNT, } ScrDynHandleTag; @@ -597,6 +600,7 @@ ScrDyn *scr_dyn_native_search_params(ScrSearchParams *value); bool scr_dyn_native_search_params_is(const ScrDyn *value); bool scr_dyn_native_date_is(const ScrDyn *value); ScrDyn *scr_dyn_native_date_new(const ScrDyn *arguments); +ScrDyn *scr_dyn_native_date_from_ms(double ms); double scr_dyn_native_date_value(const ScrDyn *value); ScrDyn *scr_dyn_native_regex(ScrRegex *value); bool scr_dyn_native_regex_is(const ScrDyn *value); @@ -619,6 +623,19 @@ ScrDyn *scr_dyn_symbol_key_get(const ScrDyn *value, const ScrDyn *key, bool opti * and return NULL with the exception pending. */ ScrDyn *scr_dyn_using_method(const ScrDyn *value, bool async); ScrDyn *scr_dyn_using_dispose(const ScrDyn *method, const ScrDyn *value); +/* DisposableStack (scr_symbol.c). scr_disposable_stack_new answers a new + * stack (+1). The use, adopt and defer calls borrow their operands, throw + * Node's ReferenceError on a disposed stack and Node's TypeError on an + * invalid resource or callback, and return false with the exception + * pending. scr_disposable_stack_use_callback registers `dispose(value)` + * with the error texts of use(); the compiler passes it the disposal of a + * class instance, whose dyn view has no methods. The stack's other members + * are function values and getters of its dyn handle. */ +ScrDyn *scr_disposable_stack_new(void); +bool scr_disposable_stack_is(const ScrDyn *value); +bool scr_disposable_stack_use(const ScrDyn *stack, const ScrDyn *value); +bool scr_disposable_stack_use_callback(const ScrDyn *stack, const ScrDyn *value, const ScrDyn *dispose); +bool scr_disposable_stack_adopt(const ScrDyn *stack, const ScrDyn *value, const ScrDyn *on_dispose); void scr_dyn_symbol_key_set(ScrDyn *value, ScrDyn *key, ScrDyn *stored); void scr_dyn_symbol_key_placeholder(ScrDyn *value, ScrDyn *key); bool scr_dyn_has_key_computed(const ScrDyn *value, const ScrDyn *key); @@ -653,6 +670,10 @@ ScrDyn *scr_dyn_from_iterator(const ScrDyn *value, int *kind); ScrDyn *scr_dyn_iterator_result_get(const ScrDyn *result, const char *key, size_t len); void scr_dyn_install_iterator_symbol(ScrDyn *key); void scr_dyn_install_async_iterator_symbol(ScrDyn *key); +void scr_dyn_install_async_dispose_symbol(ScrDyn *key); +/* True when key is Symbol.asyncDispose (registered by the symbol unit when + * the program names it); handle symbol_get ops outside that unit use it. */ +bool scr_dyn_is_async_dispose_symbol(const ScrDyn *key); ScrDyn *scr_dyn_array_values_function(void); ScrDyn *scr_dyn_array_from_iterator(const ScrDyn *value); ScrDyn *scr_dyn_iterator_result(ScrDyn *value); @@ -874,6 +895,7 @@ bool scr_dyn_same_value(const ScrDyn *left, const ScrDyn *right); ScrDyn *scr_dyn_builtin_method(const ScrStr *prototype, const ScrStr *method); ScrStr *scr_dyn_to_string_argument(const ScrDyn *d, const ScrDyn *argument, const ScrStr *what); ScrDyn *scr_dyn_value_of(const ScrDyn *d, const ScrStr *what); +ScrStr *scr_dyn_error_string(const ScrDyn *d); ScrStr *scr_num_to_string_radix(double value, const ScrDyn *radix); /* JS String() over the dyn kind (units render "null"/"undefined" where * scr_dyn_to_string throws) — the web globals' WebIDL ToString. +1. */ @@ -940,6 +962,13 @@ ScrDyn *scr_structured_clone_transfer_fail_with(const ScrDyn *first); /* Validates a structuredClone options dyn value (throws on failure — * callers must check scr_exc_pending). Borrowed, NULL-tolerant. */ void scr_sc_validate_options(const ScrDyn *options); +/* markAsUncloneable(value) and the check structuredClone and postMessage + * make (scr_json.c); both borrow the value. scr_uncloneable_count is the + * number of marked values, so the clone paths test it inline and call + * scr_uncloneable_marked only once something is marked. */ +void scr_mark_uncloneable(ScrDyn *value); +bool scr_uncloneable_marked(const ScrDyn *value); +extern SCR_TL size_t scr_uncloneable_count; /* An %Error as the boundary's dyn shape ({name, message[, code]}). * Borrows; +1. */ ScrDyn *scr_dyn_from_error(const ScrError *e); @@ -1129,6 +1158,10 @@ typedef struct ScrDynHandleOps { /* The view has no properties, so util.inspect prints it in full at any * depth, as Node prints `{}`. */ bool inspect_view_empty; + /* A symbol-keyed READ (`stack[Symbol.dispose]`, the `using` method + * lookup): +1 value, or NULL when the class has no member for `key` + * (the read answers undefined). NULL hook = no symbol-keyed members. */ + ScrDyn *(*symbol_get)(void *h, const ScrDyn *key); } ScrDynHandleOps; ScrDyn *scr_dyn_native_handle_iterator(ScrDyn *source, unsigned selection); @@ -1218,11 +1251,18 @@ ScrDyn *scr_gen_receiver(void); void scr_dyn_chunk_enc(bool utf8); ScrDyn *scr_dyn_new_chunk(const ScrBytes *b); -/* util.format's %j over a dyn value: the JSON.stringify text (+1), +/* The JSON text of a dyn value: the JSON.stringify text (+1), * "undefined" when the root drops (undefined/function), or NULL with a * pending exception (a runtime handle inside the tree throws the "not * supported yet" Error). */ ScrStr *scr_dyn_format_j(const ScrDyn *d); +/* util.format's %j (Node's tryStringify), +1: over a dyn value, the + * JSON.stringify(d) text, or "undefined" when it returns undefined; over + * the text of a typed serializer that may have thrown, the text itself. + * A circular-structure TypeError becomes "[Circular]"; any other + * exception stays pending and the result is NULL. */ +ScrStr *scr_util_format_j(const ScrDyn *d); +ScrStr *scr_util_format_j_text(ScrStr *text); /* borrows text */ /* Node's ERR_INVALID_ARG_TYPE listener gate over a dyn value — shared by * the emitter unit's checked-dynamic registrations and the handle * dispatchers' .on(...) paths (both render errors.js's diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index 72d277271c..a3085399fb 100644 --- a/packages/runtime/src/scr_dyn_invoke.c +++ b/packages/runtime/src/scr_dyn_invoke.c @@ -82,13 +82,10 @@ static void scr_dyn_display_buf(ScrJsonBuf *b, const ScrDyn *d) { case SCR_DYN_OBJ: { const ScrDyn *marker = scr_dyn_obj_get(d, "%error", 6); if (marker) { - const ScrDyn *en = scr_dyn_obj_get(d, "name", 4); - const ScrDyn *em = scr_dyn_obj_get(d, "message", 7); - const ScrStr *ens = (en && en->kind == SCR_DYN_STR) ? en->v.str : NULL; - const ScrStr *ems = (em && em->kind == SCR_DYN_STR) ? em->v.str : NULL; - if (ens) scr_jb_write(b, ens->data, ens->len); - if (ens && ens->len && ems && ems->len) scr_jb_puts(b, ": "); - if (ems) scr_jb_write(b, ems->data, ems->len); + ScrStr *text = scr_dyn_error_string(d); + if (!text) return; + scr_jb_write(b, text->data, text->len); + scr_str_release(text); return; } scr_jb_puts(b, "[object Object]"); @@ -1520,7 +1517,7 @@ static ScrDyn *scr_dyn_invoke_impl( if (recv->kind == SCR_DYN_BYTES) { ScrBytes *bytes = recv->v.bytes; if (bytes->is_data_view) { - static const char *const kinds[] = { "Uint8", "Int8", "Uint16", "Int16", "Uint32", "Int32", "Float32", "Float64", "BigUint64", "BigInt64" }; + static const char *const kinds[] = { "Uint8", "Int8", "Uint16", "Int16", "Uint32", "Int32", "Float32", "Float64", "BigUint64", "BigInt64", "Float16" }; const bool get = !strncmp(method, "get", 3); const bool set = !strncmp(method, "set", 3); for (size_t kind = 0; (get || set) && kind < sizeof kinds / sizeof kinds[0]; kind++) { @@ -1537,7 +1534,7 @@ static ScrDyn *scr_dyn_invoke_impl( } const bool le = argc > (get ? 1 : 2) && scr_dyn_truthy(args[get ? 1 : 2]); if (get) { - if (kind >= SCR_DV_BIGU64) { + if (kind == SCR_DV_BIGU64 || kind == SCR_DV_BIGI64) { ScrBigInt *value = scr_bigint_dataview_get(bytes, offset, kind == SCR_DV_BIGI64, le); if (scr_exc_pending()) return NULL; ScrDyn *result = scr_dyn_new_bigint(value); @@ -1547,7 +1544,7 @@ static ScrDyn *scr_dyn_invoke_impl( double value = scr_dataview_get(bytes, offset, (ScrDataViewGet)kind, le); return scr_exc_pending() ? NULL : scr_dyn_new_num(value); } - if (kind >= SCR_DV_BIGU64) { + if (kind == SCR_DV_BIGU64 || kind == SCR_DV_BIGI64) { ScrBigInt *value = scr_dyn_bigint_coerce(argc > 1 ? args[1] : scr_dyn_undefined()); if (scr_exc_pending()) return NULL; scr_bigint_dataview_set(bytes, offset, value, le); diff --git a/packages/runtime/src/scr_event_target.c b/packages/runtime/src/scr_event_target.c index 68b7f6e879..7c8a465a69 100644 --- a/packages/runtime/src/scr_event_target.c +++ b/packages/runtime/src/scr_event_target.c @@ -783,7 +783,8 @@ static ScrEventObj *scr_event_obj_of(const ScrDyn *d); static ScrEvent *scr_event_of(const ScrDyn *d) { if (d && d->kind == SCR_DYN_HANDLE && - (d->v.handle.tag == SCR_DYNH_EVENT || d->v.handle.tag == SCR_DYNH_CUSTOM_EVENT)) { + (d->v.handle.tag == SCR_DYNH_EVENT || d->v.handle.tag == SCR_DYNH_CUSTOM_EVENT || + d->v.handle.tag == SCR_DYNH_MESSAGE_EVENT)) { return (ScrEvent *)d->v.handle.ptr; } ScrEventObj *obj = scr_event_obj_of(d); @@ -1077,6 +1078,17 @@ static ScrDyn *scr_event_get(void *ptr, const char *key, size_t len) { if (scr_et_key(key, len, "isTrusted")) return scr_dyn_new_bool(ev->trusted); if (ev->custom && scr_et_key(key, len, "detail")) return ev->detail ? scr_dyn_retain(ev->detail) : scr_dyn_new_null(); + if (ev->message) { + if (scr_et_key(key, len, "data")) return ev->detail ? scr_dyn_retain(ev->detail) : scr_dyn_new_null(); + if (scr_et_key(key, len, "origin") || scr_et_key(key, len, "lastEventId")) { + ScrStr *empty = scr_str_new("", 0); + ScrDyn *value = scr_dyn_new_str(empty); + scr_str_release(empty); + return value; + } + if (scr_et_key(key, len, "source")) return scr_dyn_new_null(); + if (scr_et_key(key, len, "ports")) return scr_dyn_new_arr(); + } return scr_et_method_value(key, len, false); } @@ -1170,6 +1182,16 @@ static const ScrDynHandleOps scr_event_ops = { .inspect_view = scr_event_inspect_view, }; +static const ScrDynHandleOps scr_message_event_ops = { + .cls = "MessageEvent", + .retain = scr_event_retain_v, + .release = scr_event_release_v, + .invoke = scr_event_invoke, + .get = scr_event_get, + .set = scr_event_set, + .inspect_view = scr_event_inspect_view, +}; + static const ScrDynHandleOps scr_custom_event_ops = { .cls = "CustomEvent", .retain = scr_event_retain_v, @@ -1491,6 +1513,7 @@ static void scr_et_install(void) { installed = true; scr_dyn_handle_install(SCR_DYNH_EVENT, &scr_event_ops); scr_dyn_handle_install(SCR_DYNH_CUSTOM_EVENT, &scr_custom_event_ops); + scr_dyn_handle_install(SCR_DYNH_MESSAGE_EVENT, &scr_message_event_ops); scr_dyn_handle_install(SCR_DYNH_EVENT_TARGET, &scr_event_target_ops); scr_event_ref_hooks = &scr_event_obj_ref_hooks; scr_et_register_teardown(); @@ -1590,6 +1613,46 @@ ScrDyn *scr_custom_event_new(ScrDyn *args) { return out; } +/* ── BroadcastChannel's EventTarget side ───────────────────────────── */ + +static ScrEventTargetCore *scr_et_broadcast_core_new(const char *cls) { + ScrEventTargetCore *core = scr_et_calloc(sizeof *core); + scr_et_core_init(core, 10, cls); + return core; +} + +static void scr_et_broadcast_core_free(ScrEventTargetCore *core) { + scr_et_core_drop(core); + free(core); +} + +/* the 'message' MessageEvent (not trusted, as in Node) with the data */ +static void scr_et_broadcast_dispatch(ScrEventTargetCore *core, ScrDyn *self, ScrDyn *data) { + ScrEvent *ev = scr_event_alloc(scr_str_new("message", 7), false); + ev->message = true; + ev->detail = scr_dyn_retain(data); + scr_event_track(ev); + ScrDyn *event = scr_dyn_new_handle(ev, SCR_DYNH_MESSAGE_EVENT); + scr_event_release_v(ev); + scr_et_dispatch(core, self, event); + scr_dyn_release(event); +} + +static ScrDyn *scr_et_broadcast_method_value(const char *name, size_t length) { + return scr_et_method_value(name, length, true); +} + +static const ScrEtBroadcastOps scr_et_broadcast_table = { + &scr_et_broadcast_core_new, &scr_et_broadcast_core_free, &scr_et_broadcast_dispatch, + &scr_et_target_method, &scr_et_broadcast_method_value, + &scr_et_handler_get, &scr_et_handler_set, +}; + +void scr_et_broadcast_install(void) { + scr_et_install(); + scr_et_broadcast_ops = &scr_et_broadcast_table; +} + bool scr_event_is(const ScrDyn *v, double kind) { if (v->kind == SCR_DYN_TYPED_REF) { /* Compiled subclasses extend Event or EventTarget (never CustomEvent). */ @@ -1598,7 +1661,8 @@ bool scr_event_is(const ScrDyn *v, double kind) { } if (v->kind != SCR_DYN_HANDLE) return false; ScrDynHandleTag tag = v->v.handle.tag; - if (kind == 0) return tag == SCR_DYNH_EVENT || tag == SCR_DYNH_CUSTOM_EVENT; + if (kind == 0) + return tag == SCR_DYNH_EVENT || tag == SCR_DYNH_CUSTOM_EVENT || tag == SCR_DYNH_MESSAGE_EVENT; if (kind == 1) return tag == SCR_DYNH_CUSTOM_EVENT; return tag == SCR_DYNH_EVENT_TARGET || (scr_et_signal_core_fn != NULL && tag == SCR_DYNH_ABORT_SIGNAL); @@ -1873,6 +1937,21 @@ ScrPromise *scr_tp_set_timeout_signal(double ms, ScrPromise *value, bool ref, Sc return scr_tp_signal(ms, value, ref, signal, false); } +ScrPromise *scr_tp_scheduler_wait_signal(double ms, ScrPromise *value, bool ref, ScrDyn *signal) { + if (scr_mt_wait_state == 2) { + static const char msg[] = "Value of \"this\" must be of type Scheduler"; + scr_throw_error_msg_code(SCR_ERR_TYPE, msg, sizeof msg - 1, "ERR_INVALID_THIS"); + return NULL; + } + if (scr_mt_wait_state == 1) { + static const char msg[] = + "scheduler.wait with a signal while mock.timers mocks it has no lowering"; + scr_throw_error_msg_code(SCR_ERR_ERROR, msg, sizeof msg - 1, "SC2020"); + return NULL; + } + return scr_tp_signal(ms, value, ref, signal, false); +} + ScrPromise *scr_tp_set_immediate_signal(ScrPromise *value, bool ref, ScrDyn *signal) { return scr_tp_signal(0, value, ref, signal, true); } diff --git a/packages/runtime/src/scr_event_target.h b/packages/runtime/src/scr_event_target.h index 6ed1475c6f..e787cdbdb2 100644 --- a/packages/runtime/src/scr_event_target.h +++ b/packages/runtime/src/scr_event_target.h @@ -39,9 +39,9 @@ typedef struct ScrEvent { size_t rc; ScrStr *type; ScrDyn *target; /* +1, NULL until dispatched */ - ScrDyn *detail; /* CustomEvent's detail (+1); NULL for Event */ + ScrDyn *detail; /* CustomEvent's detail, MessageEvent's data (+1); NULL for Event */ double time_stamp; - bool custom, bubbles, cancelable, composed, trusted; + bool custom, message, bubbles, cancelable, composed, trusted; bool default_prevented, propagation_stopped, stop, dispatching, in_passive; bool tracked; struct ScrEvent *tracked_prev, *tracked_next; @@ -82,6 +82,25 @@ extern SCR_TL ScrEventTargetCore *(*scr_et_signal_core_fn)(const ScrDyn *d, bool * which reaches the class's compiled methods. Without it a capsule's own * view answers. */ void scr_event_handle_event_reader(ScrDyn *reader); +/* BroadcastChannel's EventTarget side, which the worker events unit + * reaches through this table (that unit links without this one): a core + * of its own, dispatching a 'message' MessageEvent with the data, the + * EventTarget methods (false: not one of them), and the on + * attributes. scr_et_broadcast_install sets the table (defined in + * scr_json.c, which every program links). */ +typedef struct { + ScrEventTargetCore *(*core_new)(const char *cls); + void (*core_free)(ScrEventTargetCore *core); + void (*dispatch_message)(ScrEventTargetCore *core, ScrDyn *self, ScrDyn *data); + bool (*method)(ScrEventTargetCore *core, ScrDyn *self, const char *method, ScrDyn *const *args, + size_t argc, ScrDyn **out); + ScrDyn *(*method_value)(const char *name, size_t length); + ScrDyn *(*handler_get)(ScrEventTargetCore *core, const char *type); + void (*handler_set)(ScrEventTargetCore *core, const char *type, const ScrDyn *value); +} ScrEtBroadcastOps; +extern SCR_TL const ScrEtBroadcastOps *scr_et_broadcast_ops; +void scr_et_broadcast_install(void); + /* events.addAbortListener(signal, listener): +1 disposable, or NULL. */ ScrDyn *scr_events_add_abort_listener(ScrDyn *signal, ScrDyn *listener); /* timers/promises setTimeout(delay, value, options) and setImmediate(value, @@ -89,6 +108,11 @@ ScrDyn *scr_events_add_abort_listener(ScrDyn *signal, ScrDyn *listener); * `value` is the already-resolved promise of the resolve value, borrowed. * Never throw; +1. */ ScrPromise *scr_tp_set_timeout_signal(double ms, ScrPromise *value, bool ref, ScrDyn *signal); +/* scheduler.wait(delay, { signal }) in a program that uses mock.timers: + * scr_tp_set_timeout_signal, except that it throws Node's ERR_INVALID_THIS + * once a reset restored the real scheduler.wait, and SC2020 while + * mock.timers mocks it. NULL after a throw. */ +ScrPromise *scr_tp_scheduler_wait_signal(double ms, ScrPromise *value, bool ref, ScrDyn *signal); ScrPromise *scr_tp_set_immediate_signal(ScrPromise *value, bool ref, ScrDyn *signal); /* Compiled subclasses (`class Bus extends EventTarget`, `class Ping diff --git a/packages/runtime/src/scr_events.c b/packages/runtime/src/scr_events.c index 7b9bb27c66..670ea10d65 100644 --- a/packages/runtime/src/scr_events.c +++ b/packages/runtime/src/scr_events.c @@ -545,6 +545,25 @@ ScrPromise *scr_stdin_next_chunk(void) { * consumer, so the pipe itself is the buffer (backpressure, not loss). */ static void scr_stdin_deliver(ScrBytes *chunk); +/* A chunk that arrived (moves): delivered, or buffered while paused or + * without a consumer. */ +static void scr_stdin_arrived(ScrBytes *chunk) { + if (scr_stdin_paused || (!scr_stdin_ndata && !scr_stdin_resumed && !scr_stdin_waiter)) { + if (scr_stdin_buffer) { + size_t before = scr_stdin_buffer->len; + ScrBytes *joined = scr_bytes_new(SCR_BYTES_U8, (double)(before + chunk->len)); + memcpy(joined->data, scr_stdin_buffer->data, before); + memcpy(joined->data + before, chunk->data, chunk->len); + scr_bytes_release(scr_stdin_buffer); + scr_bytes_release(chunk); + scr_stdin_buffer = joined; + } else scr_stdin_buffer = chunk; + return; + } + scr_stdin_deliver(chunk); + scr_bytes_release(chunk); +} + static void scr_stdin_service(void) { if (!scr_stdin_watching()) return; if (!scr_stdin_paused && scr_stdin_buffer) { @@ -558,6 +577,18 @@ static void scr_stdin_service(void) { if (!scr_stdin_buffer) scr_stdin_finish(true); return; } + if (scr_stdin_fed) { + /* the chunks the parent wrote to worker.stdin, one per pass like a + * read(2) */ + bool ended = false; + ScrBytes *chunk = scr_stdin_fed_take(&ended); + if (chunk) scr_stdin_arrived(chunk); + else if (ended) { + scr_stdin_read_eof = true; + if (!scr_stdin_buffer) scr_stdin_finish(true); + } + return; + } #ifdef _WIN32 /* The poll(2)-probe's win32 spelling, by handle type: a PIPE (the * harness/ssh shape) answers PeekNamedPipe — bytes available, or a @@ -623,20 +654,7 @@ static void scr_stdin_service(void) { } ScrBytes *chunk = scr_bytes_new(SCR_BYTES_U8, (double)n); memcpy(chunk->data, buf, (size_t)n); - if (scr_stdin_paused || (!scr_stdin_ndata && !scr_stdin_resumed && !scr_stdin_waiter)) { - if (scr_stdin_buffer) { - size_t before = scr_stdin_buffer->len; - ScrBytes *joined = scr_bytes_new(SCR_BYTES_U8, (double)(before + (size_t)n)); - memcpy(joined->data, scr_stdin_buffer->data, before); - memcpy(joined->data + before, buf, (size_t)n); - scr_bytes_release(scr_stdin_buffer); - scr_bytes_release(chunk); - scr_stdin_buffer = joined; - } else scr_stdin_buffer = chunk; - return; - } - scr_stdin_deliver(chunk); - scr_bytes_release(chunk); + scr_stdin_arrived(chunk); } static void scr_stdin_deliver(ScrBytes *chunk) { @@ -1201,8 +1219,11 @@ void scr_stdin_data_thunk_bytes(ScrClosure *cb, ScrBytes *chunk) { static bool scr_events_pending(void) { return scr_stdin_pending(); } +/* A worker's fed stdin has no descriptor to poll: its chunks arrive in + * the worker's mailbox, which the worker unit waits on while + * scr_stdin_fed_waiting answers true. */ static bool scr_events_watching(void) { - return (scr_context_is_main() && scr_sig_watched > 0) || scr_stdin_pending(); + return (scr_context_is_main() && scr_sig_watched > 0) || (!scr_stdin_fed && scr_stdin_pending()); } /* One dispatch pass at a loop turn: wake-pipe bytes are consumed (safe @@ -1250,7 +1271,12 @@ void scr_events_install(void) { static SCR_TL bool installed = false; if (installed) return; installed = true; - if (!scr_context_is_main()) scr_stdin_read_eof = true; + if (scr_stdin_fed) { + scr_stdin_fed_waiting = scr_stdin_pending; + if (scr_stdin_fed_attach) scr_stdin_fed_attach(); + } else if (!scr_context_is_main()) { + scr_stdin_read_eof = true; + } #ifdef __wasi__ int stdin_flags = fcntl(0, F_GETFL, 0); if (stdin_flags >= 0) (void)fcntl(0, F_SETFL, stdin_flags | O_NONBLOCK); diff --git a/packages/runtime/src/scr_events_emitter.c b/packages/runtime/src/scr_events_emitter.c index 4f934ce246..ceecb15e3d 100644 --- a/packages/runtime/src/scr_events_emitter.c +++ b/packages/runtime/src/scr_events_emitter.c @@ -1115,3 +1115,11 @@ void scr_emitter_set_default_max(double n) { scr_emitter_default_max = n; } double scr_emitter_get_default_max(void) { return scr_emitter_default_max; } + +/* The events module's ES module namespace holds a copy of + * defaultMaxListeners, taken when the program starts; ESM namespace + * and named-import reads answer it. module.syncBuiltinESMExports() copies + * the current value into it. */ +static SCR_TL double scr_emitter_esm_default_max = 10; +double scr_emitter_esm_default_max_get(void) { return scr_emitter_esm_default_max; } +void scr_emitter_esm_sync(void) { scr_emitter_esm_default_max = scr_emitter_default_max; } diff --git a/packages/runtime/src/scr_events_emitter.h b/packages/runtime/src/scr_events_emitter.h index c7edcf1070..99da57a504 100644 --- a/packages/runtime/src/scr_events_emitter.h +++ b/packages/runtime/src/scr_events_emitter.h @@ -143,6 +143,8 @@ ScrEmitter *scr_emitter_set_max(ScrEmitter *em, double n); /* returns em +1 */ double scr_emitter_get_max(ScrEmitter *em); void scr_emitter_set_default_max(double n); double scr_emitter_get_default_max(void); +double scr_emitter_esm_default_max_get(void); +void scr_emitter_esm_sync(void); /* True when the emitter has at least one listener for `name` — the * stream unit's registration probe (and anyone else's). */ bool scr_emitter_has(ScrEmitter *em, const char *name); diff --git a/packages/runtime/src/scr_exception.h b/packages/runtime/src/scr_exception.h index c9622ceb30..80fb989579 100644 --- a/packages/runtime/src/scr_exception.h +++ b/packages/runtime/src/scr_exception.h @@ -213,5 +213,10 @@ extern SCR_TL int (*scr_uncaught_exception_hook)(bool from_promise); bool scr_exc_handle_uncaught(bool from_promise); void scr_process_on_uncaught_exception(ScrDyn *fn, bool once, bool monitor); void scr_process_off_uncaught_exception(ScrDyn *fn, bool monitor); +/* The value Node reports for an unhandled rejection (scr_async_dyn.c): an + * error-like reason itself, otherwise an UnhandledPromiseRejection Error + * (code ERR_UNHANDLED_REJECTION) quoting the reason. Borrows; +1, NULL + * after a throw. */ +ScrDyn *scr_unhandled_rejection_error(ScrDyn *reason); #endif /* SCR_EXCEPTION_H */ diff --git a/packages/runtime/src/scr_fetch.c b/packages/runtime/src/scr_fetch.c index a7b540adb4..f586b96f89 100644 --- a/packages/runtime/src/scr_fetch.c +++ b/packages/runtime/src/scr_fetch.c @@ -496,6 +496,15 @@ static void sf_signal_timer_fire(ScrClosure *cb) { sf_signal_release(s); } +/* The mocked AbortSignal.timeout's timer: Node's mock calls abort() on + * the signal's controller, so the reason is the AbortError. */ +static void sf_signal_mock_timer_fire(ScrClosure *cb) { + SfSignal *s = (SfSignal *)scr_box_get_ref(cb->caps[0]); + if (!s) return; + if (!s->aborted) sf_signal_abort_default(s, false); + sf_signal_release(s); +} + ScrDyn *scr_fetch_abort_timeout(ScrDyn *delay) { if (!delay || delay->kind != SCR_DYN_NUM) { if (delay) { @@ -527,13 +536,19 @@ ScrDyn *scr_fetch_abort_timeout(ScrDyn *delay) { return NULL; } SfSignal *s = sf_signal_new(); - ScrClosure *cb = scr_closure_new((void *)&sf_signal_timer_fire, 1); + bool mocked = scr_mt_abort_timeout_arm != NULL; + ScrClosure *cb = scr_closure_new( + mocked ? (void *)&sf_signal_mock_timer_fire : (void *)&sf_signal_timer_fire, 1); ScrBox *box = scr_box_new_obj(&sf_signal_retain_v, &sf_signal_release_v, NULL); scr_box_set_ref(box, sf_signal_retain(s)); cb->caps[0] = box; - s->timer_id = scr_set_timeout_handle(cb, ms); - scr_timer_unref(s->timer_id); + if (mocked) { + scr_mt_abort_timeout_arm(cb, ms); + } else { + s->timer_id = scr_set_timeout_handle(cb, ms); + scr_timer_unref(s->timer_id); + } ScrDyn *out = scr_dyn_new_handle(s, SCR_DYNH_ABORT_SIGNAL); sf_signal_release(s); return out; diff --git a/packages/runtime/src/scr_generator.h b/packages/runtime/src/scr_generator.h index f528b8b8fa..2c2c0e9538 100644 --- a/packages/runtime/src/scr_generator.h +++ b/packages/runtime/src/scr_generator.h @@ -133,6 +133,8 @@ void scr_gen_yield_f64(double v); void scr_gen_yield_bool(bool v); void scr_gen_yield_ref(void *v, void (*release)(void *)); /* moves */ void scr_async_gen_hop_done(void); +/* An async generator's `return x` whose completion turn awaits x. */ +void scr_async_gen_return_awaits(void); void scr_gen_out_f64(ScrGen *g, double v); /* trampoline completion stores */ void scr_gen_out_bool(ScrGen *g, bool v); void scr_gen_out_ref(ScrGen *g, void *v, void (*release)(void *)); /* moves */ diff --git a/packages/runtime/src/scr_inspect.c b/packages/runtime/src/scr_inspect.c index e0615635a4..34438353f7 100644 --- a/packages/runtime/src/scr_inspect.c +++ b/packages/runtime/src/scr_inspect.c @@ -6,9 +6,10 @@ * escapes, the 10000-char string cap and the per-line ` +` continuation * form), Buffer's hex form, and the LAYOUT engine — a * frame stack reproducing reduceToSingleString, isBelowBreakLength, and - * groupArrayElements from Node v24's lib/internal/util/inspect.js for the - * DEFAULT options (compact: 3, breakLength: 80, maxArrayLength: 100, - * maxStringLength: 10000; non-default options fence in the frontend). + * groupArrayElements from Node v24's lib/internal/util/inspect.js for + * maxArrayLength: 100 and maxStringLength: 10000, a numeric or false + * compact (default 3), any breakLength (default 80) and a boolean sorted; + * other non-default options fence in the frontend. * * Lengths follow Node exactly: the break-length math counts UTF-16 code * units (JS .length); the grid-grouping math counts DISPLAY WIDTH. Node @@ -242,8 +243,20 @@ static SCR_TL size_t g_nframes; static SCR_TL size_t g_frames_cap; static SCR_TL size_t g_indent; /* ctx.indentationLvl */ static SCR_TL bool g_no_break; /* breakLength: Infinity (console.table cells) */ +static SCR_TL double g_break_length = 80; /* ctx.breakLength */ +static SCR_TL double g_compact = 3; /* ctx.compact: a number, 0 for false */ +static SCR_TL bool g_sorted; /* ctx.sorted === true */ static SCR_TL double g_cur_depth; /* ctx.currentDepth (last-entered composite) */ +/* util.inspect's breakLength, compact and sorted for the renders that + * follow (compact false is 0); the call site restores 80, 3 and false + * after its render. */ +void scr_insp_layout(double break_length, double compact, bool sorted) { + g_break_length = break_length; + g_compact = compact; + g_sorted = sorted; +} + static InspFrame *insp_top(void) { return &g_frames[g_nframes - 1]; } static void insp_circ_reset(void); /* circular-reference state, below */ @@ -360,6 +373,13 @@ ScrStr *scr_insp_ref_wrap(const void *v, ScrStr *s) { return out; } +/* formatRaw's Date base: toISOString(), or toString()'s "Invalid Date" + * when the time value is NaN (the only time value outside the ISO range). */ +ScrStr *scr_insp_date(double ms) { + if (isnan(ms)) return scr_str_new("Invalid Date", 12); + return scr_date_to_iso(ms); +} + /* remainingText: the "... N more items" tail entry. */ ScrStr *scr_insp_more_items(double remaining) { char buf[64]; @@ -382,15 +402,15 @@ static void insp_group(InspFrame *f, bool trailing_more) { if ((double)w > max_length) max_length = (double)w; } double actual_max = max_length + 2; - if (actual_max * 3 + (double)g_indent < 80 && + if (actual_max * 3 + (double)g_indent < g_break_length && (total_length / actual_max > 5 || max_length <= 6)) { double approx_char_heights = 2.5; double average_bias = sqrt(actual_max - total_length / (double)f->len); double biased_max = fmax(actual_max - 3 - average_bias, 1); double columns_d = fmin( fmin(round(sqrt(approx_char_heights * biased_max * (double)output_len) / biased_max), - floor((80 - (double)g_indent) / actual_max)), - fmin(3 * 4 /* ctx.compact * 4 */, 15)); + floor((g_break_length - (double)g_indent) / actual_max)), + fmin(g_compact * 4, 15)); if (columns_d <= 1) { free(data_len); return; @@ -458,18 +478,23 @@ static void insp_group(InspFrame *f, bool trailing_more) { free(data_len); } -/* isBelowBreakLength: UTF-16 lengths against breakLength 80. */ +static int insp_item_cmp(const void *a, const void *b) { + return scr_str_cmp_u16(*(ScrStr *const *)a, *(ScrStr *const *)b); +} + +/* isBelowBreakLength: UTF-16 lengths against ctx.breakLength. */ static bool insp_below_break_length(InspFrame *f, size_t start, const ScrStr *base) { size_t total = f->len + start; - if (total + f->len > 80) return false; + if ((double)(total + f->len) > g_break_length) return false; for (size_t i = 0; i < f->len; i++) { total += insp_utf16_len(f->items[i]->data, f->items[i]->len); - if (total > 80) return false; + if ((double)total > g_break_length) return false; } return base->len == 0 || !insp_contains_byte(base->data, base->len, '\n'); } -/* reduceToSingleString for the default options (compact: 3). Pops the +/* reduceToSingleString for a numeric or false compact (only a compact of + * at least 1 groups arrays and tries the single line). Pops the * frame. base/b0/b1 are borrowed; +1 result. `recurse` is the frame's * recurseTimes (the children's depth); array_extras marks kArrayExtrasType * (grid grouping applies); trailing_more marks a "... N more items" tail @@ -478,10 +503,15 @@ ScrStr *scr_insp_end(ScrStr *base, ScrStr *b0, ScrStr *b1, double recurse, bool bool trailing_more) { InspFrame *f = insp_top(); g_indent -= 2; + /* sorted: true orders an object-type composite's entries (properties, + * and Map and Set entries) by their rendered text, in UTF-16 code-unit + * order; arrays keep their element order. */ + if (g_sorted && !array_extras && f->len > 1) qsort(f->items, f->len, sizeof *f->items, insp_item_cmp); size_t entries = f->len; - if (array_extras && entries > 6) insp_group(f, trailing_more); + bool compact = g_compact >= 1; + if (compact && array_extras && entries > 6) insp_group(f, trailing_more); InspBuf out = {0}; - if (g_cur_depth - recurse < 3 && entries == f->len) { + if (compact && g_cur_depth - recurse < g_compact && entries == f->len) { size_t start = f->len + g_indent + insp_utf16_len(b0->data, b0->len) + insp_utf16_len(base->data, base->len) + 10; if (insp_below_break_length(f, start, base)) { @@ -619,7 +649,7 @@ static size_t insp_utf16_offset(const char *data, size_t len, size_t units, bool /* formatPrimitive over a string value under the default options: * maxStringLength 10000, then the per-line ` +` continuation when the - * (truncated) text is longer than 16 chars AND breakLength(80) - + * (truncated) text is longer than 16 chars AND breakLength - * indentationLvl - 4, split after each newline; the trailer counts the * cut characters. Uses the CURRENT indentation. */ ScrStr *scr_insp_str(ScrStr *s) { @@ -652,7 +682,8 @@ ScrStr *scr_insp_str(ScrStr *s) { if (surrogate_tail_len) ib_bytes(&val, surrogate_tail, surrogate_tail_len); size_t vlen = val.len; const char *v = val.data ? val.data : ""; - if (!g_no_break && units > 16 && units > 80 - g_indent - 4 && memchr(v, '\n', vlen) != NULL) { + if (!g_no_break && units > 16 && (double)units > g_break_length - (double)g_indent - 4 && + memchr(v, '\n', vlen) != NULL) { /* split after each newline; quote each chunk; join with ` +\n` */ size_t chunk_start = 0; bool first = true; @@ -946,6 +977,22 @@ static ScrStr *scr_insp_class_tag(const char *cls, const char *tail) { /* A native Set or Map held as a checked-dynamic value: `Set(n) { a, b }`, * `Map(n) { k => v }`, at most 100 entries like Node's maxArrayLength. */ +/* Node's formatWeakCollection: the entries of a WeakMap or WeakSet are not + * listed without showHidden, so the body is the one "" + * entry, and past the depth limit the value prints as [WeakMap]. */ +static ScrStr *scr_insp_weak_collection(const char *label, double recurse, double depth) { + InspBuf out = {0}; + if (recurse > depth) { + ib_char(&out, '['); + ib_cstr(&out, label); + ib_char(&out, ']'); + return ib_take(&out); + } + ib_cstr(&out, label); + ib_cstr(&out, " { }"); + return ib_take(&out); +} + static ScrStr *scr_insp_dyn_collection(ScrDyn *d, double recurse, double depth, bool is_map) { ScrMap *map = d->v.handle.ptr; const char *label = is_map ? "Map" : "Set"; @@ -1077,6 +1124,46 @@ static ScrStr *scr_insp_obj(ScrDyn *d, double recurse, double depth, const char return out; } +/* A numeric typed array (not a Buffer): `Uint8Array(n) [ ... ]` with + * Node's maxArrayLength (100) and array grid, `[Uint8Array]` past the + * depth limit. Borrows b; +1. */ +ScrStr *scr_insp_typed_array(ScrBytes *b, double recurse, double depth) { + char prefix[48]; + int pn = snprintf(prefix, sizeof prefix, "%s(%zu) [", scr_bytes_elem_name(b->elem), b->len); + if (b->len == 0) { + InspBuf out = {0}; + ib_bytes(&out, prefix, (size_t)pn); + ib_char(&out, ']'); + return ib_take(&out); + } + if (recurse > depth) { + char tag[32]; + int n = snprintf(tag, sizeof tag, "[%s]", scr_bytes_elem_name(b->elem)); + return scr_str_new(tag, (size_t)n); + } + scr_insp_begin(recurse + 1); + size_t shown = b->len < 100 ? b->len : 100; + for (size_t i = 0; i < shown; i++) { + ScrStr *s = scr_insp_f64(scr_bytes_get(b, (double)i)); + scr_insp_entry(s, true); + scr_str_release(s); + } + bool more = b->len > 100; + if (more) { + ScrStr *m = scr_insp_more_items((double)(b->len - 100)); + scr_insp_entry(m, true); + scr_str_release(m); + } + ScrStr *base = scr_str_new("", 0); + ScrStr *b0 = scr_str_new(prefix, (size_t)pn); + ScrStr *b1 = scr_str_new("]", 1); + ScrStr *out = scr_insp_end(base, b0, b1, recurse + 1, true, more); + scr_str_release(base); + scr_str_release(b0); + scr_str_release(b1); + return out; +} + ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { if (d->kind == SCR_DYN_PROXY) return scr_insp_dyn(d->v.proxy.target, recurse, depth); switch (d->kind) { @@ -1153,6 +1240,10 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { } if (d->v.handle.tag == SCR_DYNH_SET || d->v.handle.tag == SCR_DYNH_MAP) return scr_insp_dyn_collection(d, recurse, depth, d->v.handle.tag == SCR_DYNH_MAP); + if (d->v.handle.tag == SCR_DYNH_WEAK_MAP || d->v.handle.tag == SCR_DYNH_WEAK_SET) + return scr_insp_weak_collection(d->v.handle.tag == SCR_DYNH_WEAK_MAP ? "WeakMap" : "WeakSet", + recurse, depth); + if (d->v.handle.tag == SCR_DYNH_DATE) return scr_insp_date(scr_dyn_native_date_value(d)); const ScrDynHandleOps *ops = scr_dyn_handle_ops_of(d); if (ops->inspect_view) { /* Node's custom inspectors answer the bare class name beyond the @@ -1245,40 +1336,7 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { /* Buffer-flavored (a stream chunk): Node's form. */ return scr_insp_buffer(b); } - char prefix[48]; - int pn = snprintf(prefix, sizeof prefix, "%s(%zu) [", scr_bytes_elem_name(b->elem), b->len); - if (b->len == 0) { - InspBuf out = {0}; - ib_bytes(&out, prefix, (size_t)pn); - ib_char(&out, ']'); - return ib_take(&out); - } - if (recurse > depth) { - char tag[32]; - int n = snprintf(tag, sizeof tag, "[%s]", scr_bytes_elem_name(b->elem)); - return scr_str_new(tag, (size_t)n); - } - scr_insp_begin(recurse + 1); - size_t shown = b->len < 100 ? b->len : 100; - for (size_t i = 0; i < shown; i++) { - ScrStr *s = scr_insp_f64(scr_bytes_get(b, (double)i)); - scr_insp_entry(s, true); - scr_str_release(s); - } - bool more = b->len > 100; - if (more) { - ScrStr *m = scr_insp_more_items((double)(b->len - 100)); - scr_insp_entry(m, true); - scr_str_release(m); - } - ScrStr *base = scr_str_new("", 0); - ScrStr *b0 = scr_str_new(prefix, (size_t)pn); - ScrStr *b1 = scr_str_new("]", 1); - ScrStr *out = scr_insp_end(base, b0, b1, recurse + 1, true, more); - scr_str_release(base); - scr_str_release(b0); - scr_str_release(b1); - return out; + return scr_insp_typed_array(b, recurse, depth); } } return scr_str_new("undefined", 9); /* unreachable */ diff --git a/packages/runtime/src/scr_inspect.h b/packages/runtime/src/scr_inspect.h index e489b23b78..f8d28b4f09 100644 --- a/packages/runtime/src/scr_inspect.h +++ b/packages/runtime/src/scr_inspect.h @@ -23,6 +23,8 @@ ScrStr *scr_insp_f64(double x); /* JS ToString, except -0 → "-0" */ ScrStr *scr_insp_str(ScrStr *s); /* the quoting ladder + line splitting */ ScrStr *scr_insp_regex(ScrRegex *re); /* /source/flags */ +/* A Date (its time value): toISOString(), or "Invalid Date" for NaN (+1). */ +ScrStr *scr_insp_date(double ms); ScrStr *scr_insp_buffer(ScrBytes *b); /* , 50-byte cap */ /* The stackless error form: [Name: message] / [Name], plus the stamped * code slot as its one extra property ({ code: 'X' }); [Name] beyond @@ -32,9 +34,16 @@ ScrStr *scr_insp_error(ScrError *e, double recurse, double depth); * traversal is here. dyn-boxed bytes render in the checked-dynamic tree's documented * Uint8Array identity. */ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth); +/* A numeric typed array that is not a Buffer: `Uint8Array(n) [ ... ]`, + * `[Uint8Array]` past the depth limit. Borrows b; +1. */ +ScrStr *scr_insp_typed_array(ScrBytes *b, double recurse, double depth); /* format's %s / rest-args twin: dyn strings pass verbatim. */ ScrStr *scr_insp_dyn_s(ScrDyn *d, double depth); void scr_insp_begin(double recurse); +/* util.inspect's breakLength, compact (0 for false) and sorted for the + * renders that follow; the call site restores the defaults (80, 3, false) + * afterwards. */ +void scr_insp_layout(double break_length, double compact, bool sorted); void scr_insp_entry(ScrStr *s, bool is_num); /* Circular references, Node-exact ( / [Circular *N]): the * compiler-emitted helpers over CYCLE-CAPABLE composites call circ_check diff --git a/packages/runtime/src/scr_itable.c b/packages/runtime/src/scr_itable.c index 08db948b55..4842bf79cb 100644 --- a/packages/runtime/src/scr_itable.c +++ b/packages/runtime/src/scr_itable.c @@ -428,11 +428,13 @@ static const ScrITableEntry *scr_itable_insert_bytes(ScrITable *t, uint64_t hash const ScrITableEntry *scr_itable_insert_str(ScrITable *t, const ScrStr *key, uint64_t value) { scr_itable_enter(); + scr_sa_thread_shares(); /* the stored value graph is read by every thread */ return scr_itable_insert_bytes(t, scr_itable_str_hash(key), key->data, key->len, value); } const ScrITableEntry *scr_itable_insert_f64(ScrITable *t, double key, uint64_t value) { scr_itable_enter(); + scr_sa_thread_shares(); /* the stored value graph is read by every thread */ uint64_t stored = scr_itable_f64_bits(key) ^ SCR_ITABLE_F64_FLIP; uint64_t hash = scr_itable_f64_hash(stored); ScrITableSegment *seg = scr_itable_segment(t, hash); @@ -588,6 +590,7 @@ const ScrITableEntry *scr_itable_find_arr(ScrITable *t, const ScrArr *key) { const ScrITableEntry *scr_itable_insert_arr(ScrITable *t, const ScrArr *key, uint64_t value) { scr_itable_enter(); + scr_sa_thread_shares(); /* the stored value graph is read by every thread */ char stack[SCR_ITABLE_STACK_KEY]; size_t len; char *bytes = scr_itable_encoded(t, key, stack, sizeof stack, &len); diff --git a/packages/runtime/src/scr_itable.test.c b/packages/runtime/src/scr_itable.test.c index 0f218806b9..5e5f22bc95 100644 --- a/packages/runtime/src/scr_itable.test.c +++ b/packages/runtime/src/scr_itable.test.c @@ -8,13 +8,18 @@ #include #include -/* The table unit needs only scr_trap and scr_throw_error_msg from the - * rest of the runtime; refusals are recorded here. */ +/* The table unit needs only scr_trap, scr_throw_error_msg and + * scr_sa_thread_shares from the rest of the runtime; refusals and the + * inserting threads' shares are recorded here. */ _Noreturn void scr_trap(const char *msg) { fputs(msg, stderr); abort(); } +/* Inserts share the stored values with every thread. */ +static atomic_int shares; +void scr_sa_thread_shares(void) { atomic_fetch_add(&shares, 1); } + static char refused[128]; void scr_throw_error_msg(int kind, const char *msg, size_t len) { assert(kind == SCR_ERR_TYPE && len < sizeof refused); @@ -222,6 +227,7 @@ int main(void) { for (unsigned i = 1; i < THREADS; i++) assert(seen_str[i][k] == seen_str[0][k]); assert(scr_itable_size(strings) == KEYS); assert(scr_itable_size(numbers) == KEYS); + assert(atomic_load(&shares) >= 2 * KEYS); printf("intern tables share one entry per key across racing inserts and lookups\n"); return 0; } diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index cdfb3d0f9f..b23e782414 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -108,6 +108,26 @@ static void scr_async_iterator_symbol_cleanup(void) { scr_async_iterator_symbol = NULL; } +static SCR_TL ScrDyn *scr_async_dispose_symbol; + +static void scr_async_dispose_symbol_cleanup(void) { + scr_dyn_release(scr_async_dispose_symbol); + scr_async_dispose_symbol = NULL; +} + +/* Symbol.asyncDispose, registered by the symbol unit when the program + * first names it, for handle classes outside that unit (a Worker). */ +void scr_dyn_install_async_dispose_symbol(ScrDyn *key) { + if (scr_async_dispose_symbol) return; + scr_async_dispose_symbol = scr_dyn_retain(key); + scr_atexit(scr_async_dispose_symbol_cleanup); +} + +bool scr_dyn_is_async_dispose_symbol(const ScrDyn *key) { + return scr_async_dispose_symbol && key->kind == SCR_DYN_SYMBOL && + key->v.symbol.value == scr_async_dispose_symbol->v.symbol.value; +} + static void scr_to_primitive_symbol_cleanup(void) { scr_dyn_release(scr_to_primitive_symbol); scr_to_primitive_symbol = NULL; @@ -482,6 +502,7 @@ typedef struct ScrJsonSeenEnt { const void *ptr; bool is_array; const char *prop; /* static property edge (emitted C literal) */ + size_t prop_len; /* its length, or SIZE_MAX for a NUL-terminated name */ const ScrStr *prop_str; /* overflow-key edge (borrowed for the write) */ size_t index; /* array/tuple index edge */ } ScrJsonSeenEnt; @@ -489,7 +510,7 @@ typedef struct ScrJsonSeenEnt { static void scr_jb_put_edge(ScrJsonBuf *m, const ScrJsonSeenEnt *e) { if (e->prop || e->prop_str) { scr_jb_puts(m, "property '"); - if (e->prop) scr_jb_puts(m, e->prop); + if (e->prop) scr_jb_write(m, e->prop, e->prop_len == SIZE_MAX ? strlen(e->prop) : e->prop_len); else scr_jb_write(m, e->prop_str->data, e->prop_str->len); scr_jb_putc(m, '\''); return; @@ -539,6 +560,7 @@ bool scr_jb_enter(ScrJsonBuf *b, const void *v, bool is_array) { e->ptr = v; e->is_array = is_array; e->prop = NULL; + e->prop_len = SIZE_MAX; e->prop_str = NULL; e->index = 0; return true; @@ -551,6 +573,16 @@ void scr_jb_leave(ScrJsonBuf *b) { void scr_jb_edge_prop(ScrJsonBuf *b, const char *name) { ScrJsonSeenEnt *e = &b->seen[b->seen_len - 1]; e->prop = name; + e->prop_len = SIZE_MAX; + e->prop_str = NULL; +} + +/* A property edge whose name is not NUL-terminated (a declared field name + * from a record source's strided table). */ +static void scr_jb_edge_span(ScrJsonBuf *b, const char *name, size_t len) { + ScrJsonSeenEnt *e = &b->seen[b->seen_len - 1]; + e->prop = name; + e->prop_len = len; e->prop_str = NULL; } @@ -1449,6 +1481,8 @@ ScrDyn *scr_dyn_iter_pack(const ScrDyn *src, const ScrStr *msg) { return scr_dyn_not_iterable(src, msg); } +static ScrDyn *scr_native_iterator_drain(const ScrDyn *src); + /* The for-of-over-dyn pack accessors: the emitted index loop drives them * over a scr_dyn_iter_pack result (ARR by construction — the defensive * arms cover nothing reachable from that lowering). Never throw. */ @@ -1463,6 +1497,13 @@ ScrDyn *scr_dyn_map_seed_entries(const ScrDyn *src) { scr_dyn_release(view); if (scr_exc_pending()) return NULL; } + /* A native iterator (sp.entries(), a Map's entries()) or a handle that + * steps through live entries (URLSearchParams): producing its entries + * runs no program code, so draining them first is what the consumer + * observes. */ + if (src->kind == SCR_DYN_HANDLE && + (src->v.handle.tag == SCR_DYNH_ITERATOR || scr_dyn_handle_ops_of(src)->iter_step)) + return scr_native_iterator_drain(src); if (src->kind == SCR_DYN_HANDLE || src->kind == SCR_DYN_TYPED_REF) { static const char message[] = "new Map(entries) over native non-array iterables has no lowering"; scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); @@ -1729,13 +1770,40 @@ static bool scr_dyn_typed_ref_prototype(const ScrDyn *value) { return view && (view->kind == SCR_DYN_OBJ || view->kind == SCR_DYN_ARR); } +/* True when `prototype` is null or an object or function, the values + * Object.create and Object.setPrototypeOf accept. Otherwise throws Node's + * TypeError, whose message ends with the value's string form. */ +static bool scr_dyn_prototype_valid(const ScrDyn *prototype) { + if (scr_dyn_is_object(prototype) || scr_dyn_is_callable(prototype)) return true; + if (scr_exc_pending()) return false; + ScrStr *text = prototype->kind == SCR_DYN_SYMBOL + ? prototype->v.symbol.render(prototype->v.symbol.value) : scr_dyn_string_coerce_js(prototype); + if (!text) return false; + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, "Object prototype may only be an Object or null: "); + scr_jb_put_str(&b, text); + scr_str_release(text); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b)); + return false; +} + +/* A valid receiver or prototype whose kind this operation does not handle. */ +static void scr_dyn_prototype_unsupported(const char *operation) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, operation); + scr_jb_puts(&b, " with this value is not supported yet"); + scr_throw_error(SCR_ERR_ERROR, scr_jb_finish(&b)); +} + ScrDyn *scr_dyn_obj_create(ScrDyn *prototype) { + if (!scr_dyn_prototype_valid(prototype)) return NULL; if (prototype != scr_builtin_object_prototype && scr_dyn_class_reflection_fence(prototype)) return NULL; bool typed = scr_dyn_typed_ref_prototype(prototype); if (scr_exc_pending()) return NULL; if (prototype->kind != SCR_DYN_OBJ && prototype->kind != SCR_DYN_ARR && prototype->kind != SCR_DYN_NULL && !typed) { - static const char message[] = "Object prototype may only be an Object or null"; - scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + scr_dyn_prototype_unsupported("Object.create"); return NULL; } ScrDyn *object = prototype->kind == SCR_DYN_NULL ? scr_dyn_new_obj_null_proto() : scr_dyn_new_obj(); @@ -1786,12 +1854,7 @@ ScrDyn *scr_dyn_set_prototype(ScrDyn *object, ScrDyn *prototype) { * only check. */ if (object->kind == SCR_DYN_BOOL || object->kind == SCR_DYN_NUM || object->kind == SCR_DYN_STR || object->kind == SCR_DYN_BIGINT || object->kind == SCR_DYN_SYMBOL) { - bool valid = scr_dyn_is_object(prototype) || scr_dyn_is_callable(prototype); - if (scr_exc_pending()) return NULL; - if (valid) return scr_dyn_retain(object); - static const char message[] = "Object prototype may only be an Object or null"; - scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); - return NULL; + return scr_dyn_prototype_valid(prototype) ? scr_dyn_retain(object) : NULL; } if (scr_dyn_class_reflection_fence(object) || scr_dyn_class_reflection_fence(prototype)) return NULL; if (object->kind == SCR_DYN_TYPED_REF) { @@ -1821,12 +1884,12 @@ ScrDyn *scr_dyn_set_prototype(ScrDyn *object, ScrDyn *prototype) { scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); return NULL; } + if (!scr_dyn_prototype_valid(prototype)) return NULL; bool typed = scr_dyn_typed_ref_prototype(prototype); if (scr_exc_pending()) return NULL; if ((object->kind != SCR_DYN_OBJ && object->kind != SCR_DYN_ARR) || (prototype->kind != SCR_DYN_OBJ && prototype->kind != SCR_DYN_ARR && prototype->kind != SCR_DYN_FUNC && prototype->kind != SCR_DYN_NULL && !typed)) { - static const char message[] = "Object prototype may only be an Object or null"; - scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + scr_dyn_prototype_unsupported("Object.setPrototypeOf"); return NULL; } for (const ScrDyn *current = scr_dyn_chain_link(prototype); current;) { @@ -2615,54 +2678,116 @@ ScrDyn *scr_record_source_apply(ScrDyn *snapshot, const void *rec) { return merged; } +static ScrDyn *scr_json_member(const ScrDyn *holder, const ScrStr *key); +static bool scr_jb_try_pure_member(ScrJsonBuf *b, const char *key, size_t key_len, + const ScrStr *key_str, const ScrDyn *value, bool *first); +static bool scr_jb_put_member(ScrJsonBuf *b, const ScrStr *key, ScrDyn *value, bool *first, + void *rec); + /* JSON for a record that kept a source: the view's own-key order, declared * fields through the compiled writer (`field` writes the separator, key * and value of field `index` unless the record lacks it, and answers - * whether it wrote), every other enumerable property through the dyn - * writer with JSON.stringify's member rules, then declared fields the view - * had not seen. May leave an exception pending (a BigInt property). */ + * whether it wrote), every other enumerable property with JSON.stringify's + * member rules (accessors read, toJSON called with the key, undefined, + * function and symbol results dropped, circular structures rejected), then + * declared fields the view had not seen. May leave an exception pending + * (a BigInt property, a throwing toJSON or getter, a circular structure). */ void scr_jb_put_record_source(ScrJsonBuf *b, void *rec, bool (*field)(ScrJsonBuf *, void *, size_t, bool)) { if (!scr_jb_try_record_source(b, rec, field)) scr_jb_puts(b, "{}"); } +/* Writes declared field `index`; when the record is on the circular- + * structure stack, the field's name becomes its edge first. */ +static bool scr_jb_put_source_field(ScrJsonBuf *b, void *rec, const ScrRecordSource *source, + size_t index, bool first, bool on_stack, + bool (*field)(ScrJsonBuf *, void *, size_t, bool)) { + if (on_stack) { + size_t len; + memcpy(&len, source->lens + index * source->len_stride, sizeof len); + const char *name; + memcpy(&name, source->names + index * source->name_stride, sizeof name); + scr_jb_edge_span(b, name, len); + } + return field(b, rec, index, first); +} + /* The same in one lookup: writes the record and answers true when it kept * a source, otherwise writes nothing and answers false. */ bool scr_jb_try_record_source(ScrJsonBuf *b, void *rec, bool (*field)(ScrJsonBuf *, void *, size_t, bool)) { - ScrRecordSource *source = scr_record_source_find(rec); - if (!source) return false; + ScrRecordSource *found = scr_record_source_find(rec); + if (!found) return false; + /* Program code run by a member can add record sources and move the + * table's entries, so the walk keeps its own copy. */ + const ScrRecordSource source = *found; + /* Walkers over cycle-capable shapes have pushed the record already. + * Others push it before the first member that needs the protocol walk, + * so a member leading back to the record closes the circle at the + * record. */ + bool on_stack = b->seen_len && b->seen[b->seen_len - 1].ptr == rec; + bool entered = false; + ScrDyn *view = scr_dyn_retain(source.view); scr_jb_putc(b, '{'); bool first = true; - { - size_t count = source->count; - bool small[64] = { false }; - bool *seen = count <= 64 ? small : calloc(count, sizeof *seen); - if (!seen) scr_json_oom(); - const ScrDyn *view = source->view; - for (size_t i = 0; i < view->v.obj.len && !scr_exc_pending(); i++) { - const ScrDynEntry *entry = &view->v.obj.entries[i]; - size_t index = scr_record_source_index(source, entry->key, entry->key_len); + size_t count = source.count; + bool small[64] = { false }; + bool *seen = count <= 64 ? small : calloc(count, sizeof *seen); + if (!seen) scr_json_oom(); + /* Members whose writing cannot run program code are written in place. + * The first member that can (an accessor, a toJSON method, a getter below + * it) ends that loop: the remaining keys are taken first, as + * SerializeJSONObject takes them before reading any value. */ + size_t i = 0; + for (; i < view->v.obj.len && !scr_exc_pending(); i++) { + const ScrDynEntry *entry = &view->v.obj.entries[i]; + size_t index = scr_record_source_index(&source, entry->key, entry->key_len); + if (index < count) { + if (!seen[index] && scr_jb_put_source_field(b, rec, &source, index, first, on_stack, field)) + first = false; + seen[index] = true; + continue; + } + if (!entry->enumerable) continue; + if (entry->accessor || + !scr_jb_try_pure_member(b, entry->key, entry->key_len, NULL, entry->value, &first)) + break; + } + if (i < view->v.obj.len && !scr_exc_pending() && !on_stack) { + entered = scr_jb_enter(b, rec, false); + on_stack = entered; + } + if (i < view->v.obj.len && !scr_exc_pending()) { + ScrDyn *rest = scr_dyn_new_arr(); + for (size_t j = i; j < view->v.obj.len; j++) { + const ScrDynEntry *entry = &view->v.obj.entries[j]; + if (!entry->enumerable && !scr_record_source_declared(&source, entry->key, entry->key_len)) continue; + ScrStr *name = scr_str_new(entry->key, entry->key_len); + scr_dyn_arr_push(rest, scr_dyn_new_str(name)); + scr_str_release(name); + } + for (size_t j = 0; j < rest->v.arr.len && !scr_exc_pending(); j++) { + const ScrStr *key = rest->v.arr.items[j]->v.str; + size_t index = scr_record_source_index(&source, key->data, key->len); if (index < count) { - if (!seen[index] && field(b, rec, index, first)) first = false; + if (!seen[index] && scr_jb_put_source_field(b, rec, &source, index, first, on_stack, field)) + first = false; seen[index] = true; continue; } - if (!entry->enumerable || entry->accessor) continue; - ScrDynKind kind = entry->value->kind; - if (kind == SCR_DYN_UNDEF || kind == SCR_DYN_FUNC || kind == SCR_DYN_SYMBOL) continue; - if (!first) scr_jb_putc(b, ','); - first = false; - scr_jb_put_json_span(b, entry->key, entry->key_len); - scr_jb_putc(b, ':'); - scr_jb_put_dyn(b, entry->value); - } - for (size_t index = 0; index < count && !scr_exc_pending(); index++) { - if (!seen[index] && field(b, rec, index, first)) first = false; + ScrDyn *value = scr_json_member(view, key); + if (!value || !scr_jb_put_member(b, key, value, &first, NULL)) break; } - if (seen != small) free(seen); + scr_dyn_release(rest); } + for (size_t index = 0; index < count && !scr_exc_pending(); index++) { + if (!seen[index] && scr_jb_put_source_field(b, rec, &source, index, first, on_stack, field)) + first = false; + } + if (seen != small) free(seen); + scr_dyn_release(view); scr_jb_putc(b, '}'); + if (entered) scr_jb_leave(b); return true; } @@ -3095,6 +3220,8 @@ bool scr_dyn_typed_array_is(const ScrDyn *d, int elem) { return d && d->kind == SCR_DYN_BYTES && !d->v.bytes->is_data_view && d->v.bytes->elem == (ScrBytesElem)elem; } +static bool scr_native_iterator_over(const ScrDyn *value, unsigned kind); + /* Node util.types reads internal brands, never user properties or prototypes. */ bool scr_dyn_util_type_is(const ScrDyn *value, const ScrStr *probe) { if (!value) return false; @@ -3113,6 +3240,7 @@ bool scr_dyn_util_type_is(const ScrDyn *value, const ScrStr *probe) { if (PROBE("isInt8Array")) return scr_dyn_typed_array_is(value, SCR_BYTES_I8); if (PROBE("isInt16Array")) return scr_dyn_typed_array_is(value, SCR_BYTES_I16); if (PROBE("isInt32Array")) return scr_dyn_typed_array_is(value, SCR_BYTES_I32); + if (PROBE("isFloat16Array")) return scr_dyn_typed_array_is(value, SCR_BYTES_F16); if (PROBE("isFloat32Array")) return scr_dyn_typed_array_is(value, SCR_BYTES_F32); if (PROBE("isFloat64Array")) return scr_dyn_typed_array_is(value, SCR_BYTES_F64); if (PROBE("isMap")) return value->kind == SCR_DYN_HANDLE && value->v.handle.tag == SCR_DYNH_MAP; @@ -3125,6 +3253,16 @@ bool scr_dyn_util_type_is(const ScrDyn *value, const ScrStr *probe) { if (PROBE("isAsyncFunction")) return value->kind == SCR_DYN_FUNC && (scr_closure_identity(value->v.fn.clo)->function_kind & 2) != 0; if (PROBE("isGeneratorFunction")) return value->kind == SCR_DYN_FUNC && (scr_closure_identity(value->v.fn.clo)->function_kind & 1) != 0; if (PROBE("isGeneratorObject")) return scr_dyn_generator(value); + if (PROBE("isDate")) return value->kind == SCR_DYN_HANDLE && value->v.handle.tag == SCR_DYNH_DATE; + if (PROBE("isMapIterator")) return scr_native_iterator_over(value, 2); + if (PROBE("isSetIterator")) return scr_native_iterator_over(value, 3); + if (PROBE("isKeyObject")) return value->kind == SCR_DYN_HANDLE && value->v.handle.tag == SCR_DYNH_KEY_OBJECT; + /* Boxed primitives, BigInt64Array, BigUint64Array, CryptoKey and external + * values cannot be created here, so no value has these brands. */ + if (PROBE("isNumberObject") || PROBE("isStringObject") || PROBE("isBooleanObject") || + PROBE("isBigIntObject") || PROBE("isSymbolObject") || PROBE("isBoxedPrimitive") || + PROBE("isBigInt64Array") || PROBE("isBigUint64Array") || PROBE("isCryptoKey") || PROBE("isExternal")) + return false; if (PROBE("isNativeError")) { ScrError *error = scr_errdyn_err_of(value); if (!error) return false; @@ -3139,7 +3277,7 @@ bool scr_dyn_util_type_is(const ScrDyn *value, const ScrStr *probe) { * anonymous with length zero. Keep their native closure identity intact. */ ScrDyn *scr_dyn_util_type_value(const ScrDyn *value, const ScrStr *probe) { static const char *const named[] = { "isDataView", "isTypedArray", "isUint8Array", "isUint8ClampedArray", "isUint16Array", "isUint32Array", - "isInt8Array", "isInt16Array", "isInt32Array", "isFloat32Array", "isFloat64Array" }; + "isInt8Array", "isInt16Array", "isInt32Array", "isFloat16Array", "isFloat32Array", "isFloat64Array" }; const char *name = ""; if (probe->len == 17 && memcmp(probe->data, "isArrayBufferView", 17) == 0) name = "isView"; for (size_t i = 0; i < sizeof named / sizeof named[0]; i++) @@ -3158,16 +3296,16 @@ ScrBytes *scr_dyn_bytes_unbox(const ScrDyn *d) { /* Numeric typed-array constructor values share native function identity. * The cache owns one reference until runtime cleanup; arbitrary strings or * functions cannot impersonate one of these constructors. */ -static SCR_TL ScrDyn *scr_bytes_ctors[9]; +static SCR_TL ScrDyn *scr_bytes_ctors[SCR_BYTES_ELEM_COUNT]; static void scr_bytes_ctors_cleanup(void) { - for (size_t i = 0; i < 9; i++) { + for (size_t i = 0; i < SCR_BYTES_ELEM_COUNT; i++) { scr_dyn_release(scr_bytes_ctors[i]); scr_bytes_ctors[i] = NULL; } } static ScrDyn *scr_bytes_ctor_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { (void)args; (void)argc; - for (size_t i = 0; i < 9; i++) { + for (size_t i = 0; i < SCR_BYTES_ELEM_COUNT; i++) { if (!scr_bytes_ctors[i] || scr_bytes_ctors[i]->v.fn.clo != closure) continue; ScrJsonBuf message; scr_jb_init(&message); @@ -3183,7 +3321,7 @@ static ScrDyn *scr_dyn_fn_properties(const ScrDyn *function); static ScrDyn *scr_bytes_constructor_for(ScrBytesElem elem) { if (!scr_bytes_ctors[elem]) { bool initialized = false; - for (size_t i = 0; i < 9; i++) if (scr_bytes_ctors[i]) initialized = true; + for (size_t i = 0; i < SCR_BYTES_ELEM_COUNT; i++) if (scr_bytes_ctors[i]) initialized = true; if (!initialized) scr_atexit(scr_bytes_ctors_cleanup); scr_bytes_ctors[elem] = scr_dyn_new_func(scr_closure_new(NULL, 0), scr_bytes_ctor_call, 3, "typed-array-constructor", scr_bytes_elem_name(elem)); @@ -3196,7 +3334,7 @@ static ScrDyn *scr_bytes_constructor_for(ScrBytesElem elem) { return scr_dyn_retain(scr_bytes_ctors[elem]); } ScrDyn *scr_bytes_constructor(const ScrStr *name) { - for (size_t i = 0; i < 9; i++) { + for (size_t i = 0; i < SCR_BYTES_ELEM_COUNT; i++) { const char *candidate = scr_bytes_elem_name((ScrBytesElem)i); if (strlen(candidate) == name->len && memcmp(candidate, name->data, name->len) == 0) return scr_bytes_constructor_for((ScrBytesElem)i); @@ -3263,7 +3401,7 @@ bool scr_bytes_instanceof(const ScrDyn *value, const ScrDyn *callee) { } if (scr_array_buffer_ctor && callee->v.fn.clo == scr_array_buffer_ctor->v.fn.clo) return scr_array_buffer_is(value); if (scr_array_ctor && callee->v.fn.clo == scr_array_ctor->v.fn.clo) return value->kind == SCR_DYN_ARR || scr_dyn_ref_is_array(value); - for (size_t i = 0; i < 9; i++) { + for (size_t i = 0; i < SCR_BYTES_ELEM_COUNT; i++) { if (scr_bytes_ctors[i] && callee->v.fn.clo == scr_bytes_ctors[i]->v.fn.clo) return scr_dyn_typed_array_is(value, (int)i); } @@ -3276,7 +3414,7 @@ ScrDyn *scr_bytes_construct(const ScrDyn *callee, const ScrDyn *args, const ScrS return scr_array_ctor_call(callee->v.fn.clo, args->v.arr.items, args->v.arr.len); if (callee->kind == SCR_DYN_FUNC && scr_array_buffer_ctor && callee->v.fn.clo == scr_array_buffer_ctor->v.fn.clo) return scr_array_buffer_new(args->v.arr.len ? args->v.arr.items[0] : scr_dyn_undefined()); - for (size_t i = 0; i < 9; i++) { + for (size_t i = 0; i < SCR_BYTES_ELEM_COUNT; i++) { if (callee->kind != SCR_DYN_FUNC || !scr_bytes_ctors[i] || callee->v.fn.clo != scr_bytes_ctors[i]->v.fn.clo) continue; const ScrDyn *input = args->v.arr.len > 0 ? args->v.arr.items[0] : scr_dyn_undefined(); const ScrDyn *offset = args->v.arr.len > 1 ? args->v.arr.items[1] : scr_dyn_undefined(); @@ -3513,6 +3651,9 @@ ScrDyn *scr_dyn_bind(ScrDyn *target, ScrDyn *const *args, size_t argc) { * they can box its handles. */ static SCR_TL const ScrDynHandleOps *scr_dynh_ops[SCR_DYNH_COUNT]; +/* BroadcastChannel's EventTarget table (scr_et_broadcast_install). */ +SCR_TL const ScrEtBroadcastOps *scr_et_broadcast_ops = NULL; + void scr_dyn_handle_install(ScrDynHandleTag tag, const ScrDynHandleOps *ops) { scr_dynh_ops[tag] = ops; } @@ -3840,7 +3981,7 @@ ScrDyn *scr_dyn_new_handle(void *h, ScrDynHandleTag tag) { d->v.handle.ptr = scr_dyn_handle_ops(tag)->retain(h); d->v.handle.tag = tag; d->v.handle.traced = tag == SCR_DYNH_WEAK_MAP || tag == SCR_DYNH_WEAK_SET || - tag == SCR_DYNH_WORKER || tag == SCR_DYNH_MESSAGE_PORT || + tag == SCR_DYNH_WORKER || tag == SCR_DYNH_MESSAGE_PORT || tag == SCR_DYNH_DISPOSABLE_STACK || ((tag == SCR_DYNH_SET || tag == SCR_DYNH_MAP) && ((ScrMap *)h)->traced != 0); return d; @@ -4294,6 +4435,13 @@ typedef struct { static ScrDyn *scr_iterator_read(const ScrDyn *value, const char *key, size_t len); +/* A Map (kind 2) or Set (kind 3) iterator: util.types.isMapIterator and + * isSetIterator. */ +static bool scr_native_iterator_over(const ScrDyn *value, unsigned kind) { + return value->kind == SCR_DYN_HANDLE && value->v.handle.tag == SCR_DYNH_ITERATOR && + ((const ScrNativeIterator *)value->v.handle.ptr)->kind == kind; +} + static SCR_TL ScrDyn *scr_iterator_functions[11]; static SCR_TL bool scr_iterator_functions_registered; @@ -4554,6 +4702,33 @@ ScrDyn *scr_dyn_native_handle_iterator(ScrDyn *source, unsigned selection) { return scr_native_collection_iterator(source, selection); } +/* The values a native iterator (or a live-stepping handle's entries + * iterator) produces from its current position, into a fresh array; NULL + * with the exception pending. */ +static ScrDyn *scr_native_iterator_drain(const ScrDyn *src) { + ScrDyn *iterator = src->v.handle.tag == SCR_DYNH_ITERATOR + ? scr_dyn_retain((ScrDyn *)src) + : scr_dyn_native_handle_iterator((ScrDyn *)src, 2); + ScrDyn *out = scr_dyn_new_arr(); + for (;;) { + ScrDyn *step = scr_native_iterator_next(iterator->v.handle.ptr); + if (!step) { + scr_dyn_release(out); + out = NULL; + break; + } + bool done = scr_dyn_truthy(scr_dyn_obj_get(step, "done", 4)); + if (!done) { + const ScrDyn *value = scr_dyn_obj_get(step, "value", 5); + scr_dyn_arr_push(out, scr_dyn_retain((ScrDyn *)(value ? value : scr_dyn_undefined()))); + } + scr_dyn_release(step); + if (done) break; + } + scr_dyn_release(iterator); + return out; +} + static ScrDyn *scr_builtin_iterator_checked(int kind) { ScrDyn *self = scr_dyn_this_get(); ScrDyn *result = NULL; @@ -4820,6 +4995,11 @@ ScrDyn *scr_dyn_symbol_key_get(const ScrDyn *value, const ScrDyn *key, bool opti } bool builtin = scr_iterator_symbol && key->v.symbol.value == scr_iterator_symbol->v.symbol.value && scr_builtin_iterable(value); if (builtin && value->kind != SCR_DYN_ARR) return scr_builtin_iterator_method(value); + if (value->kind == SCR_DYN_HANDLE) { + const ScrDynHandleOps *ops = scr_dyn_handle_ops(value->v.handle.tag); + ScrDyn *member = ops->symbol_get ? ops->symbol_get(value->v.handle.ptr, key) : NULL; + if (member || scr_exc_pending()) return member; + } ScrDyn *receiver = scr_dyn_symbol_receiver(value); if (!receiver) return scr_exc_pending() ? NULL : scr_dyn_retain(scr_dyn_undefined()); ScrDynEntry *entry = scr_dyn_symbol_entry(receiver, key, false); @@ -5343,7 +5523,7 @@ ScrStr *scr_dyn_object_tag(const ScrDyn *d) { d->v.handle.tag == SCR_DYNH_WEAK_MAP || d->v.handle.tag == SCR_DYNH_WEAK_SET || d->v.handle.tag == SCR_DYNH_SET || d->v.handle.tag == SCR_DYNH_MAP || d->v.handle.tag == SCR_DYNH_REGEXP || d->v.handle.tag == SCR_DYNH_DATE || d->v.handle.tag == SCR_DYNH_BLOB || - d->v.handle.tag == SCR_DYNH_FILE) { + d->v.handle.tag == SCR_DYNH_FILE || d->v.handle.tag == SCR_DYNH_DISPOSABLE_STACK) { ScrJsonBuf b; scr_jb_init(&b); scr_jb_puts(&b, "[object "); @@ -5500,8 +5680,8 @@ static bool scr_dyn_json_write(ScrJsonBuf *b, const ScrDyn *d) { } } -/* util.format's %j argument (+1): the stringify text, "undefined" for a - * root the stringify drops, or NULL with a pending exception (a handle +/* The JSON text of a dyn value (+1): the stringify text, "undefined" for + * a root the stringify drops, or NULL with a pending exception (a handle * inside the tree). */ ScrStr *scr_dyn_format_j(const ScrDyn *d) { ScrJsonBuf b; @@ -5861,11 +6041,28 @@ void scr_errdyn_put(ScrError *e, ScrDyn *d) { scr_errdyn_append(e, d); } static void scr_error_sync_cause(ScrDyn *view, const ScrStr *key) { bool cause = key->len == 5 && memcmp(key->data, "cause", 5) == 0; bool stack = key->len == 5 && memcmp(key->data, "stack", 5) == 0; - if (!cause && !stack) return; + bool name = key->len == 4 && memcmp(key->data, "name", 4) == 0; + bool message = key->len == 7 && memcmp(key->data, "message", 7) == 0; + if (!cause && !stack && !name && !message) return; size_t hit = scr_errdyn_find(view, false); if (hit == SIZE_MAX) return; ScrDynEntry *entry = scr_dyn_entry(view, key); ScrError *e = scr_errdyn_cache[hit].err; + if (name || message) { + /* The native error's toString and inspect read these fields. */ + if (!entry || entry->accessor || entry->value->kind != SCR_DYN_STR) return; + ScrStr **field = name ? &e->name : &e->message; + scr_str_release(*field); + *field = scr_str_retain(entry->value->v.str); + if (name) { + e->name_present = true; + e->name_enumerable = entry->enumerable; + } else { + e->message_present = true; + e->message_enumerable = entry->enumerable; + } + return; + } if (stack) { if (entry && entry->value->kind == SCR_DYN_STR) { scr_str_release(e->stack); @@ -5881,6 +6078,46 @@ static void scr_error_sync_cause(ScrDyn *view, const ScrStr *key) { e->cause_enumerable = entry && entry->enumerable; } +/* Error.prototype.toString over the checked-dynamic error encoding (an + * object carrying the reserved "%error" marker): `name` defaults to "Error" + * and `message` to "", both read through the prototype; either empty leaves + * the other alone, otherwise "name: message". NULL with a pending exception + * when a member's string conversion throws. */ +ScrStr *scr_dyn_error_string(const ScrDyn *d) { + ScrStr *parts[2] = { NULL, NULL }; + static const char *const keys[2] = { "name", "message" }; + for (int i = 0; i < 2; i++) { + ScrDyn *member = scr_dyn_obj_read(d, keys[i], strlen(keys[i])); + if (!member || member->kind == SCR_DYN_UNDEF) { + parts[i] = i == 0 ? scr_str_new("Error", 5) : scr_str_new("", 0); + } else { + parts[i] = scr_dyn_string_coerce_js(member); + } + scr_dyn_release(member); + if (!parts[i]) { + scr_str_release(parts[0]); + return NULL; + } + } + if (parts[0]->len == 0 || parts[1]->len == 0) { + ScrStr *out = parts[0]->len == 0 ? parts[1] : parts[0]; + scr_str_release(out == parts[0] ? parts[1] : parts[0]); + return out; + } + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_put_str(&b, parts[0]); + scr_jb_puts(&b, ": "); + scr_jb_put_str(&b, parts[1]); + scr_str_release(parts[0]); + scr_str_release(parts[1]); + return scr_jb_finish(&b); +} + +static bool scr_dyn_error_encoding(const ScrDyn *d) { + return d->kind == SCR_DYN_OBJ && scr_dyn_obj_get(d, "%error", 6) != NULL; +} + /* Receiver-kind-dispatched toString() on a checked-dynamic value (the * dyn method surface — a stream's 'data'/for-await chunk is the common * receiver): bytes decode per the encoding (Node's Buffer.toString, @@ -5916,6 +6153,7 @@ ScrStr *scr_dyn_to_string(const ScrDyn *d, const ScrStr *enc) { case SCR_DYN_BOOL: return d->v.b ? scr_str_new("true", 4) : scr_str_new("false", 5); case SCR_DYN_OBJ: + if (scr_dyn_error_encoding(d) && !scr_dyn_property_owner(d, "toString", 8)) return scr_dyn_error_string(d); return scr_str_new("[object Object]", 15); case SCR_DYN_HANDLE: if (d->v.handle.tag == SCR_DYNH_REGEXP || d->v.handle.tag == SCR_DYNH_URL) { @@ -5928,7 +6166,8 @@ ScrStr *scr_dyn_to_string(const ScrDyn *d, const ScrStr *enc) { if ((d->v.handle.tag >= SCR_DYNH_ABORT_SIGNAL && d->v.handle.tag <= SCR_DYNH_ABORT_CONTROLLER) || d->v.handle.tag == SCR_DYNH_EVENT_TARGET || d->v.handle.tag == SCR_DYNH_CUSTOM_EVENT || d->v.handle.tag == SCR_DYNH_ARRAY_BUFFER || d->v.handle.tag == SCR_DYNH_SHARED_ARRAY_BUFFER || d->v.handle.tag == SCR_DYNH_SET || d->v.handle.tag == SCR_DYNH_MAP || - d->v.handle.tag == SCR_DYNH_BLOB || d->v.handle.tag == SCR_DYNH_FILE) { + d->v.handle.tag == SCR_DYNH_BLOB || d->v.handle.tag == SCR_DYNH_FILE || + d->v.handle.tag == SCR_DYNH_DISPOSABLE_STACK) { ScrJsonBuf b; scr_jb_init(&b); scr_jb_puts(&b, "[object "); @@ -6309,6 +6548,13 @@ bool scr_dyn_number_coerce_js(const ScrDyn *d, double *out) { * number-hint protocol advances to toString. */ continue; } + if (i == 1 && scr_dyn_error_encoding(d)) { + ScrStr *text = scr_dyn_error_string(d); + if (!text) return false; + *out = scr_string_to_number(text); + scr_str_release(text); + return true; + } if (scr_dyn_has_object_prototype(d) && i == 1) { *out = NAN; /* Number("[object Object]") */ return true; @@ -6424,6 +6670,13 @@ static ScrDyn *scr_dyn_add_primitive(const ScrDyn *d) { const ScrDyn *owner = scr_dyn_property_owner(d, methods[i], strlen(methods[i])); ScrDyn *method = owner ? scr_dyn_obj_read(d, methods[i], strlen(methods[i])) : NULL; if (!method) { + if (i == 1 && scr_dyn_error_encoding(d)) { + ScrStr *str = scr_dyn_error_string(d); + if (!str) return NULL; + ScrDyn *out = scr_dyn_new_str(str); + scr_str_release(str); + return out; + } if (i == 1 && scr_dyn_has_object_prototype(d)) { ScrStr *str = scr_str_new("[object Object]", 15); ScrDyn *out = scr_dyn_new_str(str); @@ -6977,99 +7230,6 @@ void scr_dyn_key_set_computed(ScrDyn *recv, ScrDyn *key, ScrDyn *value) { scr_str_release(name); } -/* Node's JSON.stringify over a dyn: object members holding undefined DROP, - * array slots holding undefined print null. A bare undefined never arrives - * (the record serializer drops the entry first); print null defensively. */ -void scr_jb_put_dyn(ScrJsonBuf *b, const ScrDyn *d) { - if (d->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("JSON.stringify"); return; } - switch (d->kind) { - case SCR_DYN_NULL: - case SCR_DYN_UNDEF: - case SCR_DYN_SYMBOL: - case SCR_DYN_FUNC: /* JSON.stringify: functions serialize like undefined */ - scr_jb_puts(b, "null"); - return; - case SCR_DYN_BOOL: - scr_jb_puts(b, d->v.b ? "true" : "false"); - return; - case SCR_DYN_BIGINT: { - static const char message[] = "Do not know how to serialize a BigInt"; - scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); - return; - } - case SCR_DYN_NUM: - scr_jb_put_f64(b, d->v.num); - return; - case SCR_DYN_STR: - scr_jb_put_json_str(b, d->v.str); - return; - case SCR_DYN_BYTES: { - /* Node's JSON.stringify over a typed array: the index-keyed object - * form — {"0":1,"1":2}, using each view's numeric element kind. */ - scr_jb_putc(b, '{'); - for (size_t i = 0; i < d->v.bytes->len; i++) { - if (i > 0) scr_jb_putc(b, ','); - char idx[32]; - snprintf(idx, sizeof idx, "\"%zu\":", i); - scr_jb_puts(b, idx); - scr_jb_put_f64(b, scr_bytes_get(d->v.bytes, (double)i)); - } - scr_jb_putc(b, '}'); - return; - } - case SCR_DYN_ARR: - scr_jb_putc(b, '['); - for (size_t i = 0; i < d->v.arr.len; i++) { - if (i > 0) scr_jb_putc(b, ','); - scr_jb_put_dyn(b, d->v.arr.items[i]); - } - scr_jb_putc(b, ']'); - return; - case SCR_DYN_PROMISE: - /* No own enumerable properties — Node stringifies a promise as {}. */ - scr_jb_puts(b, "{}"); - return; - case SCR_DYN_HANDLE: { - /* Node's JSON.stringify over these classes throws the circular- - * structure TypeError with a V8 path dump we cannot reproduce — - * throw "not supported yet" instead of a wrong shape (SEMANTICS.md). */ - ScrJsonBuf m; - scr_jb_init(&m); - scr_jb_puts(&m, "JSON.stringify of a dynamic "); - scr_jb_puts(&m, scr_dyn_handle_cls(d)); - scr_jb_puts(&m, " is not supported yet"); - scr_throw_error(SCR_ERR_ERROR, scr_jb_finish(&m)); - scr_jb_puts(b, "null"); /* the buffer never surfaces: the pending throw wins */ - return; - } - - case SCR_DYN_TYPED_REF: { - ScrDyn *materialized = scr_dyn_typed_ref_materialize(d); - scr_jb_put_dyn(b, materialized); - scr_dyn_release(materialized); - return; - } - case SCR_DYN_OBJ: { - scr_jb_putc(b, '{'); - bool first = true; - for (size_t i = 0; i < d->v.obj.len; i++) { - const ScrDynEntry *e = &d->v.obj.entries[i]; - if (!e->enumerable) continue; - if (e->value->kind == SCR_DYN_UNDEF || e->value->kind == SCR_DYN_FUNC || e->value->kind == SCR_DYN_SYMBOL) continue; /* dropped, like Node */ - /* engine functions drop too */ - if (!first) scr_jb_putc(b, ','); - first = false; - scr_jb_put_json_span(b, e->key, e->key_len); - scr_jb_putc(b, ':'); - scr_jb_put_dyn(b, e->value); - } - scr_jb_putc(b, '}'); - return; - } - } - scr_trap("scriptc: internal error: invalid dyn kind\n"); -} - /* ── dynCheck failure path ─────────────────────────────────────────────── */ static void scr_dyn_path_render(ScrJsonBuf *b, const ScrDynPath *p) { @@ -8854,9 +9014,12 @@ static bool scr_json_live_container(const ScrDyn *value, bool inspect) { return true; } -static ScrDyn *scr_json_replace(const ScrDyn *holder, const ScrStr *key, const ScrDyn *replacer) { - ScrDyn *value = scr_json_member(holder, key); - if (!value) return NULL; +/* SerializeJSONProperty's toJSON step over a member value (consumed): Date, + * URL and net handles convert through their toJSON, a Buffer through its + * { type, data } view, and an object with a callable toJSON through that + * method, called with the key. Returns the value to serialize (+1), or NULL + * with an exception pending. */ +static ScrDyn *scr_json_to_json(ScrDyn *value, const ScrStr *key) { /* Inspect a typed reference without replacing its live identity. Later * members must read the source again after a callback mutates it. A live * array, or a live record without a `toJSON` field, needs no conversion. */ @@ -8891,6 +9054,13 @@ static ScrDyn *scr_json_replace(const ScrDyn *holder, const ScrStr *key, const S } else scr_dyn_release(method); } scr_dyn_release(view); + return value; +} + +static ScrDyn *scr_json_replace(const ScrDyn *holder, const ScrStr *key, const ScrDyn *replacer) { + ScrDyn *value = scr_json_member(holder, key); + if (!value) return NULL; + value = scr_json_to_json(value, key); if (!value) return NULL; ScrDyn *result = scr_json_callback(replacer, holder, key, value); scr_dyn_release(value); @@ -9140,6 +9310,243 @@ ScrDyn *scr_json_stringify_value(const ScrDyn *value, const ScrDyn *replacer, co return result; } +/* util.format's %j over a pending stringify exception (Node's + * tryStringify): a TypeError whose first message line is "Converting + * circular structure to JSON" is cleared and the result is the text + * "[Circular]" (+1). Any other exception is put back and the result is + * NULL. */ +static ScrStr *scr_json_circular_or_rethrow(void) { + ScrCaught *caught = scr_exc_take(); + ScrDyn *thrown = scr_caught_to_dyn(caught); + ScrError *error = scr_errdyn_err_of(thrown); + scr_dyn_release(thrown); + static const char circular[] = "Converting circular structure to JSON"; + bool is_circular = false; + if (error && error->name && error->message) { + const ScrStr *message = error->message; + const char *newline = memchr(message->data, '\n', message->len); + size_t first_line = newline ? (size_t)(newline - message->data) : message->len; + is_circular = error->name->len == 9 && memcmp(error->name->data, "TypeError", 9) == 0 && + first_line == sizeof circular - 1 && memcmp(message->data, circular, first_line) == 0; + } + scr_error_release(error); + if (!is_circular) scr_rethrow(caught); + scr_caught_release(caught); + return is_circular ? scr_str_new("[Circular]", 10) : NULL; +} + +/* util.format's %j over a dynamic value (+1): JSON.stringify(d), the text + * "undefined" when the stringify returns undefined, and "[Circular]" for a + * circular structure. NULL with any other exception pending. */ +ScrStr *scr_util_format_j(const ScrDyn *d) { + ScrStr *gap = scr_str_new("", 0); + ScrDyn *result = scr_json_stringify_replacer(d, scr_dyn_undefined(), gap); + scr_str_release(gap); + if (!result) return scr_exc_pending() ? scr_json_circular_or_rethrow() : NULL; + ScrStr *text = result->kind == SCR_DYN_STR ? scr_str_retain(result->v.str) : scr_str_new("undefined", 9); + scr_dyn_release(result); + return text; +} + +/* util.format's %j over a typed value whose serializer can throw (+1): the + * serialized text when no exception is pending, "[Circular]" for a circular + * structure, NULL with any other exception pending. Borrows text. */ +ScrStr *scr_util_format_j_text(ScrStr *text) { + if (!scr_exc_pending()) return scr_str_retain(text); + return scr_json_circular_or_rethrow(); +} + +/* The gap of a compact walk; the walks read only its length. */ +static const ScrStr scr_json_compact_gap = { .rc = SIZE_MAX, .len = 0 }; + +#define SCR_JSON_PURE_DEPTH 32 + +/* Writes `value` when writing it runs no program code and meets no cycle: + * JSON primitives, and objects and dense arrays with the default prototype + * whose members are such values, where objects hold only data properties + * under names that are neither array indexes nor "toJSON" (so their own + * key order is their insertion order and no toJSON step applies). Answers + * false, having written part of the value, for anything else, including + * nesting deeper than SCR_JSON_PURE_DEPTH; the caller then truncates the + * buffer and runs the protocol walk. */ +static bool scr_jb_put_pure(ScrJsonBuf *b, const ScrDyn *value, size_t depth) { + switch (value->kind) { + case SCR_DYN_NULL: scr_jb_puts(b, "null"); return true; + case SCR_DYN_BOOL: scr_jb_puts(b, value->v.b ? "true" : "false"); return true; + case SCR_DYN_NUM: scr_jb_put_f64(b, value->v.num); return true; + case SCR_DYN_STR: scr_jb_put_json_str(b, value->v.str); return true; + case SCR_DYN_ARR: + if (value->prototype || value->v.arr.presence || value->v.arr.properties || + depth == SCR_JSON_PURE_DEPTH) + return false; + scr_jb_putc(b, '['); + for (size_t i = 0; i < value->v.arr.len; i++) { + if (i > 0) scr_jb_putc(b, ','); + const ScrDyn *item = value->v.arr.items[i]; + if (scr_json_omitted(item)) scr_jb_puts(b, "null"); + else if (!scr_jb_put_pure(b, item, depth + 1)) return false; + } + scr_jb_putc(b, ']'); + return true; + case SCR_DYN_OBJ: { + if (value->prototype || depth == SCR_JSON_PURE_DEPTH) return false; + scr_jb_putc(b, '{'); + bool first = true; + for (size_t i = 0; i < value->v.obj.len; i++) { + const ScrDynEntry *entry = &value->v.obj.entries[i]; + if (!entry->enumerable) continue; + double index; + if (entry->accessor || (entry->key_len == 6 && memcmp(entry->key, "toJSON", 6) == 0) || + (entry->key_len && (unsigned char)(entry->key[0] - '0') < 10 && + scr_dyn_key_is_index(entry->key, entry->key_len, &index))) + return false; + if (scr_json_omitted(entry->value)) continue; + if (!first) scr_jb_putc(b, ','); + first = false; + scr_jb_put_json_span(b, entry->key, entry->key_len); + scr_jb_putc(b, ':'); + if (!scr_jb_put_pure(b, entry->value, depth + 1)) return false; + } + scr_jb_putc(b, '}'); + return true; + } + default: + return false; + } +} + +/* Writes the separator, key and value of an object member whose value + * scr_jb_put_pure can write, or drops a member holding undefined, a + * function or a symbol. `key_str`, when not NULL, is the key as a string. + * Answers false, with the buffer and `first` as they were, for a value + * that needs the protocol walk. */ +static inline __attribute__((always_inline)) bool scr_jb_try_pure_member( + ScrJsonBuf *b, const char *key, size_t key_len, const ScrStr *key_str, const ScrDyn *value, + bool *first) { + if (scr_json_omitted(value)) return true; + size_t mark = b->len; + if (!*first) scr_jb_putc(b, ','); + if (key_str) scr_jb_put_json_str(b, key_str); + else scr_jb_put_json_span(b, key, key_len); + scr_jb_putc(b, ':'); + if (scr_jb_put_pure(b, value, 0)) { + *first = false; + return true; + } + b->len = mark; + return false; +} + +/* Writes one object member of a typed container: `value` (consumed) goes + * through its toJSON step, an undefined, function or symbol result drops + * the member, and any other result is written after the separator and the + * key through the protocol walk, whose circular-structure checks share the + * buffer's stack. The container is the top of that stack, or `rec` is + * pushed for the walk when it is not; the key becomes the container's + * edge. Answers false with an exception pending. */ +static bool scr_jb_put_member_walk(ScrJsonBuf *b, const ScrStr *key, ScrDyn *value, + bool *first, void *rec) { + ScrDyn *child = scr_json_to_json(value, key); + if (!child) return false; + if (scr_json_omitted(child)) { + scr_dyn_release(child); + return true; + } + bool entered = rec && !(b->seen_len && b->seen[b->seen_len - 1].ptr == rec); + if (entered && !scr_jb_enter(b, rec, false)) { + scr_dyn_release(child); + return false; + } + if (!*first) scr_jb_putc(b, ','); + *first = false; + scr_jb_put_json_str(b, key); + scr_jb_putc(b, ':'); + scr_jb_edge_key(b, key); + bool ok = scr_json_replaced_write(b, child, scr_dyn_undefined(), NULL, &scr_json_compact_gap, + b->seen_len); + if (entered) scr_jb_leave(b); + scr_dyn_release(child); + return ok; +} + +/* scr_jb_put_member_walk, writing the member directly when scr_jb_put_pure + * can write its value. */ +static bool scr_jb_put_member(ScrJsonBuf *b, const ScrStr *key, ScrDyn *value, bool *first, + void *rec) { + if (scr_jb_try_pure_member(b, key->data, key->len, key, value, first)) { + scr_dyn_release(value); + return true; + } + return scr_jb_put_member_walk(b, key, value, first, rec); +} + +/* An entry of a record's `unknown` index signature, written after the + * record's earlier members with JSON.stringify's member rules + * (scr_jb_put_member). Walkers over cycle-capable shapes have pushed the + * record already; others push it for an entry that needs the protocol + * walk. May leave an exception pending. */ +static void scr_jb_put_dyn_entry(ScrJsonBuf *b, void *rec, const ScrStr *key, + const ScrDyn *value, bool *first) { + if (scr_jb_try_pure_member(b, key->data, key->len, key, value, first)) return; + scr_jb_put_member_walk(b, key, scr_dyn_retain((ScrDyn *)value), first, rec); +} + +/* The entries of record `rec`'s `unknown` index signature (`map`: string + * keys, checked-dynamic values) in JS own-key order, each with + * scr_jb_put_dyn_entry's rules. Without array-index keys that order is the + * map's insertion order, walked in place: the walk holds the map's + * iteration count so no compaction moves entries, rereads each entry by + * position, and stops at the entries present when it started, so a toJSON + * call that adds or deletes entries sees Node's result (keys taken first, + * deleted ones read as undefined). With array-index keys the walk takes the + * ordered key list first. May leave an exception pending. */ +void scr_jb_put_dyn_entries(ScrJsonBuf *b, void *rec, ScrMap *map, bool *first) { + uint32_t count = map->nentries; + bool indexed = false; + for (uint32_t i = 0; i < count && !indexed; i++) { + if (!map->entries[i].hash) continue; + const ScrStr *key = (const ScrStr *)(uintptr_t)map->entries[i].key; + uint32_t index; + indexed = scr_key_array_index(key->data, key->len, &index); + } + if (indexed) { + ScrArr *keys = scr_map_keys_js_order(map); + for (size_t i = 0; i < keys->len && !scr_exc_pending(); i++) { + ScrStr *key = scr_arr_get_ref(keys, (double)i); + ScrDyn *value = scr_map_get_str_ref(map, key); + if (value) { + scr_jb_put_dyn_entry(b, rec, key, value, first); + scr_dyn_release(value); + } + scr_str_release(key); + } + scr_arr_release(keys); + return; + } + map->iter_depth++; + for (uint32_t i = 0; i < count && !scr_exc_pending(); i++) { + const ScrMapEntry *entry = &map->entries[i]; + if (!entry->hash) continue; + const ScrStr *key = (const ScrStr *)(uintptr_t)entry->key; + const ScrDyn *value = (const ScrDyn *)(uintptr_t)entry->val; + if (scr_jb_try_pure_member(b, key->data, key->len, key, value, first)) continue; + /* The walk can run program code, which may replace this entry. */ + ScrStr *held_key = scr_str_retain((ScrStr *)key); + scr_jb_put_member_walk(b, held_key, scr_dyn_retain((ScrDyn *)value), first, rec); + scr_str_release(held_key); + } + map->iter_depth--; +} + +/* Writes a dyn value whose toJSON step has run through the protocol walk: + * undefined, functions and symbols as null (an array slot's rule), and + * containers with toJSON, accessor and circular-structure handling. May + * leave an exception pending. */ +void scr_jb_put_dyn(ScrJsonBuf *b, const ScrDyn *d) { + if (scr_json_omitted(d)) scr_jb_puts(b, "null"); + else scr_json_replaced_write(b, d, scr_dyn_undefined(), NULL, &scr_json_compact_gap, b->seen_len); +} + /* Untyped RC adapters (box/promise/exception-cell currency). */ void *scr_dyn_retain_v(void *d) { return scr_dyn_retain((ScrDyn *)d); } void scr_dyn_release_v(void *d) { scr_dyn_release((ScrDyn *)d); } @@ -9356,6 +9763,61 @@ void scr_sc_validate_options(const ScrDyn *options) { scr_dyn_release(transferView); } +/* ── markAsUncloneable ─────────────────────────────────────────────── */ + +/* The plain objects, errors and class instances marked with + * markAsUncloneable (node:worker_threads): structuredClone and + * postMessage throw DataCloneError for them. Node's mark has no effect on + * primitives, arrays, Maps, Sets, Dates, typed arrays and ArrayBuffers. + * The marked values are retained until the thread exits, so an identity + * is never reused while marked. */ +static SCR_TL ScrDyn **scr_uncloneable; +SCR_TL size_t scr_uncloneable_count = 0; +static SCR_TL size_t scr_uncloneable_capacity; + +static void scr_uncloneable_cleanup(void) { + for (size_t i = 0; i < scr_uncloneable_count; i++) scr_dyn_release(scr_uncloneable[i]); + free(scr_uncloneable); + scr_uncloneable = NULL; + scr_uncloneable_count = scr_uncloneable_capacity = 0; +} + +/* The identity a mark is kept under: a class instance's or a record's + * object, or a plain object's source (its own box otherwise); NULL for + * values Node does not mark. */ +static const void *scr_uncloneable_identity(const ScrDyn *value) { + if (value->kind == SCR_DYN_TYPED_REF && + ((value->v.typed_ref.type_key_len > 7 && !memcmp(value->v.typed_ref.type_key, "object:", 7)) || + (value->v.typed_ref.type_key_len > 7 && !memcmp(value->v.typed_ref.type_key, "record:", 7)))) + return value->v.typed_ref.ptr; + if (value->kind == SCR_DYN_OBJ) + return value->v.obj.source_identity ? value->v.obj.source_identity : (const void *)value; + return NULL; +} + +void scr_mark_uncloneable(ScrDyn *value) { + const void *identity = scr_uncloneable_identity(value); + if (!identity || scr_uncloneable_marked(value)) return; + if (scr_uncloneable_count == scr_uncloneable_capacity) { + if (!scr_uncloneable_capacity) scr_atexit(scr_uncloneable_cleanup); + size_t capacity = scr_uncloneable_capacity ? scr_uncloneable_capacity * 2 : 8; + ScrDyn **values = realloc(scr_uncloneable, capacity * sizeof *values); + if (!values) scr_trap("scriptc: out of memory\n"); + scr_uncloneable = values; + scr_uncloneable_capacity = capacity; + } + scr_uncloneable[scr_uncloneable_count++] = scr_dyn_retain(value); +} + +bool scr_uncloneable_marked(const ScrDyn *value) { + if (scr_uncloneable_count == 0) return false; + const void *identity = scr_uncloneable_identity(value); + if (!identity) return false; + for (size_t i = 0; i < scr_uncloneable_count; i++) + if (scr_uncloneable_identity(scr_uncloneable[i]) == identity) return true; + return false; +} + /* The parent chain rides the C stack: a revisit is a cycle. */ typedef struct ScrScParent { const ScrDyn *node; @@ -9366,6 +9828,10 @@ typedef struct ScrScParent { * throw), so it is tagged like a parse result (ScrDynRegion); a leaf the * region may not hold clears it. */ static ScrDyn *scr_sc_clone(const ScrDyn *v, const ScrScParent *up, ScrDynRegion *region) { + if (scr_uncloneable_count && scr_uncloneable_marked(v)) { + scr_throw_domex("DataCloneError", "Cannot clone object of unsupported type."); + return NULL; + } if (v->kind == SCR_DYN_PROXY) { scr_throw_domex("DataCloneError", "# could not be cloned."); return NULL; @@ -10425,6 +10891,7 @@ static ScrArr *scr_record_shared_keys(const ScrDynTypedRecordOps *record) { #ifdef SCR_RC_AUDIT scr_arr_live_forget(); /* immortal: outside the exit-time RC audit */ #endif + scr_sa_thread_shares(); /* every thread reads the array */ return built; } built->rc = 1; @@ -12245,6 +12712,7 @@ SCR_ARRAY_BUFFER_VIEW(i16, SCR_BYTES_I16) SCR_ARRAY_BUFFER_VIEW(u8, SCR_BYTES_U8) SCR_ARRAY_BUFFER_VIEW(u32, SCR_BYTES_U32) SCR_ARRAY_BUFFER_VIEW(i32, SCR_BYTES_I32) +SCR_ARRAY_BUFFER_VIEW(f16, SCR_BYTES_F16) SCR_ARRAY_BUFFER_VIEW(f32, SCR_BYTES_F32) SCR_ARRAY_BUFFER_VIEW(f64, SCR_BYTES_F64) #undef SCR_ARRAY_BUFFER_VIEW diff --git a/packages/runtime/src/scr_json.h b/packages/runtime/src/scr_json.h index 03c9fb7cf9..8a353f40a3 100644 --- a/packages/runtime/src/scr_json.h +++ b/packages/runtime/src/scr_json.h @@ -162,9 +162,16 @@ void scr_jb_put_number(ScrJsonBuf *b, double v); void scr_jb_put_json_str(ScrJsonBuf *b, const ScrStr *s); /* JSON-serialize a dyn value into the buffer — the sc_jw_* walker for the - * dyn leaves the compiler cannot type: object members holding undefined - * DROP, array slots holding undefined print null (exactly Node). */ + * dyn leaves the compiler cannot type: undefined prints null (an array + * slot's rule); containers follow JSON.stringify (toJSON, accessors, + * circular structures). May leave an exception pending. */ void scr_jb_put_dyn(ScrJsonBuf *b, const ScrDyn *d); +/* The entries of record `rec`'s `unknown` index signature (its overflow + * map) after the record's earlier members: separators, keys and values in + * JS own-key order with JSON.stringify's member rules (toJSON called with + * the key, undefined, function and symbol results dropped). `first` is the + * record's first-member flag. May leave an exception pending. */ +void scr_jb_put_dyn_entries(ScrJsonBuf *b, void *rec, ScrMap *map, bool *first); /* Typed JSON for a record that kept a source: the kept key order, declared * fields through the compiled per-field writer. `try` writes nothing and * answers false for a record without one. */ diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index 5f595f2632..9307d9b010 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -166,6 +166,13 @@ static SCR_TL char **scr_lib_argv_owned = NULL; static SCR_TL char *scr_lib_argv_utf8 = NULL; #endif static SCR_TL bool scr_lib_has_fork = false; +/* A leading Node runtime flag taken out of process.argv (scr_lib_take_flag), + * which process.execArgv reports; NULL without one. */ +static SCR_TL const char *scr_lib_exec_flag = NULL; +#if defined(_WIN32) && !defined(SCR_LIB) +/* The flag's index in the original Windows command line, -1 without one. */ +static SCR_TL int scr_lib_exec_flag_index = -1; +#endif #if !defined(_WIN32) && !defined(__wasi__) && !defined(SCR_LIB) static void scr_title_init(int argc, char **argv); #endif @@ -321,6 +328,7 @@ static void scr_lib_prepare_utf8_argv(void) { // scr_lib_init already consumed this private ASCII fork marker. Do not // reintroduce it when replacing the narrow argument storage. if (i == 1 && scr_lib_has_fork) continue; + if (i == scr_lib_exec_flag_index) continue; int size = WideCharToMultiByte(CP_UTF8, 0, wide[i], -1, NULL, 0, NULL, NULL); if (size == 0 || WideCharToMultiByte(CP_UTF8, 0, wide[i], -1, next, size, NULL, NULL) != size) { free(text); free(args); LocalFree(wide); @@ -565,6 +573,31 @@ const char *scr_lib_arg(int i) { return scr_lib_argv[i]; } +bool scr_lib_take_flag(const char *flag) { + if (scr_lib_argc < 2 || strcmp(scr_lib_argv[1], flag) != 0) return false; + char **args = malloc((size_t)scr_lib_argc * sizeof(char *)); + if (!args) scr_trap("out of memory"); + args[0] = scr_lib_argv[0]; + for (int i = 2; i < scr_lib_argc; i++) args[i - 1] = scr_lib_argv[i]; + args[scr_lib_argc - 1] = NULL; + free(scr_lib_argv_owned); + scr_lib_argv_owned = args; + scr_lib_argv = args; + scr_lib_argc--; +#if defined(_WIN32) && !defined(SCR_LIB) + scr_lib_exec_flag_index = scr_lib_has_fork ? 2 : 1; +#endif + scr_lib_exec_flag = flag; + return true; +} + +ScrArr *scr_process_exec_argv(void) { + ScrArr *arr = scr_arr_new(SCR_ELEM_STR, 1); + if (scr_lib_exec_flag) + scr_arr_push_ref(arr, scr_str_new(scr_lib_exec_flag, strlen(scr_lib_exec_flag))); + return arr; +} + bool scr_lib_fork_info(double *target, uintptr_t *read_handle, uintptr_t *write_handle) { if (!scr_lib_has_fork) return false; @@ -1945,14 +1978,17 @@ SCR_TL void (*scr_warning_emit_hook)(const char *name, const char *code, ScrStr SCR_TL bool scr_warning_trace_hint_shown = false; SCR_TL void (*scr_stdin_destroy_hook)(void) = NULL; -void scr_process_set_exit_code(double code) { +/* process.exitCode's validation: a safe integer, or a RangeError + * (ERR_OUT_OF_RANGE). Node converts valid codes to signed int32 for + * exitCode and exit listeners; the OS observes the low eight bits. */ +static bool scr_process_exit_code_of(double code, int *out) { if (!isfinite(code) || trunc(code) != code) { char recv[48], msg[160]; scr_num_received(code, recv); int len = snprintf(msg, sizeof msg, "The value of \"code\" is out of range. It must be an integer. Received %s", recv); scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE"); - return; + return false; } if (fabs(code) > 9007199254740991.0) { char recv[48], msg[192]; @@ -1960,15 +1996,19 @@ void scr_process_set_exit_code(double code) { int len = snprintf(msg, sizeof msg, "The value of \"code\" is out of range. It must be >= -9007199254740991 && <= 9007199254740991. Received %s", recv); scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE"); - return; + return false; } - /* Node converts valid safe integers to signed int32 for exitCode and - * exit listeners. The OS subsequently observes the low eight bits. */ double low = fmod(code, 4294967296.0); if (low < 0) low += 4294967296.0; int64_t signed_code = (int64_t)low; if (signed_code > INT_MAX) signed_code -= 4294967296LL; - scr_exit_code_note((int)signed_code); + *out = (int)signed_code; + return true; +} + +void scr_process_set_exit_code(double code) { + int converted; + if (scr_process_exit_code_of(code, &converted)) scr_exit_code_note(converted); } double scr_process_exit_code_or_zero(void) { @@ -2026,6 +2066,11 @@ static void scr_detach_teardown(void) { #endif void scr_process_exit(double code) { + /* process.exit(code) assigns process.exitCode first, so a code that is + * not a safe integer throws its RangeError and the program continues. */ + int converted; + if (!scr_process_exit_code_of(code, &converted)) return; + code = converted; /* Node runs 'exit' listeners on explicit process.exit() too — they run * HERE, synchronously, before the teardown-free exit below. The hook is * non-NULL only when the events unit is linked (scr_events.c). */ @@ -10171,6 +10216,8 @@ ScrStr *scr_str_from_code_point_bytes(ScrBytes *codes) { * frontend-fenced; construction, storage, getters, and ISO formatting * observe exactly this payload. */ +SCR_TL const double *scr_date_mock_now = NULL; + double scr_date_now(void) { struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); @@ -10861,7 +10908,7 @@ static ScrDyn *scr_native_date_invoke(void *ptr, ScrDyn *self, const char *metho ScrDyn *scr_dyn_native_date_new(const ScrDyn *arguments) { double ms; - if (arguments->v.arr.len == 0) ms = scr_date_now(); + if (arguments->v.arr.len == 0) ms = scr_date_mock_now ? *scr_date_mock_now : scr_date_now(); else { ScrDyn *value = arguments->v.arr.items[0]; if (scr_dyn_native_date_is(value)) ms = scr_dyn_native_date_value(value); @@ -10869,9 +10916,69 @@ ScrDyn *scr_dyn_native_date_new(const ScrDyn *arguments) { else if (!scr_dyn_number_coerce_js(value, &ms)) return NULL; ms = scr_date_new_ms(ms); } + return scr_dyn_native_date_from_ms(ms); +} + +/* The Date.prototype methods scr_native_date_invoke answers. */ +static const char *const scr_native_date_methods[] = { + "getTime", "valueOf", "toISOString", "toJSON", "getFullYear", "getUTCFullYear", "getMonth", + "getUTCMonth", "getDate", "getUTCDate", "getDay", "getUTCDay", "getHours", "getUTCHours", + "getMinutes", "getUTCMinutes", "getSeconds", "getUTCSeconds", "getMilliseconds", + "getUTCMilliseconds", "getTimezoneOffset", +}; +#define SCR_NATIVE_DATE_METHODS (sizeof scr_native_date_methods / sizeof scr_native_date_methods[0]) +static SCR_TL ScrDyn *scr_native_date_method_values[SCR_NATIVE_DATE_METHODS]; +static SCR_TL bool scr_native_date_methods_registered; + +static void scr_native_date_methods_cleanup(void) { + for (size_t i = 0; i < SCR_NATIVE_DATE_METHODS; i++) { + ScrDyn *function = scr_native_date_method_values[i]; + scr_native_date_method_values[i] = NULL; + scr_dyn_release(function); + } +} + +/* A Date method read as a value and called later: the closure's function + * slot holds the method's name, and the receiver must be a Date. */ +static ScrDyn *scr_native_date_method_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + const char *method = closure->fn; + ScrDyn *self = scr_dyn_this_get(); + ScrDyn *result = NULL; + if (scr_dyn_native_date_is(self)) { + result = scr_native_date_invoke(self->v.handle.ptr, self, method, args, argc, method); + } else { + static const char message[] = "this is not a Date object."; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + } + scr_dyn_release(self); + return result; +} + +/* Property reads of a Date: its methods as function values, one per name + * and thread, and NULL (undefined) for other names. */ +static ScrDyn *scr_native_date_get(void *ptr, const char *key, size_t key_len) { + (void)ptr; + for (size_t i = 0; i < SCR_NATIVE_DATE_METHODS; i++) { + const char *name = scr_native_date_methods[i]; + if (strlen(name) != key_len || memcmp(name, key, key_len) != 0) continue; + if (!scr_native_date_methods_registered) { + scr_native_date_methods_registered = true; + scr_atexit(scr_native_date_methods_cleanup); + } + if (!scr_native_date_method_values[i]) { + scr_native_date_method_values[i] = scr_dyn_new_func( + scr_closure_new((void *)name, 0), scr_native_date_method_call, 0, "native:Date", name); + } + return scr_dyn_retain(scr_native_date_method_values[i]); + } + return NULL; +} + +/* A Date handle holding the time value `ms` (already TimeClip'd). */ +ScrDyn *scr_dyn_native_date_from_ms(double ms) { static const ScrDynHandleOps ops = { "Date", &scr_native_date_retain, &scr_native_date_release, &scr_native_date_invoke, - NULL, NULL, NULL, NULL, + &scr_native_date_get, NULL, NULL, NULL, }; scr_dyn_handle_install(SCR_DYNH_DATE, &ops); ScrNativeDate *date = malloc(sizeof *date); diff --git a/packages/runtime/src/scr_lib.h b/packages/runtime/src/scr_lib.h index 64f3e77a59..4c973e3e5d 100644 --- a/packages/runtime/src/scr_lib.h +++ b/packages/runtime/src/scr_lib.h @@ -46,6 +46,13 @@ ScrArr *scr_module_cache_keys(void); int scr_lib_arg_count(void); const char *scr_lib_arg(int i); +/* Takes a leading `flag` argument out of process.argv, as Node keeps its + * runtime flags out of it, and reports it in process.execArgv. Called + * right after scr_lib_init, before process.argv is first read. True when + * the flag was there. */ +bool scr_lib_take_flag(const char *flag); +/* process.execArgv: the flag scr_lib_take_flag took, or [] (+1, fresh). */ +ScrArr *scr_process_exec_argv(void); /* Private self-reexec marker parsed and removed from process.argv by * scr_lib_init. False in an ordinary parent invocation. */ bool scr_lib_fork_info(double *target, uintptr_t *read_handle, @@ -64,6 +71,23 @@ int scr_stdio_write_raw(int fd, const void *data, size_t len); /* `wide` marks string bytes that may hold lone surrogates, which become * U+FFFD when they reach the stream itself. */ extern SCR_TL bool (*scr_stdio_write_hook)(int fd, const void *data, size_t len, bool wide); +/* A worker created with the stdout or stderr option: answers true when it + * took the bytes for fd 1 or 2 instead of the process descriptor. */ +extern SCR_TL bool (*scr_stdio_capture)(int fd, const void *data, size_t len); +/* A worker created with the stdin option: process.stdin reads the chunks + * the parent writes to worker.stdin instead of fd 0. The worker's event + * dispatch queues them (scr_stdin_fed_push; NULL ends the input) and the + * stdin service takes them (+1, or NULL with *ended set once the input + * ended). scr_stdin_fed_attach (the worker unit) starts that dispatch + * when process.stdin is first used; scr_stdin_fed_waiting (the events + * unit) says whether process.stdin still wants data, which keeps the + * worker's loop waiting on its mailbox. */ +extern SCR_TL bool scr_stdin_fed; +extern SCR_TL void (*scr_stdin_fed_attach)(void); +extern SCR_TL bool (*scr_stdin_fed_waiting)(void); +void scr_stdin_fed_push(struct ScrBytes *chunk /* moves; NULL ends the input */); +struct ScrBytes *scr_stdin_fed_take(bool *ended); +void scr_stdin_fed_clear(void); /* process.stdout/.stderr .write — raw bytes (no newline or formatting; data * borrowed), promptly visible and ordered with console output. Constantly * true (the synchronous runtime never queues backpressure). */ diff --git a/packages/runtime/src/scr_library.c b/packages/runtime/src/scr_library.c index 60e11ae0c6..b2a5d4c750 100644 --- a/packages/runtime/src/scr_library.c +++ b/packages/runtime/src/scr_library.c @@ -371,6 +371,30 @@ void scr_library_bytes_out(ScrBytes *b, const uint8_t **out, size_t *out_len) { *out_len = b->len; } +ScrStr *scr_library_cstr_in(const char *p, const char *trap_msg) { + if (p == NULL) scr_trap(trap_msg); + return scr_str_from_utf8_lossy((const uint8_t *)p, strlen(p)); +} + +/* A C string ends at its first NUL: text holding U+0000 cannot cross. */ +static void scr_library_cstr_check(const ScrStr *s) { + if (memchr(s->data, 0, s->len) != NULL) { + scr_trap("scriptc: TypeError: a cstring value contains U+0000, which a C string cannot " + "hold\n"); + } +} + +const char *scr_library_cstr_out(ScrStr *s) { + scr_library_arena_keep(s, true); + scr_library_cstr_check(s); + return s->data; /* NUL-terminated after len (ScrStr layout) */ +} + +const char *scr_library_cstr_arg(const ScrStr *s) { + scr_library_cstr_check(s); + return s->data; +} + /* ── the reset registry + session reset ───────────────────────────────── * Where an executable's always-linked units atexit() their lazy teardowns, * library builds register here (scr_atexit in scr_runtime.h): registered diff --git a/packages/runtime/src/scr_mailbox.c b/packages/runtime/src/scr_mailbox.c index 89cb6ce830..f983bd289d 100644 --- a/packages/runtime/src/scr_mailbox.c +++ b/packages/runtime/src/scr_mailbox.c @@ -128,6 +128,7 @@ ScrMailbox *scr_mailbox_retain(ScrMailbox *mailbox) { void scr_mail_event_free(ScrMailEvent *event) { if (!event) return; scr_message_free(event->message); + free(event->data); free(event); } @@ -177,6 +178,24 @@ ScrMailEvent *scr_mailbox_take(ScrMailbox *mailbox) { return event; } +ScrMailEvent *scr_mailbox_take_kind(ScrMailbox *mailbox, unsigned kind, uint64_t source) { + scr_mailbox_lock(mailbox); + ScrMailEvent **slot = &mailbox->head; + while (*slot && ((*slot)->kind != kind || (*slot)->source != source)) slot = &(*slot)->next; + ScrMailEvent *event = *slot; + if (event) { + *slot = event->next; + if (mailbox->tail == &event->next) mailbox->tail = slot; + event->next = NULL; + } + if (!mailbox->head) { + mailbox->tail = &mailbox->head; + scr_mailbox_drain(mailbox); + } + scr_mailbox_unlock(mailbox); + return event; +} + void scr_mailbox_close(ScrMailbox *mailbox) { scr_mailbox_lock(mailbox); mailbox->closed = true; @@ -420,3 +439,80 @@ void scr_channel_drop(ScrChannel *channel, int side) { scr_mail_event_free(event); } } + +/* ── BroadcastChannel registry ───────────────────────────────────────── */ + +/* Every open BroadcastChannel in the process, by name: each has a channel + * whose end 0 queues its incoming messages. */ +typedef struct ScrBroadcastEntry { + char *name; + size_t length; + ScrChannel *channel; /* +1 */ + struct ScrBroadcastEntry *next; +} ScrBroadcastEntry; + +static ScrBroadcastEntry *scr_broadcast_entries; +#ifdef _WIN32 +static SRWLOCK scr_broadcast_lock = SRWLOCK_INIT; +#define SCR_BROADCAST_LOCK() AcquireSRWLockExclusive(&scr_broadcast_lock) +#define SCR_BROADCAST_UNLOCK() ReleaseSRWLockExclusive(&scr_broadcast_lock) +#else +static pthread_mutex_t scr_broadcast_lock = PTHREAD_MUTEX_INITIALIZER; +#define SCR_BROADCAST_LOCK() (void)pthread_mutex_lock(&scr_broadcast_lock) +#define SCR_BROADCAST_UNLOCK() (void)pthread_mutex_unlock(&scr_broadcast_lock) +#endif + +void scr_broadcast_join(const char *name, size_t length, ScrChannel *channel) { + ScrBroadcastEntry *entry = calloc(1, sizeof(*entry)); + if (!entry) scr_trap("scriptc: out of memory\n"); + entry->name = malloc(length ? length : 1); + if (!entry->name) scr_trap("scriptc: out of memory\n"); + memcpy(entry->name, name, length); + entry->length = length; + entry->channel = scr_channel_retain(channel); + SCR_BROADCAST_LOCK(); + /* registration order is delivery order to the subscribers */ + ScrBroadcastEntry **tail = &scr_broadcast_entries; + while (*tail) tail = &(*tail)->next; + *tail = entry; + SCR_BROADCAST_UNLOCK(); +} + +void scr_broadcast_leave(ScrChannel *channel) { + ScrBroadcastEntry *gone = NULL; + SCR_BROADCAST_LOCK(); + for (ScrBroadcastEntry **at = &scr_broadcast_entries; *at; at = &(*at)->next) { + if ((*at)->channel != channel) continue; + gone = *at; + *at = gone->next; + break; + } + SCR_BROADCAST_UNLOCK(); + if (!gone) return; + scr_channel_release(gone->channel); + free(gone->name); + free(gone); +} + +/* The other open channels named `name` (+1 each, in registration order); + * the caller frees the array. */ +size_t scr_broadcast_peers(const char *name, size_t length, ScrChannel *self, ScrChannel ***out) { + size_t n = 0, cap = 0; + ScrChannel **peers = NULL; + SCR_BROADCAST_LOCK(); + for (ScrBroadcastEntry *entry = scr_broadcast_entries; entry; entry = entry->next) { + if (entry->channel == self || entry->length != length || + (length && memcmp(entry->name, name, length) != 0)) + continue; + if (n == cap) { + cap = cap ? cap * 2 : 4; + ScrChannel **grown = realloc(peers, cap * sizeof *grown); + if (!grown) scr_trap("scriptc: out of memory\n"); + peers = grown; + } + peers[n++] = scr_channel_retain(entry->channel); + } + SCR_BROADCAST_UNLOCK(); + *out = peers; + return n; +} diff --git a/packages/runtime/src/scr_mailbox.h b/packages/runtime/src/scr_mailbox.h index 75d99bbc86..678d17723f 100644 --- a/packages/runtime/src/scr_mailbox.h +++ b/packages/runtime/src/scr_mailbox.h @@ -9,6 +9,8 @@ typedef struct ScrMailEvent { uint64_t source; int code; ScrMessage *message; + unsigned char *data; /* raw bytes (a worker's captured stdout or stderr), owned */ + size_t length; struct ScrMailEvent *next; } ScrMailEvent; @@ -19,6 +21,8 @@ void scr_mailbox_release(ScrMailbox *mailbox); * closed. Taking returns an owned event; no runtime object crosses a queue. */ bool scr_mailbox_post(ScrMailbox *mailbox, ScrMailEvent *event); ScrMailEvent *scr_mailbox_take(ScrMailbox *mailbox); +/* The first queued event with this kind and source, or NULL. */ +ScrMailEvent *scr_mailbox_take_kind(ScrMailbox *mailbox, unsigned kind, uint64_t source); void scr_mail_event_free(ScrMailEvent *event); void scr_mailbox_close(ScrMailbox *mailbox); bool scr_mailbox_pending(ScrMailbox *mailbox); @@ -46,4 +50,12 @@ size_t scr_channel_count(ScrChannel *channel, int side); uint64_t scr_channel_peer_closed(ScrChannel *channel, int side); /* the notice's order, 0: none */ void scr_channel_drop(ScrChannel *channel, int side); +/* BroadcastChannel: the process-wide registry of open channels by name. + * Each instance's channel queues its messages on end 0 (posts go through + * side 1). peers answers the others with the name (+1 each, array + * malloc'd). */ +void scr_broadcast_join(const char *name, size_t length, ScrChannel *channel); +void scr_broadcast_leave(ScrChannel *channel); +size_t scr_broadcast_peers(const char *name, size_t length, ScrChannel *self, ScrChannel ***out); + #endif diff --git a/packages/runtime/src/scr_message.c b/packages/runtime/src/scr_message.c index a0b3b8d4e3..82428a6fea 100644 --- a/packages/runtime/src/scr_message.c +++ b/packages/runtime/src/scr_message.c @@ -31,6 +31,7 @@ typedef struct { char *key; size_t length; size_t value; + bool hidden; /* an Error's non-enumerable or inherited property */ } ScrMessageProperty; typedef struct { @@ -73,6 +74,7 @@ typedef struct { ScrDyn *view; ScrDyn *keys; size_t cursor; + bool uncaught; /* an Error encoded as an uncaught exception */ } ScrMessageVisit; typedef struct { @@ -91,6 +93,7 @@ typedef struct { size_t depth; size_t stack_capacity; const ScrDyn *transfer; /* the transfer list (a dyn array), or NULL */ + bool uncaught; /* the next value is an uncaught exception or its cause */ } ScrMessageEncoder; static bool scr_message_listed(const ScrMessageEncoder *encoder, const ScrDyn *value) { @@ -211,7 +214,44 @@ static size_t scr_message_storage(ScrMessageEncoder *encoder, const ScrBytes *by return node; } +/* The nearest built-in constructor an uncaught Error keeps: Node's + * serializer knows Error and its six native subclasses. */ +static int scr_message_error_kind(const ScrError *error) { + static const int kinds[] = { + SCR_ERR_TYPE, SCR_ERR_RANGE, SCR_ERR_SYNTAX, SCR_ERR_REFERENCE, SCR_ERR_EVAL, SCR_ERR_URI, + }; + for (size_t i = 0; i < sizeof kinds / sizeof kinds[0]; i++) + if (scr_error_vts[kinds[i]].pre <= error->vt->pre && error->vt->pre <= scr_error_vts[kinds[i]].post) + return kinds[i]; + return SCR_ERR_ERROR; +} + +/* An uncaught Error's keys: name and message, which it may inherit, then + * its other own properties in order. */ +static ScrDyn *scr_message_uncaught_keys(const ScrDyn *view) { + ScrDyn *own = scr_dyn_get_own_property_names(view); + if (!own) return NULL; + ScrDyn *keys = scr_dyn_new_arr(); + static const char *const first[] = {"name", "message"}; + for (size_t i = 0; i < 2; i++) { + ScrStr *key = scr_str_new(first[i], strlen(first[i])); + scr_dyn_arr_push(keys, scr_dyn_new_str(key)); + scr_str_release(key); + } + for (size_t i = 0; i < own->v.arr.len; i++) { + const ScrStr *key = own->v.arr.items[i]->v.str; + if ((key->len == 4 && !memcmp(key->data, "name", 4)) || (key->len == 7 && !memcmp(key->data, "message", 7)) || + (key->len && key->data[0] == '%')) + continue; + scr_dyn_arr_push(keys, scr_dyn_retain(own->v.arr.items[i])); + } + scr_dyn_release(own); + return keys; +} + static size_t scr_message_value(ScrMessageEncoder *encoder, const ScrDyn *input) { + bool uncaught = encoder->uncaught; + encoder->uncaught = false; const void *identity = NULL; unsigned domain = 0; switch (input->kind) { @@ -225,6 +265,10 @@ static size_t scr_message_value(ScrMessageEncoder *encoder, const ScrDyn *input) case SCR_DYN_HANDLE: identity = input->v.handle.ptr; domain = 4; break; default: break; } + if (scr_uncloneable_count && scr_uncloneable_marked(input)) { + scr_throw_domex("DataCloneError", "Cannot clone object of unsupported type."); + return SIZE_MAX; + } size_t found = scr_message_find(encoder, identity, domain); if (found != SIZE_MAX) return found; size_t id = scr_message_node(encoder); @@ -234,6 +278,7 @@ static size_t scr_message_value(ScrMessageEncoder *encoder, const ScrDyn *input) * capsule over the same object, a dyn graph as itself. */ if (input->kind == SCR_DYN_TYPED_REF && input->v.typed_ref.ptr && *(const size_t *)input->v.typed_ref.ptr == SIZE_MAX) { + scr_sa_thread_shares(); encoder->visits[id].view = (ScrDyn *)input; ScrMessageNode *node = &encoder->message->nodes[id]; node->kind = SCR_MESSAGE_TYPED_REF; @@ -251,6 +296,7 @@ static size_t scr_message_value(ScrMessageEncoder *encoder, const ScrDyn *input) return id; } if ((input->kind == SCR_DYN_OBJ || input->kind == SCR_DYN_ARR) && input->rc == SIZE_MAX) { + scr_sa_thread_shares(); encoder->visits[id].view = (ScrDyn *)input; ScrMessageNode *node = &encoder->message->nodes[id]; node->kind = SCR_MESSAGE_PUBLISHED; @@ -264,6 +310,16 @@ static size_t scr_message_value(ScrMessageEncoder *encoder, const ScrDyn *input) ScrMessageNode *node = &encoder->message->nodes[id]; ScrError *error = scr_errdyn_err_of(input); if (!error && value != input) error = scr_errdyn_err_of(value); + if (error && uncaught && error->vt != &scr_error_vts[SCR_ERR_DOMEX]) { + node->kind = SCR_MESSAGE_ERROR; + node->number = scr_message_error_kind(error); + scr_error_release(error); + encoder->visits[id].uncaught = true; + encoder->visits[id].keys = scr_message_uncaught_keys(value); + if (!encoder->visits[id].keys) return SIZE_MAX; + scr_message_push(encoder, id); + return id; + } if (error) { node->kind = SCR_MESSAGE_ERROR; node->number = SCR_ERR_ERROR; @@ -355,7 +411,7 @@ static size_t scr_message_value(ScrMessageEncoder *encoder, const ScrDyn *input) } static void scr_message_property(ScrMessageEncoder *encoder, size_t id, - const ScrStr *key, size_t value) { + const ScrStr *key, size_t value, bool hidden) { ScrMessageNode *node = &encoder->message->nodes[id]; if (node->count == node->capacity) { size_t capacity = scr_message_grow(node->capacity); @@ -366,7 +422,7 @@ static void scr_message_property(ScrMessageEncoder *encoder, size_t id, node->capacity = capacity; } node->properties[node->count++] = (ScrMessageProperty){ - (char *)scr_message_copy(key->data, key->len), key->len, value, + (char *)scr_message_copy(key->data, key->len), key->len, value, hidden, }; } @@ -504,6 +560,7 @@ static ScrMessage *scr_message_try_publish(const ScrDyn *value) { scr_lsan_immortal(d); } } + scr_sa_thread_shares(); message = scr_message_alloc(1, sizeof(*message)); message->nodes = scr_message_alloc(1, sizeof(*message->nodes)); message->count = message->capacity = 1; @@ -521,9 +578,15 @@ static ScrMessage *scr_message_try_publish(const ScrDyn *value) { } static ScrMessage *scr_message_encode_with(const ScrDyn *value, const ScrDyn *transfer); +static ScrMessage *scr_message_encode_run(ScrMessageEncoder encoder, const ScrDyn *value); ScrMessage *scr_message_encode(const ScrDyn *value) { return scr_message_encode_with(value, NULL); } +ScrMessage *scr_message_encode_uncaught(const ScrDyn *value) { + ScrMessage *message = scr_message_alloc(1, sizeof(*message)); + return scr_message_encode_run((ScrMessageEncoder){.message = message, .uncaught = true}, value); +} + ScrMessage *scr_message_encode_transfer(const ScrDyn *value, const ScrDyn *transfer) { if (!transfer || transfer->kind != SCR_DYN_ARR || transfer->v.arr.len == 0) return scr_message_encode_with(value, NULL); @@ -552,21 +615,33 @@ static ScrMessage *scr_message_encode_with(const ScrDyn *value, const ScrDyn *tr ScrMessage *published = transfer ? NULL : scr_message_try_publish(value); if (published) return published; ScrMessage *message = scr_message_alloc(1, sizeof(*message)); - ScrMessageEncoder encoder = {.message = message, .transfer = transfer}; + return scr_message_encode_run((ScrMessageEncoder){.message = message, .transfer = transfer}, value); +} + +static ScrMessage *scr_message_encode_run(ScrMessageEncoder encoder, const ScrDyn *value) { + ScrMessage *message = encoder.message; if (scr_message_value(&encoder, value) == SIZE_MAX) goto done; while (encoder.depth && !scr_exc_pending()) { size_t id = encoder.stack[encoder.depth - 1]; ScrMessageVisit *visit = &encoder.visits[id]; if (visit->cursor == visit->keys->v.arr.len) { encoder.depth--; continue; } ScrDyn *key = visit->keys->v.arr.items[visit->cursor++]; - if (key->kind != SCR_DYN_STR || - (message->nodes[id].kind != SCR_MESSAGE_ERROR && !scr_dyn_property_is_enumerable(visit->view, key->v.str))) continue; + bool error = message->nodes[id].kind == SCR_MESSAGE_ERROR; + if (key->kind != SCR_DYN_STR || (!error && !scr_dyn_property_is_enumerable(visit->view, key->v.str))) continue; ScrDyn *child = scr_dyn_bag_get(visit->view, key->v.str, visit->view); if (!child || scr_exc_pending()) { scr_dyn_release(child); break; } + bool hidden = error; + if (visit->uncaught) { + /* Node keeps an uncaught Error's data properties except functions + * and symbols, and serializes its cause the same way. */ + if (child->kind == SCR_DYN_FUNC || child->kind == SCR_DYN_SYMBOL) { scr_dyn_release(child); continue; } + hidden = !scr_dyn_has_own(visit->view, key->v.str) || !scr_dyn_property_is_enumerable(visit->view, key->v.str); + encoder.uncaught = key->v.str->len == 5 && !memcmp(key->v.str->data, "cause", 5); + } size_t next = scr_message_value(&encoder, child); scr_dyn_release(child); if (next == SIZE_MAX) break; - scr_message_property(&encoder, id, key->v.str, next); + scr_message_property(&encoder, id, key->v.str, next, hidden); } done: for (size_t i = 0; i < message->count; i++) { @@ -710,7 +785,15 @@ ScrDyn *scr_message_decode(const ScrMessage *message) { } else scr_dyn_obj_set(values[i], property->key, property->length, value); } if (node->kind == SCR_MESSAGE_ERROR) { - for (size_t j = 0; j < values[i]->v.obj.len; j++) values[i]->v.obj.entries[j].enumerable = false; + for (size_t j = 0; j < values[i]->v.obj.len; j++) { + ScrDynEntry *entry = &values[i]->v.obj.entries[j]; + entry->enumerable = false; + for (size_t k = 0; k < node->count; k++) { + const ScrMessageProperty *property = &node->properties[k]; + if (!property->hidden && property->length == entry->key_len && !memcmp(property->key, entry->key, entry->key_len)) + entry->enumerable = true; + } + } ScrError *error = scr_errdyn_err_of(values[i]); scr_error_commit_dyn(error, values[i]); scr_error_release(error); diff --git a/packages/runtime/src/scr_message.h b/packages/runtime/src/scr_message.h index ba0a7391cf..ffd155fe69 100644 --- a/packages/runtime/src/scr_message.h +++ b/packages/runtime/src/scr_message.h @@ -12,6 +12,12 @@ ScrMessage *scr_message_encode(const ScrDyn *value); * message (a port in the value that is not listed throws DataCloneError); * transfer is NULL or a dyn array. */ ScrMessage *scr_message_encode_transfer(const ScrDyn *value, const ScrDyn *transfer); +/* A worker's uncaught exception, as Node's error serializer sends it: an + * Error keeps the nearest of Error and its six native subclasses, and its + * name, message and other data properties (functions and symbols aside) + * with their enumerability, own or inherited; its cause encodes the same + * way. Other values encode as scr_message_encode encodes them. */ +ScrMessage *scr_message_encode_uncaught(const ScrDyn *value); /* MessagePort transfer, installed by the worker events unit: channel_of * answers a channel port's end (+1 channel; NULL for other values, side diff --git a/packages/runtime/src/scr_number.h b/packages/runtime/src/scr_number.h index 50b2705874..84db270754 100644 --- a/packages/runtime/src/scr_number.h +++ b/packages/runtime/src/scr_number.h @@ -91,6 +91,10 @@ ScrStr *scr_intl_num_format_en_us(double x); * mutation are frontend-fenced; construction, storage, getters, and ISO * formatting are exact over this representation. */ double scr_date_now(void); /* integer ms since epoch, like Node */ +/* The clock of the MockTimers that mocks Date (scr_test.c), NULL while + * Date is not mocked. Dynamic `new Date()` reads it; the runtime's own + * time stamps keep scr_date_now, as Node's internals keep the real Date. */ +extern SCR_TL const double *scr_date_mock_now; double scr_date_new_ms(double ms); /* TimeClip (NaN when invalid) */ double scr_date_get_time(double ms); /* Node's exact ISO 8601 UTC format (expanded ±YYYYYY years outside diff --git a/packages/runtime/src/scr_object.c b/packages/runtime/src/scr_object.c index a533a409ce..7cafbeca3d 100644 --- a/packages/runtime/src/scr_object.c +++ b/packages/runtime/src/scr_object.c @@ -2,6 +2,11 @@ * one runtime layout, including captured bindings for local classes. */ #include "scr_runtime.h" +/* The lifetime-region fiber hooks (scr_region.h) live in this always-linked + * unit: scr_async.c reads them, and some runtime test links take + * scr_async.c without scr_region.c. scr_region_create installs them. */ +_Atomic(const ScrRgnFiberHooks *) scr_region_fiber_hooks = NULL; + void scr_classobj_trace_v(void *object, ScrTraceVisit visit, void *ctx) { ScrClassObj *c = object; for (size_t i = 0; i < c->ncaps; i++) visit(c->caps[i], ctx); diff --git a/packages/runtime/src/scr_os.c b/packages/runtime/src/scr_os.c index 0bc50fb3fb..76f69588e9 100644 --- a/packages/runtime/src/scr_os.c +++ b/packages/runtime/src/scr_os.c @@ -296,23 +296,27 @@ static void scr_os_system_error(const char *syscall, int uv_code) { if (cell->kind == SCR_EXC_OBJ) scr_error_set_code((ScrError *)cell->payload, "ERR_SYSTEM_ERROR"); } -static bool scr_os_validate_int32(const char *name, double value) { +static bool scr_os_validate_int32_in(const char *name, double value, int min, int max) { if (isfinite(value) && trunc(value) == value) { - if (value >= -2147483648.0 && value <= 2147483647.0) return true; + if (value >= (double)min && value <= (double)max) return true; } char received[48]; scr_num_received(value, received); char text[192]; int n = isfinite(value) && trunc(value) == value ? snprintf(text, sizeof text, - "The value of \"%s\" is out of range. It must be >= -2147483648 && <= 2147483647. Received %s", - name, received) + "The value of \"%s\" is out of range. It must be >= %d && <= %d. Received %s", + name, min, max, received) : snprintf(text, sizeof text, "The value of \"%s\" is out of range. It must be an integer. Received %s", name, received); scr_throw_error_msg_code(SCR_ERR_RANGE, text, n < 0 ? 0 : (size_t)n, "ERR_OUT_OF_RANGE"); return false; } +static bool scr_os_validate_int32(const char *name, double value) { + return scr_os_validate_int32_in(name, value, INT32_MIN, INT32_MAX); +} + double scr_os_get_priority(double pid) { if (!scr_os_validate_int32("pid", pid)) return 0; #ifdef _WIN32 @@ -351,6 +355,48 @@ double scr_os_get_priority(double pid) { #endif } +/* ── setPriority ────────────────────────────────────────────────────── + * validateInt32(pid, 'pid') and validateInt32(priority, 'priority', -20, + * 19) (a one-argument call passes pid 0), then uv_os_setpriority; a + * failure is Node's ERR_SYSTEM_ERROR. */ + +void scr_os_set_priority(double pid, double priority) { + if (!scr_os_validate_int32("pid", pid)) return; + if (!scr_os_validate_int32_in("priority", priority, SCR_OS_PRIORITY_HIGHEST, SCR_OS_PRIORITY_LOW)) return; + int value = (int)priority; +#ifdef _WIN32 + /* libuv's mapping of nice values to Windows priority classes. */ + DWORD klass = value < SCR_OS_PRIORITY_HIGH ? REALTIME_PRIORITY_CLASS + : value < SCR_OS_PRIORITY_ABOVE_NORMAL ? HIGH_PRIORITY_CLASS + : value < SCR_OS_PRIORITY_NORMAL ? ABOVE_NORMAL_PRIORITY_CLASS + : value < SCR_OS_PRIORITY_BELOW_NORMAL ? NORMAL_PRIORITY_CLASS + : value < SCR_OS_PRIORITY_LOW ? BELOW_NORMAL_PRIORITY_CLASS + : IDLE_PRIORITY_CLASS; + HANDLE handle = pid == 0 ? GetCurrentProcess() + : OpenProcess(PROCESS_SET_INFORMATION, FALSE, (DWORD)(int)pid); + if (handle == NULL) { + DWORD err = GetLastError(); + scr_os_system_error("uv_os_setpriority", err == ERROR_ACCESS_DENIED ? SCR_NODE_UV__EPERM + : err == ERROR_INVALID_PARAMETER ? SCR_NODE_UV__EINVAL + : SCR_NODE_UV__UNKNOWN); + return; + } + BOOL ok = SetPriorityClass(handle, klass); + DWORD err = ok ? 0 : GetLastError(); + if (pid != 0) CloseHandle(handle); + if (!ok) { + scr_os_system_error("uv_os_setpriority", err == ERROR_ACCESS_DENIED ? SCR_NODE_UV__EPERM + : SCR_NODE_UV__UNKNOWN); + } +#elif defined(__wasi__) + (void)value; + scr_os_system_error("uv_os_setpriority", SCR_NODE_UV__ENOSYS); +#else + if (setpriority(PRIO_PROCESS, (id_t)(int)pid, value) != 0) + scr_os_system_error("uv_os_setpriority", SCR_NODE_UV__ERR(errno)); +#endif +} + /* ── cpus ───────────────────────────────────────────────────────────── * uv_cpu_info, snapshotted once per os.cpus() call: scr_os_cpus_take * reads every CPU and answers the count; the accessors read that diff --git a/packages/runtime/src/scr_os.h b/packages/runtime/src/scr_os.h index b40a8027a8..3c5009fc52 100644 --- a/packages/runtime/src/scr_os.h +++ b/packages/runtime/src/scr_os.h @@ -27,6 +27,10 @@ ScrArr *scr_os_loadavg(void); /* os.getPriority(pid): validateInt32, then the scheduling priority; * failures throw Node's RangeError or SystemError (ERR_SYSTEM_ERROR). */ double scr_os_get_priority(double pid); +/* os.setPriority(pid, priority): validateInt32 for both (priority within + * -20..19), then the scheduling priority; failures throw Node's RangeError + * or SystemError (ERR_SYSTEM_ERROR). */ +void scr_os_set_priority(double pid, double priority); /* os.cpus(): take answers the CPU count of a fresh snapshot; model and * field (0 speed, 1 user, 2 nice, 3 sys, 4 idle, 5 irq) read it. */ double scr_os_cpus_take(void); diff --git a/packages/runtime/src/scr_qs.c b/packages/runtime/src/scr_qs.c index a61f95d947..1b110ba0de 100644 --- a/packages/runtime/src/scr_qs.c +++ b/packages/runtime/src/scr_qs.c @@ -478,8 +478,8 @@ ScrDyn *scr_qs_parse_dyn(const ScrStr *qs) { /* ── stringify ───────────────────────────────────────────────────────── */ /* Node's encodeStringified over one dyn value: strings escape, finite - * numbers render then escape, booleans are bare, everything else is the - * empty value. Appends to b. */ + * numbers render then escape, bigints and booleans are bare, everything + * else is the empty value. Appends to b. */ static void qs_stringify_value(QsBuf *b, const ScrDyn *v) { switch (v->kind) { case SCR_DYN_STR: { @@ -497,12 +497,18 @@ static void qs_stringify_value(QsBuf *b, const ScrDyn *v) { scr_str_release(num); return; } + case SCR_DYN_BIGINT: { + ScrStr *text = scr_bigint_to_string(v->v.bigint, 10); + qb_append_str(b, text); + scr_str_release(text); + return; + } case SCR_DYN_BOOL: if (v->v.b) qb_append(b, "true", 4); else qb_append(b, "false", 5); return; default: - return; /* null/undefined/objects/arrays/functions → '' */ + return; /* null/undefined/symbols/objects/arrays/functions → '' */ } } diff --git a/packages/runtime/src/scr_rc.h b/packages/runtime/src/scr_rc.h index 7069839c75..cdc03c9668 100644 --- a/packages/runtime/src/scr_rc.h +++ b/packages/runtime/src/scr_rc.h @@ -235,6 +235,11 @@ void scr_rt_free(void *p); * of [lo, hi), aligned to 256 KiB, in ahead of use; builds without the * helper ignore it. */ void scr_mem_prefault(void *lo, void *hi); +/* Reorders ptrs, distinct pointers `offset` bytes into blocks that are + * about to be freed in array order, so that the free lists hand the blocks + * back in ascending address order (scr_alloc.c). Builds without the + * small-object allocator leave the array as it is. */ +void scr_mem_order_for_free(void **ptrs, size_t n, size_t offset); /* Size classes: requests up to 128 bytes take a class per 8-byte step * (small objects often sit just under a 16-byte boundary), larger ones a @@ -336,11 +341,18 @@ void scr_sa_thread_park(void); /* Waits until the arenas of exited threads are trimmed (tests). */ void scr_sa_wait_trims(void); /* Called first on a new worker thread: prepares the thread's system- - * allocator heap (scr_alloc.c). */ + * allocator heap, and lets the thread's memory go back when it exits + * (scr_alloc.c). */ void scr_sa_thread_starting(void); +/* The calling thread made objects it allocated reachable from other + * threads: a message or an intern table shares a published graph by + * pointer, or a process-wide cache keeps an immortal object. Its memory + * then stays when it exits (scr_alloc.c). */ +void scr_sa_thread_shares(void); #define SCR_SA_FREE_FOREIGN(p) scr_sa_free_foreign(p) #else #define SCR_SA_FREE_FOREIGN(p) free(p) +#define scr_sa_thread_shares() ((void)0) #endif #if SCR_SMALL_ALLOC diff --git a/packages/runtime/src/scr_regex.c b/packages/runtime/src/scr_regex.c index 2526ca5951..b24261a4d9 100644 --- a/packages/runtime/src/scr_regex.c +++ b/packages/runtime/src/scr_regex.c @@ -85,6 +85,7 @@ static int scr_lre_flags(const ScrStr *flags) { case 's': mask |= LRE_FLAG_DOTALL; break; case 'u': mask |= LRE_FLAG_UNICODE; break; case 'y': mask |= LRE_FLAG_STICKY; break; + case 'v': mask |= LRE_FLAG_UNICODE_SETS; break; default: scr_trap_fmt("scriptc: internal error: unexpected regex flag '%c'\n", flags->data[i]); @@ -201,7 +202,7 @@ static uint8_t *scr_regex_bc(ScrRegex *re) { const char *pat = re->source->data; size_t pat_len = re->source->len; char *cesu = NULL; - if (!(flags & LRE_FLAG_UNICODE)) { + if (!(flags & (LRE_FLAG_UNICODE | LRE_FLAG_UNICODE_SETS))) { cesu = scr_pattern_cesu8(re->source, &pat_len); if (cesu) pat = cesu; } @@ -2042,11 +2043,9 @@ ScrRegex *scr_regex_new(ScrStr *pattern, ScrStr *flags) { } /* Valid in Node, absent from the engine: name the gap rather than * report a flag Node accepts as invalid. */ - if (seen_flags & ((1u << 6) | (1u << 7))) { + if (seen_flags & (1u << 6)) { static const char d_msg[] = "the regex 'd' flag (match indices) is not supported yet [SC1120]"; - static const char v_msg[] = "the regex 'v' flag (unicode sets) is not supported yet [SC1120]"; - if (seen_flags & (1u << 6)) scr_throw_error_msg(SCR_ERR_ERROR, d_msg, sizeof d_msg - 1); - else scr_throw_error_msg(SCR_ERR_ERROR, v_msg, sizeof v_msg - 1); + scr_throw_error_msg(SCR_ERR_ERROR, d_msg, sizeof d_msg - 1); return NULL; } ScrRegex *re = calloc(1, sizeof *re); @@ -2057,11 +2056,11 @@ ScrRegex *scr_regex_new(ScrStr *pattern, ScrStr *flags) { re->source = pattern->len > 0 ? scr_str_retain(pattern) : scr_str_new("(?:)", 4); /* Public flags use getter order, independently of constructor spelling. * Validate first so duplicates and invalid flags retain their errors. */ - char canonical[6]; + char canonical[8]; size_t flag_count = 0; - const char *order = "gimsuy"; - for (unsigned i = 0; i < 6; i++) { - if (seen_flags & (1u << i)) canonical[flag_count++] = order[i]; + static const unsigned order_bits[] = {0, 1, 2, 3, 4, 7, 5}; + for (unsigned i = 0; i < sizeof order_bits / sizeof order_bits[0]; i++) { + if (seen_flags & (1u << order_bits[i])) canonical[flag_count++] = "gimsuvy"[i]; } re->flags = flag_count == flags->len && memcmp(canonical, flags->data, flag_count) == 0 ? scr_str_retain(flags) : scr_str_new(canonical, flag_count); @@ -2078,7 +2077,7 @@ ScrRegex *scr_regex_new(ScrStr *pattern, ScrStr *flags) { const char *pat = re->source->data; size_t pat_len = re->source->len; char *cesu = NULL; - if (!(lre_flags & LRE_FLAG_UNICODE)) { + if (!(lre_flags & (LRE_FLAG_UNICODE | LRE_FLAG_UNICODE_SETS))) { cesu = scr_pattern_cesu8(re->source, &pat_len); if (cesu) pat = cesu; } diff --git a/packages/runtime/src/scr_region.c b/packages/runtime/src/scr_region.c index c1a125e8bc..31d01296e3 100644 --- a/packages/runtime/src/scr_region.c +++ b/packages/runtime/src/scr_region.c @@ -70,6 +70,18 @@ void __lsan_unregister_root_region(const void *p, size_t size) __attribute__((we #define SCR_RGN_AHEAD_START ((size_t)1 << 20) /* see scr_rgn_ahead */ _Static_assert(sizeof(ScrRgnChunk) <= SCR_RGN_HDR, "the chunk header fits before the first segment"); +/* Freed chunks a thread keeps for its next regions: a program that frees + * a region per request or per batch would otherwise map, fault in and + * unmap its chunks every time. A cached chunk keeps at most its first + * SCR_RGN_CACHE_WARM bytes resident (cleared), and gets fresh pages past + * them, so a small region that reuses the chunk of a large one does not + * hold the large one's memory. The cache holds at most SCR_RGN_CACHE_BYTES + * of resident chunk memory (whole chunks where chunks are not mapped + * pages); chunks beyond it, and chunks huge pages may back, are unmapped + * as before. */ +#define SCR_RGN_CACHE_BYTES ((size_t)8 << 20) +#define SCR_RGN_CACHE_WARM ((size_t)256 << 10) + /* Regions start with small pages and switch to huge pages once they span * this many chunks (a default region at once): small regions then do not * pay a 2 MiB fault, large ones do not pay 512 small ones. */ @@ -99,6 +111,7 @@ typedef struct ScrRegion { uint64_t id; /* 0: the thread's default region */ int state; uint32_t depth; /* run() frames active on this region */ + size_t fibers; /* async fibers that carry it (scr_region_fiber_hooks) */ ScrRgnChunk *chunks; size_t nchunks; ScrRgnSeg *segs; @@ -210,7 +223,10 @@ typedef struct { size_t allocated; /* bytes of region segments this thread has allocated */ size_t checked; /* `allocated` at the last retry */ ScrRgnWalk walk; + ScrRgnChunk *cache; /* freed chunks, linked through `next` (SCR_RGN_CACHE_BYTES) */ + size_t cache_bytes; /* their resident bytes, as recorded in each one's `found` */ bool exit_registered; + bool exited; /* the exit cleanup ran: the fiber hooks do nothing */ double cycle_ms; /* the cycle pass before the next attempt (stats) */ struct { size_t created, released, freed, pinned, leaked, attempts; @@ -329,18 +345,39 @@ static bool scr_rgn_huge(const ScrRegion *r) { return r->id == 0 || r->nchunks + /* A chunk for `r`'s next one, with its page policy. */ static ScrRgnChunk *scr_rgn_chunk_reserve(const ScrRegion *r) { ScrRgnChunk *c = scr_rgn_chunk_map(); + c->huge = false; +#if SCR_RGN_MMAP + c->touched = (char *)c + SCR_RGN_HDR; +#else + c->touched = (char *)c + SCR_RGN_CHUNK; /* allocated zeroed */ +#endif #if SCR_RGN_MMAP && defined(MADV_HUGEPAGE) - if (scr_rgn_huge(r)) (void)madvise(c, SCR_RGN_CHUNK, MADV_HUGEPAGE); + if (scr_rgn_huge(r)) { + (void)madvise(c, SCR_RGN_CHUNK, MADV_HUGEPAGE); + c->huge = true; + } #else (void)r; #endif return c; } +/* Faults [from, to) of chunk `c` in ahead of use. */ +static void scr_rgn_prefault(ScrRgnChunk *c, char *from, char *to) { + if (to > c->touched) c->touched = to; + scr_mem_prefault(from, to); +} + static ScrRgnChunk *scr_rgn_chunk_new(ScrRegion *r) { - ScrRgnChunk *c = r->spare ? r->spare : scr_rgn_chunk_reserve(r); + ScrRgnChunk *c = r->spare; + if (!c && scr_rgn.cache) { + c = scr_rgn.cache; + scr_rgn.cache = c->next; + scr_rgn.cache_bytes -= (size_t)c->found; + } + if (!c) c = scr_rgn_chunk_reserve(r); r->spare = NULL; - if (r->bytes >= SCR_RGN_AHEAD_START) scr_mem_prefault(c, (char *)c + 2 * SCR_RGN_AHEAD); + if (r->bytes >= SCR_RGN_AHEAD_START) scr_rgn_prefault(c, (char *)c, (char *)c + 2 * SCR_RGN_AHEAD); #ifdef SCR_LSAN_IMMORTAL if (__lsan_register_root_region) __lsan_register_root_region(c, SCR_RGN_CHUNK); #endif @@ -369,26 +406,74 @@ static void scr_rgn_ahead(ScrRegion *r, ScrRgnChunk *c, char *from) { size_t before = (size_t)(from - (char *)c), after = (size_t)(c->bump - (char *)c); if (after / SCR_RGN_AHEAD != before / SCR_RGN_AHEAD) { char *lo = (char *)c + (after / SCR_RGN_AHEAD + 1) * SCR_RGN_AHEAD; - if (lo < (char *)c + SCR_RGN_CHUNK) scr_mem_prefault(lo, lo + SCR_RGN_AHEAD); + if (lo < (char *)c + SCR_RGN_CHUNK) scr_rgn_prefault(c, lo, lo + SCR_RGN_AHEAD); } if (!r->spare && after >= SCR_RGN_CHUNK / 2 && before < SCR_RGN_CHUNK / 2) { bool huge = scr_rgn_huge(r); r->spare = scr_rgn_chunk_reserve(r); - scr_mem_prefault(r->spare, (char *)r->spare + (huge ? SCR_RGN_CHUNK : SCR_RGN_AHEAD)); + scr_rgn_prefault(r->spare, (char *)r->spare, (char *)r->spare + (huge ? SCR_RGN_CHUNK : SCR_RGN_AHEAD)); } } +/* Returns a chunk that holds no object to the system. */ +static void scr_rgn_chunk_drop(ScrRgnChunk *c) { +#if SCR_RGN_MMAP + munmap(c, SCR_RGN_CHUNK); +#elif defined(_WIN32) + _aligned_free(c); +#else + free(c); +#endif +} + /* A spare chunk never held objects: it goes straight back. */ static void scr_rgn_spare_drop(ScrRegion *r) { if (!r->spare) return; + scr_rgn_chunk_drop(r->spare); + r->spare = NULL; +} + +/* A freed region's chunk: kept for the thread's next regions while the + * cache has room, cleared (a fresh chunk is zero, and allocation relies on + * that), else unmapped (SCR_RGN_CACHE_BYTES). Sanitized builds never reuse + * a chunk (scr_rgn_chunk_unmap). */ +static void scr_rgn_chunk_retire(ScrRgnThread *t, ScrRgnChunk *c) { +#ifndef SCR_RGN_POISON + char *first = (char *)c + SCR_RGN_HDR; + if (c->bump > c->touched) c->touched = c->bump; #if SCR_RGN_MMAP - munmap(r->spare, SCR_RGN_CHUNK); -#elif defined(_WIN32) - _aligned_free(r->spare); + char *warm = (char *)c + SCR_RGN_CACHE_WARM; + size_t resident = (size_t)((c->touched < warm ? c->touched : warm) - (char *)c); #else - free(r->spare); + size_t resident = SCR_RGN_CHUNK; #endif - r->spare = NULL; + if (!c->huge && t->cache_bytes + resident <= SCR_RGN_CACHE_BYTES) { +#if SCR_RGN_MMAP + if (c->touched > warm) { + /* Fresh zero pages past the warm part (64 KiB steps cover every + * page size). */ + size_t tail = ((size_t)(c->touched - warm) + 0xffff) & ~(size_t)0xffff; + if (tail > SCR_RGN_CHUNK - SCR_RGN_CACHE_WARM) tail = SCR_RGN_CHUNK - SCR_RGN_CACHE_WARM; + if (mmap(warm, tail, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) == MAP_FAILED) { + scr_rgn_chunk_unmap(c); + return; + } + c->touched = warm; + } + memset(first, 0, (size_t)((c->bump < warm ? c->bump : warm) - first)); +#else + memset(first, 0, (size_t)(c->bump - first)); +#endif + c->found = (int64_t)resident; + c->next = t->cache; + t->cache = c; + t->cache_bytes += resident; + return; + } +#else + (void)t; +#endif + scr_rgn_chunk_unmap(c); } /* The instance stride: the emitted fast path rounds the size the same way. */ @@ -875,7 +960,7 @@ static void scr_rgn_free_region(ScrRgnThread *t, ScrRegion *r) { scr_rgn_spare_drop(r); for (ScrRgnChunk *c = r->chunks, *next; c; c = next) { next = c->next; - scr_rgn_chunk_unmap(c); + scr_rgn_chunk_retire(t, c); } for (ScrRgnSeg *s = r->segs, *next; s; s = next) { next = s->next; @@ -1118,7 +1203,7 @@ static bool scr_rgn_try_free(ScrRgnThread *t, ScrRegion *r, bool tentative) { /* A condemned region not in use, never attempted, or kept by an attempt * whose outcome may have changed since (see SCR_RGN_RETRY_BYTES). */ static bool scr_rgn_due(const ScrRegion *r, const ScrRgnThread *t) { - if (r->state != SCR_RGN_CONDEMNED || r->depth != 0 || r == t->current) return false; + if (r->state != SCR_RGN_CONDEMNED || r->depth != 0 || r->fibers != 0 || r == t->current) return false; return !r->kept || t->allocated >= r->retry_at || scr_rgn_counted(r) != r->kept_counted; } @@ -1162,11 +1247,53 @@ static void scr_rgn_retry(ScrRgnThread *t) { scr_rgn_try_due(t, false); } +/* ── async continuations ─────────────────────────────────────────────── */ + +/* A fiber spawned while a region is current carries it: the region + * cannot be freed until the fiber is gone, so work that resumes after an + * await can keep allocating in it, condemned or not (scr_rgn_due). The + * fiber's own references to the region's objects are counted like any + * others. Fibers of the default region carry nothing. */ +static void *scr_rgn_fiber_capture(void) { + ScrRgnThread *t = &scr_rgn; + ScrRegion *r = t->current; + if (t->exited || !r || r->id == 0) return NULL; + r->fibers++; + return r; +} + +static void scr_rgn_fiber_drop(void *region) { + if (region && !scr_rgn.exited) ((ScrRegion *)region)->fibers--; +} + +/* The event loop resumes a fiber: its region becomes current until the + * fiber suspends or finishes. Run frames never span a suspension, so the + * fiber leaves the region current that it found. */ +static void *scr_rgn_fiber_resume(void *region) { + ScrRgnThread *t = &scr_rgn; + if (t->exited || !t->dflt) return NULL; /* no region on this thread yet */ + ScrRegion *previous = t->current; + scr_rgn_switch(t, region ? (ScrRegion *)region : t->dflt); + return previous; +} + +static void scr_rgn_fiber_suspend(void *previous) { + if (previous && !scr_rgn.exited) scr_rgn_switch(&scr_rgn, previous); +} + +static const ScrRgnFiberHooks scr_rgn_fiber_hooks = { + scr_rgn_fiber_capture, + scr_rgn_fiber_drop, + scr_rgn_fiber_resume, + scr_rgn_fiber_suspend, +}; + /* ── program-facing entry points ─────────────────────────────────────── */ double scr_region_create(const ScrRgnType *types) { const ScrRgnType *expected = NULL; atomic_compare_exchange_strong(&scr_rgn_types, &expected, types); + atomic_store_explicit(&scr_region_fiber_hooks, &scr_rgn_fiber_hooks, memory_order_relaxed); /* Counting starts before the region's first object exists. */ atomic_store_explicit(&scr_region_counting, 1, memory_order_relaxed); ScrRgnThread *t = scr_rgn_thread(); @@ -1230,6 +1357,10 @@ static void scr_rgn_thread_exit(void) { free(r->cur); free(r); } + for (ScrRgnChunk *c = t->cache, *next; c; c = next) { + next = c->next; + scr_rgn_chunk_drop(c); + } free(t->regions); free(t->cursors); free(t->stack); @@ -1243,4 +1374,5 @@ static void scr_rgn_thread_exit(void) { #endif memset(t, 0, sizeof *t); t->exit_registered = true; /* the cleanup ran; a late allocation keeps its memory */ + t->exited = true; /* fibers destroyed later must not touch the freed regions */ } diff --git a/packages/runtime/src/scr_region.h b/packages/runtime/src/scr_region.h index 14eae6951a..aa07f90527 100644 --- a/packages/runtime/src/scr_region.h +++ b/packages/runtime/src/scr_region.h @@ -77,6 +77,11 @@ typedef struct ScrRgnChunk { struct ScrRgnChunk *next; char *bump; /* the next free segment byte */ int64_t found; /* references the last walk saw into this chunk */ + /* What stays resident across reuse (scr_region.c's chunk cache): the + * highest byte any region carved or faulted in, and whether the chunk + * may be backed by huge pages. */ + char *touched; + bool huge; } ScrRgnChunk; _Static_assert(offsetof(ScrRgnChunk, count) == 0 && offsetof(ScrRgnChunk, owner) == sizeof(int64_t), "llvm/regions.ts addresses the chunk count at 0 and the owner at 8"); @@ -111,4 +116,21 @@ void scr_region_release(double id); void scr_region_ref(ScrRgnWalk *w, void *obj); void scr_region_slot(ScrRgnWalk *w, void *obj, void (*release)(void *)); +/* Async continuations. A fiber that runs an async function body, on its + * own stack or as a stackless coroutine, carries the region that was + * current when it was spawned (`capture`; NULL is the default region), and + * the event loop makes that region current while the fiber runs after an + * await (`resume` answers the region to restore with + * `suspend`). A region is never freed while a fiber that carries it + * exists; `drop` ends the fiber's hold. scr_region_create installs the + * hooks (process-wide, once); until then the fiber machinery (scr_async.c) + * calls nothing. Every hook acts on the calling thread's regions. */ +typedef struct ScrRgnFiberHooks { + void *(*capture)(void); + void (*drop)(void *region); + void *(*resume)(void *region); + void (*suspend)(void *previous); +} ScrRgnFiberHooks; +extern _Atomic(const ScrRgnFiberHooks *) scr_region_fiber_hooks; + #endif /* SCR_REGION_H */ diff --git a/packages/runtime/src/scr_region.test.c b/packages/runtime/src/scr_region.test.c index b79c818cd3..31e98942a8 100644 --- a/packages/runtime/src/scr_region.test.c +++ b/packages/runtime/src/scr_region.test.c @@ -381,6 +381,30 @@ static void check_debug_report(void) { puts("the debug report names what keeps a region"); } +/* A freed region's chunk serves the thread's next region, cleared where + * the freed objects were. Sanitized builds never reuse a chunk. */ +static void check_chunk_reuse(void) { +#ifndef SCR_RC_AUDIT + double r = region(); + scr_region_enter(r); + Node *first = node_new(7); + first->other = node_retain(first); + scr_region_leave(); + ScrRgnChunk *chunk = scr_rgn_chunk_of(first); + node_release(first); + scr_region_release(r); + double s = region(); + scr_region_enter(s); + Node *again = node_new(8); + scr_region_leave(); + assert(scr_rgn_chunk_of(again) == chunk); + assert(again->other == NULL && again->parent == NULL && again->kids == NULL); + node_release(again); + scr_region_release(s); + puts("a freed region's chunk serves the next region, cleared"); +#endif +} + static void check_poison(void) { #if defined(SCR_RC_AUDIT) && !defined(_WIN32) double r = region(); @@ -499,6 +523,7 @@ int main(int argc, char **argv) { check_in_use(); check_cursors(); check_retry_schedule(); + check_chunk_reuse(); check_poison(); } else if (!strcmp(mode, "debug")) { check_debug_report(); diff --git a/packages/runtime/src/scr_region.test.ts b/packages/runtime/src/scr_region.test.ts index cfadac91eb..28e44e09af 100644 --- a/packages/runtime/src/scr_region.test.ts +++ b/packages/runtime/src/scr_region.test.ts @@ -122,11 +122,16 @@ describe.skipIf(process.platform === "win32")("counted lifetime regions", () => const sanitized = lane === "sanitized"; expect(owned.stdout.trim().split("\n")).toEqual([ ...OWNED_STDOUT, - ...(sanitized ? ["sanitized builds fault on any access to a freed region"] : []), + sanitized + ? "sanitized builds fault on any access to a freed region" + : "a freed region's chunk serves the next region, cleared", ]); const ownedStats = attempts(owned.stderr); - expect(ownedStats.attempts).toEqual([...OWNED_ATTEMPTS, ...(sanitized ? ["18 freed"] : [])]); - const n = sanitized ? 18 : 17; + expect(ownedStats.attempts).toEqual([ + ...OWNED_ATTEMPTS, + ...(sanitized ? ["18 freed"] : ["18 freed", "19 freed"]), + ]); + const n = sanitized ? 18 : 19; expect(ownedStats.summaries).toEqual([ `scriptc regions: ${n} created, ${n} released, ${n} freed, 0 pinned, 0 kept`, ]); diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index df08ea3182..244e2ec5d3 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -6367,6 +6367,15 @@ const ScrChildStreamOps scr_stream_child_ops = { scr_stream_write_done, scr_stream_final_done, }; +static ScrDyn *scr_stream_dyn_view_v(void *stream) { return scr_stream_dyn_view(stream); } + +/* new Worker() with the stdout or stderr option (readable.workerStdio) */ +void scr_stream_worker_stdio(void) { +#ifdef SCR_WORKERS + scr_worker_stdio_default(&scr_stream_child_ops, scr_stream_dyn_view_v); +#endif +} + static ScrStream *scr_stream_view_receiver(const ScrDyn *value) { if (value->kind == SCR_DYN_TYPED_REF && value->v.typed_ref.type_key_len > 7 && !memcmp(value->v.typed_ref.type_key, "object:", 7)) { diff --git a/packages/runtime/src/scr_stream.h b/packages/runtime/src/scr_stream.h index 1836c206df..82727c1dcd 100644 --- a/packages/runtime/src/scr_stream.h +++ b/packages/runtime/src/scr_stream.h @@ -123,6 +123,7 @@ void scr_stream_init_passthrough(ScrStream *s, double rhwm, double whwm, bool au * (a subclass's overridden underscore-method wrappers, shadowed by * options callbacks per Node's instance-property rule) MOVE. */ ScrStream *scr_stream_new_readable_dyn(struct ScrDyn *opts); +void scr_stream_worker_stdio(void); ScrStream *scr_stream_new_writable_dyn(struct ScrDyn *opts); ScrStream *scr_stream_new_duplex_dyn(struct ScrDyn *opts); ScrStream *scr_stream_new_transform_dyn(struct ScrDyn *opts); diff --git a/packages/runtime/src/scr_symbol.c b/packages/runtime/src/scr_symbol.c index a29a28ad2e..383b959861 100644 --- a/packages/runtime/src/scr_symbol.c +++ b/packages/runtime/src/scr_symbol.c @@ -90,6 +90,11 @@ ScrSym *scr_sym_well_known(ScrStr *name) { scr_dyn_install_async_iterator_symbol(key); scr_dyn_release(key); } + if (name->len == 12 && !memcmp(name->data, "asyncDispose", 12)) { + ScrDyn *key = scr_dyn_new_symbol(symbol); + scr_dyn_install_async_dispose_symbol(key); + scr_dyn_release(key); + } if (name->len == 11 && !memcmp(name->data, "toPrimitive", 11)) { ScrDyn *key = scr_dyn_new_symbol(symbol); scr_dyn_install_to_primitive_symbol(key); @@ -457,3 +462,371 @@ ScrDyn *scr_dyn_using_dispose(const ScrDyn *method, const ScrDyn *value) { scr_dyn_release(result); return scr_dyn_retain(scr_dyn_undefined()); } + +/* ── DisposableStack ─────────────────────────────────────────────────── + * The stack is a collector-headered object behind a traced dyn handle: its + * resources hold callbacks that can capture the stack itself. Each entry + * records how disposal calls it: `use` calls the value's dispose method + * with the value as `this`, `adopt` calls the callback with the value, and + * `defer` calls the callback with no arguments. Disposal runs the entries + * in reverse order; each later failure becomes a SuppressedError over the + * error so far, and a single failure is rethrown as it was thrown. */ +enum { SCR_DS_USE, SCR_DS_ADOPT, SCR_DS_DEFER }; + +typedef struct { + ScrDyn *value; /* the resource (use, adopt); NULL for defer */ + ScrDyn *method; + unsigned kind; + /* Which members the last trace visited: the collector frees those + * itself, and gcfree must not read them afterwards. */ + bool value_traced, method_traced; +} ScrDisposableResource; + +typedef struct { + size_t rc; + bool disposed; + ScrDisposableResource *items; + size_t len, cap; +} ScrDisposableStack; + +static bool scr_ds_traced(const ScrDyn *value) { + return value && value->kind > SCR_DYN_STR && value->kind != SCR_DYN_UNDEF; +} + +static void scr_ds_trace(void *ptr, ScrTraceVisit visit, void *ctx) { + ScrDisposableStack *s = ptr; + for (size_t i = 0; i < s->len; i++) { + ScrDisposableResource *r = &s->items[i]; + r->value_traced = scr_ds_traced(r->value); + r->method_traced = scr_ds_traced(r->method); + if (r->value_traced) visit(r->value, ctx); + if (r->method_traced) visit(r->method, ctx); + } +} + +/* The collector frees traced members itself; the rest are released here. */ +static void scr_ds_gcfree(void *ptr) { + ScrDisposableStack *s = ptr; + for (size_t i = 0; i < s->len; i++) { + if (!s->items[i].value_traced) scr_dyn_release(s->items[i].value); + if (!s->items[i].method_traced) scr_dyn_release(s->items[i].method); + } + free(s->items); + scr_obj_free_note(); + scr_cyc_free(ptr); +} + +static void scr_ds_destroy(void *ptr) { + ScrDisposableStack *s = ptr; + for (size_t i = 0; i < s->len; i++) { + scr_dyn_release(s->items[i].value); + scr_dyn_release(s->items[i].method); + } + s->len = 0; + scr_ds_gcfree(ptr); +} + +static void *scr_ds_retain(void *ptr) { + ((ScrDisposableStack *)ptr)->rc++; + return ptr; +} + +static void scr_ds_release(void *ptr) { + ScrDisposableStack *s = ptr; + if (!s) return; + if (--s->rc == 0) { + scr_cyc_dead(s); + scr_rc_destroy(s, scr_ds_destroy); + } else scr_cyc_released(s); +} + +static void scr_ds_push(ScrDisposableStack *s, ScrDyn *value, ScrDyn *method, unsigned kind) { + if (s->len == s->cap) { + size_t cap = s->cap ? s->cap * 2 : 4; + ScrDisposableResource *items = realloc(s->items, cap * sizeof *items); + if (!items) scr_trap("scriptc: out of memory\n"); + s->items = items; + s->cap = cap; + } + s->items[s->len++] = (ScrDisposableResource){value, method, kind, false, false}; +} + +/* V8's rendering of a value in a TypeError: primitives by ToString, + * arrays as [object Array], other objects as #. */ +static void scr_ds_put_value(ScrJsonBuf *b, const ScrDyn *value) { + switch (value->kind) { + case SCR_DYN_UNDEF: scr_jb_puts(b, "undefined"); return; + case SCR_DYN_NULL: scr_jb_puts(b, "null"); return; + case SCR_DYN_BOOL: + case SCR_DYN_NUM: + case SCR_DYN_STR: + case SCR_DYN_BIGINT: { + ScrStr *text = scr_dyn_to_string(value, NULL); + if (text) scr_jb_write(b, text->data, text->len); + scr_str_release(text); + return; + } + case SCR_DYN_SYMBOL: { + ScrStr *text = value->v.symbol.render(value->v.symbol.value); + scr_jb_write(b, text->data, text->len); + scr_str_release(text); + return; + } + case SCR_DYN_ARR: scr_jb_puts(b, "[object Array]"); return; + case SCR_DYN_HANDLE: + scr_jb_puts(b, "#<"); + scr_jb_puts(b, scr_dyn_handle_cls(value)); + scr_jb_putc(b, '>'); + return; + default: scr_jb_puts(b, "#"); return; + } +} + +static ScrDisposableStack *scr_ds_of(const ScrDyn *value) { + return value && value->kind == SCR_DYN_HANDLE && value->v.handle.tag == SCR_DYNH_DISPOSABLE_STACK + ? value->v.handle.ptr : NULL; +} + +bool scr_disposable_stack_is(const ScrDyn *value) { return scr_ds_of(value) != NULL; } + +/* The receiver of DisposableStack.prototype., or NULL with Node's + * TypeError (an incompatible receiver) or ReferenceError (a disposed + * stack, when `live` is required) pending. */ +static ScrDisposableStack *scr_ds_receiver(const ScrDyn *self, const char *name, bool getter, bool live) { + ScrDisposableStack *s = scr_ds_of(self); + ScrJsonBuf message; + if (!s) { + scr_jb_init(&message); + scr_jb_puts(&message, getter ? "Method get DisposableStack.prototype." : "Method DisposableStack.prototype."); + scr_jb_puts(&message, name); + scr_jb_puts(&message, " called on incompatible receiver "); + scr_ds_put_value(&message, self); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&message)); + return NULL; + } + if (live && s->disposed) { + scr_jb_init(&message); + scr_jb_puts(&message, "Cannot call DisposableStack.prototype."); + scr_jb_puts(&message, name); + scr_jb_puts(&message, " on an already-disposed DisposableStack"); + scr_throw_error(SCR_ERR_REFERENCE, scr_jb_finish(&message)); + return NULL; + } + return s; +} + +static bool scr_ds_callable(const ScrDyn *value) { + if (scr_using_typeof_is(value, "function", 8)) return true; + ScrJsonBuf message; + scr_jb_init(&message); + scr_ds_put_value(&message, value); + scr_jb_puts(&message, " is not a function"); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&message)); + return false; +} + +bool scr_disposable_stack_use(const ScrDyn *stack, const ScrDyn *value) { + ScrDisposableStack *s = scr_ds_receiver(stack, "use", false, true); + if (!s) return false; + if (value->kind == SCR_DYN_NULL || value->kind == SCR_DYN_UNDEF) return true; + ScrDyn *method = scr_dyn_using_method(value, false); + if (!method) return false; + scr_ds_push(s, scr_dyn_retain((ScrDyn *)value), method, SCR_DS_USE); + return true; +} + +bool scr_disposable_stack_use_callback(const ScrDyn *stack, const ScrDyn *value, const ScrDyn *dispose) { + ScrDisposableStack *s = scr_ds_receiver(stack, "use", false, true); + if (!s) return false; + scr_ds_push(s, scr_dyn_retain((ScrDyn *)value), scr_dyn_retain((ScrDyn *)dispose), SCR_DS_ADOPT); + return true; +} + +bool scr_disposable_stack_adopt(const ScrDyn *stack, const ScrDyn *value, const ScrDyn *on_dispose) { + ScrDisposableStack *s = scr_ds_receiver(stack, "adopt", false, true); + if (!s || !scr_ds_callable(on_dispose)) return false; + scr_ds_push(s, scr_dyn_retain((ScrDyn *)value), scr_dyn_retain((ScrDyn *)on_dispose), SCR_DS_ADOPT); + return true; +} + +static bool scr_disposable_stack_defer(const ScrDyn *stack, const ScrDyn *on_dispose) { + ScrDisposableStack *s = scr_ds_receiver(stack, "defer", false, true); + if (!s || !scr_ds_callable(on_dispose)) return false; + scr_ds_push(s, NULL, scr_dyn_retain((ScrDyn *)on_dispose), SCR_DS_DEFER); + return true; +} + +/* DisposeResources over the stack's entries: NULL with the disposal error + * pending, or undefined (+1). */ +static ScrDyn *scr_ds_dispose(ScrDisposableStack *s) { + if (s->disposed) return scr_dyn_retain(scr_dyn_undefined()); + s->disposed = true; + ScrDisposableResource *items = s->items; + size_t len = s->len; + s->items = NULL; + s->len = s->cap = 0; + ScrCaught *error = NULL; + for (size_t i = len; i-- > 0;) { + ScrDisposableResource *r = &items[i]; + ScrDyn *result; + if (r->kind == SCR_DS_USE) { + result = scr_dyn_using_dispose(r->method, r->value); + } else { + scr_dyn_this_push_dyn(scr_dyn_undefined()); + result = r->kind == SCR_DS_ADOPT ? scr_dyn_call(r->method, &r->value, 1, "onDispose") + : scr_dyn_call(r->method, NULL, 0, "onDispose"); + scr_dyn_this_pop(); + } + if (result) { + scr_dyn_release(result); + } else { + if (error) scr_exc_suppress(error); + error = scr_exc_take(); + } + scr_dyn_release(r->value); + scr_dyn_release(r->method); + } + free(items); + if (!error) return scr_dyn_retain(scr_dyn_undefined()); + scr_rethrow(error); + scr_caught_release(error); + return NULL; +} + +static SCR_TL ScrDyn *scr_ds_method_values[6]; +static const char *const scr_ds_method_names[] = {"use", "adopt", "defer", "move", "dispose", "disposed"}; +static const uint32_t scr_ds_method_lengths[] = {1, 2, 1, 0, 0, 0}; +enum { SCR_DS_M_USE, SCR_DS_M_ADOPT, SCR_DS_M_DEFER, SCR_DS_M_MOVE, SCR_DS_M_DISPOSE, SCR_DS_M_DISPOSED }; + +static ScrDisposableStack *scr_ds_alloc(void); +static ScrDyn *scr_ds_box(ScrDisposableStack *s); + +static ScrDyn *scr_ds_method_call(ScrClosure *clo, ScrDyn *const *args, size_t argc) { + unsigned index = (unsigned)scr_box_get_f64(clo->caps[0]); + ScrDyn *self = scr_dyn_this_get(); + const ScrDyn *arg0 = argc > 0 ? args[0] : scr_dyn_undefined(); + const ScrDyn *arg1 = argc > 1 ? args[1] : scr_dyn_undefined(); + ScrDyn *out = NULL; + switch (index) { + case SCR_DS_M_USE: + if (scr_disposable_stack_use(self, arg0)) out = scr_dyn_retain((ScrDyn *)arg0); + break; + case SCR_DS_M_ADOPT: + if (scr_disposable_stack_adopt(self, arg0, arg1)) out = scr_dyn_retain((ScrDyn *)arg0); + break; + case SCR_DS_M_DEFER: + if (scr_disposable_stack_defer(self, arg0)) out = scr_dyn_retain(scr_dyn_undefined()); + break; + case SCR_DS_M_MOVE: { + /* The new stack takes the entries; this one becomes disposed. */ + ScrDisposableStack *s = scr_ds_receiver(self, "move", false, true); + if (!s) break; + ScrDisposableStack *moved = scr_ds_alloc(); + moved->items = s->items; + moved->len = s->len; + moved->cap = s->cap; + s->items = NULL; + s->len = s->cap = 0; + s->disposed = true; + out = scr_ds_box(moved); + scr_ds_release(moved); + break; + } + default: { + ScrDisposableStack *s = scr_ds_receiver(self, "dispose", false, false); + if (s) out = scr_ds_dispose(s); + break; + } + } + scr_dyn_release(self); + return out; +} + +/* The function value of method `index` (+1), cached per thread. */ +static ScrDyn *scr_ds_method_value(unsigned index) { + if (!scr_ds_method_values[index]) { + ScrClosure *clo = scr_closure_new(NULL, 1); + clo->caps[0] = scr_box_new(SCR_BOX_F64); + scr_box_set_f64(clo->caps[0], (double)index); + scr_ds_method_values[index] = scr_dyn_new_func(clo, scr_ds_method_call, scr_ds_method_lengths[index], + "", scr_ds_method_names[index]); + } + return scr_dyn_retain(scr_ds_method_values[index]); +} + +static ScrDyn *scr_ds_get(void *ptr, const char *key, size_t len) { + ScrDisposableStack *s = ptr; + if (len == 8 && memcmp(key, "disposed", 8) == 0) return scr_dyn_new_bool(s->disposed); + for (unsigned i = 0; i < SCR_DS_M_DISPOSED; i++) { + if (strlen(scr_ds_method_names[i]) == len && memcmp(key, scr_ds_method_names[i], len) == 0) + return scr_ds_method_value(i); + } + return NULL; +} + +static bool scr_ds_set(void *ptr, const char *key, size_t len, const ScrDyn *value) { + (void)ptr; + (void)key; + (void)len; + (void)value; + return false; +} + +/* [Symbol.dispose] is the dispose method itself, as in Node. */ +static ScrDyn *scr_ds_symbol_get(void *ptr, const ScrDyn *key) { + (void)ptr; + if (key->kind != SCR_DYN_SYMBOL) return NULL; + ScrStr *name = scr_str_new("dispose", 7); + ScrSym *dispose = scr_sym_well_known(name); + scr_str_release(name); + bool match = key->v.symbol.value == dispose; + scr_sym_release(dispose); + return match ? scr_ds_method_value(SCR_DS_M_DISPOSE) : NULL; +} + +static ScrDyn *scr_ds_inspect_view(void *ptr) { + (void)ptr; + return scr_dyn_new_obj(); +} + +static void scr_ds_teardown(void) { + for (size_t i = 0; i < sizeof scr_ds_method_values / sizeof scr_ds_method_values[0]; i++) { + scr_dyn_release(scr_ds_method_values[i]); + scr_ds_method_values[i] = NULL; + } +} + +static const ScrDynHandleOps scr_ds_ops = { + .cls = "DisposableStack", + .retain = scr_ds_retain, + .release = scr_ds_release, + .get = scr_ds_get, + .set = scr_ds_set, + .inspect_view = scr_ds_inspect_view, + .json_empty_object = true, + .inspect_view_empty = true, + .symbol_get = scr_ds_symbol_get, +}; + +static ScrDisposableStack *scr_ds_alloc(void) { + static SCR_TL bool installed; + if (!installed) { + installed = true; + scr_dyn_handle_install(SCR_DYNH_DISPOSABLE_STACK, &scr_ds_ops); + scr_atexit(scr_ds_teardown); + } + static ScrCycType cyc_type = {&scr_ds_trace, &scr_ds_gcfree, SCR_CYC_KIND_NONE, 0}; + ScrDisposableStack *s = scr_cyc_alloc(sizeof *s, &cyc_type); + s->rc = 1; + scr_obj_alloc_note(); + return s; +} + +static ScrDyn *scr_ds_box(ScrDisposableStack *s) { return scr_dyn_new_handle(s, SCR_DYNH_DISPOSABLE_STACK); } + +ScrDyn *scr_disposable_stack_new(void) { + ScrDisposableStack *s = scr_ds_alloc(); + ScrDyn *out = scr_ds_box(s); + scr_ds_release(s); + return out; +} diff --git a/packages/runtime/src/scr_test.c b/packages/runtime/src/scr_test.c index 89301cbd39..9e6da5939f 100644 --- a/packages/runtime/src/scr_test.c +++ b/packages/runtime/src/scr_test.c @@ -35,6 +35,7 @@ #include "scr_platform.h" #include "scr_runtime.h" +#include #include #include #include @@ -120,6 +121,7 @@ struct ScrTestCtx { bool on_fiber; /* started on its own fiber by a concurrent parent */ ScrPromise *done; /* owned; settles when a concurrent subtest finishes */ ScrPromise *slot; /* owned; the parent waiting for its next subtest to finish */ + struct ScrMockTimers *mock_timers; /* t.mock.timers, made on first use; freed at exit */ }; static SCR_TL ScrTestCtx *scr_test_root = NULL; @@ -443,6 +445,8 @@ static bool scr_test_any_only(ScrTestCtx *parent) { } static void scr_test_run_node(ScrTestCtx *t); +typedef struct ScrMockTimers ScrMockTimers; +static void scr_mt_reset(ScrMockTimers *m); /* ── concurrency ─────────────────────────────────────────────────────── */ @@ -679,8 +683,9 @@ static void scr_test_run_node(ScrTestCtx *t) { scr_exc_clear(); } } - /* Then t.mock is reset: its mocks are restored. */ + /* Then t.mock is reset: its mocks are restored and its timers reset. */ scr_test_mocks_restore(&t->mocks, true); + scr_mt_reset(t->mock_timers); scr_test_parent_set(prev_parent); t->dur_ms = scr_now_ms() - start; if (t->mode == SCR_TEST_SKIP) { @@ -808,6 +813,575 @@ static void scr_test_print_failures(ScrTestCtx *t, bool *any) { } } +/* ── t.assert.snapshot and t.assert.fileSnapshot ─────────────────────── + * Node's SnapshotManager (lib/internal/test_runner/snapshot.js). A + * snapshot is the serialized value, template-escaped and wrapped in + * newlines, stored under " " in ".snapshot". + * Compare mode reads that file on the first assertion and returns the + * stored value, which the emitted assert.strictEqual compares; update + * mode records the value and writes every file, keys sorted, after the + * last test. The file is read in the form Node writes it: + * exports[`key`] = `value`; statements of template literals. */ + +typedef struct { + ScrStr *key; /* the snapshot id (cooked, as Node's exports keys) */ + ScrStr *value; /* the escaped, wrapped value */ +} ScrSnapEntry; + +typedef struct ScrSnapFile { + ScrStr *path; + ScrSnapEntry *entries; + size_t n, cap; + ScrStr **names; /* nextId's counters: test full names ... */ + long *counts; /* ... and the next number for each */ + size_t nnames, ncap; + bool loaded; + struct ScrSnapFile *next; +} ScrSnapFile; + +static SCR_TL ScrSnapFile *scr_snap_files = NULL; +/* Node's --test-update-snapshots: a leading argument of the binary, + * which scr_test_init takes out of process.argv. */ +static SCR_TL bool scr_test_update_snapshots = false; + +void scr_test_init(void) { + scr_test_update_snapshots = scr_lib_take_flag("--test-update-snapshots"); +} + +static void scr_snap_throw_state(const char *text, size_t len) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, "Invalid state: "); + scr_jb_write(&b, text, len); + ScrStr *msg = scr_jb_finish(&b); + scr_throw_error_msg_code(SCR_ERR_ERROR, msg->data, msg->len, "ERR_INVALID_STATE"); + scr_str_release(msg); +} + +/* "Cannot read snapshot file '.'", with the tip when it is missing. */ +static void scr_snap_throw_file(const char *verb, const ScrStr *file, bool missing) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, "Cannot "); + scr_jb_puts(&b, verb); + scr_jb_puts(&b, " snapshot file '"); + scr_jb_put_str(&b, file); + scr_jb_puts(&b, ".'"); + if (missing) + scr_jb_puts(&b, " Missing snapshots can be generated by rerunning the command with the " + "--test-update-snapshots flag."); + ScrStr *msg = scr_jb_finish(&b); + scr_snap_throw_state(msg->data, msg->len); + scr_str_release(msg); +} + +/* Node's templateEscape: \ ` and ${ escaped for a template literal. */ +static void scr_snap_put_escaped(ScrJsonBuf *b, const char *s, size_t n) { + for (size_t i = 0; i < n; i++) { + if (s[i] == '\\') scr_jb_puts(b, "\\\\"); + else if (s[i] == '`') scr_jb_puts(b, "\\`"); + else if (s[i] == '$' && i + 1 < n && s[i + 1] == '{') scr_jb_puts(b, "\\$"); + else scr_jb_putc(b, s[i]); + } +} + +/* Node's serializeValue: each serializer's result feeds the next; with no + * serializers, the default JSON.stringify(value, null, 2). A throw becomes + * Node's ERR_INVALID_STATE. Returns +1, or NULL after a throw. */ +ScrDyn *scr_test_snapshot_serialize(ScrDyn *value, ScrDyn *serializers) { + ScrDyn *v; + if (serializers->kind != SCR_DYN_ARR) { + ScrDyn *undef = scr_dyn_undefined(); + ScrDyn *space = scr_dyn_new_num(2); + v = scr_json_stringify_value(value, undef, space); + scr_dyn_release(undef); + scr_dyn_release(space); + } else { + v = scr_dyn_retain(value); + for (size_t i = 0; v && i < serializers->v.arr.len; i++) { + ScrDyn *next = scr_dyn_call(serializers->v.arr.items[i], &v, 1, "serializer"); + scr_dyn_release(v); + v = next; + } + } + if (v && !scr_exc_pending()) return v; + scr_dyn_release(v); + scr_exc_clear(); + static const char msg[] = "The provided serializers did not generate a string."; + scr_snap_throw_state(msg, sizeof msg - 1); + return NULL; +} + +ScrStr *scr_test_snapshot_value(ScrStr *serialized) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_putc(&b, '\n'); + scr_snap_put_escaped(&b, serialized->data, serialized->len); + scr_jb_putc(&b, '\n'); + return scr_jb_finish(&b); +} + +static ScrSnapFile *scr_snap_file(ScrStr *path) { + for (ScrSnapFile *f = scr_snap_files; f; f = f->next) { + if (f->path->len == path->len && memcmp(f->path->data, path->data, path->len) == 0) return f; + } + ScrSnapFile *f = calloc(1, sizeof *f); + if (!f) scr_test_oom(); + f->path = scr_str_retain(path); + f->loaded = scr_test_update_snapshots; + f->next = scr_snap_files; + scr_snap_files = f; + return f; +} + +/* nextId(name): " ", counting from 1 per name. */ +static ScrStr *scr_snap_next_id(ScrSnapFile *f, ScrStr *name) { + size_t i = 0; + while (i < f->nnames && + !(f->names[i]->len == name->len && memcmp(f->names[i]->data, name->data, name->len) == 0)) + i++; + if (i == f->nnames) { + if (f->nnames == f->ncap) { + f->ncap = f->ncap ? f->ncap * 2 : 8; + f->names = realloc(f->names, f->ncap * sizeof *f->names); + f->counts = realloc(f->counts, f->ncap * sizeof *f->counts); + if (!f->names || !f->counts) scr_test_oom(); + } + f->names[i] = scr_str_retain(name); + f->counts[i] = 1; + f->nnames++; + } + char num[24]; + int len = snprintf(num, sizeof num, " %ld", f->counts[i]++); + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_put_str(&b, name); + scr_jb_write(&b, num, (size_t)len); + return scr_jb_finish(&b); +} + +static ScrSnapEntry *scr_snap_find(ScrSnapFile *f, const ScrStr *key) { + for (size_t i = 0; i < f->n; i++) { + const ScrStr *k = f->entries[i].key; + if (k->len == key->len && memcmp(k->data, key->data, key->len) == 0) return &f->entries[i]; + } + return NULL; +} + +/* Sets key to value (both moved), replacing an earlier entry. */ +static void scr_snap_set(ScrSnapFile *f, ScrStr *key, ScrStr *value) { + ScrSnapEntry *e = scr_snap_find(f, key); + if (e) { + scr_str_release(key); + scr_str_release(e->value); + e->value = value; + return; + } + if (f->n == f->cap) { + f->cap = f->cap ? f->cap * 2 : 8; + f->entries = realloc(f->entries, f->cap * sizeof *f->entries); + if (!f->entries) scr_test_oom(); + } + f->entries[f->n].key = key; + f->entries[f->n].value = value; + f->n++; +} + +/* Appends the UTF-8 encoding of code point `cp`. */ +static void scr_snap_put_cp(ScrJsonBuf *b, unsigned long cp) { + char u[4]; + size_t n; + if (cp < 0x80) { + u[0] = (char)cp; + n = 1; + } else if (cp < 0x800) { + u[0] = (char)(0xC0 | (cp >> 6)); + u[1] = (char)(0x80 | (cp & 0x3F)); + n = 2; + } else if (cp < 0x10000) { + u[0] = (char)(0xE0 | (cp >> 12)); + u[1] = (char)(0x80 | ((cp >> 6) & 0x3F)); + u[2] = (char)(0x80 | (cp & 0x3F)); + n = 3; + } else { + u[0] = (char)(0xF0 | (cp >> 18)); + u[1] = (char)(0x80 | ((cp >> 12) & 0x3F)); + u[2] = (char)(0x80 | ((cp >> 6) & 0x3F)); + u[3] = (char)(0x80 | (cp & 0x3F)); + n = 4; + } + scr_jb_write(b, u, n); +} + +static int scr_snap_hex(char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +/* Reads `count` hex digits at s[*i]. */ +static bool scr_snap_hex_run(const char *s, size_t n, size_t *i, int count, unsigned long *out) { + unsigned long v = 0; + for (int k = 0; k < count; k++) { + if (*i >= n) return false; + int h = scr_snap_hex(s[*i]); + if (h < 0) return false; + v = v * 16 + (unsigned long)h; + (*i)++; + } + *out = v; + return true; +} + +/* The cooked value of the template literal whose body starts at s[*i], + * past its opening backtick; *i ends past the closing one. Returns NULL + * for an unterminated literal, an invalid escape, or a substitution. */ +static ScrStr *scr_snap_template(const char *s, size_t n, size_t *i) { + ScrJsonBuf b; + scr_jb_init(&b); + while (*i < n) { + char c = s[(*i)++]; + if (c == '`') return scr_jb_finish(&b); + if (c == '$' && *i < n && s[*i] == '{') break; + if (c == '\r') { + /* Template literals cook CR and CRLF to LF. */ + if (*i < n && s[*i] == '\n') (*i)++; + scr_jb_putc(&b, '\n'); + continue; + } + if (c != '\\') { + scr_jb_putc(&b, c); + continue; + } + if (*i >= n) break; + char e = s[(*i)++]; + unsigned long cp; + switch (e) { + case 'n': + scr_jb_putc(&b, '\n'); + continue; + case 't': + scr_jb_putc(&b, '\t'); + continue; + case 'r': + scr_jb_putc(&b, '\r'); + continue; + case 'b': + scr_jb_putc(&b, '\b'); + continue; + case 'f': + scr_jb_putc(&b, '\f'); + continue; + case 'v': + scr_jb_putc(&b, '\v'); + continue; + case '0': + if (*i < n && s[*i] >= '0' && s[*i] <= '9') goto bad; + scr_jb_putc(&b, '\0'); + continue; + case '\r': + if (*i < n && s[*i] == '\n') (*i)++; + continue; /* line continuation */ + case '\n': + continue; + case 'x': + if (!scr_snap_hex_run(s, n, i, 2, &cp)) goto bad; + scr_snap_put_cp(&b, cp); + continue; + case 'u': + if (*i < n && s[*i] == '{') { + (*i)++; + cp = 0; + size_t start = *i; + while (*i < n && s[*i] != '}') { + int h = scr_snap_hex(s[*i]); + if (h < 0 || cp > 0x10FFFF) goto bad; + cp = cp * 16 + (unsigned long)h; + (*i)++; + } + if (*i >= n || *i == start || cp > 0x10FFFF) goto bad; + (*i)++; + } else if (!scr_snap_hex_run(s, n, i, 4, &cp)) { + goto bad; + } + if (cp >= 0xD800 && cp <= 0xDBFF && *i + 6 <= n && s[*i] == '\\' && s[*i + 1] == 'u') { + size_t j = *i + 2; + unsigned long lo; + if (scr_snap_hex_run(s, n, &j, 4, &lo) && lo >= 0xDC00 && lo <= 0xDFFF) { + cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00); + *i = j; + } + } + scr_snap_put_cp(&b, cp); + continue; + default: + if (e >= '1' && e <= '9') goto bad; + scr_jb_putc(&b, e); /* \\ \` \$ and identity escapes */ + continue; + } + } +bad: + scr_str_release(scr_jb_finish(&b)); + return NULL; +} + +static void scr_snap_skip_space(const char *s, size_t n, size_t *i) { + while (*i < n && (s[*i] == ' ' || s[*i] == '\t' || s[*i] == '\n' || s[*i] == '\r')) (*i)++; +} + +static bool scr_snap_expect(const char *s, size_t n, size_t *i, const char *text) { + size_t len = strlen(text); + if (*i + len > n || memcmp(s + *i, text, len) != 0) return false; + *i += len; + return true; +} + +/* Parses the file's exports[`key`] = `value`; statements. Values are + * escaped again, as Node escapes the exports it read. */ +static bool scr_snap_parse(ScrSnapFile *f, const char *s, size_t n) { + size_t i = 0; + for (;;) { + scr_snap_skip_space(s, n, &i); + if (i >= n) return true; + if (!scr_snap_expect(s, n, &i, "exports[`")) return false; + ScrStr *key = scr_snap_template(s, n, &i); + if (!key) return false; + scr_snap_skip_space(s, n, &i); + bool ok = scr_snap_expect(s, n, &i, "]"); + scr_snap_skip_space(s, n, &i); + ok = ok && scr_snap_expect(s, n, &i, "="); + scr_snap_skip_space(s, n, &i); + ok = ok && scr_snap_expect(s, n, &i, "`"); + ScrStr *raw = ok ? scr_snap_template(s, n, &i) : NULL; + if (!raw) { + scr_str_release(key); + return false; + } + scr_snap_skip_space(s, n, &i); + (void)scr_snap_expect(s, n, &i, ";"); + ScrJsonBuf b; + scr_jb_init(&b); + scr_snap_put_escaped(&b, raw->data, raw->len); + scr_str_release(raw); + scr_snap_set(f, key, scr_jb_finish(&b)); + } +} + +static bool scr_snap_read(ScrSnapFile *f) { + if (f->loaded) return true; + FILE *fp = fopen(f->path->data, "rb"); + if (!fp) { + scr_snap_throw_file("read", f->path, errno == ENOENT); + return false; + } + ScrJsonBuf b; + scr_jb_init(&b); + char chunk[8192]; + size_t got; + while ((got = fread(chunk, 1, sizeof chunk, fp)) > 0) scr_jb_write(&b, chunk, got); + bool read_ok = !ferror(fp); + fclose(fp); + ScrStr *text = scr_jb_finish(&b); + bool ok = read_ok && scr_snap_parse(f, text->data, text->len); + scr_str_release(text); + if (!ok) { + scr_snap_throw_file("read", f->path, false); + return false; + } + f->loaded = true; + return true; +} + +/* Node's escapeSnapshotKey: the JSON string body of the id, with ` and + * ${ escaped (+1). Update mode stores and sorts keys in this form. */ +static ScrStr *scr_snap_escape_key(const ScrStr *key) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_put_json_str(&b, key); + ScrStr *json = scr_jb_finish(&b); + const char *s = json->data + 1; + size_t n = json->len - 2; + ScrJsonBuf out; + scr_jb_init(&out); + for (size_t i = 0; i < n; i++) { + if (s[i] == '`') scr_jb_puts(&out, "\\`"); + else if (s[i] == '$' && i + 1 < n && s[i + 1] == '{') scr_jb_puts(&out, "\\$"); + else scr_jb_putc(&out, s[i]); + } + scr_str_release(json); + return scr_jb_finish(&out); +} + +/* t.assert.snapshot: the stored value for the test's next id (+1), or the + * recorded `value` in update mode. NULL after a throw. */ +ScrStr *scr_test_ctx_snapshot(ScrTestCtx *t, ScrStr *value, ScrStr *file) { + ScrSnapFile *f = scr_snap_file(file); + ScrStr *name = scr_test_ctx_full_name(t); + ScrStr *id = scr_snap_next_id(f, name); + scr_str_release(name); + if (scr_test_update_snapshots) { + scr_snap_set(f, scr_snap_escape_key(id), scr_str_retain(value)); + scr_str_release(id); + return scr_str_retain(value); + } + if (!scr_snap_read(f)) { + scr_str_release(id); + return NULL; + } + ScrSnapEntry *e = scr_snap_find(f, id); + if (!e) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, "Snapshot '"); + scr_jb_put_str(&b, id); + scr_jb_puts(&b, "' not found in '"); + scr_jb_put_str(&b, f->path); + scr_jb_puts(&b, ".' Missing snapshots can be generated by rerunning the command with the " + "--test-update-snapshots flag."); + ScrStr *msg = scr_jb_finish(&b); + scr_snap_throw_state(msg->data, msg->len); + scr_str_release(msg); + scr_str_release(id); + return NULL; + } + scr_str_release(id); + return scr_str_retain(e->value); +} + +static bool scr_snap_write_text(const ScrStr *path, const char *data, size_t len) { + FILE *fp = fopen(path->data, "wb"); + if (!fp) return false; + bool ok = fwrite(data, 1, len, fp) == len; + return fclose(fp) == 0 && ok; +} + +/* mkdirSync(dirname(path), { recursive: true }); false after an error. */ +static bool scr_snap_mkdir_parent(const ScrStr *path) { + size_t end = path->len; + while (end > 0 && path->data[end - 1] != '/' +#ifdef _WIN32 + && path->data[end - 1] != '\\' +#endif + ) + end--; + if (end <= 1) return true; + ScrStr *dir = scr_str_new(path->data, end - 1); + scr_fs_mkdir_recursive(dir); + scr_str_release(dir); + if (!scr_exc_pending()) return true; + scr_exc_clear(); + return false; +} + +/* t.assert.fileSnapshot: the file's contents (+1), or the recorded value + * in update mode, which writes the file. NULL after a throw. */ +ScrStr *scr_test_file_snapshot(ScrStr *value, ScrStr *path) { + if (scr_test_update_snapshots) { + if (!scr_snap_mkdir_parent(path) || !scr_snap_write_text(path, value->data, value->len)) { + scr_snap_throw_file("write", path, false); + return NULL; + } + return scr_str_retain(value); + } + FILE *fp = fopen(path->data, "rb"); + if (!fp) { + scr_snap_throw_file("read", path, errno == ENOENT); + return NULL; + } + ScrJsonBuf b; + scr_jb_init(&b); + char chunk[8192]; + size_t got; + while ((got = fread(chunk, 1, sizeof chunk, fp)) > 0) scr_jb_write(&b, chunk, got); + bool ok = !ferror(fp); + fclose(fp); + ScrStr *text = scr_jb_finish(&b); + if (!ok) { + scr_str_release(text); + scr_snap_throw_file("read", path, false); + return NULL; + } + return text; +} + +/* The next UTF-16 code unit of a UTF-8 key: *low carries the low + * surrogate of an astral character to the following call. */ +static unsigned long scr_snap_unit(const ScrStr *s, size_t *i, unsigned long *low) { + if (*low) { + unsigned long u = *low; + *low = 0; + return u; + } + const unsigned char *d = (const unsigned char *)s->data; + unsigned char c = d[*i]; + size_t len = c < 0x80 ? 1 : c < 0xE0 ? 2 : c < 0xF0 ? 3 : 4; + if (*i + len > s->len) len = s->len - *i; + unsigned long cp = len == 1 ? c : len == 2 ? (c & 0x1Fu) : len == 3 ? (c & 0x0Fu) : (c & 0x07u); + for (size_t k = 1; k < len; k++) cp = (cp << 6) | (d[*i + k] & 0x3Fu); + *i += len; + if (cp < 0x10000) return cp; + *low = 0xDC00 + ((cp - 0x10000) & 0x3FF); + return 0xD800 + ((cp - 0x10000) >> 10); +} + +/* Array.prototype.sort's default order: UTF-16 code units. */ +static int scr_snap_key_cmp(const void *a, const void *b) { + const ScrStr *x = ((const ScrSnapEntry *)a)->key, *y = ((const ScrSnapEntry *)b)->key; + size_t i = 0, j = 0; + unsigned long xl = 0, yl = 0; + while ((i < x->len || xl) && (j < y->len || yl)) { + unsigned long u = scr_snap_unit(x, &i, &xl), v = scr_snap_unit(y, &j, &yl); + if (u != v) return u < v ? -1 : 1; + } + bool xmore = i < x->len || xl, ymore = j < y->len || yl; + return xmore == ymore ? 0 : xmore ? 1 : -1; +} + + +/* writeSnapshotFiles(): every snapshot file of the run, in update mode. */ +static void scr_snap_write_all(void) { + if (!scr_test_update_snapshots) return; + for (ScrSnapFile *f = scr_snap_files; f; f = f->next) { + qsort(f->entries, f->n, sizeof *f->entries, scr_snap_key_cmp); + ScrJsonBuf b; + scr_jb_init(&b); + for (size_t i = 0; i < f->n; i++) { + if (i > 0) scr_jb_putc(&b, '\n'); + scr_jb_puts(&b, "exports[`"); + scr_jb_put_str(&b, f->entries[i].key); + scr_jb_puts(&b, "`] = `"); + scr_jb_put_str(&b, f->entries[i].value); + scr_jb_puts(&b, "`;\n"); + } + ScrStr *text = scr_jb_finish(&b); + if (!scr_snap_mkdir_parent(f->path) || !scr_snap_write_text(f->path, text->data, text->len)) { + fflush(stdout); + fprintf(stderr, "Error [ERR_INVALID_STATE]: Invalid state: Cannot write snapshot file '%.*s.'\n", + (int)f->path->len, f->path->data); + scr_test_suite_hook_failed = true; + } + scr_str_release(text); + } +} + +static void scr_snap_free_all(void) { + while (scr_snap_files) { + ScrSnapFile *f = scr_snap_files; + scr_snap_files = f->next; + for (size_t i = 0; i < f->n; i++) { + scr_str_release(f->entries[i].key); + scr_str_release(f->entries[i].value); + } + for (size_t i = 0; i < f->nnames; i++) scr_str_release(f->names[i]); + free(f->entries); + free(f->names); + free(f->counts); + scr_str_release(f->path); + free(f); + } +} + /* ── the runner fiber ────────────────────────────────────────────────── */ static void scr_test_runner_entry(ScrFiber *self, void *argpack) { @@ -819,6 +1393,7 @@ static void scr_test_runner_entry(ScrFiber *self, void *argpack) { ScrTestCtx *root = scr_test_root; scr_test_run_children(root); scr_test_run_done = true; + scr_snap_write_all(); scr_test_count(root); scr_test_info_line("tests", (double)scr_test_ct_tests); scr_test_info_line("suites", (double)scr_test_ct_suites); @@ -1042,16 +1617,529 @@ static void scr_test_mocks_add(ScrTestHooks *h, ScrClosure *restore /*moves*/) { h->n++; } +/* ── mock.timers ────────────────────────────────────────────────────── + * Node's MockTimers (lib/internal/test_runner/mock/mock_timers.js). The + * module's `mock` and every t.mock own one. enable() makes it the owner + * of the APIs it lists: setTimeout, setInterval, setImmediate and their + * clear functions then queue entries on it instead of arming loop timers, + * AbortSignal.timeout aborts from its queue, timers/promises + * scheduler.wait settles from it, Date.now() and new Date() read its + * clock, and tick() runs the due entries synchronously. enable() saves the owner each API had and reset() restores it, as Node + * saves and restores the functions it replaces. The queue is Node's + * binary heap (internal/priority_queue.js) with Node's comparator, so + * entries with equal, negative or NaN run times fire in Node's order. + * Mock handles are negative, which the loop's timer functions ignore. + * + * The scr_test_mt_* entry points replace the timer calls of programs + * that use mock.timers (the moduleUsesMockTimers switch), so other + * programs call the loop's timer functions directly. */ + +/* The APIs enable() mocks, in Node's SUPPORTED_APIS order. */ +enum { + SCR_MT_TIMEOUT, + SCR_MT_INTERVAL, + SCR_MT_IMMEDIATE, + SCR_MT_DATE, + SCR_MT_WAIT, + SCR_MT_ABORT, + SCR_MT_NAPIS +}; + +typedef struct { + double run_at; + double id; /* Node's #currentTimer: the tie-break between equal run times */ + double interval; /* the period of a setInterval entry */ + bool has_interval; /* false for timeouts and cleared intervals (Node's undefined) */ + ScrClosure *cb; /* owned */ + size_t pos; /* heap slot, 0 when unset (Node's priorityQueuePosition) */ + double handle; /* the negative handle the program holds */ + ScrMockTimers *owner; + int refs; /* heap slots holding the entry, plus a running callback */ +} ScrMockTimer; + +struct ScrMockTimers { + bool enabled; + char apis[16]; /* the APIs enable() listed, in order (Node's #timersInContext) */ + size_t napis; + double now; + double next_id; + ScrMockTimer **heap; /* 1-based, as Node's #heap */ + size_t size, cap; + ScrMockTimers *saved[SCR_MT_NAPIS]; /* each API's owner before enable() */ + ScrMockTimers *next_all; +}; + +/* The owner of each API: the MockTimers its calls queue on, NULL for the + * loop's timers. */ +static SCR_TL ScrMockTimers *scr_mt_owner[SCR_MT_NAPIS]; +static SCR_TL ScrMockTimers *scr_mt_module = NULL; +/* Every MockTimers, freed at exit: an owner or saved slot can still name + * one after its test ended, as Node keeps the replaced functions. */ +static SCR_TL ScrMockTimers *scr_mt_all = NULL; +static SCR_TL double scr_mt_next_handle = -1; +/* Set once a MockTimers that mocked scheduler.wait gives the real one + * back: Node restores it bound to the MockTimers, so every later call + * throws ERR_INVALID_THIS. */ +static SCR_TL bool scr_mt_wait_unbound = false; +/* Entries the program can still reach through a handle. */ +static SCR_TL ScrMockTimer **scr_mt_live = NULL; +static SCR_TL size_t scr_mt_nlive = 0, scr_mt_live_cap = 0; + +static ScrMockTimers *scr_mt_new(void) { + ScrMockTimers *m = calloc(1, sizeof *m); + if (!m) scr_test_oom(); + m->next_id = 1; + m->next_all = scr_mt_all; + scr_mt_all = m; + return m; +} + +static ScrMockTimers *scr_mt_of(ScrTestCtx *t) { + ScrMockTimers **slot = t ? &t->mock_timers : &scr_mt_module; + if (!*slot) *slot = scr_mt_new(); + return *slot; +} + +static void scr_mt_unref(ScrMockTimer *e) { + if (--e->refs > 0) return; + for (size_t i = 0; i < scr_mt_nlive; i++) { + if (scr_mt_live[i] == e) { + scr_mt_live[i] = scr_mt_live[--scr_mt_nlive]; + break; + } + } + scr_closure_release(e->cb); + free(e); +} + +static ScrMockTimer *scr_mt_find(double handle) { + if (!(handle < 0)) return NULL; + for (size_t i = 0; i < scr_mt_nlive; i++) { + if (scr_mt_live[i]->handle == handle) return scr_mt_live[i]; + } + return NULL; +} + +/* Node's compareTimersLists: (a.runAt - b.runAt) || (a.id - b.id). */ +static double scr_mt_compare(const ScrMockTimer *a, const ScrMockTimer *b) { + double d = a->run_at - b->run_at; + if (d != 0 && !isnan(d)) return d; + return a->id - b->id; +} + +static void scr_mt_percolate_down(ScrMockTimers *m, size_t pos) { + ScrMockTimer **heap = m->heap; + size_t size = m->size, hsize = size >> 1; + ScrMockTimer *item = heap[pos]; + while (pos <= hsize) { + size_t child = pos << 1, next = child + 1; + ScrMockTimer *child_item = heap[child]; + if (next <= size && scr_mt_compare(heap[next], child_item) < 0) { + child = next; + child_item = heap[next]; + } + if (scr_mt_compare(item, child_item) <= 0) break; + child_item->pos = pos; + heap[pos] = child_item; + pos = child; + } + heap[pos] = item; + item->pos = pos; +} + +static void scr_mt_percolate_up(ScrMockTimers *m, size_t pos) { + ScrMockTimer **heap = m->heap; + ScrMockTimer *item = heap[pos]; + while (pos > 1) { + size_t parent = pos >> 1; + ScrMockTimer *parent_item = heap[parent]; + if (scr_mt_compare(parent_item, item) <= 0) break; + heap[pos] = parent_item; + parent_item->pos = pos; + pos = parent; + } + heap[pos] = item; + item->pos = pos; +} + +static void scr_mt_insert(ScrMockTimers *m, ScrMockTimer *e) { + if (m->size + 2 > m->cap) { + m->cap = m->cap ? m->cap * 2 : 8; + m->heap = realloc(m->heap, m->cap * sizeof *m->heap); + if (!m->heap) scr_test_oom(); + } + e->refs++; + m->heap[++m->size] = e; + scr_mt_percolate_up(m, m->size); +} + +static void scr_mt_remove_at(ScrMockTimers *m, size_t pos) { + ScrMockTimer **heap = m->heap; + ScrMockTimer *removed = heap[pos]; + heap[pos] = heap[m->size]; + heap[m->size] = NULL; + size_t size = --m->size; + if (size > 0 && pos <= size) { + if (pos > 1 && scr_mt_compare(heap[pos >> 1], heap[pos]) > 0) scr_mt_percolate_up(m, pos); + else scr_mt_percolate_down(m, pos); + } + scr_mt_unref(removed); +} + +static ScrMockTimer *scr_mt_peek(ScrMockTimers *m) { return m->size > 0 ? m->heap[1] : NULL; } + +static void scr_mt_throw_state(const char *what) { + char msg[160]; + int len = snprintf(msg, sizeof msg, "Invalid state: %s", what); + scr_throw_error_msg_code(SCR_ERR_ERROR, msg, (size_t)len, "ERR_INVALID_STATE"); +} + +/* Node's #assertTimeArg: ERR_INVALID_ARG_VALUE('time', 'positive integer', time). */ +static bool scr_mt_time_ok(double time) { + if (!(time < 0)) return true; + char num[32]; + size_t n = scr_f64_to_str(time, num); + char msg[96]; + int len = snprintf(msg, sizeof msg, "The argument 'time' %.*s. Received 'positive integer'", + (int)n, num); + scr_throw_error_msg_code(SCR_ERR_TYPE, msg, (size_t)len, "ERR_INVALID_ARG_VALUE"); + return false; +} + +static int scr_mt_api(char code) { + switch (code) { + case 'I': + return SCR_MT_INTERVAL; + case 'M': + return SCR_MT_IMMEDIATE; + case 'D': + return SCR_MT_DATE; + case 'W': + return SCR_MT_WAIT; + case 'A': + return SCR_MT_ABORT; + default: + return SCR_MT_TIMEOUT; + } +} + +/* Node's #createTimer: the delay is used as given, except that one past + * TIMEOUT_MAX becomes 1; an omitted delay arrives as NaN (Node's + * undefined). */ +static ScrMockTimer *scr_mt_create(ScrMockTimers *m, bool is_interval, ScrClosure *cb /*moves*/, + double delay) { + if (delay > 2147483647.0) delay = 1; + ScrMockTimer *e = calloc(1, sizeof *e); + if (!e) scr_test_oom(); + e->id = m->next_id++; + e->cb = cb; + e->run_at = m->now + delay; + e->has_interval = is_interval && !isnan(delay); + e->interval = delay; + e->handle = scr_mt_next_handle--; + e->owner = m; + if (scr_mt_nlive == scr_mt_live_cap) { + scr_mt_live_cap = scr_mt_live_cap ? scr_mt_live_cap * 2 : 8; + scr_mt_live = realloc(scr_mt_live, scr_mt_live_cap * sizeof *scr_mt_live); + if (!scr_mt_live) scr_test_oom(); + } + scr_mt_live[scr_mt_nlive++] = e; + scr_mt_insert(m, e); + return e; +} + +/* Node's #clearTimer on the API's owner. A handle the owner did not + * create is left alone. */ +static void scr_mt_clear(ScrMockTimers *m, double handle) { + ScrMockTimer *e = scr_mt_find(handle); + if (!e || e->owner != m || e->pos == 0) return; + size_t pos = e->pos; + e->pos = 0; + e->has_interval = false; + scr_mt_remove_at(m, pos); +} + +static void scr_mt_tick(ScrMockTimers *m, double time) { + if (!m->enabled) { + scr_mt_throw_state("You should enable MockTimers first by calling the .enable function"); + return; + } + if (!scr_mt_time_ok(time)) return; + m->now += time; + ScrMockTimer *timer = scr_mt_peek(m); + while (timer) { + if (timer->run_at > m->now) break; + timer->refs++; + ((void (*)(ScrClosure *))timer->cb->fn)(timer->cb); + if (scr_exc_pending()) { + /* The throw leaves the entry queued, as in Node. */ + scr_mt_unref(timer); + return; + } + /* Node's tick() removes the fired entry only while it is still first + * in the queue: a callback that cleared its own timer already removed + * it, and one that queued an entry sorting before it (an immediate, + * due at now - 1) leaves it queued, so it runs again, as in Node. The + * mock-timers-date fixture checks that order against Node. */ + ScrMockTimer *after = scr_mt_peek(m); + if (after && after->id == timer->id) { + timer->pos = 0; + scr_mt_remove_at(m, 1); + } + if (timer->has_interval) { + timer->run_at += timer->interval; + scr_mt_insert(m, timer); + } + scr_mt_unref(timer); + timer = scr_mt_peek(m); + } +} + +static void scr_mt_abort_arm(ScrClosure *cb /*moves*/, double ms) { + (void)scr_mt_create(scr_mt_owner[SCR_MT_ABORT], false, cb, ms); +} + +/* Points the Date, AbortSignal.timeout and scheduler.wait hooks at their + * owners. */ +static void scr_mt_hooks(void) { + ScrMockTimers *date = scr_mt_owner[SCR_MT_DATE]; + scr_date_mock_now = date ? &date->now : NULL; + scr_mt_abort_timeout_arm = scr_mt_owner[SCR_MT_ABORT] ? &scr_mt_abort_arm : NULL; + scr_mt_wait_state = scr_mt_owner[SCR_MT_WAIT] ? 1 : scr_mt_wait_unbound ? 2 : 0; +} + +/* Node's #toggleEnableTimers. Mocking Date while another MockTimers mocks + * it throws ERR_INVALID_STATE; the APIs listed before Date stay mocked, + * and the MockTimers stays disabled, as in Node. */ +static bool scr_mt_toggle(ScrMockTimers *m, bool activate) { + for (size_t i = 0; i < m->napis; i++) { + int api = scr_mt_api(m->apis[i]); + if (activate) { + if (api == SCR_MT_DATE && scr_mt_owner[api] != NULL) { + scr_mt_hooks(); + scr_mt_throw_state("Date is already being mocked!"); + return false; + } + m->saved[api] = scr_mt_owner[api]; + scr_mt_owner[api] = m; + } else { + scr_mt_owner[api] = m->saved[api]; + if (api == SCR_MT_WAIT && scr_mt_owner[api] == NULL) scr_mt_wait_unbound = true; + } + } + scr_mt_hooks(); + m->enabled = activate; + return true; +} + +static void scr_mt_reset(ScrMockTimers *m) { + if (!m || !m->enabled) return; + scr_mt_toggle(m, false); + m->napis = 0; + m->now = 0; + while (m->size > 0) scr_mt_remove_at(m, 1); +} + +/* enable({ apis, now }): `apis` holds one code per listed API, in order + * ('T' setTimeout, 'I' setInterval, 'M' setImmediate, 'D' Date, 'W' + * scheduler.wait, 'A' AbortSignal.timeout). flags: 1 = `now` is given, 2 = it + * came from a Date (Node then takes its time without checks). */ +static void scr_mt_enable(ScrMockTimers *m, ScrStr *apis, double now, double flags) { + if (m->enabled) { + scr_mt_throw_state("MockTimers is already enabled!"); + return; + } + bool given = ((int)flags & 1) != 0, from_date = ((int)flags & 2) != 0; + if (given && !from_date && isnan(now)) { + static const char msg[] = + "The argument 'now' epoch must be a positive integer received NaN. Received NaN"; + scr_throw_error_msg_code(SCR_ERR_TYPE, msg, sizeof msg - 1, "ERR_INVALID_ARG_VALUE"); + return; + } + if (!given || (!from_date && now == 0)) now = 0; + m->napis = apis->len < sizeof m->apis ? apis->len : sizeof m->apis; + memcpy(m->apis, apis->data, m->napis); + if (!from_date && !scr_mt_time_ok(now)) return; + m->now = now; + (void)scr_mt_toggle(m, true); +} + +void scr_test_mock_timers_enable(ScrStr *apis, double now, double flags) { + scr_mt_enable(scr_mt_of(NULL), apis, now, flags); +} +void scr_test_ctx_mock_timers_enable(ScrTestCtx *t, ScrStr *apis, double now, double flags) { + scr_mt_enable(scr_mt_of(t), apis, now, flags); +} +void scr_test_mock_timers_tick(double time) { scr_mt_tick(scr_mt_of(NULL), time); } +void scr_test_ctx_mock_timers_tick(ScrTestCtx *t, double time) { scr_mt_tick(scr_mt_of(t), time); } + +static void scr_mt_run_all(ScrMockTimers *m) { + if (!m->enabled) { + scr_mt_throw_state("You should enable MockTimers first by calling the .enable function"); + return; + } + /* Node ticks to the entry in the heap's last slot. */ + if (m->size == 0) return; + scr_mt_tick(m, m->heap[m->size]->run_at - m->now); +} +void scr_test_mock_timers_run_all(void) { scr_mt_run_all(scr_mt_of(NULL)); } +void scr_test_ctx_mock_timers_run_all(ScrTestCtx *t) { scr_mt_run_all(scr_mt_of(t)); } +void scr_test_mock_timers_reset(void) { scr_mt_reset(scr_mt_module); } +void scr_test_ctx_mock_timers_reset(ScrTestCtx *t) { scr_mt_reset(t->mock_timers); } + +/* setTime(time): Node checks the time before whether timers are enabled. */ +static void scr_mt_set_time(ScrMockTimers *m, double time) { + if (!scr_mt_time_ok(time)) return; + if (!m->enabled) { + scr_mt_throw_state("You should enable MockTimers first by calling the .enable function"); + return; + } + m->now = time; +} +void scr_test_mock_timers_set_time(double time) { scr_mt_set_time(scr_mt_of(NULL), time); } +void scr_test_ctx_mock_timers_set_time(ScrTestCtx *t, double time) { + scr_mt_set_time(scr_mt_of(t), time); +} + +/* The timer calls of a program that uses mock.timers: an owned API + * queues on its owner, else the loop's timer functions run. */ +void scr_test_mt_set_timeout(ScrClosure *cb /*moves*/, double ms) { + ScrMockTimers *m = scr_mt_owner[SCR_MT_TIMEOUT]; + if (m) (void)scr_mt_create(m, false, cb, ms); + else scr_set_timeout(cb, ms); +} +double scr_test_mt_set_timeout_handle(ScrClosure *cb /*moves*/, double ms) { + ScrMockTimers *m = scr_mt_owner[SCR_MT_TIMEOUT]; + return m ? scr_mt_create(m, false, cb, ms)->handle : scr_set_timeout_handle(cb, ms); +} +double scr_test_mt_set_interval(ScrClosure *cb /*moves*/, double ms) { + ScrMockTimers *m = scr_mt_owner[SCR_MT_INTERVAL]; + return m ? scr_mt_create(m, true, cb, ms)->handle : scr_set_interval(cb, ms); +} +void scr_test_mt_clear_timeout(double handle) { + ScrMockTimers *m = scr_mt_owner[SCR_MT_TIMEOUT]; + if (m) scr_mt_clear(m, handle); + else scr_clear_interval(handle); +} +void scr_test_mt_clear_interval(double handle) { + ScrMockTimers *m = scr_mt_owner[SCR_MT_INTERVAL]; + if (m) scr_mt_clear(m, handle); + else scr_clear_interval(handle); +} +/* A mock Timeout's ref() and unref() return it unchanged, refresh() does + * nothing, and hasRef() is true. */ +void scr_test_mt_unref(double handle) { + if (!(handle < 0)) scr_timer_unref(handle); +} +void scr_test_mt_ref(double handle) { + if (!(handle < 0)) scr_timer_ref(handle); +} +void scr_test_mt_refresh(double handle) { + if (!(handle < 0)) scr_timer_refresh(handle); +} +bool scr_test_mt_has_ref(double handle) { return handle < 0 || scr_timer_has_ref(handle); } +/* Node's mocked setImmediate queues a timer with delay -1, so it runs + * before timers due at the same tick; its handle is a mock Timeout. */ +double scr_test_mt_set_immediate(ScrClosure *cb /*moves*/) { + ScrMockTimers *m = scr_mt_owner[SCR_MT_IMMEDIATE]; + return m ? scr_mt_create(m, false, cb, -1)->handle : scr_set_immediate(cb); +} +void scr_test_mt_clear_immediate(double handle) { + ScrMockTimers *m = scr_mt_owner[SCR_MT_IMMEDIATE]; + if (m) scr_mt_clear(m, handle); + else scr_clear_immediate(handle); +} +void scr_test_mt_immediate_unref(double handle) { + if (!(handle < 0)) scr_immediate_unref(handle); +} +void scr_test_mt_immediate_ref(double handle) { + if (!(handle < 0)) scr_immediate_ref(handle); +} +bool scr_test_mt_immediate_has_ref(double handle) { + return handle < 0 || scr_immediate_has_ref(handle); +} +/* timers/promises scheduler.wait(delay). Node's mock settles the result + * from an async function that awaits an inner promise the entry + * resolves, so the result settles one microtask after the entry fires. + * The entry's closure holds the result promise until it first fires. */ +static void scr_mt_wait_fire(ScrClosure *self) { + ScrPromise *p = (ScrPromise *)scr_box_get_ref(self->caps[0]); + if (!p) return; + scr_box_set_ref(self->caps[0], NULL); + scr_queue_microtask(scr_make_resolve(p, 3 /* void */)); + scr_promise_release(p); +} + +static bool scr_mt_wait_bound(void) { + if (!scr_mt_wait_unbound) return true; + static const char msg[] = "Value of \"this\" must be of type Scheduler"; + scr_throw_error_msg_code(SCR_ERR_TYPE, msg, sizeof msg - 1, "ERR_INVALID_THIS"); + return false; +} + +static ScrPromise *scr_mt_wait(ScrMockTimers *m, double ms) { + ScrPromise *p = scr_promise_new(); + ScrClosure *c = scr_closure_new((void *)&scr_mt_wait_fire, 1); + ScrBox *b = scr_box_new_obj(scr_promise_retain_v, scr_promise_release_v, scr_promise_trace_v); + scr_box_set_ref(b, scr_promise_retain(p)); + c->caps[0] = b; + (void)scr_mt_create(m, false, c, ms); + return p; +} + +ScrPromise *scr_test_mt_scheduler_wait(double ms) { + ScrMockTimers *m = scr_mt_owner[SCR_MT_WAIT]; + if (m) return scr_mt_wait(m, ms); + return scr_mt_wait_bound() ? scr_tp_set_timeout(ms) : NULL; +} + +/* scheduler.wait(delay, { ref }): Node's mock ignores ref. */ +ScrPromise *scr_test_mt_scheduler_wait_value(double ms, ScrPromise *value, bool ref) { + ScrMockTimers *m = scr_mt_owner[SCR_MT_WAIT]; + if (m) return scr_mt_wait(m, ms); + return scr_mt_wait_bound() ? scr_tp_set_timeout_value(ms, value, ref) : NULL; +} + +/* Date.now() and new Date() under a mocked Date read its clock. */ +double scr_test_mt_date_now(void) { + ScrMockTimers *m = scr_mt_owner[SCR_MT_DATE]; + return m ? m->now : scr_date_now(); +} + +/* Releases every MockTimers and its queued entries at exit. */ +static void scr_mt_free_all(void) { + while (scr_mt_all) { + ScrMockTimers *m = scr_mt_all; + scr_mt_all = m->next_all; + while (m->size > 0) scr_mt_remove_at(m, 1); + free(m->heap); + free(m); + } + free(scr_mt_live); + scr_mt_live = NULL; + scr_mt_nlive = scr_mt_live_cap = 0; + for (int i = 0; i < SCR_MT_NAPIS; i++) scr_mt_owner[i] = NULL; + scr_mt_hooks(); + scr_mt_module = NULL; +} + void scr_test_mock_track(ScrClosure *restore /*moves*/) { scr_test_mocks_add(&scr_test_mocks, restore); } void scr_test_mock_restore_all(void) { scr_test_mocks_restore(&scr_test_mocks, false); } -void scr_test_mock_reset(void) { scr_test_mocks_restore(&scr_test_mocks, true); } +/* reset() restores the tracker's mocks and resets its timers. */ +void scr_test_mock_reset(void) { + scr_test_mocks_restore(&scr_test_mocks, true); + scr_mt_reset(scr_mt_module); +} void scr_test_ctx_mock_track(ScrTestCtx *t, ScrClosure *restore /*moves*/) { scr_test_mocks_add(&t->mocks, restore); } void scr_test_ctx_mock_restore_all(ScrTestCtx *t) { scr_test_mocks_restore(&t->mocks, false); } -void scr_test_ctx_mock_reset(ScrTestCtx *t) { scr_test_mocks_restore(&t->mocks, true); } +void scr_test_ctx_mock_reset(ScrTestCtx *t) { + scr_test_mocks_restore(&t->mocks, true); + scr_mt_reset(t->mock_timers); +} void scr_test_ctx_plan(ScrTestCtx *t, double count) { t->plan = (int)count; } @@ -1098,7 +2186,11 @@ ScrStr *scr_test_ctx_full_name(ScrTestCtx *t) { int scr_test_exit_code(void) { /* The program is done: the module tracker's restore closures go. */ - if (scr_exit_frees()) scr_test_hooks_free(&scr_test_mocks); + if (scr_exit_frees()) { + scr_test_hooks_free(&scr_test_mocks); + scr_mt_free_all(); + scr_snap_free_all(); + } /* The runner never finished: a test awaited a promise nobody settles * and the loop exhausted (Node cancels the test and exits 1 — the * summary it prints there is not reproduced; SEMANTICS.md). The run diff --git a/packages/runtime/src/scr_test.h b/packages/runtime/src/scr_test.h index e1d97e12c5..b1078cccd6 100644 --- a/packages/runtime/src/scr_test.h +++ b/packages/runtime/src/scr_test.h @@ -57,6 +57,58 @@ void scr_test_mock_reset(void); void scr_test_ctx_mock_track(ScrTestCtx *t, ScrClosure *restore); void scr_test_ctx_mock_restore_all(ScrTestCtx *t); void scr_test_ctx_mock_reset(ScrTestCtx *t); +/* Called by node:test programs right after scr_lib_init: a leading + * --test-update-snapshots argument, Node's flag, turns on snapshot update + * mode and moves from process.argv to process.execArgv. */ +void scr_test_init(void); +/* t.assert.snapshot and t.assert.fileSnapshot. snapshot_value escapes + * and wraps a serialized value (+1); ctx_snapshot returns the stored value + * for the test's next snapshot id in `file` and file_snapshot the + * contents of `path` (+1), or the recorded value in update mode. Both + * throw Node's ERR_INVALID_STATE for missing or unreadable snapshots and + * return NULL then. */ +ScrStr *scr_test_snapshot_value(ScrStr *serialized); +ScrDyn *scr_test_snapshot_serialize(ScrDyn *value, ScrDyn *serializers); +ScrStr *scr_test_ctx_snapshot(ScrTestCtx *t, ScrStr *value, ScrStr *file); +ScrStr *scr_test_file_snapshot(ScrStr *value, ScrStr *path); +/* mock.timers on the module's `mock` and on t.mock: enable({ apis, now }) + * (`apis` one code per listed API: 'T' setTimeout, 'I' setInterval, 'M' + * setImmediate, 'D' Date, 'W' scheduler.wait, 'A' AbortSignal.timeout; + * flags 1 = `now` given, 2 = it came from a Date), tick, runAll, reset + * and setTime. enable, tick, runAll and setTime throw Node's + * ERR_INVALID_STATE and ERR_INVALID_ARG_VALUE errors, and tick and runAll + * run the due timer callbacks, whose throws propagate. */ +void scr_test_mock_timers_enable(ScrStr *apis, double now, double flags); +void scr_test_ctx_mock_timers_enable(ScrTestCtx *t, ScrStr *apis, double now, double flags); +void scr_test_mock_timers_tick(double time); +void scr_test_ctx_mock_timers_tick(ScrTestCtx *t, double time); +void scr_test_mock_timers_run_all(void); +void scr_test_ctx_mock_timers_run_all(ScrTestCtx *t); +void scr_test_mock_timers_reset(void); +void scr_test_ctx_mock_timers_reset(ScrTestCtx *t); +void scr_test_mock_timers_set_time(double time); +void scr_test_ctx_mock_timers_set_time(ScrTestCtx *t, double time); +/* The timer calls of programs that use mock.timers (moduleUsesMockTimers): + * each queues on the API's MockTimers owner when it has one, else calls + * the loop's function of the same shape. */ +void scr_test_mt_set_timeout(ScrClosure *cb, double ms); +double scr_test_mt_set_timeout_handle(ScrClosure *cb, double ms); +double scr_test_mt_set_interval(ScrClosure *cb, double ms); +void scr_test_mt_clear_timeout(double handle); +void scr_test_mt_clear_interval(double handle); +void scr_test_mt_unref(double handle); +void scr_test_mt_ref(double handle); +void scr_test_mt_refresh(double handle); +bool scr_test_mt_has_ref(double handle); +double scr_test_mt_set_immediate(ScrClosure *cb); +void scr_test_mt_clear_immediate(double handle); +void scr_test_mt_immediate_unref(double handle); +void scr_test_mt_immediate_ref(double handle); +bool scr_test_mt_immediate_has_ref(double handle); +double scr_test_mt_date_now(void); +/* timers/promises scheduler.wait(delay) (+1 promise; NULL after a throw). */ +ScrPromise *scr_test_mt_scheduler_wait(double ms); +ScrPromise *scr_test_mt_scheduler_wait_value(double ms, ScrPromise *value, bool ref); /* Main's epilogue (emitted when moduleUsesNodeTest): 1 when any non-todo * test failed, Node's exit contract. */ int scr_test_exit_code(void); diff --git a/packages/runtime/src/scr_url.c b/packages/runtime/src/scr_url.c index b6fdb9cd6e..eeadba98fc 100644 --- a/packages/runtime/src/scr_url.c +++ b/packages/runtime/src/scr_url.c @@ -1686,9 +1686,10 @@ static ScrDyn *scr_native_sp_step(void *ptr, double *index, unsigned selection) } else value = scr_dyn_new_str(selection == 1 ? key : text); scr_str_release(key); scr_str_release(text); } + /* { value, done }, in the iterator result's own key order. */ ScrDyn *result = scr_dyn_new_obj(); - scr_dyn_obj_set(result, "done", 4, scr_dyn_new_bool(done)); scr_dyn_obj_set(result, "value", 5, value); + scr_dyn_obj_set(result, "done", 4, scr_dyn_new_bool(done)); return result; } diff --git a/packages/runtime/src/scr_util_callable.c b/packages/runtime/src/scr_util_callable.c index a005015d65..f5ef488dc7 100644 --- a/packages/runtime/src/scr_util_callable.c +++ b/packages/runtime/src/scr_util_callable.c @@ -610,7 +610,7 @@ static ScrStr *uc_format(ScrDyn *const *args, size_t argc, bool colors) { ScrStr *piece = NULL; switch (next) { case 's': piece = uc_format_s(args[++a]); break; - case 'j': piece = scr_dyn_format_j(args[++a]); break; + case 'j': piece = scr_util_format_j(args[++a]); break; case 'd': case 'i': case 'f': piece = uc_format_number(args[++a], next); break; diff --git a/packages/runtime/src/scr_worker.c b/packages/runtime/src/scr_worker.c index a64c6c6e69..67f4ec0846 100644 --- a/packages/runtime/src/scr_worker.c +++ b/packages/runtime/src/scr_worker.c @@ -10,6 +10,7 @@ #include #else #include +#include #endif #if defined(__linux__) @@ -60,7 +61,11 @@ struct ScrWorkerThread { ScrMailbox *inbox; ScrMailbox *parent; ScrMessage *data; + /* data is [workerData, entries] when the parent had environment data */ + bool environment_data; ScrContextEnv *environment; + ScrStr *name; /* threadName: immortal copy shared by both threads */ + unsigned capture; /* 1 stdout, 2 stderr: writes go to the parent as events */ int argc; char **argv; bool joined; @@ -75,6 +80,7 @@ struct ScrWorkerThread { static atomic_uint_fast64_t scr_worker_next_id = 1; static SCR_TL ScrWorkerThread *scr_worker_current; static SCR_TL ScrDyn *scr_worker_local_data; +static SCR_TL ScrDyn *scr_worker_local_environment; /* SCRIPTC_WORKER_STATS=1 reports, as each worker thread exits, how long it * took to start, to run its script (runtime and module initialization @@ -115,6 +121,7 @@ void scr_worker_thread_release(ScrWorkerThread *worker) { scr_mailbox_release(worker->parent); scr_message_free(worker->data); scr_context_env_free(worker->environment); + free(worker->name); for (int i = 0; i < worker->argc; i++) free(worker->argv[i]); free(worker->argv); #ifdef SCR_EXC_FLAG @@ -125,23 +132,46 @@ void scr_worker_thread_release(ScrWorkerThread *worker) { static void scr_worker_data_cleanup(void) { ScrDyn *data = scr_worker_local_data; + ScrDyn *environment = scr_worker_local_environment; scr_worker_local_data = NULL; + scr_worker_local_environment = NULL; scr_dyn_release(data); + scr_dyn_release(environment); +} + +/* Decodes the start message once: workerData and, when the parent had + * environment data, its entries. */ +static bool scr_worker_decode_start(void) { + if (scr_worker_local_data) return true; + ScrDyn *data = scr_message_decode(scr_worker_current->data); + if (!data) return false; + if (scr_worker_current->environment_data) { + scr_worker_local_environment = scr_dyn_arr_at(data, 1); + ScrDyn *value = scr_dyn_arr_at(data, 0); + scr_dyn_release(data); + data = value; + } + scr_worker_local_data = data; + if (scr_context_atexit(scr_worker_data_cleanup) != 0) scr_trap("scriptc: out of memory\n"); + scr_message_free(scr_worker_current->data); + scr_worker_current->data = NULL; + return true; } ScrDyn *scr_worker_data(void) { if (!scr_worker_current) return scr_dyn_new_null(); - if (!scr_worker_local_data) { - ScrDyn *data = scr_message_decode(scr_worker_current->data); - if (!data) return NULL; - scr_worker_local_data = data; - if (scr_context_atexit(scr_worker_data_cleanup) != 0) scr_trap("scriptc: out of memory\n"); - scr_message_free(scr_worker_current->data); - scr_worker_current->data = NULL; - } + if (!scr_worker_decode_start()) return NULL; return scr_dyn_retain(scr_worker_local_data); } +ScrDyn *scr_worker_take_environment(void) { + if (!scr_worker_current || !scr_worker_current->environment_data) return NULL; + if (!scr_worker_decode_start()) return NULL; + ScrDyn *entries = scr_worker_local_environment; + scr_worker_local_environment = NULL; + return entries; +} + ScrMailbox *scr_worker_inbox(void) { return scr_worker_current ? scr_worker_current->inbox : NULL; } @@ -152,6 +182,22 @@ char **scr_worker_argv(void) { return scr_worker_current ? scr_worker_current->a uint64_t scr_worker_thread_id(const ScrWorkerThread *worker) { return worker->id; } +const ScrStr *scr_worker_thread_name(const ScrWorkerThread *worker) { return worker->name; } + +ScrDyn *scr_worker_thread_name_current(void) { + if (!scr_worker_current) { + ScrStr *empty = scr_str_new("", 0); + ScrDyn *name = scr_dyn_new_str(empty); + scr_str_release(empty); + return name; + } + /* a copy: the thread record's name is freed with the record */ + ScrStr *copy = scr_str_new(scr_worker_current->name->data, scr_worker_current->name->len); + ScrDyn *name = scr_dyn_new_str(copy); + scr_str_release(copy); + return name; +} + void scr_worker_thread_terminate(ScrWorkerThread *worker) { atomic_store_explicit(&worker->cancel, true, memory_order_relaxed); #ifdef SCR_EXC_FLAG @@ -187,16 +233,59 @@ void scr_worker_port_close(void) { atomic_store_explicit(&scr_worker_current->port_closed, true, memory_order_relaxed); } +static _Atomic(void (*)(void)) scr_worker_stdin_attach; + +void scr_worker_set_stdin_attach(void (*attach)(void)) { + atomic_store_explicit(&scr_worker_stdin_attach, attach, memory_order_relaxed); +} + +bool scr_worker_thread_post_stdin(ScrWorkerThread *worker, const void *data, size_t len, bool end) { + ScrMailEvent *event = scr_worker_event(SCR_WORKER_STDIN, 0, 0, NULL); + if (!end) { + event->data = malloc(len ? len : 1); + if (!event->data) scr_trap("scriptc: out of memory\n"); + memcpy(event->data, data, len); + event->length = len; + } + return scr_mailbox_post(worker->inbox, event); +} + bool scr_worker_post_parent(ScrMessage *message) { if (!scr_worker_current) { scr_message_free(message); return false; } return scr_mailbox_post(scr_worker_current->parent, scr_worker_event(SCR_WORKER_MESSAGE, scr_worker_current->id, 0, message)); } +/* The stdout and stderr options: a captured write reaches the parent's + * worker.stdout or worker.stderr in order with the worker's other events. */ +static bool scr_worker_capture(int fd, const void *data, size_t len) { + unsigned bit = fd == 2 ? 2 : 1; + if (!scr_worker_current || !(scr_worker_current->capture & bit)) return false; + if (!len) return true; + ScrMailEvent *event = scr_worker_event(bit == 2 ? SCR_WORKER_STDERR : SCR_WORKER_STDOUT, + scr_worker_current->id, 0, NULL); + event->data = malloc(len); + if (!event->data) scr_trap("scriptc: out of memory\n"); + memcpy(event->data, data, len); + event->length = len; + scr_mailbox_post(scr_worker_current->parent, event); + return true; +} + static void scr_worker_report_error(void) { ScrCaught *caught = scr_exc_take(); ScrDyn *value = scr_caught_to_dyn(caught); - ScrMessage *message = scr_message_encode(value); + if (scr_context_report_rejection) { + ScrDyn *error = scr_unhandled_rejection_error(value); + scr_dyn_release(value); + value = error; + } + ScrMessage *message = value ? scr_message_encode_uncaught(value) : NULL; + if (!message && value) { + /* Node falls back to the plain clone when a property does not clone. */ + scr_exc_clear(); + message = scr_message_encode(value); + } scr_dyn_release(value); scr_caught_release(caught); if (!message) { @@ -230,10 +319,16 @@ static void scr_worker_thread_run(ScrWorkerThread *worker) { worker->environment = NULL; scr_worker_current = worker; scr_context_report_error = scr_worker_report_error; + if (worker->capture & 3) scr_stdio_capture = scr_worker_capture; + if (worker->capture & 4) { + scr_stdin_fed = true; + scr_stdin_fed_attach = atomic_load_explicit(&scr_worker_stdin_attach, memory_order_relaxed); + } scr_mailbox_post(worker->parent, scr_worker_event(SCR_WORKER_ONLINE, worker->id, 0, NULL)); int code = worker->entry(worker->root); scr_context_note_script_end(); /* when the program frees nothing at exit */ scr_mailbox_close(worker->inbox); + scr_stdin_fed_clear(); /* chunks the worker never read */ scr_context_cleanup(); if (stats) { uint64_t now = scr_hrtime_ns(); @@ -241,6 +336,7 @@ static void scr_worker_thread_run(ScrWorkerThread *worker) { (unsigned long long)worker->id, (double)(started_ns - worker->spawned_ns) / 1e6, (double)(scr_context_script_end_ns() - started_ns) / 1e6, (double)(now - scr_context_script_end_ns()) / 1e6); } + scr_stdio_capture = NULL; scr_worker_current = NULL; scr_exc_alert_thread_exit(); #ifdef SCR_EXC_FLAG @@ -275,8 +371,10 @@ static void *scr_worker_thread_main(void *data) { #endif ScrWorkerThread *scr_worker_thread_start(ScrWorkerEntry entry, uint32_t root, - ScrMessage *data, ScrMailbox *parent, - ScrArr *arguments) { + ScrMessage *data, bool environment_data, + ScrMailbox *parent, ScrArr *arguments, + size_t stack_size, const ScrStr *name, + unsigned capture) { ScrWorkerThread *worker = calloc(1, sizeof(*worker)); if (!worker) scr_trap("scriptc: out of memory\n"); atomic_init(&worker->references, 2); /* owner and native thread */ @@ -292,7 +390,13 @@ ScrWorkerThread *scr_worker_thread_start(ScrWorkerEntry entry, uint32_t root, worker->entry = entry; worker->root = root; worker->data = data; + worker->environment_data = environment_data; + worker->capture = capture; worker->environment = scr_context_env_capture(); + worker->name = malloc(sizeof(ScrStr) + name->len + 1); + if (!worker->name) scr_trap("scriptc: out of memory\n"); + memcpy(worker->name, name, sizeof(ScrStr) + name->len + 1); + worker->name->rc = SIZE_MAX; worker->parent = scr_mailbox_retain(parent); if (arguments) { if (arguments->len > INT_MAX) scr_trap("scriptc: too many worker arguments\n"); @@ -311,16 +415,24 @@ ScrWorkerThread *scr_worker_thread_start(ScrWorkerEntry entry, uint32_t root, if (!worker->inbox) goto failed; if (scr_worker_stats()) worker->spawned_ns = scr_hrtime_ns(); #ifdef _WIN32 - worker->thread = (HANDLE)_beginthreadex(NULL, 0, scr_worker_thread_main, worker, 0, NULL); + worker->thread = (HANDLE)_beginthreadex(NULL, stack_size > UINT_MAX ? UINT_MAX : (unsigned)stack_size, + scr_worker_thread_main, worker, 0, NULL); if (!worker->thread) goto failed; #else /* An explicit size keeps recursion depth independent of the C library's * default thread stack (as small as 128 KiB with musl and 512 KiB on - * macOS): workers get the 8 MiB a main thread usually has. Pages are - * committed only as the stack grows. */ + * macOS): workers get the 8 MiB a main thread usually has, or + * resourceLimits.stackSizeMb. Pages are committed only as the stack + * grows. */ pthread_attr_t attr; if (pthread_attr_init(&attr) != 0) goto failed; - (void)pthread_attr_setstacksize(&attr, (size_t)8 << 20); + if (!stack_size) stack_size = (size_t)8 << 20; + long page = sysconf(_SC_PAGESIZE); + if (page > 0 && stack_size % (size_t)page) stack_size += (size_t)page - stack_size % (size_t)page; + if (pthread_attr_setstacksize(&attr, stack_size) != 0) { + pthread_attr_destroy(&attr); + goto failed; + } int started = pthread_create(&worker->thread, &attr, scr_worker_thread_main, worker); pthread_attr_destroy(&attr); if (started != 0) goto failed; @@ -329,6 +441,7 @@ ScrWorkerThread *scr_worker_thread_start(ScrWorkerEntry entry, uint32_t root, failed: for (int i = 0; i < worker->argc; i++) free(worker->argv[i]); free(worker->argv); + free(worker->name); scr_context_env_free(worker->environment); scr_mailbox_release(worker->inbox); scr_mailbox_release(worker->parent); diff --git a/packages/runtime/src/scr_worker.h b/packages/runtime/src/scr_worker.h index 008159fa2b..ba27f69af0 100644 --- a/packages/runtime/src/scr_worker.h +++ b/packages/runtime/src/scr_worker.h @@ -18,24 +18,47 @@ enum { SCR_WORKER_WAKE, SCR_WORKER_STDOUT, SCR_WORKER_STDERR, + SCR_WORKER_STDIN, /* parent to worker: a worker.stdin chunk, or its end (no data) */ }; -/* Consumes data, including on failure. Borrows the parent's mailbox. */ +/* Consumes data, including on failure. Borrows the parent's mailbox. With + * environment_data, data encodes [workerData, environment entries]. A + * stack_size of 0 selects the default 8 MiB thread stack. capture (1 + * stdout, 2 stderr) posts the worker's writes to those descriptors to the + * parent as SCR_WORKER_STDOUT/STDERR events; 4 (stdin) has the worker's + * process.stdin read what scr_worker_thread_post_stdin sends. */ ScrWorkerThread *scr_worker_thread_start(ScrWorkerEntry entry, uint32_t root, - ScrMessage *data, ScrMailbox *parent, - ScrArr *arguments); + ScrMessage *data, bool environment_data, + ScrMailbox *parent, ScrArr *arguments, + size_t stack_size, const ScrStr *name, + unsigned capture); ScrWorkerThread *scr_worker_thread_retain(ScrWorkerThread *worker); void scr_worker_thread_release(ScrWorkerThread *worker); /* Join is owner-only and idempotent. The caller holds a reference. */ void scr_worker_thread_join(ScrWorkerThread *worker); void scr_worker_thread_terminate(ScrWorkerThread *worker); uint64_t scr_worker_thread_id(const ScrWorkerThread *worker); +/* The Worker's threadName, owned by the thread record and freed with it: + * callers copy it into the strings they return. */ +const ScrStr *scr_worker_thread_name(const ScrWorkerThread *worker); /* Posting consumes the message even after the destination has closed. */ bool scr_worker_thread_post(ScrWorkerThread *worker, ScrMessage *message); +/* A worker.stdin chunk (data copied), or with end the end of the input. + * False once the worker's inbox has closed. */ +bool scr_worker_thread_post_stdin(ScrWorkerThread *worker, const void *data, size_t len, bool end); +/* The worker event unit's function that starts a worker's event dispatch, + * which queues the stdin chunks. The unit stores it before it starts a + * worker with the stdin option; that worker installs it as + * scr_stdin_fed_attach. */ +void scr_worker_set_stdin_attach(void (*attach)(void)); bool scr_worker_post_parent(ScrMessage *message); void scr_worker_port_close(void); /* Current-thread access: inbox is borrowed, data is a retained local value. */ ScrMailbox *scr_worker_inbox(void); +/* The parent's environment data entries ([key, value, ...], +1) on the + * first call in a worker; NULL afterwards, in the main thread, when the + * parent had none, and after a throw. */ +ScrDyn *scr_worker_take_environment(void); void scr_loop_set_workers(bool (*pending)(void), bool (*ready)(void), void (*dispatch)(void), int (*pollfd)(void), void (*wait)(double)); /* After a dispatch: true keeps the loop pass delivering (a MessagePort poll diff --git a/packages/runtime/src/scr_worker.test.c b/packages/runtime/src/scr_worker.test.c index 12065b43de..a50fd2fe28 100644 --- a/packages/runtime/src/scr_worker.test.c +++ b/packages/runtime/src/scr_worker.test.c @@ -95,7 +95,9 @@ int main(void) { ScrMessage *wire = scr_message_encode(input); scr_dyn_release(input); assert(wire && !scr_exc_pending()); - workers[i] = scr_worker_thread_start(entry, i, wire, mailbox, NULL); + ScrStr *name = scr_str_new("WorkerThread", 12); + workers[i] = scr_worker_thread_start(entry, i, wire, false, mailbox, NULL, 0, name, 0); + scr_str_release(name); assert(workers[i] && !scr_exc_pending()); assert(scr_worker_thread_id(workers[i]) != 0); } diff --git a/packages/runtime/src/scr_worker.test.ts b/packages/runtime/src/scr_worker.test.ts index edcca4eaa1..2d2cbcae2d 100644 --- a/packages/runtime/src/scr_worker.test.ts +++ b/packages/runtime/src/scr_worker.test.ts @@ -28,6 +28,7 @@ test.skipIf(process.platform === "win32")( "scr_worker.c", "scr_mailbox.c", "scr_message.c", + "scr_async_dyn.c", ...EXECUTABLE_RUNTIME_SOURCES, ].map((name) => join(import.meta.dirname, name)), ...(process.platform === "linux" ? ["-lm"] : []), diff --git a/packages/runtime/src/scr_worker_events.c b/packages/runtime/src/scr_worker_events.c index 51fbce59fc..53041f3d5b 100644 --- a/packages/runtime/src/scr_worker_events.c +++ b/packages/runtime/src/scr_worker_events.c @@ -10,6 +10,7 @@ typedef struct ScrWorkerView { ScrEmitter *events; ScrWorkerThread *thread; ScrPromise *termination; + ScrPromise *disposal; /* worker[Symbol.asyncDispose]()'s promise */ bool port; bool closed; bool referenced; @@ -25,6 +26,21 @@ typedef struct ScrWorkerView { uint64_t close_gen; /* the poll generation close() ran in */ size_t phase_left; /* messages this poll phase still delivers */ bool phase_close; /* the other end's close, delivered this phase */ + /* A BroadcastChannel: its name and EventTarget core; its channel's end 0 + * queues what the other instances with the name post. */ + bool broadcast; + ScrStr *name; + ScrEventTargetCore *et; + /* A Worker's stdout/stderr streams (the stdout and stderr options) and + * their checked-dynamic boxes, made on first read */ + ScrStream *stdio[2]; + ScrDyn *stdio_box[2]; + bool stdio_captured[2]; + /* worker.stdin (the stdin option): a Writable whose chunks go to the + * worker's process.stdin, and its box */ + ScrStream *stdin_stream; + ScrDyn *stdin_box; + bool stdin_captured; struct ScrWorkerView *next; } ScrWorkerView; @@ -47,16 +63,90 @@ static const char *const scr_worker_method_names[] = { "listenerCount", "close", "start", }; static SCR_TL ScrDyn *scr_worker_methods[sizeof scr_worker_method_names / sizeof scr_worker_method_names[0]]; +static const char *const scr_broadcast_method_names[] = { + "postMessage", "close", "ref", "unref", "addEventListener", "removeEventListener", "dispatchEvent", +}; +static SCR_TL ScrDyn *scr_broadcast_methods[sizeof scr_broadcast_method_names / sizeof scr_broadcast_method_names[0]]; + + +/* The stream unit's half of worker.stdout, worker.stderr and + * worker.stdin, installed by main when the program reads them as streams + * (scr_worker_stdio_install; a box the program has no type for is NULL). */ +static SCR_TL const ScrChildStreamOps *scr_worker_stdio_ops; +static SCR_TL ScrDyn *(*scr_worker_stdio_box)(void *); +static SCR_TL ScrDyn *(*scr_worker_stdin_box)(void *); + +void scr_worker_stdio_install(const ScrChildStreamOps *ops, ScrDyn *(*readable_box)(void *), + ScrDyn *(*writable_box)(void *)) { + scr_worker_stdio_ops = ops; + scr_worker_stdio_box = readable_box; + scr_worker_stdin_box = writable_box; +} + +/* A Worker with the stdout, stderr or stdin option in a program that has + * no typed Readable or Writable box: the streams go through the stream + * unit's live view. */ +void scr_worker_stdio_default(const ScrChildStreamOps *ops, ScrDyn *(*view)(void *)) { + if (!scr_worker_stdio_ops) scr_worker_stdio_ops = ops; + if (!scr_worker_stdio_box) scr_worker_stdio_box = view; + if (!scr_worker_stdin_box) scr_worker_stdin_box = view; +} + +/* worker.stdin: each chunk written goes to the worker as an + * SCR_WORKER_STDIN event, and end() sends the end of the input. The + * callbacks hold the thread record, not the view (the view owns the + * stream). Writes after the worker exited are dropped. */ +static void *scr_worker_thread_retain_v(void *thread) { return scr_worker_thread_retain(thread); } +static void scr_worker_thread_release_v(void *thread) { scr_worker_thread_release(thread); } + +static void scr_worker_stdin_write(ScrClosure *cb, ScrStream *stream, ScrBytes *chunk) { + ScrWorkerThread *thread = scr_box_get_ref(cb->caps[0]); /* +1 */ + /* an empty chunk pushes nothing in Node's process.stdin */ + if (chunk->len) scr_worker_thread_post_stdin(thread, chunk->data, chunk->len, false); + scr_worker_thread_release(thread); + scr_worker_stdio_ops->write_done(stream, NULL); +} + +static void scr_worker_stdin_final(ScrClosure *cb, ScrStream *stream) { + ScrWorkerThread *thread = scr_box_get_ref(cb->caps[0]); /* +1 */ + scr_worker_thread_post_stdin(thread, NULL, 0, true); + scr_worker_thread_release(thread); + scr_worker_stdio_ops->final_done(stream, NULL); +} + +static ScrClosure *scr_worker_stdin_callback(ScrWorkerThread *thread, void *fire) { + ScrClosure *cb = scr_closure_new(fire, 1); + cb->caps[0] = scr_box_new_obj(scr_worker_thread_retain_v, scr_worker_thread_release_v, NULL); + scr_box_set_ref(cb->caps[0], scr_worker_thread_retain(thread)); + return cb; +} + +static ScrStream *scr_worker_stdin_new(ScrWorkerThread *thread) { + return scr_worker_stdio_ops->writable( + scr_worker_stdin_callback(thread, (void *)scr_worker_stdin_write), scr_worker_stdin_write, + scr_worker_stdin_callback(thread, (void *)scr_worker_stdin_final), scr_worker_stdin_final); +} static void scr_worker_view_trace(void *ptr, ScrTraceVisit visit, void *ctx) { - visit(((ScrWorkerView *)ptr)->events, ctx); - visit(((ScrWorkerView *)ptr)->termination, ctx); + ScrWorkerView *view = ptr; + visit(view->events, ctx); + visit(view->termination, ctx); + visit(view->disposal, ctx); + for (int i = 0; i < 2; i++) { + visit(view->stdio[i], ctx); + visit(view->stdio_box[i], ctx); + } + visit(view->stdin_stream, ctx); + visit(view->stdin_box, ctx); } static void scr_worker_view_gcfree(void *ptr) { ScrWorkerView *view = ptr; scr_worker_thread_release(view->thread); + if (view->broadcast && view->channel && !view->closed) scr_broadcast_leave(view->channel); scr_channel_release(view->channel); + if (view->name) scr_str_release(view->name); + if (view->et) scr_et_broadcast_ops->core_free(view->et); while (view->queued_head) { ScrMailEvent *event = view->queued_head; view->queued_head = event->next; @@ -70,6 +160,13 @@ static void scr_worker_view_destroy(void *ptr) { ScrWorkerView *view = ptr; scr_emitter_release(view->events); scr_promise_release(view->termination); + scr_promise_release(view->disposal); + for (int i = 0; i < 2; i++) { + if (view->stdio[i]) scr_worker_stdio_ops->release(view->stdio[i]); + scr_dyn_release(view->stdio_box[i]); + } + if (view->stdin_stream) scr_worker_stdio_ops->release(view->stdin_stream); + scr_dyn_release(view->stdin_box); scr_worker_view_gcfree(ptr); } @@ -125,6 +222,70 @@ static bool scr_worker_empty_transfer(const ScrDyn *value) { return value->kind == SCR_DYN_UNDEF || (value->kind == SCR_DYN_ARR && value->v.arr.len == 0); } +/* ── markAsUntransferable ─────────────────────────────────────────────── */ + +/* An ArrayBuffer's mark lives on its storage (Buffer pool slabs carry it + * from creation, as in Node); other marked objects stay listed, retained, + * for this thread's lifetime. A typed array shares its owner's storage with + * its ArrayBuffer, so views are listed by their own identity. */ +static SCR_TL ScrDyn **scr_untransferable; +static SCR_TL size_t scr_untransferable_count; +static SCR_TL size_t scr_untransferable_capacity; + +static void scr_untransferable_cleanup(void) { + for (size_t i = 0; i < scr_untransferable_count; i++) scr_dyn_release(scr_untransferable[i]); + free(scr_untransferable); + scr_untransferable = NULL; + scr_untransferable_count = scr_untransferable_capacity = 0; +} + +static bool scr_untransferable_same(const ScrDyn *a, const ScrDyn *b) { + if (a->kind != b->kind) return false; + switch (a->kind) { + case SCR_DYN_BYTES: return a->v.bytes == b->v.bytes; + case SCR_DYN_HANDLE: return a->v.handle.ptr == b->v.handle.ptr; + case SCR_DYN_TYPED_REF: return a->v.typed_ref.ptr == b->v.typed_ref.ptr; + default: return scr_dyn_strict_eq(a, b); + } +} + +/* typeof value is "object" or "function", and value is not null */ +static bool scr_untransferable_object(const ScrDyn *value) { + switch (value->kind) { + case SCR_DYN_UNDEF: case SCR_DYN_NULL: case SCR_DYN_BOOL: case SCR_DYN_NUM: + case SCR_DYN_STR: case SCR_DYN_BIGINT: case SCR_DYN_SYMBOL: return false; + default: return true; + } +} + +bool scr_worker_is_marked_untransferable(ScrDyn *value) { + if (!scr_untransferable_object(value)) return false; + if (scr_array_buffer_is(value) || scr_shared_array_buffer_is(value)) + return ((ScrBytes *)value->v.handle.ptr)->untransferable; + for (size_t i = 0; i < scr_untransferable_count; i++) + if (scr_untransferable_same(scr_untransferable[i], value)) return true; + return false; +} + +/* markAsUntransferable(value): primitives are left as they are */ +void scr_worker_mark_untransferable(ScrDyn *value) { + if (!scr_untransferable_object(value) || scr_worker_is_marked_untransferable(value)) return; + if (scr_array_buffer_is(value) || scr_shared_array_buffer_is(value)) { + ((ScrBytes *)value->v.handle.ptr)->untransferable = true; + return; + } + if (scr_untransferable_count == scr_untransferable_capacity) { + if (!scr_untransferable_capacity && scr_context_atexit(scr_untransferable_cleanup) != 0) + scr_trap("scriptc: out of memory\n"); + size_t capacity = scr_untransferable_capacity ? scr_untransferable_capacity * 2 : 8; + ScrDyn **values = realloc(scr_untransferable, capacity * sizeof *values); + if (!values) scr_trap("scriptc: out of memory\n"); + scr_untransferable = values; + scr_untransferable_capacity = capacity; + } + scr_untransferable[scr_untransferable_count++] = scr_dyn_retain(value); +} + /* postMessage's transfer argument: an array, or { transfer } (+1, or NULL * when empty) */ static ScrDyn *scr_worker_transfer_list(ScrDyn *arg) { @@ -145,6 +306,14 @@ static ScrDyn *scr_worker_transfer_list(ScrDyn *arg) { return result; } if (value->kind == SCR_DYN_ARR && value->v.arr.len > 0) { + for (size_t i = 0; i < value->v.arr.len; i++) { + ScrDyn *item = value->v.arr.items[i]; + if (item && scr_worker_is_marked_untransferable(item)) { + scr_dyn_release(own); + scr_throw_domex("DataCloneError", "Cannot transfer object of unsupported type."); + return NULL; + } + } ScrDyn *result = scr_dyn_retain(value); scr_dyn_release(own); return result; @@ -291,10 +460,71 @@ static ScrDyn *scr_worker_method_call(ScrClosure *closure, ScrDyn *const *args, return result; } +/* worker[Symbol.asyncDispose](): Node's `await this.terminate()`, a + * promise of undefined once the worker exited. */ +static ScrDyn *scr_worker_async_dispose_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + (void)args; + (void)argc; + ScrDyn *self = scr_dyn_this_get(); + ScrDyn *result = NULL; + if (self->kind != SCR_DYN_HANDLE || self->v.handle.tag != SCR_DYNH_WORKER) { + static const char message[] = "Illegal invocation"; + scr_throw_error_msg_code(SCR_ERR_TYPE, message, sizeof message - 1, "ERR_INVALID_THIS"); + } else { + ScrWorkerView *view = self->v.handle.ptr; + if (!view->disposal) { + view->disposal = scr_promise_new(); + if (view->closed) scr_promise_fulfill_ref(view->disposal, + scr_dyn_retain(scr_dyn_undefined()), scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v); + else if (!view->termination) scr_worker_thread_terminate(view->thread); + } + result = scr_dyn_new_promise(view->disposal); + } + scr_dyn_release(self); + return result; +} + +/* worker[Symbol.asyncDispose] */ +static ScrDyn *scr_worker_symbol_get(void *ptr, const ScrDyn *key) { + (void)ptr; + if (!scr_dyn_is_async_dispose_symbol(key)) return NULL; + ScrClosure *closure = scr_closure_new((void *)&scr_worker_async_dispose_call, 0); + return scr_dyn_new_func(closure, &scr_worker_async_dispose_call, 0, "()", "[Symbol.asyncDispose]"); +} + static ScrDyn *scr_worker_view_get(void *ptr, const char *key, size_t length) { ScrWorkerView *view = ptr; if (!view->port && length == 8 && !memcmp(key, "threadId", 8)) return scr_dyn_new_num(view->closed ? -1 : (double)scr_worker_thread_id(view->thread)); + /* Node reports the running worker's V8 heap configuration; an exited + * worker answers {}. */ + if (!view->port && length == 5 && !memcmp(key, "stdin", 5)) { + if (!view->stdin_stream) return scr_dyn_new_null(); + if (!view->stdin_box) view->stdin_box = scr_worker_stdin_box(view->stdin_stream); + return scr_dyn_retain(view->stdin_box); + } + if (!view->port && length == 6 && (!memcmp(key, "stdout", 6) || !memcmp(key, "stderr", 6))) { + int i = key[3] == 'e' ? 1 : 0; + if (!view->stdio_captured[i]) + return scr_worker_refusal(i ? "worker.stderr without the stderr option is not supported yet" + : "worker.stdout without the stdout option is not supported yet"); + if (!view->stdio[i]) return scr_dyn_new_null(); + if (!view->stdio_box[i]) view->stdio_box[i] = scr_worker_stdio_box(view->stdio[i]); + return scr_dyn_retain(view->stdio_box[i]); + } + if (!view->port && length == 10 && !memcmp(key, "threadName", 10)) { + if (view->closed) return scr_dyn_new_null(); + /* a copy: the thread record's name is freed with the record, which + * a string value can outlive */ + const ScrStr *name = scr_worker_thread_name(view->thread); + ScrStr *copy = scr_str_new(name->data, name->len); + ScrDyn *value = scr_dyn_new_str(copy); + scr_str_release(copy); + return value; + } + if (!view->port && length == 14 && !memcmp(key, "resourceLimits", 14)) + return view->closed ? scr_dyn_new_obj() : scr_worker_refusal("worker.resourceLimits is not supported yet"); for (size_t i = 0; i < sizeof scr_worker_methods / sizeof scr_worker_methods[0]; i++) { const char *name = scr_worker_method_names[i]; if (strlen(name) != length || memcmp(name, key, length)) continue; @@ -312,6 +542,8 @@ static ScrDyn *scr_worker_view_get(void *ptr, const char *key, size_t length) { } static bool scr_workers_pending(void) { + /* a worker's process.stdin waiting for worker.stdin chunks */ + if (scr_stdin_fed && scr_stdin_fed_waiting && scr_stdin_fed_waiting()) return true; for (ScrWorkerView *view = scr_worker_views; view; view = view->next) if (view->referenced) return true; for (ScrWorkerView *view = scr_channel_ports; view; view = view->next) @@ -412,7 +644,13 @@ static bool scr_channel_dispatch(void) { } scr_worker_view_retain(view); ScrDyn *argument = event->message ? scr_message_decode(event->message) : NULL; - if (argument) scr_worker_port_emit(view, "message", argument); + if (argument && view->broadcast) { + ScrDyn *self = scr_dyn_new_handle(view, SCR_DYNH_BROADCAST_CHANNEL); + scr_et_broadcast_ops->dispatch_message(view->et, self, argument); + scr_dyn_release(self); + } else if (argument) { + scr_worker_port_emit(view, "message", argument); + } scr_dyn_release(argument); scr_mail_event_free(event); scr_worker_view_release(view); @@ -483,6 +721,31 @@ static void scr_worker_dispatch(void) { } else event = scr_mailbox_take(scr_worker_events); if (!event) return; if (event->kind == SCR_WORKER_WAKE) { scr_mail_event_free(event); return; } + if (event->kind == SCR_WORKER_STDIN) { + ScrBytes *chunk = NULL; + if (event->data) { + chunk = scr_bytes_new(SCR_BYTES_U8, (double)event->length); + memcpy(chunk->data, event->data, event->length); + chunk->is_buffer = true; + } + scr_stdin_fed_push(chunk); + scr_mail_event_free(event); + return; + } + if (event->kind == SCR_WORKER_STDOUT || event->kind == SCR_WORKER_STDERR) { + ScrWorkerView *source = scr_worker_views; + while (source && scr_worker_thread_id(source->thread) != event->source) source = source->next; + ScrStream *stream = source ? source->stdio[event->kind == SCR_WORKER_STDERR] : NULL; + if (stream) { + ScrBytes *chunk = scr_bytes_new(SCR_BYTES_U8, (double)event->length); + memcpy(chunk->data, event->data, event->length); + chunk->is_buffer = true; + scr_worker_stdio_ops->push(stream, chunk); + scr_bytes_release(chunk); + } + scr_mail_event_free(event); + return; + } ScrWorkerView *view = scr_worker_port; if (event->source) { view = scr_worker_views; @@ -508,6 +771,8 @@ static void scr_worker_dispatch(void) { view->closed = true; if (view->termination) scr_promise_fulfill_ref(view->termination, scr_dyn_new_num(event->code), scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v); + if (view->disposal) scr_promise_fulfill_ref(view->disposal, + scr_dyn_retain(scr_dyn_undefined()), scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v); ScrWorkerView **slot = &scr_worker_views; while (*slot != view) slot = &(*slot)->next; *slot = view->next; @@ -522,6 +787,11 @@ static void scr_worker_dispatch(void) { scr_dyn_this_pop(); scr_str_release(name); } + /* Node ends worker.stdout and worker.stderr as the worker exits: their + * 'end' events follow 'exit'. */ + if (event->kind == SCR_WORKER_EXIT) + for (int i = 0; i < 2; i++) + if (view->stdio[i]) scr_worker_stdio_ops->end(view->stdio[i]); scr_dyn_release(argument); scr_worker_view_release(view); scr_mail_event_free(event); @@ -547,17 +817,36 @@ static void scr_worker_events_cleanup(void) { scr_channel_ports = view->next; view->next = NULL; if (view->channel) scr_channel_set_owner(view->channel, view->side, NULL, SCR_WORKER_WAKE); + /* a BroadcastChannel's name and listeners go now: program values + * may still hold the handle past this teardown */ + if (view->broadcast) { + if (!view->closed) { + view->closed = true; + scr_broadcast_leave(view->channel); + } + if (view->name) scr_str_release(view->name); + view->name = NULL; + if (view->et) scr_et_broadcast_ops->core_free(view->et); + view->et = NULL; + } scr_worker_view_release(view); } for (size_t i = 0; i < sizeof scr_worker_methods / sizeof scr_worker_methods[0]; i++) { scr_dyn_release(scr_worker_methods[i]); scr_worker_methods[i] = NULL; } + for (size_t i = 0; i < sizeof scr_broadcast_methods / sizeof scr_broadcast_methods[0]; i++) { + scr_dyn_release(scr_broadcast_methods[i]); + scr_broadcast_methods[i] = NULL; + } scr_mailbox_release(scr_worker_events); scr_worker_events = NULL; } static void scr_port_ops_install(void); +static bool scr_worker_events_install(void); + +static void scr_worker_stdin_attach(void) { (void)scr_worker_events_install(); } static bool scr_worker_events_install(void) { if (scr_worker_events) return true; @@ -568,6 +857,7 @@ static bool scr_worker_events_install(void) { static const ScrDynHandleOps worker_ops = { .cls = "Worker", .retain = scr_worker_view_retain, .release = scr_worker_view_release, .invoke = scr_worker_view_invoke, .get = scr_worker_view_get, + .symbol_get = scr_worker_symbol_get, }; static const ScrDynHandleOps port_ops = { .cls = "MessagePort", .retain = scr_worker_view_retain, .release = scr_worker_view_release, @@ -647,6 +937,151 @@ ScrDyn *scr_worker_message_channel(void) { return object; } +/* ── BroadcastChannel ───────────────────────────────────────────────── */ + + +static ScrDyn *scr_broadcast_invoke(void *ptr, ScrDyn *self, const char *method, + ScrDyn *const *args, size_t argc, const char *what) { + (void)what; + ScrWorkerView *view = ptr; + if (!strcmp(method, "postMessage")) { + if (view->closed) { + scr_throw_domex("InvalidStateError", "BroadcastChannel is closed."); + return NULL; + } + if (!argc) { + static const char message[] = "The \"message\" argument must be specified"; + scr_throw_error_msg_code(SCR_ERR_TYPE, message, sizeof message - 1, "ERR_MISSING_ARGS"); + return NULL; + } + ScrMessage *first = scr_message_encode(args[0]); + if (!first) return NULL; + ScrChannel **peers = NULL; + size_t n = scr_broadcast_peers(view->name->data, view->name->len, view->channel, &peers); + for (size_t i = 0; i < n; i++) { + ScrMessage *message = i == 0 ? first : scr_message_encode(args[0]); + if (i == 0) first = NULL; + if (message) scr_channel_post(peers[i], 1, message, SCR_WORKER_MESSAGE, SCR_WORKER_WAKE); + scr_channel_release(peers[i]); + } + free(peers); + scr_message_free(first); + return scr_exc_pending() ? NULL : scr_dyn_retain(scr_dyn_undefined()); + } + if (!strcmp(method, "close")) { + if (!view->closed) { + view->closed = true; + scr_broadcast_leave(view->channel); + scr_channel_drop(view->channel, 0); + } + return scr_dyn_retain(scr_dyn_undefined()); + } + if (!strcmp(method, "ref") || !strcmp(method, "unref")) { + view->referenced = !strcmp(method, "ref"); + return scr_dyn_retain(self); + } + ScrDyn *out = NULL; + if (scr_et_broadcast_ops->method(view->et, self, method, args, argc, &out)) return out; + return scr_worker_refusal("This BroadcastChannel method is not supported yet"); +} + +static ScrDyn *scr_broadcast_method_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + ScrDyn *self = scr_dyn_this_get(); + ScrDyn *result = NULL; + if (self->kind != SCR_DYN_HANDLE || self->v.handle.tag != SCR_DYNH_BROADCAST_CHANNEL) { + static const char message[] = "Illegal invocation"; + scr_throw_error_msg_code(SCR_ERR_TYPE, message, sizeof message - 1, "ERR_INVALID_THIS"); + } else { + size_t method = (size_t)scr_box_get_f64(closure->caps[0]); + result = scr_broadcast_invoke(self->v.handle.ptr, self, scr_broadcast_method_names[method], args, + argc, "BroadcastChannel method"); + } + scr_dyn_release(self); + return result; +} + +static ScrDyn *scr_broadcast_get(void *ptr, const char *key, size_t length) { + ScrWorkerView *view = ptr; + if (length == 4 && !memcmp(key, "name", 4)) return scr_dyn_new_str(view->name); + if ((length == 9 && !memcmp(key, "onmessage", 9)) || + (length == 14 && !memcmp(key, "onmessageerror", 14))) { + char type[16]; + memcpy(type, key + 2, length - 2); + type[length - 2] = '\0'; + return scr_et_broadcast_ops->handler_get(view->et, type); + } + for (size_t i = 0; i < sizeof scr_broadcast_methods / sizeof scr_broadcast_methods[0]; i++) { + const char *name = scr_broadcast_method_names[i]; + if (strlen(name) != length || memcmp(name, key, length)) continue; + if (!scr_broadcast_methods[i]) { + ScrClosure *closure = scr_closure_new(NULL, 1); + closure->caps[0] = scr_box_new(SCR_BOX_F64); + scr_box_set_f64(closure->caps[0], (double)i); + scr_broadcast_methods[i] = scr_dyn_new_func(closure, scr_broadcast_method_call, 0, "", name); + } + return scr_dyn_retain(scr_broadcast_methods[i]); + } + return NULL; +} + +static bool scr_broadcast_set(void *ptr, const char *key, size_t length, const ScrDyn *value) { + ScrWorkerView *view = ptr; + if ((length == 9 && !memcmp(key, "onmessage", 9)) || + (length == 14 && !memcmp(key, "onmessageerror", 14))) { + char type[16]; + memcpy(type, key + 2, length - 2); + type[length - 2] = '\0'; + scr_et_broadcast_ops->handler_set(view->et, type, value); + return true; + } + return false; +} + +/* new BroadcastChannel(name): open and referenced until close() or + * unref(); args is the argument vector as one dyn array */ +ScrDyn *scr_worker_broadcast_channel(ScrDyn *args) { + if (!scr_worker_events_install()) return NULL; + if (args->kind != SCR_DYN_ARR || args->v.arr.len == 0) { + static const char message[] = "The \"name\" argument must be specified"; + scr_throw_error_msg_code(SCR_ERR_TYPE, message, sizeof message - 1, "ERR_MISSING_ARGS"); + return NULL; + } + ScrStr *name = scr_dyn_string_coerce_js(args->v.arr.items[0]); + if (!name) return NULL; + static SCR_TL bool installed = false; + if (!installed) { + installed = true; + static const ScrDynHandleOps broadcast_ops = { + .cls = "BroadcastChannel", .retain = scr_worker_view_retain, .release = scr_worker_view_release, + .invoke = scr_broadcast_invoke, .get = scr_broadcast_get, .set = scr_broadcast_set, + }; + scr_dyn_handle_install(SCR_DYNH_BROADCAST_CHANNEL, &broadcast_ops); + } + ScrChannel *channel = scr_channel_new(); + ScrWorkerView *view = scr_channel_port_new(channel, 0); + scr_channel_release(channel); + view->broadcast = true; + view->name = name; + view->et = scr_et_broadcast_ops->core_new("BroadcastChannel"); + view->started = true; + view->referenced = true; + scr_broadcast_join(name->data, name->len, view->channel); + return scr_dyn_new_handle(view, SCR_DYNH_BROADCAST_CHANNEL); +} + +/* A port's next queued message. For parentPort: the messages held until + * start(), then the inbox's next message from the parent (events from this + * thread's own workers stay queued). */ +static ScrMailEvent *scr_port_take(ScrWorkerView *view) { + if (view->channel) return scr_channel_take(view->channel, view->side); + ScrMailEvent *event = view->queued_head; + if (!event) return scr_mailbox_take_kind(scr_worker_events, SCR_WORKER_MESSAGE, 0); + view->queued_head = event->next; + if (!view->queued_head) view->queued_tail = NULL; + event->next = NULL; + return event; +} + /* receiveMessageOnPort(port): { message } of the next queued message, or * undefined */ ScrDyn *scr_worker_receive_message_on_port(ScrDyn *port) { @@ -656,8 +1091,7 @@ ScrDyn *scr_worker_receive_message_on_port(ScrDyn *port) { scr_throw_error_msg_code(SCR_ERR_TYPE, message, sizeof message - 1, "ERR_INVALID_ARG_TYPE"); return NULL; } - if (!view->channel) return scr_worker_refusal("receiveMessageOnPort on parentPort is not supported yet"); - ScrMailEvent *event = view->closed || view->detached ? NULL : scr_channel_take(view->channel, view->side); + ScrMailEvent *event = view->closed || view->detached ? NULL : scr_port_take(view); if (!event) return scr_dyn_retain(scr_dyn_undefined()); ScrDyn *value = event->message ? scr_message_decode(event->message) : NULL; scr_mail_event_free(event); @@ -667,6 +1101,124 @@ ScrDyn *scr_worker_receive_message_on_port(ScrDyn *port) { return object; } +/* ── environment data ─────────────────────────────────────────────────── */ + +/* setEnvironmentData's map for this thread: keys compare with + * SameValueZero, and a deleted key re-added goes last. A worker starts + * from a clone of its parent's entries taken by new Worker(). */ +typedef struct { + ScrDyn *key; + ScrDyn *value; +} ScrEnvironmentEntry; +static SCR_TL ScrEnvironmentEntry *scr_environment; +static SCR_TL size_t scr_environment_count; +static SCR_TL size_t scr_environment_capacity; +static SCR_TL bool scr_environment_ready; + +static void scr_environment_cleanup(void) { + for (size_t i = 0; i < scr_environment_count; i++) { + scr_dyn_release(scr_environment[i].key); + scr_dyn_release(scr_environment[i].value); + } + free(scr_environment); + scr_environment = NULL; + scr_environment_count = scr_environment_capacity = 0; + scr_environment_ready = false; +} + +static void scr_environment_append(ScrDyn *key, ScrDyn *value) { + if (scr_environment_count == scr_environment_capacity) { + size_t capacity = scr_environment_capacity ? scr_environment_capacity * 2 : 4; + ScrEnvironmentEntry *entries = realloc(scr_environment, capacity * sizeof *entries); + if (!entries) scr_trap("scriptc: out of memory\n"); + scr_environment = entries; + scr_environment_capacity = capacity; + } + scr_environment[scr_environment_count++] = (ScrEnvironmentEntry){key, value}; +} + +/* This thread's entries, the inherited ones decoded on first use. */ +static bool scr_environment_load(void) { + if (scr_environment_ready) return true; + ScrDyn *inherited = scr_worker_take_environment(); + if (scr_exc_pending()) return false; + scr_environment_ready = true; + if (scr_context_atexit(scr_environment_cleanup) != 0) scr_trap("scriptc: out of memory\n"); + for (size_t i = 0; inherited && i + 1 < inherited->v.arr.len; i += 2) + scr_environment_append(scr_dyn_arr_at(inherited, (double)i), scr_dyn_arr_at(inherited, (double)i + 1)); + scr_dyn_release(inherited); + return true; +} + +static bool scr_environment_key_eq(const ScrDyn *a, const ScrDyn *b) { + if (a->kind == SCR_DYN_NUM && b->kind == SCR_DYN_NUM) + return a->v.num == b->v.num || (a->v.num != a->v.num && b->v.num != b->v.num); + if (a->kind == SCR_DYN_TYPED_REF && b->kind == SCR_DYN_TYPED_REF) + return a->v.typed_ref.ptr == b->v.typed_ref.ptr; + return scr_dyn_strict_eq(a, b); +} + +static size_t scr_environment_find(const ScrDyn *key) { + for (size_t i = 0; i < scr_environment_count; i++) + if (scr_environment_key_eq(scr_environment[i].key, key)) return i; + return SIZE_MAX; +} + +/* setEnvironmentData(key, value): undefined deletes the key. */ +void scr_worker_set_environment_data(ScrDyn *key, ScrDyn *value) { + if (!scr_environment_load()) return; + size_t index = scr_environment_find(key); + if (value->kind == SCR_DYN_UNDEF) { + if (index == SIZE_MAX) return; + scr_dyn_release(scr_environment[index].key); + scr_dyn_release(scr_environment[index].value); + memmove(&scr_environment[index], &scr_environment[index + 1], + (scr_environment_count - index - 1) * sizeof *scr_environment); + scr_environment_count--; + return; + } + if (index != SIZE_MAX) { + scr_dyn_release(scr_environment[index].value); + scr_environment[index].value = scr_dyn_retain(value); + return; + } + scr_environment_append(scr_dyn_retain(key), scr_dyn_retain(value)); +} + +ScrDyn *scr_worker_get_environment_data(ScrDyn *key) { + if (!scr_environment_load()) return NULL; + size_t index = scr_environment_find(key); + return scr_dyn_retain(index == SIZE_MAX ? scr_dyn_undefined() : scr_environment[index].value); +} + +/* new Worker()'s start message: workerData alone, or [workerData, + * [key, value, ...]] when this thread has environment data, cloned in one + * message as Node clones both in one postMessage. */ +static ScrMessage *scr_worker_start_message(ScrDyn *data, ScrDyn *transfer, bool *environment_data) { + *environment_data = false; + if (!scr_environment_load()) return NULL; + if (!scr_environment_count) return scr_message_encode_transfer(data, transfer); + ScrDyn *entries = scr_dyn_new_arr(); + for (size_t i = 0; i < scr_environment_count; i++) { + scr_dyn_arr_push(entries, scr_dyn_retain(scr_environment[i].key)); + scr_dyn_arr_push(entries, scr_dyn_retain(scr_environment[i].value)); + } + ScrDyn *start = scr_dyn_new_arr(); + scr_dyn_arr_push(start, scr_dyn_retain(data)); + scr_dyn_arr_push(start, entries); + ScrMessage *message = scr_message_encode_transfer(start, transfer); + scr_dyn_release(start); + *environment_data = message != NULL; + return message; +} + +/* worker_threads.resourceLimits: {} in the main thread, as Node; a worker's + * values are its V8 heap configuration. */ +ScrDyn *scr_worker_resource_limits(void) { + if (scr_context_is_main()) return scr_dyn_new_obj(); + return scr_worker_refusal("resourceLimits in a worker is not supported yet"); +} + ScrDyn *scr_worker_parent_port(void) { if (scr_context_is_main()) return scr_dyn_new_null(); if (!scr_worker_events_install()) return NULL; @@ -709,11 +1261,42 @@ static ScrArr *scr_worker_arguments(ScrStr *filename, ScrDyn *argv) { return arguments; } +/* resourceLimits: Node reads the four numbers when the option is an object + * and ignores other values. stackSizeMb sizes the thread's stack (at least + * 192 KiB, as Node); the V8 heap limits are refused. */ +static bool scr_worker_stack_size(ScrDyn *limits, size_t *stack_size) { + if (limits->kind == SCR_DYN_TYPED_REF) { + ScrDyn *view = scr_dyn_typed_ref_materialize(limits); + bool ok = view && !scr_exc_pending() && scr_worker_stack_size(view, stack_size); + scr_dyn_release(view); + return ok; + } + if (limits->kind != SCR_DYN_OBJ) return true; + static const char *const heap[] = {"maxOldGenerationSizeMb", "maxYoungGenerationSizeMb", "codeRangeSizeMb"}; + for (size_t i = 0; i < sizeof heap / sizeof heap[0]; i++) { + const ScrDyn *value = scr_dyn_obj_get(limits, heap[i], strlen(heap[i])); + if (value && value->kind == SCR_DYN_NUM) { + scr_worker_refusal("Worker resourceLimits heap limits are not supported yet"); + return false; + } + } + const ScrDyn *megabytes = scr_dyn_obj_get(limits, "stackSizeMb", 11); + if (megabytes && megabytes->kind == SCR_DYN_NUM && megabytes->v.num > 0) { + double bytes = megabytes->v.num * 1048576.0; + if (bytes < 192.0 * 1024) bytes = 192.0 * 1024; + *stack_size = bytes >= (double)(SIZE_MAX / 2) ? SIZE_MAX / 2 : (size_t)bytes; + } + return true; +} + static ScrDyn *scr_worker_new_options(double root, ScrStr *filename, ScrDyn *options) { if (!scr_worker_events_install()) return NULL; ScrDyn *data = scr_dyn_undefined(); ScrDyn *argv = scr_dyn_undefined(); ScrDyn *transfer_list = scr_dyn_undefined(); + ScrDyn *limits = scr_dyn_undefined(); + ScrDyn *name = scr_dyn_undefined(); + unsigned capture = 0; if (options->kind != SCR_DYN_UNDEF) { if (options->kind != SCR_DYN_OBJ) { scr_dyn_arg_type_fail("options", "object", options); @@ -725,21 +1308,52 @@ static ScrDyn *scr_worker_new_options(double root, ScrStr *filename, ScrDyn *opt if (entry->key_len == 10 && !memcmp(entry->key, "workerData", 10)) data = entry->value; else if (entry->key_len == 4 && !memcmp(entry->key, "argv", 4)) argv = entry->value; else if (entry->key_len == 12 && !memcmp(entry->key, "transferList", 12)) transfer_list = entry->value; + else if (entry->key_len == 14 && !memcmp(entry->key, "resourceLimits", 14)) limits = entry->value; + else if (entry->key_len == 4 && !memcmp(entry->key, "name", 4)) name = entry->value; + else if (entry->key_len == 6 && !memcmp(entry->key, "stdout", 6)) capture |= scr_dyn_truthy(entry->value) ? 1 : 0; + else if (entry->key_len == 6 && !memcmp(entry->key, "stderr", 6)) capture |= scr_dyn_truthy(entry->value) ? 2 : 0; + else if (entry->key_len == 5 && !memcmp(entry->key, "stdin", 5)) capture |= scr_dyn_truthy(entry->value) ? 4 : 0; else return scr_worker_refusal("This Worker constructor option is not supported yet"); } } + size_t stack_size = 0; + if (!scr_worker_stack_size(limits, &stack_size)) return NULL; ScrArr *arguments = scr_worker_arguments(filename, argv); if (!arguments) return NULL; + /* Node: a truthy name must be a string, trimmed; otherwise WorkerThread */ + ScrStr *thread_name; + if (scr_dyn_truthy(name)) { + if (name->kind != SCR_DYN_STR) { + scr_arr_release(arguments); + scr_dyn_prop_type_fail("options.name", "of type string", name); + return NULL; + } + thread_name = scr_str_trim(name->v.str); + } else { + thread_name = scr_str_new("WorkerThread", 12); + } ScrDyn *transfer = scr_worker_transfer_list(transfer_list); - if (scr_exc_pending()) { scr_arr_release(arguments); return NULL; } - ScrMessage *wire = scr_message_encode_transfer(data, transfer); + if (scr_exc_pending()) { scr_arr_release(arguments); scr_str_release(thread_name); return NULL; } + bool environment_data; + ScrMessage *wire = scr_worker_start_message(data, transfer, &environment_data); + if (capture & 4) scr_worker_set_stdin_attach(scr_worker_stdin_attach); scr_dyn_release(transfer); - if (!wire) { scr_arr_release(arguments); return NULL; } - ScrWorkerThread *thread = scr_worker_thread_start(sc_worker_entry, (uint32_t)root, wire, scr_worker_events, arguments); + if (!wire) { scr_arr_release(arguments); scr_str_release(thread_name); return NULL; } + ScrWorkerThread *thread = scr_worker_thread_start(sc_worker_entry, (uint32_t)root, wire, environment_data, + scr_worker_events, arguments, stack_size, thread_name, + capture); + scr_str_release(thread_name); scr_arr_release(arguments); if (!thread) return NULL; ScrWorkerView *view = scr_worker_view_new(false); view->thread = thread; + for (int i = 0; i < 2; i++) { + view->stdio_captured[i] = (capture >> i) & 1; + /* without the stream unit nothing can read the stream: the output is dropped */ + if (view->stdio_captured[i] && scr_worker_stdio_ops) view->stdio[i] = scr_worker_stdio_ops->readable(); + } + view->stdin_captured = capture & 4; + if (view->stdin_captured && scr_worker_stdio_ops) view->stdin_stream = scr_worker_stdin_new(thread); view->next = scr_worker_views; scr_worker_views = view; return scr_dyn_new_handle(view, SCR_DYNH_WORKER); diff --git a/packages/runtime/test/test_cycle.c b/packages/runtime/test/test_cycle.c index ba0bc08198..8fdac2d131 100644 --- a/packages/runtime/test/test_cycle.c +++ b/packages/runtime/test/test_cycle.c @@ -1187,6 +1187,84 @@ static void check_large_old_buffer_with_nursery_candidates(void) { } } +/* The death rule: once objects have died outside passes (the live count + * fell by 1/32 of the heap the last full pass left, here the floor's), a + * full pass is due as soon as the heap has grown the base quarter past it, + * however wide the growth fraction has become. With the old level's + * fraction at the cap, the dead old ring below would otherwise wait until + * the heap had grown to the cap. A fall that the pass it brings finds + * nothing behind doubles the fall the next one needs. */ +static size_t watched_freed; +static void watched_free(void *obj) { + watched_freed++; + node_free(obj); +} + +/* Fresh live leaves that are never released, each followed by a dead + * one-node ring, which keeps the release path running its passes without + * buffering the leaves, until the watched ring is freed or `limit` leaves + * have been made. Returns how many it made. */ +static size_t grow_until_watched(Node **keep, size_t limit, size_t watched) { + size_t n = 0; + while (watched_freed < watched && n < limit) { + keep[n++] = make_leaf(); + release_live(make_ring(1)); + } + return n; +} + +static void check_death_brings_full_pass(void) { + enum { FLOOR = 4096, RING = 64, LEAVES = 1024 }; + static ScrCycType watched_type = { &node_trace, &watched_free, 0 }; + size_t cap = configured_growth_cap(); + if (cap < 4) return; /* growth alone is due about as soon */ + size_t limit = FLOOR + FLOOR / 4 + 2 * configured_nursery_threshold() + 2; + Node **leaves = calloc(LEAVES, sizeof(*leaves)); + Node **keep = calloc(limit, sizeof(*keep)); + if (!leaves || !keep) scr_trap("cycle test: out of memory\n"); + /* Growth passes that find nothing widen the old fraction to the cap. */ + for (int i = 0; i < 4; i++) grow_dead_cycle_until_full_pass(0, false); + + Node *ring = make_ring_of(RING, &watched_type); + age_to_old(ring); + for (size_t i = 0; i < LEAVES; i++) leaves[i] = make_leaf(); + scr_collect_cycles(); + check(scr_cyc_live < FLOOR - LEAVES, "live heap did not start below the growth floor"); + watched_freed = 0; + drop_leaves(leaves, LEAVES); /* gone by reference counting: the live count falls */ + scr_cyc_collect_scheduled(); /* a check reads the whole fall */ + release_live(ring); /* a dead old cycle */ + size_t grown = grow_until_watched(keep, limit, RING); + check(watched_freed == RING, "the fall did not bring a full pass by the base fraction"); + drop_leaves(keep, grown); + scr_collect_cycles(); + + /* A fall with nothing behind it: the pass it brings finds nothing. */ + for (size_t i = 0; i < LEAVES; i++) leaves[i] = make_leaf(); + scr_collect_cycles(); + drop_leaves(leaves, LEAVES); + scr_cyc_collect_scheduled(); + grown = grow_until_watched(keep, limit, 1); + drop_leaves(keep, grown); + /* So the next death pass needs twice the fall: 1/32 of the floor, enough + * before, no longer is. */ + ring = make_ring_of(RING, &watched_type); + age_to_old(ring); + for (size_t i = 0; i < FLOOR / 32; i++) leaves[i] = make_leaf(); + scr_collect_cycles(); + watched_freed = 0; + drop_leaves(leaves, FLOOR / 32); + scr_cyc_collect_scheduled(); + release_live(ring); + grown = grow_until_watched(keep, limit, RING); + check(watched_freed == 0, "a fall smaller than the backed-off one brought a full pass"); + drop_leaves(keep, grown); + scr_collect_cycles(); + check(watched_freed == RING, "the full sweep did not reclaim the dead old ring"); + free(leaves); + free(keep); +} + int main(void) { check_old_garbage_reclaimed_while_growing(); check_older_rings_into_old(); @@ -1207,6 +1285,7 @@ int main(void) { check_growth_schedule_follows_yield(); check_growth_cap_bounds_floating_garbage(); check_mixed_dead_structure_narrows(); + check_death_brings_full_pass(); check_nursery_bypass(); check_mature_bypass(); check_probe_sees_candidate_rings(); diff --git a/packages/runtime/vendor/quickjs-ng/libregexp.c b/packages/runtime/vendor/quickjs-ng/libregexp.c index 2191c6879c..09720139d0 100644 --- a/packages/runtime/vendor/quickjs-ng/libregexp.c +++ b/packages/runtime/vendor/quickjs-ng/libregexp.c @@ -1020,7 +1020,7 @@ static int parse_class_string_disjunction(REParseState *s, REStringList *cr, for(;;) { str.size = 0; while (*p != '}' && *p != '|') { - c = get_class_atom(s, NULL, &p, false); + c = get_class_atom(s, NULL, &p, true); if (c < 0) goto fail; if (dbuf_put_u32(&str, c)) { @@ -1193,7 +1193,7 @@ static int get_class_atom(REParseState *s, REStringList *cr, case '^': case '`': case '~': - if (s->unicode_sets && p[1] == c) { + if (s->unicode_sets && inclass && p[1] == c) { /* forbidden double characters */ return re_parse_error(s, "invalid class set operation in regular expression"); } @@ -1208,7 +1208,7 @@ static int get_class_atom(REParseState *s, REStringList *cr, case '/': case '-': case '|': - if (s->unicode_sets) { + if (s->unicode_sets && inclass) { /* invalid characters in unicode sets */ return re_parse_error(s, "invalid character in class in regular expression"); } diff --git a/scripts/bench-runtime.mjs b/scripts/bench-runtime.mjs index ff9c9d6905..4e4f2c245a 100644 --- a/scripts/bench-runtime.mjs +++ b/scripts/bench-runtime.mjs @@ -56,9 +56,10 @@ Each checkout must have a built CLI (packages/cli/dist/bootstrap.js) and native artifacts for this host. Without --baseline, reports absolute times only. Node is timed in the same interleaved loop as the reference to beat (--no-node skips it). ---layouts=N (macOS/Linux) links every executable N times, shifting the program -object by 0, 16, 32, ... bytes of padding, and spreads the timed runs evenly over -those layouts, so verdicts cover code-placement luck instead of one placement. +--layouts=N (macOS/Linux) links every executable N times, shifting the runtime +and program code by 0, 16, 32, ... bytes of padding, and spreads the timed runs +evenly over those layouts, so verdicts cover code-placement luck instead of one +placement. Use it for small codegen changes (e.g. --layouts=4 --runs=16). Workload names are listed in benchmarks/runtime/workloads.json; for example @@ -103,10 +104,11 @@ const work = mkdtempSync( ); /* ── code-layout perturbation ──────────────────────────────────────────── */ -// Layout k > 0 links a retained k*16-byte padding object directly before the -// program object, through a SCRIPTC_LINKER wrapper that forwards to clang (the -// default linker driver on macOS and Linux). Code after that point moves as if -// earlier code had grown; any checkout's compiler can be perturbed this way. +// Layout k > 0 links a retained k*16-byte padding object before every other +// input, through a SCRIPTC_LINKER wrapper that forwards to clang (the default +// linker driver on macOS and Linux). The runtime is linked before the program +// object, so both move as if earlier code had grown; any checkout's compiler can +// be perturbed this way. function layoutTooling() { if (layouts === 1) return null; const root = join(work, "layouts"); @@ -137,12 +139,11 @@ const args = process.argv.slice(2); const pad = process.env.SCRIPTC_BENCH_LAYOUT_PAD; let out = args; if (pad && !args.some((a) => a.startsWith("-print-prog-name"))) { - const at = args.findIndex((a) => basename(a).startsWith(".scriptc-native-program-object-")); - if (at < 0) { + if (!args.some((a) => basename(a).startsWith(".scriptc-native-program-object-"))) { process.stderr.write("bench layout wrapper: no program object on the link line\\n"); process.exit(2); } - out = ["-Wl,-u,${symbol}", ...args.slice(0, at), pad, ...args.slice(at)]; + out = ["-Wl,-u,${symbol}", pad, ...args]; } const result = spawnSync("clang", out, { stdio: "inherit" }); process.exit(result.status ?? 1); diff --git a/tests/corpus/4642-acyclic-contract.ts b/tests/corpus/4642-acyclic-contract.ts index dffbba761e..bbd256fe1e 100644 --- a/tests/corpus/4642-acyclic-contract.ts +++ b/tests/corpus/4642-acyclic-contract.ts @@ -50,6 +50,16 @@ interface Point extends Acyclic { type Segment = Acyclic<{ readonly from: Point; readonly to: Point; readonly label: string }>; +// A generic class is acyclic like any other class. +class Chain implements Acyclic { + readonly value: T; + readonly next: Chain | undefined; + constructor(value: T, next: Chain | undefined) { + this.value = value; + this.next = next; + } +} + function build(depth: number): Tree { return depth === 0 ? new Tree(null, null, 1) : new Tree(build(depth - 1), build(depth - 1), depth); } @@ -74,3 +84,15 @@ const slots: Record = { a: new Slot() }; for (let i = 0; i < 3; i++) slots["a"]!.index = (slots["a"]!.index + 2) | 0; slots["b"] = new Slot(); console.log(Object.keys(slots).join(","), slots["a"]!.index, slots["b"]!.index); + +let numbers: Chain | undefined = undefined; +let words: Chain | undefined = undefined; +for (let i = 0; i < 1000; i++) { + numbers = new Chain(i, numbers); + words = new Chain(`w${i % 7}`, words); +} +let chained = 0; +for (let c = numbers; c !== undefined; c = c.next) chained += c.value; +let longest = ""; +for (let c = words; c !== undefined; c = c.next) if (c.value > longest) longest = c.value; +console.log(chained, longest); diff --git a/tests/corpus/5013-lifetime-region-async.ts b/tests/corpus/5013-lifetime-region-async.ts index 683d93d6d7..930c6af677 100644 --- a/tests/corpus/5013-lifetime-region-async.ts +++ b/tests/corpus/5013-lifetime-region-async.ts @@ -1,10 +1,9 @@ // LifetimeRegion (@scriptc/experimental) with async callbacks: run(fn) // returns fn's promise once fn first awaits, and the work that resumes -// after an await runs outside the region (it allocates in the default -// region, which is never freed). Releasing the region while the async work -// is still pending is safe: everything the pending work refers to stays -// allocated, and objects of the default region that refer into a released -// region keep it allocated for good. Node.js prints the same. +// after an await still allocates in the region. Releasing the region while +// the async work is still pending is safe: the region is not freed before +// that work has finished and nothing outside refers to its objects. +// Node.js prints the same. import type { ProgramLifetime } from "@scriptc/types"; import { LifetimeRegion } from "@scriptc/experimental"; diff --git a/tests/corpus/5023-lifetime-region-await.ts b/tests/corpus/5023-lifetime-region-await.ts new file mode 100644 index 0000000000..b17ef07aa1 --- /dev/null +++ b/tests/corpus/5023-lifetime-region-await.ts @@ -0,0 +1,200 @@ +// LifetimeRegion (@scriptc/experimental) across await: an async callback +// keeps allocating in its region after it resumes, so a region per request +// holds the whole request's graph and frees it once released. Requests run +// concurrently and interleave at every await; continuations nest other +// regions, reject, release their region while still pending, spawn work +// that outlives the request, and return objects that escape into a cache. +// Node.js prints the same. +import type { ProgramLifetime } from "@scriptc/types"; +import { LifetimeRegion } from "@scriptc/experimental"; + +class Token implements ProgramLifetime { + readonly text: string; + readonly pos: number; + constructor(text: string, pos: number) { + this.text = text; + this.pos = pos; + } +} + +class Node implements ProgramLifetime { + readonly token: Token; + readonly kids: Node[] = []; + parent: Node | undefined = undefined; + readonly attrs = new Map(); + constructor(token: Token) { + this.token = token; + } +} + +const tick = (): Promise => new Promise((resolve) => setTimeout(resolve, 1)); +const turn = (): Promise => new Promise((resolve) => setImmediate(resolve)); + +function tokenize(source: string): Token[] { + return source.split(" ").map((text, i) => new Token(text, i)); +} + +async function parse(tokens: Token[]): Promise { + const root = new Node(tokens[0]!); + for (let i = 1; i < tokens.length; i++) { + if (i % 3 === 0) await turn(); + const node = new Node(tokens[i]!); + node.parent = root; + node.attrs.set("pos", `${tokens[i]!.pos}`); + root.kids.push(node); + } + return root; +} + +function render(root: Node): string { + return `${root.token.text}(${root.kids.map((k) => `${k.token.text}@${k.attrs.get("pos")}`).join(" ")})`; +} + +async function handle(id: number, source: string): Promise { + const tokens = tokenize(source); + await tick(); + const root = await parse(tokens); + await Promise.resolve(); + // Children spawned after a resume run in the region too. + const sizes = await Promise.all(root.kids.map(async (kid) => { + await turn(); + const copy = new Node(kid.token); + copy.parent = kid; + return copy.token.text.length; + })); + return `#${id} ${render(root)} sizes=${sizes.join(",")}`; +} + +async function serve(id: number, source: string): Promise { + const region = new LifetimeRegion(); + try { + return await region.run(() => handle(id, source)); + } finally { + region.release(); + } +} + +// A sync generator driven by a resumed continuation. +function* chain(tokens: Token[]): Generator { + let prev: Node | undefined; + for (const token of tokens) { + const node = new Node(token); + node.parent = prev; + prev = node; + yield node; + } +} + +async function nested(): Promise { + const outer = new LifetimeRegion(); + const inner = new LifetimeRegion(); + const result = await outer.run(async () => { + const before = new Node(new Token("outer", 0)); + await tick(); + const local = inner.run(() => new Node(new Token("inner-sync", 1))); + const later = await inner.run(async () => { + await turn(); + const n = new Node(new Token("inner-async", 2)); + n.parent = local; + return n; + }); + const after = new Node(new Token("after", 3)); + after.parent = before; + after.kids.push(local, later); + const gen = [...chain(tokenize("g0 g1 g2"))]; + return `${after.parent.token.text} ${after.kids.map((k) => k.token.text).join("+")} ${later.parent!.token.text} ${gen.at(-1)!.parent!.token.text}`; + }); + inner.release(); + outer.release(); + return result; +} + +async function rejects(): Promise { + const region = new LifetimeRegion(); + try { + await region.run(async () => { + const node = new Node(new Token("doomed", 0)); + await tick(); + node.kids.push(new Node(new Token("child", 1))); + throw new Error(`failed after ${node.kids.length} child`); + }); + return "no error"; + } catch (e) { + return e instanceof Error ? e.message : String(e); + } finally { + region.release(); + } +} + +async function releasedWhilePending(): Promise { + const region = new LifetimeRegion(); + const pending = region.run(async () => { + const first = new Node(new Token("first", 0)); + await tick(); + // The region was released meanwhile; the work still allocates in it. + const second = new Node(new Token("second", 1)); + second.parent = first; + await turn(); + return `${second.parent.token.text}>${second.token.text}`; + }); + region.release(); + return await pending; +} + +// Background work started in a request outlives it. +let background: Promise | undefined; +async function outlives(): Promise { + const region = new LifetimeRegion(); + const text = await region.run(async () => { + const node = new Node(new Token("request", 0)); + background = (async (): Promise => { + await tick(); + await tick(); + const late = new Node(new Token("late", 1)); + late.parent = node; + return `${late.parent.token.text}<-${late.token.text}`; + })(); + await turn(); + return node.token.text; + }); + region.release(); + return text; +} + +// A result that escapes the request into a module-level cache keeps the +// region allocated until the cache lets go of it. +const cache = new Map(); +async function escapes(key: string): Promise { + const region = new LifetimeRegion(); + const node = await region.run(async () => { + await turn(); + const n = new Node(new Token(key, 0)); + n.kids.push(new Node(new Token(`${key}-kid`, 1))); + return n; + }); + region.release(); + cache.set(key, node); + return `${node.token.text}/${node.kids[0]!.token.text}`; +} + +async function main(): Promise { + const sources = ["let a = b + c ;", "fn f ( x ) { x }", "1 2 3 4 5 6 7", "if ( x ) y else z"]; + for (let round = 0; round < 2; round++) { + const responses = await Promise.all(sources.map((s, i) => serve(round * 10 + i, s))); + for (const r of responses) console.log(r); + } + console.log(await nested()); + console.log(await rejects()); + console.log(await releasedWhilePending()); + console.log(await outlives()); + console.log(await background); + console.log(await escapes("cached")); + console.log(cache.get("cached")!.kids[0]!.token.pos); + cache.clear(); + // A later region, after the cache let go. + console.log(await serve(99, "done .")); +} + +main().catch((e) => { + console.log("unexpected", String(e)); +}); diff --git a/tests/corpus/5024-lifetime-region-await-forms.ts b/tests/corpus/5024-lifetime-region-await-forms.ts new file mode 100644 index 0000000000..e2c9d4622e --- /dev/null +++ b/tests/corpus/5024-lifetime-region-await-forms.ts @@ -0,0 +1,114 @@ +// LifetimeRegion (@scriptc/experimental) across await in both forms an +// async body compiles to: a body that awaits only at its own awaits runs as +// a stackless coroutine, and one that drives an async generator with +// for await keeps a fiber. Each form spawns and awaits the other inside a +// region, requests interleave, and a region is released while its work is +// still pending; everything allocated after a resume stays in the region, +// so every region is freed (tests/harness/lifetime-regions.test.ts). +// Node.js prints the same. +import type { ProgramLifetime } from "@scriptc/types"; +import { LifetimeRegion } from "@scriptc/experimental"; + +class Item implements ProgramLifetime { + readonly name: string; + next: Item | undefined = undefined; + readonly tags: string[] = []; + constructor(name: string) { + this.name = name; + } +} + +const tick = (): Promise => new Promise((resolve) => setTimeout(resolve, 1)); +const turn = (): Promise => new Promise((resolve) => setImmediate(resolve)); + +function names(head: Item | undefined): string { + const out: string[] = []; + for (let at = head; at !== undefined; at = at.next) out.push(`${at.name}[${at.tags.join("/")}]`); + return out.join(","); +} + +// Stackless: suspends only at its own awaits. +async function decorate(name: string, round: number): Promise { + const item = new Item(name); + await turn(); + item.tags.push(`r${round}`); + await Promise.resolve(); + item.tags.push(`${name.length}`); + return item; +} + +async function* produce(prefix: string, n: number): AsyncGenerator { + for (let i = 0; i < n; i++) { + await tick(); + yield `${prefix}${i}`; + } +} + +// Fiber: drives an async generator, and awaits stackless work per step. +async function streamed(prefix: string, n: number): Promise { + let head: Item | undefined; + let round = 0; + for await (const name of produce(prefix, n)) { + const item = await decorate(name, round++); + item.next = head; + head = item; + } + return head; +} + +// Stackless again: awaits the fiber form, then keeps allocating. +async function request(id: number): Promise { + const first = await decorate(`q${id}`, 0); + const chain = await streamed(`s${id}.`, 2 + (id % 2)); + first.next = chain; + await tick(); + const last = new Item(`end${id}`); + last.next = first; + last.tags.push("done"); + return `#${id} ${names(last)}`; +} + +async function serve(id: number): Promise { + const region = new LifetimeRegion(); + try { + return await region.run(() => request(id)); + } finally { + region.release(); + } +} + +async function releasedWhilePending(): Promise { + const region = new LifetimeRegion(); + const pending = region.run(() => streamed("late.", 3)); + region.release(); + const head = await pending; + return `released early: ${names(head)}`; +} + +async function failing(): Promise { + const region = new LifetimeRegion(); + try { + await region.run(async () => { + const head = await streamed("f.", 2); + await turn(); + throw new Error(`failed after ${names(head)}`); + }); + return "no error"; + } catch (e) { + return e instanceof Error ? e.message : String(e); + } finally { + region.release(); + } +} + +async function main(): Promise { + const responses = await Promise.all([1, 2, 3, 4].map((id) => serve(id))); + for (const r of responses) console.log(r); + console.log(await releasedWhilePending()); + console.log(await failing()); + console.log(await serve(9)); +} + +main().catch((e) => { + console.log("unexpected", String(e)); +}); diff --git a/tests/corpus/5627-exec-callback.ts b/tests/corpus/5627-exec-callback.ts new file mode 100644 index 0000000000..eed79e738c --- /dev/null +++ b/tests/corpus/5627-exec-callback.ts @@ -0,0 +1,36 @@ +// child_process.exec's callback forms: the command line runs with +// /bin/sh -c, the call returns a live ChildProcess immediately, utf8 +// stdout/stderr are captured, and the error-first callback fires after the +// current turn. A non-zero exit (a missing command is the shell's 127) +// reports "Command failed: \n". +import { exec } from "node:child_process"; +import * as childProcess from "node:child_process"; + +const child = exec("printf out; printf err >&2", (error, stdout, stderr) => { + console.log("ok", error === null, JSON.stringify(stdout), JSON.stringify(stderr)); + exec("printf 'a\\nb\\nc\\n' | wc -l", (pipeError, lines) => { + console.log("pipe", pipeError === null, lines.trim()); + exec(`printf '%s|' "a b" 'c d' e`, { encoding: "utf8", maxBuffer: 1024 * 1024 }, (_, quoted) => { + console.log("quoting", quoted); + exec("printf partial; printf failed >&2; exit 3", (failed, failedOut, failedErr) => { + console.log("fail", failed !== null, JSON.stringify(failedOut), JSON.stringify(failedErr)); + console.log("fail-message", JSON.stringify(failed === null ? "" : failed.message)); + exec("exit 1", (silent) => { + console.log("silent", JSON.stringify(silent === null ? "" : silent.message)); + exec("scriptc-definitely-not-a-command", (missing, missingOut) => { + const message = missing === null ? "" : missing.message; + console.log("missing", missing !== null, JSON.stringify(missingOut)); + console.log("missing-message", message.split("\n")[0]); + exec("true", () => { + console.log("zero-parameter callback"); + childProcess.exec("printf namespace", (_, out) => console.log(out)); + }); + }); + }); + }); + }); + }); +}); + +console.log("returned", child.pid !== undefined, child.stdout !== null, child.stderr !== null); +console.log("scheduled"); diff --git a/tests/corpus/5681-array-pop-and-length.ts b/tests/corpus/5681-array-pop-and-length.ts new file mode 100644 index 0000000000..451b32576b --- /dev/null +++ b/tests/corpus/5681-array-pop-and-length.ts @@ -0,0 +1,143 @@ +// Removing the last element with a statement-level pop() or `length = n`, +// for every element kind, on dense arrays, arrays with holes and present +// undefined values, sparse arrays and arrays shared between bindings. +// Removed references are released; the remaining elements, the length and +// later appends match Node. + +class Item { + readonly id: number; + constructor(id: number) { + this.id = id; + } +} + +function show(label: string, values: unknown[]): void { + const parts: string[] = []; + for (let i = 0; i < values.length; i++) { + if (!(i in values)) parts.push(""); + else { + const v = values[i]; + parts.push(v instanceof Item ? `I${v.id}` : (JSON.stringify(v) ?? "undefined")); + } + } + console.log(label, values.length, parts.join(" ")); +} + +const numbers = [1.5, 2.5, 3.5, 4.5]; +numbers.pop(); +numbers.length = numbers.length - 1; +numbers.push(9); +show("numbers", numbers); + +const flags = [true, false, true]; +flags.pop(); +flags.length = 1; +flags.push(false); +show("flags", flags); + +const words = ["a", "b", "c", "d"]; +const alias = words; +words.pop(); +alias.length = alias.length - 1; +words.push("z"); +show("words", words); +show("alias", alias); + +const items = [new Item(1), new Item(2), new Item(3)]; +const kept = items[2]!; +items.pop(); +items.length = 1; +items.push(new Item(4)); +show("items", items); +console.log("kept", kept.id); + +// Holes and present undefined at the end. +const holes: (number | undefined)[] = [1, 2]; +holes[4] = 5; +holes.length = 4; +show("holes trimmed", holes); +holes.length = 3; +show("holes trimmed again", holes); +const undef: (string | undefined)[] = ["x", undefined, undefined]; +undef.pop(); +undef.length = undef.length - 1; +show("undefined", undef); + +// A sparse array: a far index then shrinking past it and back by one. +const sparse: number[] = [1, 2, 3]; +sparse[2000000] = 4; +sparse.length = 2000000; +show("sparse cut", sparse.slice(0, 4)); +console.log("sparse length", sparse.length, 1999999 in sparse); +sparse.length = sparse.length - 1; +console.log("sparse length", sparse.length); + +// A stack used the way a checker pushes and pops contextual entries. +const stack: Item[] = []; +const top: number[] = []; +let checksum = 0; +for (let i = 0; i < 20000; i++) { + stack.push(new Item(i)); + top.push(i); + if (i % 3 === 2) { + stack.pop(); + top.length = top.length - 1; + } + if (stack.length > 50) { + stack.length = stack.length - 1; + top.pop(); + } + checksum = (checksum + stack[stack.length - 1]!.id + top[top.length - 1]!) % 1000003; +} +console.log("stack", stack.length, top.length, checksum); + +// Lengths that are not one less: unchanged, longer, invalid. +const other = [1, 2, 3]; +other.length = 3; +other.length = 5; +show("longer", other); +other.length = 1; +show("shorter by many", other); +try { + other.length = -1; +} catch (e) { + console.log("invalid", (e as Error).name, other.length); +} +try { + other.length = 0.5; +} catch (e) { + console.log("fraction", (e as Error).name, other.length); +} +const empty: number[] = []; +empty.pop(); +empty.length = 0; +show("empty", empty); + +// pop() results: the last element, undefined for an empty array, a hole or a +// present undefined, and values taken from sparse storage. +const nums = [0.5, -0, 7]; +console.log("pop number", nums.pop(), Object.is(nums.pop(), -0), nums.pop(), nums.pop(), nums.length); +const bools = [false, true]; +console.log("pop bool", bools.pop(), bools.pop(), bools.pop(), bools.length); +const strs = ["p", "q"]; +const strAlias = strs; +console.log("pop string", strs.pop(), strAlias.pop(), strs.pop(), strAlias.length); +const objs = [new Item(7), new Item(8)]; +const popped = objs.pop(); +console.log("pop item", popped?.id, objs.pop()?.id, objs.pop(), objs.length); +const gaps: number[] = [1]; +gaps[3] = 4; +console.log("pop gaps", gaps.pop(), gaps.pop(), gaps.pop(), gaps.pop(), gaps.pop(), gaps.length); +const maybe: (number | undefined)[] = [1, undefined]; +console.log("pop present undefined", maybe.pop(), maybe.length, maybe.pop(), maybe.length); +const far: string[] = ["a"]; +far[100000] = "b"; +console.log("pop sparse", far.pop(), far.length, far.pop(), far.length); +let popTotal = 0; +const work: number[] = []; +for (let i = 0; i < 10000; i++) { + work.push(i, i + 1); + popTotal += work.pop()!; + if (i % 5 === 0) popTotal += work.pop() ?? -1; +} +console.log("pop loop", work.length, popTotal); diff --git a/tests/corpus/5800-broadcast-channel.ts b/tests/corpus/5800-broadcast-channel.ts new file mode 100644 index 0000000000..648d2caedb --- /dev/null +++ b/tests/corpus/5800-broadcast-channel.ts @@ -0,0 +1,35 @@ +import { BroadcastChannel } from "node:worker_threads"; + +// BroadcastChannel within one thread: delivery to every other channel with +// the name (not the sender), per channel in creation order before +// setImmediate, MessageEvent fields, listeners, and the errors. +const a = new BroadcastChannel("o"); +const b = new BroadcastChannel("o"); +const c = new BroadcastChannel("o"); +const other = new BroadcastChannel("p"); +const log = (n: string) => (e: MessageEvent) => + console.log(n, JSON.stringify(e.data), e.type, e.origin === "", e.lastEventId === "", e.source, e.isTrusted, e.target === (n === "a" ? a : n === "b" ? b : c)); +a.onmessage = log("a"); +b.onmessage = log("b"); +c.onmessage = log("c"); +c.addEventListener("message", (e: Event) => console.log("c listener", JSON.stringify((e as MessageEvent).data))); +other.onmessage = () => console.log("other got (should not)"); +a.postMessage("from a"); +b.postMessage({ from: "b" }); +console.log("name", a.name, typeof b.onmessage, b.onmessageerror); +const named = new BroadcastChannel(String(42)); +console.log("coerced", named.name); +named.close(); +try { + named.postMessage(1); +} catch (err) { + const e = err as Error; + console.log(e.name, e.message); +} +setImmediate(() => { + console.log("immediate"); + a.close(); + b.close(); + c.close(); + other.close(); +}); diff --git a/tests/corpus/5801-broadcast-channel.cjs b/tests/corpus/5801-broadcast-channel.cjs new file mode 100644 index 0000000000..9fd8c759c5 --- /dev/null +++ b/tests/corpus/5801-broadcast-channel.cjs @@ -0,0 +1,30 @@ +'use strict'; +// BroadcastChannel in JavaScript: error codes, the MessageEvent's ports, +// once and removed listeners, ref()/unref() returning the channel, and an +// unref'd channel that does not keep the process alive. +const { BroadcastChannel } = require('node:worker_threads'); + +for (const f of [ + () => new BroadcastChannel(), + () => { const c = new BroadcastChannel('x'); try { c.postMessage(); } finally { c.close(); } }, + () => { const c = new BroadcastChannel('x'); c.close(); c.close(); c.postMessage(1); }, +]) { + try { + f(); + console.log('ok'); + } catch (e) { + console.log(e.name, e.code, e.message); + } +} +const a = new BroadcastChannel('n'); +const b = new BroadcastChannel('n'); +const removed = () => console.log('removed listener ran'); +b.addEventListener('message', (e) => console.log('once', e.data, Array.isArray(e.ports), e.ports.length), { once: true }); +b.addEventListener('message', removed); +b.removeEventListener('message', removed); +b.onmessage = (e) => console.log('onmessage', e.data, e.target === b, this === undefined); +console.log('ref returns', a.ref() === a, a.unref() === a, b.unref() === b); +a.postMessage(1); +a.postMessage(2); +setTimeout(() => console.log('never: unref channels do not hold the loop'), 10000).unref(); +setImmediate(() => console.log('immediate')); diff --git a/tests/corpus/5802-broadcast-channel-workers.ts b/tests/corpus/5802-broadcast-channel-workers.ts new file mode 100644 index 0000000000..2d66735375 --- /dev/null +++ b/tests/corpus/5802-broadcast-channel-workers.ts @@ -0,0 +1,25 @@ +import { BroadcastChannel, Worker, isMainThread } from "node:worker_threads"; + +// BroadcastChannel across threads: the main thread's channel and a +// worker's channel with the same name hear each other, a sender never +// hears its own message, and closing both lets the worker exit. +if (isMainThread) { + const bc = new BroadcastChannel("jobs"); + bc.onmessage = (e: MessageEvent) => { + console.log("main got", JSON.stringify(e.data)); + if (e.data === "ready") bc.postMessage("work"); + if (e.data === "done:work") { + bc.postMessage("bye"); + bc.close(); + } + }; + const worker = new Worker(new URL(import.meta.url)); + worker.on("exit", (code: number) => console.log("exit", code)); +} else { + const bc = new BroadcastChannel("jobs"); + bc.onmessage = (e: MessageEvent) => { + if (e.data === "work") bc.postMessage("done:" + e.data); + if (e.data === "bye") bc.close(); + }; + bc.postMessage("ready"); +} diff --git a/tests/corpus/5803-worker-environment-data.ts b/tests/corpus/5803-worker-environment-data.ts new file mode 100644 index 0000000000..4debbbe3e2 --- /dev/null +++ b/tests/corpus/5803-worker-environment-data.ts @@ -0,0 +1,53 @@ +import { Worker, getEnvironmentData, isMainThread, parentPort, setEnvironmentData, workerData } from "node:worker_threads"; + +// setEnvironmentData/getEnvironmentData: keys compare with SameValueZero, +// undefined deletes, and each new Worker starts from a clone of its +// parent's entries taken at construction, which it passes on to its own +// workers. +if (isMainThread) { + console.log("missing", getEnvironmentData("missing")); + setEnvironmentData("a", 1); + setEnvironmentData("config", { list: [1, 2], name: "x" }); + setEnvironmentData(42, "number key"); + setEnvironmentData(NaN, "nan key"); + setEnvironmentData(-0, "zero key"); + const key = { k: 1 }; + setEnvironmentData(key, "object key"); + const shared = { v: 1 }; + setEnvironmentData("shared", shared); + console.log(getEnvironmentData("a"), getEnvironmentData(42), getEnvironmentData(NaN), getEnvironmentData(0)); + console.log("object key", getEnvironmentData(key), getEnvironmentData({ k: 1 })); + console.log("same value", getEnvironmentData("shared") === shared); + setEnvironmentData("gone", 5); + setEnvironmentData("gone", undefined); + setEnvironmentData("gone too", 5); + setEnvironmentData("gone too"); + console.log("deleted", getEnvironmentData("gone"), getEnvironmentData("gone too")); + + const first = new Worker(new URL(import.meta.url), { workerData: "first" }); + setEnvironmentData("a", 2); + first.on("message", (m: unknown) => console.log("first", JSON.stringify(m))); + first.on("exit", (code: number) => { + console.log("first exit", code, getEnvironmentData("a"), getEnvironmentData("set by worker")); + const second = new Worker(new URL(import.meta.url), { workerData: "second" }); + second.on("message", (m: unknown) => console.log("second", JSON.stringify(m))); + second.on("exit", (c: number) => console.log("second exit", c)); + }); +} else if (workerData === "first" || workerData === "second") { + parentPort!.postMessage([ + getEnvironmentData("a"), + getEnvironmentData("config"), + getEnvironmentData(42), + getEnvironmentData(NaN), + getEnvironmentData(0), + getEnvironmentData("gone"), + ]); + setEnvironmentData("set by worker", true); + setEnvironmentData("a", 10); + if (workerData === "second") { + const nested = new Worker(new URL(import.meta.url), { workerData: "nested" }); + nested.on("message", (m: unknown) => parentPort!.postMessage({ nested: m })); + } +} else { + parentPort!.postMessage([getEnvironmentData("set by worker"), getEnvironmentData("a"), getEnvironmentData("shared")]); +} diff --git a/tests/corpus/5804-worker-environment-data.cjs b/tests/corpus/5804-worker-environment-data.cjs new file mode 100644 index 0000000000..e16fe248d1 --- /dev/null +++ b/tests/corpus/5804-worker-environment-data.cjs @@ -0,0 +1,26 @@ +'use strict'; +// Environment data in CommonJS: a worker sees its parent's entries, a +// value that cannot be cloned makes new Worker() throw DataCloneError, +// and the thread keeps its own entries after that failure. +const { Worker, getEnvironmentData, isMainThread, parentPort, setEnvironmentData } = require('node:worker_threads'); + +if (isMainThread) { + setEnvironmentData('greeting', 'hello'); + setEnvironmentData('list', [1, [2, 3]]); + const worker = new Worker(__filename); + worker.on('message', (m) => console.log('worker saw', JSON.stringify(m))); + worker.on('exit', (code) => { + console.log('exit', code); + setEnvironmentData('callback', () => 1); + try { + new Worker(__filename); + console.log('constructed'); + } catch (err) { + console.log('caught', err.name); + } + setEnvironmentData('callback'); + console.log('kept', getEnvironmentData('greeting'), typeof getEnvironmentData('callback')); + }); +} else { + parentPort.postMessage({ greeting: getEnvironmentData('greeting'), list: getEnvironmentData('list') }); +} diff --git a/tests/corpus/5805-worker-receive-message-parent-port.ts b/tests/corpus/5805-worker-receive-message-parent-port.ts new file mode 100644 index 0000000000..5cda1574d2 --- /dev/null +++ b/tests/corpus/5805-worker-receive-message-parent-port.ts @@ -0,0 +1,48 @@ +import { Worker, isMainThread, parentPort, receiveMessageOnPort, workerData } from "node:worker_threads"; + +// receiveMessageOnPort(parentPort) in a worker takes the parent's messages +// one at a time, before and between 'message' listener deliveries, and +// answers undefined once the queue is empty. The worker starts reading +// once the parent has posted its first three messages (flag[1]), so the +// second read does not depend on thread timing. +if (isMainThread) { + const flag = new Int32Array(new SharedArrayBuffer(8)); + const worker = new Worker(new URL(import.meta.url), { workerData: flag }); + worker.postMessage("a"); + worker.postMessage({ b: 2 }); + worker.postMessage("c"); + Atomics.store(flag, 1, 1); + Atomics.notify(flag, 1); + worker.on("message", (m: unknown) => { + console.log("main got", JSON.stringify(m)); + if (m === "ready") { + worker.postMessage("d"); + worker.postMessage("e"); + Atomics.store(flag, 0, 1); + Atomics.notify(flag, 0); + } + }); + worker.on("exit", (code: number) => console.log("exit", code)); +} else { + const flag = workerData as Int32Array; + const port = parentPort!; + Atomics.wait(flag, 1, 0); + let first = receiveMessageOnPort(port); + while (first === undefined) first = receiveMessageOnPort(port); + port.postMessage(["first", first]); + port.postMessage(["second", receiveMessageOnPort(port)]); + port.on("message", (v: unknown) => { + port.postMessage(["listener", v]); + if (v === "c") { + port.postMessage("ready"); + Atomics.wait(flag, 0, 0); + let next = receiveMessageOnPort(port); + while (next === undefined) next = receiveMessageOnPort(port); + port.postMessage(["sync", next]); + } + if (v === "e") { + port.postMessage(["empty", receiveMessageOnPort(port) === undefined]); + port.close(); + } + }); +} diff --git a/tests/corpus/5806-worker-resource-limits.ts b/tests/corpus/5806-worker-resource-limits.ts new file mode 100644 index 0000000000..a4f5f73906 --- /dev/null +++ b/tests/corpus/5806-worker-resource-limits.ts @@ -0,0 +1,38 @@ +import { Worker, isMainThread, parentPort, resourceLimits, workerData } from "node:worker_threads"; + +// resourceLimits is {} in the main thread and after a worker exits. A +// worker's resourceLimits.stackSizeMb sets its thread's stack size: deep +// recursion still ends in a catchable RangeError. A stackSizeMb of 0 keeps +// the default stack. +function depth(n: number, path: number[]): number { + path.push(n); + const result = n === 0 ? 0 : 1 + depth(n - 1, path); + path.pop(); + return result; +} + +if (isMainThread) { + console.log("main", JSON.stringify(resourceLimits), Object.keys(resourceLimits).length); + const worker = new Worker(new URL(import.meta.url), { + workerData: "small", + resourceLimits: { stackSizeMb: 1 }, + }); + worker.on("message", (m: unknown) => console.log("small stack", JSON.stringify(m))); + worker.on("exit", (code: number) => { + console.log("exit", code, JSON.stringify(worker.resourceLimits)); + const other = new Worker(new URL(import.meta.url), { + workerData: "default", + resourceLimits: { stackSizeMb: 0 }, + }); + other.on("message", (m: unknown) => console.log("default stack", JSON.stringify(m))); + other.on("exit", (c: number) => console.log("exit", c)); + }); +} else { + let overflow = "none"; + try { + depth(1e7, []); + } catch (error) { + if (error instanceof RangeError) overflow = error.name; + } + parentPort!.postMessage({ data: workerData, overflow, shallow: depth(1000, []) }); +} diff --git a/tests/corpus/5807-worker-error-values.ts b/tests/corpus/5807-worker-error-values.ts new file mode 100644 index 0000000000..e2edabfeea --- /dev/null +++ b/tests/corpus/5807-worker-error-values.ts @@ -0,0 +1,68 @@ +import { Worker, isMainThread, workerData } from "node:worker_threads"; + +// A worker's uncaught exception reaches the 'error' listener as Node's +// error serializer sends it: an Error keeps the nearest built-in +// constructor, its name (own or inherited), message, cause and other data +// properties with their enumerability; other values clone as messages do. +class ConfigError extends RangeError { + setting: string; + constructor(message: string, setting: string) { + super(message); + this.name = "ConfigError"; + this.setting = setting; + } +} + +const cases = ["props", "subclass", "aggregate", "cause", "string", "null", "number", "object", "runtime"]; + +if (isMainThread) { + let next = 0; + const run = (): void => { + const name = cases[next++]; + if (name === undefined) return; + const worker = new Worker(new URL(import.meta.url), { workerData: name }); + worker.on("error", (e: any) => { + if (typeof e !== "object" || e === null) { + console.log(name, typeof e, e); + return; + } + console.log(name, e instanceof Error, e instanceof RangeError, e instanceof TypeError, e.name, e.message); + console.log(" ", JSON.stringify(e), e.code, e.setting, e.errors?.length, e.cause?.message, e.cause?.detail); + }); + worker.on("exit", (code: number) => { + console.log(" ", "exit", code); + run(); + }); + }; + run(); +} else { + switch (workerData) { + case "props": { + const e: any = new Error("with props"); + e.code = "E_PROPS"; + e.extra = { y: 2 }; + throw e; + } + case "subclass": + throw new ConfigError("bad setting", "port"); + case "aggregate": + throw new AggregateError([new Error("a"), new TypeError("b")], "several"); + case "cause": { + const inner: any = new Error("inner"); + inner.detail = 7; + throw new Error("outer", { cause: inner }); + } + case "string": + throw "plain string"; + case "null": + throw null; + case "number": + throw 42; + case "object": + throw { a: 1, b: "x" }; + case "runtime": { + const value: any = JSON.parse("null"); + console.log(value.missing); + } + } +} diff --git a/tests/corpus/5808-worker-unhandled-rejections.ts b/tests/corpus/5808-worker-unhandled-rejections.ts new file mode 100644 index 0000000000..d133696713 --- /dev/null +++ b/tests/corpus/5808-worker-unhandled-rejections.ts @@ -0,0 +1,39 @@ +import { Worker, isMainThread, workerData } from "node:worker_threads"; + +// An unhandled rejection ends a worker with an 'error' event: an +// error-like reason as itself, any other reason as Node's +// UnhandledPromiseRejection Error quoting the reason. +const reasons = ["string", "number", "undefined", "null", "object", "array", "symbol", "bigint", "errorLike", "error"]; + +if (isMainThread) { + let next = 0; + const run = (): void => { + const reason = reasons[next++]; + if (reason === undefined) return; + const worker = new Worker(new URL(import.meta.url), { workerData: reason }); + worker.on("error", (e: any) => { + const at = typeof e.message === "string" ? e.message.indexOf("The promise") : -1; + const message = at < 0 ? e.message : e.message.slice(at); + console.log(reason, e instanceof Error, e.name, e.code, message, JSON.stringify(e)); + }); + worker.on("exit", (code: number) => { + console.log(" ", "exit", code); + run(); + }); + }; + run(); +} else { + const values: Record = { + string: "rejected", + number: 1.5, + undefined: undefined, + null: null, + object: { a: 1 }, + array: [1, [2, 3]], + symbol: Symbol("why"), + bigint: 10n, + errorLike: { stack: "custom stack", message: "like an error" }, + error: new RangeError("real error"), + }; + void Promise.reject(values[workerData as string]); +} diff --git a/tests/corpus/5809-process-exit-and-rejections.cjs b/tests/corpus/5809-process-exit-and-rejections.cjs new file mode 100644 index 0000000000..77b2a3609f --- /dev/null +++ b/tests/corpus/5809-process-exit-and-rejections.cjs @@ -0,0 +1,46 @@ +// @exit: 3 +'use strict'; +// process.exit(code) assigns process.exitCode first: a code that is not a +// safe integer throws a RangeError and execution continues. Valid codes +// convert to int32, which a worker's 'exit' event reports in full. An +// 'uncaughtException' listener receives an unhandled rejection with origin +// 'unhandledRejection': an Error reason as itself, any other reason as +// Node's UnhandledPromiseRejection Error quoting the reason. +const { Worker, isMainThread, workerData } = require('node:worker_threads'); + +const codes = [1.5, -1, 261, 2 ** 32 + 5]; + +if (isMainThread) { + process.on('uncaughtException', (err, origin) => { + const at = typeof err.message === 'string' ? err.message.indexOf('The promise') : -1; + console.log(origin, err instanceof Error, err.name, err.code, at < 0 ? err.message : err.message.slice(at)); + console.log(' ', JSON.stringify(err), typeof err === 'object' ? Object.keys(err).join(',') : typeof err); + }); + for (const code of [3.7, 2 ** 53, NaN]) { + try { + process.exit(code); + } catch (error) { + if (error instanceof RangeError) console.log(error.name, error.message); + } + } + void Promise.reject('main reason'); + setTimeout(() => void Promise.reject({ a: 1 }), 1); + setTimeout(() => void Promise.reject(new TypeError('typed reason')), 2); + setTimeout(() => { + throw 'thrown string'; + }, 3); + let next = 0; + const run = () => { + const code = codes[next++]; + if (code === undefined) process.exit(256 + 3); + const worker = new Worker(__filename, { workerData: code }); + worker.on('error', (e) => console.log(code, 'error', e.name, e.code)); + worker.on('exit', (exitCode) => { + console.log(code, 'exit', exitCode); + run(); + }); + }; + setTimeout(run, 20); +} else { + process.exit(workerData); +} diff --git a/tests/corpus/5813-mock-timers-module.ts b/tests/corpus/5813-mock-timers-module.ts new file mode 100644 index 0000000000..e9a3433360 --- /dev/null +++ b/tests/corpus/5813-mock-timers-module.ts @@ -0,0 +1,91 @@ +// The module's mock.timers outside a test: a debounce and a retry loop +// driven by tick and runAll, the order due timers run in, intervals that +// clear themselves, setTime, Node's errors, and real timers made before +// enable() that keep running through the loop. +import { mock } from "node:test"; + +function debounce(fn: (value: string) => void, ms: number): (value: string) => void { + let pending: ReturnType | null = null; + return (value: string) => { + if (pending !== null) clearTimeout(pending); + pending = setTimeout(() => { + pending = null; + fn(value); + }, ms); + }; +} + +const real = setTimeout(() => console.log("real timer fired"), 1); + +mock.timers.enable({ apis: ["setTimeout", "setInterval"] }); +clearTimeout(real); // the mocked clearTimeout leaves the real timer armed + +const save = debounce((value) => console.log(`saved ${value}`), 100); +save("a"); +mock.timers.tick(50); +save("ab"); +mock.timers.tick(99); +console.log("nothing saved yet"); +save("abc"); +mock.timers.tick(100); + +let attempts = 0; +function attempt(delay: number): void { + attempts++; + console.log(`attempt ${attempts} after ${delay}ms`); + if (attempts < 4) setTimeout(() => attempt(delay * 2), delay * 2); +} +setTimeout(() => attempt(10), 10); +mock.timers.runAll(); +console.log(`runAll stopped after ${attempts} attempts`); +mock.timers.runAll(); +mock.timers.runAll(); +mock.timers.runAll(); +console.log(`attempts ${attempts}`); + +const order: string[] = []; +setTimeout(() => order.push("omitted")); +setTimeout(() => order.push("zero"), 0); +setTimeout(() => order.push("negative"), -1); +setTimeout((tag: string) => order.push(tag), 0, "zero with argument"); +mock.timers.tick(0); +console.log(order.join(", ")); + +let beats = 0; +const heartbeat = setInterval(() => { + beats++; + if (beats === 5) clearInterval(heartbeat); +}, 30); +mock.timers.tick(1000); +console.log(`beats ${beats}`); + +const handle = setTimeout(() => console.log("refreshed timer fired once"), 20); +console.log(handle.hasRef(), handle.unref().hasRef(), handle.ref().hasRef()); +handle.refresh(); +mock.timers.tick(20); +mock.timers.tick(20); + +mock.timers.setTime(1_000_000); +setTimeout(() => console.log("fires after setTime"), 1); +mock.timers.tick(1); + +for (const run of [ + () => mock.timers.enable({ apis: ["setTimeout"] }), + () => mock.timers.tick(-2), + () => mock.timers.setTime(-0.5), +]) { + try { + run(); + } catch (error) { + const e = error as { name: string; code: string; message: string }; + console.log(`${e.name} ${e.code}: ${e.message}`); + } +} + +mock.timers.reset(); +try { + mock.timers.runAll(); +} catch (error) { + console.log((error as Error).message); +} +setTimeout(() => console.log("real again after reset"), 30); diff --git a/tests/corpus/5814-mock-timers-date.ts b/tests/corpus/5814-mock-timers-date.ts new file mode 100644 index 0000000000..494e3b7d8a --- /dev/null +++ b/tests/corpus/5814-mock-timers-date.ts @@ -0,0 +1,83 @@ +// The module's mock.timers for Date, setImmediate and AbortSignal.timeout: +// a cache whose entries expire by Date.now(), a batch that flushes on +// setImmediate, a request deadline from AbortSignal.timeout, and +// enable() without options, which mocks every API. +import { mock } from "node:test"; + +class TtlCache { + private entries = new Map(); + private ttl: number; + constructor(ttl: number) { + this.ttl = ttl; + } + set(key: string, value: string): void { + this.entries.set(key, { value, expires: Date.now() + this.ttl }); + } + get(key: string): string | undefined { + const entry = this.entries.get(key); + if (entry === undefined) return undefined; + if (Date.now() >= entry.expires) { + this.entries.delete(key); + return undefined; + } + return entry.value; + } +} + +mock.timers.enable({ apis: ["Date"], now: new Date("2024-05-01T12:00:00Z") }); +console.log(new Date().toISOString()); +const cache = new TtlCache(60_000); +cache.set("user", "ada"); +mock.timers.tick(59_999); +console.log(cache.get("user")); +mock.timers.tick(1); +console.log(cache.get("user")); +mock.timers.setTime(0); +console.log(Date.now(), new Date().toISOString()); +mock.timers.reset(); +console.log(Date.now() > 1_700_000_000_000); + +const batches: string[][] = []; +let pending: string[] = []; +function queue(item: string): void { + if (pending.length === 0) { + setImmediate(() => { + batches.push(pending); + pending = []; + }); + } + pending.push(item); +} + +mock.timers.enable({ apis: ["setImmediate"] }); +queue("a"); +queue("b"); +mock.timers.tick(0); +console.log(JSON.stringify(batches)); +queue("c"); +queue("d"); +console.log(JSON.stringify(pending)); +mock.timers.tick(0); +console.log(JSON.stringify(batches)); +const skipped = setImmediate(() => console.log("never")); +clearImmediate(skipped); +console.log(skipped.hasRef()); +mock.timers.runAll(); +mock.timers.reset(); + +mock.timers.enable(); +const deadline = AbortSignal.timeout(5_000); +deadline.addEventListener("abort", () => { + const reason = deadline.reason as { name: string; message: string }; + console.log(`aborted at ${Date.now()}: ${reason.name}: ${reason.message}`); +}); +mock.timers.tick(4_999); +console.log(deadline.aborted); +mock.timers.tick(1); +console.log(deadline.aborted); +mock.timers.reset(); + +const real = AbortSignal.timeout(1); +real.addEventListener("abort", () => { + console.log(`real deadline: ${(real.reason as { name: string }).name}`); +}); diff --git a/tests/corpus/5831-util-types-brands.ts b/tests/corpus/5831-util-types-brands.ts new file mode 100644 index 0000000000..d382009146 --- /dev/null +++ b/tests/corpus/5831-util-types-brands.ts @@ -0,0 +1,73 @@ +// util.types brand checks over typed values: isDate, isSharedArrayBuffer, +// isMapIterator, isSetIterator and isKeyObject answer from the value's +// internal brand, and isBoxedPrimitive (with the per-kind boxed checks), +// isBigInt64Array, isBigUint64Array, isCryptoKey and isExternal answer +// false for every value a program here can create. A Date-typed argument +// answers from its static type. +import { types } from "node:util"; +import * as direct from "node:util/types"; +import { createSecretKey } from "node:crypto"; + +const map = new Map([["a", 1]]); +const set = new Set([1, 2]); +const values: [string, unknown][] = [ + ["shared buffer", new SharedArrayBuffer(8)], + ["array buffer", new ArrayBuffer(8)], + ["map keys", map.keys()], + ["map entries", map.entries()], + ["map values", map.values()], + ["set values", set.values()], + ["set entries", set.entries()], + ["array values", [1, 2].values()], + ["map", map], + ["set", set], + ["key object", createSecretKey(Buffer.from("secret"))], + ["number", 1], + ["string", "s"], + ["boolean", true], + ["bigint", 1n], + ["symbol", Symbol("s")], + ["object", { getTime: () => 0 }], + ["uint8", new Uint8Array(2)], + ["null", null], + ["undefined", undefined], +]; + +function brands(value: unknown): string { + const hits: string[] = []; + if (types.isDate(value)) hits.push("isDate"); + if (types.isSharedArrayBuffer(value)) hits.push("isSharedArrayBuffer"); + if (types.isAnyArrayBuffer(value)) hits.push("isAnyArrayBuffer"); + if (types.isMapIterator(value)) hits.push("isMapIterator"); + if (types.isSetIterator(value)) hits.push("isSetIterator"); + if (types.isKeyObject(value)) hits.push("isKeyObject"); + if (types.isBoxedPrimitive(value)) hits.push("isBoxedPrimitive"); + if (types.isNumberObject(value)) hits.push("isNumberObject"); + if (types.isStringObject(value)) hits.push("isStringObject"); + if (types.isBooleanObject(value)) hits.push("isBooleanObject"); + if (types.isBigIntObject(value)) hits.push("isBigIntObject"); + if (types.isSymbolObject(value)) hits.push("isSymbolObject"); + if (types.isBigInt64Array(value)) hits.push("isBigInt64Array"); + if (types.isBigUint64Array(value)) hits.push("isBigUint64Array"); + if (types.isFloat16Array(value)) hits.push("isFloat16Array"); + if (types.isCryptoKey(value)) hits.push("isCryptoKey"); + if (types.isExternal(value)) hits.push("isExternal"); + return hits.join(",") || "-"; +} + +for (const [label, value] of values) console.log(label, brands(value)); + +let reads = 0; +function date(ms: number): Date { + reads++; + return new Date(ms); +} +console.log("date", types.isDate(date(0)), types.isDate(date(Number.NaN)), direct.isDate(date(1))); +console.log("date others", types.isMap(date(0)), types.isRegExp(date(0)), types.isBoxedPrimitive(date(0)), types.isNativeError(date(0)), reads); + +// The node:util/types spellings and a consumed iterator. +const it = map.keys(); +for (const _ of it) { + // drain +} +console.log("drained", direct.isMapIterator(it), direct.isSetIterator(set.keys())); diff --git a/tests/corpus/5832-util-types-brands-dynamic.js b/tests/corpus/5832-util-types-brands-dynamic.js new file mode 100644 index 0000000000..cc8688f5d1 --- /dev/null +++ b/tests/corpus/5832-util-types-brands-dynamic.js @@ -0,0 +1,31 @@ +// util.types brand checks over checked-dynamic values: Dates, Map and Set +// iterators (before and after they are consumed), SharedArrayBuffer and +// KeyObject carry their brands; arrays, plain objects and objects that +// imitate a Date or an iterator do not. +import { types } from "node:util"; +import { createSecretKey } from "node:crypto"; + +const map = new Map([["a", 1]]); +const set = new Set([1, 2]); +const drained = set.values(); +for (const _ of drained) { + // drain +} +const values = [ + ["date", new Date(0)], + ["map keys", map.keys()], + ["map iterator", map[Symbol.iterator]()], + ["set values", set.values()], + ["drained set iterator", drained], + ["array iterator", [1].values()], + ["shared buffer", new SharedArrayBuffer(4)], + ["key object", createSecretKey(Buffer.from("k"))], + ["date-like", { getTime() { return 0; }, [Symbol.toStringTag]: "Date" }], + ["iterator-like", { next() { return { done: true }; }, [Symbol.toStringTag]: "Map Iterator" }], + ["array", [1, 2]], + ["number", 1], +]; +const probes = ["isDate", "isMapIterator", "isSetIterator", "isSharedArrayBuffer", "isKeyObject", "isBoxedPrimitive", "isNumberObject", "isBigInt64Array", "isExternal"]; +for (const [label, value] of values) { + console.log(label, probes.filter((name) => types[name](value)).join(",") || "-"); +} diff --git a/tests/corpus/5833-url-search-params-iterators.ts b/tests/corpus/5833-url-search-params-iterators.ts new file mode 100644 index 0000000000..eb0b99d33a --- /dev/null +++ b/tests/corpus/5833-url-search-params-iterators.ts @@ -0,0 +1,40 @@ +// URLSearchParams keys(), values() and entries() outside a for-of head: +// a stored iterator steps with next() ({ value, done }), spreads from its +// current position, and walks the live pair list by index, so pairs +// appended after it was created are visited. Spreads, Array.from, Object.fromEntries and +// new Map over the params or their iterators read the same pairs. +const sp = new URLSearchParams("b=2&a=1&a=3&c=%20x"); + +const keys = sp.keys(); +console.log(JSON.stringify(keys.next()), JSON.stringify(keys.next())); +sp.append("d", "4"); +console.log(JSON.stringify([...keys])); +console.log(JSON.stringify(keys.next())); + +console.log(JSON.stringify([...sp])); +console.log(JSON.stringify([...sp.keys()])); +console.log(JSON.stringify([...sp.values()])); +console.log(JSON.stringify([...sp.entries()])); +console.log(JSON.stringify(Array.from(sp))); +console.log(JSON.stringify(Array.from(sp.entries()))); +console.log(JSON.stringify(Array.from(sp.values()))); +console.log(JSON.stringify(Object.fromEntries(sp))); +console.log(JSON.stringify(Object.fromEntries(sp.entries()))); +console.log(JSON.stringify([...new Map(sp)])); +console.log(JSON.stringify([...new Map(sp.entries())])); + +const values = sp.values(); +const seen: string[] = []; +for (const v of values) { + seen.push(v); + if (v === "1") sp.delete("b"); +} +console.log(JSON.stringify(seen), sp.toString()); + +const entries = new URL("https://h.example/p?x=1&y=2&x=3").searchParams.entries(); +const pairs: string[] = []; +for (const [k, v] of entries) pairs.push(`${k}:${v}`); +console.log(pairs.join(" ")); + +const empty = new URLSearchParams().entries(); +console.log(JSON.stringify(empty.next()), JSON.stringify([...empty])); diff --git a/tests/corpus/5834-url-search-params-iterators-dynamic.mjs b/tests/corpus/5834-url-search-params-iterators-dynamic.mjs new file mode 100644 index 0000000000..82b0c6d287 --- /dev/null +++ b/tests/corpus/5834-url-search-params-iterators-dynamic.mjs @@ -0,0 +1,22 @@ +// URLSearchParams iterators in a checked-dynamic program: next() results, +// live iteration after append and delete, and the consumers (spread, +// Array.from with and without a mapper, Object.fromEntries, new Map) over +// the params and each projection. +const sp = new URLSearchParams("b=2&a=1&a=3"); +const it = sp.keys(); +console.log(it.next()); +sp.append("c", "4"); +console.log(JSON.stringify([...it]), it.next()); +console.log(JSON.stringify([...sp.values()]), JSON.stringify(Array.from(sp.entries()))); +console.log(JSON.stringify(Array.from(sp.keys(), (k) => k.toUpperCase()))); +console.log(JSON.stringify(Object.fromEntries(sp.entries())), JSON.stringify(Object.fromEntries(sp))); +console.log(JSON.stringify([...new Map(sp.entries())]), new Map(sp).size); +const values = sp.values(); +const seen = []; +let step = values.next(); +while (!step.done) { + seen.push(step.value); + if (step.value === "2") sp.delete("a"); + step = values.next(); +} +console.log(JSON.stringify(seen), sp.toString()); diff --git a/tests/corpus/5835-util-format-integer-specifiers.ts b/tests/corpus/5835-util-format-integer-specifiers.ts new file mode 100644 index 0000000000..ed5648761e --- /dev/null +++ b/tests/corpus/5835-util-format-integer-specifiers.ts @@ -0,0 +1,29 @@ +// util.format and console.log %d / %i over arguments Node formats without a +// number conversion: a bigint prints with its n suffix, a symbol as NaN, +// null as 0 under %d and NaN under %i, and undefined as NaN. Arguments still +// evaluate in order. +import { format } from "node:util"; + +const calls: string[] = []; +function big(n: bigint): bigint { + calls.push(`big ${n}`); + return n; +} +function sym(): symbol { + calls.push("sym"); + return Symbol("s"); +} +function nothing(): undefined { + calls.push("nothing"); + return undefined; +} + +console.log(format("%d %i", 10n, -5n)); +console.log(format("%d", 2n ** 70n), format("%i", -(2n ** 65n))); +console.log(format("%d|%i", 0n, 0n)); +console.log(format("%d %i", Symbol("x"), Symbol.iterator)); +console.log(format("%d %i", null, null)); +console.log(format("%d %i", undefined, undefined)); +console.log(format("%d-%i-%d", big(3n), sym(), nothing()), calls.join(", ")); +console.log("count %d of %i", 42n, 7n); +console.log("missing %d", null, "extra"); diff --git a/tests/corpus/5836-url-search-params-tuple-pairs.ts b/tests/corpus/5836-url-search-params-tuple-pairs.ts new file mode 100644 index 0000000000..d6fe1a86a8 --- /dev/null +++ b/tests/corpus/5836-url-search-params-tuple-pairs.ts @@ -0,0 +1,34 @@ +// new URLSearchParams(pairs) over [string, string] tuple rows, such as an +// Object.entries result: each pair appends its name and value in array +// order, so repeated names keep every value and an empty array gives empty +// params. The params are then as mutable as any other. +const record: Record = { a: "1", "b c": "x&y", é: "ü" }; +const fromEntries = new URLSearchParams(Object.entries(record)); +console.log(fromEntries.toString(), fromEntries.size); + +const pairs: [string, string][] = [ + ["k", "v"], + ["k", "w"], + ["", ""], + ["plus", "a+b c"], +]; +const repeated = new URLSearchParams(pairs); +console.log(repeated.toString(), JSON.stringify(repeated.getAll("k"))); + +const none: [string, string][] = []; +const empty = new URLSearchParams(none); +console.log(JSON.stringify(empty.toString()), empty.size); + +const edited = new URLSearchParams(pairs); +edited.append("y", "2"); +edited.delete("k"); +edited.sort(); +console.log(edited.toString(), pairs.length); + +const lone: [string, string][] = [["z", "\uD800x"]]; +console.log(new URLSearchParams(lone).toString()); + +const url = new URL("https://example.test/?q=1"); +for (const [name, value] of new URLSearchParams(Object.entries({ q: "2", r: "3" }))) + url.searchParams.set(name, value); +console.log(url.href); diff --git a/tests/corpus/5837-util-format-json-circular.ts b/tests/corpus/5837-util-format-json-circular.ts new file mode 100644 index 0000000000..5e79c7ce24 --- /dev/null +++ b/tests/corpus/5837-util-format-json-circular.ts @@ -0,0 +1,67 @@ +// util.format, formatWithOptions and console.log %j over circular values: +// Node's tryStringify prints "[Circular]" when JSON.stringify throws the +// circular-structure TypeError, and rethrows every other error. A checked- +// dynamic argument stringifies as JSON.stringify does (toJSON hooks, +// dropped members, shared references); a recursive typed value prints +// "[Circular]" where JSON.stringify throws. +import { format, formatWithOptions } from "node:util"; + +const parsed = JSON.parse('{"name":"root","children":[{"name":"leaf"}]}'); +parsed.children[0].parent = parsed; +console.log(format("%j", parsed)); +console.log(format("%j|%j", parsed.children, parsed.children[0].name)); +console.log("%j", parsed); + +const bag: Record = { id: 1 }; +bag.self = bag; +console.log(format("%j", bag), formatWithOptions({}, "[%j]", bag)); + +const list: unknown[] = [1, 2]; +list.push(list); +console.log(format("%j", list), format("%j", [list])); + +const shared = { s: 1 }; +const twice: unknown = { a: shared, b: shared }; +console.log(format("%j", twice)); + +const hooked: unknown = { toJSON: (key: string) => `toJSON(${JSON.stringify(key)})` }; +const dropped: unknown = { skip: undefined, n: NaN, f: () => 1 }; +console.log(format("%j %j", hooked, dropped)); + +const big: unknown = { n: 1n }; +try { + format("%j", big); +} catch (error) { + console.log("rethrown", (error as Error).name, (error as Error).message); +} +const ranged: unknown = { + toJSON: () => { + throw new RangeError("Converting circular structure to JSON"); + }, +}; +try { + format("%j", ranged); +} catch (error) { + console.log("rethrown", (error as Error).name); +} +const firstLine: unknown = { + toJSON: () => { + throw new TypeError("Converting circular structure to JSON\n --> elsewhere"); + }, +}; +console.log(format("%j", firstLine)); + +interface Item { + name: string; + next: Item | null; +} +const item: Item = { name: "a", next: null }; +console.log(format("%j", item)); +item.next = { name: "b", next: item }; +console.log(format("%j", item), format("%j", [item])); +console.log("%j and %j", { item }, 5); +try { + JSON.stringify(item); +} catch (error) { + console.log((error as Error).name, (error as Error).message.split("\n")[0]); +} diff --git a/tests/corpus/5838-qs-stringify-bigint.ts b/tests/corpus/5838-qs-stringify-bigint.ts new file mode 100644 index 0000000000..cdc668a556 --- /dev/null +++ b/tests/corpus/5838-qs-stringify-bigint.ts @@ -0,0 +1,19 @@ +// querystring.stringify writes a bigint value as its decimal digits, bare +// like a boolean (Node's encodeStringified), in plain and array-valued +// fields and under custom separators. url.format's query object follows +// the same rule. +import { encode, stringify } from "node:querystring"; +import { format } from "node:url"; + +console.log(stringify({ a: 1n, b: -2n, c: 0n })); +console.log(stringify({ big: 12345678901234567890n, neg: -(2n ** 70n) })); +console.log(stringify({ list: [1n, "two", 3, true, -4n] })); +console.log(stringify({ a: 5n, b: [6n, 7n] }, ";", ":")); +console.log(encode({ id: 42n, name: "x y" })); + +const counts: Record = { first: 10n, second: 20n }; +console.log(stringify(counts)); +const mixed: Record = { n: 1n, s: "v", z: null }; +console.log(stringify(mixed)); + +console.log(format({ protocol: "https", host: "example.test", pathname: "/p", query: { id: 99n, tag: ["a", 2n] } })); diff --git a/tests/corpus/5841-json-typed-dyn-members.ts b/tests/corpus/5841-json-typed-dyn-members.ts new file mode 100644 index 0000000000..e8f7f02bb2 --- /dev/null +++ b/tests/corpus/5841-json-typed-dyn-members.ts @@ -0,0 +1,76 @@ +// JSON.stringify over typed records whose members are checked-dynamic +// values: the undeclared properties a checked record keeps from its source, +// and the entries of an `unknown` index signature. Each member follows +// SerializeJSONProperty: toJSON runs with the member's key, getters are +// read, a Buffer becomes its { type, data } form, undefined, function and +// symbol results drop, and a circular structure throws V8's TypeError. +type Point = { a: number }; +type Bag = { a: number; [key: string]: unknown }; + +const report = (label: string, run: () => string | undefined): void => { + try { + console.log(label, run()); + } catch (error) { + const e = error as Error; + console.log(label, e.name); + console.log(e.message); + } +}; + +const source: any = JSON.parse('{"a":1,"b":"two"}'); +source.stamp = { toJSON: (key: string) => `stamp:${key}` }; +source.gone = { toJSON: () => undefined }; +source.bytes = Buffer.from("hi"); +Object.defineProperty(source, "computed", { get: () => 42, enumerable: true }); +source.list = [1, { toJSON: (key: string) => `item:${key}` }]; +const point = source as Point; +report("source", () => JSON.stringify(point)); +report("source pretty", () => JSON.stringify(point, null, 2)); +report("source nested", () => JSON.stringify({ point, n: 2 })); +report("source array", () => JSON.stringify([point, point])); + +const bag: Bag = { a: 1 }; +bag.stamp = { toJSON: (key: string) => `stamp:${key}` }; +bag.gone = { toJSON: () => undefined }; +bag.data = JSON.parse('{"x":[1,2,{"y":null}]}'); +bag.nothing = undefined; +bag.order = JSON.parse('{"b":1,"2":"two","a":2,"0":"zero"}'); +const layered: any = JSON.parse('{"level":{"items":[1,{"x":null}]}}'); +layered.level.items.push({ toJSON: (key: string) => `deep:${key}` }); +bag.deep = layered; +const hidden: any = JSON.parse('{"shown":1}'); +Object.defineProperty(hidden, "hidden", { value: 2, enumerable: false }); +bag.hidden = hidden; +let nested: any = JSON.parse('{"end":true}'); +for (let i = 0; i < 40; i++) nested = { i, nested }; +bag.nested = nested; +report("bag", () => JSON.stringify(bag)); +report("bag in array", () => JSON.stringify([bag])); + +const self: any = JSON.parse('{"k":1}'); +self.self = self; +const cyclic: any = JSON.parse('{"a":1,"x":{"k":2}}'); +cyclic.x.self = cyclic.x; +report("source circular", () => JSON.stringify(cyclic as Point)); +const deep: any = JSON.parse('{"a":1,"x":{"y":{"z":{"w":{"v":{}}}}}}'); +deep.x.y.z.w.v.back = deep.x; +report("source deep circular", () => JSON.stringify(deep as Point)); +const circular: Bag = { a: 1 }; +circular.loop = self; +report("bag circular", () => JSON.stringify(circular)); +report("bag circular nested", () => JSON.stringify({ outer: circular })); + +const shared: any = JSON.parse('{"v":1}'); +const twice: Bag = { a: 1, first: shared, second: shared }; +report("bag shared", () => JSON.stringify(twice)); + +const big: any = JSON.parse('{"a":1}'); +big.n = 1n; +report("source bigint", () => JSON.stringify(big as Point)); +const throwing: Bag = { a: 1 }; +throwing.bad = { + toJSON: () => { + throw new RangeError("from toJSON"); + }, +}; +report("bag throwing", () => JSON.stringify(throwing)); diff --git a/tests/corpus/5842-qs-parse-null-prototype.ts b/tests/corpus/5842-qs-parse-null-prototype.ts new file mode 100644 index 0000000000..02d5cc1346 --- /dev/null +++ b/tests/corpus/5842-qs-parse-null-prototype.ts @@ -0,0 +1,34 @@ +// querystring.parse and querystring.decode return a null-prototype +// object: inspection prints the "[Object: null prototype]" base, +// getPrototypeOf answers null, and ToPrimitive and Object.prototype +// methods throw TypeErrors. Spread copies keep Object.prototype. +import * as qs from "node:querystring"; +import { inspect } from "node:util"; + +const parsed = qs.parse("a=1&b=2&b=3&c"); +console.log(parsed); +console.log(inspect(parsed), inspect({ nested: parsed }), inspect([qs.parse("")])); +console.log(qs.decode("x=1;y=2", ";")); +console.log(Object.getPrototypeOf(parsed) === null, Object.keys(parsed), JSON.stringify(parsed)); + +const attempt = (label: string, f: () => unknown): void => { + try { + console.log(label, f()); + } catch (e) { + console.log(label, (e as Error).name, (e as Error).message); + } +}; +attempt("String", () => String(parsed)); +attempt("template", () => `${parsed}`); +attempt("concat", () => "q=" + parsed); +attempt("hasOwnProperty", () => parsed.hasOwnProperty("a")); +attempt("toString", () => parsed.toString()); +console.log(Object.prototype.toString.call(parsed), Object.hasOwn(parsed, "b")); +console.log(Object.prototype.hasOwnProperty.call(parsed, "c")); + +const copy = { ...parsed }; +console.log(copy, Object.getPrototypeOf(copy) === Object.prototype, String(copy)); + +parsed.added = "yes"; +delete parsed.a; +console.log(parsed, parsed.b, parsed.added); diff --git a/tests/corpus/5843-qs-parse-null-prototype-js.mjs b/tests/corpus/5843-qs-parse-null-prototype-js.mjs new file mode 100644 index 0000000000..676d188c4a --- /dev/null +++ b/tests/corpus/5843-qs-parse-null-prototype-js.mjs @@ -0,0 +1,12 @@ +// querystring.parse results keep their null prototype when they cross into +// checked-dynamic values in a JavaScript module: inspection, getPrototypeOf, +// ToPrimitive and the `in` operator. +import qs from "node:querystring"; +const parsed = qs.parse("a=1&b=2&b=3"); +const holder = { parsed, list: [parsed] }; +console.log(holder); +const dyn = JSON.parse("{}"); +dyn.p = parsed; +console.log(dyn, Object.getPrototypeOf(dyn.p) === null); +try { console.log(String(dyn.p)); } catch (e) { console.log(e.name, e.message); } +console.log("toString" in parsed, "a" in parsed); diff --git a/tests/corpus/5844-date-unknown-values.ts b/tests/corpus/5844-date-unknown-values.ts new file mode 100644 index 0000000000..e5b1aed5f1 --- /dev/null +++ b/tests/corpus/5844-date-unknown-values.ts @@ -0,0 +1,57 @@ +// Date values converted into `unknown` slots stay Dates: typeof answers +// "object", instanceof Date holds, util.inspect prints the ISO form or +// "Invalid Date", JSON.stringify calls toJSON (null for an invalid date), +// a method read through an untyped reference is a function that takes a +// Date receiver, and the value converts back to a typed Date with the same +// time value. +import { inspect } from "node:util"; + +const epoch = new Date(0); +const bare: unknown = epoch; +const record: unknown = { when: new Date(86_400_000), n: 1 }; +const list: unknown = [new Date(Date.UTC(2024, 1, 29, 12, 30)), new Date(Number.NaN)]; +const bag: Record = { at: epoch }; + +console.log(typeof bare, bare instanceof Date, inspect(bare), JSON.stringify(bare)); +console.log(record, JSON.stringify(record), Object.keys(record as object)); +console.log(list, JSON.stringify(list)); +console.log(bag, JSON.stringify(bag)); + +const when = (record as { when: unknown }).when; +console.log(typeof when, when instanceof Date); +if (when instanceof Date) console.log(when.getTime(), when.toISOString(), when.getUTCDate()); +const back = bare as Date; +console.log(back.getTime(), back.toISOString()); +const dynamic = record as any; +console.log(dynamic.when.getTime(), dynamic.when.getUTCFullYear(), String(dynamic.when instanceof Date)); +const read = dynamic.when.getUTCHours; +console.log(typeof read, read.name, read.call(dynamic.when), dynamic.when.missing); +try { + read.call({}); +} catch (error) { + console.log((error as Error).name, (error as Error).message); +} + +type Event = { at: Date; tags: string[] }; +const events: Event[] = [ + { at: new Date(0), tags: ["a"] }, + { at: new Date(7), tags: [] }, +]; +const eventsUnknown: unknown = events; +console.log(eventsUnknown, JSON.stringify(eventsUnknown)); +console.log((eventsUnknown as Event[])[1]!.at.toISOString()); +class Holder { + value: unknown = null; +} +const holder = new Holder(); +holder.value = new Date(1000); +console.log(holder, JSON.stringify(holder.value)); +const invalid: any = { d: new Date(Number.NaN) } as unknown; +try { + console.log(invalid.d.toISOString()); +} catch (error) { + console.log((error as Error).name, (error as Error).message); +} +console.log(invalid.d.getTime(), JSON.stringify(invalid)); +const stamped: Readonly = new Date(0); +console.log(stamped.getTime(), stamped.toISOString()); diff --git a/tests/corpus/5846-events-esm-default-max.ts b/tests/corpus/5846-events-esm-default-max.ts new file mode 100644 index 0000000000..acc57328ad --- /dev/null +++ b/tests/corpus/5846-events-esm-default-max.ts @@ -0,0 +1,28 @@ +// events.defaultMaxListeners through the module's ES module namespace and +// named imports reads the namespace's copy, which keeps the value from +// program start until module.syncBuiltinESMExports() refreshes it. The +// default import and the EventEmitter class read the current default. +import EventEmitterDefault, { defaultMaxListeners, EventEmitter, setMaxListeners } from "node:events"; +import * as events from "node:events"; +import { syncBuiltinESMExports } from "node:module"; + +const show = (label: string): void => { + console.log( + label, + events.defaultMaxListeners, + defaultMaxListeners, + EventEmitterDefault.defaultMaxListeners, + EventEmitter.defaultMaxListeners, + ); +}; +show("start"); +setMaxListeners(20); +show("after setMaxListeners"); +EventEmitter.defaultMaxListeners = 7; +show("after assignment"); +syncBuiltinESMExports(); +show("after sync"); +events.setMaxListeners(3); +show("after namespace setMaxListeners"); +const emitter = new EventEmitter(); +console.log(emitter.getMaxListeners(), events.getMaxListeners(emitter)); diff --git a/tests/corpus/5847-events-cjs-default-max.cjs b/tests/corpus/5847-events-cjs-default-max.cjs new file mode 100644 index 0000000000..9edbe811d3 --- /dev/null +++ b/tests/corpus/5847-events-cjs-default-max.cjs @@ -0,0 +1,10 @@ +// CommonJS bindings of node:events read defaultMaxListeners from the module +// object, which is the EventEmitter class: every read sees the current +// default. +const events = require("node:events"); +const { EventEmitter, setMaxListeners } = require("node:events"); +console.log(events.defaultMaxListeners, EventEmitter.defaultMaxListeners); +setMaxListeners(15); +console.log(events.defaultMaxListeners, EventEmitter.defaultMaxListeners, require("events").defaultMaxListeners); +events.defaultMaxListeners = 4; +console.log(events.defaultMaxListeners, EventEmitter.defaultMaxListeners, new EventEmitter().getMaxListeners()); diff --git a/tests/corpus/5857-regexp-unicode-sets.ts b/tests/corpus/5857-regexp-unicode-sets.ts new file mode 100644 index 0000000000..3bfdaf6922 --- /dev/null +++ b/tests/corpus/5857-regexp-unicode-sets.ts @@ -0,0 +1,48 @@ +// The regex v flag (unicode sets): class subtraction and intersection, nested +// classes, \q{...} string literals, properties of strings, case folding, +// code-point iteration, and the constructor with literal and computed flags. +// Regex literals report their flags in getter order. Node.js prints the same. +const letters = /[\p{L}--[a-z]]/gv; +console.log(letters.flags, letters.source, String(letters), letters); +console.log("aBcD-É".match(letters)); +console.log(/[[a-z]&&[aeiou]]/v.test("e"), /[[a-z]&&[aeiou]]/v.test("b")); +console.log("vowels: education".replace(/[[a-z]&&[^aeiou]]/gv, "_")); +console.log("x😀y".match(/[\q{😀|xy}]/gv), "xy😀".match(/[\q{😀|xy}]/gv)); +console.log("abcab".match(/[\q{abc}a]/gv)); +console.log(/^\p{RGI_Emoji}$/v.test("👍🏽"), /^\p{RGI_Emoji}$/v.test("a")); +console.log("hi 🇫🇷 1️⃣".match(/\p{RGI_Emoji_Flag_Sequence}|\p{Emoji_Keycap_Sequence}/gv)); +console.log("family \u{1F468}\u200D\u{1F469}\u200D\u{1F467} ok".replace(/\p{RGI_Emoji_ZWJ_Sequence}/v, "[zwj]")); +console.log(/[^\d--[5]]/v.test("5"), /[^\d--[5]]/v.test("7"), /[^\d--[5]]/v.test("x")); +console.log(/[[0-9]--[5-9]]+/v.exec("98123")?.[0]); +console.log(/[\p{Lu}--[A-C]]/vi.test("a"), /[\p{Lu}--[A-C]]/vi.test("d"), /[^\p{Lu}]/vi.test("A")); +console.log("😀😀".replace(/(?:)/gv, "-"), "😀😀".split(/(?:)/v)); +console.log("a😀b".replace(/./gv, (c) => `<${c}>`)); +console.log("A1b2C3".replace(/[\w--\d]/gv, (c) => c.toLowerCase())); +console.log("one, two;three".split(/[[\p{P}\s]--[\-]]+/v)); +const m = /(?[\p{L}--\p{Lu}]+)-(?[\d--[0]]+)/v.exec("ABcd-102"); +if (m) console.log(m[0], m.index, m.groups?.word, m.groups?.num); +console.log("2024-07".replace(/(?[\d]{4})-(?\d\d)/v, "$/$")); + +const spelled = /x/vgimsy; +console.log(spelled.flags, String(spelled), /x/yg.flags, /x/mi.flags); +console.log(letters.global, letters.unicodeSets, letters.unicode, letters.ignoreCase, letters.hasIndices); +console.log(spelled.multiline, spelled.dotAll, spelled.sticky, /x/u.unicode, /x/u.unicodeSets); + +console.log(new RegExp("[\\w--\\d]", "v").test("5"), new RegExp("[\\w--\\d]", "vg").flags); +const computed = ["v", "gv", "iv"]; +for (const flags of computed) { + const re = new RegExp("[[0-9a-f]--[5]]", flags); + console.log(flags, re.flags, re.test("5"), re.test("F")); +} +for (const flags of ["uv", "vv"]) { + try { + new RegExp("a", flags); + } catch (e) { + console.log((e as Error).name, (e as Error).message); + } +} +const u: unknown = /[\q{abc}]/v; +console.log(u instanceof RegExp, String(u)); +// Punctuation that only class contents reserve stays literal outside them. +console.log(/x-y/v.test("x-y"), /a,,b/v.test("a,,b"), /a&&b|!!/v.exec("!!")?.[0], /a--b/v.test("a--b")); +console.log("a-b".match(/[\q{\-}]/gv), "key-value".split(/-/v)); diff --git a/tests/corpus/5858-disposable-stack.ts b/tests/corpus/5858-disposable-stack.ts new file mode 100644 index 0000000000..7497309977 --- /dev/null +++ b/tests/corpus/5858-disposable-stack.ts @@ -0,0 +1,157 @@ +// DisposableStack: use, adopt, defer, move and dispose with class instances, +// object literals and callbacks; LIFO disposal; SuppressedError chains; +// Node's ReferenceError and TypeError texts; using over a stack; inspection; +// and stacks whose callbacks capture the stack. Node.js prints the same. +const log: string[] = []; +class Resource { + readonly name: string; + constructor(name: string) { + this.name = name; + } + [Symbol.dispose](): void { + log.push("dispose " + this.name); + } +} +class Failing { + readonly name: string; + constructor(name: string) { + this.name = name; + } + [Symbol.dispose](): void { + throw new Error("fail " + this.name); + } +} +function scope(): void { + using stack = new DisposableStack(); + const a = stack.use(new Resource("a")); + log.push("got " + a.name); + stack.defer(() => log.push("deferred")); + const n = stack.adopt(42, (v) => log.push("adopted " + v)); + const r = stack.adopt(new Resource("adopted-res"), (v) => log.push("adopt " + v.name)); + stack.use({ [Symbol.dispose]() { log.push("literal"); } }); + stack.use(null); + stack.use(undefined); + log.push("n=" + n + " r=" + r.name + " disposed=" + stack.disposed); +} +scope(); +console.log(log.join("\n")); +log.length = 0; + +const outer = new DisposableStack(); +outer.use(new Resource("x")); +const moved = outer.move(); +console.log(outer.disposed, moved.disposed, outer instanceof DisposableStack, moved instanceof DisposableStack); +moved.dispose(); +moved.dispose(); +console.log(log.join(","), moved.disposed); +log.length = 0; + +function attempt(label: string, f: () => void): void { + try { + f(); + console.log(label, "ok"); + } catch (e) { + if (e instanceof SuppressedError) { + const chain: string[] = []; + let cur: unknown = e; + while (cur instanceof SuppressedError) { + chain.push((cur.error as Error).message); + cur = cur.suppressed; + } + chain.push(cur instanceof Error ? cur.message : String(cur)); + console.log(label, "SuppressedError", e.message, chain.join(" <- ")); + } else if (e instanceof Error) { + console.log(label, e.name, e.message); + } else { + console.log(label, "thrown", e); + } + } +} +attempt("three failures", () => { + const s = new DisposableStack(); + s.use(new Failing("one")); + s.defer(() => { throw new Error("two"); }); + s.use(new Failing("three")); + s.dispose(); +}); +attempt("single", () => { + const s = new DisposableStack(); + s.use(new Resource("before")); + s.defer(() => { throw new RangeError("only"); }); + s.dispose(); +}); +console.log(log.join(",")); +attempt("thrown value", () => { + const s = new DisposableStack(); + s.defer(() => { throw 7; }); + s.dispose(); +}); +attempt("using body error", () => { + using s = new DisposableStack(); + s.defer(() => { throw new Error("disposal"); }); + throw new Error("body"); +}); +const done = new DisposableStack(); +done.dispose(); +attempt("use after dispose", () => { done.use(new Resource("late")); }); +attempt("defer after dispose", () => done.defer(() => {})); +attempt("adopt after dispose", () => { done.adopt(1, () => {}); }); +attempt("move after dispose", () => { done.move(); }); +attempt("dispose twice", () => done.dispose()); +attempt("use non-object", () => { new DisposableStack().use(5 as unknown as Disposable); }); +attempt("use missing method", () => { new DisposableStack().use({} as unknown as Disposable); }); +attempt("defer non-function", () => new DisposableStack().defer(3 as unknown as () => void)); +attempt("adopt non-function", () => { new DisposableStack().adopt(1, "x" as unknown as () => void); }); +const reentrant = new DisposableStack(); +reentrant.defer(() => { + console.log("inside disposed", reentrant.disposed); + attempt("defer during dispose", () => reentrant.defer(() => {})); +}); +reentrant.dispose(); +const shown = new DisposableStack(); +console.log(shown, String(shown), Object.prototype.toString.call(shown), typeof shown, JSON.stringify(shown)); +const self = new DisposableStack(); +self.defer(() => console.log("self disposed", self.disposed)); +self[Symbol.dispose](); +const nested = new DisposableStack(); +const inner = nested.use(new DisposableStack()); +inner.defer(() => console.log("inner via outer")); +nested.dispose(); +console.log(inner.disposed); +class Base { + readonly name: string; + constructor(name: string) { + this.name = name; + } + [Symbol.dispose](): void { + console.log("base dispose", this.name); + } +} +class Derived extends Base { + override [Symbol.dispose](): void { + console.log("derived dispose", this.name); + } +} +class Plain extends Base {} +const inherit = new DisposableStack(); +const items: Base[] = [new Derived("d"), new Plain("p"), new Base("b")]; +for (const item of items) inherit.use(item); +const back = inherit.use(new Derived("returned")); +console.log(back.name, back instanceof Derived); +inherit.dispose(); +// A stack whose callback captures the stack is collected without disposal. +for (let i = 0; i < 3; i++) { + const cyc = new DisposableStack(); + cyc.defer(() => console.log("never", cyc.disposed)); +} +const holder = { stack: new DisposableStack() }; +console.log(holder, [holder.stack]); +function make(): DisposableStack { + const st = new DisposableStack(); + st.defer(() => console.log("made stack disposed")); + return st; +} +{ + using made = make(); + console.log("in block", made.disposed); +} diff --git a/tests/corpus/5859-disposable-stack-js.js b/tests/corpus/5859-disposable-stack-js.js new file mode 100644 index 0000000000..491beee163 --- /dev/null +++ b/tests/corpus/5859-disposable-stack-js.js @@ -0,0 +1,77 @@ +// DisposableStack from JavaScript: untyped resources and callbacks, methods +// detached from the stack, and Node's TypeError and ReferenceError texts for +// invalid receivers, resources and callbacks. Node.js prints the same. +const attempt = (label, f) => { + try { + console.log(label, f()); + } catch (e) { + console.log(label, e.name + ": " + e.message); + } +}; +const s = new DisposableStack(); +attempt("use number", () => s.use(5)); +attempt("use empty object", () => s.use({})); +attempt("use null", () => s.use(null)); +attempt("use undefined", () => s.use(undefined)); +for (const v of ["x", 5, {}, [], null, undefined, true, Symbol("q"), 1n, new Map()]) { + attempt("defer", () => s.defer(v)); +} +attempt("adopt", () => s.adopt(1, 2)); +const { defer, use, dispose, move } = s; +attempt("detached defer", () => defer(() => {})); +attempt("use on object", () => use.call({}, null)); +attempt("dispose on number", () => dispose.call(5)); +attempt("move on string", () => move.call("x")); +console.log(s.defer.length, s.use.length, s.adopt.length, s.move.length, s.dispose.length); +console.log(s.defer.name, s[Symbol.dispose].name, s[Symbol.dispose] === s.dispose); +console.log(typeof s.disposed, s.disposed, JSON.stringify(s), String(s), s); + +const order = []; +const resource = (name) => ({ + [Symbol.dispose]() { + order.push("dispose " + name); + }, +}); +const stack = new DisposableStack(); +console.log(stack.use(resource("a")) !== null, stack.adopt(7, (v) => order.push("adopt " + v))); +stack.defer(() => order.push("defer")); +stack.use(resource("b")); +const moved = stack.move(); +moved.defer(() => order.push("after move")); +console.log(stack.disposed, moved.disposed, moved instanceof DisposableStack); +moved[Symbol.dispose](); +console.log(order.join(", ")); +console.log(moved.dispose(), moved.disposed); +attempt("use after dispose", () => moved.use(null)); +attempt("adopt after dispose", () => moved.adopt(1, () => {})); + +const failing = new DisposableStack(); +failing.defer(() => { + throw new Error("one"); +}); +failing.defer(() => { + throw new TypeError("two"); +}); +failing.defer(() => { + throw "three"; +}); +try { + failing.dispose(); +} catch (e) { + console.log(e.name, e.message, e.error.name, e.suppressed.error.message, e.suppressed.suppressed); +} +const getterResource = { + get [Symbol.dispose]() { + console.log("getter read"); + return () => console.log("getter resource disposed"); + }, +}; +const g = new DisposableStack(); +g.use(getterResource); +console.log("after use"); +g.dispose(); +{ + using scoped = new DisposableStack(); + scoped.defer(() => console.log("scope exit")); + console.log("in scope"); +} diff --git a/tests/corpus/5870-worker-thread-names.ts b/tests/corpus/5870-worker-thread-names.ts new file mode 100644 index 0000000000..ce5d98ccae --- /dev/null +++ b/tests/corpus/5870-worker-thread-names.ts @@ -0,0 +1,35 @@ +import { Worker, isMainThread, parentPort, threadId, threadName, workerData } from "node:worker_threads"; + +// The Worker name option: a truthy name must be a string and is trimmed, +// any other value selects "WorkerThread"; threadName reports it inside the +// worker and on the Worker until exit (null afterwards), and is "" in the +// main thread. +if (isMainThread) { + console.log("main", threadId, JSON.stringify(threadName)); + for (const name of [5, true, {}] as unknown[]) { + try { + const options: Record = { name }; + new Worker(new URL(import.meta.url), options); + } catch (e) { + const err = e as Error & { code?: string }; + console.log(JSON.stringify(name), err.name, err.code, err.message); + } + } + const names: unknown[] = [" reader one ", "", " ", 0, null, undefined, "a\tb "]; + let next = 0; + const run = (): void => { + if (next >= names.length) return; + const name = names[next++]; + const options: Record = { name, workerData: next }; + const worker = new Worker(new URL(import.meta.url), options); + console.log(JSON.stringify(name), "threadName", JSON.stringify(worker.threadName)); + worker.on("message", (m: unknown) => console.log(" worker saw", JSON.stringify(m))); + worker.on("exit", (code: number) => { + console.log(" exit", code, JSON.stringify(worker.threadName)); + run(); + }); + }; + run(); +} else { + parentPort!.postMessage([workerData, threadName, threadId > 0]); +} diff --git a/tests/corpus/5871-worker-untransferable.cjs b/tests/corpus/5871-worker-untransferable.cjs new file mode 100644 index 0000000000..b79482ddea --- /dev/null +++ b/tests/corpus/5871-worker-untransferable.cjs @@ -0,0 +1,56 @@ +'use strict'; +// markAsUntransferable and isMarkedAsUntransferable: marks on objects, +// ArrayBuffers (a typed array and its ArrayBuffer are marked separately), +// functions and MessagePorts; primitives are never marked. A marked +// ArrayBuffer or port in a transfer list throws DataCloneError, and so does +// the ArrayBuffer of a pooled Buffer, which Node marks. Cloning a marked +// value is unaffected. +const { MessageChannel, isMarkedAsUntransferable, markAsUntransferable } = require('node:worker_threads'); + +const ab = new ArrayBuffer(8); +const before = isMarkedAsUntransferable(ab); +markAsUntransferable(ab); +console.log('arraybuffer', before, isMarkedAsUntransferable(ab)); +const obj = { a: 1 }; +const objectBefore = isMarkedAsUntransferable(obj); +markAsUntransferable(obj); +console.log('object', objectBefore, isMarkedAsUntransferable(obj), isMarkedAsUntransferable({ a: 1 })); +const fn = () => 1; +markAsUntransferable(fn); +console.log('function', isMarkedAsUntransferable(fn)); +markAsUntransferable(5); +console.log('primitives', isMarkedAsUntransferable(5), isMarkedAsUntransferable('s'), isMarkedAsUntransferable(null), isMarkedAsUntransferable(undefined), isMarkedAsUntransferable()); +const view = new Uint8Array(4); +markAsUntransferable(view.buffer); +console.log('view buffer', isMarkedAsUntransferable(view), isMarkedAsUntransferable(view.buffer)); +const other = new Uint8Array(4); +markAsUntransferable(other); +console.log('view itself', isMarkedAsUntransferable(other), isMarkedAsUntransferable(other.buffer)); +console.log('pooled buffer', isMarkedAsUntransferable(Buffer.from('x').buffer), isMarkedAsUntransferable(Buffer.alloc(10).buffer), isMarkedAsUntransferable(Buffer.from('x'))); +const sab = new SharedArrayBuffer(4); +markAsUntransferable(sab); +console.log('shared', isMarkedAsUntransferable(sab)); + +const { port1, port2 } = new MessageChannel(); +const spare = new MessageChannel(); +markAsUntransferable(spare.port1); +console.log('port', isMarkedAsUntransferable(spare.port1), isMarkedAsUntransferable(spare.port2)); +for (const [label, value] of [['marked arraybuffer', ab], ['pooled arraybuffer', Buffer.from('ab').buffer], ['marked port', spare.port1]]) { + try { + port1.postMessage(value, [value]); + console.log(label, 'posted'); + } catch (e) { + console.log(label, e.name, e.message); + } +} +try { + port1.postMessage('options form', { transfer: [spare.port1] }); +} catch (e) { + console.log('options form', e.name, e.message); +} +port1.postMessage({ ab, obj }); +port2.on('message', (m) => { + console.log('cloned', m.ab.byteLength, JSON.stringify(m.obj), isMarkedAsUntransferable(m.ab), isMarkedAsUntransferable(m.obj)); + port1.close(); + spare.port1.close(); +}); diff --git a/tests/corpus/5874-worker-stdio-streams.ts b/tests/corpus/5874-worker-stdio-streams.ts new file mode 100644 index 0000000000..28d2ff4a2b --- /dev/null +++ b/tests/corpus/5874-worker-stdio-streams.ts @@ -0,0 +1,28 @@ +import { Worker, isMainThread } from "node:worker_threads"; + +// The stdout and stderr options: the worker's console and process.stdout/ +// process.stderr writes reach worker.stdout and worker.stderr instead of +// the process output, as Readable streams that end after the worker's +// 'exit' event. worker.stdin is null without the stdin option. +if (isMainThread) { + const worker = new Worker(new URL(import.meta.url), { stdout: true, stderr: true }); + let out = ""; + let err = ""; + worker.stdout.setEncoding("utf8"); + worker.stdout.on("data", (chunk: string) => { + out += chunk; + }); + worker.stdout.on("end", () => console.log("stdout end", JSON.stringify(out))); + worker.stderr.on("data", (chunk: Buffer) => { + err += chunk.toString(); + }); + worker.stderr.on("end", () => console.log("stderr end", JSON.stringify(err))); + worker.on("exit", (code: number) => console.log("exit", code)); + console.log("stdin", worker.stdin); +} else { + console.log("hello from the worker"); + process.stdout.write("raw write\n"); + process.stdout.write(Buffer.from("bytes\n")); + console.error("an error line"); + process.stderr.write("err bytes\n"); +} diff --git a/tests/corpus/5875-worker-stdout-pipe.cjs b/tests/corpus/5875-worker-stdout-pipe.cjs new file mode 100644 index 0000000000..8a9551a127 --- /dev/null +++ b/tests/corpus/5875-worker-stdout-pipe.cjs @@ -0,0 +1,24 @@ +'use strict'; +// worker.stdout as a pipe source: with only the stdout option, the +// worker's stdout writes reach the parent's Writable while its stderr +// output goes to the process's stderr as before. +const { Worker, isMainThread } = require('node:worker_threads'); +const { Writable } = require('node:stream'); + +if (isMainThread) { + const worker = new Worker(__filename, { stdout: true }); + const chunks = []; + const sink = new Writable({ + write(chunk, encoding, callback) { + chunks.push(chunk.toString().toUpperCase()); + callback(); + }, + }); + sink.on('finish', () => console.log('piped', JSON.stringify(chunks.join('')))); + worker.stdout.pipe(sink); + worker.on('exit', (code) => console.log('exit', code)); +} else { + console.log('first line'); + console.error('to stderr'); + console.log('second line'); +} diff --git a/tests/corpus/5879-worker-stdin.ts b/tests/corpus/5879-worker-stdin.ts new file mode 100644 index 0000000000..197ee719c3 --- /dev/null +++ b/tests/corpus/5879-worker-stdin.ts @@ -0,0 +1,42 @@ +// The Worker stdin option: worker.stdin is a Writable whose chunks the +// worker reads from process.stdin, which ends when worker.stdin ends. +// Without the option worker.stdin is null and the worker's process.stdin +// ends without data. +import { Worker, isMainThread, parentPort } from "node:worker_threads"; +import { Writable } from "node:stream"; +import * as readline from "node:readline"; + +function run(mode: string, stdin: boolean, feed: (sink: Writable) => void, next: () => void): void { + const worker = new Worker(new URL(import.meta.url), { argv: [mode], stdin }); + const messages: string[] = []; + worker.on("message", (m: string) => messages.push(m)); + worker.on("exit", (code: number) => { + console.log(mode, "exit", code, JSON.stringify(messages)); + next(); + }); + if (worker.stdin === null) console.log(mode, "stdin", null); + else feed(worker.stdin); +} + +if (isMainThread) { + run("plain", false, () => {}, () => + run("chunks", true, (sink) => { + sink.write("first line\n"); + sink.write(Buffer.from("second ")); + sink.end("line\n", () => console.log("chunks end callback")); + }, () => + run("lines", true, (sink) => { + sink.write("alpha\nbeta"); + sink.end("\ngamma\n"); + }, () => console.log("done")))); +} else if (process.argv[2] === "lines") { + const rl = readline.createInterface({ input: process.stdin }); + rl.on("line", (line) => parentPort!.postMessage(line.toUpperCase())); + rl.on("close", () => parentPort!.postMessage("closed")); +} else { + const chunks: string[] = []; + process.stdin.on("data", (d: Uint8Array) => { + chunks.push(Buffer.from(d).toString()); + }); + process.stdin.on("end", () => parentPort!.postMessage(`${chunks.length} ${JSON.stringify(chunks.join(""))}`)); +} diff --git a/tests/corpus/5881-inspect-weak-collections.ts b/tests/corpus/5881-inspect-weak-collections.ts new file mode 100644 index 0000000000..b281dcb120 --- /dev/null +++ b/tests/corpus/5881-inspect-weak-collections.ts @@ -0,0 +1,24 @@ +// util.inspect, console.log and util.format's %s and %O print a WeakMap or +// WeakSet as Node's formatWeakCollection does: the entries are not +// listed, so the body is "", and past the depth limit the +// value prints as [WeakMap] / [WeakSet]. Nested positions follow the +// usual line breaking. +import { format, inspect } from "node:util"; + +const cache = new WeakMap(); +const seen = new WeakSet(); +const key = {}; +cache.set(key, 1); +seen.add(key); + +console.log(cache, seen); +console.log(inspect(cache), inspect(new WeakSet())); +console.log({ cache, seen }); +console.log([cache, [seen, [cache]]]); +console.log(inspect({ a: { b: { c: cache } } }), inspect({ a: { b: { c: { d: seen } } } })); +console.log(inspect({ cache }, { depth: 0 }), inspect([seen], { depth: 0 })); +console.log(inspect({ cache }, { depth: null })); +console.log(new Map>([["k", cache]])); +console.log(new Set>([seen])); +console.log(format("%s|%O", cache, seen), inspect(cache).length); +console.log(cache.has(key), seen.has(key), cache.get(key)); diff --git a/tests/corpus/5882-string-decoder-runtime-encoding.ts b/tests/corpus/5882-string-decoder-runtime-encoding.ts new file mode 100644 index 0000000000..059d2144fd --- /dev/null +++ b/tests/corpus/5882-string-decoder-runtime-encoding.ts @@ -0,0 +1,39 @@ +// new StringDecoder(encoding) with an encoding known only at run time +// normalizes it as Node does: any ASCII case of a Buffer encoding name +// selects that decoder and `.encoding` answers the canonical name +// (ucs2 and utf-16le are utf16le, binary is latin1), undefined and "" +// mean utf8, and an unknown name throws a TypeError with code +// ERR_UNKNOWN_ENCODING. Literal names keep their compile-time form. +import { StringDecoder } from "node:string_decoder"; + +const bytes = Buffer.from([0xe2, 0x82, 0xac, 0x41, 0xc3, 0xa9, 0xff, 0x00]); + +function decodeAll(encoding: string | undefined): string { + const decoder = new StringDecoder(encoding as BufferEncoding | undefined); + const parts: string[] = []; + for (let i = 0; i < bytes.length; i += 3) parts.push(decoder.write(bytes.subarray(i, i + 3))); + parts.push(decoder.end()); + return `${decoder.encoding} ${JSON.stringify(parts)}`; +} + +const names = [ + "utf8", "UTF-8", "Utf8", "hex", "HEX", "base64", "Base64Url", "latin1", "BINARY", + "ascii", "utf16le", "UTF-16LE", "ucs2", "Ucs-2", "", undefined, +]; +for (const name of names) console.log(JSON.stringify(name), decodeAll(name)); + +for (const bad of ["utf-16", "buffer", "utf 8", "latin-1"]) { + try { + new StringDecoder(bad as BufferEncoding); + console.log("no throw", bad); + } catch (error) { + const e = error as Error & { code?: string }; + console.log(e.name, e.code, e.message); + } +} + +const fromOptions: { encoding?: BufferEncoding } = { encoding: "hex" }; +const viaOption = new StringDecoder(fromOptions.encoding); +console.log(viaOption.encoding, viaOption.write(Buffer.from("hi")), viaOption.end(Buffer.from("!"))); +const literal = new StringDecoder("ucs2"); +console.log(literal.encoding, JSON.stringify(literal.write(Buffer.from([0x3d, 0xd8, 0x00, 0xde])))); diff --git a/tests/corpus/5883-os-set-priority.ts b/tests/corpus/5883-os-set-priority.ts new file mode 100644 index 0000000000..d871001830 --- /dev/null +++ b/tests/corpus/5883-os-set-priority.ts @@ -0,0 +1,33 @@ +// os.setPriority([pid, ]priority): the one-argument form sets the current +// process, both arguments go through validateInt32 (the priority within +// -20..19, a RangeError with code ERR_OUT_OF_RANGE otherwise), and a +// refused change is a SystemError with code ERR_SYSTEM_ERROR naming +// uv_os_setpriority. Raising the nice value is always allowed; lowering it +// again depends on the privileges of the user running the program. +import * as os from "node:os"; +import { getPriority, setPriority } from "node:os"; + +function attempt(label: string, run: () => void): void { + try { + run(); + console.log(label, "ok", getPriority()); + } catch (error) { + const e = error as Error & { code?: string }; + console.log(label, e.name, e.code, e.message); + } +} + +const start = getPriority(); +attempt("same value", () => setPriority(start)); +attempt("same value with pid 0", () => os.setPriority(0, start)); +attempt("same value with own pid", () => setPriority(process.pid, start)); +attempt("raise", () => setPriority(Math.min(start + 5, 19))); +console.log("raised by 5 or to 19:", getPriority() === Math.min(start + 5, 19)); +attempt("lower again", () => setPriority(start)); +attempt("priority 20", () => setPriority(20)); +attempt("priority -21", () => setPriority(0, -21)); +attempt("priority 1.5", () => setPriority(1.5)); +attempt("priority NaN", () => setPriority(Number.NaN)); +attempt("pid 2 ** 31", () => setPriority(2 ** 31, 0)); +attempt("pid -1.5", () => setPriority(-1.5, 0)); +attempt("missing process", () => setPriority(2147483646, 19)); diff --git a/tests/corpus/5884-inspect-layout-options.ts b/tests/corpus/5884-inspect-layout-options.ts new file mode 100644 index 0000000000..f75528b9b6 --- /dev/null +++ b/tests/corpus/5884-inspect-layout-options.ts @@ -0,0 +1,51 @@ +// util.inspect and console.dir with the breakLength and compact options: +// breakLength (a number or Infinity) is the line width the single-line +// form and the array grid must fit, and compact sets how many inner +// levels may join one line and the grid's column limit (compact * 4). +// compact: false (or a number below 1) puts every entry of every +// composite on its own line. A render with these options leaves later +// renders at the defaults. +import { inspect } from "node:util"; + +class Point { + x: number; + y: number; + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } +} + +const config = { + name: "service", + ports: [80, 443], + limits: { cpu: 2, memory: { soft: "1G", hard: "2G" } }, + tags: new Set(["a", "b"]), + routes: new Map([["/", { handler: "index", methods: ["GET", "HEAD"] }]]), +}; +const numbers = Array.from({ length: 30 }, (_, i) => i * 37); +const words = Array.from({ length: 26 }, (_, i) => String.fromCharCode(97 + i).repeat((i % 5) + 1)); +const points = Array.from({ length: 8 }, (_, i) => new Point(i, i * i)); +const text = { text: "line one\nline two\nline three is a longer one" }; + +console.log(inspect(config, { compact: false })); +console.log(inspect(config, { compact: 1 })); +console.log(inspect(config, { compact: 5, depth: Infinity })); +console.log(inspect(config, { breakLength: 40 })); +console.log(inspect(config, { breakLength: Infinity })); +console.log(inspect(config, { breakLength: 120, compact: 2 })); +console.log(inspect(numbers, { compact: false })); +console.log(inspect(numbers, { compact: 1 })); +console.log(inspect(numbers, { breakLength: 40 }), inspect(numbers, { breakLength: 150 })); +console.log(inspect(words, { compact: 6 }), inspect(words, { breakLength: 60 })); +console.log(inspect(points, { compact: 2, breakLength: 120 })); +console.log(inspect(points, { compact: false })); +console.log(inspect(text, { breakLength: Infinity }), inspect(text, { compact: false })); +console.log(inspect(text.text, { breakLength: 20 })); +console.log(inspect([], { compact: false }), inspect({}, { compact: false }), inspect(new Map([[1, 2]]), { compact: false })); +console.log(inspect({ a: 1 }, { compact: 0 }), inspect({ a: [1, 2] }, { breakLength: 0 })); +const parsed: unknown = JSON.parse('{"x":[1,2,{"y":"z"}],"w":{"v":null}}'); +console.log(inspect(parsed, { compact: false }), inspect(parsed, { breakLength: 10 })); +console.dir(config, { compact: false, depth: 0 }); +console.dir(parsed, { breakLength: 20 }); +console.log(config); diff --git a/tests/corpus/5886-inspect-dates.ts b/tests/corpus/5886-inspect-dates.ts new file mode 100644 index 0000000000..37338135d1 --- /dev/null +++ b/tests/corpus/5886-inspect-dates.ts @@ -0,0 +1,32 @@ +// util.inspect, console.log, console.dir and util.format's %s, %O and %o +// print a Date as Node does: its toISOString() at every depth (including +// six-digit signed years), and "Invalid Date" for a NaN time value. Dates +// in record fields and class instances follow the usual layout. +import { format, inspect } from "node:util"; + +const epoch = new Date(0); +console.log(epoch); +console.log(inspect(epoch), inspect(epoch, { depth: 0 }), inspect(new Date("garbage"))); +console.log(inspect({ a: { b: { c: { epoch } } } }), inspect({ a: { b: { epoch } } })); +console.log(format("%s|%O|%o", epoch, epoch, { epoch })); +console.log("at %s", new Date(1e12)); +console.log({ + epoch, + invalid: new Date(Number.NaN), + latest: new Date(8.64e15), + earliest: new Date(-8.64e15), +}); +console.log({ + leap: new Date(Date.UTC(2024, 1, 29, 12, 30, 45, 123)), + negativeYear: new Date(Date.UTC(-1, 0, 1)), + farFuture: new Date(Date.UTC(10000, 0, 1)), + parsed: new Date("2020-06-15T08:09:10.011+02:00"), +}); + +class Entry { + at = new Date(5); + name = "entry"; +} +console.log(new Entry(), [new Entry()]); +console.error(epoch); +console.dir({ epoch }, { depth: 0 }); diff --git a/tests/corpus/5887-inspect-typed-arrays.ts b/tests/corpus/5887-inspect-typed-arrays.ts new file mode 100644 index 0000000000..eb6fb6750a --- /dev/null +++ b/tests/corpus/5887-inspect-typed-arrays.ts @@ -0,0 +1,30 @@ +// util.inspect, console.log and console.dir print a numeric typed array +// that is not a Buffer as Node does: `Float64Array(n) [ ... ]` with the +// number formatting, the 100-element maxArrayLength tail, the array grid +// and `[Int16Array]` past the depth limit. Typed arrays other than +// Uint8Array also print inside records and arrays, and under the +// breakLength, compact and sorted options; Buffers keep their +// form. +import { inspect } from "node:util"; + +console.log(new Uint8Array([1, 2, 3]), new Uint8Array(0)); +console.log(new Float64Array([1.5, -0, Number.NaN, Infinity, -1e21])); +console.log(new Float32Array([0.1, 16777217])); +console.log(new Int32Array(30)); +console.log(new Uint8ClampedArray([300, -5, 1.5]), new Int8Array([-1, 127, 128])); +console.log(inspect(new Uint16Array(150))); +console.log({ samples: new Uint16Array([7, 8]), deltas: new Int8Array([-1, 2]) }); +console.log([new Float32Array([1, 2]), new Float32Array(10)]); +console.log(inspect({ x: { y: { z: new Uint32Array([1]) } } }), inspect(new Int16Array([5]), { depth: 0 })); +console.log(inspect({ x: { y: new Int16Array([5]) } }, { depth: 1 })); +console.dir(new Uint8Array([4, 5])); +console.log(new Uint8Array([1, 2, 3, 4]).subarray(1, 3), new Uint8Array(Buffer.from("hi")), Buffer.from("hi")); +const grid = new Float64Array(12); +for (let i = 0; i < grid.length; i++) grid[i] = i * 1.25; +console.log(grid); +const wide = new Uint8Array(40); +for (let i = 0; i < wide.length; i++) wide[i] = i * 7; +console.log(inspect(wide, { breakLength: 40 })); +console.log(inspect(new Int16Array([1, -2, 300, -4000, 5]), { compact: false })); +console.log(inspect(new Uint32Array(120), { depth: 0, breakLength: 60 })); +console.log(inspect(new Int8Array([3, 1, 2]), { sorted: true, breakLength: Infinity })); diff --git a/tests/corpus/5888-inspect-sorted.ts b/tests/corpus/5888-inspect-sorted.ts new file mode 100644 index 0000000000..97a6ffb308 --- /dev/null +++ b/tests/corpus/5888-inspect-sorted.ts @@ -0,0 +1,34 @@ +// util.inspect and console.dir with sorted: true order the entries of +// objects, class instances, Maps and Sets by their rendered text, in +// UTF-16 code-unit order (quoted keys sort by their quote, the "... more +// items" tail sorts with the rest), at every nesting level. Arrays keep +// their element order. sorted: false is the default. +import { inspect } from "node:util"; + +const record = { + zeta: 1, + alpha: { c: 3, b: 2, a: 1 }, + "quoted-key": true, + Beta: [3, 1, 2], + _under: null, + 10: "ten", + 2: "two", +}; +console.log(inspect(record, { sorted: true })); +console.log(inspect(record, { sorted: false })); +console.log(inspect(new Map([["b", 1], ["a", 2], ["c", { z: 1, y: 2 }]]), { sorted: true })); +console.log(inspect(new Set(["pear", "apple", "fig"]), { sorted: true })); +console.log(inspect({ é: 1, e: 2, "😀": 3, "￿": 4 }, { sorted: true })); +console.log(inspect([{ b: 1, a: 2 }, { d: 1, c: 2 }], { sorted: true, compact: false })); + +class Point { + y = 1; + x = 2; +} +console.log(inspect(new Point(), { sorted: true }), inspect([3, 1, 2], { sorted: true })); +const many = new Set(Array.from({ length: 105 }, (_, i) => `k${i}`)); +console.log(inspect(many, { sorted: true, breakLength: Infinity })); +const parsed: unknown = JSON.parse('{"b":1,"a":{"d":1,"c":2}}'); +console.log(inspect(parsed, { sorted: true })); +console.dir({ b: 1, a: 2 }, { sorted: true }); +console.log(record); diff --git a/tests/corpus/5900-float16-array.ts b/tests/corpus/5900-float16-array.ts new file mode 100644 index 0000000000..bcf57ea8df --- /dev/null +++ b/tests/corpus/5900-float16-array.ts @@ -0,0 +1,94 @@ +// Float16Array: construction from lengths, arrays, typed arrays and ArrayBuffer +// views; round-to-nearest-even stores with overflow, NaN, -0 and subnormals; +// fill, set, subarray, slice and both sort orders; DataView getFloat16 and +// setFloat16 in both byte orders; Math.f16round; util.types and instanceof; +// structuredClone, assert and Buffer views. Node.js prints the same. +import { types } from "node:util"; +import assert from "node:assert"; +const show = (label: string, a: Float16Array): void => console.log(label, a.length, Array.from(a).join(",")); +const a = new Float16Array(6); +a[0] = 1.5; +a[1] = 65519; +a[2] = 65520; +a[3] = 0.1; +a[4] = -0; +a[5] = 5.960464477539063e-8 / 2; +show("rounding", a); +const b = new Float16Array([NaN, Infinity, -Infinity, 1e-8, 2049, 2051, 4097, -1e6]); +show("specials", b); +console.log(Object.is(a[4], -0), a[6], a.length, a.byteLength, Number.isNaN(b[0])); +const c = Float16Array.from([3.14159, 2.71828, 1.41421]); +show("from", c); +const e = new Float16Array(c); +e[0] = 100; +show("copy", e); +show("source untouched", c); +const f32 = new Float32Array([1 / 3, 2 / 3]); +show("from f32", new Float16Array(f32)); +const buf = new ArrayBuffer(8); +const view = new Float16Array(buf, 2, 2); +view[0] = 1; +view[1] = -2; +console.log("view bytes", Array.from(new Uint8Array(buf)).join(","), new Uint16Array(buf)[1]); +const g = new Float16Array(5); +g.fill(0.3); +show("fill", g); +g.set([1.1, 2.2], 3); +show("set", g); +show("subarray", g.subarray(1, 4)); +show("slice", g.slice(-2)); +const h = new Float16Array([3, -0, 0, NaN, -1, 2.5, -Infinity, 0.1]); +show("sort", h.slice().sort()); +show("sort desc", h.slice().sort((x, y) => y - x)); +show("toSorted", h.toSorted()); +let sum = 0; +for (const v of c) sum += v; +console.log("for-of", sum, h.join("|"), String(c), Object.prototype.toString.call(c)); +const dv = new DataView(new ArrayBuffer(8)); +dv.setFloat16(0, 3.14159); +dv.setFloat16(2, 3.14159, true); +dv.setFloat16(4, -65504); +dv.setFloat16(6, NaN, true); +console.log("dataview", dv.getFloat16(0), dv.getFloat16(2, true), dv.getFloat16(4), dv.getFloat16(6, true), dv.getUint16(0), dv.getUint16(2)); +try { + dv.getFloat16(7); +} catch (err) { + console.log((err as Error).name, (err as Error).message); +} +console.log("f16round", Math.f16round(1.337), Math.f16round(65520), Math.f16round(-0), Math.f16round(5e-8), Math.f16round(2 ** -25), Math.f16round(1.0009765625 + 2 ** -12)); +console.log("types", types.isFloat16Array(a), types.isFloat32Array(a), types.isTypedArray(a), a instanceof Float16Array); +const u: unknown = a; +console.log("unknown", u instanceof Float16Array, u instanceof Float32Array); +const big = new Float16Array(1000); +for (let i = 0; i < big.length; i++) big[i] = Math.sin(i) * 1000; +let acc = 0; +for (let i = 0; i < big.length; i++) acc += big[i]!; +console.log("loop", acc, big[999]); +const f = new Float16Array([1.5, NaN, -0, 65504]); +const clone = structuredClone(f); +clone[0] = 2; +console.log(clone[0], f[0], clone.length); +assert.deepStrictEqual(new Float16Array([NaN, 1]), new Float16Array([NaN, 1])); +try { + assert.deepStrictEqual(new Float16Array([0]), new Float16Array([-0])); +} catch (e) { + console.log("deepStrictEqual -0", (e as Error).name); +} +assert.deepEqual(new Float16Array([0]), new Float16Array([-0])); +const raw = Buffer.from(f.buffer); +console.log(raw.length, raw.readUInt16LE(0), raw.readUInt16LE(6)); +const fromBuf = new Float16Array(f.buffer, 2, 1); +console.log(Number.isNaN(fromBuf[0])); +const list: Float16Array[] = [f, new Float16Array(2)]; +const m = new Map([["a", f]]); +console.log(list.length, m.get("a")![3], list[1]!.length); +const Ctor = Float16Array; +const x = new Ctor(3); +x[1] = 0.2; +console.log(x.length, x[1]); +const y = new Float16Array([1, 2]); +const D = y.constructor as Float16ArrayConstructor; +const z = new D(2); +console.log(z.length, z instanceof Float16Array); +const unknownView: unknown = new Float16Array([7]); +console.log(unknownView instanceof Float16Array); diff --git a/tests/corpus/5901-float16-array-js.js b/tests/corpus/5901-float16-array-js.js new file mode 100644 index 0000000000..8c0f6f7bc2 --- /dev/null +++ b/tests/corpus/5901-float16-array-js.js @@ -0,0 +1,95 @@ +// Float16Array from JavaScript: construction from lengths, arrays, typed +// arrays and ArrayBuffer views; round-to-nearest-even stores with overflow, +// NaN, -0 and subnormals; fill, set, subarray, slice and both sort orders; +// DataView getFloat16 and setFloat16 in both byte orders; Math.f16round; +// util.types and instanceof; structuredClone, assert and Buffer views. +// Node.js prints the same. +import { types } from "node:util"; +import assert from "node:assert"; +const show = (label , a ) => console.log(label, a.length, Array.from(a).join(",")); +const a = new Float16Array(6); +a[0] = 1.5; +a[1] = 65519; +a[2] = 65520; +a[3] = 0.1; +a[4] = -0; +a[5] = 5.960464477539063e-8 / 2; +show("rounding", a); +const b = new Float16Array([NaN, Infinity, -Infinity, 1e-8, 2049, 2051, 4097, -1e6]); +show("specials", b); +console.log(Object.is(a[4], -0), a[6], a.length, a.byteLength, Number.isNaN(b[0])); +const c = Float16Array.from([3.14159, 2.71828, 1.41421]); +show("from", c); +const e = new Float16Array(c); +e[0] = 100; +show("copy", e); +show("source untouched", c); +const f32 = new Float32Array([1 / 3, 2 / 3]); +show("from f32", new Float16Array(f32)); +const buf = new ArrayBuffer(8); +const view = new Float16Array(buf, 2, 2); +view[0] = 1; +view[1] = -2; +console.log("view bytes", Array.from(new Uint8Array(buf)).join(","), new Uint16Array(buf)[1]); +const g = new Float16Array(5); +g.fill(0.3); +show("fill", g); +g.set([1.1, 2.2], 3); +show("set", g); +show("subarray", g.subarray(1, 4)); +show("slice", g.slice(-2)); +const h = new Float16Array([3, -0, 0, NaN, -1, 2.5, -Infinity, 0.1]); +show("sort", h.slice().sort()); +show("sort desc", h.slice().sort((x, y) => y - x)); +show("toSorted", h.toSorted()); +let sum = 0; +for (const v of c) sum += v; +console.log("for-of", sum, h.join("|"), String(c), Object.prototype.toString.call(c)); +const dv = new DataView(new ArrayBuffer(8)); +dv.setFloat16(0, 3.14159); +dv.setFloat16(2, 3.14159, true); +dv.setFloat16(4, -65504); +dv.setFloat16(6, NaN, true); +console.log("dataview", dv.getFloat16(0), dv.getFloat16(2, true), dv.getFloat16(4), dv.getFloat16(6, true), dv.getUint16(0), dv.getUint16(2)); +try { + dv.getFloat16(7); +} catch (err) { + console.log((err ).name, (err ).message); +} +console.log("f16round", Math.f16round(1.337), Math.f16round(65520), Math.f16round(-0), Math.f16round(5e-8), Math.f16round(2 ** -25), Math.f16round(1.0009765625 + 2 ** -12)); +console.log("types", types.isFloat16Array(a), types.isFloat32Array(a), types.isTypedArray(a), a instanceof Float16Array); +const u = a; +console.log("unknown", u instanceof Float16Array, u instanceof Float32Array); +const big = new Float16Array(1000); +for (let i = 0; i < big.length; i++) big[i] = Math.sin(i) * 1000; +let acc = 0; +for (let i = 0; i < big.length; i++) acc += big[i] ; +console.log("loop", acc, big[999]); +const f = new Float16Array([1.5, NaN, -0, 65504]); +const clone = structuredClone(f); +clone[0] = 2; +console.log(clone[0], f[0], clone.length); +assert.deepStrictEqual(new Float16Array([NaN, 1]), new Float16Array([NaN, 1])); +try { + assert.deepStrictEqual(new Float16Array([0]), new Float16Array([-0])); +} catch (e) { + console.log("deepStrictEqual -0", (e ).name); +} +assert.deepEqual(new Float16Array([0]), new Float16Array([-0])); +const raw = Buffer.from(f.buffer); +console.log(raw.length, raw.readUInt16LE(0), raw.readUInt16LE(6)); +const fromBuf = new Float16Array(f.buffer, 2, 1); +console.log(Number.isNaN(fromBuf[0])); +const list = [f, new Float16Array(2)]; +const m = new Map ([["a", f]]); +console.log(list.length, m.get("a") [3], list[1] .length); +const Ctor = Float16Array; +const x = new Ctor(3); +x[1] = 0.2; +console.log(x.length, x[1]); +const y = new Float16Array([1, 2]); +const D = y.constructor ; +const z = new D(2); +console.log(z.length, z instanceof Float16Array); +const unknownView = new Float16Array([7]); +console.log(unknownView instanceof Float16Array); diff --git a/tests/corpus/5904-async-generator-completion.ts b/tests/corpus/5904-async-generator-completion.ts new file mode 100644 index 0000000000..478aae6e9f --- /dev/null +++ b/tests/corpus/5904-async-generator-completion.ts @@ -0,0 +1,188 @@ +// Async generator completion against a microtask ticker: falling off the end +// and bare returns settle as the body completes, `return x` takes a turn to +// await x, return() at a yield awaits its value before finally blocks run, +// and for await loops end on the turn Node ends them. Node.js prints the +// same. +const log: string[] = []; +async function ticker(n: number): Promise { + for (let i = 0; i < n; i++) { + log.push("t" + i); + await null; + } +} +async function run(name: string, body: () => Promise): Promise { + log.length = 0; + const tick = ticker(14); + await body(); + await tick; + console.log(name + ": " + log.join(" ")); +} +class Source { + readonly items: number[]; + constructor(items: number[]) { + this.items = items; + } + async *values(): AsyncGenerator { + for (const i of this.items) yield i; + } +} +async function main(): Promise { + await run("fall off", async () => { + async function* g(): AsyncGenerator { yield 1; } + const it = g(); + void it.next().then((r) => log.push(`a:${String(r.value)}/${r.done}`)); + await it.next().then((r) => log.push(`b:${String(r.value)}/${r.done}`)); + }); + await run("return value", async () => { + async function* g(): AsyncGenerator { yield 1; return 5; } + const it = g(); + void it.next().then((r) => log.push(`a:${String(r.value)}/${r.done}`)); + await it.next().then((r) => log.push(`b:${String(r.value)}/${r.done}`)); + }); + await run("bare return", async () => { + async function* g(): AsyncGenerator { yield 1; return; } + const it = g(); + void it.next().then((r) => log.push(`a:${String(r.value)}/${r.done}`)); + await it.next().then((r) => log.push(`b:${String(r.value)}/${r.done}`)); + }); + await run("empty body", async () => { + async function* g(): AsyncGenerator {} + await g().next().then((r) => log.push(`a:${String(r.value)}/${r.done}`)); + }); + await run("after done", async () => { + async function* g(): AsyncGenerator {} + const it = g(); + await it.next(); + log.push("mark"); + await it.next().then((r) => log.push(`b:${String(r.value)}/${r.done}`)); + }); + await run("return() at yield", async () => { + async function* g(): AsyncGenerator { try { yield 1; } finally { log.push("finally"); } return 0; } + const it = g(); + await it.next().then((r) => log.push(`a:${String(r.value)}/${r.done}`)); + await it.return(7).then((r) => log.push(`r:${String(r.value)}/${r.done}`)); + }); + await run("return() before start", async () => { + async function* g(): AsyncGenerator { yield 1; return 0; } + await g().return(3).then((r) => log.push(`r:${String(r.value)}/${r.done}`)); + }); + await run("throw at end", async () => { + async function* g(): AsyncGenerator { yield 1; throw new Error("x"); } + const it = g(); + void it.next().then((r) => log.push(`a:${String(r.value)}/${r.done}`)); + await it.next().then((r) => log.push(`b:${String(r.value)}/${r.done}`), () => log.push("rejected")); + }); + await run("await before end", async () => { + async function* g(): AsyncGenerator { yield 1; await null; } + const it = g(); + void it.next().then((r) => log.push(`a:${String(r.value)}/${r.done}`)); + await it.next().then((r) => log.push(`b:${String(r.value)}/${r.done}`)); + }); + await run("for await", async () => { + async function* g(): AsyncGenerator { yield 1; yield 2; } + for await (const v of g()) log.push("v" + v); + log.push("loop done"); + }); + await run("for await break", async () => { + async function* g(): AsyncGenerator { try { yield 1; yield 2; } finally { log.push("cleanup"); } } + for await (const v of g()) { log.push("v" + v); break; } + log.push("loop done"); + }); + await run("queued", async () => { + async function* g(): AsyncGenerator { yield 1; } + const it = g(); + void it.next().then((r) => log.push(`a:${String(r.value)}/${r.done}`)); + void it.next().then((r) => log.push(`b:${String(r.value)}/${r.done}`)); + await it.next().then((r) => log.push(`c:${String(r.value)}/${r.done}`)); + }); + + await run("yield in finally", async () => { + async function* g(): AsyncGenerator { try { yield 1; } finally { yield 9; log.push("after"); } return 0; } + const it = g(); + await it.next(); + void it.return(7).then((r) => log.push(`r:${r.value}/${r.done}`)); + await it.next().then((r) => log.push(`n:${r.value}/${r.done}`)); + }); + await run("await in finally", async () => { + async function* g(): AsyncGenerator { try { yield 1; } finally { await null; log.push("cleaned"); } return 0; } + const it = g(); + await it.next(); + await it.return(4).then((r) => log.push(`r:${r.value}/${r.done}`)); + }); + await run("throw at yield", async () => { + async function* g(): AsyncGenerator { try { yield 1; } catch { log.push("caught"); } } + const it = g(); + await it.next(); + await it.throw(new Error("x")).then((r) => log.push(`t:${r.value}/${r.done}`)); + }); + await run("return through finally", async () => { + async function* g(): AsyncGenerator { try { yield 1; return 5; } finally { log.push("fin"); } } + const it = g(); + void it.next().then((r) => log.push(`a:${r.value}/${r.done}`)); + await it.next().then((r) => log.push(`b:${r.value}/${r.done}`)); + }); + await run("return promise", async () => { + async function* g(): AsyncGenerator { yield 1; return Promise.resolve(3); } + const it = g(); + void it.next().then((r) => log.push(`a:${r.value}/${r.done}`)); + await it.next().then((r) => log.push(`b:${r.value}/${r.done}`)); + }); + await run("two returns", async () => { + async function* g(): AsyncGenerator { try { yield 1; } finally { log.push("fin"); } return 0; } + const it = g(); + await it.next(); + void it.return(1).then((r) => log.push(`r1:${r.value}/${r.done}`)); + await it.return(2).then((r) => log.push(`r2:${r.value}/${r.done}`)); + }); + await run("return after done", async () => { + async function* g(): AsyncGenerator { return 1; } + const it = g(); + await it.next(); + log.push("mark"); + await it.return(8).then((r) => log.push(`r:${r.value}/${r.done}`)); + }); + await run("nested loops break", async () => { + async function* g(n: number): AsyncGenerator { try { for (let i = 0; i < n; i++) yield i; } finally { log.push("close" + n); } } + for await (const a of g(2)) { + for await (const b of g(3)) { log.push(`${a}${b}`); if (b === 1) break; } + } + log.push("done"); + }); + await run("strings", async () => { + async function* g(): AsyncGenerator { yield "a"; yield "b"; } + const parts: string[] = []; + for await (const s of g()) parts.push(s); + log.push(parts.join("")); + }); + + await run("finally return override", async () => { + async function* g(): AsyncGenerator { try { yield 1; } finally { log.push("fin"); return 42; } } + const it = g(); + await it.next(); + await it.return(7).then((r) => log.push(`r:${r.value}/${r.done}`)); + }); + await run("class method", async () => { + const parts: number[] = []; + for await (const v of new Source([1, 2, 3]).values()) parts.push(v); + log.push(parts.join(",")); + }); + await run("return while awaiting", async () => { + async function* g(): AsyncGenerator { try { await null; await null; yield 1; } finally { log.push("fin"); } return 0; } + const it = g(); + void it.next().then((r) => log.push(`n:${r.value}/${r.done}`)); + await it.return(5).then((r) => log.push(`r:${r.value}/${r.done}`)); + }); + await run("throw after done", async () => { + async function* g(): AsyncGenerator {} + const it = g(); + await it.next(); + await it.throw(new Error("late")).then(() => log.push("ok"), (e: unknown) => log.push("rejected " + (e as Error).message)); + }); + await run("rejection inside", async () => { + async function* g(): AsyncGenerator { yield 1; await Promise.reject(new Error("inner")); } + const it = g(); + void it.next().then((r) => log.push(`a:${r.value}/${r.done}`)); + await it.next().then(() => log.push("ok"), (e: unknown) => log.push("rejected " + (e as Error).message)); + }); +} +void main(); diff --git a/tests/corpus/5905-array-from-async-generator.ts b/tests/corpus/5905-array-from-async-generator.ts new file mode 100644 index 0000000000..f95fd53271 --- /dev/null +++ b/tests/corpus/5905-array-from-async-generator.ts @@ -0,0 +1,61 @@ +// Array.fromAsync over async generators: plain, mapped, async-mapped, a +// throwing or rejecting mapping function (which closes the generator), a +// generator that throws, an empty generator, and two calls at once, +// interleaved with a microtask ticker. Node.js prints the same. +const log: string[] = []; +async function ticker(n: number): Promise { + for (let i = 0; i < n; i++) { + log.push("t" + i); + await null; + } +} +async function* nums(n: number): AsyncGenerator { + try { + for (let i = 0; i < n; i++) { + log.push("yield " + i); + yield i; + } + } finally { + log.push("gen finally"); + } +} +async function* failing(): AsyncGenerator { + yield 1; + throw new Error("gen failure"); +} +async function report(label: string, job: Promise): Promise { + await job.then( + (a) => log.push(label + " [" + a.join(",") + "]"), + (e: unknown) => log.push(label + " rejected " + (e as Error).message), + ); +} +async function run(name: string, body: () => Promise): Promise { + log.length = 0; + const tick = ticker(20); + await body(); + await tick; + console.log(name + ": " + log.join(" ")); +} +async function main(): Promise { + await run("plain", () => report("plain", Array.fromAsync(nums(3)))); + await run("map", () => report("map", Array.fromAsync(nums(3), (x: number, i: number) => x * 10 + i))); + await run("async map", () => report("async map", Array.fromAsync(nums(2), async (x: number) => x + 1))); + await run("map throws", () => + report("map throws", Array.fromAsync(nums(3), (x: number) => { + if (x === 1) throw new Error("map failure"); + return x; + })), + ); + await run("map rejects", () => + report("map rejects", Array.fromAsync(nums(3), async (x: number) => { + if (x === 1) throw new Error("async map failure"); + return x; + })), + ); + await run("generator throws", () => report("generator throws", Array.fromAsync(failing()))); + await run("empty", () => report("empty", Array.fromAsync(nums(0)))); + await run("two at once", async () => { + await Promise.all([report("a", Array.fromAsync(nums(2))), report("b", Array.fromAsync(nums(1)))]); + }); +} +void main(); diff --git a/tests/corpus/5906-array-from-async-generator-js.js b/tests/corpus/5906-array-from-async-generator-js.js new file mode 100644 index 0000000000..fa4d22147a --- /dev/null +++ b/tests/corpus/5906-array-from-async-generator-js.js @@ -0,0 +1,60 @@ +// Array.fromAsync over async generators from JavaScript, with and without a +// mapping function, interleaved with a microtask ticker. Node.js prints the +// same. +const log = []; +async function ticker(n ) { + for (let i = 0; i < n; i++) { + log.push("t" + i); + await null; + } +} +async function* nums(n ) { + try { + for (let i = 0; i < n; i++) { + log.push("yield " + i); + yield i; + } + } finally { + log.push("gen finally"); + } +} +async function* failing() { + yield 1; + throw new Error("gen failure"); +} +async function report(label , job ) { + await job.then( + (a) => log.push(label + " [" + a.join(",") + "]"), + (e ) => log.push(label + " rejected " + (e ).message), + ); +} +async function run(name , body ) { + log.length = 0; + const tick = ticker(20); + await body(); + await tick; + console.log(name + ": " + log.join(" ")); +} +async function main() { + await run("plain", () => report("plain", Array.fromAsync(nums(3)))); + await run("map", () => report("map", Array.fromAsync(nums(3), (x , i ) => x * 10 + i))); + await run("async map", () => report("async map", Array.fromAsync(nums(2), async (x ) => x + 1))); + await run("map throws", () => + report("map throws", Array.fromAsync(nums(3), (x ) => { + if (x === 1) throw new Error("map failure"); + return x; + })), + ); + await run("map rejects", () => + report("map rejects", Array.fromAsync(nums(3), async (x ) => { + if (x === 1) throw new Error("async map failure"); + return x; + })), + ); + await run("generator throws", () => report("generator throws", Array.fromAsync(failing()))); + await run("empty", () => report("empty", Array.fromAsync(nums(0)))); + await run("two at once", async () => { + await Promise.all([report("a", Array.fromAsync(nums(2))), report("b", Array.fromAsync(nums(1)))]); + }); +} +void main(); diff --git a/tests/corpus/5913-prototype-type-error-messages.ts b/tests/corpus/5913-prototype-type-error-messages.ts new file mode 100644 index 0000000000..cb78e4cb1c --- /dev/null +++ b/tests/corpus/5913-prototype-type-error-messages.ts @@ -0,0 +1,39 @@ +// Object.setPrototypeOf and Object.create reject a prototype that is +// neither an object nor null with Node's TypeError, whose message ends with +// the value's string form: numbers as String(n) (-0 as 0), bigints without +// the n suffix, symbols as Symbol(description), and the empty string as +// nothing. + +function message(run: () => unknown): string { + try { + run(); + return "no error"; + } catch (error) { + return `${error instanceof TypeError} ${(error as Error).message}`; + } +} + +const prototypes: [string, unknown][] = [ + ["number", 7], + ["fraction", 1.5], + ["negative zero", -0], + ["nan", NaN], + ["string", "proto"], + ["empty string", ""], + ["boolean", true], + ["undefined", undefined], + ["bigint", 10n], + ["symbol", Symbol("tag")], +]; + +for (const [label, prototype] of prototypes) { + const invalid = prototype as object | null; + const target: unknown = JSON.parse('{"own":1}'); + console.log(label, "object receiver |", message(() => Object.setPrototypeOf(target, invalid))); + console.log(label, "primitive receiver |", message(() => Object.setPrototypeOf(5, invalid))); + console.log(label, "create |", message(() => Object.create(invalid))); +} + +const valid: unknown = JSON.parse('{"own":1}'); +console.log("null prototype |", message(() => Object.setPrototypeOf(valid, null))); +console.log("object prototype |", message(() => Object.create({ inherited: true }))); diff --git a/tests/corpus/5914-dyn-in-class-prototype-members.ts b/tests/corpus/5914-dyn-in-class-prototype-members.ts new file mode 100644 index 0000000000..dd4f85f759 --- /dev/null +++ b/tests/corpus/5914-dyn-in-class-prototype-members.ts @@ -0,0 +1,52 @@ +// The `in` operator with literal keys on class instances held as `any`, in +// a program whose instanceof and Object.getPrototypeOf checks give the +// classes runtime prototypes: methods, getters, setters and inherited +// members answer true, as do own fields and Object.prototype members; +// static methods and missing names answer false. + +class Shape { + name = "shape"; + static create(): Shape { + return new Shape(); + } + area(): number { + return 0; + } + get label(): string { + return this.name; + } + set size(value: number) { + this.name = `size ${value}`; + } +} + +class Square extends Shape { + side = 2; + override area(): number { + return this.side * this.side; + } + perimeter(): number { + return this.side * 4; + } +} + +const shape: any = new Shape(); +const square: any = new Square(); +console.log("instanceof", square instanceof Shape, shape instanceof Square); +console.log("same prototype", Object.getPrototypeOf(shape) === Object.getPrototypeOf(square)); + +console.log("method", "area" in shape, "area" in square); +console.log("subclass method", "perimeter" in shape, "perimeter" in square); +console.log("getter", "label" in shape, "label" in square); +console.log("setter", "size" in shape, "size" in square); +console.log("own field", "name" in shape, "side" in square, "side" in shape); +console.log("object prototype", "toString" in shape, "hasOwnProperty" in square); +console.log("static", "create" in shape, "create" in square); +console.log("missing", "volume" in shape, "volume" in square); +console.log("negated", !("area" in shape), !("volume" in square)); + +const names = ["area", "perimeter", "label", "size", "name", "create", "volume"]; +for (const name of names) { + const key: any = JSON.parse(JSON.stringify(name)); + console.log("computed", name, key in shape, key in square); +} diff --git a/tests/corpus/5915-dyn-error-string-conversions.ts b/tests/corpus/5915-dyn-error-string-conversions.ts new file mode 100644 index 0000000000..4e56b5c010 --- /dev/null +++ b/tests/corpus/5915-dyn-error-string-conversions.ts @@ -0,0 +1,48 @@ +// Errors held as `any` convert to strings through Error.prototype.toString +// in every dynamic conversion: template literals, `+` with a string on +// either side, String(), Array join, loose equality, Number() and property +// keys. The name is read through the prototype ("Error", "TypeError", a +// subclass's own name), an empty name or message leaves the other alone, +// and a subclass without its own name inherits "Error". + +class NamedError extends Error { + constructor(message: string) { + super(message); + this.name = "NamedError"; + } +} + +class PlainSubclass extends Error {} + +function show(label: string, error: any): void { + console.log(label, "|", `${error}`, "|", "" + error, "|", error + "!", "|", String(error), "|", [error, 1].join(" ")); +} + +show("error", new Error("plain")); +show("type", new TypeError("bad thing")); +show("range", new RangeError("too far")); +show("syntax", new SyntaxError("unexpected")); +show("named", new NamedError("mine")); +show("subclass", new PlainSubclass("inherited name")); +show("empty message", new TypeError("")); +show("no message", new Error()); + +const renamed: any = new Error("message only"); +renamed.name = ""; +show("empty name", renamed); + +const changed: any = new RangeError("before"); +changed.message = "after"; +show("changed message", changed); + +const numeric: any = new Error("42"); +numeric.name = ""; +const unnumbered: any = new TypeError("x"); +console.log("Number", Number(numeric), Number(unnumbered), +numeric); + +const type: any = new TypeError("bad thing"); +console.log("loose equality", type == "TypeError: bad thing", type == "bad thing"); + +const keyed: any = {}; +keyed[type] = 1; +console.log("key", Object.keys(keyed).join(",")); diff --git a/tests/corpus/5922-matchall-groups-callbacks.ts b/tests/corpus/5922-matchall-groups-callbacks.ts index e7dbfa4769..9c0c62639b 100644 --- a/tests/corpus/5922-matchall-groups-callbacks.ts +++ b/tests/corpus/5922-matchall-groups-callbacks.ts @@ -1,6 +1,7 @@ // .groups on matchAll rows reached through array callbacks (map, forEach, -// filter, some, find) and Array.from's mapping function, with plain and -// duplicate named groups. +// filter, some, find), Array.from's mapping function and parameter +// destructuring, on rows kept by filter, slice and toReversed or picked by +// find and at, with plain, duplicate and nonparticipating named groups. const text = "a1b2c3"; console.log([...text.matchAll(/(?[a-z])/g)].map((m) => m.groups!.k).join("")); console.log([...text.matchAll(/(?[a-z])|(?\d)/g)].map((m) => m.groups!.k).join("")); @@ -9,3 +10,10 @@ const rows = [...text.matchAll(/(?[a-z])(?\d)/g)]; rows.forEach((m) => console.log(m.groups!.letter, m.groups!.digit, JSON.stringify(m.groups))); console.log(rows.filter((m) => m.groups!.digit !== "2").length); console.log(rows.some((m) => m.groups!.letter === "c"), rows.find((m) => m.groups!.digit === "3") !== undefined); +console.log(rows.map(({ groups }) => groups!.letter + groups!.digit).join(",")); +console.log(rows[0]!.groups); +const mixed = [..."2024 and x9".matchAll(/(?[a-z]+)|(?\d+)/g)]; +console.log(mixed.filter((m) => m.groups!.num !== undefined).map((m) => m.groups!.num).join(",")); +console.log(mixed.slice(1).map((m) => m.groups!.word ?? m.groups!.num).join(" ")); +console.log(mixed.find((m) => m.groups!.word === "and")!.groups); +console.log(mixed.at(-1)!.groups!.num, mixed.toReversed().map((m) => m.groups!.word ?? "-").join("")); diff --git a/tests/corpus/5923-matchall-groups-js.js b/tests/corpus/5923-matchall-groups-js.js new file mode 100644 index 0000000000..883d184b68 --- /dev/null +++ b/tests/corpus/5923-matchall-groups-js.js @@ -0,0 +1,42 @@ +// .groups on matchAll rows in JavaScript: array callbacks, Array.from's +// mapping function, destructuring, rows picked with find/at or kept by +// filter/slice, nonparticipating and duplicate named groups. +const text = "a1b2c3"; +console.log([...text.matchAll(/(?[a-z])/g)].map((m) => m.groups.k).join("")); +console.log([...text.matchAll(/(?[a-z])|(?\d)/g)].map((m) => m.groups.k).join("")); +console.log(Array.from(text.matchAll(/(?\d)/g), (m) => m.groups.d).join("+")); +const rows = [...text.matchAll(/(?[a-z])(?\d)/g)]; +rows.forEach((m) => console.log(m.groups.letter, m.groups.digit, JSON.stringify(m.groups))); +console.log(rows.filter((m) => m.groups.digit !== "2").length); +console.log(rows.some((m) => m.groups.letter === "c"), rows.find((m) => m.groups.digit === "3") !== undefined); +rows.forEach(function (m) { + const { groups } = m; + const { + groups: { digit }, + } = m; + console.log(groups.letter, digit); +}); +console.log(rows.map(({ groups }) => groups.letter + groups.digit).join(",")); + +const mixed = [..."2024 and x9".matchAll(/(?[a-z]+)|(?\d+)/g)]; +mixed.forEach((m) => { + console.log(m.groups.word === undefined, m.groups.num ?? "-", typeof m.groups.word, m.groups); +}); +console.log(mixed.filter((m) => m.groups.num !== undefined).map((m) => m.groups.num).join(",")); +console.log(mixed.slice(1).map((m) => m.groups.word ?? m.groups.num).join(" ")); +console.log(mixed.find((m) => m.groups.word === "and").groups); +console.log(mixed.findLast((m) => m.groups.word !== undefined).groups.word); +console.log(mixed.at(-1).groups.num, Object.keys(mixed.at(0).groups)); +try { + console.log(mixed.find((m) => m.groups.word === "zzz").groups); +} catch (e) { + console.log(e instanceof TypeError, e.message); +} + +const dup = [..."a1-b".matchAll(/(?[a-z])|(?\d)|-/g)]; +console.log(dup.map((m) => String(m.groups.v)).join("|")); +const plain = [..."ab".matchAll(/[a-z]/g)]; +console.log(plain.map((m) => m.groups === undefined).join()); + +const one = /(?a)|(?b)/.exec("a"); +console.log(one.groups.n, one.groups.n === undefined, one.groups); diff --git a/tests/corpus/5932-generic-class-observed-methods.ts b/tests/corpus/5932-generic-class-observed-methods.ts new file mode 100644 index 0000000000..0756a88ce3 --- /dev/null +++ b/tests/corpus/5932-generic-class-observed-methods.ts @@ -0,0 +1,93 @@ +// A write with a computed string key can replace any method, so calls of +// every method name check the receiver for an own property first. Generic +// class instances, their subclasses and mixin classes keep their declared +// methods when no own property replaces them. +class Ctx { + v: number; + constructor() { + this.v = 0; + } + json(o: T): string { + return JSON.stringify(o); + } + status(n: number): Ctx { + this.v = n; + return this; + } +} + +class Box { + value: T; + constructor(value: T) { + this.value = value; + } + get(): T { + return this.value; + } + describe(): string { + return `box(${String(this.value)})`; + } +} + +class NumberBox extends Box { + double(): number { + return this.value * 2; + } + describe(): string { + return `number ${super.describe()}`; + } +} + +class Base { + name(): string { + return "base"; + } +} + +class Tagged extends Base { + tag: T; + constructor(tag: T) { + super(); + this.tag = tag; + } + label(): string { + return `${this.name()}:${String(this.tag)}`; + } +} + +type Ctor = new (...args: any[]) => T; +function Sized(Base: B) { + return class extends Base { + size(): number { + return 3; + } + }; +} +const SizedBase = Sized(Base); + +function write(target: object, key: string, value: unknown): void { + (target as any)[key] = value; +} + +const c = new Ctx<{ ok: boolean }>(); +write(c, "other", () => 42); +console.log(typeof c.json, typeof c.status, c.status(200).json({ ok: true }), c.v); + +const a = new Box("a"); +const b = new Box(1); +const n = new NumberBox(4); +const t = new Tagged(true); +const s = new SizedBase(); +console.log(a.get(), a.describe(), b.get(), b.describe()); +console.log(n.get(), n.double(), n.describe(), t.label(), t.name(), s.size(), s.name()); +console.log(Object.prototype.hasOwnProperty.call(a, "get")); +const read = a.get; +console.log(read.call(a)); + +write(b, "describe", () => "replaced"); +write(n, "double", () => -1); +write(s, "size", () => 0); +console.log(b.describe(), a.describe(), Object.prototype.hasOwnProperty.call(b, "describe")); +console.log(n.double(), new NumberBox(3).double(), s.size(), new SizedBase().size()); +const boxes: Box[] = [b, n, new Box(7)]; +console.log(boxes.map((box) => box.describe()).join(" | ")); diff --git a/tests/corpus/5938-object-literal-computed-keys-keep-methods.ts b/tests/corpus/5938-object-literal-computed-keys-keep-methods.ts new file mode 100644 index 0000000000..bd324210a9 --- /dev/null +++ b/tests/corpus/5938-object-literal-computed-keys-keep-methods.ts @@ -0,0 +1,39 @@ +// An object literal with a computed key that is only known at runtime +// defines a property of that new object. Class methods, including methods +// of generic classes, keep their calls. +class Ctx { + v: number; + constructor() { + this.v = 0; + } + json(o: T): string { + return JSON.stringify(o); + } + status(n: number): Ctx { + this.v = n; + return this; + } +} + +class Counter { + count = 0; + bump(): number { + return ++this.count; + } +} + +function headers(names: string[]): Record { + const out: Record = {}; + for (const name of names) Object.assign(out, { [name.toLowerCase()]: name.length }); + return out; +} + +const prefix = process.argv.length > 99 ? "never" : "x-"; +const key = `${prefix}id`; +const literal: Record = { [key]: 1, [`${key}-2`]: 2 }; +const ctx = new Ctx<{ ok: boolean }>(); +const counter = new Counter(); +counter.bump(); +console.log(literal, headers(["Accept", "Host"])); +console.log(typeof ctx.json, typeof ctx.status, ctx.status(200).json({ ok: true }), ctx.v); +console.log(counter.bump(), counter.count); diff --git a/tests/corpus/5939-js-let-object-reassigned-wider.js b/tests/corpus/5939-js-let-object-reassigned-wider.js new file mode 100644 index 0000000000..f23a6a0596 --- /dev/null +++ b/tests/corpus/5939-js-let-object-reassigned-wider.js @@ -0,0 +1,29 @@ +// An untyped `let` that starts as one object literal and is later +// assigned object literals with more properties holds each object as +// written: the added properties read back, and properties an object lacks +// read as undefined. +function zip(left, right) { + let state = { _tag: "PullBoth" }; + const out = []; + for (let i = 0; i < left.length; i++) { + if (state._tag === "PullBoth") state = { _tag: "PullLeft", right: right[i] }; + else if (state._tag === "PullLeft") { + out.push([left[i], state.right]); + state = { _tag: "PullBoth" }; + } + } + return { out, last: state._tag, pending: state.right }; +} +console.log(zip([1, 2, 3, 4], ["a", "b", "c", "d"])); + +let point = { x: 1 }; +const update = () => { + point = { x: 2, y: 3 }; +}; +console.log(point.y, Object.keys(point)); +update(); +console.log(point.x, point.y, Object.keys(point), JSON.stringify(point)); + +let config = { name: "base" }; +config = { ...config, debug: true }; +console.log(config.debug, config); diff --git a/tests/corpus/5942-local-class-observed-methods.ts b/tests/corpus/5942-local-class-observed-methods.ts new file mode 100644 index 0000000000..7445b322a2 --- /dev/null +++ b/tests/corpus/5942-local-class-observed-methods.ts @@ -0,0 +1,63 @@ +// After a write with a computed string key, calls of every method name +// check the receiver for the property. Instances of function-local +// classes keep their declared methods, see own-property replacements, and +// see methods the program replaces on the class's prototype. +function write(target: object, key: string, value: unknown): void { + (target as any)[key] = value; +} + +function makeCounter(start: number) { + class Counter { + n: number; + constructor() { + this.n = start; + } + bump(): number { + return ++this.n; + } + label(): string { + return `counter ${this.n}`; + } + } + return new Counter(); +} + +function run(start: number): string { + class Step { + value = start; + next(): number { + this.value *= 2; + return this.value; + } + twice(): number { + this.next(); + return this.next(); + } + } + const step = new Step(); + return `${typeof step.next} ${step.twice()} ${step.next()}`; +} + +function makePatched(start: number) { + class Patched { + n = start; + bump(): number { + return ++this.n; + } + } + Patched.prototype.bump = function (this: Patched) { + this.n += 10; + return this.n; + }; + return new Patched(); +} + +const counter = makeCounter(5); +write({}, "other", 1); +console.log(typeof counter.bump, counter.bump(), counter.label()); +const bump = counter.bump; +console.log(bump.call(counter)); +write(counter, "label", () => "replaced"); +console.log(counter.label(), makeCounter(1).label()); +console.log(run(1), run(10)); +console.log(makePatched(1).bump()); diff --git a/tests/corpus/5943-fields-read-before-assignment.ts b/tests/corpus/5943-fields-read-before-assignment.ts new file mode 100644 index 0000000000..922b3c34d4 --- /dev/null +++ b/tests/corpus/5943-fields-read-before-assignment.ts @@ -0,0 +1,181 @@ +// @transform-types +// Fields that a base constructor (or the constructor itself) can read +// through an override or a method call before they are assigned hold +// undefined then, as in Node, for every field kind. After construction +// the fields read and update normally. +class Point { + x: number; + constructor(x: number) { + this.x = x; + } +} +function show(label: string, value: number): string { + return `${label}=${value}`; +} +abstract class Widget { + log: string[] = []; + constructor() { + this.describe("base"); + } + abstract describe(when: string): void; +} +class Panel extends Widget { + label: string; + width: number; + visible: boolean; + origin: Point; + items: number[]; + options: { depth: number }; + key: string | number; + index: Map; + tags: Set; + count = 5; + constructor(label: string, readonly id: string) { + super(); + this.label = label; + this.width = 10; + this.visible = true; + this.origin = new Point(1); + this.items = [1, 2]; + this.options = { depth: 2 }; + this.key = 7; + this.index = new Map([["a", 1]]); + this.tags = new Set(["t"]); + this.describe("ctor"); + } + describe(when: string): void { + console.log(when, this.label, this.width, this.visible, this.origin, this.items, this.options, this.key, this.count, this.id); + console.log(when, `${this.label}:${this.width}`, this.width + 1, this.count * 2, this.label === undefined, typeof this.items, this.origin?.x, this.options?.depth ?? -1, this.index?.size, this.tags?.has("t")); + console.log(when, show(this.label, this.width), String(this.key), [this.label].length); + try { + console.log(this.label.length, this.items.length); + } catch (error) { + console.log(when, (error as Error).message); + } + } + grow(): number { + this.count++; + this.width += 5; + this.label += "!"; + this.items.push(this.width); + return this.count + this.width + this.items.length; + } +} +const panel = new Panel("main", "p1"); +console.log(panel.grow(), panel.label, panel.items, panel.count, panel.width); + +class Plain { + total: number; + name: string; + constructor() { + this.total = 1; + this.name = "plain"; + this.bump(); + } + bump(): void { + this.total++; + } +} +const plain = new Plain(); +console.log(plain.total, plain.name); + +class Toucher { + constructor() { + this.touch(); + } + touch(): void {} +} +class Counter extends Toucher { + count = 0; + label: string; + items: string[]; + constructor() { + super(); + this.label = "c"; + this.items = []; + } + override touch(): void { + this.count++; + this.label += "x"; + console.log(this.count, this.label, Number.isNaN(this.count)); + try { + this.items.push("a"); + } catch (error) { + console.log(error instanceof TypeError); + } + } +} +const c = new Counter(); +c.touch(); +console.log(c.count, c.label, c.items); + +class Shower { + constructor() { + this.show(); + } + show(): void {} +} +class Box extends Shower { + label: string; + value: T; + constructor(value: T) { + super(); + this.label = "box"; + this.value = value; + } + override show(): void { + console.log("show", this.label, this.value); + } +} +new Box(3).show(); +new Box("s").show(); + +type Ctor = new (...args: any[]) => T; +class Viewer { + constructor() { + this.show(); + } + show(): void {} +} +function Labeled>(Parent: B) { + return class extends Parent { + label = "mixed"; + override show(): void { + console.log("show", this.label); + } + }; +} +const L = Labeled(Viewer); +new L().show(); + +interface Crash { loc: number; message: string } +class Setupper { + constructor() { + this.setup(); + } + setup(): void {} +} +class Host extends Setupper { + crash: Crash | null; + name: string | null; + constructor(c: Crash | null) { + super(); + this.crash = c; + this.name = "host"; + } + override setup(): void { + console.log("setup", this.crash, this.name); + if (this.crash !== null) { + try { + console.log(this.crash.loc); + } catch (error) { + console.log(error instanceof TypeError); + } + } + } + report(): string { + if (this.crash) return `${this.crash.message} at ${this.crash.loc}`; + return this.name === null ? "none" : this.name.toUpperCase(); + } +} +console.log(new Host({ loc: 3, message: "boom" }).report(), new Host(null).report()); diff --git a/tests/corpus/5944-js-fields-read-before-assignment.js b/tests/corpus/5944-js-fields-read-before-assignment.js new file mode 100644 index 0000000000..e313a12b00 --- /dev/null +++ b/tests/corpus/5944-js-fields-read-before-assignment.js @@ -0,0 +1,28 @@ +// JavaScript class fields that the constructor creates after it calls a +// method, or after a base constructor that does, hold undefined until +// they are assigned, as in Node. +class Widget { + constructor(name) { + this.render(); + this.name = name; + this.size = 3; + this.title = "widget"; + this.parts = ["a"]; + this.meta = { id: 1 }; + } + render() { + console.log("render", this.name, this.title, this.size, this.parts, this.meta, this.size + 1, `${this.name}!`); + } +} +class Big extends Widget { + constructor() { + super("big"); + this.extra = 2; + } + render() { + super.render(); + console.log("big", this.extra); + } +} +new Widget("w").render(); +new Big().render(); diff --git a/tests/corpus/5958-asserted-union-receiver.ts b/tests/corpus/5958-asserted-union-receiver.ts new file mode 100644 index 0000000000..23a591827d --- /dev/null +++ b/tests/corpus/5958-asserted-union-receiver.ts @@ -0,0 +1,47 @@ +// A class instance asserted to a wider union (`(concrete as A | B).value`) +// keeps its class: field and getter reads go to the known class. +class A { + value: number; + label = "a"; + constructor(value: number) { + this.value = value; + } + describe(): string { + return `A(${this.value})`; + } + get doubled(): number { + return this.value * 2; + } +} +class B { + value: number; + flag = true; + constructor(value: number) { + this.value = value; + } + describe(): string { + return `B(${this.value})`; + } + get doubled(): number { + return this.value * 3; + } +} +class C extends A { + override get doubled(): number { + return this.value * 10; + } +} +type Rec = { value: boolean }; + +const concrete = new A(41); +console.log((concrete as A | B).value + 1, (concrete as A | B).doubled); +console.log((new C(5) as A | B).value, (new C(5) as A | B).doubled, (concrete as A | B).describe()); +console.log((concrete as A | Rec).value, ((concrete as A | B) as A | B).value); +const maybe = (concrete as A | B | undefined)?.value; +console.log(maybe); +function show(x: A): string { + return String((x as A | B).value) + (x satisfies A as A | B).doubled; +} +console.log(show(concrete), show(new C(2))); +const widened = concrete as A | B; +console.log(widened.value, widened instanceof A); diff --git a/tests/corpus/5959-date-to-json.ts b/tests/corpus/5959-date-to-json.ts new file mode 100644 index 0000000000..4776a76f95 --- /dev/null +++ b/tests/corpus/5959-date-to-json.ts @@ -0,0 +1,32 @@ +// Date.prototype.toJSON answers the toISOString() string for a valid date +// and null for an invalid one (the lib types say string), through +// bindings, literals, JSON.stringify and console.log. +const good = new Date(Date.UTC(2024, 1, 29, 12, 30, 15, 250)); +const bad = new Date("not a date"); +const epoch = new Date(0); + +console.log(good.toJSON(), bad.toJSON(), epoch.toJSON("ignored key")); +console.log(JSON.stringify(good.toJSON()), JSON.stringify(bad.toJSON())); + +const kept = bad.toJSON(); +const shown = good.toJSON(); +console.log(kept, shown, kept === null, typeof kept, kept ?? "fallback"); +console.log(`${bad.toJSON()} / ${good.toJSON()}`); + +const annotated: string | null = bad.toJSON(); +console.log(annotated, JSON.stringify({ annotated })); + +let changing: string | null = "start"; +changing = bad.toJSON(); +console.log(changing === null, JSON.stringify([changing, epoch.toJSON()])); + +const record = { at: good.toJSON(), never: bad.toJSON(), kept }; +console.log(JSON.stringify(record), record.never === null, record.at); +console.log(JSON.stringify({ direct: bad.toJSON(), also: epoch.toJSON() })); +console.log(JSON.stringify([bad.toJSON(), good.toJSON(), shown])); + +function stamp(date: Date): string { + const text = date.toJSON(); + return text === null ? "invalid" : text.slice(0, 10); +} +console.log(stamp(good), stamp(bad), stamp(epoch)); diff --git a/tests/corpus/5960-frame-records-own-references.ts b/tests/corpus/5960-frame-records-own-references.ts new file mode 100644 index 0000000000..894037ff70 --- /dev/null +++ b/tests/corpus/5960-frame-records-own-references.ts @@ -0,0 +1,164 @@ +// Records whose fields hold strings, arrays, other records and class +// instances, and that only flow into field reads, release exactly the +// references they hold: on normal scope exit, per loop iteration, on +// break/continue/return, and when a field expression or a producer +// throws while the record is half built. Records that are stored, +// returned, captured, compared by identity or printed stay observable +// exactly as in Node. +interface Token { + kind: string; + text: string; + pos: number; +} +interface Span { + label: string; + parts: string[]; + token: Token; +} +class Counter { + hits = 0; + name: string; + constructor(name: string) { + this.name = name; + } +} +interface Owned { + owner: Counter; + tag: string | undefined; + note?: string; +} + +const words = ["alpha", "beta", "gamma", "delta"]; +function word(i: number): string { + return words[i % words.length]! + i; +} +function makeToken(kind: string, text: string, pos: number): Token { + return { kind, text, pos }; +} +function pick(flag: boolean, a: string, b: string): Token { + return flag ? { kind: "a", text: a, pos: 1 } : makeToken("b", b, 2); +} +function width(t: Token): number { + return t.text.length + t.kind.length; +} +function spanWidth(s: Span): number { + return s.label.length + s.parts.length + width(s.token); +} +function failing(text: string, n: number): string { + if (n % 4 === 3) throw new Error(`bad ${text}`); + return text.toUpperCase(); +} +function strict(kind: string, n: number): Token { + return { kind, text: failing(kind + n, n), pos: n }; +} +const kept: Token[] = []; +function keep(t: Token): number { + kept.push(t); + return t.pos; +} + +let total = 0; +let log = ""; +for (let i = 0; i < 12; i++) { + const w = word(i); + const t = { kind: "id", text: w, pos: i }; + total += t.pos + t.text.length; + const u = makeToken("num", w + "!", i); + if (u.kind === "num") total += width(u); + total += width({ kind: w, text: w + w, pos: 0 }); + const s: Span = { label: "s" + i, parts: [w, t.text, u.text], token: makeToken("x", w, i) }; + total += spanWidth(s) + s.parts.join("").length; + const p = pick(i % 2 === 0, w, t.kind); + log += p.text[0]; + if (i % 5 === 4) total += keep(t); + try { + const bad = { kind: "k", text: failing(w, i), pos: i }; + log += bad.text.length; + } catch (e) { + log += "!"; + } + try { + const half = strict(w, i); + log += half.text.length; + } catch (e) { + log += "?"; + } + if (i === 6) continue; + const late = { kind: w, text: String(i), pos: -i }; + if (late.text === "9") break; + total += late.kind.length; +} +console.log(total, log, kept.map((t) => t.text).join(",")); + +function find(names: string[], target: string): number { + for (let i = 0; i < names.length; i++) { + const entry = { name: names[i]!, index: i, upper: names[i]!.toUpperCase() }; + if (entry.name === target) return entry.index * 10 + entry.upper.length; + } + return -1; +} +console.log(find(words, "gamma"), find(words, "omega")); + +function owners(n: number): string { + const counters = [new Counter("a"), new Counter("b")]; + let out = ""; + for (let i = 0; i < n; i++) { + const o: Owned = { owner: counters[i % 2]!, tag: i % 3 === 0 ? undefined : "t" + i }; + o.owner.hits++; + out += o.tag ?? "-"; + const optional: Owned = { owner: counters[0]!, tag: "x", note: i % 2 ? "odd" : undefined }; + out += optional.note === undefined ? "." : optional.note[0]; + } + return `${out} ${counters[0]!.hits} ${counters[1]!.hits}`; +} +console.log(owners(7)); + +function tally(lines: string[]): string { + let out = ""; + for (const line of lines) { + const parsed = { head: line.slice(0, 1), rest: line.slice(1), size: line.length }; + switch (parsed.head) { + case "a": { + const scoped = { head: parsed.rest, rest: parsed.head, size: 1 }; + out += scoped.head; + break; + } + case "b": + out += parsed.rest.toUpperCase(); + break; + default: + out += parsed.size; + } + } + return out; +} +console.log(tally(["abc", "bcd", "cde", "a"])); + +function guarded(n: number): string { + let out = ""; + for (let i = 0; i < n; i++) { + try { + const r = { name: word(i), list: [i, i + 1] }; + if (i % 2 === 0) throw new Error(r.name); + out += r.list.length; + } catch (e) { + out += (e as Error).message.length; + } finally { + const f = { name: "f" + i, list: [i] }; + out += f.name.length; + } + } + return out; +} +console.log(guarded(5)); + +// Identity, storage, capture and printing keep heap records. +const a = { kind: "same", text: "x", pos: 1 }; +const b = { kind: "same", text: "x", pos: 1 }; +const readers: (() => string)[] = []; +for (let i = 0; i < 3; i++) { + const r = { kind: "r", text: word(i), pos: i }; + readers.push(() => r.text); +} +console.log(a === b, readers.map((read) => read()).join(" ")); +console.log({ kind: "shown", text: word(2), pos: 3 }, JSON.stringify(makeToken("j", "s", 1))); diff --git a/tests/corpus/5960-generic-mapped-types.ts b/tests/corpus/5960-generic-mapped-types.ts new file mode 100644 index 0000000000..229a97d887 --- /dev/null +++ b/tests/corpus/5960-generic-mapped-types.ts @@ -0,0 +1,64 @@ +// Mapped types over a generic function's own type parameters lower in its +// monomorphized instances: homomorphic copies, modifiers, conditional +// templates with infer, and indexed templates, including over an +// index-signature constraint whose keys only the instance knows. +type Schema = { parse(input: unknown): T }; +type Infer = S extends Schema ? T : never; +interface Typed { + readonly _output: Output; +} +type Output> = T["_output"]; + +const str: Schema = { parse: (input) => String(input) }; +const num: Schema = { parse: (input) => Number(input) }; + +function same(value: S): { [K in keyof S]: S[K] } { + return value; +} +function frozen(value: S): { readonly [K in keyof S]: S[K] } { + return { ...value }; +} +function loose(value: S): { [K in keyof S]?: S[K] } { + return value; +} +function pair; right: Schema }>( + shape: S, + input: { left: unknown; right: unknown }, +): { [K in keyof S]: Infer } { + return { + left: shape.left.parse(input.left), + right: shape.right.parse(input.right), + } as { [K in keyof S]: Infer }; +} +function objectWith>>( + shape: S, + build: (shape: S) => { [K in keyof S]: Infer }, +): { [K in keyof S]: Infer } { + const parsed: { [K in keyof S]: Infer } = build(shape); + return parsed; +} +function outputs>>( + values: S, + read: (values: S) => { [K in keyof S]: Output }, +): { [K in keyof S]: Output } { + return read(values); +} + +const s = same({ a: 1, b: "two" }); +console.log(s.a + 1, s.b.length); +const f = frozen({ x: true, y: [1, 2] }); +console.log(f.x, f.y.length); +const l = loose({ n: 3 }); +console.log(l.n ?? 0); +const p = pair({ left: str, right: num }, { left: 12, right: "34" }); +console.log(p.left.length, p.right + 1); +const person = objectWith({ name: str, age: num }, (shape) => ({ + name: shape.name.parse("Ada"), + age: shape.age.parse("36"), +})); +console.log(person.name, person.age + 1); +const out = outputs({ id: { _output: 7 }, tag: { _output: "t" } }, (values) => ({ + id: values.id._output, + tag: values.tag._output, +})); +console.log(out.id * 2, out.tag.toUpperCase()); diff --git a/tests/corpus/5961-frame-spread-updates.ts b/tests/corpus/5961-frame-spread-updates.ts new file mode 100644 index 0000000000..ef2f79cd9a --- /dev/null +++ b/tests/corpus/5961-frame-spread-updates.ts @@ -0,0 +1,96 @@ +// Same-shape spread updates (`{ ...state, field: value }`) of records that +// hold strings and arrays: each update reads the whole old value before +// any override evaluates, releases the replaced fields exactly once, and +// keeps the previous value when an override throws. Aliases, identity, +// storage and printing keep the heap object Node would have. +interface State { + name: string; + count: number; + items: string[]; + last: string | undefined; +} +interface Num { + count: number; + total: number; +} +function bump(s: State, item: string): State { + return { ...s, count: s.count + 1, last: item }; +} +function rename(s: State, name: string): State { + return name.length > 3 ? { ...s, name } : s.count > 2 ? { ...s, name: name + "!" } : bump(s, name); +} +function check(n: number): number { + if (n === 3) throw new Error("three"); + return n * 2; +} +function describe(s: State): string { + return `${s.name}:${s.count}:${s.items.length}:${s.last ?? "-"}`; +} + +function run(n: number): string { + let state: State = { name: "s", count: 0, items: ["a"], last: undefined }; + let out = ""; + for (let i = 0; i < n; i++) { + state = { ...state, count: state.count + 1 }; + if (i % 2 === 0) state = { ...state, last: "i" + i, name: state.name + i }; + state = bump(state, "b" + i); + state = rename(state, i % 3 === 0 ? "long" + i : "x"); + try { + state = { ...state, count: check(i) }; + } catch (e) { + out += "!"; + } + out += describe(state) + " "; + } + return out + state.items.join(""); +} +console.log(run(6)); + +function numeric(n: number): number { + let state: Num = { count: 0, total: 0 }; + for (let i = 0; i < n; i++) { + state = { ...state, count: state.count + 1 }; + if (i % 3 === 0) state = { ...state, total: state.total + i }; + } + return state.count * 1000 + state.total; +} +console.log(numeric(10)); + +// Overrides see the old value and run after the copy. +function order(): string { + let s: State = { name: "n", count: 1, items: [], last: "l" }; + let log = ""; + s = { ...s, name: (log += s.name + s.count, s.name + "2"), count: s.count + 10 }; + s = { ...s, last: s.name }; + return `${log} ${describe(s)}`; +} +console.log(order()); + +// Spread of a temporary and of a producer result. +function fresh(i: number): State { + return { name: "f" + i, count: i, items: [String(i)], last: undefined }; +} +function temporaries(n: number): string { + let out = ""; + for (let i = 0; i < n; i++) { + const s = { ...fresh(i), count: 100 + i }; + const t = { ...s, items: [s.name, s.name] }; + out += describe(s) + "/" + describe(t) + " "; + } + return out; +} +console.log(temporaries(3)); + +// Aliases and identity are observable: these stay heap objects. +let current: State = { name: "c", count: 0, items: [], last: undefined }; +const before = current; +current = { ...current, count: 5 }; +console.log(before === current, before.count, current.count); +const history: State[] = []; +let tracked: State = { name: "h", count: 0, items: [], last: undefined }; +for (let i = 0; i < 3; i++) { + tracked = { ...tracked, count: tracked.count + 1 }; + history.push(tracked); +} +console.log(history.map((s) => s.count).join(","), history[0] === history[1]); +console.log({ ...tracked, last: "shown" }); diff --git a/tests/corpus/5962-frame-tuples.ts b/tests/corpus/5962-frame-tuples.ts new file mode 100644 index 0000000000..b03cbc79d4 --- /dev/null +++ b/tests/corpus/5962-frame-tuples.ts @@ -0,0 +1,57 @@ +// Tuples that are only destructured or indexed: number and string +// elements, producer functions returning fresh tuples, literals passed to +// functions that only read their elements, and tuples that escape into +// arrays, closures, identity comparisons and printing. +function divmod(a: number, b: number): [number, number] { + return [Math.floor(a / b), a % b]; +} +const names = ["a", "bb", "ccc"]; +function entry(i: number): [string, number] { + return [names[i % 3]! + i, i]; +} +function flip(p: [string, number]): [number, string] { + return [p[1] * 2, p[0].toUpperCase()]; +} +function area(r: [number, number]): number { + return r[0] * r[1]; +} +function risky(i: number): [string, string] { + if (i % 4 === 2) throw new Error("risky " + i); + return ["r" + i, String(i * i)]; +} +const kept: [string, number][] = []; + +let acc = 0; +let log = ""; +for (let i = 0; i < 10; i++) { + const [q, r] = divmod(i * 13, 7); + acc += q + r; + const [k, v] = entry(i); + acc += k.length + v; + const pair: [number, number] = [i, i + 1]; + acc += pair[0] * pair[1] + area([i, 2]) + area(pair); + const [n, s] = flip(entry(i + 1)); + log += s + n; + try { + const [x, y] = risky(i); + log += x + y.length; + } catch (e) { + log += "!"; + } + if (i % 3 === 0) kept.push(entry(i)); +} +console.log(acc, log); +console.log(kept.map(([name, i]) => name + "=" + i).join(" ")); + +let [left, right] = ["l", "r"]; +for (let i = 0; i < 3; i++) [left, right] = [right, left + i]; +console.log(left, right); + +const t1: [number, string] = [1, "x"]; +const t2: [number, string] = [1, "x"]; +const getters: (() => string)[] = []; +for (let i = 0; i < 2; i++) { + const t: [string, number] = [names[i]!, i]; + getters.push(() => t[0] + t[1]); +} +console.log(t1 === t2, getters.map((get) => get()).join(","), entry(4), JSON.stringify(divmod(9, 4))); diff --git a/tests/corpus/5973-large-closure-environment.ts b/tests/corpus/5973-large-closure-environment.ts new file mode 100644 index 0000000000..1ade2570c9 --- /dev/null +++ b/tests/corpus/5973-large-closure-environment.ts @@ -0,0 +1,1859 @@ +// One factory with 600 captured bindings (more than any ordinary function holds), +// shared by methods that read and rebind them, closures created per loop +// iteration, a nested closure, and a function declared before the bindings +// it reads. Every environment observes the same boxes: writes through one +// method are visible through the others and in later calls, and each +// factory call gets its own bindings. +function total(list: number[]): number { + let t = 0; + for (const n of list) t += n; + return t; +} +const one = Number("1"); +function make(seed: number) { + function early(): number { + return n0 + n539 + s541.length; + } + let before = "unset"; + try { + before = String(early()); + } catch (e) { + before = (e as Error).name + ": " + (e as Error).message; + } + let n0 = seed + 0; + let n1 = seed + 1; + let n2 = seed + 2; + let n3 = seed + 3; + let n4 = seed + 4; + let n5 = seed + 5; + let n6 = seed + 6; + let n7 = seed + 7; + let n8 = seed + 8; + let n9 = seed + 9; + let n10 = seed + 10; + let n11 = seed + 11; + let n12 = seed + 12; + let n13 = seed + 13; + let n14 = seed + 14; + let n15 = seed + 15; + let n16 = seed + 16; + let n17 = seed + 17; + let n18 = seed + 18; + let n19 = seed + 19; + let n20 = seed + 20; + let n21 = seed + 21; + let n22 = seed + 22; + let n23 = seed + 23; + let n24 = seed + 24; + let n25 = seed + 25; + let n26 = seed + 26; + let n27 = seed + 27; + let n28 = seed + 28; + let n29 = seed + 29; + let n30 = seed + 30; + let n31 = seed + 31; + let n32 = seed + 32; + let n33 = seed + 33; + let n34 = seed + 34; + let n35 = seed + 35; + let n36 = seed + 36; + let n37 = seed + 37; + let n38 = seed + 38; + let n39 = seed + 39; + let n40 = seed + 40; + let n41 = seed + 41; + let n42 = seed + 42; + let n43 = seed + 43; + let n44 = seed + 44; + let n45 = seed + 45; + let n46 = seed + 46; + let n47 = seed + 47; + let n48 = seed + 48; + let n49 = seed + 49; + let n50 = seed + 50; + let n51 = seed + 51; + let n52 = seed + 52; + let n53 = seed + 53; + let n54 = seed + 54; + let n55 = seed + 55; + let n56 = seed + 56; + let n57 = seed + 57; + let n58 = seed + 58; + let n59 = seed + 59; + let n60 = seed + 60; + let n61 = seed + 61; + let n62 = seed + 62; + let n63 = seed + 63; + let n64 = seed + 64; + let n65 = seed + 65; + let n66 = seed + 66; + let n67 = seed + 67; + let n68 = seed + 68; + let n69 = seed + 69; + let n70 = seed + 70; + let n71 = seed + 71; + let n72 = seed + 72; + let n73 = seed + 73; + let n74 = seed + 74; + let n75 = seed + 75; + let n76 = seed + 76; + let n77 = seed + 77; + let n78 = seed + 78; + let n79 = seed + 79; + let n80 = seed + 80; + let n81 = seed + 81; + let n82 = seed + 82; + let n83 = seed + 83; + let n84 = seed + 84; + let n85 = seed + 85; + let n86 = seed + 86; + let n87 = seed + 87; + let n88 = seed + 88; + let n89 = seed + 89; + let n90 = seed + 90; + let n91 = seed + 91; + let n92 = seed + 92; + let n93 = seed + 93; + let n94 = seed + 94; + let n95 = seed + 95; + let n96 = seed + 96; + let n97 = seed + 97; + let n98 = seed + 98; + let n99 = seed + 99; + let n100 = seed + 100; + let n101 = seed + 101; + let n102 = seed + 102; + let n103 = seed + 103; + let n104 = seed + 104; + let n105 = seed + 105; + let n106 = seed + 106; + let n107 = seed + 107; + let n108 = seed + 108; + let n109 = seed + 109; + let n110 = seed + 110; + let n111 = seed + 111; + let n112 = seed + 112; + let n113 = seed + 113; + let n114 = seed + 114; + let n115 = seed + 115; + let n116 = seed + 116; + let n117 = seed + 117; + let n118 = seed + 118; + let n119 = seed + 119; + let n120 = seed + 120; + let n121 = seed + 121; + let n122 = seed + 122; + let n123 = seed + 123; + let n124 = seed + 124; + let n125 = seed + 125; + let n126 = seed + 126; + let n127 = seed + 127; + let n128 = seed + 128; + let n129 = seed + 129; + let n130 = seed + 130; + let n131 = seed + 131; + let n132 = seed + 132; + let n133 = seed + 133; + let n134 = seed + 134; + let n135 = seed + 135; + let n136 = seed + 136; + let n137 = seed + 137; + let n138 = seed + 138; + let n139 = seed + 139; + let n140 = seed + 140; + let n141 = seed + 141; + let n142 = seed + 142; + let n143 = seed + 143; + let n144 = seed + 144; + let n145 = seed + 145; + let n146 = seed + 146; + let n147 = seed + 147; + let n148 = seed + 148; + let n149 = seed + 149; + let n150 = seed + 150; + let n151 = seed + 151; + let n152 = seed + 152; + let n153 = seed + 153; + let n154 = seed + 154; + let n155 = seed + 155; + let n156 = seed + 156; + let n157 = seed + 157; + let n158 = seed + 158; + let n159 = seed + 159; + let n160 = seed + 160; + let n161 = seed + 161; + let n162 = seed + 162; + let n163 = seed + 163; + let n164 = seed + 164; + let n165 = seed + 165; + let n166 = seed + 166; + let n167 = seed + 167; + let n168 = seed + 168; + let n169 = seed + 169; + let n170 = seed + 170; + let n171 = seed + 171; + let n172 = seed + 172; + let n173 = seed + 173; + let n174 = seed + 174; + let n175 = seed + 175; + let n176 = seed + 176; + let n177 = seed + 177; + let n178 = seed + 178; + let n179 = seed + 179; + let n180 = seed + 180; + let n181 = seed + 181; + let n182 = seed + 182; + let n183 = seed + 183; + let n184 = seed + 184; + let n185 = seed + 185; + let n186 = seed + 186; + let n187 = seed + 187; + let n188 = seed + 188; + let n189 = seed + 189; + let n190 = seed + 190; + let n191 = seed + 191; + let n192 = seed + 192; + let n193 = seed + 193; + let n194 = seed + 194; + let n195 = seed + 195; + let n196 = seed + 196; + let n197 = seed + 197; + let n198 = seed + 198; + let n199 = seed + 199; + let n200 = seed + 200; + let n201 = seed + 201; + let n202 = seed + 202; + let n203 = seed + 203; + let n204 = seed + 204; + let n205 = seed + 205; + let n206 = seed + 206; + let n207 = seed + 207; + let n208 = seed + 208; + let n209 = seed + 209; + let n210 = seed + 210; + let n211 = seed + 211; + let n212 = seed + 212; + let n213 = seed + 213; + let n214 = seed + 214; + let n215 = seed + 215; + let n216 = seed + 216; + let n217 = seed + 217; + let n218 = seed + 218; + let n219 = seed + 219; + let n220 = seed + 220; + let n221 = seed + 221; + let n222 = seed + 222; + let n223 = seed + 223; + let n224 = seed + 224; + let n225 = seed + 225; + let n226 = seed + 226; + let n227 = seed + 227; + let n228 = seed + 228; + let n229 = seed + 229; + let n230 = seed + 230; + let n231 = seed + 231; + let n232 = seed + 232; + let n233 = seed + 233; + let n234 = seed + 234; + let n235 = seed + 235; + let n236 = seed + 236; + let n237 = seed + 237; + let n238 = seed + 238; + let n239 = seed + 239; + let n240 = seed + 240; + let n241 = seed + 241; + let n242 = seed + 242; + let n243 = seed + 243; + let n244 = seed + 244; + let n245 = seed + 245; + let n246 = seed + 246; + let n247 = seed + 247; + let n248 = seed + 248; + let n249 = seed + 249; + let n250 = seed + 250; + let n251 = seed + 251; + let n252 = seed + 252; + let n253 = seed + 253; + let n254 = seed + 254; + let n255 = seed + 255; + let n256 = seed + 256; + let n257 = seed + 257; + let n258 = seed + 258; + let n259 = seed + 259; + let n260 = seed + 260; + let n261 = seed + 261; + let n262 = seed + 262; + let n263 = seed + 263; + let n264 = seed + 264; + let n265 = seed + 265; + let n266 = seed + 266; + let n267 = seed + 267; + let n268 = seed + 268; + let n269 = seed + 269; + let n270 = seed + 270; + let n271 = seed + 271; + let n272 = seed + 272; + let n273 = seed + 273; + let n274 = seed + 274; + let n275 = seed + 275; + let n276 = seed + 276; + let n277 = seed + 277; + let n278 = seed + 278; + let n279 = seed + 279; + let n280 = seed + 280; + let n281 = seed + 281; + let n282 = seed + 282; + let n283 = seed + 283; + let n284 = seed + 284; + let n285 = seed + 285; + let n286 = seed + 286; + let n287 = seed + 287; + let n288 = seed + 288; + let n289 = seed + 289; + let n290 = seed + 290; + let n291 = seed + 291; + let n292 = seed + 292; + let n293 = seed + 293; + let n294 = seed + 294; + let n295 = seed + 295; + let n296 = seed + 296; + let n297 = seed + 297; + let n298 = seed + 298; + let n299 = seed + 299; + let n300 = seed + 300; + let n301 = seed + 301; + let n302 = seed + 302; + let n303 = seed + 303; + let n304 = seed + 304; + let n305 = seed + 305; + let n306 = seed + 306; + let n307 = seed + 307; + let n308 = seed + 308; + let n309 = seed + 309; + let n310 = seed + 310; + let n311 = seed + 311; + let n312 = seed + 312; + let n313 = seed + 313; + let n314 = seed + 314; + let n315 = seed + 315; + let n316 = seed + 316; + let n317 = seed + 317; + let n318 = seed + 318; + let n319 = seed + 319; + let n320 = seed + 320; + let n321 = seed + 321; + let n322 = seed + 322; + let n323 = seed + 323; + let n324 = seed + 324; + let n325 = seed + 325; + let n326 = seed + 326; + let n327 = seed + 327; + let n328 = seed + 328; + let n329 = seed + 329; + let n330 = seed + 330; + let n331 = seed + 331; + let n332 = seed + 332; + let n333 = seed + 333; + let n334 = seed + 334; + let n335 = seed + 335; + let n336 = seed + 336; + let n337 = seed + 337; + let n338 = seed + 338; + let n339 = seed + 339; + let n340 = seed + 340; + let n341 = seed + 341; + let n342 = seed + 342; + let n343 = seed + 343; + let n344 = seed + 344; + let n345 = seed + 345; + let n346 = seed + 346; + let n347 = seed + 347; + let n348 = seed + 348; + let n349 = seed + 349; + let n350 = seed + 350; + let n351 = seed + 351; + let n352 = seed + 352; + let n353 = seed + 353; + let n354 = seed + 354; + let n355 = seed + 355; + let n356 = seed + 356; + let n357 = seed + 357; + let n358 = seed + 358; + let n359 = seed + 359; + let n360 = seed + 360; + let n361 = seed + 361; + let n362 = seed + 362; + let n363 = seed + 363; + let n364 = seed + 364; + let n365 = seed + 365; + let n366 = seed + 366; + let n367 = seed + 367; + let n368 = seed + 368; + let n369 = seed + 369; + let n370 = seed + 370; + let n371 = seed + 371; + let n372 = seed + 372; + let n373 = seed + 373; + let n374 = seed + 374; + let n375 = seed + 375; + let n376 = seed + 376; + let n377 = seed + 377; + let n378 = seed + 378; + let n379 = seed + 379; + let n380 = seed + 380; + let n381 = seed + 381; + let n382 = seed + 382; + let n383 = seed + 383; + let n384 = seed + 384; + let n385 = seed + 385; + let n386 = seed + 386; + let n387 = seed + 387; + let n388 = seed + 388; + let n389 = seed + 389; + let n390 = seed + 390; + let n391 = seed + 391; + let n392 = seed + 392; + let n393 = seed + 393; + let n394 = seed + 394; + let n395 = seed + 395; + let n396 = seed + 396; + let n397 = seed + 397; + let n398 = seed + 398; + let n399 = seed + 399; + let n400 = seed + 400; + let n401 = seed + 401; + let n402 = seed + 402; + let n403 = seed + 403; + let n404 = seed + 404; + let n405 = seed + 405; + let n406 = seed + 406; + let n407 = seed + 407; + let n408 = seed + 408; + let n409 = seed + 409; + let n410 = seed + 410; + let n411 = seed + 411; + let n412 = seed + 412; + let n413 = seed + 413; + let n414 = seed + 414; + let n415 = seed + 415; + let n416 = seed + 416; + let n417 = seed + 417; + let n418 = seed + 418; + let n419 = seed + 419; + let n420 = seed + 420; + let n421 = seed + 421; + let n422 = seed + 422; + let n423 = seed + 423; + let n424 = seed + 424; + let n425 = seed + 425; + let n426 = seed + 426; + let n427 = seed + 427; + let n428 = seed + 428; + let n429 = seed + 429; + let n430 = seed + 430; + let n431 = seed + 431; + let n432 = seed + 432; + let n433 = seed + 433; + let n434 = seed + 434; + let n435 = seed + 435; + let n436 = seed + 436; + let n437 = seed + 437; + let n438 = seed + 438; + let n439 = seed + 439; + let n440 = seed + 440; + let n441 = seed + 441; + let n442 = seed + 442; + let n443 = seed + 443; + let n444 = seed + 444; + let n445 = seed + 445; + let n446 = seed + 446; + let n447 = seed + 447; + let n448 = seed + 448; + let n449 = seed + 449; + let n450 = seed + 450; + let n451 = seed + 451; + let n452 = seed + 452; + let n453 = seed + 453; + let n454 = seed + 454; + let n455 = seed + 455; + let n456 = seed + 456; + let n457 = seed + 457; + let n458 = seed + 458; + let n459 = seed + 459; + let n460 = seed + 460; + let n461 = seed + 461; + let n462 = seed + 462; + let n463 = seed + 463; + let n464 = seed + 464; + let n465 = seed + 465; + let n466 = seed + 466; + let n467 = seed + 467; + let n468 = seed + 468; + let n469 = seed + 469; + let n470 = seed + 470; + let n471 = seed + 471; + let n472 = seed + 472; + let n473 = seed + 473; + let n474 = seed + 474; + let n475 = seed + 475; + let n476 = seed + 476; + let n477 = seed + 477; + let n478 = seed + 478; + let n479 = seed + 479; + let n480 = seed + 480; + let n481 = seed + 481; + let n482 = seed + 482; + let n483 = seed + 483; + let n484 = seed + 484; + let n485 = seed + 485; + let n486 = seed + 486; + let n487 = seed + 487; + let n488 = seed + 488; + let n489 = seed + 489; + let n490 = seed + 490; + let n491 = seed + 491; + let n492 = seed + 492; + let n493 = seed + 493; + let n494 = seed + 494; + let n495 = seed + 495; + let n496 = seed + 496; + let n497 = seed + 497; + let n498 = seed + 498; + let n499 = seed + 499; + let n500 = seed + 500; + let n501 = seed + 501; + let n502 = seed + 502; + let n503 = seed + 503; + let n504 = seed + 504; + let n505 = seed + 505; + let n506 = seed + 506; + let n507 = seed + 507; + let n508 = seed + 508; + let n509 = seed + 509; + let n510 = seed + 510; + let n511 = seed + 511; + let n512 = seed + 512; + let n513 = seed + 513; + let n514 = seed + 514; + let n515 = seed + 515; + let n516 = seed + 516; + let n517 = seed + 517; + let n518 = seed + 518; + let n519 = seed + 519; + let n520 = seed + 520; + let n521 = seed + 521; + let n522 = seed + 522; + let n523 = seed + 523; + let n524 = seed + 524; + let n525 = seed + 525; + let n526 = seed + 526; + let n527 = seed + 527; + let n528 = seed + 528; + let n529 = seed + 529; + let n530 = seed + 530; + let n531 = seed + 531; + let n532 = seed + 532; + let n533 = seed + 533; + let n534 = seed + 534; + let n535 = seed + 535; + let n536 = seed + 536; + let n537 = seed + 537; + let n538 = seed + 538; + let n539 = seed + 539; + let o540 = { x: 540 }; + let s541 = "s" + (541 + one); + let o542 = { x: 542 }; + let s543 = "s" + (543 + one); + let o544 = { x: 544 }; + let s545 = "s" + (545 + one); + let o546 = { x: 546 }; + let s547 = "s" + (547 + one); + let o548 = { x: 548 }; + let s549 = "s" + (549 + one); + let o550 = { x: 550 }; + let s551 = "s" + (551 + one); + let o552 = { x: 552 }; + let s553 = "s" + (553 + one); + let o554 = { x: 554 }; + let s555 = "s" + (555 + one); + let o556 = { x: 556 }; + let s557 = "s" + (557 + one); + let o558 = { x: 558 }; + let s559 = "s" + (559 + one); + let o560 = { x: 560 }; + let s561 = "s" + (561 + one); + let o562 = { x: 562 }; + let s563 = "s" + (563 + one); + let o564 = { x: 564 }; + let s565 = "s" + (565 + one); + let o566 = { x: 566 }; + let s567 = "s" + (567 + one); + let o568 = { x: 568 }; + let s569 = "s" + (569 + one); + let o570 = { x: 570 }; + let s571 = "s" + (571 + one); + let o572 = { x: 572 }; + let s573 = "s" + (573 + one); + let o574 = { x: 574 }; + let s575 = "s" + (575 + one); + let o576 = { x: 576 }; + let s577 = "s" + (577 + one); + let o578 = { x: 578 }; + let s579 = "s" + (579 + one); + let o580 = { x: 580 }; + let s581 = "s" + (581 + one); + let o582 = { x: 582 }; + let s583 = "s" + (583 + one); + let o584 = { x: 584 }; + let s585 = "s" + (585 + one); + let o586 = { x: 586 }; + let s587 = "s" + (587 + one); + let o588 = { x: 588 }; + let s589 = "s" + (589 + one); + let o590 = { x: 590 }; + let s591 = "s" + (591 + one); + let o592 = { x: 592 }; + let s593 = "s" + (593 + one); + let o594 = { x: 594 }; + let s595 = "s" + (595 + one); + let o596 = { x: 596 }; + let s597 = "s" + (597 + one); + let o598 = { x: 598 }; + let s599 = "s" + (599 + one); + const readers: (() => string)[] = []; + for (let i = 0; i < 3; i++) { + const label = "r" + i; + readers.push(() => label + ":" + (n539 + i) + ":" + o540.x + ":" + s541); + } + return { + before, + early, + sum(): number { + return total([ + n0, + n1, + n2, + n3, + n4, + n5, + n6, + n7, + n8, + n9, + n10, + n11, + n12, + n13, + n14, + n15, + n16, + n17, + n18, + n19, + n20, + n21, + n22, + n23, + n24, + n25, + n26, + n27, + n28, + n29, + n30, + n31, + n32, + n33, + n34, + n35, + n36, + n37, + n38, + n39, + n40, + n41, + n42, + n43, + n44, + n45, + n46, + n47, + n48, + n49, + n50, + n51, + n52, + n53, + n54, + n55, + n56, + n57, + n58, + n59, + n60, + n61, + n62, + n63, + n64, + n65, + n66, + n67, + n68, + n69, + n70, + n71, + n72, + n73, + n74, + n75, + n76, + n77, + n78, + n79, + n80, + n81, + n82, + n83, + n84, + n85, + n86, + n87, + n88, + n89, + n90, + n91, + n92, + n93, + n94, + n95, + n96, + n97, + n98, + n99, + n100, + n101, + n102, + n103, + n104, + n105, + n106, + n107, + n108, + n109, + n110, + n111, + n112, + n113, + n114, + n115, + n116, + n117, + n118, + n119, + n120, + n121, + n122, + n123, + n124, + n125, + n126, + n127, + n128, + n129, + n130, + n131, + n132, + n133, + n134, + n135, + n136, + n137, + n138, + n139, + n140, + n141, + n142, + n143, + n144, + n145, + n146, + n147, + n148, + n149, + n150, + n151, + n152, + n153, + n154, + n155, + n156, + n157, + n158, + n159, + n160, + n161, + n162, + n163, + n164, + n165, + n166, + n167, + n168, + n169, + n170, + n171, + n172, + n173, + n174, + n175, + n176, + n177, + n178, + n179, + n180, + n181, + n182, + n183, + n184, + n185, + n186, + n187, + n188, + n189, + n190, + n191, + n192, + n193, + n194, + n195, + n196, + n197, + n198, + n199, + n200, + n201, + n202, + n203, + n204, + n205, + n206, + n207, + n208, + n209, + n210, + n211, + n212, + n213, + n214, + n215, + n216, + n217, + n218, + n219, + n220, + n221, + n222, + n223, + n224, + n225, + n226, + n227, + n228, + n229, + n230, + n231, + n232, + n233, + n234, + n235, + n236, + n237, + n238, + n239, + n240, + n241, + n242, + n243, + n244, + n245, + n246, + n247, + n248, + n249, + n250, + n251, + n252, + n253, + n254, + n255, + n256, + n257, + n258, + n259, + n260, + n261, + n262, + n263, + n264, + n265, + n266, + n267, + n268, + n269, + n270, + n271, + n272, + n273, + n274, + n275, + n276, + n277, + n278, + n279, + n280, + n281, + n282, + n283, + n284, + n285, + n286, + n287, + n288, + n289, + n290, + n291, + n292, + n293, + n294, + n295, + n296, + n297, + n298, + n299, + n300, + n301, + n302, + n303, + n304, + n305, + n306, + n307, + n308, + n309, + n310, + n311, + n312, + n313, + n314, + n315, + n316, + n317, + n318, + n319, + n320, + n321, + n322, + n323, + n324, + n325, + n326, + n327, + n328, + n329, + n330, + n331, + n332, + n333, + n334, + n335, + n336, + n337, + n338, + n339, + n340, + n341, + n342, + n343, + n344, + n345, + n346, + n347, + n348, + n349, + n350, + n351, + n352, + n353, + n354, + n355, + n356, + n357, + n358, + n359, + n360, + n361, + n362, + n363, + n364, + n365, + n366, + n367, + n368, + n369, + n370, + n371, + n372, + n373, + n374, + n375, + n376, + n377, + n378, + n379, + n380, + n381, + n382, + n383, + n384, + n385, + n386, + n387, + n388, + n389, + n390, + n391, + n392, + n393, + n394, + n395, + n396, + n397, + n398, + n399, + n400, + n401, + n402, + n403, + n404, + n405, + n406, + n407, + n408, + n409, + n410, + n411, + n412, + n413, + n414, + n415, + n416, + n417, + n418, + n419, + n420, + n421, + n422, + n423, + n424, + n425, + n426, + n427, + n428, + n429, + n430, + n431, + n432, + n433, + n434, + n435, + n436, + n437, + n438, + n439, + n440, + n441, + n442, + n443, + n444, + n445, + n446, + n447, + n448, + n449, + n450, + n451, + n452, + n453, + n454, + n455, + n456, + n457, + n458, + n459, + n460, + n461, + n462, + n463, + n464, + n465, + n466, + n467, + n468, + n469, + n470, + n471, + n472, + n473, + n474, + n475, + n476, + n477, + n478, + n479, + n480, + n481, + n482, + n483, + n484, + n485, + n486, + n487, + n488, + n489, + n490, + n491, + n492, + n493, + n494, + n495, + n496, + n497, + n498, + n499, + n500, + n501, + n502, + n503, + n504, + n505, + n506, + n507, + n508, + n509, + n510, + n511, + n512, + n513, + n514, + n515, + n516, + n517, + n518, + n519, + n520, + n521, + n522, + n523, + n524, + n525, + n526, + n527, + n528, + n529, + n530, + n531, + n532, + n533, + n534, + n535, + n536, + n537, + n538, + n539, + o540.x, + s541.length, + o542.x, + s543.length, + o544.x, + s545.length, + o546.x, + s547.length, + o548.x, + s549.length, + o550.x, + s551.length, + o552.x, + s553.length, + o554.x, + s555.length, + o556.x, + s557.length, + o558.x, + s559.length, + o560.x, + s561.length, + o562.x, + s563.length, + o564.x, + s565.length, + o566.x, + s567.length, + o568.x, + s569.length, + o570.x, + s571.length, + o572.x, + s573.length, + o574.x, + s575.length, + o576.x, + s577.length, + o578.x, + s579.length, + o580.x, + s581.length, + o582.x, + s583.length, + o584.x, + s585.length, + o586.x, + s587.length, + o588.x, + s589.length, + o590.x, + s591.length, + o592.x, + s593.length, + o594.x, + s595.length, + o596.x, + s597.length, + o598.x, + s599.length, + ]); + }, + bump(): void { + n0 += 1; + n1 += 1; + n2 += 1; + n3 += 1; + n4 += 1; + n5 += 1; + n6 += 1; + n7 += 1; + n8 += 1; + n9 += 1; + n10 += 1; + n11 += 1; + n12 += 1; + n13 += 1; + n14 += 1; + n15 += 1; + n16 += 1; + n17 += 1; + n18 += 1; + n19 += 1; + n20 += 1; + n21 += 1; + n22 += 1; + n23 += 1; + n24 += 1; + n25 += 1; + n26 += 1; + n27 += 1; + n28 += 1; + n29 += 1; + n30 += 1; + n31 += 1; + n32 += 1; + n33 += 1; + n34 += 1; + n35 += 1; + n36 += 1; + n37 += 1; + n38 += 1; + n39 += 1; + n40 += 1; + n41 += 1; + n42 += 1; + n43 += 1; + n44 += 1; + n45 += 1; + n46 += 1; + n47 += 1; + n48 += 1; + n49 += 1; + n50 += 1; + n51 += 1; + n52 += 1; + n53 += 1; + n54 += 1; + n55 += 1; + n56 += 1; + n57 += 1; + n58 += 1; + n59 += 1; + n60 += 1; + n61 += 1; + n62 += 1; + n63 += 1; + n64 += 1; + n65 += 1; + n66 += 1; + n67 += 1; + n68 += 1; + n69 += 1; + n70 += 1; + n71 += 1; + n72 += 1; + n73 += 1; + n74 += 1; + n75 += 1; + n76 += 1; + n77 += 1; + n78 += 1; + n79 += 1; + n80 += 1; + n81 += 1; + n82 += 1; + n83 += 1; + n84 += 1; + n85 += 1; + n86 += 1; + n87 += 1; + n88 += 1; + n89 += 1; + n90 += 1; + n91 += 1; + n92 += 1; + n93 += 1; + n94 += 1; + n95 += 1; + n96 += 1; + n97 += 1; + n98 += 1; + n99 += 1; + n100 += 1; + n101 += 1; + n102 += 1; + n103 += 1; + n104 += 1; + n105 += 1; + n106 += 1; + n107 += 1; + n108 += 1; + n109 += 1; + n110 += 1; + n111 += 1; + n112 += 1; + n113 += 1; + n114 += 1; + n115 += 1; + n116 += 1; + n117 += 1; + n118 += 1; + n119 += 1; + n120 += 1; + n121 += 1; + n122 += 1; + n123 += 1; + n124 += 1; + n125 += 1; + n126 += 1; + n127 += 1; + n128 += 1; + n129 += 1; + n130 += 1; + n131 += 1; + n132 += 1; + n133 += 1; + n134 += 1; + n135 += 1; + n136 += 1; + n137 += 1; + n138 += 1; + n139 += 1; + n140 += 1; + n141 += 1; + n142 += 1; + n143 += 1; + n144 += 1; + n145 += 1; + n146 += 1; + n147 += 1; + n148 += 1; + n149 += 1; + n150 += 1; + n151 += 1; + n152 += 1; + n153 += 1; + n154 += 1; + n155 += 1; + n156 += 1; + n157 += 1; + n158 += 1; + n159 += 1; + n160 += 1; + n161 += 1; + n162 += 1; + n163 += 1; + n164 += 1; + n165 += 1; + n166 += 1; + n167 += 1; + n168 += 1; + n169 += 1; + n170 += 1; + n171 += 1; + n172 += 1; + n173 += 1; + n174 += 1; + n175 += 1; + n176 += 1; + n177 += 1; + n178 += 1; + n179 += 1; + n180 += 1; + n181 += 1; + n182 += 1; + n183 += 1; + n184 += 1; + n185 += 1; + n186 += 1; + n187 += 1; + n188 += 1; + n189 += 1; + n190 += 1; + n191 += 1; + n192 += 1; + n193 += 1; + n194 += 1; + n195 += 1; + n196 += 1; + n197 += 1; + n198 += 1; + n199 += 1; + n200 += 1; + n201 += 1; + n202 += 1; + n203 += 1; + n204 += 1; + n205 += 1; + n206 += 1; + n207 += 1; + n208 += 1; + n209 += 1; + n210 += 1; + n211 += 1; + n212 += 1; + n213 += 1; + n214 += 1; + n215 += 1; + n216 += 1; + n217 += 1; + n218 += 1; + n219 += 1; + n220 += 1; + n221 += 1; + n222 += 1; + n223 += 1; + n224 += 1; + n225 += 1; + n226 += 1; + n227 += 1; + n228 += 1; + n229 += 1; + n230 += 1; + n231 += 1; + n232 += 1; + n233 += 1; + n234 += 1; + n235 += 1; + n236 += 1; + n237 += 1; + n238 += 1; + n239 += 1; + n240 += 1; + n241 += 1; + n242 += 1; + n243 += 1; + n244 += 1; + n245 += 1; + n246 += 1; + n247 += 1; + n248 += 1; + n249 += 1; + n250 += 1; + n251 += 1; + n252 += 1; + n253 += 1; + n254 += 1; + n255 += 1; + n256 += 1; + n257 += 1; + n258 += 1; + n259 += 1; + n260 += 1; + n261 += 1; + n262 += 1; + n263 += 1; + n264 += 1; + n265 += 1; + n266 += 1; + n267 += 1; + n268 += 1; + n269 += 1; + n270 += 1; + n271 += 1; + n272 += 1; + n273 += 1; + n274 += 1; + n275 += 1; + n276 += 1; + n277 += 1; + n278 += 1; + n279 += 1; + n280 += 1; + n281 += 1; + n282 += 1; + n283 += 1; + n284 += 1; + n285 += 1; + n286 += 1; + n287 += 1; + n288 += 1; + n289 += 1; + n290 += 1; + n291 += 1; + n292 += 1; + n293 += 1; + n294 += 1; + n295 += 1; + n296 += 1; + n297 += 1; + n298 += 1; + n299 += 1; + n300 += 1; + n301 += 1; + n302 += 1; + n303 += 1; + n304 += 1; + n305 += 1; + n306 += 1; + n307 += 1; + n308 += 1; + n309 += 1; + n310 += 1; + n311 += 1; + n312 += 1; + n313 += 1; + n314 += 1; + n315 += 1; + n316 += 1; + n317 += 1; + n318 += 1; + n319 += 1; + n320 += 1; + n321 += 1; + n322 += 1; + n323 += 1; + n324 += 1; + n325 += 1; + n326 += 1; + n327 += 1; + n328 += 1; + n329 += 1; + n330 += 1; + n331 += 1; + n332 += 1; + n333 += 1; + n334 += 1; + n335 += 1; + n336 += 1; + n337 += 1; + n338 += 1; + n339 += 1; + n340 += 1; + n341 += 1; + n342 += 1; + n343 += 1; + n344 += 1; + n345 += 1; + n346 += 1; + n347 += 1; + n348 += 1; + n349 += 1; + n350 += 1; + n351 += 1; + n352 += 1; + n353 += 1; + n354 += 1; + n355 += 1; + n356 += 1; + n357 += 1; + n358 += 1; + n359 += 1; + n360 += 1; + n361 += 1; + n362 += 1; + n363 += 1; + n364 += 1; + n365 += 1; + n366 += 1; + n367 += 1; + n368 += 1; + n369 += 1; + n370 += 1; + n371 += 1; + n372 += 1; + n373 += 1; + n374 += 1; + n375 += 1; + n376 += 1; + n377 += 1; + n378 += 1; + n379 += 1; + n380 += 1; + n381 += 1; + n382 += 1; + n383 += 1; + n384 += 1; + n385 += 1; + n386 += 1; + n387 += 1; + n388 += 1; + n389 += 1; + n390 += 1; + n391 += 1; + n392 += 1; + n393 += 1; + n394 += 1; + n395 += 1; + n396 += 1; + n397 += 1; + n398 += 1; + n399 += 1; + n400 += 1; + n401 += 1; + n402 += 1; + n403 += 1; + n404 += 1; + n405 += 1; + n406 += 1; + n407 += 1; + n408 += 1; + n409 += 1; + n410 += 1; + n411 += 1; + n412 += 1; + n413 += 1; + n414 += 1; + n415 += 1; + n416 += 1; + n417 += 1; + n418 += 1; + n419 += 1; + n420 += 1; + n421 += 1; + n422 += 1; + n423 += 1; + n424 += 1; + n425 += 1; + n426 += 1; + n427 += 1; + n428 += 1; + n429 += 1; + n430 += 1; + n431 += 1; + n432 += 1; + n433 += 1; + n434 += 1; + n435 += 1; + n436 += 1; + n437 += 1; + n438 += 1; + n439 += 1; + n440 += 1; + n441 += 1; + n442 += 1; + n443 += 1; + n444 += 1; + n445 += 1; + n446 += 1; + n447 += 1; + n448 += 1; + n449 += 1; + n450 += 1; + n451 += 1; + n452 += 1; + n453 += 1; + n454 += 1; + n455 += 1; + n456 += 1; + n457 += 1; + n458 += 1; + n459 += 1; + n460 += 1; + n461 += 1; + n462 += 1; + n463 += 1; + n464 += 1; + n465 += 1; + n466 += 1; + n467 += 1; + n468 += 1; + n469 += 1; + n470 += 1; + n471 += 1; + n472 += 1; + n473 += 1; + n474 += 1; + n475 += 1; + n476 += 1; + n477 += 1; + n478 += 1; + n479 += 1; + n480 += 1; + n481 += 1; + n482 += 1; + n483 += 1; + n484 += 1; + n485 += 1; + n486 += 1; + n487 += 1; + n488 += 1; + n489 += 1; + n490 += 1; + n491 += 1; + n492 += 1; + n493 += 1; + n494 += 1; + n495 += 1; + n496 += 1; + n497 += 1; + n498 += 1; + n499 += 1; + n500 += 1; + n501 += 1; + n502 += 1; + n503 += 1; + n504 += 1; + n505 += 1; + n506 += 1; + n507 += 1; + n508 += 1; + n509 += 1; + n510 += 1; + n511 += 1; + n512 += 1; + n513 += 1; + n514 += 1; + n515 += 1; + n516 += 1; + n517 += 1; + n518 += 1; + n519 += 1; + n520 += 1; + n521 += 1; + n522 += 1; + n523 += 1; + n524 += 1; + n525 += 1; + n526 += 1; + n527 += 1; + n528 += 1; + n529 += 1; + n530 += 1; + n531 += 1; + n532 += 1; + n533 += 1; + n534 += 1; + n535 += 1; + n536 += 1; + n537 += 1; + n538 += 1; + n539 += 1; + o540 = { x: o540.x + 1 }; + s541 += "!"; + o542 = { x: o542.x + 1 }; + s543 += "!"; + o544 = { x: o544.x + 1 }; + s545 += "!"; + o546 = { x: o546.x + 1 }; + s547 += "!"; + o548 = { x: o548.x + 1 }; + s549 += "!"; + o550 = { x: o550.x + 1 }; + s551 += "!"; + o552 = { x: o552.x + 1 }; + s553 += "!"; + o554 = { x: o554.x + 1 }; + s555 += "!"; + o556 = { x: o556.x + 1 }; + s557 += "!"; + o558 = { x: o558.x + 1 }; + s559 += "!"; + o560 = { x: o560.x + 1 }; + s561 += "!"; + o562 = { x: o562.x + 1 }; + s563 += "!"; + o564 = { x: o564.x + 1 }; + s565 += "!"; + o566 = { x: o566.x + 1 }; + s567 += "!"; + o568 = { x: o568.x + 1 }; + s569 += "!"; + o570 = { x: o570.x + 1 }; + s571 += "!"; + o572 = { x: o572.x + 1 }; + s573 += "!"; + o574 = { x: o574.x + 1 }; + s575 += "!"; + o576 = { x: o576.x + 1 }; + s577 += "!"; + o578 = { x: o578.x + 1 }; + s579 += "!"; + o580 = { x: o580.x + 1 }; + s581 += "!"; + o582 = { x: o582.x + 1 }; + s583 += "!"; + o584 = { x: o584.x + 1 }; + s585 += "!"; + o586 = { x: o586.x + 1 }; + s587 += "!"; + o588 = { x: o588.x + 1 }; + s589 += "!"; + o590 = { x: o590.x + 1 }; + s591 += "!"; + o592 = { x: o592.x + 1 }; + s593 += "!"; + o594 = { x: o594.x + 1 }; + s595 += "!"; + o596 = { x: o596.x + 1 }; + s597 += "!"; + o598 = { x: o598.x + 1 }; + s599 += "!"; + }, + readers, + nested(): () => string { + let calls = 0; + return () => { + calls++; + n0 += 10; + return calls + "/" + n0 + "/" + s599; + }; + }, + }; +} +const a = make(0); +const b = make(100); +console.log(a.before); +console.log(a.sum(), b.sum(), a.early()); +a.bump(); +a.bump(); +console.log(a.sum(), b.sum(), a.early()); +console.log(a.readers.map((r) => r()).join(" ")); +const next = a.nested(); +console.log(next(), next(), a.sum()); +b.bump(); +console.log(b.readers.map((r) => r()).join(" "), b.early()); diff --git a/tests/corpus/5974-typeof-module-exports.cjs b/tests/corpus/5974-typeof-module-exports.cjs new file mode 100644 index 0000000000..7e209f1475 --- /dev/null +++ b/tests/corpus/5974-typeof-module-exports.cjs @@ -0,0 +1,9 @@ +// typeof on functions and classes exported by Node builtin modules, read +// through require bindings: Node answers "function" for each. +const zlib = require("node:zlib"); +const path = require("node:path"); +const fs = require("node:fs"); +console.log(typeof zlib.createGzip, typeof zlib.createBrotliDecompress, typeof zlib.gunzipSync); +console.log(typeof zlib.Inflate, typeof zlib.Gzip, typeof zlib.DeflateRaw); +console.log(typeof path.join, typeof path.posix.join, typeof fs.readFileSync); +console.log(typeof path.sep); diff --git a/tests/corpus/5990-worker-uncloneable.mjs b/tests/corpus/5990-worker-uncloneable.mjs new file mode 100644 index 0000000000..5960a18bba --- /dev/null +++ b/tests/corpus/5990-worker-uncloneable.mjs @@ -0,0 +1,59 @@ +// markAsUncloneable: structuredClone and postMessage throw DataCloneError +// for a marked object, error or class instance, also nested in an object; +// the mark has no effect on primitives, arrays and typed arrays. +import { markAsUncloneable, MessageChannel, isInternalThread } from "node:worker_threads"; + +console.log("isInternalThread", isInternalThread); +const { port1, port2 } = new MessageChannel(); +port2.on("message", (m) => console.log("received", JSON.stringify(m))); + +class Point { + constructor(x) { + this.x = x; + } +} +const cases = { + object: { a: 1 }, + error: new Error("e"), + instance: new Point(1), + array: [1, 2], + bytes: new Uint8Array(2), +}; +for (const [name, value] of Object.entries(cases)) { + markAsUncloneable(value); + let cloned; + let posted; + try { + structuredClone(value); + cloned = "cloned"; + } catch (e) { + cloned = `${e.name} ${e.message} ${e.code}`; + } + try { + port1.postMessage(value); + posted = "posted"; + } catch (e) { + posted = `${e.name} ${e.message}`; + } + console.log(name, "|", cloned, "|", posted); +} +markAsUncloneable(5); +markAsUncloneable("text"); +markAsUncloneable(undefined); +console.log("primitives", structuredClone(5), structuredClone("text")); +const marked = { inner: true }; +markAsUncloneable(marked); +markAsUncloneable(marked); +try { + structuredClone({ nested: marked }); +} catch (e) { + console.log("nested", e.name, e.message); +} +try { + port1.postMessage({ nested: marked }); +} catch (e) { + console.log("nested post", e.name, e.message); +} +const copy = { ...marked }; +console.log("spread copy", JSON.stringify(structuredClone(copy))); +setTimeout(() => port1.close(), 50); diff --git a/tests/corpus/5991-worker-async-dispose.mjs b/tests/corpus/5991-worker-async-dispose.mjs new file mode 100644 index 0000000000..94ee22036d --- /dev/null +++ b/tests/corpus/5991-worker-async-dispose.mjs @@ -0,0 +1,25 @@ +// worker[Symbol.asyncDispose]() terminates the worker and resolves with +// undefined after its 'exit' event; `await using` disposes a Worker at the +// end of its block. +import { Worker, isMainThread, parentPort } from "node:worker_threads"; + +if (isMainThread) { + const worker = new Worker(new URL(import.meta.url)); + const dispose = worker[Symbol.asyncDispose]; + console.log(typeof dispose, dispose.name, dispose.length); + worker.on("exit", (code) => console.log("exit", code)); + await new Promise((resolve) => worker.once("message", resolve)); + const result = await worker[Symbol.asyncDispose](); + console.log("disposed", result); + console.log("again", await worker[Symbol.asyncDispose]()); + { + await using scoped = new Worker(new URL(import.meta.url)); + scoped.on("exit", (code) => console.log("scoped exit", code)); + await new Promise((resolve) => scoped.once("message", resolve)); + console.log("leaving block"); + } + console.log("after block"); +} else { + parentPort.postMessage("ready"); + setInterval(() => {}, 1000); +} diff --git a/tests/corpus/5992-worker-thread-name-lifetime.mjs b/tests/corpus/5992-worker-thread-name-lifetime.mjs new file mode 100644 index 0000000000..455ccec4ba --- /dev/null +++ b/tests/corpus/5992-worker-thread-name-lifetime.mjs @@ -0,0 +1,33 @@ +// worker.threadName read while the worker runs stays a usable string after +// the worker exits and the Worker object is released. +import { Worker, isMainThread, parentPort, threadName } from "node:worker_threads"; + +async function run(name) { + const worker = new Worker(new URL(import.meta.url), { name }); + const kept = worker.threadName; + await new Promise((resolve) => worker.once("exit", resolve)); + return kept; +} + +if (isMainThread) { + const fromFunction = await run("function worker"); + const names = []; + for (const name of ["first worker", " second "]) { + let worker = new Worker(new URL(import.meta.url), { name }); + names.push(worker.threadName); + const exited = new Promise((resolve) => worker.once("exit", resolve)); + const inner = await new Promise((resolve) => worker.once("message", resolve)); + await exited; + console.log("after exit", JSON.stringify(worker.threadName), JSON.stringify(inner)); + worker = null; + } + // allocations that would reuse the released names' storage + const filler = []; + for (let i = 0; i < 1000; i++) filler.push("x".repeat(i % 64)); + console.log("kept", filler.length, JSON.stringify(names), names.map((n) => n.length).join(",")); + console.log("joined", names.join("|").toUpperCase()); + console.log("from function", JSON.stringify(fromFunction), fromFunction.length, fromFunction.toUpperCase()); +} else { + const own = threadName; + parentPort.postMessage(`${own}:${own.length}`); +} diff --git a/tests/corpus/6020-class-extends-function-constructor.js b/tests/corpus/6020-class-extends-function-constructor.js new file mode 100644 index 0000000000..d98d22a551 --- /dev/null +++ b/tests/corpus/6020-class-extends-function-constructor.js @@ -0,0 +1,27 @@ +// Classes whose base is an ordinary function constructor, with a declared +// constructor calling super and with the implicit derived constructor. +function Shape(name, sides) { + this.name = name; + this.sides = sides; +} +Shape.prototype.describe = function () { + return `${this.name} with ${this.sides} sides`; +}; + +class Polygon extends Shape { + constructor(name, sides, length) { + super(name, sides); + this.length = length; + } + perimeter() { + return this.sides * this.length; + } +} + +class Unnamed extends Shape {} + +const square = new Polygon("square", 4, 3); +console.log(square.describe(), square.perimeter()); +console.log(square instanceof Polygon); +const shape = new Unnamed("triangle", 3); +console.log(shape.describe(), Object.keys(shape).join(",")); diff --git a/tests/corpus/6140-cjs-require-tdz-throws/lib.js b/tests/corpus/6140-cjs-require-tdz-throws/lib.js new file mode 100644 index 0000000000..2a9669e123 --- /dev/null +++ b/tests/corpus/6140-cjs-require-tdz-throws/lib.js @@ -0,0 +1,8 @@ +'use strict'; + +/** @param {string} who */ +function greet(who) { + return `hello ${who}`; +} + +module.exports = { greet }; diff --git a/tests/corpus/6140-cjs-require-tdz-throws/main.js b/tests/corpus/6140-cjs-require-tdz-throws/main.js new file mode 100644 index 0000000000..13163137e2 --- /dev/null +++ b/tests/corpus/6140-cjs-require-tdz-throws/main.js @@ -0,0 +1,15 @@ +// @exit: 1 +// A hoisted function called above the require reads the binding before +// the require ran: Node throws "Cannot access 'greet' before +// initialization" there, uncaught, and nothing after it runs. +'use strict'; + +console.log('start'); +console.log('boot', helper()); +const { greet } = require('./lib.js'); + +function helper() { + return greet('early'); +} + +console.log('unreachable', helper()); diff --git a/tests/corpus/6140-cjs-require-tdz-throws/package.json b/tests/corpus/6140-cjs-require-tdz-throws/package.json new file mode 100644 index 0000000000..a3c15a7a63 --- /dev/null +++ b/tests/corpus/6140-cjs-require-tdz-throws/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/corpus/6141-cjs-require-tdz-checked/counter.js b/tests/corpus/6141-cjs-require-tdz-checked/counter.js new file mode 100644 index 0000000000..24ef13e5de --- /dev/null +++ b/tests/corpus/6141-cjs-require-tdz-checked/counter.js @@ -0,0 +1,14 @@ +'use strict'; + +class Counter { + /** @param {number} start */ + constructor(start) { + this.value = start; + } + next() { + this.value += 1; + return this.value; + } +} + +module.exports = Counter; diff --git a/tests/corpus/6141-cjs-require-tdz-checked/lib.js b/tests/corpus/6141-cjs-require-tdz-checked/lib.js new file mode 100644 index 0000000000..aa15c97d6a --- /dev/null +++ b/tests/corpus/6141-cjs-require-tdz-checked/lib.js @@ -0,0 +1,8 @@ +'use strict'; + +/** @param {string} who */ +function greet(who) { + return `hello ${who}`; +} + +module.exports = { greet, answer: 42 }; diff --git a/tests/corpus/6141-cjs-require-tdz-checked/main.js b/tests/corpus/6141-cjs-require-tdz-checked/main.js new file mode 100644 index 0000000000..917eb78743 --- /dev/null +++ b/tests/corpus/6141-cjs-require-tdz-checked/main.js @@ -0,0 +1,49 @@ +// Require bindings that code running before the require can read. Every +// read that happens before the require throws Node's ReferenceError for +// the local binding name, before any argument of the access evaluates; +// the same reads succeed once the require ran. +'use strict'; + +/** + * @param {string} label + * @param {() => unknown} read + */ +function attempt(label, read) { + try { + console.log(label, 'ok', read()); + } catch (e) { + if (!(e instanceof ReferenceError)) throw e; + console.log(label, e.name, e.message); + } +} + +/** @param {string} s */ +function note(s) { + console.log('evaluated', s); + return s; +} + +const later = [() => greet('later'), () => new Counter(5).next()]; + +attempt('call', () => greet('a')); +attempt('call-arguments', () => greet(note('argument'))); +attempt('member', () => lib.greet('b')); +attempt('construct', () => new Counter(1).next()); +attempt('builtin', () => path.join('a', 'b')); +attempt('value', () => answer + 1); +attempt('renamed', () => hello('c')); + +const { greet } = require('./lib.js'); +const lib = require('./lib.js'); +const Counter = require('./counter.js'); +const path = require('path'); +const { answer, greet: hello } = require('./lib.js'); + +attempt('call', () => greet('a')); +attempt('call-arguments', () => greet(note('argument'))); +attempt('member', () => lib.greet('b')); +attempt('construct', () => new Counter(1).next()); +attempt('builtin', () => path.join('a', 'b')); +attempt('value', () => answer + 1); +attempt('renamed', () => hello('c')); +for (const read of later) console.log('later', read()); diff --git a/tests/corpus/6141-cjs-require-tdz-checked/package.json b/tests/corpus/6141-cjs-require-tdz-checked/package.json new file mode 100644 index 0000000000..a3c15a7a63 --- /dev/null +++ b/tests/corpus/6141-cjs-require-tdz-checked/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/corpus/6142-cjs-require-published-early/bundle.js b/tests/corpus/6142-cjs-require-published-early/bundle.js new file mode 100644 index 0000000000..c123872197 --- /dev/null +++ b/tests/corpus/6142-cjs-require-published-early/bundle.js @@ -0,0 +1,17 @@ +'use strict'; + +// The esbuild CommonJS layout: stored class expressions and the export +// table come before the var requires their methods use. Publishing the +// classes runs none of their methods. +const Provider = class { + /** @param {string} name */ + constructor(name) { + this.name = name; + } + describe() { + return core.label(this.name); + } +}; +const REASON = 'default'; +module.exports = { Provider, REASON }; +var core = require('./core.js'); diff --git a/tests/corpus/6142-cjs-require-published-early/comparator.js b/tests/corpus/6142-cjs-require-published-early/comparator.js new file mode 100644 index 0000000000..2b2ea6146d --- /dev/null +++ b/tests/corpus/6142-cjs-require-published-early/comparator.js @@ -0,0 +1,37 @@ +'use strict'; + +// The semver layout: the class and its export come first, the requires its +// methods use come after. Publishing the class runs none of its methods, so +// no read can happen before the requires ran. +const ANY = Symbol('ANY'); + +class Comparator { + static get ANY() { + return ANY; + } + + static describe() { + return `Comparator(${typeof parseOptions})`; + } + + /** + * @param {string} text + * @param {{ loose?: boolean }} [options] + */ + constructor(text, options) { + this.mode = parseOptions(options); + this.text = text.trim(); + debug('comparator', this.text, this.mode); + } + + /** @param {string} other */ + test(other) { + return path.basename(other) === this.text; + } +} + +module.exports = Comparator; + +const parseOptions = require('./parse-options.js'); +const { debug } = require('./debug.js'); +const path = require('path'); diff --git a/tests/corpus/6142-cjs-require-published-early/core.js b/tests/corpus/6142-cjs-require-published-early/core.js new file mode 100644 index 0000000000..9e3a9a565c --- /dev/null +++ b/tests/corpus/6142-cjs-require-published-early/core.js @@ -0,0 +1,10 @@ +'use strict'; + +console.log('core: init'); + +/** @param {string} name */ +function label(name) { + return `<${name}>`; +} + +module.exports = { label }; diff --git a/tests/corpus/6142-cjs-require-published-early/debug.js b/tests/corpus/6142-cjs-require-published-early/debug.js new file mode 100644 index 0000000000..30451569b1 --- /dev/null +++ b/tests/corpus/6142-cjs-require-published-early/debug.js @@ -0,0 +1,10 @@ +'use strict'; + +console.log('debug: init'); + +/** @param {...unknown} args */ +function debug(...args) { + console.log('debug:', args.join(' ')); +} + +module.exports = { debug }; diff --git a/tests/corpus/6142-cjs-require-published-early/glob.js b/tests/corpus/6142-cjs-require-published-early/glob.js new file mode 100644 index 0000000000..3d2907f005 --- /dev/null +++ b/tests/corpus/6142-cjs-require-published-early/glob.js @@ -0,0 +1,20 @@ +'use strict'; + +// The glob@7 layout with var: export first, then var requires, a runnable +// statement between them, and properties stored on the exported function. +module.exports = glob; +glob.sync = globSync; + +var parseOptions = require('./parse-options.js'); +var mode = parseOptions({ loose: true }); +var helpers = require('./helpers.js'); + +/** @param {string} pattern */ +function glob(pattern) { + return `${mode}:${helpers.normalize(pattern)}`; +} + +/** @param {string} pattern */ +function globSync(pattern) { + return helpers.normalize(pattern).toUpperCase(); +} diff --git a/tests/corpus/6142-cjs-require-published-early/helpers.js b/tests/corpus/6142-cjs-require-published-early/helpers.js new file mode 100644 index 0000000000..206ca77851 --- /dev/null +++ b/tests/corpus/6142-cjs-require-published-early/helpers.js @@ -0,0 +1,10 @@ +'use strict'; + +console.log('helpers: init'); + +/** @param {string} s */ +function normalize(s) { + return s.replace(/\/+/g, '/'); +} + +module.exports = { normalize }; diff --git a/tests/corpus/6142-cjs-require-published-early/main.js b/tests/corpus/6142-cjs-require-published-early/main.js new file mode 100644 index 0000000000..310dacdb01 --- /dev/null +++ b/tests/corpus/6142-cjs-require-published-early/main.js @@ -0,0 +1,15 @@ +// CommonJS modules that export first and require afterwards: code that can +// reach the bindings only runs after the module finished, so they compile +// without a fence and behave exactly as under Node. +'use strict'; + +console.log('main: start'); +const Comparator = require('./comparator.js'); +const glob = require('./glob.js'); +const { Provider, REASON } = require('./bundle.js'); + +const c = new Comparator(' a.txt ', { loose: true }); +console.log(c.mode, c.text, c.test('/x/a.txt'), Comparator.ANY.toString()); +console.log(new Comparator('b').mode, Comparator.describe()); +console.log(glob('a//b'), glob.sync('c//d')); +console.log(new Provider('custom').describe(), REASON); diff --git a/tests/corpus/6142-cjs-require-published-early/package.json b/tests/corpus/6142-cjs-require-published-early/package.json new file mode 100644 index 0000000000..a3c15a7a63 --- /dev/null +++ b/tests/corpus/6142-cjs-require-published-early/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/corpus/6142-cjs-require-published-early/parse-options.js b/tests/corpus/6142-cjs-require-published-early/parse-options.js new file mode 100644 index 0000000000..0872049ea3 --- /dev/null +++ b/tests/corpus/6142-cjs-require-published-early/parse-options.js @@ -0,0 +1,8 @@ +'use strict'; + +/** @param {{ loose?: boolean } | undefined} options */ +function parseOptions(options) { + return options && options.loose ? 'loose' : 'strict'; +} + +module.exports = parseOptions; diff --git a/tests/corpus/6143-cjs-require-json/counter.js b/tests/corpus/6143-cjs-require-json/counter.js new file mode 100644 index 0000000000..b794d1d4c8 --- /dev/null +++ b/tests/corpus/6143-cjs-require-json/counter.js @@ -0,0 +1,14 @@ +'use strict'; + +// A second requirer: it receives the same cached object and mutates it. +const data = require('./data.json'); + +console.log('counter: sees', data.count); +data.count += 1; + +function bump() { + data.count += 100; + data.tags.push('bumped'); +} + +module.exports = { bump }; diff --git a/tests/corpus/6143-cjs-require-json/data.json b/tests/corpus/6143-cjs-require-json/data.json new file mode 100644 index 0000000000..b1cf3d3bc9 --- /dev/null +++ b/tests/corpus/6143-cjs-require-json/data.json @@ -0,0 +1,7 @@ +{ + "name": "fixture-data", + "version": "1.2.3", + "count": 41, + "tags": ["native", "json"], + "nested": { "flag": true, "ratio": 0.5 } +} diff --git a/tests/corpus/6143-cjs-require-json/list.json b/tests/corpus/6143-cjs-require-json/list.json new file mode 100644 index 0000000000..98ace19c09 --- /dev/null +++ b/tests/corpus/6143-cjs-require-json/list.json @@ -0,0 +1 @@ +[3, 1, 2] diff --git a/tests/corpus/6143-cjs-require-json/main.js b/tests/corpus/6143-cjs-require-json/main.js new file mode 100644 index 0000000000..b67c39f3eb --- /dev/null +++ b/tests/corpus/6143-cjs-require-json/main.js @@ -0,0 +1,27 @@ +// require() of a JSON document: Node parses it at the first require and +// caches the object, so every require of the document, from any module, +// returns the same object and a mutation is visible to all requirers. +// Destructuring copies the properties at the require. +'use strict'; + +const data = require('./data.json'); +const { count, nested: { flag, ratio }, tags: [firstTag] } = require('./data.json'); +const version = require('./data.json').version; + +console.log(data.name, version, data.count, count, flag, ratio, firstTag); +console.log('same object', require('./data.json') === data); + +const { bump } = require('./counter.js'); +console.log('after counter', data.count, count); +bump(); +console.log('after bump', data.count, data.tags.join(','), count); + +function load() { + return require('./data.json'); +} +console.log('lazy same object', load() === data, load().count); +load().nested.flag = false; +console.log('flag', data.nested.flag, flag); + +const list = require('./list.json'); +console.log(list.length, list.slice().sort().join(','), require('./list.json') === list); diff --git a/tests/corpus/6143-cjs-require-json/package.json b/tests/corpus/6143-cjs-require-json/package.json new file mode 100644 index 0000000000..a3c15a7a63 --- /dev/null +++ b/tests/corpus/6143-cjs-require-json/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/corpus/6144-cjs-require-builtin-nested/main.js b/tests/corpus/6144-cjs-require-builtin-nested/main.js new file mode 100644 index 0000000000..3058767623 --- /dev/null +++ b/tests/corpus/6144-cjs-require-builtin-nested/main.js @@ -0,0 +1,15 @@ +// Nested destructuring of a builtin require (undici's `const { types: { +// isPromise } } = require('util')`): util.types IS the util/types module, +// so the nested pattern binds that module's members. +'use strict'; + +const { + types: { isPromise, isPromise: promiseCheck }, + format, +} = require('util'); +const { promises: { readFile } } = require('fs'); +const { posix: { join } } = require('path'); + +console.log(isPromise(Promise.resolve(1)), isPromise({}), promiseCheck(Promise.reject(new Error('x')).catch(() => {}))); +console.log(format('%s=%d', 'a', 1), join('a', 'b', '..', 'c')); +readFile(__filename, 'utf8').then((text) => console.log('read', text.includes('isPromise'))); diff --git a/tests/corpus/6144-cjs-require-builtin-nested/package.json b/tests/corpus/6144-cjs-require-builtin-nested/package.json new file mode 100644 index 0000000000..a3c15a7a63 --- /dev/null +++ b/tests/corpus/6144-cjs-require-builtin-nested/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/corpus/6145-cjs-require-cycle/comparator.js b/tests/corpus/6145-cjs-require-cycle/comparator.js new file mode 100644 index 0000000000..1f0d4eb9d2 --- /dev/null +++ b/tests/corpus/6145-cjs-require-cycle/comparator.js @@ -0,0 +1,29 @@ +'use strict'; + +// semver's layout: the class and its export come first ("hoisted class for +// cyclic dependency"), the requires — one of them closing the cycle with +// range.js — after it. When range.js requires this module mid-evaluation, +// Node hands it the exports as they stand: already the class. +const ANY = Symbol('ANY'); + +class Comparator { + /** @param {string} value */ + constructor(value) { + this.value = value; + } + + /** @param {string} other */ + within(other) { + return new Range(other).has(this.value); + } + + static get ANY() { + return ANY; + } +} + +module.exports = Comparator; + +const Range = require('./range.js'); +const { trim } = require('./util.js'); +console.log('comparator: loaded', trim(' done ')); diff --git a/tests/corpus/6145-cjs-require-cycle/main.js b/tests/corpus/6145-cjs-require-cycle/main.js new file mode 100644 index 0000000000..90cbc88247 --- /dev/null +++ b/tests/corpus/6145-cjs-require-cycle/main.js @@ -0,0 +1,15 @@ +// A require cycle (comparator.js ↔ range.js) whose modules settle their +// exports before they require anything, and use each other only inside +// methods: the cycle evaluates in Node's order and each binding is the +// other module's class. +'use strict'; + +console.log('main: start'); +const Comparator = require('./comparator.js'); +const Range = require('./range.js'); + +const range = new Range('1.0.0 2.0.0'); +console.log(range.has('2.0.0'), range.has('3.0.0')); +const first = range.first(); +console.log(first instanceof Comparator, first.value, first.within('2.0.0 1.0.0')); +console.log(new Comparator('9').within('1 2'), String(Comparator.ANY)); diff --git a/tests/corpus/6145-cjs-require-cycle/package.json b/tests/corpus/6145-cjs-require-cycle/package.json new file mode 100644 index 0000000000..a3c15a7a63 --- /dev/null +++ b/tests/corpus/6145-cjs-require-cycle/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/corpus/6145-cjs-require-cycle/range.js b/tests/corpus/6145-cjs-require-cycle/range.js new file mode 100644 index 0000000000..ab71e4a52d --- /dev/null +++ b/tests/corpus/6145-cjs-require-cycle/range.js @@ -0,0 +1,24 @@ +'use strict'; + +const SEPARATOR = /\s+/; + +class Range { + /** @param {string} spec */ + constructor(spec) { + this.parts = spec.split(SEPARATOR).map((part) => new Comparator(part)); + } + + /** @param {string} value */ + has(value) { + return this.parts.some((part) => part.value === value); + } + + first() { + return this.parts[0]; + } +} + +module.exports = Range; + +const Comparator = require('./comparator.js'); +console.log('range: loaded'); diff --git a/tests/corpus/6145-cjs-require-cycle/util.js b/tests/corpus/6145-cjs-require-cycle/util.js new file mode 100644 index 0000000000..4e13760cd4 --- /dev/null +++ b/tests/corpus/6145-cjs-require-cycle/util.js @@ -0,0 +1,10 @@ +'use strict'; + +console.log('util: loaded'); + +/** @param {string} s */ +function trim(s) { + return s.trim(); +} + +module.exports = { trim }; diff --git a/tests/corpus/native-worker-discarded-heaps.ts b/tests/corpus/native-worker-discarded-heaps.ts new file mode 100644 index 0000000000..bf7a764493 --- /dev/null +++ b/tests/corpus/native-worker-discarded-heaps.ts @@ -0,0 +1,102 @@ +import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads"; +import { publish } from "@scriptc/threads"; +import type { ProgramLifetime } from "@scriptc/types"; + +// Workers whose memory goes back when they exit, and workers whose memory +// stays because other threads hold what they allocated. Builder workers +// create ProgramLifetime instances (which scriptc never frees), ordinary +// objects, strings and large typed arrays, post a checksum and exit. A +// publisher worker posts published graphs (a class graph and a plain +// object) and exits. The main thread reads those graphs again after each +// wave of builders, which in scriptc on Linux reuse the memory of the +// builders before them. + +class Node implements ProgramLifetime { + readonly kind: number; + readonly label: string; + readonly children: Node[] = []; + parent: Node | undefined = undefined; + constructor(kind: number, label: string) { + this.kind = kind; + this.label = label; + } +} + +type Summary = { readonly name: string; readonly values: readonly number[]; readonly nested: { readonly total: number } }; + +type Job = { readonly role: string; readonly seed: number }; + +function build(seed: number, count: number): Node { + const root = new Node(seed, "root" + seed); + let parent = root; + for (let i = 0; i < count; i++) { + const node = new Node((i * 7 + seed) % 31, "n" + (i % 101)); + node.parent = parent; + parent.children.push(node); + if (i % 64 === 63) parent = node; + } + return root; +} + +function checksum(root: Node): number { + let sum = 0; + const stack: Node[] = [root]; + while (stack.length > 0) { + const node = stack.pop()!; + sum = (sum * 31 + node.kind + node.label.length + node.children.length) % 1_000_003; + for (const child of node.children) stack.push(child); + } + return sum; +} + +function startWorker(job: Job, onMessage: (value: unknown) => void): Promise { + return new Promise((resolve, reject) => { + const worker = new Worker(new URL(import.meta.url), { workerData: job }); + worker.on("message", onMessage); + worker.once("messageerror", reject); + worker.once("error", reject); + worker.once("exit", () => resolve()); + }); +} + +if (isMainThread) { + let tree: Node | undefined = undefined; + let summary: Summary | undefined = undefined; + // The publisher posts the tree, then the summary. + await startWorker({ role: "publisher", seed: 3 }, (value) => { + if (tree === undefined) tree = value as Node; + else summary = value as Summary; + }); + const describe = (): string => + `tree ${checksum(tree!)} summary ${summary!.name} ${summary!.values.join(",")} ${summary!.nested.total}`; + console.log("published", describe()); + for (let wave = 0; wave < 3; wave++) { + const sums: number[] = []; + await Promise.all( + [1, 2, 3, 4].map((k) => startWorker({ role: "builder", seed: wave * 10 + k }, (value) => sums.push(value as number))), + ); + sums.sort((a, b) => a - b); + console.log("wave", wave, sums.join(" "), describe()); + } +} else { + const job = workerData as Job; + if (job.role === "publisher") { + parentPort!.postMessage(publish(build(job.seed, 20_000))); + const values: number[] = []; + for (let i = 0; i < 50; i++) values.push(i * job.seed); + parentPort!.postMessage(publish({ name: "summary" + job.seed, values, nested: { total: values.length } })); + } else { + const root = build(job.seed, 150_000); + const samples: Float64Array[] = []; + for (let i = 0; i < 40; i++) { + const sample = new Float64Array(8192); + for (let j = 0; j < sample.length; j += 97) sample[j] = i + j + job.seed; + samples.push(sample); + } + let extra = 0; + for (const sample of samples) extra = (extra + sample[97]! + sample[8148]!) % 1_000_003; + const words: string[] = []; + for (let i = 0; i < 20_000; i++) words.push(`word-${i % 977}-${job.seed}`); + parentPort!.postMessage((checksum(root) + extra + words.join("").length) % 1_000_003); + } +} diff --git a/tests/diagnostics/cjs-require-cycle/main.js b/tests/diagnostics/cjs-require-cycle/main.js index 619117b910..376aa0a363 100644 --- a/tests/diagnostics/cjs-require-cycle/main.js +++ b/tests/diagnostics/cjs-require-cycle/main.js @@ -1,6 +1,8 @@ -// A require cycle: Node hands B the PARTIAL exports of A mid-init, a +// A require cycle whose module exports only after requiring into the +// cycle: Node hands B the PARTIAL exports of A mid-init, a // partially-initialized-module observation the export-global model cannot -// represent — cycles keep the named SC1016 fence. +// represent — the cycle keeps the named SC1016 fence. (A cycle whose +// modules export first compiles: tests/corpus/6145-cjs-require-cycle.) 'use strict'; console.log('main: start'); diff --git a/tests/diagnostics/cjs-require-tdz-unchecked/lib.js b/tests/diagnostics/cjs-require-tdz-unchecked/lib.js new file mode 100644 index 0000000000..2a9669e123 --- /dev/null +++ b/tests/diagnostics/cjs-require-tdz-unchecked/lib.js @@ -0,0 +1,8 @@ +'use strict'; + +/** @param {string} who */ +function greet(who) { + return `hello ${who}`; +} + +module.exports = { greet }; diff --git a/tests/diagnostics/cjs-require-tdz-unchecked/main.js b/tests/diagnostics/cjs-require-tdz-unchecked/main.js new file mode 100644 index 0000000000..52b12fcb47 --- /dev/null +++ b/tests/diagnostics/cjs-require-tdz-unchecked/main.js @@ -0,0 +1,8 @@ +// A const require binding read by a statement above the require that +// lowers to no expression: publishing the binding as an export is module +// plumbing, so no initialization check can run where Node throws +// "Cannot access 'greet' before initialization". The read keeps SC1013. +'use strict'; + +module.exports = { greet }; +const { greet } = require('./lib.js'); diff --git a/tests/diagnostics/cjs-require-tdz-unchecked/package.json b/tests/diagnostics/cjs-require-tdz-unchecked/package.json new file mode 100644 index 0000000000..5bbefffbab --- /dev/null +++ b/tests/diagnostics/cjs-require-tdz-unchecked/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/tests/diagnostics/cjs-require-tdz/main.js b/tests/diagnostics/cjs-require-tdz/main.js index a571b45242..c2b5a945ce 100644 --- a/tests/diagnostics/cjs-require-tdz/main.js +++ b/tests/diagnostics/cjs-require-tdz/main.js @@ -1,14 +1,15 @@ -// A require whose binding IS reachable from code above it: Node would -// throw "Cannot access 'greet' before initialization" at the helper() -// call (the const initializes AT the require), an ordering the compiled -// aliasing does not model — the require keeps a named SC1013 fence. +// A var require binding that code above the require can reach: Node reads +// undefined there (the var is hoisted, the require has not run), a value +// the compiled module alias cannot hold — the require keeps a named SC1013 +// fence. A const or let binding compiles with an initialization check +// instead (tests/corpus/6141-cjs-require-tdz-checked). 'use strict'; console.log('boot', helper()); -const { greet } = require('./lib.js'); +var lib = require('./lib.js'); function helper() { - return greet('early'); + return lib.greet('early'); } console.log(helper()); diff --git a/tests/diagnostics/dataview.ts b/tests/diagnostics/dataview.ts index 01255b47c3..c201932790 100644 --- a/tests/diagnostics/dataview.ts +++ b/tests/diagnostics/dataview.ts @@ -1,6 +1,8 @@ -// Stored ArrayBuffers and DataViews lower; Float16 accessors remain unsupported. +// Stored ArrayBuffers and DataViews lower, Float16 accessors included; +// detached accessor methods remain unsupported. const buffer = new ArrayBuffer(16); const view = new DataView(buffer); view.setFloat16(0, 1.5); console.log(view.getFloat16(0)); +const getter = view.getFloat16; // End of the diagnostic fixture. diff --git a/tests/diagnostics/es2025-fences.ts b/tests/diagnostics/es2025-fences.ts index 2df12226f5..067da192e2 100644 --- a/tests/diagnostics/es2025-fences.ts +++ b/tests/diagnostics/es2025-fences.ts @@ -1,9 +1,8 @@ // The es2025-lib surface WITHOUT a lowering fences by name: resizable // ArrayBuffers and transfer, SharedArrayBuffer.grow, Atomics.waitAsync, -// Float16Array and its Math/DataView companions, first-class iterator -// objects, and the unlowered forms of the implemented statics -// (Promise.try with fewer arguments than parameters, groupBy's -// literal-union keys, a set-like non-Set argument). +// first-class iterator objects, and the unlowered forms of the +// implemented statics (Promise.try with fewer arguments than parameters, +// groupBy's literal-union keys, a set-like non-Set argument). // Resizable ArrayBuffer + transfer (ES2024). const ab = new ArrayBuffer(8, { maxByteLength: 16 }); @@ -22,12 +21,6 @@ sab.grow(16); const ia = new Int32Array(4); const waited = Atomics.waitAsync(ia, 0, 0, 1); -// Float16 (ES2025): the typed array, Math.f16round, DataView accessors. -const f16 = new Float16Array(4); -console.log(Math.f16round(1.337)); -const dv = new DataView(new Uint8Array(8).buffer); -dv.setFloat16(0, 1.5); - // First-class iterator objects. const it = Iterator.from([1, 2, 3].values()); diff --git a/tests/diagnostics/mock-timers-fences.ts b/tests/diagnostics/mock-timers-fences.ts new file mode 100644 index 0000000000..8deb95e76d --- /dev/null +++ b/tests/diagnostics/mock-timers-fences.ts @@ -0,0 +1,35 @@ +// mock.timers lowers enable(), enable({ apis, now }), tick, runAll, reset, +// setTime and [Symbol.dispose](), on the module's `mock` and on t.mock, and +// timers/promises scheduler.wait(delay) lowers. The rest is refused, each +// with a named diagnostic: an apis list or options object that is not a +// literal, and the MockTimers object used as a value. +import { mock, test } from "node:test"; +import { scheduler } from "node:timers/promises"; + +mock.timers.enable({ apis: ["setTimeout", "setInterval"], now: 10 }); // lowers +mock.timers.tick(5); // lowers +mock.timers.setTime(20); // lowers +mock.timers.runAll(); // lowers +mock.timers.reset(); // lowers + +mock.timers.enable(); // lowers +mock.timers.enable({ apis: ["Date"] }); // lowers +mock.timers.enable({ apis: ["setTimeout", "setImmediate"] }); // lowers + +const apis: ("setTimeout" | "setInterval")[] = ["setTimeout"]; +mock.timers.enable({ apis }); // apis list that is not a literal + +const options = { apis: ["setTimeout"] as const }; +mock.timers.enable(options); // options object that is not a literal + +const timers = mock.timers; // the object as a value +timers.tick(1); + +test("t.mock.timers", (t) => { + t.mock.timers.enable({ apis: ["setInterval"] }); // lowers + t.mock.timers.tick(); // lowers + t.mock.timers.enable({ apis: ["setTimeout"], now: new Date(0) }); // lowers + t.mock.timers[Symbol.dispose](); // lowers +}); + +void scheduler.wait(1); // lowers diff --git a/tests/diagnostics/regex-constructor-flags.ts b/tests/diagnostics/regex-constructor-flags.ts index 3d388601c3..cbf7de7ee7 100644 --- a/tests/diagnostics/regex-constructor-flags.ts +++ b/tests/diagnostics/regex-constructor-flags.ts @@ -1,4 +1,3 @@ -// new RegExp with literal d or v flags refuses like the regex literal -// forms; computed flags reach the runtime, which names the same gap. -const sets = new RegExp("[\\w--\\d]", "v"); +// new RegExp with a literal d flag refuses like the regex literal form; +// computed flags reach the runtime, which names the same gap. const indices = new RegExp("cat", "dg"); diff --git a/tests/diagnostics/regex.ts b/tests/diagnostics/regex.ts index ec29bc70e5..e2b3fc4ae6 100644 --- a/tests/diagnostics/regex.ts +++ b/tests/diagnostics/regex.ts @@ -1,8 +1,7 @@ -// The d/v flags are outside the native slice; .groups needs a +// The d flag is outside the native slice; .groups needs a // statically-known regex. Named capture groups compile (corpus 2604). // Method values and regex union arms remain unsupported; arrays compile. const indices = /cat/d; -const sets = /[\p{L}]/v; const asValue = /x/.test; const maybe: RegExp | undefined = /a/; function readGroups(re: RegExp): void { diff --git a/tests/diagnostics/snapshot-fences.ts b/tests/diagnostics/snapshot-fences.ts new file mode 100644 index 0000000000..afdf35b707 --- /dev/null +++ b/tests/diagnostics/snapshot-fences.ts @@ -0,0 +1,19 @@ +// t.assert.snapshot and t.assert.fileSnapshot lower with no options and +// with an options object literal whose serializers is an array literal. +// The rest is refused, each with a named diagnostic: options that are not +// an object literal, a serializers list that is not an array literal, and +// a spread serializer. +import { test } from "node:test"; + +test("snapshots", (t) => { + t.assert.snapshot({ a: 1 }); // lowers + t.assert.snapshot(1, { serializers: [(v: number) => `${v}`] }); // lowers + t.assert.fileSnapshot("text", "out.txt"); // lowers + + const options = { serializers: [(v: unknown) => String(v)] }; + t.assert.snapshot(1, options); // options that are not an object literal + + const serializers = [(v: unknown) => String(v)]; + t.assert.snapshot(1, { serializers }); // a serializers list that is not an array literal + t.assert.fileSnapshot(1, "out.txt", { serializers: [...serializers] }); // a spread serializer +}); diff --git a/tests/diagnostics/utility-types.ts b/tests/diagnostics/utility-types.ts index d22a8f9692..c6d10648b4 100644 --- a/tests/diagnostics/utility-types.ts +++ b/tests/diagnostics/utility-types.ts @@ -7,9 +7,9 @@ // differential corpus); what stays rejected: // - symbol-keyed index signatures (no lowering for symbol keys); // - index-signature value types outside the overflow-map slice (Dates and -// other lib API objects, even when the value itself has a representation); -// - utility types over LIB interfaces stay a type world, not data shapes. -// (Dot access/writes to index-signature keys compile now — the overflow +// other lib API objects, even when the value itself has a representation). +// (Readonly bindings compile, holding the Date as a checked value. +// Dot access/writes to index-signature keys compile now — the overflow // path in dot spelling — and self-referential mapped types intern as // named recursive shapes; differential corpus covers both.) const bySymbol: { [s: symbol]: string } = {}; diff --git a/tests/fixtures/node-test/cases/mock-timers-date.ts b/tests/fixtures/node-test/cases/mock-timers-date.ts new file mode 100644 index 0000000000..ca2714e05c --- /dev/null +++ b/tests/fixtures/node-test/cases/mock-timers-date.ts @@ -0,0 +1,197 @@ +// mock.timers for Date, setImmediate, AbortSignal.timeout and +// timers/promises scheduler.wait, enable() without apis, which mocks every +// API, and [Symbol.dispose](): Date.now() and new Date() read the mock +// clock that tick and setTime move, a mocked setImmediate runs before +// timers due at the same tick, a mocked AbortSignal.timeout aborts with +// the AbortError reason, mocking Date twice throws, and once a reset gives +// scheduler.wait back, Node's restored scheduler.wait throws +// ERR_INVALID_THIS, so its suite runs first. +import { describe, it, mock } from "node:test"; +import * as assert from "node:assert"; +import * as timersNamespace from "node:timers"; +import { setImmediate as namedSetImmediate } from "node:timers"; +import { scheduler } from "node:timers/promises"; +import * as timersPromises from "node:timers/promises"; + +type Fail = { name: string; code?: string; message: string }; +const failure = (fn: () => void): string => { + try { + fn(); + return "no throw"; + } catch (error) { + const e = error as Fail; + return `${e.name} ${e.code ?? "-"} ${e.message}`; + } +}; + +describe("scheduler.wait", () => { + it("settles from the mock queue, a microtask after tick", async (t) => { + // scheduler.wait is not in the typed apis list; enable() mocks it. + t.mock.timers.enable(); + const log: string[] = []; + const named = scheduler.wait(50).then(() => { + log.push("named"); + }); + const namespace = timersPromises.scheduler.wait(50).then(() => { + log.push("namespace"); + }); + const omitted = scheduler.wait().then(() => { + log.push("omitted"); + }); + const unref = scheduler.wait(50, { ref: false }).then(() => { + log.push("unref"); + }); + t.mock.timers.tick(50); + log.push("after tick"); + queueMicrotask(() => log.push("microtask")); + await Promise.all([named, namespace, omitted, unref]); + assert.deepStrictEqual(log, [ + "after tick", + "microtask", + "named", + "namespace", + "omitted", + "unref", + ]); + }); + + it("throws once a reset gave it back", () => { + const got = failure(() => { + void scheduler.wait(1); + }); + assert.strictEqual( + got, + 'TypeError ERR_INVALID_THIS Value of "this" must be of type Scheduler', + ); + }); +}); + +describe("Date", () => { + it("reads the mock clock", (t) => { + t.mock.timers.enable({ apis: ["Date"], now: 1000 }); + assert.strictEqual(Date.now(), 1000); + assert.strictEqual(new Date().getTime(), 1000); + assert.strictEqual(new Date(5).getTime(), 5); + assert.strictEqual(new Date() instanceof Date, true); + t.mock.timers.tick(5); + assert.strictEqual(Date.now(), 1005); + t.mock.timers.setTime(50); + assert.strictEqual(new Date().toISOString(), "1970-01-01T00:00:00.050Z"); + }); + + it("is real again after reset", (t) => { + t.mock.timers.enable({ apis: ["Date"] }); + assert.strictEqual(Date.now(), 0); + t.mock.timers.reset(); + assert.strictEqual(Date.now() > 1e12, true); + }); + + it("shares the clock with mocked timers", (t) => { + t.mock.timers.enable({ apis: ["Date", "setTimeout"], now: 100 }); + const seen: number[] = []; + setTimeout(() => seen.push(Date.now()), 25); + t.mock.timers.tick(30); + seen.push(Date.now()); + assert.deepStrictEqual(seen, [130, 130]); + }); + + it("throws when another tracker mocks Date", (t) => { + mock.timers.enable({ apis: ["Date"], now: 7 }); + const got = failure(() => t.mock.timers.enable({ apis: ["Date"] })); + assert.strictEqual(got, "Error ERR_INVALID_STATE Invalid state: Date is already being mocked!"); + assert.strictEqual(failure(() => t.mock.timers.tick(1)).includes("enable MockTimers first"), true); + assert.strictEqual(Date.now(), 7); + mock.timers.reset(); + }); +}); + +describe("setImmediate", () => { + it("runs before timers due at the same tick", (t) => { + t.mock.timers.enable({ apis: ["setImmediate", "setTimeout"] }); + const log: string[] = []; + setTimeout(() => log.push("timeout 0"), 0); + setImmediate(() => log.push("immediate")); + setImmediate((tag: string) => log.push(`immediate ${tag}`), "x"); + const cleared = setImmediate(() => log.push("cleared")); + clearImmediate(cleared); + assert.strictEqual(cleared.hasRef(), true); + assert.strictEqual(cleared.unref().hasRef(), true); + assert.strictEqual(cleared.ref().hasRef(), true); + t.mock.timers.tick(0); + log.push("tick 0"); + assert.deepStrictEqual(log, ["immediate", "immediate x", "timeout 0", "tick 0"]); + }); + + it("an immediate queued by a due timeout runs that timeout again", (t) => { + // Node removes a fired timer only when it is still first in the queue; + // the immediate's run time (now - 1) puts it first. + t.mock.timers.enable({ apis: ["setImmediate", "setTimeout"] }); + const log: string[] = []; + setTimeout(() => { + log.push("timeout"); + if (log.length === 1) setImmediate(() => log.push("immediate")); + }, 0); + t.mock.timers.tick(0); + assert.deepStrictEqual(log, ["timeout", "immediate", "timeout"]); + }); + + it("named and namespace imports keep the loop's immediates", async (t) => { + t.mock.timers.enable({ apis: ["setImmediate"] }); + const log: string[] = []; + await new Promise((resolve) => + namedSetImmediate(() => { + log.push("named"); + resolve(); + }), + ); + await new Promise((resolve) => + timersNamespace.setImmediate(() => { + log.push("namespace"); + resolve(); + }), + ); + assert.deepStrictEqual(log, ["named", "namespace"]); + }); +}); + +describe("enable() without apis", () => { + it("mocks every API", (t) => { + t.mock.timers.enable(); + const log: string[] = []; + assert.strictEqual(Date.now(), 0); + setTimeout(() => log.push(`timeout at ${Date.now()}`), 2); + setImmediate(() => log.push("immediate")); + const beat = setInterval(() => { + log.push(`interval at ${Date.now()}`); + clearInterval(beat); + }, 1); + const signal = AbortSignal.timeout(3); + signal.addEventListener("abort", () => { + const reason = signal.reason as { name: string; message: string }; + log.push(`abort ${reason.name}: ${reason.message}`); + }); + t.mock.timers.tick(3); + assert.strictEqual(signal.aborted, true); + assert.deepStrictEqual(log, [ + "immediate", + "interval at 3", + "timeout at 3", + "abort AbortError: This operation was aborted", + ]); + }); + + it("[Symbol.dispose]() resets", (t) => { + t.mock.timers.enable({ apis: ["setTimeout"] }); + let fired = 0; + setTimeout(() => fired++, 1); + t.mock.timers[Symbol.dispose](); + assert.strictEqual(failure(() => t.mock.timers.tick(1)).includes("enable MockTimers first"), true); + assert.strictEqual(fired, 0); + }); + + it("mocks nothing with an empty apis list", (t) => { + t.mock.timers.enable({ apis: [] }); + assert.strictEqual(Date.now() > 1e12, true); + t.mock.timers.tick(1); + }); +}); diff --git a/tests/fixtures/node-test/cases/mock-timers.ts b/tests/fixtures/node-test/cases/mock-timers.ts new file mode 100644 index 0000000000..1e5e5883f8 --- /dev/null +++ b/tests/fixtures/node-test/cases/mock-timers.ts @@ -0,0 +1,348 @@ +// mock.timers on the module's `mock` and on t.mock, for setTimeout and +// setInterval: enable({ apis, now }), tick, runAll, reset and setTime, the +// order due timers run in (equal, zero, negative, fractional and omitted +// delays), clears, handle methods, Node's errors, and the reset of +// t.mock.timers when its test ends. Node replaces the globals, so calls +// through ESM named and namespace imports of node:timers and +// node:timers/promises keep the loop's timers. +import { describe, it, mock, test } from "node:test"; +import * as assert from "node:assert"; +import * as timersNamespace from "node:timers"; +import { setTimeout as namedSetTimeout } from "node:timers"; +import { setTimeout as sleep } from "node:timers/promises"; + +type Fail = { name: string; code?: string; message: string }; +const failure = (fn: () => void): string => { + try { + fn(); + return "no throw"; + } catch (error) { + const e = error as Fail; + return `${e.name} ${e.code ?? "-"} ${e.message}`; + } +}; +const NOT_ENABLED = + "Error ERR_INVALID_STATE Invalid state: You should enable MockTimers first by calling the .enable function"; +const negative = (n: string) => + `TypeError ERR_INVALID_ARG_VALUE The argument 'time' ${n}. Received 'positive integer'`; + +describe("order", () => { + it("runs due timers by run time, then creation", (t) => { + t.mock.timers.enable({ apis: ["setTimeout"] }); + const log: string[] = []; + setTimeout(() => log.push("omitted")); + setTimeout(() => log.push("zero"), 0); + setTimeout(() => log.push("negative"), -5); + setTimeout(() => log.push("fraction"), 0.5); + setTimeout(() => log.push("ten a"), 10); + setTimeout(() => log.push("five"), 5); + setTimeout(() => log.push("ten b"), 10); + t.mock.timers.tick(0); + log.push("tick 0"); + t.mock.timers.tick(1); + log.push("tick 1"); + t.mock.timers.tick(9); + assert.deepStrictEqual(log, [ + "omitted", + "negative", + "zero", + "tick 0", + "fraction", + "tick 1", + "five", + "ten a", + "ten b", + ]); + }); + + it("runs timers scheduled by a callback once they are due", (t) => { + t.mock.timers.enable({ apis: ["setTimeout"] }); + const log: string[] = []; + setTimeout(() => { + log.push("outer"); + setTimeout(() => log.push("inner 0"), 0); + setTimeout(() => log.push("inner 5"), 5); + }, 10); + t.mock.timers.tick(10); + log.push("after 10"); + t.mock.timers.tick(5); + log.push("after 15"); + t.mock.timers.tick(); + assert.deepStrictEqual(log, ["outer", "inner 0", "after 10", "inner 5", "after 15"]); + }); + + it("passes the trailing arguments", (t) => { + t.mock.timers.enable({ apis: ["setTimeout"] }); + const log: string[] = []; + setTimeout((a: string, b: number) => log.push(`${a} ${b}`), 1, "x", 2); + t.mock.timers.tick(1); + assert.deepStrictEqual(log, ["x 2"]); + }); +}); + +describe("intervals", () => { + it("repeats until cleared", (t) => { + t.mock.timers.enable({ apis: ["setInterval", "setTimeout"] }); + const log: string[] = []; + let n = 0; + const handle = setInterval(() => { + n++; + log.push(`interval ${n}`); + if (n === 3) clearInterval(handle); + }, 10); + setTimeout(() => log.push("timeout 25"), 25); + t.mock.timers.tick(100); + assert.deepStrictEqual(log, ["interval 1", "interval 2", "timeout 25", "interval 3"]); + }); + + it("fires once per elapsed period", (t) => { + t.mock.timers.enable({ apis: ["setInterval"] }); + let n = 0; + const handle = setInterval(() => n++, 3); + t.mock.timers.tick(10); + assert.strictEqual(n, 3); + clearInterval(handle); + t.mock.timers.tick(10); + assert.strictEqual(n, 3); + }); + + it("clears from another callback", (t) => { + t.mock.timers.enable({ apis: ["setInterval", "setTimeout"] }); + let n = 0; + const handle = setInterval(() => n++, 4); + setTimeout(() => clearInterval(handle), 9); + t.mock.timers.tick(20); + assert.strictEqual(n, 2); + }); + + it("leaves intervals alone when only setTimeout is mocked", async (t) => { + t.mock.timers.enable({ apis: ["setTimeout"] }); + let n = 0; + await new Promise((resolve) => { + const handle = setInterval(() => { + n++; + clearInterval(handle); + resolve(); + }, 5); + }); + assert.strictEqual(n, 1); + }); +}); + +describe("runAll", () => { + it("ticks to the last queued timer", (t) => { + t.mock.timers.enable({ apis: ["setTimeout"] }); + const log: string[] = []; + setTimeout(() => { + log.push("a"); + setTimeout(() => log.push("b"), 100); + }, 10); + setTimeout(() => log.push("c"), 20); + t.mock.timers.runAll(); + log.push("first runAll"); + t.mock.timers.runAll(); + log.push("second runAll"); + t.mock.timers.runAll(); + assert.deepStrictEqual(log, ["a", "c", "first runAll", "b", "second runAll"]); + }); +}); + +describe("clock", () => { + it("starts at now and moves with setTime", (t) => { + t.mock.timers.enable({ apis: ["setTimeout"], now: 1000 }); + const log: string[] = []; + setTimeout(() => log.push("a"), 10); + t.mock.timers.setTime(5000); + setTimeout(() => log.push("b"), 10); + t.mock.timers.tick(1); + log.push("tick 1"); + t.mock.timers.tick(9); + t.mock.timers.setTime(); + setTimeout(() => log.push("c"), 1); + t.mock.timers.tick(1); + assert.deepStrictEqual(log, ["a", "tick 1", "b", "c"]); + }); + + it("takes now from a Date", (t) => { + t.mock.timers.enable({ apis: ["setTimeout"], now: new Date(2000) }); + const log: string[] = []; + setTimeout(() => log.push("a"), 10); + t.mock.timers.setTime(2005); + t.mock.timers.tick(4); + log.push("tick 4"); + t.mock.timers.tick(1); + assert.deepStrictEqual(log, ["tick 4", "a"]); + }); +}); + +describe("handles", () => { + it("ref, unref and refresh leave a mock timer as it is", (t) => { + t.mock.timers.enable({ apis: ["setTimeout"] }); + let fired = 0; + const handle = setTimeout(() => fired++, 5); + assert.strictEqual(handle.hasRef(), true); + assert.strictEqual(handle.unref().hasRef(), true); + assert.strictEqual(handle.ref().hasRef(), true); + handle.refresh(); + t.mock.timers.tick(5); + t.mock.timers.tick(5); + assert.strictEqual(fired, 1); + }); + + it("clearTimeout of a mock timer", (t) => { + t.mock.timers.enable({ apis: ["setTimeout"] }); + let fired = 0; + const handle = setTimeout(() => fired++, 5); + clearTimeout(handle); + t.mock.timers.tick(10); + assert.strictEqual(fired, 0); + }); + + it("a real timer made before enable keeps running", async (t) => { + let fired = 0; + const done = new Promise((resolve) => { + const real = setTimeout(() => { + fired++; + resolve(); + }, 5); + t.mock.timers.enable({ apis: ["setTimeout"] }); + // The mocked clearTimeout leaves the real timer armed. + clearTimeout(real); + assert.strictEqual(real.hasRef(), true); + }); + await done; + assert.strictEqual(fired, 1); + }); +}); + +describe("errors", () => { + it("throws Node's errors", (t) => { + const got: string[] = []; + got.push(failure(() => t.mock.timers.tick(1))); + got.push(failure(() => t.mock.timers.runAll())); + got.push(failure(() => t.mock.timers.setTime(-1))); + got.push(failure(() => t.mock.timers.setTime(5))); + got.push(failure(() => t.mock.timers.enable({ apis: ["setTimeout"], now: -1 }))); + got.push(failure(() => t.mock.timers.enable({ apis: ["setTimeout"], now: NaN }))); + got.push(failure(() => t.mock.timers.reset())); + t.mock.timers.enable({ apis: ["setTimeout"] }); + got.push(failure(() => t.mock.timers.enable({ apis: ["setTimeout"] }))); + got.push(failure(() => t.mock.timers.tick(-1.5))); + got.push(failure(() => t.mock.timers.tick(-Infinity))); + got.push(failure(() => t.mock.timers.tick(0))); + assert.deepStrictEqual(got, [ + NOT_ENABLED, + NOT_ENABLED, + negative("-1"), + NOT_ENABLED, + negative("-1"), + "TypeError ERR_INVALID_ARG_VALUE The argument 'now' epoch must be a positive integer received NaN. Received NaN", + "no throw", + "Error ERR_INVALID_STATE Invalid state: MockTimers is already enabled!", + negative("-1.5"), + negative("-Infinity"), + "no throw", + ]); + }); + + it("a throwing callback stays queued", (t) => { + t.mock.timers.enable({ apis: ["setTimeout"] }); + let runs = 0; + setTimeout(() => { + runs++; + if (runs === 1) throw new Error("boom"); + }, 5); + assert.throws(() => t.mock.timers.tick(5), { message: "boom" }); + t.mock.timers.tick(0); + t.mock.timers.tick(0); + assert.strictEqual(runs, 2); + }); +}); + +describe("imports", () => { + it("globalThis.setTimeout is mocked", (t) => { + t.mock.timers.enable({ apis: ["setTimeout"] }); + const log: string[] = []; + globalThis.setTimeout(() => log.push("timeout"), 5); + t.mock.timers.tick(5); + assert.deepStrictEqual(log, ["timeout"]); + }); + + it("named and namespace imports keep the loop's timers", async (t) => { + t.mock.timers.enable({ apis: ["setTimeout"] }); + const log: string[] = []; + await Promise.all([ + sleep(5).then(() => { + log.push("named promise"); + }), + new Promise((resolve) => + namedSetTimeout(() => { + log.push("named"); + resolve(); + }, 2), + ), + new Promise((resolve) => + timersNamespace.setTimeout(() => { + log.push("namespace"); + resolve(); + }, 3), + ), + ]); + t.mock.timers.tick(10); + assert.deepStrictEqual(log, ["named", "namespace", "named promise"]); + }); +}); + +describe("trackers", () => { + it("leaves t.mock.timers enabled", (t) => { + t.mock.timers.enable({ apis: ["setTimeout"] }); + setTimeout(() => { + throw new Error("never runs"); + }, 1); + }); + + it("the previous test's timers were reset", async () => { + let fired = false; + await new Promise((resolve) => + setTimeout(() => { + fired = true; + resolve(); + }, 1), + ); + assert.strictEqual(fired, true); + }); + + it("t.mock takes setTimeout over from mock and gives it back", (t) => { + const log: string[] = []; + mock.timers.enable({ apis: ["setTimeout"] }); + setTimeout(() => log.push("module"), 5); + t.mock.timers.enable({ apis: ["setTimeout"] }); + setTimeout(() => log.push("test"), 5); + mock.timers.tick(5); + log.push("module ticked"); + t.mock.timers.tick(5); + t.mock.timers.reset(); + setTimeout(() => log.push("module again"), 1); + mock.timers.tick(1); + mock.timers.reset(); + assert.deepStrictEqual(log, ["module", "module ticked", "test", "module again"]); + }); + + it("mock.reset() resets mock.timers", () => { + mock.timers.enable({ apis: ["setInterval"] }); + mock.reset(); + assert.strictEqual(failure(() => mock.timers.tick(1)), NOT_ENABLED); + }); +}); + +test("timers outside node:test bodies", () => { + // A fresh enable after reset starts the clock at 0 again. + mock.timers.enable({ apis: ["setTimeout"], now: 100 }); + mock.timers.reset(); + mock.timers.enable({ apis: ["setTimeout"] }); + const log: string[] = []; + setTimeout(() => log.push("at 1"), 1); + mock.timers.tick(1); + mock.timers.reset(); + assert.deepStrictEqual(log, ["at 1"]); +}); diff --git a/tests/fixtures/node-test/cases/snapshot-files/value.json b/tests/fixtures/node-test/cases/snapshot-files/value.json new file mode 100644 index 0000000000..bb44c02abe --- /dev/null +++ b/tests/fixtures/node-test/cases/snapshot-files/value.json @@ -0,0 +1,6 @@ +{ + "f": 1, + "list": [ + "a" + ] +} \ No newline at end of file diff --git a/tests/fixtures/node-test/cases/snapshot.ts b/tests/fixtures/node-test/cases/snapshot.ts new file mode 100644 index 0000000000..eb5d97ce4a --- /dev/null +++ b/tests/fixtures/node-test/cases/snapshot.ts @@ -0,0 +1,72 @@ +// @exit: 1 +// t.assert.snapshot and t.assert.fileSnapshot in compare mode, against the +// snapshot.ts.snapshot file `node --test-update-snapshots` wrote (then +// edited so that one snapshot differs and one is missing). Values serialize +// with JSON.stringify(value, null, 2) unless options.serializers replace +// it; each test numbers its snapshots under its full name; a differing +// snapshot fails as assert.strictEqual does, and missing snapshots and +// files and throwing serializers throw ERR_INVALID_STATE. +import { describe, test } from "node:test"; + +describe("values", () => { + test("records and arrays", (t) => { + t.assert.snapshot({ zeta: 1, alpha: [1, "two", { b: true, a: null }] }); + t.assert.snapshot([{ id: 3, name: "n" }]); + }); + test("strings", (t) => { + t.assert.snapshot("plain"); + t.assert.snapshot("tick ` dollar ${x} backslash \\ newline \n tab \t"); + t.assert.snapshot("unicode é 漢 😀"); + }); + test("scalars", (t) => { + t.assert.snapshot(42); + t.assert.snapshot(-0.5); + t.assert.snapshot(true); + t.assert.snapshot(null); + t.assert.snapshot(undefined); + }); +}); + +test("name with `backticks` and ${braces}", (t) => { + t.assert.snapshot({ ok: true }); +}); + +describe("serializers", () => { + test("run in order", (t) => { + t.assert.snapshot({ n: 5 }, { serializers: [(v: { n: number }) => v.n * 2, (v: number) => `n is ${v}`] }); + t.assert.snapshot("x", { serializers: [(v: string) => v.toUpperCase()] }); + t.assert.snapshot(7, { serializers: [(v: number) => v + 1] }); + }); + test("a throwing serializer", (t) => { + t.assert.snapshot(1, { + serializers: [ + () => { + throw new Error("no"); + }, + ], + }); + }); + test("a circular value", (t) => { + const loop: { self?: unknown } = {}; + loop.self = loop; + t.assert.snapshot(loop); + }); +}); + +test("counts toward the plan", { plan: 2 }, (t) => { + t.assert.snapshot(1); + t.assert.ok(true); +}); + +test("differs from the stored snapshot", (t) => { + t.assert.snapshot({ count: 2 }); +}); + +test("missing from the snapshot file", (t) => { + t.assert.snapshot("new"); +}); + +test("file snapshots", (t) => { + t.assert.fileSnapshot({ f: 1, list: ["a"] }, "tests/fixtures/node-test/cases/snapshot-files/value.json"); + t.assert.fileSnapshot("text", "tests/fixtures/node-test/cases/snapshot-files/missing.txt"); +}); diff --git a/tests/fixtures/node-test/cases/snapshot.ts.snapshot b/tests/fixtures/node-test/cases/snapshot.ts.snapshot new file mode 100644 index 0000000000..ba201eb68d --- /dev/null +++ b/tests/fixtures/node-test/cases/snapshot.ts.snapshot @@ -0,0 +1,82 @@ +exports[`counts toward the plan 1`] = ` +1 +`; + +exports[`differs from the stored snapshot 1`] = ` +{ + "count": 1 +} +`; + +exports[`name with \`backticks\` and \${braces} 1`] = ` +{ + "ok": true +} +`; + +exports[`serializers > run in order 1`] = ` +n is 10 +`; + +exports[`serializers > run in order 2`] = ` +X +`; + +exports[`serializers > run in order 3`] = ` +8 +`; + +exports[`values > records and arrays 1`] = ` +{ + "zeta": 1, + "alpha": [ + 1, + "two", + { + "b": true, + "a": null + } + ] +} +`; + +exports[`values > records and arrays 2`] = ` +[ + { + "id": 3, + "name": "n" + } +] +`; + +exports[`values > scalars 1`] = ` +42 +`; + +exports[`values > scalars 2`] = ` +-0.5 +`; + +exports[`values > scalars 3`] = ` +true +`; + +exports[`values > scalars 4`] = ` +null +`; + +exports[`values > scalars 5`] = ` +undefined +`; + +exports[`values > strings 1`] = ` +"plain" +`; + +exports[`values > strings 2`] = ` +"tick \` dollar \${x} backslash \\\\ newline \\n tab \\t" +`; + +exports[`values > strings 3`] = ` +"unicode é 漢 😀" +`; diff --git a/tests/fixtures/node-test/update/snapshot-update.ts b/tests/fixtures/node-test/update/snapshot-update.ts new file mode 100644 index 0000000000..7d14509d19 --- /dev/null +++ b/tests/fixtures/node-test/update/snapshot-update.ts @@ -0,0 +1,27 @@ +// Run with --test-update-snapshots (before the program arguments): the +// snapshot assertions record their values, the run writes +// snapshot-update.ts.snapshot with sorted keys and out/value.json, and the +// flag is in process.execArgv instead of process.argv. +import { describe, test } from "node:test"; + +console.log(JSON.stringify(process.argv.slice(2)), JSON.stringify(process.execArgv)); + +describe("suite", () => { + test("values", (t) => { + t.assert.snapshot({ b: 1, a: "x`${y}\\z\n" }); + t.assert.snapshot([1, 2]); + t.assert.snapshot("é 😀 \u0001"); + }); + test("serializers", (t) => { + t.assert.snapshot(3, { serializers: [(v: number) => v * 2] }); + }); +}); + +test("zz last", (t) => { + t.assert.snapshot(undefined); +}); + +test("aa `first` ${x}", (t) => { + t.assert.snapshot(null); + t.assert.fileSnapshot({ f: [1] }, "out/value.json"); +}); diff --git a/tests/fixtures/node-types/child-exec-code-fences.ts b/tests/fixtures/node-types/child-exec-code-fences.ts new file mode 100644 index 0000000000..71f627df46 --- /dev/null +++ b/tests/fixtures/node-types/child-exec-code-fences.ts @@ -0,0 +1,8 @@ +import { exec } from "node:child_process"; + +exec("exit 3", (error) => { + if (error === null) return; + console.log(error.code); + console.log("code" in error); + console.log(typeof error.code === "number"); +}); diff --git a/tests/fixtures/node-types/child-exec.ts b/tests/fixtures/node-types/child-exec.ts new file mode 100644 index 0000000000..97e9c546df --- /dev/null +++ b/tests/fixtures/node-types/child-exec.ts @@ -0,0 +1,9 @@ +import { exec } from "node:child_process"; + +exec("printf typed-exec; printf err >&2", (error, stdout, stderr) => { + if (error !== null) throw error; + console.log(stdout, stderr); + exec("exit 2", { encoding: "utf8" }, (failed) => { + console.log(JSON.stringify(failed === null ? "" : failed.message)); + }); +}); diff --git a/tests/fixtures/node-types/fenced.ts b/tests/fixtures/node-types/fenced.ts index 3ad6839c83..b884de3ced 100644 --- a/tests/fixtures/node-types/fenced.ts +++ b/tests/fixtures/node-types/fenced.ts @@ -33,10 +33,10 @@ ReadableStream.from(new Set([1, 2])); * typecheck under @types/node and fence with the module-qualified name — * calls and value reads alike. */ import { watchFile } from "fs"; -import { setPriority } from "node:os"; +import { EventEmitter, on } from "node:events"; import { win32 } from "path"; watchFile("x", () => {}); -setPriority(0); +on(new EventEmitter(), "tick"); console.log(win32.sep); /* URL getters and serialization lower under @types/node, but object-URL * registry operations still fence by member name with the supported diff --git a/tests/fixtures/npm-static/cjs-interop-cli.ts b/tests/fixtures/npm-static/cjs-interop-cli.ts new file mode 100644 index 0000000000..2b9a88f635 --- /dev/null +++ b/tests/fixtures/npm-static/cjs-interop-cli.ts @@ -0,0 +1,9 @@ +// npm-static pilots: real CommonJS packages that preflight refused before. +// fs.realpath@1.0.0 and on-finished@2.4.1 assign module.exports and run +// code before they require their dependencies (each binding is reachable +// only after its require ran); statuses@2.0.1 requires its codes.json. +import realpath from "fs.realpath"; +import onFinished from "on-finished"; +import status from "statuses"; + +console.log(typeof realpath.realpathSync, typeof onFinished, typeof status); diff --git a/tests/fixtures/npm-static/node_modules/ee-first/LICENSE b/tests/fixtures/npm-static/node_modules/ee-first/LICENSE new file mode 100644 index 0000000000..a7ae8ee9b8 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/ee-first/LICENSE @@ -0,0 +1,22 @@ + +The MIT License (MIT) + +Copyright (c) 2014 Jonathan Ong me@jongleberry.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/tests/fixtures/npm-static/node_modules/ee-first/index.js b/tests/fixtures/npm-static/node_modules/ee-first/index.js new file mode 100644 index 0000000000..501287cd3b --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/ee-first/index.js @@ -0,0 +1,95 @@ +/*! + * ee-first + * Copyright(c) 2014 Jonathan Ong + * MIT Licensed + */ + +'use strict' + +/** + * Module exports. + * @public + */ + +module.exports = first + +/** + * Get the first event in a set of event emitters and event pairs. + * + * @param {array} stuff + * @param {function} done + * @public + */ + +function first(stuff, done) { + if (!Array.isArray(stuff)) + throw new TypeError('arg must be an array of [ee, events...] arrays') + + var cleanups = [] + + for (var i = 0; i < stuff.length; i++) { + var arr = stuff[i] + + if (!Array.isArray(arr) || arr.length < 2) + throw new TypeError('each array member must be [ee, events...]') + + var ee = arr[0] + + for (var j = 1; j < arr.length; j++) { + var event = arr[j] + var fn = listener(event, callback) + + // listen to the event + ee.on(event, fn) + // push this listener to the list of cleanups + cleanups.push({ + ee: ee, + event: event, + fn: fn, + }) + } + } + + function callback() { + cleanup() + done.apply(null, arguments) + } + + function cleanup() { + var x + for (var i = 0; i < cleanups.length; i++) { + x = cleanups[i] + x.ee.removeListener(x.event, x.fn) + } + } + + function thunk(fn) { + done = fn + } + + thunk.cancel = cleanup + + return thunk +} + +/** + * Create the event listener. + * @private + */ + +function listener(event, done) { + return function onevent(arg1) { + var args = new Array(arguments.length) + var ee = this + var err = event === 'error' + ? arg1 + : null + + // copy args to prevent arguments escaping scope + for (var i = 0; i < args.length; i++) { + args[i] = arguments[i] + } + + done(err, ee, event, args) + } +} diff --git a/tests/fixtures/npm-static/node_modules/ee-first/package.json b/tests/fixtures/npm-static/node_modules/ee-first/package.json new file mode 100644 index 0000000000..b6d0b7d675 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/ee-first/package.json @@ -0,0 +1,29 @@ +{ + "name": "ee-first", + "description": "return the first event in a set of ee/event pairs", + "version": "1.1.1", + "author": { + "name": "Jonathan Ong", + "email": "me@jongleberry.com", + "url": "http://jongleberry.com", + "twitter": "https://twitter.com/jongleberry" + }, + "contributors": [ + "Douglas Christopher Wilson " + ], + "license": "MIT", + "repository": "jonathanong/ee-first", + "devDependencies": { + "istanbul": "0.3.9", + "mocha": "2.2.5" + }, + "files": [ + "index.js", + "LICENSE" + ], + "scripts": { + "test": "mocha --reporter spec --bail --check-leaks test/", + "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --reporter dot --check-leaks test/", + "test-travis": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --reporter spec --check-leaks test/" + } +} diff --git a/tests/fixtures/npm-static/node_modules/fs.realpath/LICENSE b/tests/fixtures/npm-static/node_modules/fs.realpath/LICENSE new file mode 100644 index 0000000000..5bd884c252 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/fs.realpath/LICENSE @@ -0,0 +1,43 @@ +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +---- + +This library bundles a version of the `fs.realpath` and `fs.realpathSync` +methods from Node.js v0.10 under the terms of the Node.js MIT license. + +Node's license follows, also included at the header of `old.js` which contains +the licensed code: + + Copyright Joyent, Inc. and other Node contributors. + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. diff --git a/tests/fixtures/npm-static/node_modules/fs.realpath/index.js b/tests/fixtures/npm-static/node_modules/fs.realpath/index.js new file mode 100644 index 0000000000..b09c7c7e63 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/fs.realpath/index.js @@ -0,0 +1,66 @@ +module.exports = realpath +realpath.realpath = realpath +realpath.sync = realpathSync +realpath.realpathSync = realpathSync +realpath.monkeypatch = monkeypatch +realpath.unmonkeypatch = unmonkeypatch + +var fs = require('fs') +var origRealpath = fs.realpath +var origRealpathSync = fs.realpathSync + +var version = process.version +var ok = /^v[0-5]\./.test(version) +var old = require('./old.js') + +function newError (er) { + return er && er.syscall === 'realpath' && ( + er.code === 'ELOOP' || + er.code === 'ENOMEM' || + er.code === 'ENAMETOOLONG' + ) +} + +function realpath (p, cache, cb) { + if (ok) { + return origRealpath(p, cache, cb) + } + + if (typeof cache === 'function') { + cb = cache + cache = null + } + origRealpath(p, cache, function (er, result) { + if (newError(er)) { + old.realpath(p, cache, cb) + } else { + cb(er, result) + } + }) +} + +function realpathSync (p, cache) { + if (ok) { + return origRealpathSync(p, cache) + } + + try { + return origRealpathSync(p, cache) + } catch (er) { + if (newError(er)) { + return old.realpathSync(p, cache) + } else { + throw er + } + } +} + +function monkeypatch () { + fs.realpath = realpath + fs.realpathSync = realpathSync +} + +function unmonkeypatch () { + fs.realpath = origRealpath + fs.realpathSync = origRealpathSync +} diff --git a/tests/fixtures/npm-static/node_modules/fs.realpath/old.js b/tests/fixtures/npm-static/node_modules/fs.realpath/old.js new file mode 100644 index 0000000000..b40305e73f --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/fs.realpath/old.js @@ -0,0 +1,303 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +var pathModule = require('path'); +var isWindows = process.platform === 'win32'; +var fs = require('fs'); + +// JavaScript implementation of realpath, ported from node pre-v6 + +var DEBUG = process.env.NODE_DEBUG && /fs/.test(process.env.NODE_DEBUG); + +function rethrow() { + // Only enable in debug mode. A backtrace uses ~1000 bytes of heap space and + // is fairly slow to generate. + var callback; + if (DEBUG) { + var backtrace = new Error; + callback = debugCallback; + } else + callback = missingCallback; + + return callback; + + function debugCallback(err) { + if (err) { + backtrace.message = err.message; + err = backtrace; + missingCallback(err); + } + } + + function missingCallback(err) { + if (err) { + if (process.throwDeprecation) + throw err; // Forgot a callback but don't know where? Use NODE_DEBUG=fs + else if (!process.noDeprecation) { + var msg = 'fs: missing callback ' + (err.stack || err.message); + if (process.traceDeprecation) + console.trace(msg); + else + console.error(msg); + } + } + } +} + +function maybeCallback(cb) { + return typeof cb === 'function' ? cb : rethrow(); +} + +var normalize = pathModule.normalize; + +// Regexp that finds the next partion of a (partial) path +// result is [base_with_slash, base], e.g. ['somedir/', 'somedir'] +if (isWindows) { + var nextPartRe = /(.*?)(?:[\/\\]+|$)/g; +} else { + var nextPartRe = /(.*?)(?:[\/]+|$)/g; +} + +// Regex to find the device root, including trailing slash. E.g. 'c:\\'. +if (isWindows) { + var splitRootRe = /^(?:[a-zA-Z]:|[\\\/]{2}[^\\\/]+[\\\/][^\\\/]+)?[\\\/]*/; +} else { + var splitRootRe = /^[\/]*/; +} + +exports.realpathSync = function realpathSync(p, cache) { + // make p is absolute + p = pathModule.resolve(p); + + if (cache && Object.prototype.hasOwnProperty.call(cache, p)) { + return cache[p]; + } + + var original = p, + seenLinks = {}, + knownHard = {}; + + // current character position in p + var pos; + // the partial path so far, including a trailing slash if any + var current; + // the partial path without a trailing slash (except when pointing at a root) + var base; + // the partial path scanned in the previous round, with slash + var previous; + + start(); + + function start() { + // Skip over roots + var m = splitRootRe.exec(p); + pos = m[0].length; + current = m[0]; + base = m[0]; + previous = ''; + + // On windows, check that the root exists. On unix there is no need. + if (isWindows && !knownHard[base]) { + fs.lstatSync(base); + knownHard[base] = true; + } + } + + // walk down the path, swapping out linked pathparts for their real + // values + // NB: p.length changes. + while (pos < p.length) { + // find the next part + nextPartRe.lastIndex = pos; + var result = nextPartRe.exec(p); + previous = current; + current += result[0]; + base = previous + result[1]; + pos = nextPartRe.lastIndex; + + // continue if not a symlink + if (knownHard[base] || (cache && cache[base] === base)) { + continue; + } + + var resolvedLink; + if (cache && Object.prototype.hasOwnProperty.call(cache, base)) { + // some known symbolic link. no need to stat again. + resolvedLink = cache[base]; + } else { + var stat = fs.lstatSync(base); + if (!stat.isSymbolicLink()) { + knownHard[base] = true; + if (cache) cache[base] = base; + continue; + } + + // read the link if it wasn't read before + // dev/ino always return 0 on windows, so skip the check. + var linkTarget = null; + if (!isWindows) { + var id = stat.dev.toString(32) + ':' + stat.ino.toString(32); + if (seenLinks.hasOwnProperty(id)) { + linkTarget = seenLinks[id]; + } + } + if (linkTarget === null) { + fs.statSync(base); + linkTarget = fs.readlinkSync(base); + } + resolvedLink = pathModule.resolve(previous, linkTarget); + // track this, if given a cache. + if (cache) cache[base] = resolvedLink; + if (!isWindows) seenLinks[id] = linkTarget; + } + + // resolve the link, then start over + p = pathModule.resolve(resolvedLink, p.slice(pos)); + start(); + } + + if (cache) cache[original] = p; + + return p; +}; + + +exports.realpath = function realpath(p, cache, cb) { + if (typeof cb !== 'function') { + cb = maybeCallback(cache); + cache = null; + } + + // make p is absolute + p = pathModule.resolve(p); + + if (cache && Object.prototype.hasOwnProperty.call(cache, p)) { + return process.nextTick(cb.bind(null, null, cache[p])); + } + + var original = p, + seenLinks = {}, + knownHard = {}; + + // current character position in p + var pos; + // the partial path so far, including a trailing slash if any + var current; + // the partial path without a trailing slash (except when pointing at a root) + var base; + // the partial path scanned in the previous round, with slash + var previous; + + start(); + + function start() { + // Skip over roots + var m = splitRootRe.exec(p); + pos = m[0].length; + current = m[0]; + base = m[0]; + previous = ''; + + // On windows, check that the root exists. On unix there is no need. + if (isWindows && !knownHard[base]) { + fs.lstat(base, function(err) { + if (err) return cb(err); + knownHard[base] = true; + LOOP(); + }); + } else { + process.nextTick(LOOP); + } + } + + // walk down the path, swapping out linked pathparts for their real + // values + function LOOP() { + // stop if scanned past end of path + if (pos >= p.length) { + if (cache) cache[original] = p; + return cb(null, p); + } + + // find the next part + nextPartRe.lastIndex = pos; + var result = nextPartRe.exec(p); + previous = current; + current += result[0]; + base = previous + result[1]; + pos = nextPartRe.lastIndex; + + // continue if not a symlink + if (knownHard[base] || (cache && cache[base] === base)) { + return process.nextTick(LOOP); + } + + if (cache && Object.prototype.hasOwnProperty.call(cache, base)) { + // known symbolic link. no need to stat again. + return gotResolvedLink(cache[base]); + } + + return fs.lstat(base, gotStat); + } + + function gotStat(err, stat) { + if (err) return cb(err); + + // if not a symlink, skip to the next path part + if (!stat.isSymbolicLink()) { + knownHard[base] = true; + if (cache) cache[base] = base; + return process.nextTick(LOOP); + } + + // stat & read the link if not read before + // call gotTarget as soon as the link target is known + // dev/ino always return 0 on windows, so skip the check. + if (!isWindows) { + var id = stat.dev.toString(32) + ':' + stat.ino.toString(32); + if (seenLinks.hasOwnProperty(id)) { + return gotTarget(null, seenLinks[id], base); + } + } + fs.stat(base, function(err) { + if (err) return cb(err); + + fs.readlink(base, function(err, target) { + if (!isWindows) seenLinks[id] = target; + gotTarget(err, target); + }); + }); + } + + function gotTarget(err, target, base) { + if (err) return cb(err); + + var resolvedLink = pathModule.resolve(previous, target); + if (cache) cache[base] = resolvedLink; + gotResolvedLink(resolvedLink); + } + + function gotResolvedLink(resolvedLink) { + // resolve the link, then start over + p = pathModule.resolve(resolvedLink, p.slice(pos)); + start(); + } +}; diff --git a/tests/fixtures/npm-static/node_modules/fs.realpath/package.json b/tests/fixtures/npm-static/node_modules/fs.realpath/package.json new file mode 100644 index 0000000000..3edc57d21c --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/fs.realpath/package.json @@ -0,0 +1,26 @@ +{ + "name": "fs.realpath", + "version": "1.0.0", + "description": "Use node's fs.realpath, but fall back to the JS implementation if the native one fails", + "main": "index.js", + "dependencies": {}, + "devDependencies": {}, + "scripts": { + "test": "tap test/*.js --cov" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/isaacs/fs.realpath.git" + }, + "keywords": [ + "realpath", + "fs", + "polyfill" + ], + "author": "Isaac Z. Schlueter (http://blog.izs.me/)", + "license": "ISC", + "files": [ + "old.js", + "index.js" + ] +} diff --git a/tests/fixtures/npm-static/node_modules/on-finished/LICENSE b/tests/fixtures/npm-static/node_modules/on-finished/LICENSE new file mode 100644 index 0000000000..5931fd23ea --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/on-finished/LICENSE @@ -0,0 +1,23 @@ +(The MIT License) + +Copyright (c) 2013 Jonathan Ong +Copyright (c) 2014 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/tests/fixtures/npm-static/node_modules/on-finished/index.js b/tests/fixtures/npm-static/node_modules/on-finished/index.js new file mode 100644 index 0000000000..e68df7bde3 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/on-finished/index.js @@ -0,0 +1,234 @@ +/*! + * on-finished + * Copyright(c) 2013 Jonathan Ong + * Copyright(c) 2014 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict' + +/** + * Module exports. + * @public + */ + +module.exports = onFinished +module.exports.isFinished = isFinished + +/** + * Module dependencies. + * @private + */ + +var asyncHooks = tryRequireAsyncHooks() +var first = require('ee-first') + +/** + * Variables. + * @private + */ + +/* istanbul ignore next */ +var defer = typeof setImmediate === 'function' + ? setImmediate + : function (fn) { process.nextTick(fn.bind.apply(fn, arguments)) } + +/** + * Invoke callback when the response has finished, useful for + * cleaning up resources afterwards. + * + * @param {object} msg + * @param {function} listener + * @return {object} + * @public + */ + +function onFinished (msg, listener) { + if (isFinished(msg) !== false) { + defer(listener, null, msg) + return msg + } + + // attach the listener to the message + attachListener(msg, wrap(listener)) + + return msg +} + +/** + * Determine if message is already finished. + * + * @param {object} msg + * @return {boolean} + * @public + */ + +function isFinished (msg) { + var socket = msg.socket + + if (typeof msg.finished === 'boolean') { + // OutgoingMessage + return Boolean(msg.finished || (socket && !socket.writable)) + } + + if (typeof msg.complete === 'boolean') { + // IncomingMessage + return Boolean(msg.upgrade || !socket || !socket.readable || (msg.complete && !msg.readable)) + } + + // don't know + return undefined +} + +/** + * Attach a finished listener to the message. + * + * @param {object} msg + * @param {function} callback + * @private + */ + +function attachFinishedListener (msg, callback) { + var eeMsg + var eeSocket + var finished = false + + function onFinish (error) { + eeMsg.cancel() + eeSocket.cancel() + + finished = true + callback(error) + } + + // finished on first message event + eeMsg = eeSocket = first([[msg, 'end', 'finish']], onFinish) + + function onSocket (socket) { + // remove listener + msg.removeListener('socket', onSocket) + + if (finished) return + if (eeMsg !== eeSocket) return + + // finished on first socket event + eeSocket = first([[socket, 'error', 'close']], onFinish) + } + + if (msg.socket) { + // socket already assigned + onSocket(msg.socket) + return + } + + // wait for socket to be assigned + msg.on('socket', onSocket) + + if (msg.socket === undefined) { + // istanbul ignore next: node.js 0.8 patch + patchAssignSocket(msg, onSocket) + } +} + +/** + * Attach the listener to the message. + * + * @param {object} msg + * @return {function} + * @private + */ + +function attachListener (msg, listener) { + var attached = msg.__onFinished + + // create a private single listener with queue + if (!attached || !attached.queue) { + attached = msg.__onFinished = createListener(msg) + attachFinishedListener(msg, attached) + } + + attached.queue.push(listener) +} + +/** + * Create listener on message. + * + * @param {object} msg + * @return {function} + * @private + */ + +function createListener (msg) { + function listener (err) { + if (msg.__onFinished === listener) msg.__onFinished = null + if (!listener.queue) return + + var queue = listener.queue + listener.queue = null + + for (var i = 0; i < queue.length; i++) { + queue[i](err, msg) + } + } + + listener.queue = [] + + return listener +} + +/** + * Patch ServerResponse.prototype.assignSocket for node.js 0.8. + * + * @param {ServerResponse} res + * @param {function} callback + * @private + */ + +// istanbul ignore next: node.js 0.8 patch +function patchAssignSocket (res, callback) { + var assignSocket = res.assignSocket + + if (typeof assignSocket !== 'function') return + + // res.on('socket', callback) is broken in 0.8 + res.assignSocket = function _assignSocket (socket) { + assignSocket.call(this, socket) + callback(socket) + } +} + +/** + * Try to require async_hooks + * @private + */ + +function tryRequireAsyncHooks () { + try { + return require('async_hooks') + } catch (e) { + return {} + } +} + +/** + * Wrap function with async resource, if possible. + * AsyncResource.bind static method backported. + * @private + */ + +function wrap (fn) { + var res + + // create anonymous resource + if (asyncHooks.AsyncResource) { + res = new asyncHooks.AsyncResource(fn.name || 'bound-anonymous-fn') + } + + // incompatible node.js + if (!res || !res.runInAsyncScope) { + return fn + } + + // return bound function + return res.runInAsyncScope.bind(res, fn, null) +} diff --git a/tests/fixtures/npm-static/node_modules/on-finished/package.json b/tests/fixtures/npm-static/node_modules/on-finished/package.json new file mode 100644 index 0000000000..644cd814b9 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/on-finished/package.json @@ -0,0 +1,39 @@ +{ + "name": "on-finished", + "description": "Execute a callback when a request closes, finishes, or errors", + "version": "2.4.1", + "contributors": [ + "Douglas Christopher Wilson ", + "Jonathan Ong (http://jongleberry.com)" + ], + "license": "MIT", + "repository": "jshttp/on-finished", + "dependencies": { + "ee-first": "1.1.1" + }, + "devDependencies": { + "eslint": "7.32.0", + "eslint-config-standard": "14.1.1", + "eslint-plugin-import": "2.25.4", + "eslint-plugin-markdown": "2.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-promise": "5.2.0", + "eslint-plugin-standard": "4.1.0", + "mocha": "9.2.1", + "nyc": "15.1.0" + }, + "engines": { + "node": ">= 0.8" + }, + "files": [ + "HISTORY.md", + "LICENSE", + "index.js" + ], + "scripts": { + "lint": "eslint .", + "test": "mocha --reporter spec --bail --check-leaks test/", + "test-ci": "nyc --reporter=lcovonly --reporter=text npm test", + "test-cov": "nyc --reporter=html --reporter=text npm test" + } +} diff --git a/tests/fixtures/npm-static/node_modules/statuses/LICENSE b/tests/fixtures/npm-static/node_modules/statuses/LICENSE new file mode 100644 index 0000000000..28a3161820 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/statuses/LICENSE @@ -0,0 +1,23 @@ + +The MIT License (MIT) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/tests/fixtures/npm-static/node_modules/statuses/codes.json b/tests/fixtures/npm-static/node_modules/statuses/codes.json new file mode 100644 index 0000000000..1333ed10ba --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/statuses/codes.json @@ -0,0 +1,65 @@ +{ + "100": "Continue", + "101": "Switching Protocols", + "102": "Processing", + "103": "Early Hints", + "200": "OK", + "201": "Created", + "202": "Accepted", + "203": "Non-Authoritative Information", + "204": "No Content", + "205": "Reset Content", + "206": "Partial Content", + "207": "Multi-Status", + "208": "Already Reported", + "226": "IM Used", + "300": "Multiple Choices", + "301": "Moved Permanently", + "302": "Found", + "303": "See Other", + "304": "Not Modified", + "305": "Use Proxy", + "307": "Temporary Redirect", + "308": "Permanent Redirect", + "400": "Bad Request", + "401": "Unauthorized", + "402": "Payment Required", + "403": "Forbidden", + "404": "Not Found", + "405": "Method Not Allowed", + "406": "Not Acceptable", + "407": "Proxy Authentication Required", + "408": "Request Timeout", + "409": "Conflict", + "410": "Gone", + "411": "Length Required", + "412": "Precondition Failed", + "413": "Payload Too Large", + "414": "URI Too Long", + "415": "Unsupported Media Type", + "416": "Range Not Satisfiable", + "417": "Expectation Failed", + "418": "I'm a Teapot", + "421": "Misdirected Request", + "422": "Unprocessable Entity", + "423": "Locked", + "424": "Failed Dependency", + "425": "Too Early", + "426": "Upgrade Required", + "428": "Precondition Required", + "429": "Too Many Requests", + "431": "Request Header Fields Too Large", + "451": "Unavailable For Legal Reasons", + "500": "Internal Server Error", + "501": "Not Implemented", + "502": "Bad Gateway", + "503": "Service Unavailable", + "504": "Gateway Timeout", + "505": "HTTP Version Not Supported", + "506": "Variant Also Negotiates", + "507": "Insufficient Storage", + "508": "Loop Detected", + "509": "Bandwidth Limit Exceeded", + "510": "Not Extended", + "511": "Network Authentication Required" +} diff --git a/tests/fixtures/npm-static/node_modules/statuses/index.js b/tests/fixtures/npm-static/node_modules/statuses/index.js new file mode 100644 index 0000000000..ea351c5535 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/statuses/index.js @@ -0,0 +1,146 @@ +/*! + * statuses + * Copyright(c) 2014 Jonathan Ong + * Copyright(c) 2016 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict' + +/** + * Module dependencies. + * @private + */ + +var codes = require('./codes.json') + +/** + * Module exports. + * @public + */ + +module.exports = status + +// status code to message map +status.message = codes + +// status message (lower-case) to code map +status.code = createMessageToStatusCodeMap(codes) + +// array of status codes +status.codes = createStatusCodeList(codes) + +// status codes for redirects +status.redirect = { + 300: true, + 301: true, + 302: true, + 303: true, + 305: true, + 307: true, + 308: true +} + +// status codes for empty bodies +status.empty = { + 204: true, + 205: true, + 304: true +} + +// status codes for when you should retry the request +status.retry = { + 502: true, + 503: true, + 504: true +} + +/** + * Create a map of message to status code. + * @private + */ + +function createMessageToStatusCodeMap (codes) { + var map = {} + + Object.keys(codes).forEach(function forEachCode (code) { + var message = codes[code] + var status = Number(code) + + // populate map + map[message.toLowerCase()] = status + }) + + return map +} + +/** + * Create a list of all status codes. + * @private + */ + +function createStatusCodeList (codes) { + return Object.keys(codes).map(function mapCode (code) { + return Number(code) + }) +} + +/** + * Get the status code for given message. + * @private + */ + +function getStatusCode (message) { + var msg = message.toLowerCase() + + if (!Object.prototype.hasOwnProperty.call(status.code, msg)) { + throw new Error('invalid status message: "' + message + '"') + } + + return status.code[msg] +} + +/** + * Get the status message for given code. + * @private + */ + +function getStatusMessage (code) { + if (!Object.prototype.hasOwnProperty.call(status.message, code)) { + throw new Error('invalid status code: ' + code) + } + + return status.message[code] +} + +/** + * Get the status code. + * + * Given a number, this will throw if it is not a known status + * code, otherwise the code will be returned. Given a string, + * the string will be parsed for a number and return the code + * if valid, otherwise will lookup the code assuming this is + * the status message. + * + * @param {string|number} code + * @returns {number} + * @public + */ + +function status (code) { + if (typeof code === 'number') { + return getStatusMessage(code) + } + + if (typeof code !== 'string') { + throw new TypeError('code must be a number or string') + } + + // '403' + var n = parseInt(code, 10) + if (!isNaN(n)) { + return getStatusMessage(n) + } + + return getStatusCode(code) +} diff --git a/tests/fixtures/npm-static/node_modules/statuses/package.json b/tests/fixtures/npm-static/node_modules/statuses/package.json new file mode 100644 index 0000000000..8c3e719b50 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/statuses/package.json @@ -0,0 +1,49 @@ +{ + "name": "statuses", + "description": "HTTP status utility", + "version": "2.0.1", + "contributors": [ + "Douglas Christopher Wilson ", + "Jonathan Ong (http://jongleberry.com)" + ], + "repository": "jshttp/statuses", + "license": "MIT", + "keywords": [ + "http", + "status", + "code" + ], + "files": [ + "HISTORY.md", + "index.js", + "codes.json", + "LICENSE" + ], + "devDependencies": { + "csv-parse": "4.14.2", + "eslint": "7.17.0", + "eslint-config-standard": "14.1.1", + "eslint-plugin-import": "2.22.1", + "eslint-plugin-markdown": "1.0.2", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-promise": "4.2.1", + "eslint-plugin-standard": "4.1.0", + "mocha": "8.2.1", + "nyc": "15.1.0", + "raw-body": "2.4.1", + "stream-to-array": "2.3.0" + }, + "engines": { + "node": ">= 0.8" + }, + "scripts": { + "build": "node scripts/build.js", + "fetch": "node scripts/fetch-apache.js && node scripts/fetch-iana.js && node scripts/fetch-nginx.js && node scripts/fetch-node.js", + "lint": "eslint --plugin markdown --ext js,md .", + "test": "mocha --reporter spec --check-leaks --bail test/", + "test-ci": "nyc --reporter=lcov --reporter=text npm test", + "test-cov": "nyc --reporter=html --reporter=text npm test", + "update": "npm run fetch && npm run build", + "version": "node scripts/version-history.js && git add HISTORY.md" + } +} diff --git a/tests/harness/__snapshots__/cjs-require-cycle/main.js.txt b/tests/harness/__snapshots__/cjs-require-cycle/main.js.txt index ad55be5244..8916284c3a 100644 --- a/tests/harness/__snapshots__/cjs-require-cycle/main.js.txt +++ b/tests/harness/__snapshots__/cjs-require-cycle/main.js.txt @@ -1,4 +1,4 @@ -cjs-require-cycle/a.js:1:1 - error SC1016: circular imports (cjs-require-cycle/a.js → cjs-require-cycle/b.js → cjs-require-cycle/a.js; the cycle closes through a require() edge) are not supported yet +cjs-require-cycle/a.js:1:1 - error SC1016: circular imports (cjs-require-cycle/a.js → cjs-require-cycle/b.js → cjs-require-cycle/a.js; the cycle closes through a require() edge: cjs-require-cycle/a.js uses its exports at cjs-require-cycle/a.js:6 outside the export statements that precede its first require, so a module of the cycle can observe them before they settle) are not supported yet 1 | 'use strict'; | ^ diff --git a/tests/harness/__snapshots__/cjs-require-tdz-unchecked/main.js.txt b/tests/harness/__snapshots__/cjs-require-tdz-unchecked/main.js.txt new file mode 100644 index 0000000000..eda869e9a6 --- /dev/null +++ b/tests/harness/__snapshots__/cjs-require-tdz-unchecked/main.js.txt @@ -0,0 +1,6 @@ +cjs-require-tdz-unchecked/main.js:7:20 - error SC1013: reading require() binding 'greet' where it can run before the require (this use has no checked form for Node's ReferenceError — move the require above the code that can run first) is not supported yet + + 6 | + 7 | module.exports = { greet }; + | ^~~~~ + 8 | const { greet } = require('./lib.js'); \ No newline at end of file diff --git a/tests/harness/__snapshots__/cjs-require-tdz/main.js.txt b/tests/harness/__snapshots__/cjs-require-tdz/main.js.txt index 6316ce98b4..295f7cd0c1 100644 --- a/tests/harness/__snapshots__/cjs-require-tdz/main.js.txt +++ b/tests/harness/__snapshots__/cjs-require-tdz/main.js.txt @@ -1,6 +1,6 @@ -cjs-require-tdz/main.js:8:7 - error SC1013: require() below code that can reach its binding 'greet' (Node initializes the binding AT the require and throws ReferenceError on any earlier access, which is not modeled — move the require above the code that can run first) is not supported yet +cjs-require-tdz/main.js:9:5 - error SC1013: var require() binding 'lib' below code that can reach it (Node reads undefined before the require runs, which is not modeled — declare it with const or let, or move the require above the code that can run first) is not supported yet - 7 | console.log('boot', helper()); - 8 | const { greet } = require('./lib.js'); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 9 | \ No newline at end of file + 8 | console.log('boot', helper()); + 9 | var lib = require('./lib.js'); + | ^~~~~~~~~~~~~~~~~~~~~~~~~ + 10 | \ No newline at end of file diff --git a/tests/harness/__snapshots__/console-fences.ts.txt b/tests/harness/__snapshots__/console-fences.ts.txt index 98865ff68c..1841fec658 100644 --- a/tests/harness/__snapshots__/console-fences.ts.txt +++ b/tests/harness/__snapshots__/console-fences.ts.txt @@ -59,7 +59,7 @@ console-fences.ts:17:25 - error SC2020: 'console.dir with the 'showHidden' optio | ^~~~~~~~~~~~~~~~ 18 | - hint: the lowered options are depth (numeric literal or null), colors: false, compact: 3, breakLength: 80, and customInspect: false + hint: the lowered options are depth (numeric literal or null), colors: false, compact (false or a number), breakLength, sorted (true or false), and customInspect: false console-fences.ts:22:35 - error SC2020: 'console.assert with a format string and computed arguments' is part of the standard library types but has no scriptc lowering yet diff --git a/tests/harness/__snapshots__/dataview.ts.txt b/tests/harness/__snapshots__/dataview.ts.txt index f090511260..4f1401ca33 100644 --- a/tests/harness/__snapshots__/dataview.ts.txt +++ b/tests/harness/__snapshots__/dataview.ts.txt @@ -1,17 +1,8 @@ -dataview.ts:4:1 - error SC2020: 'DataView.setFloat16' is part of the standard library types but has no scriptc lowering yet +dataview.ts:7:16 - error SC2020: 'DataView.getFloat16' is part of the standard library types but has no scriptc lowering yet - 3 | const view = new DataView(buffer); - 4 | view.setFloat16(0, 1.5); - | ^~~~~~~~~~~~~~~ - 5 | console.log(view.getFloat16(0)); - - hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) - -dataview.ts:5:13 - error SC2020: 'DataView.getFloat16' is part of the standard library types but has no scriptc lowering yet - - 4 | view.setFloat16(0, 1.5); - 5 | console.log(view.getFloat16(0)); - | ^~~~~~~~~~~~~~~ - 6 | // End of the diagnostic fixture. + 6 | console.log(view.getFloat16(0)); + 7 | const getter = view.getFloat16; + | ^~~~~~~~~~~~~~~ + 8 | // End of the diagnostic fixture. hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) \ No newline at end of file diff --git a/tests/harness/__snapshots__/date-formatters.ts.txt b/tests/harness/__snapshots__/date-formatters.ts.txt index 8817031063..66d98d861f 100644 --- a/tests/harness/__snapshots__/date-formatters.ts.txt +++ b/tests/harness/__snapshots__/date-formatters.ts.txt @@ -5,7 +5,7 @@ date-formatters.ts:4:1 - error SC2020: 'Date.prototype.toString' is part of the | ^~~~~~~~~~~~~~~ 5 | date.toLocaleString(); - hint: getTime(), valueOf(), toISOString(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering + hint: getTime(), valueOf(), toISOString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering date-formatters.ts:5:1 - error SC2020: 'Date.prototype.toLocaleString' is part of the standard library types but has no scriptc lowering yet @@ -14,4 +14,4 @@ date-formatters.ts:5:1 - error SC2020: 'Date.prototype.toLocaleString' is part o | ^~~~~~~~~~~~~~~~~~~~~ 6 | - hint: getTime(), valueOf(), toISOString(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering \ No newline at end of file + hint: getTime(), valueOf(), toISOString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering \ No newline at end of file diff --git a/tests/harness/__snapshots__/deep-binary-nesting.ts.txt b/tests/harness/__snapshots__/deep-binary-nesting.ts.txt index 02cce3cdb9..9e645656fc 100644 --- a/tests/harness/__snapshots__/deep-binary-nesting.ts.txt +++ b/tests/harness/__snapshots__/deep-binary-nesting.ts.txt @@ -1,6 +1,6 @@ deep-binary-nesting.ts:2:9 - error SC1090: expressions nested deeper than 200 levels are not supported yet 1 | // A ~300-term left-nested binary chain crosses the expression-lowering depth floor (LOWER_EXPR_MAX_DEPTH): the named fence fires with ample stack headroom, where ~3000 terms used to overflow two recursive walks as an ICE (invariant signature 11). - 2 | let x = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1; - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 2 | let x = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + … + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 3 | console.log(x); \ No newline at end of file diff --git a/tests/harness/__snapshots__/deep-nesting-preflight.ts.txt b/tests/harness/__snapshots__/deep-nesting-preflight.ts.txt index 655f881cc4..c0eb7534fc 100644 --- a/tests/harness/__snapshots__/deep-nesting-preflight.ts.txt +++ b/tests/harness/__snapshots__/deep-nesting-preflight.ts.txt @@ -1,6 +1,6 @@ deep-nesting-preflight.ts:2:1 - error SC1090: expressions nested deeper than 200 levels are not supported yet - 1 | // A ~7000-term left-nested binary chain, the binderBinaryExpressionStress shape: deep enough that the whole-file walks (the checker facade prefetch sweep, the require scans, the nested-var collection) used to overflow the JS stack as a RangeError ICE before the expression-lowering fence could fire — the corpus pair crashed at ~6500 terms, and the bare statement sat within ~50 terms of the nested-var walk's limit. Every such walk is iterative now (walkPreorder), so the program survives to the same named SC1090 fence the ~300-term deep-binary-nesting fixture pins. A BARE expression statement (no initializer position) keeps tsgo's per-node query cost — and this suite — fast; one term per line keeps the snapshot code frame small. 5.9.3's parser (the resolver-parity island sweeps this directory) trampolines binary chains, so the shape parses in both worlds. + 1 | // A ~7000-term left-nested binary chain, the binderBinaryExpressionStress shape: deep enough that the whole-file walks (the checker facade prefetch sweep, the require scans, the nested-var collection) used to overflow the JS stack as a RangeError ICE before the expression-lowering fence could fire — the corpus pair crashed at ~6500 terms, and the bare statement sat within ~50 terms of the nested-… 2 | 1 + | ^~~ 3 | 1 + \ No newline at end of file diff --git a/tests/harness/__snapshots__/es2025-fences.ts.txt b/tests/harness/__snapshots__/es2025-fences.ts.txt index e1761f73a3..e2dc5f40f7 100644 --- a/tests/harness/__snapshots__/es2025-fences.ts.txt +++ b/tests/harness/__snapshots__/es2025-fences.ts.txt @@ -1,125 +1,89 @@ -es2025-fences.ts:9:12 - error SC2020: 'resizable ArrayBuffer construction' is part of the standard library types but has no scriptc lowering yet +es2025-fences.ts:8:12 - error SC2020: 'resizable ArrayBuffer construction' is part of the standard library types but has no scriptc lowering yet - 8 | // Resizable ArrayBuffer + transfer (ES2024). - 9 | const ab = new ArrayBuffer(8, { maxByteLength: 16 }); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 10 | ab.resize(16); + 7 | // Resizable ArrayBuffer + transfer (ES2024). + 8 | const ab = new ArrayBuffer(8, { maxByteLength: 16 }); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 9 | ab.resize(16); hint: fixed-length ArrayBuffers accept a byte length; resizable storage is not supported yet -es2025-fences.ts:15:27 - error SC2020: 'resizable ArrayBuffer construction' is part of the standard library types but has no scriptc lowering yet +es2025-fences.ts:14:27 - error SC2020: 'resizable ArrayBuffer construction' is part of the standard library types but has no scriptc lowering yet - 14 | // fixed-length new Uint8Array(new ArrayBuffer(8)) lowers). - 15 | const rz = new Uint8Array(new ArrayBuffer(8, { maxByteLength: 16 })); + 13 | // fixed-length new Uint8Array(new ArrayBuffer(8)) lowers). + 14 | const rz = new Uint8Array(new ArrayBuffer(8, { maxByteLength: 16 })); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 16 | + 15 | hint: fixed-length ArrayBuffers accept a byte length; resizable storage is not supported yet -es2025-fences.ts:18:13 - error SC2020: 'resizable ArrayBuffer construction' is part of the standard library types but has no scriptc lowering yet +es2025-fences.ts:17:13 - error SC2020: 'resizable ArrayBuffer construction' is part of the standard library types but has no scriptc lowering yet - 17 | // SharedArrayBuffer growth. - 18 | const sab = new SharedArrayBuffer(8, { maxByteLength: 16 }); + 16 | // SharedArrayBuffer growth. + 17 | const sab = new SharedArrayBuffer(8, { maxByteLength: 16 }); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 19 | sab.grow(16); + 18 | sab.grow(16); hint: fixed-length ArrayBuffers accept a byte length; resizable storage is not supported yet -es2025-fences.ts:23:7 - error SC2009: values of type '{ async: false; value: "not-equal" | "timed-out"; } | { async: true; value: Promise<"ok" | "timed-out">; }' cannot be compiled: the union shape is supported, but its arm '{ async: false; value: "not-equal" | "timed-out"; }' does not compile +es2025-fences.ts:22:7 - error SC2009: values of type '{ async: false; value: "not-equal" | "timed-out"; } | { async: true; value: Promise<"ok" | "timed-out">; }' cannot be compiled: the union shape is supported, but its arm '{ async: false; value: "not-equal" | "timed-out"; }' does not compile - 22 | const ia = new Int32Array(4); - 23 | const waited = Atomics.waitAsync(ia, 0, 0, 1); + 21 | const ia = new Int32Array(4); + 22 | const waited = Atomics.waitAsync(ia, 0, 0, 1); | ^~~~~~ - 24 | + 23 | hint: the outer shape is supported — the named component is the blocker; restate it inside the compilable set (number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown') -es2025-fences.ts:23:16 - error SC2020: 'Atomics.waitAsync' is part of the standard library types but has no scriptc lowering yet +es2025-fences.ts:22:16 - error SC2020: 'Atomics.waitAsync' is part of the standard library types but has no scriptc lowering yet - 22 | const ia = new Int32Array(4); - 23 | const waited = Atomics.waitAsync(ia, 0, 0, 1); + 21 | const ia = new Int32Array(4); + 22 | const waited = Atomics.waitAsync(ia, 0, 0, 1); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 24 | + 23 | hint: use integer load, store, exchange, compareExchange, add, sub, and, or, xor, wait or notify -es2025-fences.ts:26:7 - error SC2020: 'Float16Array' is part of the standard library types but has no scriptc lowering yet +es2025-fences.ts:25:7 - error SC2020: 'IteratorObject' is part of the standard library types but has no scriptc lowering yet - 25 | // Float16 (ES2025): the typed array, Math.f16round, DataView accessors. - 26 | const f16 = new Float16Array(4); - | ^~~ - 27 | console.log(Math.f16round(1.337)); - - hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) - -es2025-fences.ts:26:13 - error SC2020: 'new Float16Array' is part of the standard library types but has no scriptc lowering yet - - 25 | // Float16 (ES2025): the typed array, Math.f16round, DataView accessors. - 26 | const f16 = new Float16Array(4); - | ^~~~~~~~~~~~~~~~~~~ - 27 | console.log(Math.f16round(1.337)); - - hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) - -es2025-fences.ts:27:13 - error SC2020: 'Math.f16round' is part of the standard library types but has no scriptc lowering yet - - 26 | const f16 = new Float16Array(4); - 27 | console.log(Math.f16round(1.337)); - | ^~~~~~~~~~~~~ - 28 | const dv = new DataView(new Uint8Array(8).buffer); - - hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) - -es2025-fences.ts:29:1 - error SC2020: 'DataView.setFloat16' is part of the standard library types but has no scriptc lowering yet - - 28 | const dv = new DataView(new Uint8Array(8).buffer); - 29 | dv.setFloat16(0, 1.5); - | ^~~~~~~~~~~~~ - 30 | - - hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) - -es2025-fences.ts:32:7 - error SC2020: 'IteratorObject' is part of the standard library types but has no scriptc lowering yet - - 31 | // First-class iterator objects. - 32 | const it = Iterator.from([1, 2, 3].values()); + 24 | // First-class iterator objects. + 25 | const it = Iterator.from([1, 2, 3].values()); | ^~ - 33 | + 26 | hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) -es2025-fences.ts:32:12 - error SC2020: 'Iterator.from' is part of the standard library types but has no scriptc lowering yet +es2025-fences.ts:25:12 - error SC2020: 'Iterator.from' is part of the standard library types but has no scriptc lowering yet - 31 | // First-class iterator objects. - 32 | const it = Iterator.from([1, 2, 3].values()); + 24 | // First-class iterator objects. + 25 | const it = Iterator.from([1, 2, 3].values()); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 33 | + 26 | hint: first-class iterator objects have no lowering — iterator helpers compile as one chain on an array iterator, consumed in place: arr.values().map(f).take(n).toArray() -es2025-fences.ts:35:27 - error SC2020: 'Promise.try over a callback of type '(a: number, b?: number | undefined) => number' with 1 argument' is part of the standard library types but has no scriptc lowering yet +es2025-fences.ts:28:27 - error SC2020: 'Promise.try over a callback of type '(a: number, b?: number | undefined) => number' with 1 argument' is part of the standard library types but has no scriptc lowering yet - 34 | // Promise.try with fewer arguments than the callback's parameters. - 35 | const tried = Promise.try((a: number, b?: number) => a + (b ?? 0), 1); + 27 | // Promise.try with fewer arguments than the callback's parameters. + 28 | const tried = Promise.try((a: number, b?: number) => a + (b ?? 0), 1); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 36 | + 29 | hint: pass one argument per callback parameter: Promise.try((a, b) => a + b, 1, 2) -es2025-fences.ts:38:17 - error SC2020: 'Object.groupBy at this result type' is part of the standard library types but has no scriptc lowering yet +es2025-fences.ts:31:17 - error SC2020: 'Object.groupBy at this result type' is part of the standard library types but has no scriptc lowering yet - 37 | // Object.groupBy at a literal-union key type (a fixed-field record). - 38 | const grouped = Object.groupBy([1, 2, 3], (n) => (n % 2 === 0 ? "even" : "odd")); + 30 | // Object.groupBy at a literal-union key type (a fixed-field record). + 31 | const grouped = Object.groupBy([1, 2, 3], (n) => (n % 2 === 0 ? "even" : "odd")); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 39 | + 32 | hint: a string-keyed result (Partial>) is the supported form — a literal-union key type makes a fixed-field record: widen the callback's key type to string (numbers stringify like JS property keys) -es2025-fences.ts:43:19 - error SC2020: 'Set.prototype.union over a set-like argument of type '{ size: number; has: (x: number) => x is 1; keys: () => ArrayIterator; }'' is part of the standard library types but has no scriptc lowering yet +es2025-fences.ts:36:19 - error SC2020: 'Set.prototype.union over a set-like argument of type '{ size: number; has: (x: number) => x is 1; keys: () => ArrayIterator; }'' is part of the standard library types but has no scriptc lowering yet - 42 | const like = { size: 1, has: (x: number) => x === 1, keys: () => [1].values() }; - 43 | const u = s.union(like); + 35 | const like = { size: 1, has: (x: number) => x === 1, keys: () => [1].values() }; + 36 | const u = s.union(like); | ^~~~ - 44 | + 37 | hint: only a real Set or Map lowers (the argument's has/size/keys must be the builtins) — build one first: new Set(...) \ No newline at end of file diff --git a/tests/harness/__snapshots__/index-signature-values.ts.txt b/tests/harness/__snapshots__/index-signature-values.ts.txt index 9774ab9ae1..d1e9395ed4 100644 --- a/tests/harness/__snapshots__/index-signature-values.ts.txt +++ b/tests/harness/__snapshots__/index-signature-values.ts.txt @@ -16,7 +16,7 @@ index-signature-values.ts:9:13 - error SC2004: uses of 'bySymbol' inherit the bl hint: the declaration of 'bySymbol' did not compile — fix the diagnostic reported there and these sites clear with it -index-signature-values.ts:13:13 - error SC1090: console.log of '{ [key: string]: Uint8Array }' values (typed arrays inside composites have no inspect lowering yet (Buffer's form vs Uint8Array's is not recorded in the static type here)) are not supported yet +index-signature-values.ts:13:13 - error SC1090: console.log of '{ [key: string]: Uint8Array }' values (Uint8Array and Buffer values inside composites have no inspect lowering yet (Buffer's form vs Uint8Array's is not recorded in the static type here)) are not supported yet 12 | const buffers: Record = {}; 13 | console.log(buffers); diff --git a/tests/harness/__snapshots__/mock-timers-fences.ts.txt b/tests/harness/__snapshots__/mock-timers-fences.ts.txt new file mode 100644 index 0000000000..dba7de64c6 --- /dev/null +++ b/tests/harness/__snapshots__/mock-timers-fences.ts.txt @@ -0,0 +1,44 @@ +mock-timers-fences.ts:20:22 - error SC2020: 'mock.timers.enable with a non-literal apis list' is part of the standard library types but has no scriptc lowering yet + + 19 | const apis: ("setTimeout" | "setInterval")[] = ["setTimeout"]; + 20 | mock.timers.enable({ apis }); // apis list that is not a literal + | ^~~~ + 21 | + + hint: list the mocked APIs as string literals: enable({ apis: ['setTimeout', 'Date'] }) + +mock-timers-fences.ts:23:20 - error SC2020: 'mock.timers.enable with a non-literal options object' is part of the standard library types but has no scriptc lowering yet + + 22 | const options = { apis: ["setTimeout"] as const }; + 23 | mock.timers.enable(options); // options object that is not a literal + | ^~~~~~~ + 24 | + + hint: list the mocked APIs as string literals: enable({ apis: ['setTimeout', 'Date'] }) + +mock-timers-fences.ts:25:7 - error SC2020: 'MockTimers' is part of the standard library types but has no scriptc lowering yet + + 24 | + 25 | const timers = mock.timers; // the object as a value + | ^~~~~~ + 26 | timers.tick(1); + + hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) + +mock-timers-fences.ts:25:16 - error SC2020: 'MockTracker.timers' is part of the standard library types but has no scriptc lowering yet + + 24 | + 25 | const timers = mock.timers; // the object as a value + | ^~~~~~~~~~~ + 26 | timers.tick(1); + + hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) + +mock-timers-fences.ts:26:1 - error SC2020: 'MockTimers.tick' is part of the standard library types but has no scriptc lowering yet + + 25 | const timers = mock.timers; // the object as a value + 26 | timers.tick(1); + | ^~~~~~~~~~~ + 27 | + + hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) \ No newline at end of file diff --git a/tests/harness/__snapshots__/node-types-fenced.txt b/tests/harness/__snapshots__/node-types-fenced.txt index dca88df394..29e4a2ee9c 100644 --- a/tests/harness/__snapshots__/node-types-fenced.txt +++ b/tests/harness/__snapshots__/node-types-fenced.txt @@ -48,22 +48,22 @@ fenced.ts:38:1 - error SC2020: 'fs.watchFile' is typed by @types/node but has no 37 | import { win32 } from "path"; 38 | watchFile("x", () => {}); | ^~~~~~~~~~~~~~~~~~~~~~~~ - 39 | setPriority(0); + 39 | on(new EventEmitter(), "tick"); hint: the type checker sees everything @types/node declares, but only the supported surface compiles (https://scriptc.dev/limitations) -fenced.ts:39:1 - error SC2020: 'os.setPriority' is typed by @types/node but has no scriptc lowering yet +fenced.ts:39:1 - error SC2020: 'events.on' is typed by @types/node but has no scriptc lowering yet 38 | watchFile("x", () => {}); - 39 | setPriority(0); - | ^~~~~~~~~~~~~~ + 39 | on(new EventEmitter(), "tick"); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 40 | console.log(win32.sep); hint: the type checker sees everything @types/node declares, but only the supported surface compiles (https://scriptc.dev/limitations) fenced.ts:40:13 - error SC2020: 'PlatformPath.sep' is typed by @types/node but has no scriptc lowering yet - 39 | setPriority(0); + 39 | on(new EventEmitter(), "tick"); 40 | console.log(win32.sep); | ^~~~~~~~~ 41 | /* URL getters and serialization lower under @types/node, but object-URL diff --git a/tests/harness/__snapshots__/regex-constructor-flags.ts.txt b/tests/harness/__snapshots__/regex-constructor-flags.ts.txt index 74aa9ab94e..e3aaeb1d96 100644 --- a/tests/harness/__snapshots__/regex-constructor-flags.ts.txt +++ b/tests/harness/__snapshots__/regex-constructor-flags.ts.txt @@ -1,17 +1,8 @@ -regex-constructor-flags.ts:3:39 - error SC1120: the regex 'v' flag (unicode sets) is not supported yet +regex-constructor-flags.ts:3:35 - error SC1120: the regex 'd' flag (match indices) is not supported yet - 2 | // forms; computed flags reach the runtime, which names the same gap. - 3 | const sets = new RegExp("[\\w--\\d]", "v"); - | ^~~ - 4 | const indices = new RegExp("cat", "dg"); - - hint: supported: literal regexes with the g/i/m/s/u/y flags — .test(), .exec()/.match()/.matchAll(), named capture groups (.groups, $ templates, \k), .source/.flags, and string replace/replaceAll/split with string replacement templates - -regex-constructor-flags.ts:4:35 - error SC1120: the regex 'd' flag (match indices) is not supported yet - - 3 | const sets = new RegExp("[\\w--\\d]", "v"); - 4 | const indices = new RegExp("cat", "dg"); + 2 | // computed flags reach the runtime, which names the same gap. + 3 | const indices = new RegExp("cat", "dg"); | ^~~~ - 5 | + 4 | - hint: supported: literal regexes with the g/i/m/s/u/y flags — .test(), .exec()/.match()/.matchAll(), named capture groups (.groups, $ templates, \k), .source/.flags, and string replace/replaceAll/split with string replacement templates \ No newline at end of file + hint: supported: literal regexes with the g/i/m/s/u/v/y flags — .test(), .exec()/.match()/.matchAll(), named capture groups (.groups, $ templates, \k), .source/.flags, and string replace/replaceAll/split with string replacement templates \ No newline at end of file diff --git a/tests/harness/__snapshots__/regex.ts.txt b/tests/harness/__snapshots__/regex.ts.txt index 2db17d9c8b..22bdbef90f 100644 --- a/tests/harness/__snapshots__/regex.ts.txt +++ b/tests/harness/__snapshots__/regex.ts.txt @@ -3,31 +3,22 @@ regex.ts:4:17 - error SC1120: the regex 'd' flag (match indices) is not supporte 3 | // Method values and regex union arms remain unsupported; arrays compile. 4 | const indices = /cat/d; | ^~~~~~ - 5 | const sets = /[\p{L}]/v; + 5 | const asValue = /x/.test; - hint: supported: literal regexes with the g/i/m/s/u/y flags — .test(), .exec()/.match()/.matchAll(), named capture groups (.groups, $ templates, \k), .source/.flags, and string replace/replaceAll/split with string replacement templates + hint: supported: literal regexes with the g/i/m/s/u/v/y flags — .test(), .exec()/.match()/.matchAll(), named capture groups (.groups, $ templates, \k), .source/.flags, and string replace/replaceAll/split with string replacement templates -regex.ts:5:14 - error SC1120: the regex 'v' flag (unicode sets) is not supported yet +regex.ts:5:17 - error SC1090: regex methods as values (call 'test' directly) are not supported yet 4 | const indices = /cat/d; - 5 | const sets = /[\p{L}]/v; - | ^~~~~~~~~~ - 6 | const asValue = /x/.test; - - hint: supported: literal regexes with the g/i/m/s/u/y flags — .test(), .exec()/.match()/.matchAll(), named capture groups (.groups, $ templates, \k), .source/.flags, and string replace/replaceAll/split with string replacement templates - -regex.ts:6:17 - error SC1090: regex methods as values (call 'test' directly) are not supported yet - - 5 | const sets = /[\p{L}]/v; - 6 | const asValue = /x/.test; + 5 | const asValue = /x/.test; | ^~~~~~~~ - 7 | const maybe: RegExp | undefined = /a/; + 6 | const maybe: RegExp | undefined = /a/; -regex.ts:10:22 - error SC2020: 'RegExpExecArray.groups' is part of the standard library types but has no scriptc lowering yet +regex.ts:9:22 - error SC2020: 'RegExpExecArray.groups' is part of the standard library types but has no scriptc lowering yet - 9 | const m = re.exec("2024-07"); - 10 | if (m) console.log(m.groups); + 8 | const m = re.exec("2024-07"); + 9 | if (m) console.log(m.groups); | ^~~~~~~~ - 11 | } + 10 | } hint: '.groups' lowers when the match's regex is STATICALLY known — a regex literal, or a const initialized with one (the group-name table is built at compile time) — and the read is not an optional-chain step: narrow the match instead (if (m) { m.groups } or m!.groups) \ No newline at end of file diff --git a/tests/harness/__snapshots__/snapshot-fences.ts.txt b/tests/harness/__snapshots__/snapshot-fences.ts.txt new file mode 100644 index 0000000000..3f71fd7102 --- /dev/null +++ b/tests/harness/__snapshots__/snapshot-fences.ts.txt @@ -0,0 +1,26 @@ +snapshot-fences.ts:14:24 - error SC2020: 't.assert.snapshot options that are not an object literal' is part of the standard library types but has no scriptc lowering yet + + 13 | const options = { serializers: [(v: unknown) => String(v)] }; + 14 | t.assert.snapshot(1, options); // options that are not an object literal + | ^~~~~~~ + 15 | + + hint: pass the options as an object literal: { serializers: [(value) => ...] } + +snapshot-fences.ts:17:26 - error SC2020: 't.assert.snapshot with a non-literal serializers list' is part of the standard library types but has no scriptc lowering yet + + 16 | const serializers = [(v: unknown) => String(v)]; + 17 | t.assert.snapshot(1, { serializers }); // a serializers list that is not an array literal + | ^~~~~~~~~~~ + 18 | t.assert.fileSnapshot(1, "out.txt", { serializers: [...serializers] }); // a spread serializer + + hint: pass the options as an object literal: { serializers: [(value) => ...] } + +snapshot-fences.ts:18:55 - error SC2020: 't.assert.fileSnapshot with a spread serializer' is part of the standard library types but has no scriptc lowering yet + + 17 | t.assert.snapshot(1, { serializers }); // a serializers list that is not an array literal + 18 | t.assert.fileSnapshot(1, "out.txt", { serializers: [...serializers] }); // a spread serializer + | ^~~~~~~~~~~~~~ + 19 | }); + + hint: pass the options as an object literal: { serializers: [(value) => ...] } \ No newline at end of file diff --git a/tests/harness/__snapshots__/stdlib-fence.ts.txt b/tests/harness/__snapshots__/stdlib-fence.ts.txt index 77b0e05d85..337354eec7 100644 --- a/tests/harness/__snapshots__/stdlib-fence.ts.txt +++ b/tests/harness/__snapshots__/stdlib-fence.ts.txt @@ -66,7 +66,7 @@ stdlib-fence.ts:25:1 - error SC2020: 'Date.prototype.setUTCFullYear with argumen | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 26 | const sameDate = mutableDate === mutableDate; // scalar storage must not fake object identity - hint: getTime(), valueOf(), toISOString(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering + hint: getTime(), valueOf(), toISOString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering stdlib-fence.ts:26:18 - error SC1043: comparing non-number, non-string values are not supported yet diff --git a/tests/harness/__snapshots__/utility-types.ts.txt b/tests/harness/__snapshots__/utility-types.ts.txt index 371215a65d..71f4d3083e 100644 --- a/tests/harness/__snapshots__/utility-types.ts.txt +++ b/tests/harness/__snapshots__/utility-types.ts.txt @@ -32,20 +32,4 @@ utility-types.ts:19:14 - error SC2004: uses of 'dateBag' inherit the blocker on | ^~~~~~~ 20 | console.log(boot); - hint: the declaration of 'dateBag' did not compile — fix the diagnostic reported there and these sites clear with it - -utility-types.ts:20:13 - error SC1090: console.log of 'Date' values ('Date' values have no inspect lowering) are not supported yet - - 19 | const boot = dateBag["boot"]; - 20 | console.log(boot); - | ^~~~ - 21 | - -utility-types.ts:22:33 - error SC1101: converting typed values to 'unknown' is not supported yet - - 21 | - 22 | const stamped: Readonly = new Date(0); - | ^~~~~~~~~~~ - 23 | console.log(stamped.getTime()); - - hint: numbers, strings, booleans, JSON-safe records/arrays/unions (a deep copy), functions, program class instances, and supported native handles convert into 'unknown' slots; functions, exact-class round trips, and handles preserve identity, while this value's type has no dynamic representation yet \ No newline at end of file + hint: the declaration of 'dateBag' did not compile — fix the diagnostic reported there and these sites clear with it \ No newline at end of file diff --git a/tests/harness/closure-environment-scaling.test.ts b/tests/harness/closure-environment-scaling.test.ts new file mode 100644 index 0000000000..785f073492 --- /dev/null +++ b/tests/harness/closure-environment-scaling.test.ts @@ -0,0 +1,127 @@ +/* Compile time stays linear in the size of a closure environment. A factory + * whose methods share thousands of captured bindings (the shape of Bun's + * large-environment AOT program) used to take minutes: capture analyses + * walked the whole body once per binding, and LLVM received every box + * pointer as a value live across the entire function. Behavior of such a + * program is pinned by corpus 5973; this test bounds how compile time grows + * between two generated sizes. */ +import { execFile } from "node:child_process"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { expect, test } from "vitest"; + +const execFileAsync = promisify(execFile); +const repoRoot = join(import.meta.dirname, "../.."); +// Sanitized builds compile the runtime with each uncached build, which +// dwarfs both sizes; the growth this test bounds is the same in every lane. +const sanitize = process.env["SCRIPTC_SAN"] === "1"; + +/** `n` number bindings plus n/20 objects and strings, all captured by a + * reader and a writer method, and the output the program prints. */ +function program(n: number): { source: string; expected: string } { + const lines = [ + `function total(list: number[]): number {`, + ` let t = 0;`, + ` for (const v of list) t += v;`, + ` return t;`, + `}`, + `const one = Number("1");`, + `function make() {`, + ]; + const reads: string[] = []; + const writes: string[] = []; + let sum = 0; + let bumped = 0; + for (let i = 0; i < n; i++) { + lines.push(` let n${i} = ${i} + 0.5;`); + reads.push(`n${i}`); + writes.push(`n${i} += 1;`); + sum += i + 0.5; + bumped += i + 1.5; + } + for (let i = n; i < n + n / 10; i++) { + if (i % 2 === 0) { + lines.push(` let o${i} = { x: ${i} };`); + reads.push(`o${i}.x`); + writes.push(`o${i} = { x: o${i}.x + 1 };`); + sum += i; + bumped += i + 1; + } else { + lines.push(` let s${i} = "s" + (${i} + one);`); + reads.push(`s${i}.length`); + writes.push(`s${i} += "!";`); + const length = `s${i + 1}`.length; + sum += length; + bumped += length + 1; + } + } + lines.push( + ` return {`, + ` sum(): number {`, + ` return total([${reads.join(", ")}]);`, + ` },`, + ` bump(): void {`, + ...writes.map((write) => ` ${write}`), + ` },`, + ` };`, + `}`, + `const a = make();`, + `const b = make();`, + `a.bump();`, + `console.log(a.sum(), b.sum());`, + ); + return { source: lines.join("\n") + "\n", expected: `${bumped} ${sum}\n` }; +} + +/** Milliseconds from the end of the frontend load to the finished + * executable (SCRIPTC_TIMING phases): lowering, IR passes, LLVM emission, + * native code generation and the link, without process and tsgo startup. */ +async function compileTime(dir: string, n: number): Promise { + const { source, expected } = program(n); + const entry = join(dir, `env${n}.ts`); + const binary = join(dir, `env${n}`); + writeFileSync(entry, source); + const { stderr } = await execFileAsync( + process.execPath, + [join(repoRoot, "packages/cli/dist/main.js"), "build", entry, "-o", binary], + { + env: { ...process.env, SCRIPTC_TIMING: "1", SCRIPTC_NO_CACHE: "1" }, + maxBuffer: 16 * 1024 * 1024, + }, + ); + const phases = new Map(); + for (const line of stderr.split("\n")) { + if (!line.startsWith("scriptc timing ")) continue; + const { phase, total_ms } = JSON.parse(line.slice("scriptc timing ".length)) as { + phase: string; + total_ms: number; + }; + phases.set(phase, total_ms); + } + const loaded = phases.get("frontend-load"); + const complete = phases.get("complete"); + if (loaded === undefined || complete === undefined) throw new Error(stderr); + const run = await execFileAsync(binary, [], { encoding: "utf8" }); + expect(run.stdout).toBe(expected); + return complete - loaded; +} + +test.skipIf(sanitize)( + "compile time grows linearly with a shared closure environment", + async () => { + const dir = mkdtempSync(join(tmpdir(), "scriptc-closure-scaling-")); + try { + const small = await compileTime(dir, 500); + const large = await compileTime(dir, 4000); + // Eight times the bindings. Linear growth stays at or below 8x (fixed + // costs keep it near 5x); the quadratic analyses and code generation + // measured 24x at these sizes, and every doubling beyond adds 4x. + expect(large / small, `${small.toFixed(0)} ms -> ${large.toFixed(0)} ms`).toBeLessThan(16); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }, + 300_000, +); diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index ecd7f5d9b5..4acd0928d4 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -1834,6 +1834,7 @@ "5566-fs-dirent-dyn-boundaries/main.mjs": 2607, "5585-record-results-inlined.ts": 2922, "5595-net-ref-unref.ts": 2593, + "5627-exec-callback.ts": 1900, "5644-net-socket-addresses.ts": 2220, "5645-net-socket-callbacks.ts": 2442, "5646-net-server-connections.ts": 2389, @@ -1841,12 +1842,87 @@ "5648-dns-lookup-forms.ts": 2992, "5649-dns-service-servers.ts": 2712, "5650-dns-lookup-errors-untyped.cjs": 2822, + "5681-array-pop-and-length.ts": 3754, + "5800-broadcast-channel.ts": 2899, + "5801-broadcast-channel.cjs": 2727, + "5802-broadcast-channel-workers.ts": 2809, + "5803-worker-environment-data.ts": 2748, + "5804-worker-environment-data.cjs": 2469, + "5805-worker-receive-message-parent-port.ts": 2839, + "5806-worker-resource-limits.ts": 3153, + "5807-worker-error-values.ts": 3304, + "5808-worker-unhandled-rejections.ts": 3430, + "5809-process-exit-and-rejections.cjs": 3294, + "5810-mock-method-records.ts": 3976, + "5811-mock-tracker-restore.ts": 3698, + "5812-dyn-box-async-fields.ts": 3522, + "5813-mock-timers-module.ts": 3766, + "5814-mock-timers-date.ts": 3760, + "5831-util-types-brands.ts": 3347, + "5832-util-types-brands-dynamic.js": 3250, + "5833-url-search-params-iterators.ts": 2678, + "5834-url-search-params-iterators-dynamic.mjs": 2696, + "5835-util-format-integer-specifiers.ts": 2534, + "5836-url-search-params-tuple-pairs.ts": 2118, + "5837-util-format-json-circular.ts": 3117, + "5838-qs-stringify-bigint.ts": 2207, + "5841-json-typed-dyn-members.ts": 5467, + "5842-qs-parse-null-prototype.ts": 1774, + "5843-qs-parse-null-prototype-js.mjs": 1559, + "5844-date-unknown-values.ts": 3969, + "5846-events-esm-default-max.ts": 2633, + "5847-events-cjs-default-max.cjs": 2823, + "5857-regexp-unicode-sets.ts": 3350, + "5858-disposable-stack.ts": 3050, + "5859-disposable-stack-js.js": 3100, + "5870-worker-thread-names.ts": 3106, + "5871-worker-untransferable.cjs": 2786, + "5874-worker-stdio-streams.ts": 2839, + "5875-worker-stdout-pipe.cjs": 2740, + "5879-worker-stdin.ts": 3158, + "5881-inspect-weak-collections.ts": 2615, + "5882-string-decoder-runtime-encoding.ts": 2541, + "5883-os-set-priority.ts": 2380, + "5884-inspect-layout-options.ts": 3108, + "5886-inspect-dates.ts": 2559, + "5887-inspect-typed-arrays.ts": 3448, + "5888-inspect-sorted.ts": 2998, + "5900-float16-array.ts": 3000, + "5901-float16-array-js.js": 2900, + "5904-async-generator-completion.ts": 2950, + "5905-array-from-async-generator.ts": 2750, + "5906-array-from-async-generator-js.js": 2950, + "5910-set-prototype-of-primitive-object-kinds.ts": 2719, + "5911-dyn-class-to-primitive.ts": 2850, + "5912-dyn-class-conversion-methods.ts": 3251, + "5913-prototype-type-error-messages.ts": 2484, + "5914-dyn-in-class-prototype-members.ts": 3169, + "5915-dyn-error-string-conversions.ts": 3200, + "5922-matchall-groups-callbacks.ts": 3650, + "5923-matchall-groups-js.js": 3970, + "5938-object-literal-computed-keys-keep-methods.ts": 3195, + "5939-js-let-object-reassigned-wider.js": 1945, + "5942-local-class-observed-methods.ts": 5063, + "5943-fields-read-before-assignment.ts": 6856, + "5944-js-fields-read-before-assignment.js": 4247, + "5950-tuple-predicate-methods.ts": 5990, + "5951-result-never-widening.ts": 4710, + "5955-any-promise-methods.js": 3940, + "5956-erased-generic-members.ts": 6730, + "5958-asserted-union-receiver.ts": 2430, + "5959-date-to-json.ts": 3330, + "5960-generic-mapped-types.ts": 2540, "5970-net-connect-host-name.ts": 2400, "5971-buffer-chunks-to-string.ts": 2700, "5972-net-set-encoding-listeners.ts": 2550, + "5974-typeof-module-exports.cjs": 2000, + "5990-worker-uncloneable.mjs": 3283, + "5991-worker-async-dispose.mjs": 5699, + "5992-worker-thread-name-lifetime.mjs": 2687, "600-closures-basic.ts": 2349, "601-closures-loops.ts": 1788, "602-closures-identity-recursion.ts": 2372, + "6020-class-extends-function-constructor.js": 4300, "603-closures-rc-stress.ts": 1832, "604-closures-for-of.ts": 2639, "605-closures-forward-capture-tdz.ts": 3277, @@ -2356,6 +2432,7 @@ "native-worker-clone-errors.ts": 2347, "native-worker-commonjs.cjs": 2188, "native-worker-delayed-messages.ts": 3114, + "native-worker-discarded-heaps.ts": 4430, "native-worker-environment.ts": 3538, "native-worker-error.ts": 2810, "native-worker-exception-state.ts": 3938, @@ -4461,6 +4538,7 @@ "5566-fs-dirent-dyn-boundaries/main.mjs": 3333, "5585-record-results-inlined.ts": 3681, "5595-net-ref-unref.ts": 3036, + "5627-exec-callback.ts": 2500, "5644-net-socket-addresses.ts": 2393, "5645-net-socket-callbacks.ts": 2060, "5646-net-server-connections.ts": 2755, @@ -4468,12 +4546,87 @@ "5648-dns-lookup-forms.ts": 3416, "5649-dns-service-servers.ts": 4916, "5650-dns-lookup-errors-untyped.cjs": 3889, + "5681-array-pop-and-length.ts": 3216, + "5800-broadcast-channel.ts": 28374, + "5801-broadcast-channel.cjs": 27251, + "5802-broadcast-channel-workers.ts": 27251, + "5803-worker-environment-data.ts": 29298, + "5804-worker-environment-data.cjs": 26404, + "5805-worker-receive-message-parent-port.ts": 30268, + "5806-worker-resource-limits.ts": 29711, + "5807-worker-error-values.ts": 35248, + "5808-worker-unhandled-rejections.ts": 33209, + "5809-process-exit-and-rejections.cjs": 30261, + "5810-mock-method-records.ts": 6363, + "5811-mock-tracker-restore.ts": 4503, + "5812-dyn-box-async-fields.ts": 5278, + "5813-mock-timers-module.ts": 4227, + "5814-mock-timers-date.ts": 17708, + "5831-util-types-brands.ts": 4380, + "5832-util-types-brands-dynamic.js": 3658, + "5833-url-search-params-iterators.ts": 5339, + "5834-url-search-params-iterators-dynamic.mjs": 3797, + "5835-util-format-integer-specifiers.ts": 2768, + "5836-url-search-params-tuple-pairs.ts": 3193, + "5837-util-format-json-circular.ts": 3408, + "5838-qs-stringify-bigint.ts": 3519, + "5841-json-typed-dyn-members.ts": 3238, + "5842-qs-parse-null-prototype.ts": 7077, + "5843-qs-parse-null-prototype-js.mjs": 5395, + "5844-date-unknown-values.ts": 4543, + "5846-events-esm-default-max.ts": 3102, + "5847-events-cjs-default-max.cjs": 3039, + "5857-regexp-unicode-sets.ts": 3900, + "5858-disposable-stack.ts": 3750, + "5859-disposable-stack-js.js": 3600, + "5870-worker-thread-names.ts": 31697, + "5871-worker-untransferable.cjs": 27481, + "5874-worker-stdio-streams.ts": 27801, + "5875-worker-stdout-pipe.cjs": 31391, + "5879-worker-stdin.ts": 33232, + "5881-inspect-weak-collections.ts": 2915, + "5882-string-decoder-runtime-encoding.ts": 3854, + "5883-os-set-priority.ts": 3417, + "5884-inspect-layout-options.ts": 3596, + "5886-inspect-dates.ts": 2736, + "5887-inspect-typed-arrays.ts": 2997, + "5888-inspect-sorted.ts": 2861, + "5900-float16-array.ts": 3750, + "5901-float16-array-js.js": 3700, + "5904-async-generator-completion.ts": 3800, + "5905-array-from-async-generator.ts": 3500, + "5906-array-from-async-generator-js.js": 3750, + "5910-set-prototype-of-primitive-object-kinds.ts": 5033, + "5911-dyn-class-to-primitive.ts": 4732, + "5912-dyn-class-conversion-methods.ts": 4424, + "5913-prototype-type-error-messages.ts": 4068, + "5914-dyn-in-class-prototype-members.ts": 4327, + "5915-dyn-error-string-conversions.ts": 4247, + "5922-matchall-groups-callbacks.ts": 5960, + "5923-matchall-groups-js.js": 6650, + "5938-object-literal-computed-keys-keep-methods.ts": 7194, + "5939-js-let-object-reassigned-wider.js": 4436, + "5942-local-class-observed-methods.ts": 6212, + "5943-fields-read-before-assignment.ts": 72293, + "5944-js-fields-read-before-assignment.js": 40310, + "5950-tuple-predicate-methods.ts": 7790, + "5951-result-never-widening.ts": 6120, + "5955-any-promise-methods.js": 5120, + "5956-erased-generic-members.ts": 8750, + "5958-asserted-union-receiver.ts": 3160, + "5959-date-to-json.ts": 4330, + "5960-generic-mapped-types.ts": 3300, "5970-net-connect-host-name.ts": 2800, "5971-buffer-chunks-to-string.ts": 3100, "5972-net-set-encoding-listeners.ts": 3000, + "5974-typeof-module-exports.cjs": 2500, + "5990-worker-uncloneable.mjs": 22432, + "5991-worker-async-dispose.mjs": 25768, + "5992-worker-thread-name-lifetime.mjs": 28932, "600-closures-basic.ts": 2534, "601-closures-loops.ts": 2297, "602-closures-identity-recursion.ts": 2427, + "6020-class-extends-function-constructor.js": 7500, "603-closures-rc-stress.ts": 2589, "604-closures-for-of.ts": 3514, "605-closures-forward-capture-tdz.ts": 4638, @@ -4983,6 +5136,7 @@ "native-worker-clone-errors.ts": 3629, "native-worker-commonjs.cjs": 3085, "native-worker-delayed-messages.ts": 28752, + "native-worker-discarded-heaps.ts": 39440, "native-worker-environment.ts": 27133, "native-worker-error.ts": 2958, "native-worker-exception-state.ts": 3945, diff --git a/tests/harness/differential.test.ts b/tests/harness/differential.test.ts index fbf9cb1ec4..a2206fd97c 100644 --- a/tests/harness/differential.test.ts +++ b/tests/harness/differential.test.ts @@ -22,6 +22,7 @@ import { describe, expect, test } from "vitest"; import ts5 from "typescript"; import { compile } from "@scriptc/compiler"; import { corpusSelect, shardSuffix } from "./shard.js"; +import { oracleEnvKey } from "./oracle-env.js"; const execFileAsync = promisify(execFile); const repoRoot = join(import.meta.dirname, "../.."); @@ -238,8 +239,8 @@ function programInputs(file: string): string[] { // Node's verdict for a corpus program is a pure function of the program bytes, // the shims, and the Node build (corpus stdout is deterministic by // construction — it must be identical to a non-Node native binary's output). So -// cache it, keyed by all of those plus the invocation shape (SCRIPTC_TEST_ENV -// and the cwd). Only the SPAWN is skipped: the native side always runs live +// cache it, keyed by all of those plus the invocation shape (the inherited +// environment, see oracle-env.ts, and the cwd). Only the SPAWN is skipped: the native side always runs live // and the comparison itself never changes. SCRIPTC_NO_CACHE=1 (or an unset // SCRIPTC_CACHE_DIR) disables the cache in both directions — no reads, no writes. // Storage shares the compile cache's root and its LRU sweep (see native-toolchain.ts). @@ -265,7 +266,7 @@ function oracleKeyBase(): Promise { .update(readFileSync(fileURLToPath(comptimeShim))) .update("\0") .update("\0") - .update(process.env["SCRIPTC_TEST_ENV"] ?? "") + .update(oracleEnvKey(process.env)) .update("\0") .update(process.cwd()) .update("\0") diff --git a/tests/harness/dyncheck.test.ts b/tests/harness/dyncheck.test.ts index ca597c0db6..2ab323f370 100644 --- a/tests/harness/dyncheck.test.ts +++ b/tests/harness/dyncheck.test.ts @@ -764,6 +764,38 @@ console.log("recovered"); expect(r.stdout).toBe("TypeError true\narray: TypeError\nfunc: TypeError\nrecovered\n"); }); + test("a non-null assertion on a null object result throws at the assertion, even when the binding is never read", async () => { + // Node lets the null ride until a member read; the compiled program + // throws the stranded TypeError where `!` extracts the object. + const r = await compileAndRun( + "asserted-null-object-result", + `interface Info { + name: string; + base: Info | null; + callable: boolean; +} +function baseOf(info: Info): Info | null { + return info.base; +} +function describe(info: Info): string { + const base = baseOf(info)!; + if (info.callable) return info.name + ": callable base"; + return info.name + " extends " + base.name; +} +const root: Info = { name: "Root", base: null, callable: false }; +console.log(describe({ name: "Child", base: root, callable: false })); +try { + console.log("unreachable", describe({ name: "Wrapped", base: null, callable: true })); +} catch (e) { + console.log((e as Error).name, (e as Error).message.includes("not representable in the target union")); +} +console.log("recovered"); +`, + ); + expect(r.exitCode).toBe(0); + expect(r.stdout).toBe("Child extends Root\nTypeError true\nrecovered\n"); + }); + test("a checker-approved function mismatch strands per piece: mismatched params compile at the flow, void results trap after the call", async () => { // funcCoerceAdapter's stranded dispositions. Param strand: an array // literal of arrows collapses to one element signature — the diff --git a/tests/harness/library-mode.test.ts b/tests/harness/library-mode.test.ts index 3825cb3f43..0eb0cf2c34 100644 --- a/tests/harness/library-mode.test.ts +++ b/tests/harness/library-mode.test.ts @@ -299,6 +299,51 @@ wrap empty: len 2 bytes 60 62 expect(run.stdout).toBe(BUFFERS_EXPECTED); }); + /* ── cstring: NUL-terminated text in, out, and through callbacks ─────── */ + + test("cstring parameters, results, and callback arguments", async () => { + const { archive, outDir } = await buildLibrary("cstrings", emission); + const probe = buildProbe("cstrings", archive, outDir); + const run = runProbe(probe, ["ok"]); + expect(run.signal).toBeNull(); + expect(run.status).toBe(0); + expect(run.stdout).toBe(`register: 0 +greet: hello, w\u00f6rld! (strlen 14) +length ascii: 3 +length utf8: 4 +length malformed: 3 +length empty: 0 +log: QUIET (strlen 5, n 5) +echo: 1 +done +`); + }); + + test.each([ + ["nullarg", "scriptc: library inbound cstring parameter is NULL\n", "SC4012", "kc_length"], + [ + "nulresult", + "scriptc: TypeError: a cstring value contains U+0000, which a C string cannot hold\n", + "SC4015", + "kc_with_nul", + ], + [ + "nularg", + "scriptc: TypeError: a cstring value contains U+0000, which a C string cannot hold\n", + "SC4015", + "kc_echo_nul", + ], + ])("cstring %s traps through the sink", async (mode, text, code, symbol) => { + const { archive, outDir } = await buildLibrary("cstrings", emission); + const probe = buildProbe("cstrings", archive, outDir); + const run = runProbe(probe, [mode]); + expect(run.signal).toBeNull(); + expect(run.status).toBe(0); + expect(run.stdout).toBe( + `register: 0\ntext=[${text}]\ncode=[${code}]\nsymbol=[${symbol}]\ntrapped\n`, + ); + }); + /* ── K4: init re-run determinism ─────────────────────────────────────── */ const SESSION = `session start counter=0 diff --git a/tests/harness/library-profile.test.ts b/tests/harness/library-profile.test.ts index fffa88cc3b..fc243ccb58 100644 --- a/tests/harness/library-profile.test.ts +++ b/tests/harness/library-profile.test.ts @@ -160,7 +160,56 @@ describe("library profile validation", () => { }); test("parse error", () => expectSc4001("{ not json", "not valid JSON")); - test("wrong format", () => expectSc4001({ ...good, profile_format: 2 }, "profile_format")); + test("wrong format", () => expectSc4001({ ...good, profile_format: 3 }, "profile_format")); + test("the cstring class requires profile_format 2", () => { + const cstringExport = { + export: "greet", + symbol: "kx_greet", + params: ["cstring"], + returns: "cstring", + }; + const v2 = loadLibraryProfile( + writeProfile({ ...good, profile_format: 2, exports: [...good.exports, cstringExport] }), + ); + expect(v2.ok).toBe(true); + if (v2.ok) { + expect(v2.profile.profileFormat).toBe(2); + expect(v2.profile.exports.at(-1)).toMatchObject({ params: ["cstring"], returns: "cstring" }); + } + expectSc4001( + { ...good, exports: [...good.exports, cstringExport] }, + "'exports[2].params[0]' class 'cstring' requires profile_format 2", + ); + expectSc4001( + { ...good, exports: [...good.exports, { ...cstringExport, params: [] }] }, + "'exports[2].returns' class 'cstring' requires profile_format 2", + ); + const callbackAbi = { ...good.abi, callback_register_symbol: "kx_set_callback" }; + const cstringCallback = { name: "log", params: ["cstring"], returns: "void" }; + expect( + loadLibraryProfile( + writeProfile({ + ...good, + profile_format: 2, + abi: callbackAbi, + callbacks: [cstringCallback], + }), + ).ok, + ).toBe(true); + expectSc4001( + { ...good, abi: callbackAbi, callbacks: [cstringCallback] }, + "'callbacks[0].params[0]' class 'cstring' requires profile_format 2", + ); + expectSc4001( + { + ...good, + profile_format: 2, + abi: callbackAbi, + callbacks: [{ name: "log", params: [], returns: "cstring" }], + }, + "a callback return stays scalar", + ); + }); test("missing name", () => { const { name: _drop, ...rest } = good; expectSc4001(rest, "'name'"); diff --git a/tests/harness/lifetime-regions.test.ts b/tests/harness/lifetime-regions.test.ts index 7eb294482e..ac802cb503 100644 --- a/tests/harness/lifetime-regions.test.ts +++ b/tests/harness/lifetime-regions.test.ts @@ -145,9 +145,9 @@ describe.skipIf(process.platform === "win32")("lifetime regions", { timeout: 240 }); test("async work, nesting, holders the walk does not inspect and handles", async () => { - // Objects allocated after an await live in the default region and keep - // the regions they refer into. - expect((await run("5013-lifetime-region-async.ts")).summaries).toEqual([summary(7, 2)]); + // Work that resumes after an await allocates in its region, which is + // freed once that work has finished. + expect((await run("5013-lifetime-region-async.ts")).summaries).toEqual([summary(7, 7)]); const nesting = await run("5014-lifetime-region-nesting.ts"); expect(nesting.summaries).toEqual([summary(4, 2)]); const opaque = await run("5021-lifetime-region-opaque-holders.ts"); @@ -156,6 +156,36 @@ describe.skipIf(process.platform === "win32")("lifetime regions", { timeout: 240 expect((await run("5019-lifetime-region-values.ts")).summaries).toEqual([summary(2, 0)]); }); + test("regions carried across await by concurrent requests are freed", async () => { + const stats = await run("5023-lifetime-region-await.ts"); + expect(stats.summaries).toEqual([summary(15, 15)]); + // Each request's region (eight interleaved ones, then a later one) is + // freed when the request releases it; a region released while its work + // is pending, or while work it started is still running, is freed once + // that work ends, without an attempt that keeps it. + for (const id of [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 15]) + expect( + stats.attempts.filter((a) => a.startsWith(`${id} `)), + `region ${id}`, + ).toEqual([`${id} freed`]); + // The nested region is referenced by the outer one until that is freed; + // the escaping result keeps its region until the cache lets go. + keptThenFreed(stats.attempts, [10, 14]); + }); + + test("regions are carried by stackless and fiber async bodies alike", async () => { + // Stackless coroutine bodies and a fiber body (for await) spawn and + // await each other inside each region; whatever resumes allocates in + // the region, so each one is freed on its single release attempt. + const stats = await run("5024-lifetime-region-await-forms.ts"); + expect(stats.summaries).toEqual([summary(7, 7)]); + for (let id = 1; id <= 7; id++) + expect( + stats.attempts.filter((a) => a.startsWith(`${id} `)), + `region ${id}`, + ).toEqual([`${id} freed`]); + }); + test("weak keys of freed regions do not survive", async () => { const stats = await run("5016-lifetime-region-weak-keys.ts"); expect(stats.summaries).toEqual([summary(5, 4)]); diff --git a/tests/harness/native-frontend.ts b/tests/harness/native-frontend.ts index feee2f5911..475db54aa6 100644 --- a/tests/harness/native-frontend.ts +++ b/tests/harness/native-frontend.ts @@ -65,6 +65,7 @@ export const programs = [ "nullish-long-chain.ts", "class-array-optional-return.ts", "contextual-array-union-write.ts", + "6020-class-extends-function-constructor.js", ]; interface LowerRequest { diff --git a/tests/harness/node-test-normalize.ts b/tests/harness/node-test-normalize.ts index c150bb7071..e02b5169b8 100644 --- a/tests/harness/node-test-normalize.ts +++ b/tests/harness/node-test-normalize.ts @@ -2,10 +2,11 @@ * node-test.test.ts's function, extracted so the Linux lane applies the * IDENTICAL scrub to both of its in-container lanes. Node's spec reporter * embeds real durations in every line (raw byte-compare is impossible for - * ANY node:test program); stack frames and the inspect property block a - * frame opens are runner-internal output the compiled runtime - * deliberately omits (SEMANTICS.md). Everything else must match - * byte-exactly. */ + * ANY node:test program); stack frames, the "... N lines matching cause + * stack trace ..." line inspect folds an error cause's shared frames into, + * and the inspect property block a frame opens are runner-internal output + * the compiled runtime deliberately omits (SEMANTICS.md). Everything else + * must match byte-exactly. */ /** The documented normalization — durations, frames, the property block. */ export function normalizeNodeTestOutput(stdout: string): string { @@ -16,6 +17,7 @@ export function normalizeNodeTestOutput(stdout: string): string { if (/^\s*\}\s*$/.test(line)) inPropBlock = false; continue; } + if (/^\s+\.\.\. \d+ lines? matching cause stack trace \.\.\.$/.test(line)) continue; if (/^\s+at /.test(line)) { if (line.trimEnd().endsWith("{")) inPropBlock = true; continue; diff --git a/tests/harness/node-test.test.ts b/tests/harness/node-test.test.ts index 7cf10ac504..054a77aaa9 100644 --- a/tests/harness/node-test.test.ts +++ b/tests/harness/node-test.test.ts @@ -11,10 +11,12 @@ * NORMALIZATIONS (both lanes): * - "(1.234ms)" duration parentheticals → "(Xms)" * - the "ℹ duration_ms N" summary line → "ℹ duration_ms X" - * - stack frames (" at ...") and the inspect property block a frame - * opens ("... {" through its closing "}") are dropped — Node prints - * its runner-internal frames and an error-property dump the compiled - * runtime deliberately omits (SEMANTICS.md). + * - stack frames (" at ..."), the "... N lines matching cause stack + * trace ..." line that folds an error cause's shared frames, and the + * inspect property block a frame opens ("... {" through its closing + * "}") are dropped — Node prints its runner-internal frames and an + * error-property dump the compiled runtime deliberately omits + * (SEMANTICS.md). * * Fixtures never console.log inside test bodies or hooks: Node's * reporter stream lags console output RACILY (its ordering varies run to @@ -25,7 +27,8 @@ * the RC audit over every case. */ import { spawn } from "node:child_process"; import { createHash } from "node:crypto"; -import { globSync, mkdirSync, readFileSync } from "node:fs"; +import { copyFileSync, globSync, mkdirSync, mkdtempSync, readFileSync } from "node:fs"; +import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, test } from "vitest"; import { compile } from "@scriptc/compiler"; @@ -41,11 +44,12 @@ interface ProgramRun { exitCode: number; } -function runLane(cmd: string, args: string[]): Promise { +function runLane(cmd: string, args: string[], cwd = repoRoot): Promise { return new Promise((resolve, reject) => { - // cwd pinned to the repo root: both reporters render "test at" paths - // relative to the working directory, so the lanes must share one. - const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"], cwd: repoRoot }); + // cwd pinned to the repo root by default: both reporters render "test + // at" paths relative to the working directory, so the lanes must share + // one. + const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"], cwd }); const out: Buffer[] = []; let errText = ""; const timer = setTimeout(() => { @@ -125,3 +129,48 @@ describe(`node:test differential (${cases.length} programs${sanitize ? ", saniti 120_000, ); }); + +/* Snapshot update mode: Node's --test-update-snapshots flag, which the + * compiled binary takes as its leading argument. Each lane runs the + * fixture from its own temporary copy (the snapshot file sits beside the + * entry file and fileSnapshot paths resolve against the working + * directory), writes the snapshot files, and then passes in compare mode + * against them. The written files and the printed argv/execArgv match. */ +describe(`node:test snapshot update mode${sanitize ? " (sanitized)" : ""}`, () => { + test("--test-update-snapshots writes the files Node writes", async () => { + const fixture = join(fixturesRoot, "update/snapshot-update.ts"); + const lanes = await Promise.all( + (["node", "native"] as const).map(async (lane) => { + const dir = mkdtempSync(join(tmpdir(), `scriptc-snapshot-${lane}-`)); + const entry = join(dir, "snapshot-update.ts"); + copyFileSync(fixture, entry); + const binary = lane === "native" ? await build(entry) : null; + const run = (update: boolean) => + binary + ? runLane(binary, [...(update ? ["--test-update-snapshots"] : []), "one", "two"], dir) + : runLane( + "node", + [...(update ? ["--test-update-snapshots"] : []), entry, "one", "two"], + dir, + ); + const update = await run(true); + const compare = await run(false); + return { + update, + compare, + snapshot: readFileSync(`${entry}.snapshot`, "utf8"), + file: readFileSync(join(dir, "out/value.json"), "utf8"), + }; + }), + ); + const [node, native] = lanes as [(typeof lanes)[number], (typeof lanes)[number]]; + expect(native.snapshot).toBe(node.snapshot); + expect(native.file).toBe(node.file); + expect(normalize(native.update.stdout)).toBe(normalize(node.update.stdout)); + expect(normalize(native.compare.stdout)).toBe(normalize(node.compare.stdout)); + for (const lane of lanes) { + expect(lane.update.exitCode).toBe(0); + expect(lane.compare.exitCode).toBe(0); + } + }, 120_000); +}); diff --git a/tests/harness/npm-static.test.ts b/tests/harness/npm-static.test.ts index 89bd2e0b8d..c5021fd062 100644 --- a/tests/harness/npm-static.test.ts +++ b/tests/harness/npm-static.test.ts @@ -912,6 +912,24 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => { expect(nativeRes.exitCode).toBe(nodeRes.exitCode); }, 180_000); + // Real CommonJS packages whose require() placement (a binding required + // below code that only reaches it after the require ran) or JSON + // require preflight refused before. + test("export-first CommonJS packages and JSON requires compile statically", () => { + const { coverage } = analyze(join(pilotRoot, "cjs-interop-cli.ts"), { + npmStatic: ["fs.realpath", "on-finished", "ee-first", "statuses"], + }); + expect(coverage.preflightFailed).toBe(false); + expect( + [...(coverage.npmStatic ?? [])].sort((a, b) => a.package.localeCompare(b.package)), + ).toEqual([ + { package: "ee-first", status: "static" }, + { package: "fs.realpath", status: "static" }, + { package: "on-finished", status: "static" }, + { package: "statuses", status: "static" }, + ]); + }, 120_000); + // A package preflight failure stays visible as a package-specific refusal. test("a preflight-refused package reports its native refusal", () => { const { coverage } = analyze(join(fixturesRoot, "npm/divergent/lazybuiltin.ts"), { diff --git a/tests/harness/oracle-env.test.ts b/tests/harness/oracle-env.test.ts new file mode 100644 index 0000000000..da09dde256 --- /dev/null +++ b/tests/harness/oracle-env.test.ts @@ -0,0 +1,25 @@ +import { expect, test } from "vitest"; +import { oracleEnvKey } from "./oracle-env.js"; + +test("the oracle key changes with any inherited variable", () => { + const base = { PATH: "/bin", SCRIPTC_TEST_ENV: "from-harness" }; + expect(oracleEnvKey({ ...base, NODE_ENV: "production" })).not.toBe( + oracleEnvKey({ ...base, NODE_ENV: "test" }), + ); + expect(oracleEnvKey({ ...base, NODE_ENV: "test" })).not.toBe(oracleEnvKey(base)); + expect(oracleEnvKey({ ...base, FOO: "a=b" })).not.toBe( + oracleEnvKey({ ...base, FOO: "a", B: "" }), + ); +}); + +test("the oracle key ignores the runner's worker numbers and variable order", () => { + expect(oracleEnvKey({ A: "1", B: "2", VITEST_WORKER_ID: "1", VITEST_POOL_ID: "1" })).toBe( + oracleEnvKey({ + VITEST_POOL_ID: "7", + B: "2", + TINYPOOL_WORKER_ID: "3", + A: "1", + VITEST_WORKER_ID: "9", + }), + ); +}); diff --git a/tests/harness/oracle-env.ts b/tests/harness/oracle-env.ts new file mode 100644 index 0000000000..5f66f26bb9 --- /dev/null +++ b/tests/harness/oracle-env.ts @@ -0,0 +1,15 @@ +/* The environment part of the differential oracle cache key. The Node + * oracle runs with the harness's inherited environment, so a program whose + * output reads a variable (NODE_ENV, SCRIPTC_TEST_ENV, ...) must not reuse a + * verdict recorded under a different value. The variables that number the + * test runner's own workers differ between workers of one run and are left + * out, so every worker shares the cache. */ +const WORKER_VARIABLES = new Set(["VITEST_POOL_ID", "VITEST_WORKER_ID", "TINYPOOL_WORKER_ID"]); + +export function oracleEnvKey(env: NodeJS.ProcessEnv): string { + return Object.keys(env) + .filter((name) => !WORKER_VARIABLES.has(name) && env[name] !== undefined) + .sort() + .map((name) => `${name}=${env[name]}\0`) + .join(""); +} diff --git a/tests/harness/project-config.test.ts b/tests/harness/project-config.test.ts index bbd6045e59..f5e80beb8f 100644 --- a/tests/harness/project-config.test.ts +++ b/tests/harness/project-config.test.ts @@ -462,6 +462,40 @@ test("node-types: unsupported child stdin forms remain named fences", async () = ]); }); +test("node-types: callback exec uses the typed ExecException overloads", async () => { + const dir = outDirFor("node-child-exec"); + const entry = join(nodeTypesDir, "child-exec.ts"); + const result = await compile(entry, { + outPath: join(dir, "child-exec"), + outDir: dir, + sanitize, + }); + expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true); + if (!result.ok) return; + const { stdout } = await execFileAsync(result.binaryPath); + expect(stdout).toBe('typed-exec err\n"Command failed: exit 2\\n"\n'); +}); + +test("node-types: exec's numeric ExecException.code remains a named fence", async () => { + const dir = outDirFor("node-child-exec-code-fences"); + const entry = join(nodeTypesDir, "child-exec-code-fences.ts"); + const result = await compile(entry, { + outPath: join(dir, "child-exec-code-fences"), + outDir: dir, + sanitize, + }); + expect(result.ok).toBe(false); + if (result.ok) return; + expect(result.diagnostics.map((diagnostic) => diagnostic.code)).toEqual([ + "SC2020", + "SC2020", + "SC2020", + ]); + for (const diagnostic of result.diagnostics) { + expect(diagnostic.message).toContain("ExecException.code"); + } +}); + test("node-types: path and os lower statically under @types/node's shapes", async () => { const outDir = outDirFor("node-path-os"); const result = await compile(join(nodeTypesDir, "path-os.ts"), { diff --git a/tests/harness/source-overlay-semantics.test.ts b/tests/harness/source-overlay-semantics.test.ts new file mode 100644 index 0000000000..dfa087d7b1 --- /dev/null +++ b/tests/harness/source-overlay-semantics.test.ts @@ -0,0 +1,339 @@ +/* --sources: per-package semantics for source overlays. Each case builds a + * small pnpm-shaped project whose package publishes JavaScript plus + * declarations and whose overlay holds the upstream TypeScript, writes the + * schema-1 manifest the way scriptc-pm would, and checks one mechanism: + * - `ambientDeclarations`: the package's own ambient .d.ts files type its + * source; + * - an overlay's untyped dependency compiles from its published JS; + * - `semanticOptions.useDefineForClassFields: false`: parameter + * properties are assigned before the field initializers run. + * Without the manifest field (or before the change) each overlay falls back + * to the published JS with a reason; with it, the package compiles from + * source and the binary matches Node, which runs the published JS. */ +import { execFile } from "node:child_process"; +import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { promisify } from "node:util"; +import { afterAll, beforeAll, describe, expect, test } from "vitest"; + +const execFileAsync = promisify(execFile); +const repoRoot = join(import.meta.dirname, "../.."); +const cli = join(repoRoot, "packages/cli/dist/main.js"); + +interface Outcome { + code: number; + stdout: string; + stderr: string; +} + +async function run(file: string, args: string[], cwd: string, env?: NodeJS.ProcessEnv) { + try { + const { stdout, stderr } = await execFileAsync(file, args, { + encoding: "utf8", + maxBuffer: 64 * 1024 * 1024, + cwd, + ...(env !== undefined ? { env } : {}), + }); + return { code: 0, stdout, stderr } satisfies Outcome; + } catch (e) { + const err = e as { code?: number; stdout?: string; stderr?: string }; + return { code: err.code ?? 1, stdout: err.stdout ?? "", stderr: err.stderr ?? "" }; + } +} + +let root: string; + +beforeAll(() => { + root = mkdtempSync(join(realpathSync(tmpdir()), "scriptc-overlay-semantics-")); +}); + +afterAll(() => { + if (root !== undefined) rmSync(root, { recursive: true, force: true }); +}); + +function write(path: string, text: string): void { + mkdirSync(dirname(path), { recursive: true }); + writeFileSync(path, text); +} + +interface PackageSpec { + name: string; + /** Published files (package.json is added). */ + published: Record; + /** Upstream source files of the overlay. */ + source: Record; + /** Names of this package's dependencies (installed packages). */ + deps?: string[]; + /** Extra manifest fields for the instance. */ + manifest?: Record; +} + +/** A pnpm-shaped install: node_modules/.pnpm/@1.0.0/node_modules/, + * a top-level link per direct dependency, and a source overlay (with its + * node_modules link to the instance's pnpm dependency dir) for every + * package that has a source. */ +function project(label: string, main: string, direct: string[], packages: PackageSpec[]): string { + const dir = join(root, label); + write(join(dir, "package.json"), '{ "type": "module" }\n'); + write( + join(dir, "tsconfig.json"), + '{ "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "Bundler", "strict": true, "skipLibCheck": true, "noEmit": true } }\n', + ); + write(join(dir, "main.ts"), main); + const pnpm = (name: string): string => + join(dir, "node_modules/.pnpm", `${name}@1.0.0/node_modules`); + const instances: Record[] = []; + for (const pkg of packages) { + const installed = join(pnpm(pkg.name), pkg.name); + write( + join(installed, "package.json"), + `${JSON.stringify({ name: pkg.name, version: "1.0.0", type: "module", exports: { ".": { types: "./index.d.ts", default: "./index.js" } } })}\n`, + ); + for (const [file, text] of Object.entries(pkg.published)) write(join(installed, file), text); + for (const dep of pkg.deps ?? []) symlinkSync(join(pnpm(dep), dep), join(pnpm(pkg.name), dep)); + if (Object.keys(pkg.source).length === 0) continue; + const overlay = join(dir, ".scriptc-pm/overlay", `${pkg.name}@1.0.0`, "pkg"); + const exports = { ".": { types: "./src/index.ts", default: "./src/index.ts" } }; + write( + join(overlay, "package.json"), + `${JSON.stringify({ name: pkg.name, version: "1.0.0", type: "module", exports })}\n`, + ); + for (const [file, text] of Object.entries(pkg.source)) write(join(overlay, file), text); + symlinkSync(pnpm(pkg.name), join(overlay, "node_modules")); + instances.push({ + installedDir: installed, + name: pkg.name, + version: "1.0.0", + overlayDir: overlay, + exports, + publishedFormat: "esm", + semanticOptions: { useDefineForClassFields: true }, + ...pkg.manifest, + }); + } + for (const name of direct) { + mkdirSync(join(dir, "node_modules"), { recursive: true }); + symlinkSync(join(pnpm(name), name), join(dir, "node_modules", name)); + } + write( + join(dir, ".scriptc-pm/manifest.json"), + `${JSON.stringify({ schema: 1, generator: "source-overlay-semantics.test", lockDigest: `sha256-${label}`, instances, fallbacks: [] }, null, 2)}\n`, + ); + return dir; +} + +/** Builds main.ts with --sources and runs it beside Node (which loads the + * published JS). */ +async function buildAndRun( + dir: string, +): Promise<{ build: Outcome; native: Outcome; node: Outcome }> { + const build = await run( + process.execPath, + [cli, "build", "main.ts", "--sources", "-o", "main-bin"], + dir, + { ...process.env, SCRIPTC_CACHE_DIR: join(root, "cache") }, + ); + const node = await run(process.execPath, ["main.ts"], dir); + const native = + build.code === 0 + ? await run(join(dir, "main-bin"), [], dir) + : { code: -1, stdout: "", stderr: "" }; + return { build, native, node }; +} + +const dropped = (name: string): RegExp => + new RegExp( + `sources: ${name}@1\\.0\\.0 compiles from published JS \\(source overlay dropped: ([^\\n]*)`, + ); + +describe("--sources overlay semantics", () => { + test("ambientDeclarations type the overlay's source", async () => { + const pkg = (manifest?: Record): PackageSpec => ({ + name: "ambient-pkg", + published: { + "index.js": + "export function greet(name) {\n return { text: `hello ${name}`, loud: name.length > 3 };\n}\n", + "index.d.ts": + "export interface Greeting { text: string; loud: boolean }\nexport declare function greet(name: string): Greeting;\n", + }, + source: { + // Global types its build includes from typings/, never imported. + // (The checker needs them; declaration-file shapes themselves stay + // behind scriptc's usual .d.ts provenance fence.) + "typings/label.d.ts": "type AmbientLabel = string;\ntype AmbientLoud = boolean;\n", + "src/index.ts": + "export function greet(name: AmbientLabel): { text: AmbientLabel; loud: AmbientLoud } {\n return { text: `hello ${name}`, loud: name.length > 3 };\n}\n", + }, + ...(manifest !== undefined ? { manifest } : {}), + }); + const main = + 'import { greet } from "ambient-pkg";\nconst g = greet("world");\nconsole.log(g.text, g.loud);\n'; + + const without = await buildAndRun(project("ambient-without", main, ["ambient-pkg"], [pkg()])); + expect(without.build.stderr).toMatch(dropped("ambient-pkg")); + expect(without.build.stderr.match(dropped("ambient-pkg"))![1]).toContain( + "Cannot find name 'AmbientLabel'", + ); + + const withField = await buildAndRun( + project( + "ambient-with", + main, + ["ambient-pkg"], + [pkg({ ambientDeclarations: ["typings/label.d.ts"] })], + ), + ); + expect(withField.build.stderr).not.toContain("source overlay dropped"); + expect(withField.build.code).toBe(0); + expect(withField.native).toEqual(withField.node); + expect(withField.node.stdout).toBe("hello world true\n"); + }); + + test("an overlay's untyped dependency compiles from its published JS", async () => { + const packages: PackageSpec[] = [ + { + // JavaScript only, like its dependency untyped-dep, whose @types + // package would be a devDependency of uses-untyped. + name: "untyped-leaf", + published: { "index.js": 'export const mark = "!";\n' }, + source: {}, + }, + { + name: "untyped-dep", + deps: ["untyped-leaf"], + published: { + "index.js": + 'import { mark } from "untyped-leaf";\nexport function shout(text) {\n return text.toUpperCase() + mark;\n}\n', + }, + source: {}, + }, + { + name: "uses-untyped", + deps: ["untyped-dep"], + published: { + "index.js": + 'import { shout } from "untyped-dep";\nexport function announce(name) {\n return shout(`hi ${name}`);\n}\n', + "index.d.ts": "export declare function announce(name: string): string;\n", + }, + source: { + "src/index.ts": + 'import { shout } from "untyped-dep";\n\nexport function announce(name: string): string {\n return shout(`hi ${name}`);\n}\n', + }, + }, + ]; + // The JavaScript-only packages have no "types": plain exports maps. + const dir = project( + "untyped", + 'import { announce } from "uses-untyped";\nconsole.log(announce("there"));\n', + ["uses-untyped"], + packages, + ); + for (const name of ["untyped-dep", "untyped-leaf"]) { + write( + join(dir, `node_modules/.pnpm/${name}@1.0.0/node_modules/${name}/package.json`), + `{ "name": "${name}", "version": "1.0.0", "type": "module", "exports": "./index.js" }\n`, + ); + } + const result = await buildAndRun(dir); + expect(result.build.stderr).not.toContain("source overlay dropped"); + expect(result.build.code).toBe(0); + expect(result.native).toEqual(result.node); + expect(result.node.stdout).toBe("HI THERE!\n"); + }); + + test("useDefineForClassFields: false assigns parameter properties before field initializers", async () => { + // The published JS is tsc's ES2017 emit: assignments in the + // constructor, parameter properties first. + const published = { + "index.js": `export class Settings { + constructor(_options = {}) { + this._options = _options; + this.depth = this._options.depth ?? 1; + this.label = this._options.label ?? "none"; + } +} +export class Entry { + constructor(name, size) { + this.name = name; + this.size = size; + this.kind = size > 0 ? "file" : "empty"; + } +} +export class Late { +} +`, + "index.d.ts": `export interface Options { depth?: number; label?: string } +export declare class Settings { private readonly _options; readonly depth: number; readonly label: string; constructor(_options?: Options); } +export declare class Entry { name: string; size: number; kind: string; constructor(name: string, size: number); } +export declare class Late { note?: string; } +`, + }; + const source = `export interface Options { + depth?: number; + label?: string; +} + +export class Settings { + public readonly depth: number = this._options.depth ?? 1; + public readonly label: string = this._options.label ?? "none"; + + constructor(private readonly _options: Options = {}) {} +} + +export class Entry { + public size: number; + public kind: string; + + constructor(public name: string, size: number) { + this.size = size; + this.kind = size > 0 ? "file" : "empty"; + } +} +`; + const main = `import { Entry, Settings } from "fields-pkg"; +const settings = new Settings({ depth: 3 }); +console.log(settings.depth, settings.label); +console.log(settings); +console.log(new Settings()); +const entry = new Entry("a.txt", 4); +console.log(entry, Object.keys(entry).join(",")); +`; + const pkg = (useDefineForClassFields: boolean, extra = ""): PackageSpec => ({ + name: "fields-pkg", + published, + source: { "src/index.ts": source + extra }, + manifest: { semanticOptions: { useDefineForClassFields } }, + }); + + // Define semantics (the program's): the initializer reads the + // parameter property before it is assigned, a type error. + const define = await buildAndRun(project("fields-define", main, ["fields-pkg"], [pkg(true)])); + expect(define.build.stderr.match(dropped("fields-pkg"))?.[1]).toMatch( + /SC0001 .*used before its initialization/, + ); + expect(define.native).toEqual(define.node); + + const assign = await buildAndRun(project("fields-assign", main, ["fields-pkg"], [pkg(false)])); + expect(assign.build.stderr).not.toContain("source overlay dropped"); + expect(assign.build.code).toBe(0); + expect(assign.native).toEqual(assign.node); + expect(assign.node.stdout).toContain( + "Settings { _options: { depth: 3 }, depth: 3, label: 'none' }", + ); + + // An initializer-less field the constructor may never assign is no own + // property under assign semantics; that shape keeps a named refusal. + const late = await buildAndRun( + project( + "fields-late", + `${main}import { Late } from "fields-pkg";\nconsole.log(new Late());\n`, + ["fields-pkg"], + [pkg(false, "\nexport class Late {\n public note?: string;\n}\n")], + ), + ); + expect(late.build.stderr.match(dropped("fields-pkg"))?.[1]).toMatch( + /SC1090 .*initializer-less fields the constructor does not assign/, + ); + }); +}); diff --git a/tests/harness/wasm-library.test.ts b/tests/harness/wasm-library.test.ts index 8659c03d96..d0bccdc3de 100644 --- a/tests/harness/wasm-library.test.ts +++ b/tests/harness/wasm-library.test.ts @@ -272,6 +272,85 @@ describe.skipIf(!hasZig).each(["node", "native"] as const)( }, ); + test("cstring parameters, results, and callback arguments use NUL-terminated pointers", async () => { + const entry = join(directory, "cstring-lib.ts"); + await writeFile( + entry, + [ + "declare function log(text: string): void;", + "export function greet(name: string): string {", + " log(name.toUpperCase());", + " return `hello, ${name}!`;", + "}", + "export function withNul(): string {", + ' return "a\\u0000b";', + "}", + "", + ].join("\n"), + ); + const profilePath = join(directory, "cstring.json"); + await writeFile( + profilePath, + JSON.stringify({ + profile_format: 2, + name: "wasm-cstring", + entry, + emission: "llvm", + abi: { + prefix: "cs_", + init_symbol: "cs_init", + sink_register_symbol: "cs_sink", + callback_register_symbol: "cs_callback", + collect_symbol: null, + result_reset_symbol: null, + }, + callbacks: [{ name: "log", params: ["cstring"], returns: "void" }], + exports: [ + { export: "greet", symbol: "cs_greet", params: ["cstring"], returns: "cstring" }, + { export: "withNul", symbol: "cs_with_nul", params: [], returns: "cstring" }, + ], + }), + ); + const result = await compileLibrary({ profilePath, outDir: directory }); + if (!result.ok) throw new Error(JSON.stringify(result.diagnostics)); + const wasi = new WASI({ version: "preview1" }); + let api: Api; + const logs: string[] = []; + const panics: string[] = []; + const cString = (ptr: number): string => { + const memory = new Uint8Array(api.memory.buffer); + let end = ptr; + while (memory[end] !== 0) end++; + return new TextDecoder().decode(memory.subarray(ptr, end)); + }; + const instance = new WebAssembly.Instance( + new WebAssembly.Module(await readFile(result.archivePath)), + { + ...wasi.getImportObject(), + scriptc: { + log: (ptr: number) => logs.push(cString(ptr)), + panic: (ptr: number, len: number) => { + const message = new TextDecoder().decode(new Uint8Array(api.memory.buffer, ptr, len)); + panics.push(message); + throw new Error(message); + }, + }, + }, + ); + api = instance.exports as Api; + wasi.initialize(instance); + api.cs_init(); + const input = new TextEncoder().encode("w\u00f6rld\0"); + const ptr = api.scriptc_alloc(input.length); + new Uint8Array(api.memory.buffer, ptr, input.length).set(input); + expect(cString(api.cs_greet(ptr))).toBe("hello, w\u00f6rld!"); + expect(logs).toEqual(["W\u00d6RLD"]); + api.scriptc_free(ptr); + expect(() => api.cs_with_nul()).toThrow(); + expect(panics).toHaveLength(1); + expect(panics[0]).toContain("a cstring value contains U+0000"); + }); + test("refuses collisions with built-in Wasm exports and imports", async () => { for (const change of [ { diff --git a/tests/harness/windows-differential.test.ts b/tests/harness/windows-differential.test.ts index 7e6ed205d2..dc199d38f2 100644 --- a/tests/harness/windows-differential.test.ts +++ b/tests/harness/windows-differential.test.ts @@ -149,6 +149,8 @@ const WINDOWS_SKIPS: Record = { "posix-shaped: the uncaught sh ENOENT throw crashes both sides, rendered differently (1645 covers env here)", "1464-env-writes.ts": 'posix-shaped: the observing child is ENOENT on Windows, exposing the documented spawn-failure ""-vs-null stance', + "5627-exec-callback.ts": + "posix-shaped: exec runs /bin/sh -c (execSync's static shell lowering); Windows Node runs cmd.exe", // Two programs the events unit's win32 arm surfaced (they compiled for // the first time once the events gate lifted): both drive their signal // listeners through a /bin/sh child, ENOENT on Windows Node too. diff --git a/tests/library-mode/cstrings/lib.ts b/tests/library-mode/cstrings/lib.ts new file mode 100644 index 0000000000..1016beb902 --- /dev/null +++ b/tests/library-mode/cstrings/lib.ts @@ -0,0 +1,26 @@ +// cstring conformance fixture: NUL-terminated text in both directions and +// through a callback channel. `withNul` and `echoNul` produce text holding +// U+0000, which a C string cannot carry. +declare function log(text: string, n: number): void; + +export function greet(name: string): string { + return `hello, ${name}!`; +} + +export function length(text: string): number { + return text.length; +} + +export function echo(text: string): number { + log(text.toUpperCase(), text.length); + return 1; +} + +export function withNul(): string { + return "a\u0000b"; +} + +export function echoNul(): number { + log("x\u0000y", 3); + return 1; +} diff --git a/tests/library-mode/cstrings/probe.c b/tests/library-mode/cstrings/probe.c new file mode 100644 index 0000000000..ba802f84a7 --- /dev/null +++ b/tests/library-mode/cstrings/probe.c @@ -0,0 +1,76 @@ +/* cstring probe, mode-selected by argv[1]: + * ok — cstring parameters copy in (UTF-8, U+FFFD for malformed + * bytes), cstring results are NUL-terminated arena copies, + * and a cstring callback argument arrives NUL-terminated + * nullarg — a NULL cstring parameter is the SC4012 host-contract trap + * nulresult — a result holding U+0000 traps instead of truncating + * nularg — a callback argument holding U+0000 traps the same way + */ +#include +#include +#include +#include + +extern void kc_init(void); +extern void kc_set_panic_sink(void (*fn)(void *, const uint8_t *, size_t, uint64_t), void *ctx); +extern void kc_collect(void); +extern int32_t kc_set_callback(const char *name, void (*fn)(void), void *ctx); +extern const char *kc_greet(const char *name); +extern double kc_length(const char *text); +extern double kc_echo(const char *text); +extern const char *kc_with_nul(void); +extern double kc_echo_nul(void); + +static jmp_buf trap_jmp; + +static void sink(void *ctx, const uint8_t *msg, size_t len, uint64_t addr) { + (void)ctx; (void)addr; + /* structured: 0x01 text 0x1F code 0x1F symbol */ + const uint8_t *p = msg + 1, *end = msg + len; + const char *names[3] = {"text", "code", "symbol"}; + for (int field = 0; field < 3 && p <= end; field++) { + const uint8_t *sep = memchr(p, 0x1f, (size_t)(end - p)); + const uint8_t *stop = sep != NULL ? sep : end; + printf("%s=[%.*s]\n", names[field], (int)(stop - p), (const char *)p); + if (sep == NULL) break; + p = sep + 1; + } + longjmp(trap_jmp, 1); +} + +static void on_log(void *ctx, const char *text, double n) { + (void)ctx; + printf("log: %s (strlen %zu, n %.0f)\n", text, strlen(text), n); +} + +int main(int argc, char **argv) { + const char *mode = argc > 1 ? argv[1] : "ok"; + kc_set_panic_sink(sink, NULL); + kc_init(); + printf("register: %d\n", (int)kc_set_callback("log", (void (*)(void))on_log, NULL)); + if (setjmp(trap_jmp) != 0) { + printf("trapped\n"); + return 0; + } + if (strcmp(mode, "ok") == 0) { + const char *g = kc_greet("w\xC3\xB6rld"); + printf("greet: %s (strlen %zu)\n", g, strlen(g)); + printf("length ascii: %.0f\n", kc_length("abc")); + printf("length utf8: %.0f\n", kc_length("caf\xC3\xA9")); + printf("length malformed: %.0f\n", kc_length("a\xFF" "b")); + printf("length empty: %.0f\n", kc_length("")); + printf("echo: %.0f\n", kc_echo("quiet")); + kc_collect(); + printf("done\n"); + } else if (strcmp(mode, "nullarg") == 0) { + kc_length(NULL); + printf("UNREACHABLE\n"); + } else if (strcmp(mode, "nulresult") == 0) { + kc_with_nul(); + printf("UNREACHABLE\n"); + } else if (strcmp(mode, "nularg") == 0) { + kc_echo_nul(); + printf("UNREACHABLE\n"); + } + return 0; +} diff --git a/tests/library-mode/cstrings/profile.json b/tests/library-mode/cstrings/profile.json new file mode 100644 index 0000000000..c5a36e4246 --- /dev/null +++ b/tests/library-mode/cstrings/profile.json @@ -0,0 +1,22 @@ +{ + "profile_format": 2, + "name": "conformance-cstrings", + "entry": "lib.ts", + "emission": "llvm", + "abi": { + "prefix": "kc_", + "init_symbol": "kc_init", + "sink_register_symbol": "kc_set_panic_sink", + "collect_symbol": "kc_collect", + "result_reset_symbol": null, + "callback_register_symbol": "kc_set_callback" + }, + "callbacks": [{ "name": "log", "params": ["cstring", "f64"], "returns": "void" }], + "exports": [ + { "export": "greet", "symbol": "kc_greet", "params": ["cstring"], "returns": "cstring" }, + { "export": "length", "symbol": "kc_length", "params": ["cstring"], "returns": "f64" }, + { "export": "echo", "symbol": "kc_echo", "params": ["cstring"], "returns": "f64" }, + { "export": "withNul", "symbol": "kc_with_nul", "params": [], "returns": "cstring" }, + { "export": "echoNul", "symbol": "kc_echo_nul", "params": [], "returns": "f64" } + ] +}